From f0c7fec2a32c794949a98141498143132fb81b4c Mon Sep 17 00:00:00 2001 From: Abhijith Ravindra <137736216+abhijith-darshan@users.noreply.github.com> Date: Thu, 29 Aug 2024 15:23:15 +0200 Subject: [PATCH] feat(dev-env): Setup local development environment using greenhousectl (#452) * (chore): init local env cli * (chore): adds kind cluster creation KinD cluster creation functionality. Additionally, can get a kind cluster, list kind clusters * (chore): adds cluster command cluster command to create kind cluster and optionally create a namespace after cluster creation * (chore): common variables * (chore): adds environment variables to disable webhooks and controllers * (chore): adds cluster list and delete commands * (chore): adds manifests command manifests cmd explodes the specified chart by doing a helm template and applies it to the KinD cluster using kubectl apply * (chore): adds webhook command webhook command will deploy the operator to run in webhook only mode so that local development of controllers can be done without running webhooks. Validation and mutation of CR(s) will happen in-cluster * (chore): adds setup command setup command uses a config file to spin up a full development environment * (chore): adds CLI markdown docs auto-generates docs for cobra commands * (chore): move commands to greenhousectl in order to reduce complexity and encourage contributions by end users, dev CLI commands are moved from standalone to greenhousectl * (chore): adds error handling and logging * (fix): fixes typo * Automatic application of license header * (chore): adds review dog lint * (fix): address lint issues * (chore): use review dog linting * (chore): update reviewdog lint version * (chore): run lint as a separate job * (chore): adds make setup dev make command to build greenhousectl locally and setup the dev env * (chore): adds dev setup configuration * (chore): adds dev setup docs * (chore): adds dev setup make commands * (chore): reverts changes to unit-tests.yml * (chore): tidy up! * (chore): move to greenhousectl dev subcommands * (chore): tidy up * Automatic application of license header * (chore): tidy up * tidy up * (chore): use method chains * (chore): regenerate docs * Automatic application of license header * (fix): fix linting errors * (chore): regenerate docs * (revert): revert greenhousectl worklflow * (chore): go fmt and tidy up! * Automatic generation of CRD API Docs * (chore): remove unused method * Automatic generation of CRD API Docs * (chore): re-run manifest generation * Automatic generation of CRD API Docs * (chore): refactor helm install * Automatic generation of CRD API Docs * Update dev-env/localenv/README.md Co-authored-by: IvoGoman * Automatic generation of CRD API Docs * Apply suggestions from code review Co-authored-by: IvoGoman * (chore): provides back-off options * (chore): removes unnecessary var assignment * (chore): reuse existing build command * (chore): provide helm client options as single slice * (chore): generate dev setup markdown with templates * (chore): allow one of webhook or controller modes or regular * Automatic generation of CRD API Docs * (chore): adds docker and kubectl check to persistent pre-run dev cmds * (chore): tidy up * Automatic generation of CRD API Docs * (chore): go fmt * (chore): go fmt * Automatic generation of CRD API Docs * (chore): upd variables * Automatic generation of CRD API Docs --------- Co-authored-by: License Bot Co-authored-by: CRD API Docs Bot Co-authored-by: IvoGoman --- Makefile | 16 +- charts/manager/templates/manager-role.yaml | 19 +- cmd/greenhouse/main.go | 80 +- dev-env/localenv/README.md | 219 ++ dev-env/localenv/docs.go | 111 + dev-env/localenv/sample.config.json | 42 + dev-env/localenv/sample.values.yaml | 7 + dev-env/localenv/templates/_generate-docs.md | 5 + dev-env/localenv/templates/_intro.md | 45 + docs/reference/api/openapi.yaml | 2 +- go.mod | 5 +- go.sum | 11 +- .../v1alpha1/zz_generated.deepcopy.go | 16 + pkg/cmd/dev.go | 44 + pkg/internal/local/commands/cluster.go | 68 + pkg/internal/local/commands/common.go | 78 + pkg/internal/local/commands/manifest.go | 77 + pkg/internal/local/commands/setup.go | 105 + pkg/internal/local/commands/webhook.go | 87 + pkg/internal/local/helm/helm.go | 207 ++ pkg/internal/local/klient/docker.go | 20 + pkg/internal/local/klient/kind.go | 128 ++ pkg/internal/local/klient/klient.go | 19 + pkg/internal/local/setup/cluster.go | 66 + pkg/internal/local/setup/manifest.go | 218 ++ pkg/internal/local/setup/setup.go | 73 + pkg/internal/local/setup/webhook.go | 372 ++++ pkg/internal/local/utils/exec.go | 106 + pkg/internal/local/utils/expect.go | 59 + pkg/internal/local/utils/utils.go | 210 ++ ui/types/schema.d.ts | 1810 ++++++++++------- 31 files changed, 3566 insertions(+), 759 deletions(-) create mode 100644 dev-env/localenv/README.md create mode 100644 dev-env/localenv/docs.go create mode 100644 dev-env/localenv/sample.config.json create mode 100644 dev-env/localenv/sample.values.yaml create mode 100644 dev-env/localenv/templates/_generate-docs.md create mode 100644 dev-env/localenv/templates/_intro.md create mode 100644 pkg/cmd/dev.go create mode 100644 pkg/internal/local/commands/cluster.go create mode 100644 pkg/internal/local/commands/common.go create mode 100644 pkg/internal/local/commands/manifest.go create mode 100644 pkg/internal/local/commands/setup.go create mode 100644 pkg/internal/local/commands/webhook.go create mode 100644 pkg/internal/local/helm/helm.go create mode 100644 pkg/internal/local/klient/docker.go create mode 100644 pkg/internal/local/klient/kind.go create mode 100644 pkg/internal/local/klient/klient.go create mode 100644 pkg/internal/local/setup/cluster.go create mode 100644 pkg/internal/local/setup/manifest.go create mode 100644 pkg/internal/local/setup/setup.go create mode 100644 pkg/internal/local/setup/webhook.go create mode 100644 pkg/internal/local/utils/exec.go create mode 100644 pkg/internal/local/utils/expect.go create mode 100644 pkg/internal/local/utils/utils.go diff --git a/Makefile b/Makefile index 4247a0fe4..4642dd99e 100644 --- a/Makefile +++ b/Makefile @@ -29,6 +29,8 @@ LOCALBIN ?= $(shell pwd)/bin $(LOCALBIN): mkdir -p $(LOCALBIN) +CLI ?= $(LOCALBIN)/greenhousectl + .PHONY: all all: build @@ -122,8 +124,13 @@ lint: golint .PHONY: check check: fmt lint test -##@ Build +##@ Build CLI Locally +.PHONY: cli +cli: $(CLI) +$(CLI): $(LOCALBIN) + test -s $(LOCALBIN)/greenhousectl || echo "Building Greenhouse CLI..." && make build-greenhousectl +##@ Build .PHONY: build build: generate build-greenhouse build-idproxy build-team-membership build-cors-proxy build-greenhousectl build-service-proxy @@ -211,3 +218,10 @@ else cd website && hugo server endif +.PHONY: setup-dev +setup-dev: cli + $(CLI) dev setup -f dev-env/localenv/sample.config.json + +.PHONY: dev-docs +dev-docs: + go run -tags="dev" -mod=mod dev-env/localenv/docs.go \ No newline at end of file diff --git a/charts/manager/templates/manager-role.yaml b/charts/manager/templates/manager-role.yaml index abb86d53e..b5c22fb1a 100644 --- a/charts/manager/templates/manager-role.yaml +++ b/charts/manager/templates/manager-role.yaml @@ -129,23 +129,6 @@ rules: - get - patch - update -- apiGroups: - - greenhouse.sap - resources: - - plugin - verbs: - - get - - list - - watch -- apiGroups: - - greenhouse.sap - resources: - - plugin/status - verbs: - - get - - list - - patch - - watch - apiGroups: - greenhouse.sap resources: @@ -219,8 +202,10 @@ rules: - plugins/status verbs: - get + - list - patch - update + - watch - apiGroups: - greenhouse.sap resources: diff --git a/cmd/greenhouse/main.go b/cmd/greenhouse/main.go index 769984b90..0971ac086 100644 --- a/cmd/greenhouse/main.go +++ b/cmd/greenhouse/main.go @@ -6,7 +6,10 @@ package main import ( "errors" goflag "flag" + "fmt" "os" + "strconv" + "strings" "time" flag "github.com/spf13/pflag" @@ -29,9 +32,22 @@ import ( "github.com/cloudoperators/greenhouse/pkg/version" ) +type managerMode int + +const ( + // regularMode starts Manager with registered Controllers and all Webhooks + regularMode managerMode = iota + // webhookOnlyMode starts the Manager with all Webhooks and no Controllers + webhookOnlyMode + // controllerOnlyMode starts the Manager with registered Controllers and no Webhooks + controllerOnlyMode +) + const ( defaultRemoteClusterBearerTokenValidity = 24 * time.Hour defaultRenewRemoteClusterBearerTokenAfter = 20 * time.Hour + disableControllersEnv = "WEBHOOK_ONLY" // used to deploy the operator in webhook only mode no controllers will run in this mode. + disableWebhookEnv = "CONTROLLERS_ONLY" // used to disable webhooks when running locally or in debug mode. ) var ( @@ -126,21 +142,30 @@ func main() { }) handleError(err, "unable to start manager") + mode, err := calculateManagerMode() + if err != nil { + handleError(err, "unable to calculate manager mode") + } + // Register controllers. - for controllerName, hookFunc := range knownControllers { - if !isControllerEnabled(controllerName) { - setupLog.Info("skipping controller", "name", controllerName) + if mode != webhookOnlyMode { + for controllerName, hookFunc := range knownControllers { + if !isControllerEnabled(controllerName) { + setupLog.Info("skipping controller", "name", controllerName) + continue + } + setupLog.Info("registering controller", "name", controllerName) + handleError(hookFunc(controllerName, mgr), "unable to create controller", "name", controllerName) continue } - setupLog.Info("registering controller", "name", controllerName) - handleError(hookFunc(controllerName, mgr), "unable to create controller", "name", controllerName) - continue } // Register webhooks. - for webhookName, hookFunc := range knownWebhooks { - setupLog.Info("registering webhook", "name", webhookName) - handleError(hookFunc(mgr), "unable to create webhook", "name", webhookName) + if mode != controllerOnlyMode { + for webhookName, hookFunc := range knownWebhooks { + setupLog.Info("registering webhook", "name", webhookName) + handleError(hookFunc(mgr), "unable to create webhook", "name", webhookName) + } } //+kubebuilder:scaffold:builder @@ -157,3 +182,40 @@ func handleError(err error, msg string, keysAndValues ...interface{}) { os.Exit(1) } } + +// calculateManagerMode - calculates in which mode the manager should run. +func calculateManagerMode() (managerMode, error) { + webhookOnlyEnv := os.Getenv(disableControllersEnv) + controllersOnlyEnv := os.Getenv(disableWebhookEnv) + + var webhookOnly, controllersOnly bool + var err error + + if strings.TrimSpace(webhookOnlyEnv) != "" { + webhookOnly, err = strconv.ParseBool(webhookOnlyEnv) + if err != nil { + return -1, fmt.Errorf("unable to parse %s: %w", disableControllersEnv, err) + } + } + + if strings.TrimSpace(controllersOnlyEnv) != "" { + controllersOnly, err = strconv.ParseBool(controllersOnlyEnv) + if err != nil { + return -1, fmt.Errorf("unable to parse %s: %w", disableWebhookEnv, err) + } + } + + if webhookOnly && controllersOnly { + return -1, errors.New("you can have only one of WEBHOOK_ONLY or CONTROLLERS_ONLY env be set to true") + } + + if webhookOnly { + return webhookOnlyMode, nil + } + + if controllersOnly { + return controllerOnlyMode, nil + } + + return regularMode, nil +} diff --git a/dev-env/localenv/README.md b/dev-env/localenv/README.md new file mode 100644 index 000000000..4276e8cf1 --- /dev/null +++ b/dev-env/localenv/README.md @@ -0,0 +1,219 @@ + +# Setting up development environment + +This handy CLI tool will help you to setup your development environment in no time. +## Prerequisites +- [docker](https://docs.docker.com/get-docker/) +- [KinD](https://kind.sigs.k8s.io/docs/user/quick-start/) +- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) +## Usage + +You can use `greenhousectl` either by downloading the latest binary from [here](https://github.com/cloudoperators/greenhouse/releases) + +Or you can build it from source by running the following command: `build-greenhousectl` + +> [!NOTE] +> The CLI binary will be available in the `bin` folder + +## Additional information + +Charts needed for dev env setup for `KinD` + +- `charts/manager` +- `charts/idproxy` + +When setting up your development environment, certain resources are modified for development convenience - + + - The manager `Deployment` has environment variables `WEBHOOK_ONLY` and `CONTROLLERS_ONLY` + - `WEBHOOK_ONLY=true` will only run the webhook server + - `CONTROLLERS_ONLY=true` will only run the controllers + - Only one of the above can be set to `true` at a time otherwise the manager will error out + +if `DevMode` is enabled for webhooks then depending on the OS the webhook manifests are altered by removing `clientConfig.service` and +replacing it with `clientConfig.url`, allowing you to debug the code locally. + +> [!NOTE] +> The `DevMode` can be enabled by setting the `--dev-mode` flag while individually setting up the webhook or by setting the `devMode` key to `true` in the `dev-env/localenv/sample.config.json` file. + +- `linux` - the ipv4 addr from `docker0` interface is used - ex: `https://172.17.0.2:9443/` +- `macOS` - host.docker.internal is used - ex: `https://host.docker.internal:9443/` +- `windows` - ideally `host.docker.internal` should work, otherwise please reach out with a contribution :heart +- webhook certs are generated by `charts/manager/templates/kube-webhook-certgen.yaml` Job in-cluster and they are extracted and saved to `/tmp/k8s-webhook-server/serving-certs` +- `kubeconfig` of the created cluster(s) are saved to `/tmp/greenhouse/.kubeconfig` + +Below you will find a list of commands available for dev env setup + +--- +## greenhousectl dev cluster create + +Create a kinD cluster + +### Synopsis + +Create a kinD cluster and setup the greenhouse namespace optionally + +``` +greenhousectl dev cluster create [flags] +``` + +### Examples + +``` +greenhousectl dev cluster create --name --namespace +``` + +### Options + +``` + -h, --help help for create + -c, --name string create a kind cluster with a name - e.g. -c + -n, --namespace string create a namespace in the cluster - e.g. -c -n +``` + +## greenhousectl dev cluster delete + +Delete a kinD cluster + +### Synopsis + +Delete a specific kinD cluster + +``` +greenhousectl dev cluster delete [flags] +``` + +### Examples + +``` +greenhousectl dev cluster delete --name +``` + +### Options + +``` + -h, --help help for delete + -c, --name string delete the kind cluster - e.g. -c +``` + +## greenhousectl dev setup manifest + +install manifests for Greenhouse + +### Synopsis + +install CRDs, Webhook definitions, RBACs, Certs, etc... for Greenhouse into the target cluster + +``` +greenhousectl dev setup manifest [flags] +``` + +### Examples + +``` + +# Install manifests for Greenhouse into the target cluster (All manifests except Deployment - recommended) +greenhousectl dev setup manifest --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager + +# Install only CRDs for Greenhouse into the target cluster +greenhousectl dev setup manifest --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/idproxy --crd-only + +# Install manifests with excluded kinds for Greenhouse into the target cluster (Caution: Only exclude if you know what you are doing) +greenhousectl dev setup manifest --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --excludeKinds Deployment --excludeKinds Job + +# Install manifests for Greenhouse into the target cluster with values file +greenhousectl dev setup manifest --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --values-path dev-env/localenv/sample.values.yaml + +``` + +### Options + +``` + -p, --chart-path string local absolute chart path where manifests are located - e.g. //charts/manager + -d, --crd-only Install only CRDs + -e, --excludeKinds stringArray Exclude kinds from the generated manifests: ex: -e Deployment -e Job (default [Deployment]) + -h, --help help for manifest + -c, --name string Name of the kind cluster - e.g. greenhouse-123 (without the kind prefix) + -n, --namespace string namespace to install the resources + -r, --release string Helm release name, Default value: greenhouse - e.g. your-release-name (default "greenhouse") + -v, --values-path string local absolute values file path - e.g. //my-values.yaml +``` + +## greenhousectl dev setup webhook + +Setup webhooks for Greenhouse (Validating and Mutating webhooks) + +### Synopsis + +Setup Validating and Mutating webhooks for Greenhouse controller development convenience + +``` +greenhousectl dev setup webhook [flags] +``` + +### Examples + +``` + +# Setup webhook for Greenhouse controller development convenience (Webhooks run in cluster) +greenhousectl dev setup webhook --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --dockerfile ./ + +# Setup webhook for Greenhouse webhook development convenience (Webhooks run local) +greenhousectl dev setup webhook --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --dockerfile ./ --dev-mode + +# Additionally provide values file (defaults may not work since charts change over time) +greenhousectl dev setup webhook --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --dockerfile ./ --values-path hack/localenv/sample.values.yaml + + +``` + +### Options + +``` + -p, --chart-path string local chart path where manifests are located - e.g. //charts/manager + -m, --dev-mode Enable dev mode for webhook setup - Note: Admission Webhooks will be modified for local development + -f, --dockerfile string local path to the Dockerfile of greenhouse manager + -h, --help help for webhook + -c, --name string Name of the kind cluster - e.g. my-cluster (without the kind prefix) + -n, --namespace string namespace to install the resources + -r, --release string Helm release name, Default value: greenhouse - e.g. your-release-name (default "greenhouse") + -v, --values-path string local absolute values file path - e.g. //my-values.yaml +``` + +## greenhousectl dev setup + +setup dev environment + +### Synopsis + +setup dev environment with a configuration file + +``` +greenhousectl dev setup [flags] +``` + +### Examples + +``` + +# Setup Greenhouse dev environment with a configuration file +greenhousectl dev setup -f dev-env/localenv/sample.config.json + +- This will create an admin and a remote cluster +- Install CRDs, Webhook definitions, RBACs, Certs, etc... for Greenhouse into the target cluster +- Depending on the devMode, it will install the webhook in-cluster or enable it for local development + +``` + +### Options + +``` + -f, --config string configuration file path - e.g. -f hack/localenv/sample.config.json + -h, --help help for setup +``` + + +## Generating Docs +To generate the markdown documentation, run the following command: +```shell +make dev-docs +``` diff --git a/dev-env/localenv/docs.go b/dev-env/localenv/docs.go new file mode 100644 index 000000000..f4ae86b83 --- /dev/null +++ b/dev-env/localenv/docs.go @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +//go:build dev + +package main + +import ( + "bytes" + "log" + "os" + "path/filepath" + "regexp" + "strings" + "text/template" + + "github.com/spf13/cobra/doc" + + "github.com/cloudoperators/greenhouse/pkg/cmd" +) + +var removeLinks = regexp.MustCompile(`(?s)### SEE ALSO.*`) +var removeOptsInherited = regexp.MustCompile(`(?s)### Options inherited from parent commands.*`) + +// docsTemplateData - data for the docs template +// add more fields as needed +type docsTemplateData struct { + Intro string // intro markdown content + Commands string // cobra generated command markdown content + DocGen string // doc gen markdown content +} + +// extend the template for future additions +const docsTemplate = ` +{{.Intro}} +{{.Commands}} +{{.DocGen}} +` + +func getCWD() string { + // Determine the current working directory + cwd, err := os.Getwd() + if err != nil { + log.Fatalf("Error getting current working directory: %s", err.Error()) + } + + // Check if the current working directory is hack/localenv + var outputPath string + if filepath.Base(cwd) == "localenv" && filepath.Base(filepath.Dir(cwd)) == "dev-env" { + outputPath = cwd + } else { + outputPath = filepath.Join(cwd, "dev-env", "localenv") + } + return outputPath +} + +// getDevDocsIntro - get the intro markdown content from dev-env/localenv/templates/_intro.md +func getTemplate(cwd string, template string) ([]byte, error) { + return os.ReadFile(filepath.Join(cwd, "templates", template)) +} + +func stitchMarkdown(data docsTemplateData) ([]byte, error) { + t := template.Must(template.New("docs").Parse(docsTemplate)) + var output bytes.Buffer + err := t.Execute(&output, data) + if err != nil { + return nil, err + } + return output.Bytes(), nil +} + +// auto generate dev commands documentation in greenhousectl +func main() { + commands := cmd.GenerateDevDocs() + docs := make([]string, 0, len(commands)) + for _, command := range commands { + buf := new(bytes.Buffer) + err := doc.GenMarkdownCustom(command, buf, func(s string) string { return "" }) + if err != nil { + log.Fatalf("Error generating command docs: %s", err.Error()) + } + if buf.Len() > 0 { + content := buf.String() + content = removeLinks.ReplaceAllString(content, "") + content = removeOptsInherited.ReplaceAllString(content, "") + docs = append(docs, content) + } + } + outputPath := getCWD() + intro, err := getTemplate(outputPath, "_intro.md") + if err != nil { + log.Fatalf("error getting intro: %s", err.Error()) + } + docGen, err := getTemplate(outputPath, "_generate-docs.md") + if err != nil { + log.Fatalf("error getting doc gen: %s", err.Error()) + } + docData := docsTemplateData{ + Intro: string(intro), + Commands: strings.Join(docs, ""), + DocGen: string(docGen), + } + markdown, err := stitchMarkdown(docData) + if err != nil { + log.Fatalf("Error generating markdown: %s", err.Error()) + } + err = os.WriteFile(filepath.Join(outputPath, "README.md"), markdown, 0644) + if err != nil { + log.Fatalf("Error writing docs: %s", err.Error()) + } +} diff --git a/dev-env/localenv/sample.config.json b/dev-env/localenv/sample.config.json new file mode 100644 index 000000000..4d3ebe01f --- /dev/null +++ b/dev-env/localenv/sample.config.json @@ -0,0 +1,42 @@ +{ + "config": [ + { + "cluster": { + "name": "greenhouse-remote" + } + }, + { + "cluster": { + "name": "greenhouse-admin", + "namespace": "greenhouse" + }, + "dependencies": [ + { + "manifest": { + "release": "greenhouse", + "chartPath": "charts/idproxy", + "crdOnly": true + } + }, + { + "manifest": { + "release": "greenhouse", + "chartPath": "charts/manager", + "valuesPath": "dev-env/localenv/sample.values.yaml", + "crdOnly": false, + "webhook": { + "devMode": false, + "dockerFile": "./", + "envs": [ + { + "name": "WEBHOOK_ONLY", + "value": "true" + } + ] + } + } + } + ] + } + ] +} \ No newline at end of file diff --git a/dev-env/localenv/sample.values.yaml b/dev-env/localenv/sample.values.yaml new file mode 100644 index 000000000..835c1e681 --- /dev/null +++ b/dev-env/localenv/sample.values.yaml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +# SPDX-License-Identifier: Apache-2.0 + +global: + dnsDomain: green.house +alerts: + enabled: false \ No newline at end of file diff --git a/dev-env/localenv/templates/_generate-docs.md b/dev-env/localenv/templates/_generate-docs.md new file mode 100644 index 000000000..7c8e4b016 --- /dev/null +++ b/dev-env/localenv/templates/_generate-docs.md @@ -0,0 +1,5 @@ +## Generating Docs +To generate the markdown documentation, run the following command: +```shell +make dev-docs +``` \ No newline at end of file diff --git a/dev-env/localenv/templates/_intro.md b/dev-env/localenv/templates/_intro.md new file mode 100644 index 000000000..e5e6f7b09 --- /dev/null +++ b/dev-env/localenv/templates/_intro.md @@ -0,0 +1,45 @@ +# Setting up development environment + +This handy CLI tool will help you to setup your development environment in no time. +## Prerequisites +- [docker](https://docs.docker.com/get-docker/) +- [KinD](https://kind.sigs.k8s.io/docs/user/quick-start/) +- [kubectl](https://kubernetes.io/docs/tasks/tools/install-kubectl/) +## Usage + +You can use `greenhousectl` either by downloading the latest binary from [here](https://github.com/cloudoperators/greenhouse/releases) + +Or you can build it from source by running the following command: `build-greenhousectl` + +> [!NOTE] +> The CLI binary will be available in the `bin` folder + +## Additional information + +Charts needed for dev env setup for `KinD` + +- `charts/manager` +- `charts/idproxy` + +When setting up your development environment, certain resources are modified for development convenience - + + - The manager `Deployment` has environment variables `WEBHOOK_ONLY` and `CONTROLLERS_ONLY` + - `WEBHOOK_ONLY=true` will only run the webhook server + - `CONTROLLERS_ONLY=true` will only run the controllers + - Only one of the above can be set to `true` at a time otherwise the manager will error out + +if `DevMode` is enabled for webhooks then depending on the OS the webhook manifests are altered by removing `clientConfig.service` and +replacing it with `clientConfig.url`, allowing you to debug the code locally. + +> [!NOTE] +> The `DevMode` can be enabled by setting the `--dev-mode` flag while individually setting up the webhook or by setting the `devMode` key to `true` in the `dev-env/localenv/sample.config.json` file. + +- `linux` - the ipv4 addr from `docker0` interface is used - ex: `https://172.17.0.2:9443/` +- `macOS` - host.docker.internal is used - ex: `https://host.docker.internal:9443/` +- `windows` - ideally `host.docker.internal` should work, otherwise please reach out with a contribution :heart +- webhook certs are generated by `charts/manager/templates/kube-webhook-certgen.yaml` Job in-cluster and they are extracted and saved to `/tmp/k8s-webhook-server/serving-certs` +- `kubeconfig` of the created cluster(s) are saved to `/tmp/greenhouse/.kubeconfig` + +Below you will find a list of commands available for dev env setup + +--- \ No newline at end of file diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index 6bfffe0e3..ce9154169 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: Greenhouse - version: 05c51ef + version: 3293b24 description: PlusOne operations platform paths: /Team: diff --git a/go.mod b/go.mod index 61fdada5a..69801b4db 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ replace ( ) require ( + github.com/cenkalti/backoff/v4 v4.3.0 github.com/dexidp/dex v0.0.0-20240807174518-43956db7fd75 github.com/ghodss/yaml v1.0.0 github.com/jeremywohl/flatten/v2 v2.0.0-20211013061545-07e4a09fb8e4 @@ -30,6 +31,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 github.com/spf13/pflag v1.0.5 + github.com/vladimirvivien/gexe v0.3.0 github.com/wI2L/jsondiff v0.6.0 go.uber.org/zap v1.27.0 golang.org/x/text v0.17.0 @@ -57,6 +59,7 @@ require ( github.com/akutz/memconn v0.1.0 // indirect github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect github.com/containerd/log v0.1.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect github.com/distribution/reference v0.6.0 // indirect github.com/fxamacker/cbor/v2 v2.6.0 // indirect @@ -69,6 +72,7 @@ require ( github.com/hdevalence/ed25519consensus v0.2.0 // indirect github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 // indirect github.com/jsimonetti/rtnetlink v1.4.0 // indirect + github.com/karrick/godirwalk v1.17.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.0 // indirect @@ -79,7 +83,6 @@ require ( github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect - github.com/vladimirvivien/gexe v0.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect go4.org/mem v0.0.0-20220726221520-4f986261bf13 // indirect diff --git a/go.sum b/go.sum index d2531257a..35f5991f9 100644 --- a/go.sum +++ b/go.sum @@ -66,6 +66,8 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b h1:otBG+dV+YK+Soembj github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0BsqsP2LwDJ9aOkm/6J86V6lyAXCoQWGw3K50= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -88,6 +90,7 @@ github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= +github.com/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= @@ -316,8 +319,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/juanfont/headscale v0.22.3 h1:BHpPO9cIB1vBVyp0faEAk2Pq2Zi04NXXgsf3Wt60sac= github.com/juanfont/headscale v0.22.3/go.mod h1:NdBJ+givOOABlLd7GFT2WRjGP4e/ylLYy+U1WIkpTIQ= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= +github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= +github.com/karrick/godirwalk v1.17.0/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= @@ -507,8 +510,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= -github.com/vladimirvivien/gexe v0.2.0 h1:nbdAQ6vbZ+ZNsolCgSVb9Fno60kzSuvtzVh6Ytqi/xY= -github.com/vladimirvivien/gexe v0.2.0/go.mod h1:LHQL00w/7gDUKIak24n801ABp8C+ni6eBht9vGVst8w= +github.com/vladimirvivien/gexe v0.3.0 h1:4xwiOwGrDob5OMR6E92B9olDXYDglXdHhzR1ggYtWJM= +github.com/vladimirvivien/gexe v0.3.0/go.mod h1:fp7cy60ON1xjhtEI/+bfSEIXX35qgmI+iRYlGOqbBFM= github.com/wI2L/jsondiff v0.6.0 h1:zrsH3FbfVa3JO9llxrcDy/XLkYPLgoMX6Mz3T2PP2AI= github.com/wI2L/jsondiff v0.6.0/go.mod h1:D6aQ5gKgPF9g17j+E9N7aasmU1O+XvfmWm1y8UMmNpw= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= diff --git a/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go index de954e38c..4e6516ac4 100644 --- a/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/greenhouse/v1alpha1/zz_generated.deepcopy.go @@ -217,6 +217,7 @@ func (in *ClusterKubeconfigData) DeepCopyInto(out *ClusterKubeconfigData) { *out = make([]ClusterKubeconfigContextItem, len(*in)) copy(*out, *in) } + out.Preferences = in.Preferences } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterKubeconfigData. @@ -261,6 +262,21 @@ func (in *ClusterKubeconfigList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterKubeconfigPreferences) DeepCopyInto(out *ClusterKubeconfigPreferences) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterKubeconfigPreferences. +func (in *ClusterKubeconfigPreferences) DeepCopy() *ClusterKubeconfigPreferences { + if in == nil { + return nil + } + out := new(ClusterKubeconfigPreferences) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClusterKubeconfigSpec) DeepCopyInto(out *ClusterKubeconfigSpec) { *out = *in diff --git a/pkg/cmd/dev.go b/pkg/cmd/dev.go new file mode 100644 index 000000000..dffa1d5a4 --- /dev/null +++ b/pkg/cmd/dev.go @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "errors" + "strings" + + "github.com/spf13/cobra" + "github.com/vladimirvivien/gexe" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/commands" +) + +var devSetupCmd = &cobra.Command{ + Use: "dev", + Short: "Setup development environment", + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // check if KinD is installed + knd := gexe.ProgAvail("kind") + if strings.TrimSpace(knd) == "" { + return errors.New("please install KinD first, see https://kind.sigs.k8s.io/docs/user/quick-start/") + } + dock := gexe.ProgAvail("docker") + if strings.TrimSpace(dock) == "" { + return errors.New("please install Docker first, see https://docs.docker.com/get-docker/") + } + kc := gexe.ProgAvail("kubectl") + if strings.TrimSpace(kc) == "" { + return errors.New("please install kubectl first, see https://kubernetes.io/docs/tasks/tools/install-kubectl/") + } + return nil + }, +} + +func init() { + rootCmd.AddCommand(devSetupCmd) + devSetupCmd.AddCommand(commands.GetLocalSetupCommands()...) +} + +func GenerateDevDocs() []*cobra.Command { + return commands.GenerateDevCommandDocs() +} diff --git a/pkg/internal/local/commands/cluster.go b/pkg/internal/local/commands/cluster.go new file mode 100644 index 000000000..e63545e15 --- /dev/null +++ b/pkg/internal/local/commands/cluster.go @@ -0,0 +1,68 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/setup" +) + +var clusterCmd = &cobra.Command{ + Use: "cluster", + Short: "Create / Delete KinD cluster", + DisableAutoGenTag: true, +} + +// sub commands +var ( + createClusterCmd = &cobra.Command{ + Use: "create", + Short: "Create a kinD cluster", + Long: "Create a kinD cluster and setup the greenhouse namespace optionally", + Example: `greenhousectl dev cluster create --name --namespace `, + DisableAutoGenTag: true, + PreRunE: func(cmd *cobra.Command, args []string) error { + return validateFlagInputs(cmd.Flags()) + }, + RunE: processCreateLocalCluster, + } + deleteClusterCmd = &cobra.Command{ + Use: "delete", + Short: "Delete a kinD cluster", + Long: "Delete a specific kinD cluster", + Example: `greenhousectl dev cluster delete --name `, + DisableAutoGenTag: true, + PreRunE: func(cmd *cobra.Command, _ []string) error { + return validateFlagInputs(cmd.Flags()) + }, + RunE: processDeleteLocalCluster, + } +) + +func processDeleteLocalCluster(_ *cobra.Command, _ []string) error { + err := setup.NewExecutionEnv().WithClusterDelete(clusterName).Run() + if err != nil { + return err + } + return nil +} + +func processCreateLocalCluster(_ *cobra.Command, _ []string) error { + err := setup.NewExecutionEnv().WithClusterSetup(clusterName, namespaceName).Run() + if err != nil { + return err + } + return nil +} + +func init() { + createClusterCmd.Flags().StringVarP(&clusterName, "name", "c", "", "create a kind cluster with a name - e.g. -c ") + createClusterCmd.Flags().StringVarP(&namespaceName, "namespace", "n", "", "create a namespace in the cluster - e.g. -c -n ") + deleteClusterCmd.Flags().StringVarP(&clusterName, "name", "c", "", "delete the kind cluster - e.g. -c ") + cobra.CheckErr(createClusterCmd.MarkFlagRequired("name")) + cobra.CheckErr(deleteClusterCmd.MarkFlagRequired("name")) + clusterCmd.AddCommand(createClusterCmd) + clusterCmd.AddCommand(deleteClusterCmd) +} diff --git a/pkg/internal/local/commands/common.go b/pkg/internal/local/commands/common.go new file mode 100644 index 000000000..de45dc3f6 --- /dev/null +++ b/pkg/internal/local/commands/common.go @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + clusterName string + namespaceName string + dockerFile string + releaseName string + chartPath string + valuesPath string + crdOnly bool + excludeKinds []string +) + +func GetLocalSetupCommands() []*cobra.Command { + return []*cobra.Command{ + clusterCmd, + setupCmd, + } +} + +func GenerateDevCommandDocs() []*cobra.Command { + return []*cobra.Command{ + createClusterCmd, + deleteClusterCmd, + manifestCmd, + webhookCmd, + setupCmd, + } +} + +func validateFlagInputs(flags *pflag.FlagSet) error { + invalidFlags := make([]string, 0) + flags.VisitAll(func(flag *pflag.Flag) { + switch flag.Value.Type() { + case "string": + _, required := flag.Annotations[cobra.BashCompOneRequiredFlag] + if required && strings.TrimSpace(flag.Value.String()) == "" { + invalidFlags = append(invalidFlags, flag.Name) + return + } + if !required && flag.Changed && strings.TrimSpace(flag.Value.String()) == "" { + invalidFlags = append(invalidFlags, flag.Name) + return + } + case "stringArray": + if flag.Changed { + arr, err := flags.GetStringArray(flag.Name) + if err != nil { + invalidFlags = append(invalidFlags, flag.Name) + return + } + for _, a := range arr { + if strings.TrimSpace(a) == "" || strings.Contains(a, "-") { + invalidFlags = append(invalidFlags, flag.Name) + return + } + } + } + default: + return + } + }) + if len(invalidFlags) > 0 { + return fmt.Errorf("flag validation failed for: %s", strings.Join(invalidFlags, ", ")) + } + return nil +} diff --git a/pkg/internal/local/commands/manifest.go b/pkg/internal/local/commands/manifest.go new file mode 100644 index 000000000..33cc15d7c --- /dev/null +++ b/pkg/internal/local/commands/manifest.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/setup" +) + +func manifestExample() string { + return ` +# Install manifests for Greenhouse into the target cluster (All manifests except Deployment - recommended) +greenhousectl dev setup manifest --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager + +# Install only CRDs for Greenhouse into the target cluster +greenhousectl dev setup manifest --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/idproxy --crd-only + +# Install manifests with excluded kinds for Greenhouse into the target cluster (Caution: Only exclude if you know what you are doing) +greenhousectl dev setup manifest --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --excludeKinds Deployment --excludeKinds Job + +# Install manifests for Greenhouse into the target cluster with values file +greenhousectl dev setup manifest --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --values-path dev-env/localenv/sample.values.yaml +` +} + +var manifestCmd = &cobra.Command{ + Use: "manifest", + Short: "install manifests for Greenhouse", + Long: "install CRDs, Webhook definitions, RBACs, Certs, etc... for Greenhouse into the target cluster", + Example: manifestExample(), + DisableAutoGenTag: true, + PreRunE: func(cmd *cobra.Command, _ []string) error { + return validateFlagInputs(cmd.Flags()) + }, + RunE: processManifests, +} + +func processManifests(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + manifest := &setup.Manifest{ + ReleaseName: releaseName, + ChartPath: chartPath, + ValuesPath: valuesPath, + CRDOnly: crdOnly, + ExcludeKinds: excludeKinds, + Webhook: nil, + } + err := setup.NewExecutionEnv(). + WithClusterSetup(clusterName, namespaceName). + WithLimitedManifests(ctx, manifest). + Run() + if err != nil { + return err + } + return nil +} + +func init() { + // required flags + manifestCmd.Flags().StringVarP(&clusterName, "name", "c", "", "Name of the kind cluster - e.g. greenhouse-123 (without the kind prefix)") + manifestCmd.Flags().StringVarP(&namespaceName, "namespace", "n", "", "namespace to install the resources") + manifestCmd.Flags().StringVarP(&chartPath, "chart-path", "p", "", "local absolute chart path where manifests are located - e.g. //charts/manager") + manifestCmd.Flags().StringVarP(&releaseName, "release", "r", "greenhouse", "Helm release name, Default value: greenhouse - e.g. your-release-name") + // optional flags + manifestCmd.Flags().StringVarP(&valuesPath, "values-path", "v", "", "local absolute values file path - e.g. //my-values.yaml") + manifestCmd.Flags().BoolVarP(&crdOnly, "crd-only", "d", false, "Install only CRDs") + manifestCmd.Flags().StringArrayVarP(&excludeKinds, "excludeKinds", "e", []string{"Deployment"}, "Exclude kinds from the generated manifests: ex: -e Deployment -e Job") + + cobra.CheckErr(manifestCmd.MarkFlagRequired("name")) + cobra.CheckErr(manifestCmd.MarkFlagRequired("namespace")) + cobra.CheckErr(manifestCmd.MarkFlagRequired("chart-path")) + cobra.CheckErr(manifestCmd.MarkFlagRequired("release")) + + setupCmd.AddCommand(manifestCmd) +} diff --git a/pkg/internal/local/commands/setup.go b/pkg/internal/local/commands/setup.go new file mode 100644 index 000000000..ffc192963 --- /dev/null +++ b/pkg/internal/local/commands/setup.go @@ -0,0 +1,105 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "encoding/json" + "errors" + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/setup" +) + +type Config struct { + Config []*clusterConfig `json:"config"` +} + +type clusterConfig struct { + Cluster *setup.Cluster `json:"cluster"` + Dependencies []*ClusterDependency `json:"dependencies"` +} + +type ClusterDependency struct { + Manifest *setup.Manifest `json:"manifest"` +} + +func setupExample() string { + return ` +# Setup Greenhouse dev environment with a configuration file +greenhousectl dev setup -f dev-env/localenv/sample.config.json + +- This will create an admin and a remote cluster +- Install CRDs, Webhook definitions, RBACs, Certs, etc... for Greenhouse into the target cluster +- Depending on the devMode, it will install the webhook in-cluster or enable it for local development +` +} + +var ( + setupConfigFile string + setupCmd = &cobra.Command{ + Use: "setup", + Short: "setup dev environment", + Long: "setup dev environment with a configuration file", + Example: setupExample(), + DisableAutoGenTag: true, + RunE: processSetup, + } +) + +func processSetup(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + _, err := os.Stat(setupConfigFile) + if err != nil { + return fmt.Errorf("config file - %s not found: %w", setupConfigFile, err) + } + f, err := os.ReadFile(setupConfigFile) + if err != nil { + return fmt.Errorf("failed to read config file - %s: %w", setupConfigFile, err) + } + config := &Config{} + err = json.Unmarshal(f, config) + if err != nil { + return fmt.Errorf("failed to unmarshal config file - %s: %w", setupConfigFile, err) + } + + for _, cfg := range config.Config { + namespace := "" + if cfg.Cluster == nil { + return errors.New("cluster config is missing") + } + if cfg.Cluster.Namespace != nil { + namespace = *cfg.Cluster.Namespace + } + env := setup.NewExecutionEnv(). + WithClusterSetup(cfg.Cluster.Name, namespace) + for _, dep := range cfg.Dependencies { + if dep.Manifest != nil && dep.Manifest.Webhook == nil { + env = env.WithLimitedManifests(ctx, dep.Manifest) + } + if dep.Manifest != nil && dep.Manifest.Webhook != nil { + dep.Manifest.ExcludeKinds = append( + dep.Manifest.ExcludeKinds, + "Deployment", + "Job", + "MutatingWebhookConfiguration", + "ValidatingWebhookConfiguration", + ) + env = env.WithWebhookDevelopment(ctx, dep.Manifest) + } + } + err = env.Run() + if err != nil { + return err + } + } + return nil +} + +func init() { + setupCmd.Flags().StringVarP(&setupConfigFile, "config", "f", "", "configuration file path - e.g. -f hack/localenv/sample.config.json") + cobra.CheckErr(setupCmd.MarkFlagRequired("config")) +} diff --git a/pkg/internal/local/commands/webhook.go b/pkg/internal/local/commands/webhook.go new file mode 100644 index 000000000..2e75f93a5 --- /dev/null +++ b/pkg/internal/local/commands/webhook.go @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package commands + +import ( + "github.com/spf13/cobra" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/setup" +) + +var devMode bool + +func webhookExample() string { + return ` +# Setup webhook for Greenhouse controller development convenience (Webhooks run in cluster) +greenhousectl dev setup webhook --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --dockerfile ./ + +# Setup webhook for Greenhouse webhook development convenience (Webhooks run local) +greenhousectl dev setup webhook --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --dockerfile ./ --dev-mode + +# Additionally provide values file (defaults may not work since charts change over time) +greenhousectl dev setup webhook --name greenhouse-admin --namespace greenhouse --release greenhouse --chart-path charts/manager --dockerfile ./ --values-path hack/localenv/sample.values.yaml + +` +} + +var webhookCmd = &cobra.Command{ + Use: "webhook", + Short: "Setup webhooks for Greenhouse (Validating and Mutating webhooks)", + Long: "Setup Validating and Mutating webhooks for Greenhouse controller development convenience", + Example: webhookExample(), + DisableAutoGenTag: true, + PreRunE: func(cmd *cobra.Command, _ []string) error { + return validateFlagInputs(cmd.Flags()) + }, + RunE: processWebhook, +} + +func processWebhook(cmd *cobra.Command, _ []string) error { + ctx := cmd.Context() + hookCfg := &setup.Webhook{ + DockerFile: dockerFile, + Envs: []setup.WebhookEnv{ + { + Name: "WEBHOOK_ONLY", + Value: "true", + }, + }, + DevMode: devMode, + } + manifest := &setup.Manifest{ + ReleaseName: releaseName, + ChartPath: chartPath, + ValuesPath: valuesPath, + CRDOnly: crdOnly, + ExcludeKinds: []string{"Deployment", "Job", "MutatingWebhookConfiguration", "ValidatingWebhookConfiguration"}, + Webhook: hookCfg, + } + + err := setup.NewExecutionEnv(). + WithClusterSetup(clusterName, namespaceName). + WithWebhookDevelopment(ctx, manifest). + Run() + if err != nil { + return err + } + return nil +} + +func init() { + webhookCmd.Flags().StringVarP(&clusterName, "name", "c", "", "Name of the kind cluster - e.g. my-cluster (without the kind prefix)") + webhookCmd.Flags().StringVarP(&namespaceName, "namespace", "n", "", "namespace to install the resources") + webhookCmd.Flags().StringVarP(&chartPath, "chart-path", "p", "", "local chart path where manifests are located - e.g. //charts/manager") + webhookCmd.Flags().StringVarP(&valuesPath, "values-path", "v", "", "local absolute values file path - e.g. //my-values.yaml") + webhookCmd.Flags().StringVarP(&dockerFile, "dockerfile", "f", "", "local path to the Dockerfile of greenhouse manager") + webhookCmd.Flags().StringVarP(&releaseName, "release", "r", "greenhouse", "Helm release name, Default value: greenhouse - e.g. your-release-name") + webhookCmd.Flags().BoolVarP(&devMode, "dev-mode", "m", false, "Enable dev mode for webhook setup - Note: Admission Webhooks will be modified for local development") + + cobra.CheckErr(webhookCmd.MarkFlagRequired("name")) + cobra.CheckErr(webhookCmd.MarkFlagRequired("namespace")) + cobra.CheckErr(webhookCmd.MarkFlagRequired("release")) + cobra.CheckErr(webhookCmd.MarkFlagRequired("chart-path")) + cobra.CheckErr(webhookCmd.MarkFlagRequired("dockerfile")) + + setupCmd.AddCommand(webhookCmd) +} diff --git a/pkg/internal/local/helm/helm.go b/pkg/internal/local/helm/helm.go new file mode 100644 index 000000000..13c905ced --- /dev/null +++ b/pkg/internal/local/helm/helm.go @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package helm + +import ( + "context" + "errors" + "os" + + "helm.sh/helm/v3/pkg/release" + + "gopkg.in/yaml.v3" + "helm.sh/helm/v3/pkg/action" + "helm.sh/helm/v3/pkg/chart" + "helm.sh/helm/v3/pkg/chart/loader" + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/utils" +) + +type Options struct { + ClusterName string + ReleaseName string + Namespace string + ChartPath string + ValuesPath *string + KubeConfigPath string +} + +type helmValues map[string]interface{} + +type ClientOption func(*Options) + +// WithChartPath - sets the chartPath flag for the helm client +// Note: Absolute paths are preferred +func WithChartPath(chartPath string) ClientOption { + return func(h *Options) { + h.ChartPath = chartPath + } +} + +// WithClusterName - sets the clusterName flag for the helm client +// used in a kind cluster scenario. By providing a kind cluster name, +// the kubeconfig will be fetched for the kind cluster using kind.getKubeCfg(clusterName, false) +func WithClusterName(clusterName string) ClientOption { + return func(h *Options) { + h.ClusterName = clusterName + } +} + +// WithReleaseName - sets the releaseName flag for the helm client +// release name will be used to install the chart or render the template with release labels +func WithReleaseName(releaseName string) ClientOption { + return func(h *Options) { + h.ReleaseName = releaseName + } +} + +// WithNamespace - sets the namespace flag for the helm client +// namespace will be used to install the chart or render the template +func WithNamespace(namespace string) ClientOption { + return func(h *Options) { + h.Namespace = namespace + } +} + +// WithValuesPath - sets the valuesPath flag for the helm client +// values provided in the file will be used to render the chart +// if no values path is provided, the default values will be used from util.GetManagerHelmValues() +func WithValuesPath(valuesPath string) ClientOption { + return func(h *Options) { + h.ValuesPath = utils.StringP(valuesPath) + } +} + +// WithKubeConfigPath - sets the kubeConfigPath flag for the helm client +func WithKubeConfigPath(kubeConfigPath string) ClientOption { + return func(h *Options) { + h.KubeConfigPath = kubeConfigPath + } +} + +// apply - applies the Options to the client +func apply(options *Options) *client { + return &client{ + clusterName: options.ClusterName, + chartPath: options.ChartPath, + releaseName: options.ReleaseName, + namespace: options.Namespace, + valuesPath: options.ValuesPath, + kubeConfigPath: options.KubeConfigPath, + } +} + +type client struct { + install *action.Install + upgrade *action.Upgrade + clusterName string + releaseName string + namespace string + chartPath string + values map[string]interface{} + valuesPath *string + kubeConfigPath string +} + +// IHelm - interface wrapper for an actual Helm client +type IHelm interface { + Install(ctx context.Context, dryRun bool) (*release.Release, error) + Template(ctx context.Context) (string, error) +} + +// NewClient - creates a new Helm client with given ClientOption options +// currently supporting helm install and template actions and can be extended to support other actions +func NewClient(ctx context.Context, opts ...ClientOption) (IHelm, error) { + logger := utils.NewKLog(ctx) + options := &Options{} + for _, opt := range opts { + opt(options) + } + hc := apply(options) + + if hc.clusterName == "" { + return nil, errors.New("cluster name must be provided") + } + if hc.releaseName == "" { + return nil, errors.New("release name must be provided") + } + if hc.namespace == "" { + return nil, errors.New("namespace must be provided") + } + if hc.chartPath == "" { + return nil, errors.New("chart path must be provided") + } + if hc.kubeConfigPath == "" { + return nil, errors.New("missing kubeconfig path") + } + if hc.valuesPath == nil { + hc.values = utils.GetManagerHelmValues() + } + + flags := &genericclioptions.ConfigFlags{ + Namespace: &hc.namespace, + KubeConfig: &hc.kubeConfigPath, + } + + actionConfig := new(action.Configuration) + err := actionConfig.Init( + flags, + hc.namespace, + "secret", + logger.V(10).Info, + ) + if err != nil { + return nil, err + } + hc.install = action.NewInstall(actionConfig) + hc.upgrade = action.NewUpgrade(actionConfig) + return hc, nil +} + +// Install - installs the helm chart +func (c *client) Install(ctx context.Context, dryRun bool) (*release.Release, error) { + c.install.ReleaseName = c.releaseName + c.install.Namespace = c.namespace + c.install.IncludeCRDs = true + c.install.DryRun = dryRun + localChart, vals, err := c.prepareChartAndValues() + if err != nil { + return nil, err + } + return c.install.RunWithContext(ctx, localChart, vals) +} + +// Template - returns the rendered template of the chart +func (c *client) Template(ctx context.Context) (string, error) { + c.install.Force = true // only for template functionality + c.install.IsUpgrade = true // to avoid missing helm label validation error - ex: CRD doesn't carry helm label + rel, err := c.Install(ctx, true) + if err != nil { + return "", err + } + return rel.Manifest, nil +} + +// prepareChartAndValues - loads the chart from the given local path and values specified +func (c *client) prepareChartAndValues() (*chart.Chart, helmValues, error) { + localChart, err := loader.Load(c.chartPath) + if err != nil { + return nil, nil, err + } + var vals helmValues + if c.valuesPath != nil { + valBytes, err := os.ReadFile(*c.valuesPath) + if err != nil { + return nil, nil, err + } + err = yaml.Unmarshal(valBytes, &vals) + if err != nil { + return nil, nil, err + } + } else { + vals = c.values + } + return localChart, vals, nil +} diff --git a/pkg/internal/local/klient/docker.go b/pkg/internal/local/klient/docker.go new file mode 100644 index 000000000..976fa716b --- /dev/null +++ b/pkg/internal/local/klient/docker.go @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package klient + +import ( + "github.com/cloudoperators/greenhouse/pkg/internal/local/utils" +) + +// BuildImage - uses docker cli to build an image +func BuildImage(img, platform, dockerFilePath string) error { + return utils.Shell{ + Cmd: "docker build --platform ${platform} -t ${img} ${path}", + Vars: map[string]string{ + "path": dockerFilePath, + "img": img, + "platform": platform, + }, + }.Exec() +} diff --git a/pkg/internal/local/klient/kind.go b/pkg/internal/local/klient/kind.go new file mode 100644 index 000000000..50188b295 --- /dev/null +++ b/pkg/internal/local/klient/kind.go @@ -0,0 +1,128 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package klient + +import ( + "errors" + "fmt" + "strings" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/utils" +) + +// CreateCluster - creates a kind cluster with the given name +// if the cluster already exists, it sets the context to the existing cluster +func CreateCluster(clusterName string) error { + exists, err := clusterExists(clusterName) + if err != nil { + return err + } else if exists { + utils.Logf("kind cluster with name %s already exists", clusterName) + return utils.Shell{ + Cmd: "kubectl config set-context kind-${name}", + Vars: map[string]string{ + "name": clusterName, + }, + }.Exec() + } + return utils.Shell{ + Cmd: "kind create cluster --name ${name}", + Vars: map[string]string{ + "name": clusterName, + }, + }.Exec() +} + +// DeleteCluster - deletes a kind cluster with the given name +// if the cluster does not exist, it does nothing +func DeleteCluster(clusterName string) error { + exists, err := clusterExists(clusterName) + if err != nil { + return err + } else if !exists { + utils.Logf("kind cluster with name %s does not exist", clusterName) + return nil + } + return utils.Shell{ + Cmd: "kind delete cluster --name ${name}", + Vars: map[string]string{ + "name": clusterName, + }, + }.Exec() +} + +// clusterExists - checks if a kind cluster with the given name exists +func clusterExists(clusterName string) (bool, error) { + clusters, err := getKindClusters() + if err != nil { + return false, fmt.Errorf("failed to check if cluster exists: %w", err) + } + utils.Logf("checking if cluster %s exists...", clusterName) + for _, c := range clusters { + if c == clusterName { + return true, nil + } + } + return false, nil +} + +// getKindClusters - returns a list of all kind clusters +func getKindClusters() ([]string, error) { + result, err := utils.Shell{ + Cmd: "kind get clusters", + }.ExecWithResult() + if err != nil { + return nil, err + } + return strings.FieldsFunc(result, func(r rune) bool { + return r == '\n' + }), nil +} + +// CreateNamespace - creates a namespace with the given name +func CreateNamespace(namespaceName string) error { + if strings.TrimSpace(namespaceName) == "" { + return errors.New("namespace name cannot be empty") + } + return utils.ShellPipe{ + Shells: []utils.Shell{ + { + Cmd: "kubectl create namespace ${namespace} --dry-run=client -o yaml", + Vars: map[string]string{ + "namespace": namespaceName, + }, + }, + { + Cmd: "kubectl apply -f -", + }, + }, + }.Exec() +} + +// GetKubeCfg - get kind cluster kubeconfig +// if internal is true, it returns the internal kubeconfig of the cluster +func GetKubeCfg(clusterName string, internal bool) (string, error) { + sh := utils.Shell{ + Cmd: "kind get kubeconfig --name ${name}", + Vars: map[string]string{ + "name": clusterName, + }, + } + if internal { + sh.Cmd += " --internal" + } + return sh.ExecWithResult() +} + +// LoadImage - loads a docker image into a kind cluster +func LoadImage(image, clusterName string) error { + sh := utils.Shell{ + Cmd: "kind load docker-image ${image} --name ${cluster}", + Vars: map[string]string{ + "image": image, + "cluster": clusterName, + }, + } + return sh.Exec() +} diff --git a/pkg/internal/local/klient/klient.go b/pkg/internal/local/klient/klient.go new file mode 100644 index 000000000..498e5079a --- /dev/null +++ b/pkg/internal/local/klient/klient.go @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package klient + +import ( + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// NewKubeClientFromConfig - creates a new Kubernetes CRUD client from a given kubeconfig +func NewKubeClientFromConfig(configStr string) (client.Client, error) { + config, err := clientcmd.RESTConfigFromKubeConfig([]byte(configStr)) + if err != nil { + return nil, err + } + return client.New(config, client.Options{Scheme: clientgoscheme.Scheme}) +} diff --git a/pkg/internal/local/setup/cluster.go b/pkg/internal/local/setup/cluster.go new file mode 100644 index 000000000..99805cd10 --- /dev/null +++ b/pkg/internal/local/setup/cluster.go @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package setup + +import ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/klient" + "github.com/cloudoperators/greenhouse/pkg/internal/local/utils" +) + +type Cluster struct { + Name string `json:"name"` + Namespace *string `json:"namespace"` + kubeConfigPath string +} + +// clusterSetup - creates a kind Cluster with a given name and optionally creates a namespace if specified +func clusterSetup(env *ExecutionEnv) error { + if env.cluster == nil { + return errors.New("cluster configuration is missing") + } + err := klient.CreateCluster(env.cluster.Name) + if err != nil { + return err + } + if env.cluster.Namespace != nil { + err = klient.CreateNamespace(*env.cluster.Namespace) + if err != nil { + return err + } + } + err = env.cluster.saveConfig() + if err != nil { + return err + } + env.info = append(env.info, fmt.Sprintf("cluster %s - kubeconfig: %s", env.cluster.Name, env.cluster.kubeConfigPath)) + return nil +} + +// clusterDelete - deletes a kind Cluster with a given name +func clusterDelete(env *ExecutionEnv) error { + if env.cluster == nil { + return errors.New("cluster configuration is missing") + } + return klient.DeleteCluster(env.cluster.Name) +} + +func (c *Cluster) saveConfig() error { + kubeConfig, err := klient.GetKubeCfg(c.Name, false) + if err != nil { + return err + } + dir := filepath.Join(os.TempDir(), "greenhouse") + file := c.Name + ".kubeconfig" + err = utils.WriteToPath(dir, file, kubeConfig) + if err != nil { + return err + } + c.kubeConfigPath = filepath.Join(dir, file) + return nil +} diff --git a/pkg/internal/local/setup/manifest.go b/pkg/internal/local/setup/manifest.go new file mode 100644 index 000000000..bed6f1968 --- /dev/null +++ b/pkg/internal/local/setup/manifest.go @@ -0,0 +1,218 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package setup + +import ( + "context" + "fmt" + "strings" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/helm" + "github.com/cloudoperators/greenhouse/pkg/internal/local/utils" +) + +type Manifest struct { + ReleaseName string `json:"release"` + ChartPath string `json:"chartPath"` + ValuesPath string `json:"valuesPath"` + CRDOnly bool `json:"crdOnly"` + ExcludeKinds []string `json:"excludeKinds"` + Webhook *Webhook `json:"webhook"` + hc helm.IHelm +} + +func limitedManifestSetup(ctx context.Context, m *Manifest) Step { + return func(env *ExecutionEnv) error { + var clusterName, namespace string + if env.cluster != nil { + clusterName = env.cluster.Name + } + if env.cluster.Namespace != nil { + namespace = *env.cluster.Namespace + } + err := m.prepareHelmClient(ctx, m, clusterName, namespace, env.cluster.kubeConfigPath) + if err != nil { + return err + } + resources, err := m.generateManifests(ctx) + if err != nil { + return err + } + return m.applyManifests(resources, namespace, env.cluster.kubeConfigPath) + } +} + +func allManifestSetup(ctx context.Context, m *Manifest) Step { + return func(env *ExecutionEnv) error { + var clusterName, namespace string + if env.cluster != nil { + clusterName = env.cluster.Name + } + if env.cluster.Namespace != nil { + namespace = *env.cluster.Namespace + } + err := m.prepareHelmClient(ctx, m, clusterName, namespace, env.cluster.kubeConfigPath) + if err != nil { + return err + } + resources, err := m.generateAllManifests(ctx) + if err != nil { + return err + } + return m.applyManifests(resources, namespace, env.cluster.kubeConfigPath) + } +} + +// webhookManifestSetup - generates and applies manifest to the Cluster +// if webhook configuration is provided, modified webhook manifest are generated and applied +// if dev mode is enabled, webhook certs are extracted from the Cluster and saved to the local filesystem +func webhookManifestSetup(ctx context.Context, m *Manifest) Step { + return func(env *ExecutionEnv) error { + var clusterName, namespace string + if env.cluster != nil { + clusterName = env.cluster.Name + } + if env.cluster.Namespace != nil { + namespace = *env.cluster.Namespace + } + err := m.prepareHelmClient(ctx, m, clusterName, namespace, env.cluster.kubeConfigPath) + if err != nil { + return err + } + resources, err := m.generateAllManifests(ctx) + if err != nil { + return err + } + excluded := m.resourceExclusion(resources) + filtered := m.filterCustomResources(excluded) + if m.Webhook == nil { + utils.Log("no webhook configuration provided, skipping webhook kustomization") + noWbManifests := excludeResources(filtered, []string{"MutatingWebhookConfiguration", "ValidatingWebhookConfiguration"}) + return m.applyManifests(noWbManifests, namespace, env.cluster.kubeConfigPath) + } + webHookManifests, err := m.setupWebhookManifest(resources, clusterName) + if err != nil { + return err + } + filtered = append(filtered, webHookManifests...) + err = m.applyManifests(filtered, namespace, env.cluster.kubeConfigPath) + if err != nil { + return err + } + if m.Webhook.DevMode { + return m.extractWebhookCerts(ctx, clusterName, namespace) + } + return nil + } +} + +func (m *Manifest) prepareHelmClient(ctx context.Context, manifest *Manifest, clusterName, namespace, kubeConfigPath string) error { + opts := []helm.ClientOption{ + helm.WithChartPath(manifest.ChartPath), + helm.WithClusterName(clusterName), + helm.WithNamespace(namespace), + helm.WithReleaseName(manifest.ReleaseName), + helm.WithValuesPath(manifest.ValuesPath), + helm.WithKubeConfigPath(kubeConfigPath), + } + hc, err := helm.NewClient(ctx, opts...) + if err != nil { + return err + } + m.hc = hc + return nil +} + +// GenerateManifests - uses helm templating to explode the chart and returns the raw manifest +func (m *Manifest) generateManifests(ctx context.Context) ([]map[string]interface{}, error) { + resources, err := m.generateAllManifests(ctx) + if err != nil { + return nil, err + } + excluded := m.resourceExclusion(resources) + return m.filterCustomResources(excluded), nil +} + +func (m *Manifest) generateAllManifests(ctx context.Context) ([]map[string]interface{}, error) { + utils.Logf("generating manifest for chart %s...", m.ChartPath) + templates, err := m.hc.Template(ctx) + if err != nil { + return nil, err + } + docs := strings.Split(templates, "---") + resources := make([]map[string]interface{}, 0) + for _, doc := range docs { + resource, err := utils.RawK8sInterface([]byte(doc)) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal resources: %w", err) + } + if resource != nil { + resources = append(resources, resource) + } + } + return resources, nil +} + +// ApplyManifests - applies the given resources to the Cluster using kubectl +func (m *Manifest) applyManifests(resources []map[string]interface{}, namespace, kubeConfigPath string) error { + manifests, err := utils.Stringify(resources) + if err != nil { + return err + } + return m.apply(manifests, namespace, kubeConfigPath) +} + +func (m *Manifest) apply(manifests, namespace, kubeConfigPath string) error { + utils.Log("applying manifest...") + sh := utils.Shell{} + tmpResourcePath, err := utils.RandomWriteToTmpFolder("kind-resources", manifests) + if err != nil { + return err + } + defer utils.CleanUp(tmpResourcePath) + sh.Cmd = fmt.Sprintf("kubectl apply --kubeconfig=%s -f %s -n %s", kubeConfigPath, tmpResourcePath, namespace) + return sh.Exec() +} + +func (m *Manifest) resourceExclusion(resources []map[string]interface{}) []map[string]interface{} { + if len(m.ExcludeKinds) == 0 { + return resources + } + return excludeResources(resources, m.ExcludeKinds) +} + +func excludeResources(resources []map[string]interface{}, exclusions []string) []map[string]interface{} { + excludeResources := make([]map[string]interface{}, 0) + excludeResources = append(excludeResources, resources...) + for i := 0; i < len(excludeResources); { + if k, ok := excludeResources[i]["kind"].(string); ok && utils.SliceContains(exclusions, k) { + excludeResources[i] = excludeResources[len(excludeResources)-1] + excludeResources = excludeResources[:len(excludeResources)-1] + } else { + i++ + } + } + return excludeResources +} + +func (m *Manifest) filterCustomResources(resources []map[string]interface{}) []map[string]interface{} { + if m.CRDOnly { + return filterResourcesBy(resources, "CustomResourceDefinition") + } + return resources +} + +func filterResourcesBy(resources []map[string]interface{}, filterBy string) []map[string]interface{} { + filteredResource := make([]map[string]interface{}, 0) + filteredResource = append(filteredResource, resources...) + for i := 0; i < len(filteredResource); { + if k, ok := filteredResource[i]["kind"].(string); ok && k != filterBy { + filteredResource[i] = filteredResource[len(filteredResource)-1] + filteredResource = filteredResource[:len(filteredResource)-1] + } else { + i++ + } + } + return filteredResource +} diff --git a/pkg/internal/local/setup/setup.go b/pkg/internal/local/setup/setup.go new file mode 100644 index 000000000..98f3305d0 --- /dev/null +++ b/pkg/internal/local/setup/setup.go @@ -0,0 +1,73 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package setup + +import ( + "context" + "strings" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/utils" +) + +type ExecutionEnv struct { + cluster *Cluster + steps []Step + info []string // info messages to be displayed at the end of run +} + +type Step func(builder *ExecutionEnv) error + +func NewExecutionEnv() *ExecutionEnv { + return &ExecutionEnv{ + steps: make([]Step, 0), + } +} + +func (env *ExecutionEnv) WithClusterSetup(name, namespace string) *ExecutionEnv { + env.cluster = &Cluster{ + Name: name, + Namespace: nil, + } + if strings.TrimSpace(namespace) != "" { + env.cluster.Namespace = &namespace + } + env.steps = append(env.steps, clusterSetup) + return env +} + +func (env *ExecutionEnv) WithClusterDelete(name string) *ExecutionEnv { + env.cluster = &Cluster{ + Name: name, + } + env.steps = append(env.steps, clusterDelete) + return env +} + +func (env *ExecutionEnv) WithAllManifests(ctx context.Context, manifest *Manifest) *ExecutionEnv { + env.steps = append(env.steps, allManifestSetup(ctx, manifest)) + return env +} + +func (env *ExecutionEnv) WithLimitedManifests(ctx context.Context, manifest *Manifest) *ExecutionEnv { + env.steps = append(env.steps, limitedManifestSetup(ctx, manifest)) + return env +} + +func (env *ExecutionEnv) WithWebhookDevelopment(ctx context.Context, manifest *Manifest) *ExecutionEnv { + env.steps = append(env.steps, webhookManifestSetup(ctx, manifest)) + return env +} + +func (env *ExecutionEnv) Run() error { + for _, step := range env.steps { + err := step(env) + if err != nil { + return err + } + } + for _, i := range env.info { + utils.Log(i) + } + return nil +} diff --git a/pkg/internal/local/setup/webhook.go b/pkg/internal/local/setup/webhook.go new file mode 100644 index 000000000..4cb9f32a3 --- /dev/null +++ b/pkg/internal/local/setup/webhook.go @@ -0,0 +1,372 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package setup + +import ( + "context" + "errors" + "fmt" + "net" + "os" + "path/filepath" + "runtime" + "strings" + + aregv1 "k8s.io/api/admissionregistration/v1" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/cloudoperators/greenhouse/pkg/internal/local/klient" + "github.com/cloudoperators/greenhouse/pkg/internal/local/utils" +) + +type Webhook struct { + Envs []WebhookEnv `json:"envs"` + DockerFile string `json:"dockerFile"` + DevMode bool `json:"devMode"` +} + +type WebhookEnv struct { + Name string `json:"name"` + Value string `json:"value"` +} + +const ( + MangerIMG = "greenhouse/manager:local" + MangerContainer = "manager" + DeploymentKind = "Deployment" + DeploymentNameSuffix = "-controller-manager" + JobKind = "Job" + JobNameSuffix = "-kube-webhook-certgen" + MutatingWebhookConfigurationKind = "MutatingWebhookConfiguration" + ValidatingWebhookConfigurationKind = "ValidatingWebhookConfiguration" + webhookCertSecSuffix = "-webhook-server-cert" +) + +// setupWebhookManifest - sets up the webhook manifest by modifying the manager deployment, cert job and webhook configurations +// deploys manager in WEBHOOK_ONLY mode so that you don't need to run webhooks locally during controller development +// modifies cert job (charts/manager/templates/kube-webhook-certgen.yaml) to include host.docker.internal +// if devMode is enabled, modifies mutating and validating webhook configurations to use host.docker.internal URL and removes service from clientConfig +// extracts the webhook certs from the secret and writes them to tmp/k8s-webhook-server/serving-certs directory +func (m *Manifest) setupWebhookManifest(resources []map[string]interface{}, clusterName string) ([]map[string]interface{}, error) { + webhookManifests := make([]map[string]interface{}, 0) + releaseName := m.ReleaseName + managerDeployment, err := extractResourceByNameKind(resources, releaseName+DeploymentNameSuffix, DeploymentKind) + if err != nil { + return nil, err + } + + utils.Log("modifying manager deployment...") + managerDeployment, err = m.modifyManagerDeployment(managerDeployment) + if err != nil { + return nil, err + } + + certJob, err := extractResourceByNameKind(resources, releaseName+JobNameSuffix, JobKind) + if err != nil { + return nil, err + } + utils.Log("modifying cert job...") + webhookURL := getWebhookURL() + certJob, err = m.modifyCertJob(certJob, webhookURL) + if err != nil { + return nil, err + } + + webhookManifests = append(webhookManifests, managerDeployment, certJob) + webhookResources := extractResourcesByKinds(resources, MutatingWebhookConfigurationKind, ValidatingWebhookConfigurationKind) + if m.Webhook.DevMode { + utils.Log("enabling webhook local development...") + if webhookURL != "" { + webhooks, err := m.modifyWebhooks(webhookResources, webhookURL) + if err != nil { + return nil, err + } + if len(webhooks) > 0 { + webhookManifests = append(webhookManifests, webhooks...) + } + } + } else { + webhookManifests = append(webhookManifests, webhookResources...) + } + + err = m.buildAndLoadImage(clusterName) + if err != nil { + return nil, err + } + return webhookManifests, nil +} + +// modifyManagerDeployment - appends the env in manager container by setting WEBHOOK_ONLY=true +func (m *Manifest) modifyManagerDeployment(deploymentResource map[string]interface{}) (map[string]interface{}, error) { + deployment := &appsv1.Deployment{} + deploymentStr, err := utils.Stringy(deploymentResource) + if err != nil { + return nil, err + } + // convert yaml to appsv1.Deployment + err = utils.FromYamlToK8sObject(deploymentStr, deployment) + if err != nil { + return nil, err + } + index := getManagerContainerIndex(deployment) + if index == -1 { + return nil, errors.New("manager container not found in deployment") + } + for _, e := range m.Webhook.Envs { + deployment.Spec.Template.Spec.Containers[index].Env = append(deployment.Spec.Template.Spec.Containers[index].Env, v1.EnvVar{ + Name: e.Name, + Value: e.Value, + }) + } + deployment.Spec.Template.Spec.Containers[index].Image = MangerIMG + deployment.Spec.Replicas = utils.Int32P(1) + depBytes, err := utils.FromK8sObjectToYaml(deployment, appsv1.SchemeGroupVersion) + if err != nil { + return nil, err + } + return utils.RawK8sInterface(depBytes) +} + +// modifyWebhooks - modifies the webhook configurations to use host.docker.internal URL and removes service from clientConfig +// during local development of webhooks api server will forward the request to host machine where the webhook is running at port 9443 +func (m *Manifest) modifyWebhooks(resources []map[string]interface{}, webhookURL string) ([]map[string]interface{}, error) { + modifiedWebhooks := make([]map[string]interface{}, 0) + for _, resource := range resources { + if k, ok := resource["kind"].(string); ok { + var hookBytes []byte + var err error + switch k { + case MutatingWebhookConfigurationKind: + hookBytes, err = m.modifyWebhook(resource, &aregv1.MutatingWebhookConfiguration{}, webhookURL) + case ValidatingWebhookConfigurationKind: + hookBytes, err = m.modifyWebhook(resource, &aregv1.ValidatingWebhookConfiguration{}, webhookURL) + } + if err != nil { + return nil, err + } + if hookBytes != nil { + hookInterface, err := utils.RawK8sInterface(hookBytes) + if err != nil { + return nil, err + } + modifiedWebhooks = append(modifiedWebhooks, hookInterface) + } + } + } + return modifiedWebhooks, nil +} + +func (m *Manifest) modifyWebhook(resource map[string]interface{}, hook client.Object, webhookURL string) ([]byte, error) { + resStr, err := utils.Stringy(resource) + if err != nil { + return nil, err + } + // convert yaml to aregv1.MutatingWebhookConfiguration{} or aregv1.ValidatingWebhookConfiguration{} + err = utils.FromYamlToK8sObject(resStr, hook) + if err != nil { + return nil, err + } + switch modifiedHook := any(hook).(type) { + case *aregv1.MutatingWebhookConfiguration: + utils.Logf("modifying mutating webhook %s...", modifiedHook.Name) + utils.Logf("setting webhook client config to %s...", webhookURL) + for i, c := range modifiedHook.Webhooks { + if c.ClientConfig.Service.Path != nil { + url := "https://" + net.JoinHostPort(webhookURL, "9443") + *c.ClientConfig.Service.Path + modifiedHook.Webhooks[i].ClientConfig.URL = utils.StringP(url) + modifiedHook.Webhooks[i].ClientConfig.Service = nil + } + } + // convert from aregv1.MutatingWebhookConfiguration{} to yaml + return utils.FromK8sObjectToYaml(modifiedHook, aregv1.SchemeGroupVersion) + case *aregv1.ValidatingWebhookConfiguration: + utils.Logf("modifying validating webhook %s...", modifiedHook.Name) + utils.Logf("setting webhook client config to %s...", webhookURL) + for i, c := range modifiedHook.Webhooks { + if c.ClientConfig.Service.Path != nil { + url := "https://" + net.JoinHostPort(webhookURL, "9443") + *c.ClientConfig.Service.Path + modifiedHook.Webhooks[i].ClientConfig.URL = utils.StringP(url) + modifiedHook.Webhooks[i].ClientConfig.Service = nil + } + } + // convert from aregv1.ValidatingWebhookConfiguration{} to yaml + return utils.FromK8sObjectToYaml(modifiedHook, aregv1.SchemeGroupVersion) + default: + return nil, fmt.Errorf("unexpected webhook type: %T", hook) + } +} + +// modifyCertJob - appends host.docker.internal to the args in cert job +// certs generated are valid only for a set of defined DNS names, adding host.docker.internal to hosts will prevent TLS errors +func (m *Manifest) modifyCertJob(resources map[string]interface{}, webhookURL string) (map[string]interface{}, error) { + job := &batchv1.Job{} + jobStr, err := utils.Stringy(resources) + if err != nil { + return nil, err + } + err = utils.FromYamlToK8sObject(jobStr, job) + if err != nil { + return nil, err + } + args := job.Spec.Template.Spec.InitContainers[0].Args + for i, arg := range args { + if strings.Contains(arg, "host") { + args[i] = fmt.Sprintf("%s,%s", arg, webhookURL) + } + } + job.Spec.Template.Spec.InitContainers[0].Args = args + jobBytes, err := utils.FromK8sObjectToYaml(job, batchv1.SchemeGroupVersion) + if err != nil { + return nil, err + } + return utils.RawK8sInterface(jobBytes) +} + +// buildAndLoadImage - builds the manager image as greenhouse/manager:local and loads it to the kind Cluster +func (m *Manifest) buildAndLoadImage(clusterName string) error { + if !utils.CheckIfFileExists(m.Webhook.DockerFile) { + return fmt.Errorf("docker file not found: %s", m.Webhook.DockerFile) + } + utils.Log("building manager image...") + err := klient.BuildImage(MangerIMG, utils.GetHostPlatform(), m.Webhook.DockerFile) + if err != nil { + return err + } + utils.Log("loading manager image to Cluster...") + return klient.LoadImage(MangerIMG, clusterName) +} + +func getManagerContainerIndex(deployment *appsv1.Deployment) int { + for i, container := range deployment.Spec.Template.Spec.Containers { + if container.Name == MangerContainer { + return i + } + } + return -1 +} + +func extractResourceByNameKind(resources []map[string]interface{}, name, kind string) (map[string]interface{}, error) { + for _, resource := range resources { + if k, ok := resource["kind"].(string); ok && k == kind { + if n, ok := resource["metadata"].(map[string]interface{})["name"].(string); ok && n == name { + return resource, nil + } + } + } + return nil, fmt.Errorf("resource not found: %s", name) +} + +func extractResourcesByKinds(resources []map[string]interface{}, kinds ...string) []map[string]interface{} { + extractedResources := make([]map[string]interface{}, 0) + for _, k := range kinds { + resource := extractResourceByKind(resources, k) + if resource != nil { + extractedResources = append(extractedResources, resource) + } + } + return extractedResources +} + +func extractResourceByKind(resources []map[string]interface{}, kind string) map[string]interface{} { + for _, resource := range resources { + if k, ok := resource["kind"].(string); ok && k == kind { + return resource + } + } + return nil +} + +// extractWebhookCerts - extracts the webhook cert secret generated by the cert job and writes them to tmp/k8s-webhook-server/serving-certs directory +func (m *Manifest) extractWebhookCerts(ctx context.Context, clusterName, namespace string) error { + var cl client.Client + var err error + jobName := m.ReleaseName + JobNameSuffix + secName := m.ReleaseName + webhookCertSecSuffix + kubeconfig, err := klient.GetKubeCfg(clusterName, false) + if err != nil { + return err + } + cl, err = klient.NewKubeClientFromConfig(kubeconfig) + if err != nil { + return err + } + + if err = utils.WaitUntilJobSucceeds(ctx, cl, jobName, namespace); err != nil { + return err + } + if err = utils.WaitUntilSecretCreated(ctx, cl, secName, namespace); err != nil { + return err + } + return writeCertsToTemp(ctx, cl, secName, namespace) +} + +func writeCertsToTemp(ctx context.Context, cl client.Client, name, namespace string) error { + secret := &v1.Secret{} + err := cl.Get(ctx, types.NamespacedName{ + Namespace: namespace, + Name: name, + }, secret) + if err != nil { + return err + } + cert, ok := secret.Data["tls.crt"] + if !ok { + return fmt.Errorf("tls.crt not found in secret %s", name) + } + key, ok := secret.Data["tls.key"] + if !ok { + return fmt.Errorf("tls.key not found in secret %s", name) + } + dirPath := filepath.Join(os.TempDir(), "k8s-webhook-server", "serving-certs") + err = utils.WriteToPath(dirPath, "tls.crt", string(cert)) + if err != nil { + return err + } + err = utils.WriteToPath(dirPath, "tls.key", string(key)) + if err != nil { + return err + } + utils.Logf("webhook certs written to %s", dirPath) + return nil +} + +func getWebhookURL() string { + switch runtime.GOOS { + case "darwin": + utils.Log("detected macOS...") + return "host.docker.internal" + case "linux": + utils.Log("detected linux...") + return strings.TrimSpace(getHostIPFromInterface()) + default: + utils.Logf("detected %s ...", runtime.GOOS) + return "host.docker.internal" + } +} + +// getHostIPFromInterface - returns the IP address of the docker0 interface (only for linux) +func getHostIPFromInterface() string { + i, err := net.InterfaceByName("docker0") + if err != nil { + utils.LogErr("failed to get docker0 interface - %s", err.Error()) + return "" + } + addresses, err := i.Addrs() + if err != nil { + utils.LogErr("failed to get addresses for docker0 interface - %s", err.Error()) + return "" + } + for _, addr := range addresses { + if ipv4 := addr.(*net.IPNet).IP.To4(); ipv4 != nil { + utils.Logf("found IP address for docker0 interface: %s", ipv4.String()) + return ipv4.String() + } + } + utils.LogErr("failed to get IP address for docker0 interface") + return "" +} diff --git a/pkg/internal/local/utils/exec.go b/pkg/internal/local/utils/exec.go new file mode 100644 index 000000000..64aad5a51 --- /dev/null +++ b/pkg/internal/local/utils/exec.go @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "errors" + "fmt" + "io" + "os" + "strings" + + "github.com/vladimirvivien/gexe" +) + +type ShellPipe struct { + Shells []Shell +} + +type Shell struct { + Cmd string + Vars map[string]string +} + +// Exec executes a set of commands serially and pipes the output of each command to the next +func (s ShellPipe) Exec() error { + exec := gexe.New() + if len(s.Shells) == 0 { + return errors.New("empty commands") + } + if len(s.Shells) == 1 { + return errors.New("too few commands to pipe") + } + commands := make([]string, 0) + for _, shell := range s.Shells { + if strings.TrimSpace(shell.Cmd) == "" { + return errors.New("empty command found") + } + for k, v := range shell.Vars { + exec.SetVar(k, v) + } + commands = append(commands, shell.Cmd) + } + pipe := exec.Commands(commands...).Pipe() + errs := make([]string, 0) + for _, p := range pipe.Procs() { + if err := p.Err(); err != nil { + errs = append(errs, err.Error()) + } + out, err := io.ReadAll(p.Out()) + if err != nil { + continue + } + if strings.TrimSpace(string(out)) != "" { + Log(string(out)) + } + } + if len(errs) > 0 { + return fmt.Errorf("error executing command: %s", strings.Join(errs, "\n")) + } + return nil +} + +// ExecWithResult executes the shell command and returns the output of the command +func (s Shell) ExecWithResult() (string, error) { + exec := gexe.New() + setVars(exec, s.Vars) + if err := s.checkEmptyCommand(); err != nil { + return "", err + } + proc := exec.RunProc(s.Cmd) + if err := proc.Err(); err != nil { + return "", err + } + return proc.Result(), nil +} + +func (s Shell) checkEmptyCommand() error { + if strings.TrimSpace(s.Cmd) == "" { + return errors.New("empty command") + } + return nil +} + +func setVars(exec *gexe.Echo, vars map[string]string) { + for k, v := range vars { + exec.SetVar(k, v) + } +} + +// Exec executes a single shell command +func (s Shell) Exec() error { + exec := gexe.New() + setVars(exec, s.Vars) + if err := s.checkEmptyCommand(); err != nil { + return err + } + proc := exec.NewProc(s.Cmd) + proc.SetStdout(os.Stdout) + proc.SetStderr(os.Stderr) + if err := proc.Run().Err(); err != nil { + LogErr("error running command: %s", s.Cmd) + return err + } + return nil +} diff --git a/pkg/internal/local/utils/expect.go b/pkg/internal/local/utils/expect.go new file mode 100644 index 000000000..4f78c8fe0 --- /dev/null +++ b/pkg/internal/local/utils/expect.go @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "context" + "errors" + "time" + + "github.com/cenkalti/backoff/v4" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var DefaultElapsedTime = 30 * time.Second + +// WaitUntilSecretCreated - waits until a secret is created in the given namespace with a backoff strategy +func WaitUntilSecretCreated(ctx context.Context, k8sClient client.Client, name, namespace string) error { + b := backoff.NewExponentialBackOff(backoff.WithInitialInterval(5*time.Second), backoff.WithMaxElapsedTime(DefaultElapsedTime)) + return backoff.Retry(func() error { + Logf("waiting for secret %s to be created...", name) + secret := &v1.Secret{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespace, + Name: name, + }, secret) + if err != nil { + return err + } + return nil + }, b) +} + +// WaitUntilJobSucceeds - waits until a job succeeds in the given namespace with a backoff strategy +func WaitUntilJobSucceeds(ctx context.Context, k8sClient client.Client, name, namespace string) error { + b := backoff.NewExponentialBackOff(backoff.WithInitialInterval(5*time.Second), backoff.WithMaxElapsedTime(DefaultElapsedTime)) + return backoff.Retry(func() error { + Logf("waiting for job %s to succeed...", name) + job := &batchv1.Job{} + err := k8sClient.Get(ctx, types.NamespacedName{ + Namespace: namespace, + Name: name, + }, job) + if err != nil { + return err + } + if job.Status.Failed > 0 { + return errors.New("job failed") + } + + if job.Status.Succeeded == 0 { + return errors.New("job is not yet ready") + } + return nil + }, b) +} diff --git a/pkg/internal/local/utils/utils.go b/pkg/internal/local/utils/utils.go new file mode 100644 index 000000000..d94c71087 --- /dev/null +++ b/pkg/internal/local/utils/utils.go @@ -0,0 +1,210 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "bytes" + "context" + "fmt" + "io" + "os" + "runtime" + "slices" + "strings" + + "github.com/go-logr/logr" + "gopkg.in/yaml.v3" + kruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + kyaml "k8s.io/apimachinery/pkg/util/yaml" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/klog/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + syaml "sigs.k8s.io/yaml" +) + +func Log(args ...any) { + args[0] = "===== 🤖 " + args[0].(string) + klog.Info(args...) +} + +func Logf(format string, args ...any) { + klog.Infof("===== 🤖 "+format, args...) +} + +func LogErr(format string, args ...any) { + klog.Infof("===== 😵 "+format, args...) +} + +func NewKLog(ctx context.Context) logr.Logger { + return klog.FromContext(ctx) +} + +func Int32P(i int32) *int32 { + return &i +} + +func StringP(s string) *string { + if strings.TrimSpace(s) == "" { + return nil + } + return &s +} + +// GetManagerHelmValues - returns the default values for the manager helm chart +func GetManagerHelmValues() map[string]interface{} { + return map[string]interface{}{ + "alerts": map[string]interface{}{ + "enabled": false, + }, + "global": map[string]interface{}{ + "dnsDomain": "localhost", + }, + } +} + +func SliceContains(slice []string, item string) bool { + return slices.ContainsFunc(slice, func(s string) bool { + return strings.EqualFold(s, item) + }) +} + +func WriteToPath(dir, fileName, content string) error { + if err := os.MkdirAll(dir, os.ModePerm); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + filePath := dir + "/" + fileName + file, err := os.Create(filePath) + if err != nil { + return err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + LogErr("failed to close file %s after write: %s", filePath, err.Error()) + } + }(file) + if n, err := io.WriteString(file, content); n == 0 || err != nil { + return fmt.Errorf("error writing file %s: %w", file.Name(), err) + } + return nil +} + +// RandomWriteToTmpFolder - writes the provided content to temp folder in OS +// Concurrent writes do not conflict as the file name is appended with a random string +func RandomWriteToTmpFolder(fileName, content string) (string, error) { + file, err := os.CreateTemp("", "kind-cluster-"+fileName) + if err != nil { + return "", err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + LogErr("failed to close file %s after write: %s", fileName, err.Error()) + } + }(file) + if n, err := io.WriteString(file, content); n == 0 || err != nil { + return "", fmt.Errorf("kind kubecfg file: bytes copied: %d: %w]", n, err) + } + return file.Name(), nil +} + +// RawK8sInterface - unmarshalls the provided YAML bytes into a map[string]interface{} +func RawK8sInterface(yamlBytes []byte) (map[string]interface{}, error) { + var data map[string]interface{} + err := kyaml.Unmarshal(yamlBytes, &data) + if err != nil { + return nil, fmt.Errorf("error unmarshalling YAML bytes: %w", err) + } + return data, nil +} + +func Stringy(data map[string]interface{}) (string, error) { + s, err := yaml.Marshal(data) + if err != nil { + return "", err + } + return string(s), nil +} + +func Stringify(data []map[string]interface{}) (string, error) { + stringSources := make([]string, 0) + for _, d := range data { + s, err := Stringy(d) + if err != nil { + return "", err + } + stringSources = append(stringSources, s) + } + return strings.Join(stringSources, "\n---\n"), nil +} + +// FromYamlToK8sObject - Converts a YAML document to a Kubernetes object +// if yaml contains multiple documents, then corresponding kubernetes objects should be provided +func FromYamlToK8sObject(doc string, resources ...any) error { + yamlBytes := []byte(doc) + dec := kyaml.NewDocumentDecoder(io.NopCloser(bytes.NewReader(yamlBytes))) + buffer := make([]byte, len(yamlBytes)) + + for _, resource := range resources { + n, err := dec.Read(buffer) + if err != nil { + return err + } + err = kyaml.Unmarshal(buffer[:n], resource) + if err != nil { + return err + } + } + return nil +} + +// FromK8sObjectToYaml - Converts a Kubernetes object to a YAML document +func FromK8sObjectToYaml(obj client.Object, gvk schema.GroupVersion) ([]byte, error) { + scheme := kruntime.NewScheme() + err := clientgoscheme.AddToScheme(scheme) + if err != nil { + return nil, err + } + codec := serializer.NewCodecFactory(scheme).LegacyCodec(gvk) + jsonBytes, err := kruntime.Encode(codec, obj) + if err != nil { + return nil, fmt.Errorf("error encoding object to JSON bytes: %w", err) + } + + yamlBytes, err := syaml.JSONToYAML(jsonBytes) + if err != nil { + return nil, fmt.Errorf("error marshalling as YAML bytes: %w", err) + } + + return yamlBytes, nil +} + +func CheckIfFileExists(f string) bool { + _, err := os.Stat(f) + return !os.IsNotExist(err) +} + +func CleanUp(files ...string) { + // clean up the tmp files + for _, file := range files { + if err := os.Remove(file); err != nil { + LogErr("failed to remove file %s: %s", file, err.Error()) + } + } +} + +func GetHostPlatform() string { + var platform string + switch runtime.GOARCH { + case "amd64": + platform = "linux/amd64" + case "arm64": + platform = "linux/arm64" + default: + platform = "linux/amd64" + } + return platform +} diff --git a/ui/types/schema.d.ts b/ui/types/schema.d.ts index d9a78d24f..3ae8c054e 100644 --- a/ui/types/schema.d.ts +++ b/ui/types/schema.d.ts @@ -8,782 +8,1140 @@ * Do not make direct changes to the file. */ - export interface paths { - "/Plugin": { - post: { - responses: { - /** @description Plugin */ - default: { - content: never; + "/Organization": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - }; - }; - "/Organization": { - post: { - responses: { - /** @description Organization */ - default: { - content: never; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Organization */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - }; - "/TeamRole": { - post: { - responses: { - /** @description TeamRole */ - default: { - content: never; + "/ClusterKubeconfig": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - }; - }; - "/PluginPreset": { - post: { - responses: { - /** @description PluginPreset */ - default: { - content: never; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description ClusterKubeconfig */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - }; - "/Cluster": { - post: { - responses: { - /** @description Cluster */ - default: { - content: never; + "/Cluster": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - }; - }; - "/PluginDefinition": { - post: { - responses: { - /** @description PluginDefinition */ - default: { - content: never; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Cluster */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - }; - "/Team": { - post: { - responses: { - /** @description Team */ - default: { - content: never; + "/TeamRole": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - }; - }; - "/TeamMembership": { - post: { - responses: { - /** @description TeamMembership */ - default: { - content: never; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description TeamRole */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - }; - "/TeamRoleBinding": { - post: { - responses: { - /** @description TeamRoleBinding */ - default: { - content: never; + "/TeamMembership": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - }; - }; -} - -export type webhooks = Record; - -export interface components { - schemas: { - /** - * Plugin - * @description Plugin is the Schema for the plugins API - */ - Plugin: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; - }; - }; - /** @description PluginSpec defines the desired state of Plugin */ - spec?: { - /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ - clusterName?: string; - /** @description Disabled indicates that the plugin is administratively disabled. */ - disabled: boolean; - /** @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI. This is especially helpful to distinguish multiple instances of a PluginDefinition in the same context. Defaults to a normalized version of metadata.name. */ - displayName?: string; - /** @description Values are the values for a PluginDefinition instance. */ - optionValues?: { - /** @description Name of the values. */ - name: string; - /** @description Value is the actual value in plain text. */ - value?: unknown; - /** @description ValueFrom references a potentially confidential value in another source. */ - valueFrom?: { - /** @description Secret references the secret containing the value. */ - secret?: { - /** @description Key in the secret to select the value from. */ - key: string; - /** @description Name of the secret in the same namespace. */ - name: string; - }; - }; - }[]; - /** @description PluginDefinition is the name of the PluginDefinition this instance is for. */ - pluginDefinition: string; - /** @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed. Defaults to the Greenhouse managed namespace if not set. */ - releaseNamespace?: string; - }; - /** @description PluginStatus defines the observed state of Plugin */ - status?: { - /** @description Description provides additional details of the plugin. */ - description?: string; - /** @description ExposedServices provides an overview of the Plugins services that are centrally exposed. It maps the exposed URL to the service found in the manifest. */ - exposedServices?: { - [key: string]: { - /** @description Name is the name of the service in the target cluster. */ - name: string; - /** @description Namespace is the namespace of the service in the target cluster. */ - namespace: string; - /** - * Format: int32 - * @description Port is the port of the service. - */ - port: number; - /** @description Protocol is the protocol of the service. */ - protocol?: string; - }; - }; - /** @description HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. */ - helmChart?: { - /** @description Name of the HelmChart chart. */ - name: string; - /** @description Repository of the HelmChart chart. */ - repository: string; - /** @description Version of the HelmChart chart. */ - version: string; - }; - /** @description HelmReleaseStatus reflects the status of the latest HelmChart release. This is only configured if the pluginDefinition is backed by HelmChart. */ - helmReleaseStatus?: { - /** - * Format: date-time - * @description FirstDeployed is the timestamp of the first deployment of the release. - */ - firstDeployed?: string; - /** - * Format: date-time - * @description LastDeployed is the timestamp of the last deployment of the release. - */ - lastDeployed?: string; - /** @description Status is the status of a HelmChart release. */ - status: string; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description TeamMembership */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - /** @description StatusConditions contain the different conditions that constitute the status of the Plugin. */ - statusConditions?: { - conditions?: { - /** - * Format: date-time - * @description LastTransitionTime is the last time the condition transitioned from one status to another. - */ - lastTransitionTime: string; - /** @description Message is an optional human readable message indicating details about the last transition. */ - message?: string; - /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ - reason?: string; - /** @description Status of the condition. */ - status: string; - /** @description Type of the condition. */ - type: string; - }[]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/PluginPreset": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - /** @description UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. */ - uiApplication?: { - /** @description Name of the UI application. */ - name: string; - /** @description URL specifies the url to a built javascript asset. By default, assets are loaded from the Juno asset server using the provided name and version. */ - url?: string; - /** @description Version of the frontend application. */ - version: string; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description PluginPreset */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - /** @description Version contains the latest pluginDefinition version the config was last applied with successfully. */ - version?: string; - /** - * Format: int32 - * @description Weight configures the order in which Plugins are shown in the Greenhouse UI. - */ - weight?: number; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * Organization - * @description Organization is the Schema for the organizations API - */ - Organization: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; + "/Team": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - /** @description OrganizationSpec defines the desired state of Organization */ - spec?: { - /** @description Authentication configures the organizations authentication mechanism. */ - authentication?: { - /** @description OIDConfig configures the OIDC provider. */ - oidc?: { - /** @description ClientIDReference references the Kubernetes secret containing the client id. */ - clientIDReference: { - /** @description Key in the secret to select the value from. */ - key: string; - /** @description Name of the secret in the same namespace. */ - name: string; - }; - /** @description ClientSecretReference references the Kubernetes secret containing the client secret. */ - clientSecretReference: { - /** @description Key in the secret to select the value from. */ - key: string; - /** @description Name of the secret in the same namespace. */ - name: string; - }; - /** @description Issuer is the URL of the identity service. */ - issuer: string; - /** @description RedirectURI is the redirect URI. If none is specified, the Greenhouse ID proxy will be used. */ - redirectURI?: string; - }; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Team */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - /** @description Description provides additional details of the organization. */ - description?: string; - /** @description DisplayName is an optional name for the organization to be displayed in the Greenhouse UI. Defaults to a normalized version of metadata.name. */ - displayName?: string; - /** @description MappedOrgAdminIDPGroup is the IDP group ID identifying org admins */ - mappedOrgAdminIdPGroup?: string; - }; - /** @description OrganizationStatus defines the observed state of an Organization */ - status?: Record; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * TeamRole - * @description TeamRole is the Schema for the TeamRoles API - */ - TeamRole: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; + "/TeamRoleBinding": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - annotations?: { - [key: string]: string; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description TeamRoleBinding */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - }; - /** @description TeamRoleSpec defines the desired state of a TeamRole */ - spec?: { - /** @description Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role */ - rules?: { - /** @description APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of the enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. */ - apiGroups?: string[]; - /** @description NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path Since non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding. Rules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. */ - nonResourceURLs?: string[]; - /** @description ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. */ - resourceNames?: string[]; - /** @description Resources is a list of resources this rule applies to. '*' represents all resources. */ - resources?: string[]; - /** @description Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. */ - verbs: string[]; - }[]; - }; - /** @description TeamRoleStatus defines the observed state of a TeamRole */ - status?: Record; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * PluginPreset - * @description PluginPreset is the Schema for the PluginPresets API - */ - PluginPreset: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; + "/Plugin": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - /** @description PluginPresetSpec defines the desired state of PluginPreset */ - spec?: { - /** @description ClusterSelector is a label selector to select the clusters the plugin bundle should be deployed to. */ - clusterSelector: { - /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ - matchExpressions?: { - /** @description key is the label key that the selector applies to. */ - key: string; - /** @description operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. */ - operator: string; - /** @description values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. */ - values?: string[]; - }[]; - /** @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. */ - matchLabels?: { - [key: string]: string; - }; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Plugin */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - /** @description PluginSpec is the spec of the plugin to be deployed by the PluginPreset. */ - plugin: { - /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ - clusterName?: string; - /** @description Disabled indicates that the plugin is administratively disabled. */ - disabled: boolean; - /** @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI. This is especially helpful to distinguish multiple instances of a PluginDefinition in the same context. Defaults to a normalized version of metadata.name. */ - displayName?: string; - /** @description Values are the values for a PluginDefinition instance. */ - optionValues?: { - /** @description Name of the values. */ - name: string; - /** @description Value is the actual value in plain text. */ - value?: unknown; - /** @description ValueFrom references a potentially confidential value in another source. */ - valueFrom?: { - /** @description Secret references the secret containing the value. */ - secret?: { - /** @description Key in the secret to select the value from. */ - key: string; - /** @description Name of the secret in the same namespace. */ - name: string; - }; - }; - }[]; - /** @description PluginDefinition is the name of the PluginDefinition this instance is for. */ - pluginDefinition: string; - /** @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed. Defaults to the Greenhouse managed namespace if not set. */ - releaseNamespace?: string; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/PluginDefinition": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; }; - }; - /** @description PluginPresetStatus defines the observed state of PluginPreset */ - status?: { - /** @description StatusConditions contain the different conditions that constitute the status of the PluginPreset. */ - statusConditions?: { - conditions?: { - /** - * Format: date-time - * @description LastTransitionTime is the last time the condition transitioned from one status to another. - */ - lastTransitionTime: string; - /** @description Message is an optional human readable message indicating details about the last transition. */ - message?: string; - /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ - reason?: string; - /** @description Status of the condition. */ - status: string; - /** @description Type of the condition. */ - type: string; - }[]; + get?: never; + put?: never; + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description PluginDefinition */ + default: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; }; - }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; }; - /** - * Cluster - * @description Cluster is the Schema for the clusters API - */ - Cluster: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; +} +export type webhooks = Record; +export interface components { + schemas: { + /** + * Organization + * @description Organization is the Schema for the organizations API + */ + Organization: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description OrganizationSpec defines the desired state of Organization */ + spec?: { + /** @description Authentication configures the organizations authentication mechanism. */ + authentication?: { + /** @description OIDConfig configures the OIDC provider. */ + oidc?: { + /** @description ClientIDReference references the Kubernetes secret containing the client id. */ + clientIDReference: { + /** @description Key in the secret to select the value from. */ + key: string; + /** @description Name of the secret in the same namespace. */ + name: string; + }; + /** @description ClientSecretReference references the Kubernetes secret containing the client secret. */ + clientSecretReference: { + /** @description Key in the secret to select the value from. */ + key: string; + /** @description Name of the secret in the same namespace. */ + name: string; + }; + /** @description Issuer is the URL of the identity service. */ + issuer: string; + /** @description RedirectURI is the redirect URI.\nIf none is specified, the Greenhouse ID proxy will be used. */ + redirectURI?: string; + }; + }; + /** @description Description provides additional details of the organization. */ + description?: string; + /** @description DisplayName is an optional name for the organization to be displayed in the Greenhouse UI.\nDefaults to a normalized version of metadata.name. */ + displayName?: string; + /** @description MappedOrgAdminIDPGroup is the IDP group ID identifying org admins */ + mappedOrgAdminIdPGroup?: string; + }; + /** @description OrganizationStatus defines the observed state of an Organization */ + status?: Record; }; - }; - /** @description ClusterSpec defines the desired state of the Cluster. */ - spec?: { /** - * @description AccessMode configures how the cluster is accessed from the Greenhouse operator. - * @enum {string} + * ClusterKubeconfig + * @description ClusterKubeconfig is the Schema for the clusterkubeconfigs API\nObjectMeta.OwnerReferences is used to link the ClusterKubeconfig to the Cluster\nObjectMeta.Generation is used to detect changes in the ClusterKubeconfig and sync local kubeconfig files\nObjectMeta.Name is designed to be the same with the Cluster name */ - accessMode: "direct" | "headscale"; - }; - /** @description ClusterStatus defines the observed state of Cluster */ - status?: { + ClusterKubeconfig: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description ClusterKubeconfigSpec stores the kubeconfig data for the cluster\nThe idea is to use kubeconfig data locally with minimum effort (with local tools or plain kubectl):\nkubectl get cluster-kubeconfig $NAME -o yaml | yq -y .spec.kubeconfig */ + spec?: { + /** @description ClusterKubeconfigData stores the kubeconfig data ready to use kubectl or other local tooling\nIt is a simplified version of clientcmdapi.Config: https://pkg.go.dev/k8s.io/client-go/tools/clientcmd/api#Config */ + kubeconfig?: { + apiVersion?: string; + clusters?: { + cluster: { + /** Format: byte */ + "certificate-authority-data"?: string; + server?: string; + }; + name: string; + }[]; + contexts: { + context?: { + cluster: string; + namespace?: string; + user: string; + }; + name: string; + }[]; + "current-context"?: string; + kind?: string; + preferences?: Record; + users: { + name: string; + user?: { + /** @description AuthProviderConfig holds the configuration for a specified auth provider. */ + "auth-provider"?: { + config?: { + [key: string]: string; + }; + name: string; + }; + /** Format: byte */ + "client-certificate-data"?: string; + /** Format: byte */ + "client-key-data"?: string; + }; + }[]; + }; + }; + }; /** - * Format: date-time - * @description BearerTokenExpirationTimestamp reflects the expiration timestamp of the bearer token used to access the cluster. + * Cluster + * @description Cluster is the Schema for the clusters API */ - bearerTokenExpirationTimestamp?: string; - /** @description HeadScaleStatus contains the current status of the headscale client. */ - headScaleStatus?: { - /** Format: date-time */ - createdAt?: string; - /** Format: date-time */ - expiry?: string; - forcedTags?: string[]; - /** Format: int64 */ - id?: number; - ipAddresses?: string[]; - name?: string; - online?: boolean; - /** @description PreAuthKey reflects the status of the pre-authentication key used by the Headscale machine. */ - preAuthKey?: { - /** Format: date-time */ - createdAt?: string; - ephemeral?: boolean; - /** Format: date-time */ - expiration?: string; - id?: string; - reusable?: boolean; - used?: boolean; - user?: string; - }; + Cluster: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description ClusterSpec defines the desired state of the Cluster. */ + spec?: { + /** + * @description AccessMode configures how the cluster is accessed from the Greenhouse operator. + * @enum {string} + */ + accessMode: "direct" | "headscale"; + }; + /** @description ClusterStatus defines the observed state of Cluster */ + status?: { + /** + * Format: date-time + * @description BearerTokenExpirationTimestamp reflects the expiration timestamp of the bearer token used to access the cluster. + */ + bearerTokenExpirationTimestamp?: string; + /** @description HeadScaleStatus contains the current status of the headscale client. */ + headScaleStatus?: { + /** Format: date-time */ + createdAt?: string; + /** Format: date-time */ + expiry?: string; + forcedTags?: string[]; + /** Format: int64 */ + id?: number; + ipAddresses?: string[]; + name?: string; + online?: boolean; + /** @description PreAuthKey reflects the status of the pre-authentication key used by the Headscale machine. */ + preAuthKey?: { + /** Format: date-time */ + createdAt?: string; + ephemeral?: boolean; + /** Format: date-time */ + expiration?: string; + id?: string; + reusable?: boolean; + used?: boolean; + user?: string; + }; + }; + /** @description KubernetesVersion reflects the detected Kubernetes version of the cluster. */ + kubernetesVersion?: string; + /** @description Nodes provides a map of cluster node names to node statuses */ + nodes?: { + [key: string]: { + /** @description Fast track to the node ready condition. */ + ready?: boolean; + /** @description We mirror the node conditions here for faster reference */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }[]; + }; + }; + }; + /** @description StatusConditions contain the different conditions that constitute the status of the Cluster. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }[]; + }; + }; }; - /** @description KubernetesVersion reflects the detected Kubernetes version of the cluster. */ - kubernetesVersion?: string; - /** @description Nodes provides a map of cluster node names to node statuses */ - nodes?: { - [key: string]: { - /** @description Fast track to the node ready condition. */ - ready?: boolean; - /** @description We mirror the node conditions here for faster reference */ - statusConditions?: { - conditions?: { - /** - * Format: date-time - * @description LastTransitionTime is the last time the condition transitioned from one status to another. - */ - lastTransitionTime: string; - /** @description Message is an optional human readable message indicating details about the last transition. */ - message?: string; - /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ - reason?: string; - /** @description Status of the condition. */ - status: string; - /** @description Type of the condition. */ - type: string; + /** + * TeamRole + * @description TeamRole is the Schema for the TeamRoles API + */ + TeamRole: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description TeamRoleSpec defines the desired state of a TeamRole */ + spec?: { + /** @description Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role */ + rules?: { + /** @description APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of\nthe enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. */ + apiGroups?: string[]; + /** @description NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path\nSince non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding.\nRules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. */ + nonResourceURLs?: string[]; + /** @description ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. */ + resourceNames?: string[]; + /** @description Resources is a list of resources this rule applies to. '*' represents all resources. */ + resources?: string[]; + /** @description Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. */ + verbs: string[]; }[]; }; - }; - }; - /** @description StatusConditions contain the different conditions that constitute the status of the Cluster. */ - statusConditions?: { - conditions?: { - /** - * Format: date-time - * @description LastTransitionTime is the last time the condition transitioned from one status to another. - */ - lastTransitionTime: string; - /** @description Message is an optional human readable message indicating details about the last transition. */ - message?: string; - /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ - reason?: string; - /** @description Status of the condition. */ - status: string; - /** @description Type of the condition. */ - type: string; - }[]; + /** @description TeamRoleStatus defines the observed state of a TeamRole */ + status?: Record; }; - }; - }; - /** - * PluginDefinition - * @description PluginDefinition is the Schema for the PluginDefinitions API - */ - PluginDefinition: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; - }; - }; - /** @description PluginDefinitionSpec defines the desired state of PluginDefinitionSpec */ - spec?: { - /** @description Description provides additional details of the pluginDefinition. */ - description?: string; - /** @description DisplayName provides a human-readable label for the pluginDefinition. */ - displayName?: string; - /** @description DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin. Source needs to allow all CORS origins. */ - docMarkDownUrl?: string; - /** @description HelmChart specifies where the Helm Chart for this pluginDefinition can be found. */ - helmChart?: { - /** @description Name of the HelmChart chart. */ - name: string; - /** @description Repository of the HelmChart chart. */ - repository: string; - /** @description Version of the HelmChart chart. */ - version: string; - }; - /** @description Icon specifies the icon to be used for this plugin in the Greenhouse UI. Icons can be either: - A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52 - A publicly accessable image reference to a .png file. Will be displayed 100x100px */ - icon?: string; - /** @description RequiredValues is a list of values required to create an instance of this PluginDefinition. */ - options?: ({ - /** @description Default provides a default value for the option */ - default?: unknown; - /** @description Description provides a human-readable text for the value as shown in the UI. */ - description?: string; - /** @description DisplayName provides a human-readable label for the configuration option */ - displayName?: string; - /** @description Name/Key of the config option. */ - name: string; - /** @description Regex specifies a match rule for validating configuration options. */ - regex?: string; - /** @description Required indicates that this config option is required */ - required: boolean; - /** - * @description Type of this configuration option. - * @enum {string} - */ - type: "string" | "secret" | "bool" | "int" | "list" | "map"; - })[]; - /** @description UIApplication specifies a reference to a UI application */ - uiApplication?: { - /** @description Name of the UI application. */ - name: string; - /** @description URL specifies the url to a built javascript asset. By default, assets are loaded from the Juno asset server using the provided name and version. */ - url?: string; - /** @description Version of the frontend application. */ - version: string; - }; - /** @description Version of this pluginDefinition */ - version: string; /** - * Format: int32 - * @description Weight configures the order in which Plugins are shown in the Greenhouse UI. Defaults to alphabetical sorting if not provided or on conflict. + * TeamMembership + * @description TeamMembership is the Schema for the teammemberships API */ - weight?: number; - }; - /** @description PluginDefinitionStatus defines the observed state of PluginDefinition */ - status?: Record; - }; - /** - * Team - * @description Team is the Schema for the teams API - */ - Team: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; + TeamMembership: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description TeamMembershipSpec defines the desired state of TeamMembership */ + spec?: { + /** @description Members list users that are part of a team. */ + members?: { + /** @description Email of the user. */ + email: string; + /** @description FirstName of the user. */ + firstName: string; + /** @description ID is the unique identifier of the user. */ + id: string; + /** @description LastName of the user. */ + lastName: string; + }[]; + }; + /** @description TeamMembershipStatus defines the observed state of TeamMembership */ + status?: { + /** + * Format: date-time + * @description LastSyncedTime is the information when was the last time the membership was synced + */ + lastSyncedTime?: string; + /** + * Format: date-time + * @description LastChangedTime is the information when was the last time the membership was actually changed + */ + lastUpdateTime?: string; + }; }; - }; - /** @description TeamSpec defines the desired state of Team */ - spec?: { - /** @description Description provides additional details of the team. */ - description?: string; - /** @description IdP group id matching team. */ - mappedIdPGroup?: string; - }; - /** @description TeamStatus defines the observed state of Team */ - status?: Record; - }; - /** - * TeamMembership - * @description TeamMembership is the Schema for the teammemberships API - */ - TeamMembership: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; + /** + * PluginPreset + * @description PluginPreset is the Schema for the PluginPresets API + */ + PluginPreset: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description PluginPresetSpec defines the desired state of PluginPreset */ + spec?: { + /** @description ClusterSelector is a label selector to select the clusters the plugin bundle should be deployed to. */ + clusterSelector: { + /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ + matchExpressions?: { + /** @description key is the label key that the selector applies to. */ + key: string; + /** @description operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. */ + operator: string; + /** @description values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. */ + values?: string[]; + }[]; + /** @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. */ + matchLabels?: { + [key: string]: string; + }; + }; + /** @description PluginSpec is the spec of the plugin to be deployed by the PluginPreset. */ + plugin: { + /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ + clusterName?: string; + /** @description Disabled indicates that the plugin is administratively disabled. */ + disabled: boolean; + /** @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. */ + displayName?: string; + /** @description Values are the values for a PluginDefinition instance. */ + optionValues?: { + /** @description Name of the values. */ + name: string; + /** @description Value is the actual value in plain text. */ + value?: unknown; + /** @description ValueFrom references a potentially confidential value in another source. */ + valueFrom?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string; + /** @description Name of the secret in the same namespace. */ + name: string; + }; + }; + }[]; + /** @description PluginDefinition is the name of the PluginDefinition this instance is for. */ + pluginDefinition: string; + /** @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. */ + releaseNamespace?: string; + }; + }; + /** @description PluginPresetStatus defines the observed state of PluginPreset */ + status?: { + /** @description StatusConditions contain the different conditions that constitute the status of the PluginPreset. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }[]; + }; + }; }; - annotations?: { - [key: string]: string; + /** + * Team + * @description Team is the Schema for the teams API + */ + Team: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description TeamSpec defines the desired state of Team */ + spec?: { + /** @description Description provides additional details of the team. */ + description?: string; + /** @description IdP group id matching team. */ + mappedIdPGroup?: string; + }; + /** @description TeamStatus defines the observed state of Team */ + status?: Record; }; - }; - /** @description TeamMembershipSpec defines the desired state of TeamMembership */ - spec?: { - /** @description Members list users that are part of a team. */ - members?: { - /** @description Email of the user. */ - email: string; - /** @description FirstName of the user. */ - firstName: string; - /** @description ID is the unique identifier of the user. */ - id: string; - /** @description LastName of the user. */ - lastName: string; - }[]; - }; - /** @description TeamMembershipStatus defines the observed state of TeamMembership */ - status?: { /** - * Format: date-time - * @description LastSyncedTime is the information when was the last time the membership was synced + * TeamRoleBinding + * @description TeamRoleBinding is the Schema for the rolebindings API */ - lastSyncedTime?: string; + TeamRoleBinding: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description TeamRoleBindingSpec defines the desired state of a TeamRoleBinding */ + spec?: { + /** @description ClusterName is the name of the cluster the rbacv1 resources are created on. */ + clusterName?: string; + /** @description ClusterSelector is a label selector to select the Clusters the TeamRoleBinding should be deployed to. */ + clusterSelector?: { + /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ + matchExpressions?: { + /** @description key is the label key that the selector applies to. */ + key: string; + /** @description operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. */ + operator: string; + /** @description values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. */ + values?: string[]; + }[]; + /** @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. */ + matchLabels?: { + [key: string]: string; + }; + }; + /** @description Namespaces is the immutable list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.\nIf empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace. */ + namespaces?: string[]; + /** @description TeamRef references a Greenhouse Team by name */ + teamRef?: string; + /** @description TeamRoleRef references a Greenhouse TeamRole by name */ + teamRoleRef?: string; + }; + /** @description TeamRoleBindingStatus defines the observed state of the TeamRoleBinding */ + status?: { + /** @description PropagationStatus is the list of clusters the TeamRoleBinding is applied to */ + clusters?: { + /** @description ClusterName is the name of the cluster the rbacv1 resources are created on. */ + clusterName: string; + /** @description Condition is the overall Status of the rbacv1 resources created on the cluster */ + condition?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }; + }[]; + /** @description StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }[]; + }; + }; + }; /** - * Format: date-time - * @description LastChangedTime is the information when was the last time the membership was actually changed + * Plugin + * @description Plugin is the Schema for the plugins API */ - lastUpdateTime?: string; - }; - }; - /** - * TeamRoleBinding - * @description TeamRoleBinding is the Schema for the rolebindings API - */ - TeamRoleBinding: { - /** @description APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; + Plugin: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description PluginSpec defines the desired state of Plugin */ + spec?: { + /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ + clusterName?: string; + /** @description Disabled indicates that the plugin is administratively disabled. */ + disabled: boolean; + /** @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. */ + displayName?: string; + /** @description Values are the values for a PluginDefinition instance. */ + optionValues?: { + /** @description Name of the values. */ + name: string; + /** @description Value is the actual value in plain text. */ + value?: unknown; + /** @description ValueFrom references a potentially confidential value in another source. */ + valueFrom?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string; + /** @description Name of the secret in the same namespace. */ + name: string; + }; + }; + }[]; + /** @description PluginDefinition is the name of the PluginDefinition this instance is for. */ + pluginDefinition: string; + /** @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. */ + releaseNamespace?: string; + }; + /** @description PluginStatus defines the observed state of Plugin */ + status?: { + /** @description Description provides additional details of the plugin. */ + description?: string; + /** @description ExposedServices provides an overview of the Plugins services that are centrally exposed.\nIt maps the exposed URL to the service found in the manifest. */ + exposedServices?: { + [key: string]: { + /** @description Name is the name of the service in the target cluster. */ + name: string; + /** @description Namespace is the namespace of the service in the target cluster. */ + namespace: string; + /** + * Format: int32 + * @description Port is the port of the service. + */ + port: number; + /** @description Protocol is the protocol of the service. */ + protocol?: string; + }; + }; + /** @description HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. */ + helmChart?: { + /** @description Name of the HelmChart chart. */ + name: string; + /** @description Repository of the HelmChart chart. */ + repository: string; + /** @description Version of the HelmChart chart. */ + version: string; + }; + /** @description HelmReleaseStatus reflects the status of the latest HelmChart release.\nThis is only configured if the pluginDefinition is backed by HelmChart. */ + helmReleaseStatus?: { + /** + * Format: date-time + * @description FirstDeployed is the timestamp of the first deployment of the release. + */ + firstDeployed?: string; + /** + * Format: date-time + * @description LastDeployed is the timestamp of the last deployment of the release. + */ + lastDeployed?: string; + /** @description Status is the status of a HelmChart release. */ + status: string; + }; + /** @description StatusConditions contain the different conditions that constitute the status of the Plugin. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }[]; + }; + /** @description UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. */ + uiApplication?: { + /** @description Name of the UI application. */ + name: string; + /** @description URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. */ + url?: string; + /** @description Version of the frontend application. */ + version: string; + }; + /** @description Version contains the latest pluginDefinition version the config was last applied with successfully. */ + version?: string; + /** + * Format: int32 + * @description Weight configures the order in which Plugins are shown in the Greenhouse UI. + */ + weight?: number; + }; }; - annotations?: { - [key: string]: string; + /** + * PluginDefinition + * @description PluginDefinition is the Schema for the PluginDefinitions API + */ + PluginDefinition: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description PluginDefinitionSpec defines the desired state of PluginDefinitionSpec */ + spec?: { + /** @description Description provides additional details of the pluginDefinition. */ + description?: string; + /** @description DisplayName provides a human-readable label for the pluginDefinition. */ + displayName?: string; + /** @description DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin.\nSource needs to allow all CORS origins. */ + docMarkDownUrl?: string; + /** @description HelmChart specifies where the Helm Chart for this pluginDefinition can be found. */ + helmChart?: { + /** @description Name of the HelmChart chart. */ + name: string; + /** @description Repository of the HelmChart chart. */ + repository: string; + /** @description Version of the HelmChart chart. */ + version: string; + }; + /** @description Icon specifies the icon to be used for this plugin in the Greenhouse UI.\nIcons can be either:\n- A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52\n- A publicly accessible image reference to a .png file. Will be displayed 100x100px */ + icon?: string; + /** @description RequiredValues is a list of values required to create an instance of this PluginDefinition. */ + options?: { + /** @description Default provides a default value for the option */ + default?: unknown; + /** @description Description provides a human-readable text for the value as shown in the UI. */ + description?: string; + /** @description DisplayName provides a human-readable label for the configuration option */ + displayName?: string; + /** @description Name/Key of the config option. */ + name: string; + /** @description Regex specifies a match rule for validating configuration options. */ + regex?: string; + /** @description Required indicates that this config option is required */ + required: boolean; + /** + * @description Type of this configuration option. + * @enum {string} + */ + type: "string" | "secret" | "bool" | "int" | "list" | "map"; + }[]; + /** @description UIApplication specifies a reference to a UI application */ + uiApplication?: { + /** @description Name of the UI application. */ + name: string; + /** @description URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. */ + url?: string; + /** @description Version of the frontend application. */ + version: string; + }; + /** @description Version of this pluginDefinition */ + version: string; + /** + * Format: int32 + * @description Weight configures the order in which Plugins are shown in the Greenhouse UI.\nDefaults to alphabetical sorting if not provided or on conflict. + */ + weight?: number; + }; + /** @description PluginDefinitionStatus defines the observed state of PluginDefinition */ + status?: Record; }; - }; - /** @description TeamRoleBindingSpec defines the desired state of a TeamRoleBinding */ - spec?: { - /** @description ClusterName is the name of the cluster the rbacv1 resources are created on. */ - clusterName?: string; - /** @description Namespaces is the immutable list of namespaces in the Greenhouse Clusters to apply the RoleBinding to */ - namespaces?: string[]; - /** @description TeamRef references a Greenhouse Team by name */ - teamRef?: string; - /** @description TeamRoleRef references a Greenhouse TeamRole by name */ - teamRoleRef?: string; - }; - /** @description TeamRoleBindingStatus defines the observed state of the TeamRoleBinding */ - status?: Record; }; - }; - responses: never; - parameters: never; - requestBodies: never; - headers: never; - pathItems: never; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; } - export type $defs = Record; - -export type external = Record; - export type operations = Record;