diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..7457664 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,16 @@ +--- +name: Bug Report +about: Report a bug +labels: kind/bug + +--- + +**What happened**: + +**What you expected to happen**: + +**How to reproduce it (as minimally and precisely as possible)**: + +**Anything else we need to know**: + +**Environment**: diff --git a/.github/ISSUE_TEMPLATE/enhancement_request.md b/.github/ISSUE_TEMPLATE/enhancement_request.md new file mode 100644 index 0000000..4179e17 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/enhancement_request.md @@ -0,0 +1,10 @@ +--- +name: Enhancement Request +about: Suggest an enhancement +labels: kind/enhancement + +--- + +**What would you like to be added**: + +**Why is this needed**: diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4d04bfc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,6 @@ +**What this PR does / why we need it**: + +**Which issue(s) this PR fixes**: +Fixes # + +**Special notes for your reviewer**: diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..dd5a886 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,33 @@ +name: ci +on: + push: + tags: + - v* + branches: + - master + - main + pull_request: + +jobs: + build: + runs-on: ubuntu-24.04 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: make tidy + run: | + make tidy + git diff --exit-code + + - name: make verify + run: make verify + + - name: make test + run: make test \ No newline at end of file diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..8e043a5 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,87 @@ +name: Versioned Release + +on: + push: + branches: + - main + +permissions: + contents: write # we need this to be able to push tags + +jobs: + release_tag: + name: Release version + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ssh-key: ${{ secrets.PUSH_KEY }} + fetch-tags: true + fetch-depth: 0 + + - name: Read and validate VERSION + id: version + run: | + VERSION=$(cat VERSION) + if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-dev)?$ ]]; then + echo "Invalid version format in VERSION file: $VERSION" + exit 1 + fi + echo "New version: $VERSION" + echo "version=$VERSION" >> $GITHUB_ENV + + - name: Skip release if version is a dev version + if: contains(env.version, '-dev') + run: | + echo "Skipping development version release: ${{ env.version }}" + echo "SKIP=true" >> $GITHUB_ENV + exit 0 + + - name: Check if VERSION is already tagged + id: check_tag + run: | + if git rev-parse "refs/tags/${{ env.version }}" >/dev/null 2>&1; then + echo "Tag ${{ env.version }} already exists. Skipping release." + echo "SKIP=true" >> $GITHUB_ENV + exit 0 + fi + echo "Tag ${{ env.version }} doesn't exists. Proceeding with release." + + - name: Create Git tag + if: ${{ env.SKIP != 'true' }} + run: | + AUTHOR_NAME=$(git log -1 --pretty=format:'%an') + AUTHOR_EMAIL=$(git log -1 --pretty=format:'%ae') + echo "Tagging as $AUTHOR_NAME <$AUTHOR_EMAIL>" + + echo "AUTHOR_NAME=$AUTHOR_NAME" >> $GITHUB_ENV + echo "AUTHOR_EMAIL=$AUTHOR_EMAIL" >> $GITHUB_ENV + + git config user.name "$AUTHOR_NAME" + git config user.email "$AUTHOR_EMAIL" + + git tag -a "${{ env.version }}" -m "Release ${{ env.version }}" + git push origin "${{ env.version }}" + + - name: Create GitHub release + if: ${{ env.SKIP != 'true' }} + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ env.version }} + name: Release ${{ env.version }} + body: "Automated release for version ${{ env.version }}" + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Push dev VERSION + if: ${{ env.SKIP != 'true' }} + run: | + echo "${{ env.version }}-dev" > VERSION + git config user.name "${{ env.AUTHOR_NAME }}" + git config user.email "${{ env.AUTHOR_EMAIL }}" + git add VERSION + git commit -m "Update VERSION to ${{ env.version }}-dev" + git push origin main diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e56bb46 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ + +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +bin/ +Dockerfile.cross + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Kubernetes Generated files - skip generated files, except for vendored files + +!vendor/**/zz_generated.* + +# editor and IDE paraphernalia +.idea +.vscode +*.swp +*.swo +*~ + +tmp +go.work* +components/ +!**/components/ +/**/cover.html +/**/cover.*.html + +*.tmp diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..083aa42 --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,8 @@ +run: + concurrency: 4 + timeout: 10m + +issues: + exclude-files: + - "zz_generated.*\\.go$" + - "tmp/.*" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7f0bf35..815443e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Code of Conduct -All members of the project community must abide by the [SAP Open Source Code of Conduct](https://github.com/SAP/.github/blob/main/CODE_OF_CONDUCT.md). +All members of the project community must abide by the [SAP Open Source Code of Conduct](https://github.com/openmcp-project/.github/blob/main/CODE_OF_CONDUCT.md). Only by respecting each other we can develop a productive, collaborative community. Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at [ospo@sap.com](mailto:ospo@sap.com) (SAP Open Source Program Office). All complaints will be reviewed and investigated promptly and fairly. diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..408d154 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +# NOTE: This Dockerfile is used by the pipeline, but not for the 'make image' command, which uses the Dockerfile template in hack/common instead. +# Use distroless as minimal base image to package the component binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot@sha256:6ec5aa99dc335666e79dc64e4a6c8b89c33a543a1967f20d360922a80dd21f02 +ARG TARGETOS +ARG TARGETARCH +ARG COMPONENT +WORKDIR / +COPY bin/$COMPONENT-$TARGETOS.$TARGETARCH /$COMPONENT +USER nonroot:nonroot + +ENTRYPOINT ["/$COMPONENT"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..68b3803 --- /dev/null +++ b/Makefile @@ -0,0 +1,165 @@ +PROJECT_FULL_NAME := mcp-operator +REPO_ROOT := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) +EFFECTIVE_VERSION := $(shell $(REPO_ROOT)/hack/common/get-version.sh) + +COMMON_MAKEFILE ?= $(REPO_ROOT)/hack/common/Makefile +ifneq (,$(wildcard $(COMMON_MAKEFILE))) +include $(COMMON_MAKEFILE) +endif + +# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set) +ifeq (,$(shell go env GOBIN)) +GOBIN=$(shell go env GOPATH)/bin +else +GOBIN=$(shell go env GOBIN) +endif + +# CONTAINER_TOOL defines the container tool to be used for building images. +# Be aware that the target commands are only tested with Docker which is +# scaffolded by default. However, you might want to replace it to use other +# tools. (i.e. podman) +CONTAINER_TOOL ?= docker + +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +COMPONENTS ?= mcp-operator +API_CODE_DIRS := $(REPO_ROOT)/api/constants/... $(REPO_ROOT)/api/errors/... $(REPO_ROOT)/api/install/... $(REPO_ROOT)/api/v1alpha1/... $(REPO_ROOT)/api/core/v1alpha1/... +ROOT_CODE_DIRS := $(REPO_ROOT)/cmd/... $(REPO_ROOT)/internal/... $(REPO_ROOT)/test/... + +##@ General + +ifndef HELP_TARGET +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +endif + +##@ Development + +.PHONY: manifests +manifests: controller-gen ## Generate CustomResourceDefinition objects. + @echo "> Remove existing CRD manifests" + rm -rf config/crd/bases/ + rm -rf config/webhook/manifests/ + rm -rf api/crds/manifests/ + @echo "> Generating CRD Manifests" + @$(CONTROLLER_GEN) crd paths="$(REPO_ROOT)/api/core/v1alpha1/..." output:crd:artifacts:config=config/crd/bases + @$(CONTROLLER_GEN) crd paths="$(REPO_ROOT)/api/core/v1alpha1/..." output:crd:artifacts:config=api/crds/manifests + @$(CONTROLLER_GEN) webhook paths="$(REPO_ROOT)/api/..." + +.PHONY: generate +generate: generate-code manifests generate-docs format ## Generates code (DeepCopy stuff, CRDs), documentation index, and runs formatter. + +.PHONY: generate-code +generate-code: controller-gen ## Generate code containing DeepCopy, DeepCopyInto, and DeepCopyObject method implementations. Also fetches external APIs. + @echo "> Fetching External APIs" + @go run $(REPO_ROOT)/hack/external-apis/main.go + @echo "> Generating DeepCopy Methods" + @$(CONTROLLER_GEN) object paths="$(REPO_ROOT)/api/core/v1alpha1/..." + +.PHONY: format +format: goimports ## Formats the imports. + @FORMATTER=$(FORMATTER) $(REPO_ROOT)/hack/common/format.sh $(API_CODE_DIRS) $(ROOT_CODE_DIRS) + +.PHONY: verify +verify: golangci-lint jq goimports ## Runs linter, 'go vet', and checks if the formatter has been run. + @test "$(SKIP_DOCS_INDEX_CHECK)" = "true" || \ + ( echo "> Verify documentation index ..." && \ + JQ=$(JQ) $(REPO_ROOT)/hack/common/verify-docs-index.sh ) + @( echo "> Verifying api module ..." && \ + pushd $(REPO_ROOT)/api &>/dev/null && \ + go vet $(API_CODE_DIRS) && \ + $(LINTER) run -c $(REPO_ROOT)/.golangci.yaml $(API_CODE_DIRS) && \ + popd &>/dev/null ) + @( echo "> Verifying root module ..." && \ + pushd $(REPO_ROOT) &>/dev/null && \ + go vet $(ROOT_CODE_DIRS) && \ + $(LINTER) run -c $(REPO_ROOT)/.golangci.yaml $(ROOT_CODE_DIRS) && \ + popd &>/dev/null ) + @test "$(SKIP_FORMATTING_CHECK)" = "true" || \ + ( echo "> Checking for unformatted files ..." && \ + FORMATTER=$(FORMATTER) $(REPO_ROOT)/hack/common/format.sh --verify $(API_CODE_DIRS) $(ROOT_CODE_DIRS) ) + +.PHONY: test +test: #envtest ## Run tests. +# KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) --bin-dir $(LOCALBIN) -p path)" go test $(ROOT_CODE_DIRS) -coverprofile cover.out + @( echo "> Test root module ..." && \ + pushd $(REPO_ROOT) &>/dev/null && \ + go test $(ROOT_CODE_DIRS) -coverprofile cover.root.out && \ + go tool cover --html=cover.root.out -o cover.root.html && \ + go tool cover -func cover.root.out | tail -n 1 && \ + popd &>/dev/null ) + + @( echo "> Test api module ..." && \ + pushd $(REPO_ROOT)/api &>/dev/null && \ + go test $(API_CODE_DIRS) -coverprofile cover.api.out && \ + go tool cover --html=cover.api.out -o cover.api.html && \ + go tool cover -func cover.api.out | tail -n 1 && \ + popd &>/dev/null ) + +##@ Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(REPO_ROOT)/bin + +# Tool Binaries +ENVTEST ?= $(LOCALBIN)/setup-envtest + +# Tool Versions +SETUP_ENVTEST_VERSION ?= release-0.16 + +ifndef LOCALBIN_TARGET +.PHONY: localbin +localbin: + @test -d $(LOCALBIN) || mkdir -p $(LOCALBIN) +endif + +.PHONY: envtest +envtest: localbin ## Download envtest-setup locally if necessary. + @test -s $(LOCALBIN)/setup-envtest && test -s $(LOCALBIN)/setup-envtest_version && cat $(LOCALBIN)/setup-envtest_version | grep -q $(SETUP_ENVTEST_VERSION) || \ + ( echo "Installing setup-envtest $(SETUP_ENVTEST_VERSION) ..."; \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-runtime/tools/setup-envtest@$(SETUP_ENVTEST_VERSION) && \ + echo $(SETUP_ENVTEST_VERSION) > $(LOCALBIN)/setup-envtest_version ) + +##@ Local Setup + +DISABLE_AUTHENTICATION ?= true +DISABLE_AUTHORIZATION ?= true +DISABLE_CLOUDORCHESTRATOR ?= true +DISABLE_MANAGEDCONTROLPLANE ?= false +DISABLE_APISERVER ?= true +DISABLE_LANDSCAPER ?= true +LOCAL_GOARCH ?= $(shell go env GOARCH) + +.PHONY: dev-local +dev-local: dev-clean image-build-local dev-cluster load-image helm-install-local ## All-in-one command for creating a fresh local setup. + +.PHONY: dev-clean +dev-clean: ## Removes the kind cluster for local setup. + $(KIND) delete cluster --name=$(PROJECT_FULL_NAME)-dev + +.PHONY: dev-cluster +dev-cluster: ## Creates a kind cluster for running a local setup. + $(KIND) create cluster --name=$(PROJECT_FULL_NAME)-dev + +.PHONY: load-image +load-image: ## Loads the image into the local setup kind cluster. + $(KIND) load docker-image local/mcp-operator:${EFFECTIVE_VERSION}-linux-$(LOCAL_GOARCH) --name=$(PROJECT_FULL_NAME)-dev + +.PHONY: helm-install-local +helm-install-local: ## Installs the MCP Operator into the local setup kind cluster by using its helm chart. + helm upgrade --install $(PROJECT_FULL_NAME) charts/$(PROJECT_FULL_NAME)/ --set image.repository=local/mcp-operator --set image.tag=${EFFECTIVE_VERSION}-linux-$(LOCAL_GOARCH) --set image.pullPolicy=Never \ + --set authentication.disabled=$(DISABLE_AUTHENTICATION) \ + --set authorization.disabled=$(DISABLE_AUTHORIZATION) \ + --set cloudOrchestrator.disabled=$(DISABLE_CLOUDORCHESTRATOR) \ + --set managedcontrolplane.disabled=$(DISABLE_MANAGEDCONTROLPLANE) \ + --set apiserver.disabled=$(DISABLE_APISERVER) \ + --set landscaper.disabled=$(DISABLE_LANDSCAPER) + +.PHONY: install +install: manifests ## Install CRDs into the K8s cluster specified in ~/.kube/config (or $KUBECONFIG). Usually not required, as the MCP Operator installs the CRDs on its own. + $(KUBECTL) apply -f config/crd/bases + diff --git a/PROJECT b/PROJECT new file mode 100644 index 0000000..adf1eb3 --- /dev/null +++ b/PROJECT @@ -0,0 +1,68 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html +domain: openmcp.cloud +layout: +- go.kubebuilder.io/v4 +multigroup: true +projectName: mcp-operator +repo: github.com/openmcp-project/mcp-operator +resources: +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openmcp.cloud + group: core + kind: APIServer + path: github.com/openmcp-project/mcp-operator/api/core/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openmcp.cloud + group: core + kind: CloudOrchestrator + path: github.com/openmcp-project/mcp-operator/api/core/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openmcp.cloud + group: core + kind: Landscaper + path: github.com/openmcp-project/mcp-operator/api/core/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openmcp.cloud + group: core + kind: ManagedControlPlane + path: github.com/openmcp-project/mcp-operator/api/core/v1alpha1 + version: v1alpha1 + webhooks: + validation: true + webhookVersion: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: openmcp.cloud + group: core + kind: InternalConfiguration + path: github.com/openmcp-project/mcp-operator/api/core/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + controller: true + domain: openmcp.cloud + group: core + kind: ManagedComponent + path: github.com/openmcp-project/mcp-operator/api/core/v1alpha1 + version: v1alpha1 +version: "3" diff --git a/README.md b/README.md index 2006d49..9925202 100644 --- a/README.md +++ b/README.md @@ -4,11 +4,139 @@ ## About this project -This repository contains the controllers which reconcile ManagedControlPlane resources. It is part of the onboarding system. +This repository contains the controllers which reconcile ManagedControlPlane resources. -## Requirements and Setup +This is a quickly done documentation of the contract between the ManagedControlPlane controller and the component controllers. Needs to be properly expanded in the future. + +### Component Controllers + +Component controllers have to follow a specific contract in order to work together with the rest of the onboarding system. Below, the general reconciliation flow that a component controller should follow is quickly summarized, then some aspects are explained in more detail. + +#### Reconciliation Flow + +- Fetch resource from cluster. +- Check for operation annotations and act accordingly. +- If the component has dependencies, fetch the corresponding resources. +- Check if resource is in deletion and proceed with the corresponding logic. +- Update component status (unless the resource was deleted). + - Note that the status should also be updated in case of an error, so that the failure is visible to operators and the customer. + +##### Create/Update + +- Ensure finalizer on own component resource. +- Ensure dependency finalizers on all resources of components depended on. +- Perform actual component logic. + +##### Delete + +- Check if there are any dependency finalizers on the own resource. If so, update the status accordingly and requeue the resource to wait until all dependency finalizers have been removed. Do not proceed with any actual deletion logic if there are dependency finalizers left! +- Perform actual component deletion logic. +- If the deletion has to wait for something: + - Update status accordingly and requeue resource. +- If the deletion is finished: + - Remove own dependency finalizers from all resources of components depended on. + - Remove own finalizer from own resource. This should result in the resource being deleted. + +#### No-Gos for Component Controllers + +This is a (incomplete) list of things that component controller **must not** do: + +- Modify any component resource's spec, including their own one. + - The 'ground truth' for the spec of a component resource is the `ManagedControlPlane`, from which it is generated. +- Read or modify the owning `ManagedControlPlane`. + - Component resources are expected to be self-contained. It should never be necessary to even fetch the owning `ManagedControlPlane` resource, and under no circumstances should it be modified by a component's controller. The `ManagedControlPlane`'s spec is configured by the customer, its `status` is managed by the ManagedControlPlane Controller (which also fetches the component resources' status on its own). + - Reading the `ManagedControlPlane`'s status to check the conditions of other components for dependency reasons would be ok, but if a component depends on another one, it will likely need some information only available on the component's resource (not exported into the `ManagedControlPlane`'s status) anyway and should therefore fetch that component directly. + - All component resources generated a `ManagedControlPlane` share its name and namespace. The `componentutils` package has helper functions to easily fetch resources belonging to other components. +- Expose secret/internal information in a component's condition or external status. + - The external part of a component's status as well as its conditions will be visible to the customer who created the `ManagedControlPlane`. + +#### Reacting to Changes + +Component controllers are expected to reconcile their own resource if + +- the resource's spec changes +- the resource's labels change +- an operation annotation with value `reconcile` was added to the resource +- a deletion timestamp was added to the resource + +Apart from that, there is usually no need to react to other changes to the resource. +The `componentutils` package provides some event filters that can be passed into the `ControllerBuilder` to apply corresponding filtering rules. The `DefaultComponentControllerPredicates(...)` function should work as a default filter configuration for most component controllers. + +#### Annotations + +The `componentutils` package contains a `PatchAnnotation` function to easily add or remove an annotation to/from a resource. + +##### The Operation Annotation + +The annotation `openmcp.cloud/operation` (available as constant `OperationAnnotation` in the `types` repo) can be used to control the behavior of the controller responsible for reconciling the annotated resource. Currently, two values are supported: + +- **reconcile** means the resource should be reconciled as if it was changed. The corresponding controller is expected to remove the annotation and perform the reconciliation. +- **ignore** means that the resource should be ignored. The corresponding controller (and all other ones touching the resource) is expected to treat this resource as if it didn't exist. It must not remove the annotation or change the resource in any way. + +#### Finalizers + +The component's controller is expected to put a finalizer onto the component's resource. The finalizer should follow the format `openmcp.cloud.`, e.g. `openmcp.cloud.apiserver` for the `APIServer` component. The `ComponentType` type has a `Finalizer()` method that returns the finalizer for a given component type. + +When depending on another component, the depending component's controller is expected to add a dependency finalizer to the required other component. It can fetch the component's resource using this snippet (using a dependency towards the `APIServer` component as an example): + +```golang + // r is the Reconciler struct + // obj is the currently reconciled component resource + ownCPGeneration, ownICGeneration, _ := componentutils.GetCreatedFromGeneration(obj) + apiServerComp, err := componentutils.GetComponent(ctx, r.Client, openmcpv1alpha1.APIServerComponent, obj.Name, obj.Namespace) + if err != nil { + return ctrl.Result{}, fmt.Errorf("error checking for APIServer dependency component: %w", err) + } + if apiServerComp == nil || !componentutils.IsDependencyReady(apiServerComp.Condition(), ownCPGeneration, ownICGeneration) { + log.Debug("APIServer not found or it isn't ready") + // TODO: update own status and requeue for retry + } + as, ok := apiServerComp.(*openmcpv1alpha1.APIServer) + if !ok { + panic("resource of APIServer component is not a APIServer") + } +``` + +Dependency finalizers have the format `dependency.openmcp.cloud/` and can be generated from a `ComponentType` by using its `DependencyFinalizer()` method. + +The `componentutils` package contains several helper functions to deal with dependencies. + +#### Conditions + +Each component resource's status is expected to contain at least one condition displaying the current state of the component. Each condition must have a **globally unique** identifier (because all of them are merged in the `ManagedControlPlane`'s status) and a status that must be either `True`, `False`, or `Unknown`. For any condition with a non-`True` status, the `reason` and `message` fields should be set to provide error messages or other information about why the condition is not `True`. The `reason` field is expected to contain a enum-like, CamelCase string that can be evaluated programmatically, while the `message` field should contain a human-readable message. + +The `componentutils` package contains functions that help with updating a list of conditions. + +#### Kubebuilder Scaffolding + +This project uses a structure which diverges from standard kubebuilder inside the `cmd` package. As a result, not all scaffolding functionality works out of the box. Most prominently this affects webhook scaffolding. In order to work around this we create a `cmd/main.go` shim file before running any scaffolding: + +```sh +cat << EOF > cmd/main.go +package main + +import ( + "fmt" // we need to import something here so golint is happy + //+kubebuilder:scaffold:imports +) + +func main() { + fmt.Println("I should never be called") + //+kubebuilder:scaffold:builder +} +EOF +``` + +Unfortunately leaving the shim `cmd/main.go` file in, would break any go builds or tests which use the `./...` operator. As a result before committing, remove the shim main.go again: + +```sh +rm cmd/main.go +``` + +#### Test using envtest + +In order to run any tests which are using envtest, you need etcd, kube-apiserver and kubectl. You can make use of the `api/utils/envtest/` package in your tests to automatically install them. This is mainly needed, because we cannot easily run additional commands in CI. -*Insert a short description what is required to get your project running...* ## Support, Feedback, Contributing @@ -19,7 +147,7 @@ If you find any bug that may be a security problem, please follow our instructio ## Code of Conduct -We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/SAP/.github/blob/main/CODE_OF_CONDUCT.md) at all times. +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](https://github.com/openmcp-project/.github/blob/main/CODE_OF_CONDUCT.md) at all times. ## Licensing diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..eaf8bae --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +v0.26.0 diff --git a/api/constants/conditions.go b/api/constants/conditions.go new file mode 100644 index 0000000..5fcf846 --- /dev/null +++ b/api/constants/conditions.go @@ -0,0 +1,6 @@ +package constants + +const ( + // ConditionMCPSuccessful is an aggregated condition showing whether all component resources could be reconciled successfully. + ConditionMCPSuccessful = "MCPSuccessful" +) diff --git a/api/constants/logging.go b/api/constants/logging.go new file mode 100644 index 0000000..ff64f08 --- /dev/null +++ b/api/constants/logging.go @@ -0,0 +1,8 @@ +package constants + +const ( + KeyMethod = "method" + KeyResource = "resource" + MsgStartReconcile = "Starting reconcile" + KeyReconciledResourceKind = "reconciledResourceKind" +) diff --git a/api/constants/reasons.go b/api/constants/reasons.go new file mode 100644 index 0000000..1cdaddc --- /dev/null +++ b/api/constants/reasons.go @@ -0,0 +1,99 @@ +package constants + +// General reasons +const ( + // ReasonNoConditions means that a component does not expose any conditions. + // Most probably, the reason for this is that the resource has just been created and not been properly reconciled yet. + ReasonNoConditions = "NoConditions" + + // ReasonDeletionWaitingForDependingComponents means that this component's deletion is waiting for other components that depend on this one to be removed first. + ReasonDeletionWaitingForDependingComponents = "DeletionWaitingForDependingComponents" + + // ReasonWaitingForDependencies means that the component is waiting for another component that it depends on to become healthy. + ReasonWaitingForDependencies = "WaitingForDependencies" + + // ReasonDependencyStatusInvalid means that the status of a dependency does not look like expected. + ReasonDependencyStatusInvalid = "DependencyStatusInvalid" + + // ReasonComponentIsInDeletion can be used to signal that the current component is being deleted. + ReasonComponentIsInDeletion = "ComponentIsInDeletion" + + // ReasonCrateClusterInteractionProblem hints at problems during the interaction with a Crate cluster. + ReasonCrateClusterInteractionProblem = "CrateClusterInteractionProblem" + + // ReasonReconciliationError describes a generic error that occurred during reconciliation. + ReasonReconciliationError = "ReconciliationError" + + // ReasonMissingExpectedCondition means that a condition that was expected to be present is missing. + ReasonMissingExpectedCondition = "MissingExpectedCondition" +) + +// General messages +const ( + // MessageComponentIsInDeletion can be used to signal that the current component is being deleted. + MessageComponentIsInDeletion = "This component is being deleted." + + // MessageReconciliationError is a generic message that can be used to describe a reconciliation error. + MessageReconciliationError = "An error occurred during reconciliation." +) + +// APIServer Provider +const ( + // ReasonConfigurationProblem hints at problems with the APIServer provider configuration (configuration of this controller). + ReasonConfigurationProblem = "ConfigurationProblem" + + // ReasonGardenClusterInteractionProblem hints at problems during the interaction with a Gardener cluster. + ReasonGardenClusterInteractionProblem = "GardenClusterProblem" + + // ReasonShootIdentificationNotPossible means that the shoot belonging to the APIServer cannot be identified. + ReasonShootIdentificationNotPossible = "ShootIdentificationNotPossible" + + // ReasonAPIServerAccessProvisioningNotPossible means that something went wrong creating/getting the access information for the APIServer. + ReasonAPIServerAccessProvisioningNotPossible = "APIServerAccessProvisioningNotPossible" + + // ReasonInvalidAPIServerType means that the APIServer is not of the expected type. + ReasonInvalidAPIServerType = "InvalidAPIServerType" + + // ReasonAuditLogProblem represents problems with setting up audit logging. + ReasonAuditLogProblem = "AuditLogProblem" + + // ReasonWaitingForGardenerShoot implies that the Gardener shoot cluster is not yet ready. + ReasonWaitingForGardenerShoot = "WaitingForGardenerShoot" +) + +// Landscaper Connector +const ( + // ReasonLaaSCoreClusterInteractionProblem hints at problems during the interaction with a LaaS core cluster. + ReasonLaaSCoreClusterInteractionProblem = "LaaSCoreClusterInteractionProblem" + + // ReasonWaitingForLaaS means that the component is currently waiting for the LaaS landscape to do something. + ReasonWaitingForLaaS = "WaitingForLaaS" +) + +// Cloud Orchestrator +const ( + // ReasonCOCoreClusterInteractionProblem hints at problems during the interaction with a LaaS core cluster. + ReasonCOCoreClusterInteractionProblem = "COCoreClusterInteractionProblem" + + ReasonWaitingForCloudOrchestrator = "WaitingForCloudOrchestrator" +) + +// Authentication Reconciler +const ( + // ReasonManagingOpenIDConnect indicates Creating/Updating/Deleting the OpenIDConnect resources has failed. + ReasonManagingOpenIDConnect = "ManagingOpenIDConnectResourcesProblem" +) + +// Authorization Reconciler +const ( + // ReasonManagingAuthorization indicates Creating/Updating/Deleting the authorization resources has failed. + ReasonManagingAuthorization = "ManagingAuthorizationResourcesProblem" +) + +// ManagedControlPlane Reconciler +const ( + // ReasonAllComponentsReconciledSuccessfully indicates that all components have been reconciled successfully. + ReasonAllComponentsReconciledSuccessfully = "AllComponentsReconciledSuccessfully" + // ReasonNotAllComponentsReconciledSuccessfully indicates that not all components have been reconciled successfully. + ReasonNotAllComponentsReconciledSuccessfully = "NotAllComponentsReconciledSuccessfully" +) diff --git a/api/core/v1alpha1/apiserver_component.go b/api/core/v1alpha1/apiserver_component.go new file mode 100644 index 0000000..027e4ff --- /dev/null +++ b/api/core/v1alpha1/apiserver_component.go @@ -0,0 +1,56 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/util/sets" + + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +const APIServerComponent ComponentType = "APIServer" +const ConditionAPIServerHealthy = "apiServerHealthy" + +// Type implements Component. +func (*APIServer) Type() ComponentType { + return APIServerComponent +} + +// GetHealthCondition implements Component. +func (*APIServer) GetHealthCondition() string { + return ConditionAPIServerHealthy +} + +// GetSpec implements Component. +func (as *APIServer) GetSpec() any { + return &as.Spec +} + +// SetSpec implements Component. +func (as *APIServer) SetSpec(cfg any) error { + apiServerSpec, ok := cfg.(*APIServerSpec) + if !ok { + return openmcperrors.ErrWrongComponentConfigType + } + as.Spec = *apiServerSpec + return nil +} + +// GetCommonStatus implements Component. +func (as *APIServer) GetCommonStatus() CommonComponentStatus { + return as.Status.CommonComponentStatus +} + +// SetCommonStatus implements Component. +func (as *APIServer) SetCommonStatus(status CommonComponentStatus) { + as.Status.CommonComponentStatus = status +} + +// GetExternalStatus implements Component. +func (as *APIServer) GetExternalStatus() any { + return as.Status.ExternalAPIServerStatus +} + +// GetRequiredConditions implements Component. +func (as *APIServer) GetRequiredConditions() sets.Set[string] { + // ToDo: Compute a more precise set of expected conditions based on the spec instead of returning a static set. + return sets.New(as.Type().HealthyCondition(), as.Type().ReconciliationCondition()) +} diff --git a/api/core/v1alpha1/apiserver_defaults.go b/api/core/v1alpha1/apiserver_defaults.go new file mode 100644 index 0000000..9cfa4b8 --- /dev/null +++ b/api/core/v1alpha1/apiserver_defaults.go @@ -0,0 +1,34 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" +) + +// Default sets defaults. +// This modifies the receiver object. +// Note that only the parts which belong to the configured type are defaulted, everything else is ignored. +func (asSpec *APIServerSpec) Default() { + switch asSpec.Type { + case Gardener: + if asSpec.GardenerConfig == nil { + asSpec.GardenerConfig = &GardenerConfiguration{} + } + asSpec.GardenerConfig.Default() + } +} + +// Validate validates the configuration. +// Only the configuration that belongs to the configured type is validated, configuration for other types is ignored. +func (asSpec *APIServerSpec) Validate(path string, morePaths ...string) error { + allErrs := field.ErrorList{} + fldPath := field.NewPath(path, morePaths...) + + switch asSpec.Type { + case Gardener, GardenerDedicated: + allErrs = append(allErrs, asSpec.GardenerConfig.Validate(fldPath.Child("gardener"))...) + default: + allErrs = append(allErrs, field.NotSupported(fldPath.Child("type"), string(asSpec.Type), []string{string(Gardener), string(GardenerDedicated)})) + } + + return allErrs.ToAggregate() +} diff --git a/api/core/v1alpha1/apiserver_gardener_types.go b/api/core/v1alpha1/apiserver_gardener_types.go new file mode 100644 index 0000000..1eebcdb --- /dev/null +++ b/api/core/v1alpha1/apiserver_gardener_types.go @@ -0,0 +1,144 @@ +package v1alpha1 + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation/field" + "sigs.k8s.io/yaml" +) + +const ( + // HighAvailabilityFailureToleranceNode specifies that the control plane is tolerant to node failures within a single zone. + HighAvailabilityFailureToleranceNode = "node" + // HighAvailabilityFailureToleranceZone specifies that the control plane is tolerant to zone failures. + HighAvailabilityFailureToleranceZone = "zone" +) + +// GardenerConfiguration contains the configuration that is required for setting up a Gardener-based APIServer. +// +kubebuilder:validation:XValidation:rule="has(self.highAvailability) == has(oldSelf.highAvailability) || has(self.highAvailability)",message="highAvailability is required once set" +type GardenerConfiguration struct { + // Region is the region to be used for the Shoot cluster. + // This is usually derived from the ManagedControlPlane's common configuration, but can be overwritten here. + // +kubebuilder:validation:XValidation:message="region is immutable",rule="self == oldSelf" + // +kubebuilder:validation:Optional + Region string `json:"region,omitempty"` + + // HighAvailabilityConfig specifies the HA configuration for the API server. + // +kubebuilder:validation:XValidation:message="highAvailability is immutable",rule="self == oldSelf" + // +kubebuilder:validation:Optional + HighAvailabilityConfig *HighAvailabilityConfig `json:"highAvailability,omitempty"` + + // AuditLogConfig defines the AuditLog configuration for the ManagedControlPlane cluster. + // +kubebuilder:validation:Optional + AuditLog *AuditLogConfig `json:"auditLog,omitempty"` + + // EncryptionConfig contains customizable encryption configuration of the API server. + // +optional + EncryptionConfig *EncryptionConfig `json:"encryptionConfig,omitempty"` +} + +type GardenerInternalConfiguration struct { + // ShootOverwrite allows to overwrite the shoot to be used. This could be useful for migration tasks. + // +kubebuilder:validation:Optional + ShootOverwrite *NamespacedObjectReference `json:"shootOverwrite,omitempty"` + + // K8SVersionOverwrite is the k8s version for the Shoot cluster. + // Will be defaulted if not specified. + // +kubebuilder:validation:Optional + K8SVersionOverwrite string `json:"k8sVersionOverwrite,omitempty"` + + // LandscapeConfiguration is the name of the landscape and the name of the configuration to use. + // The expected format is "/". + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Pattern="^[a-z0-9-]+/[a-z0-9-]+$" + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + LandscapeConfiguration string `json:"landscapeConfiguration,omitempty"` +} + +// HighAvailabilityConfig specifies the High Availability configuration for the API server. +type HighAvailabilityConfig struct { + // FailureToleranceType specifies failure tolerance mode for the API server. + // Allowed values are: node, zone + // node: The API server is tolerant to node failures within a single zone. + // zone: The API server is tolerant to zone failures. + // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="failureToleranceType is immutable" + // +kubebuilder:validation:Enum=node;zone + FailureToleranceType string `json:"failureToleranceType"` +} + +// AuditLogConfig defines the AuditLog configuration for the resource cluster (shoot cluster). +type AuditLogConfig struct { + // Type is the type of the audit log. + // +kubebuilder:validation:Enum=standard + Type string `json:"type"` + + // TenantID is the tenant ID of the BTP Subaccount. Can be seen in the BTP Cockpit dashboard. + TenantID string `json:"tenantID"` + + // ServiceURL is the URL from the Service Keys. + ServiceURL string `json:"serviceURL"` + + // SecretRef is the reference to the secret containing the credentials for the audit log service. + SecretRef corev1.LocalObjectReference `json:"secretRef"` + + // PolicyRef is the reference to the policy containing the configuration for the audit log service. + PolicyRef corev1.LocalObjectReference `json:"policyRef"` +} + +// EncryptionConfig contains customizable encryption configuration of the API server. +type EncryptionConfig struct { + // Resources contains the list of resources that shall be encrypted in addition to secrets. + // Each item is a Kubernetes resource name in plural (resource or resource.group) that should be encrypted. + // Example: ["configmaps", "statefulsets.apps", "flunders.emxample.com"] + Resources []string `json:"resources,omitempty"` +} + +// GardenerStatus contains internal status for 'Gardener' type APIServer. +type GardenerStatus struct { + // Shoot contains the shoot manifest generated by the controller. + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + Shoot *runtime.RawExtension `json:"shoot,omitempty"` +} + +func (gc *GardenerConfiguration) Default() { + +} + +func (gc *GardenerConfiguration) Validate(fldPath *field.Path) field.ErrorList { + if gc == nil { + return nil + } + allErrs := field.ErrorList{} + + // TODO validate OIDC config? + + return allErrs +} + +// GetShoot returns the shoot in the GardenerStatus as unstructured object. +// Returns nil if no shoot is contained in the GardenerStatus. +func (gs *GardenerStatus) GetShoot() (*unstructured.Unstructured, error) { + if gs == nil || gs.Shoot == nil { + return nil, nil + } + if gs.Shoot.Object != nil { + // this is mostly relevant for tests + uShoot, ok := gs.Shoot.Object.(*unstructured.Unstructured) + if ok { + return uShoot, nil + } + } + if gs.Shoot.Raw != nil { + uShoot := &unstructured.Unstructured{} + if err := yaml.Unmarshal(gs.Shoot.Raw, uShoot); err != nil { + return nil, fmt.Errorf("failed to unmarshal shoot: %w", err) + } + return uShoot, nil + } + return nil, nil +} diff --git a/api/core/v1alpha1/apiserver_types.go b/api/core/v1alpha1/apiserver_types.go new file mode 100644 index 0000000..8f3c36f --- /dev/null +++ b/api/core/v1alpha1/apiserver_types.go @@ -0,0 +1,127 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type APIServerType string + +const ( + // Gardener is the APIServerType for a workerless shoot cluster. + Gardener APIServerType = "Gardener" + + // GardenerDedicated is the APIServerType for a cluster with worker nodes. + GardenerDedicated APIServerType = "GardenerDedicated" +) + +// APIServerConfiguration contains the configuration which is required for setting up a k8s cluster to be used as APIServer. +type APIServerConfiguration struct { + // Type is the type of APIServer. This determines which other configuration fields need to be specified. + // Valid values are: + // - Gardener + // - GardenerDedicated + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="type is immutable" + // +kubebuilder:validation:Enum=Gardener;GardenerDedicated + // +kubebuilder:default="GardenerDedicated" + Type APIServerType `json:"type"` + + // GardenerConfig contains configuration for a Gardener APIServer. + // Must be set if type is 'Gardener', is ignored otherwise. + // +optional + GardenerConfig *GardenerConfiguration `json:"gardener,omitempty"` +} + +type APIServerInternalConfiguration struct { + // GardenerConfig contains internal configuration for a Gardener APIServer. + // +optional + GardenerConfig *GardenerInternalConfiguration `json:"gardener,omitempty"` +} + +// APIServerSpec contains the APIServer configuration and potentially other fields which should not be exposed to the customer. +type APIServerSpec struct { + APIServerConfiguration `json:",inline"` + + // Internal contains the parts of the configuration which are not exposed to the customer. + // It would be nice to have this as an inline field, but since both APIServerConfiguration and APIServerInternalConfiguration + // contain a field 'gardener', this would clash. + // +optional + Internal *APIServerInternalConfiguration `json:"internal,omitempty"` + + // DesiredRegion is part of the common configuration. + // If specified, it will be used to determine the region for the created cluster. + // +optional + DesiredRegion *RegionSpecification `json:"desiredRegion"` +} + +// ExternalAPIServerStatus contains the status of the API server / ManagedControlPlane cluster. The Kuberenetes can act as an OIDC +// compatible provider in a sense that they serve OIDC issuer endpoint URL so that other system can validate tokens that have been +// issued by the external party. +type ExternalAPIServerStatus struct { + // Endpoint represents the Kubernetes API server endpoint + // +optional + Endpoint string `json:"endpoint,omitempty"` + + // ServiceAccountIssuer represents the OpenIDConnect issuer URL that can be used to verify service account tokens. + // +optional + ServiceAccountIssuer string `json:"serviceAccountIssuer,omitempty"` +} + +// APIServerStatus contains the APIServer status and potentially other fields which should not be exposed to the customer. +type APIServerStatus struct { + CommonComponentStatus `json:",inline"` + + // ExternalAPIServerStatus contains the status of the external API server + *ExternalAPIServerStatus `json:",inline"` + + // AdminAccess is an admin kubeconfig for accessing the API server. + // +optional + AdminAccess *APIServerAccess `json:"adminAccess,omitempty"` + + // GardenerStatus contains status if the type is 'Gardener'. + // +optional + GardenerStatus *GardenerStatus `json:"gardener,omitempty"` +} + +// APIServerAccess contains access information for the API server. +// Usually a kubeconfig, optional some metadata. +type APIServerAccess struct { + // Kubeconfig is the kubeconfig for accessing the APIServer cluster. + Kubeconfig string `json:"kubeconfig,omitempty"` + + // CreationTimestamp is the time when this access was created. + // +optional + CreationTimestamp *metav1.Time `json:"creationTimestamp,omitempty"` + + // ExpirationTimestamp is the time until the access loses its validity. + // +optional + ExpirationTimestamp *metav1.Time `json:"expirationTimestamp,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// APIServer is the Schema for the APIServer API +// +kubebuilder:resource:shortName=as +// +kubebuilder:printcolumn:name="Successfully_Reconciled",type=string,JSONPath=`.status.conditions[?(@.type=="APIServerReconciliation")].status` +// +kubebuilder:printcolumn:name="Deleted",type="date",JSONPath=".metadata.deletionTimestamp" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type APIServer struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec APIServerSpec `json:"spec,omitempty"` + Status APIServerStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// APIServerList contains a list of APIServer +type APIServerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []APIServer `json:"items"` +} + +func init() { + SchemeBuilder.Register(&APIServer{}, &APIServerList{}) +} diff --git a/api/core/v1alpha1/authentication_component.go b/api/core/v1alpha1/authentication_component.go new file mode 100644 index 0000000..a8716e1 --- /dev/null +++ b/api/core/v1alpha1/authentication_component.go @@ -0,0 +1,50 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/util/sets" + + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +const AuthenticationComponent ComponentType = "Authentication" + +// Type returns the type of the Authentication component. +func (*Authentication) Type() ComponentType { + return AuthenticationComponent +} + +// GetSpec returns the spec of the Authentication component. +func (a *Authentication) GetSpec() any { + return &a.Spec +} + +// SetSpec sets the spec of the Authentication component. +func (a *Authentication) SetSpec(cfg any) error { + as, ok := cfg.(*AuthenticationSpec) + if !ok { + return openmcperrors.ErrWrongComponentConfigType + } + a.Spec = *as + return nil +} + +// GetCommonStatus returns the common status of the Authentication component. +func (a *Authentication) GetCommonStatus() CommonComponentStatus { + return a.Status.CommonComponentStatus +} + +// SetCommonStatus sets the common status of the Authentication component. +func (a *Authentication) SetCommonStatus(status CommonComponentStatus) { + a.Status.CommonComponentStatus = status +} + +// GetExternalStatus returns the external status of the Authentication component. +func (a *Authentication) GetExternalStatus() any { + return a.Status.ExternalAuthenticationStatus +} + +// GetRequiredConditions implements Component. +func (a *Authentication) GetRequiredConditions() sets.Set[string] { + // ToDo: Compute a more precise set of expected conditions based on the spec instead of returning a static set. + return sets.New(a.Type().HealthyCondition(), a.Type().ReconciliationCondition()) +} diff --git a/api/core/v1alpha1/authentication_defaults.go b/api/core/v1alpha1/authentication_defaults.go new file mode 100644 index 0000000..3496716 --- /dev/null +++ b/api/core/v1alpha1/authentication_defaults.go @@ -0,0 +1,87 @@ +package v1alpha1 + +import ( + "unicode" + + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" +) + +// Default sets the default values for the AuthenticationSpec. +// This modifies the receiver object. +func (as *AuthenticationSpec) Default() { + if as.AuthenticationConfiguration.EnableSystemIdentityProvider == nil { + as.AuthenticationConfiguration.EnableSystemIdentityProvider = ptr.To(true) + } +} + +// Validate validates the AuthenticationSpec +func (as *AuthenticationSpec) Validate(path string, morePaths ...string) error { + allErrs := field.ErrorList{} + fldPath := field.NewPath(path, morePaths...) + + uniqueness := make(map[string]interface{}) + + for _, idp := range as.IdentityProviders { + if _, ok := uniqueness[idp.Name]; ok { + allErrs = append(allErrs, field.Duplicate(fldPath.Child("identityProviders").Child(idp.Name), idp.Name)) + } else { + uniqueness[idp.Name] = nil + } + + allErrs = append(allErrs, ValidateIdp(idp, fldPath.Child("identityProviders"))...) + } + + return allErrs.ToAggregate() +} + +// ValidateIdp validates the IdentityProvider +func ValidateIdp(idp IdentityProvider, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + idpPath := fldPath.Child(idp.Name) + + if idp.IssuerURL == "" { + allErrs = append(allErrs, field.Required(idpPath.Child("issuerURL"), "issuerURL must be set")) + } + + if idp.ClientID == "" { + allErrs = append(allErrs, field.Required(idpPath.Child("clientID"), "clientID must be set")) + } + + if !isLowerCaseLetter(idp.Name) { + allErrs = append(allErrs, field.Invalid(idpPath.Child("name"), idp.Name, "name must only contain lowercase letters")) + } + + if len(idp.Name) > 63 { + allErrs = append(allErrs, field.TooLong(idpPath.Child("name"), idp.Name, 63)) + } + + if idp.ClientConfig.ExtraConfig != nil { + fldPath = idpPath.Child("client").Child("extraConfig") + + if _, ok := idp.ClientConfig.ExtraConfig["oidc-issuer-url"]; ok { + allErrs = append(allErrs, field.Forbidden(fldPath.Key("oidc-issuer-url"), "oidc-issuer-url is a reserved key")) + } + + if _, ok := idp.ClientConfig.ExtraConfig["oidc-client-id"]; ok { + allErrs = append(allErrs, field.Forbidden(fldPath.Key("oidc-client-id"), "oidc-client-id is a reserved key")) + } + + if _, ok := idp.ClientConfig.ExtraConfig["oidc-client-secret"]; ok { + allErrs = append(allErrs, field.Forbidden(fldPath.Key("oidc-client-secret"), "oidc-client-secret is a reserved key")) + } + } + + return allErrs +} + +// isLowerCaseLetter checks if the given string is a lowercase letter. +func isLowerCaseLetter(s string) bool { + for _, r := range s { + if !(unicode.IsLetter(r) && unicode.IsLower(r)) { + return false + } + } + return true +} diff --git a/api/core/v1alpha1/authentication_types.go b/api/core/v1alpha1/authentication_types.go new file mode 100644 index 0000000..3bdb0c1 --- /dev/null +++ b/api/core/v1alpha1/authentication_types.go @@ -0,0 +1,133 @@ +package v1alpha1 + +import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + +const ( + // Well-known oidc-login parameters + + OIDCParameterIssuerURL = "oidc-issuer-url" + OIDCParameterClientID = "oidc-client-id" + OIDCParameterClientSecret = "oidc-client-secret" + OIDCParameterExtraScope = "oidc-extra-scope" + OIDCParameterUsePKCE = "oidc-use-pkce" + OIDCParameterGrantType = "grant-type" + + OIDCDefaultExtraScopes = "offline_access,email,profile" + OIDCDefaultGrantType = "auto" +) + +// AuthenticationConfiguration contains the configuration for the enabled OpenID Connect identity providers +type AuthenticationConfiguration struct { + // +kubebuilder:validation:Optional + EnableSystemIdentityProvider *bool `json:"enableSystemIdentityProvider"` + // +kubebuilder:validation:Optional + IdentityProviders []IdentityProvider `json:"identityProviders,omitempty"` +} + +// AuthenticationSpec contains the specification for the authentication component +type AuthenticationSpec struct { + AuthenticationConfiguration `json:",inline"` +} + +// ExternalAuthenticationStatus contains the status of the authentication component. +type ExternalAuthenticationStatus struct { + // UserAccess reference the secret containing the kubeconfig + // for the APIServer which is to be used by the customer. + // +optional + UserAccess *SecretReference `json:"access,omitempty"` +} + +// AuthenticationStatus contains the status of the authentication component +type AuthenticationStatus struct { + CommonComponentStatus `json:",inline"` + *ExternalAuthenticationStatus `json:",inline"` +} + +// IdentityProvider contains the configuration for an OpenID Connect identity provider +type IdentityProvider struct { + // Name is the name of the identity provider. + // The name must be unique among all identity providers. + // The name must only contain lowercase letters. + // The length must not exceed 63 characters. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern=`^[a-z]+$` + Name string `json:"name"` + // IssuerURL is the issuer URL of the identity provider. + // +kubebuilder:validation:Required + IssuerURL string `json:"issuerURL"` + // ClientID is the client ID of the identity provider. + // +kubebuilder:validation:Required + ClientID string `json:"clientID"` + // UsernameClaim is the claim that contains the username. + // +kubebuilder:validation:Required + UsernameClaim string `json:"usernameClaim"` + // GroupsClaim is the claim that contains the groups. + // +kubebuilder:validation:Optional + GroupsClaim string `json:"groupsClaim"` + // CABundle: When set, the OpenID server's certificate will be verified by one of the authorities in the bundle. + // Otherwise, the host's root CA set will be used. + // +kubebuilder:validation:Optional + CABundle string `json:"caBundle,omitempty"` + // SigningAlgs is the list of allowed JOSE asymmetric signing algorithms. + // +kubebuilder:validation:Optional + SigningAlgs []string `json:"signingAlgs,omitempty"` + // RequiredClaims is a map of required claims. If set, the identity provider must provide these claims in the ID token. + // +kubebuilder:validation:Optional + RequiredClaims map[string]string `json:"requiredClaims,omitempty"` + + // ClientAuthentication contains configuration for OIDC clients + // +kubebuilder:validation:Optional + ClientConfig ClientAuthenticationConfig `json:"clientConfig,omitempty"` +} + +// ClientAuthenticationConfig contains configuration for OIDC clients +type ClientAuthenticationConfig struct { + // ClientSecret is a references to a secret containing the client secret. + // The client secret will be added to the generated kubeconfig with the "--oidc-client-secret" flag. + // +kubebuilder:validation:Optional + ClientSecret *LocalSecretReference `json:"clientSecret,omitempty"` + // ExtraConfig is added to the client configuration in the kubeconfig. + // Can either be a single string value, a list of string values or no value. + // Must not contain any of the following keys: + // - "client-id" + // - "client-secret" + // - "issuer-url" + // + // +kubebuilder:validation:Optional + ExtraConfig map[string]SingleOrMultiStringValue `json:"extraConfig,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Authentication is the Schema for the authentication API +// +kubebuilder:resource:shortName=auth +// +kubebuilder:printcolumn:name="Successfully_Reconciled",type=string,JSONPath=`.status.conditions[?(@.type=="AuthenticationReconciliation")].status` +// +kubebuilder:printcolumn:name="Deleted",type="date",JSONPath=".metadata.deletionTimestamp" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type Authentication struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AuthenticationSpec `json:"spec,omitempty"` + Status AuthenticationStatus `json:"status,omitempty"` +} + +// IsSystemIdentityProviderEnabled returns true if the system identity provider is enabled +func (a *Authentication) IsSystemIdentityProviderEnabled() bool { + return a.Spec.EnableSystemIdentityProvider != nil && *a.Spec.EnableSystemIdentityProvider +} + +// +kubebuilder:object:root=true + +// AuthenticationList contains the list of authentications +type AuthenticationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Authentication `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Authentication{}, &AuthenticationList{}) +} diff --git a/api/core/v1alpha1/authorization_component.go b/api/core/v1alpha1/authorization_component.go new file mode 100644 index 0000000..aa1a563 --- /dev/null +++ b/api/core/v1alpha1/authorization_component.go @@ -0,0 +1,50 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/util/sets" + + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +const AuthorizationComponent ComponentType = "Authorization" + +// Type returns the type of the Authentication component. +func (*Authorization) Type() ComponentType { + return AuthorizationComponent +} + +// GetSpec returns the spec of the Authentication component. +func (a *Authorization) GetSpec() any { + return &a.Spec +} + +// SetSpec sets the spec of the Authentication component. +func (a *Authorization) SetSpec(cfg any) error { + as, ok := cfg.(*AuthorizationSpec) + if !ok { + return openmcperrors.ErrWrongComponentConfigType + } + a.Spec = *as + return nil +} + +// GetCommonStatus returns the common status of the Authentication component. +func (a *Authorization) GetCommonStatus() CommonComponentStatus { + return a.Status.CommonComponentStatus +} + +// SetCommonStatus sets the common status of the Authentication component. +func (a *Authorization) SetCommonStatus(status CommonComponentStatus) { + a.Status.CommonComponentStatus = status +} + +// GetExternalStatus returns the external status of the Authentication component. +func (a *Authorization) GetExternalStatus() any { + return a.Status.ExternalAuthorizationStatus +} + +// GetRequiredConditions implements Component. +func (a *Authorization) GetRequiredConditions() sets.Set[string] { + // ToDo: Compute a more precise set of expected conditions based on the spec instead of returning a static set. + return sets.New(a.Type().HealthyCondition(), a.Type().ReconciliationCondition()) +} diff --git a/api/core/v1alpha1/authorization_defaults.go b/api/core/v1alpha1/authorization_defaults.go new file mode 100644 index 0000000..5a6d560 --- /dev/null +++ b/api/core/v1alpha1/authorization_defaults.go @@ -0,0 +1,59 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" +) + +const ( + GroupName = "rbac.authorization.k8s.io" + GroupKind = "Group" + ServiceAccountKind = "ServiceAccount" + UserKind = "User" +) + +// Default sets the default values for the AuthorizationSpec +func (as *AuthorizationSpec) Default() { + for _, role := range as.RoleBindings { + for i, subject := range role.Subjects { + if (subject.Kind == GroupKind || subject.Kind == UserKind) && subject.APIGroup == "" { + role.Subjects[i].APIGroup = GroupName + } + } + } +} + +// Validate validates the AuthorizationSpec +func (as *AuthorizationSpec) Validate(path string, morePaths ...string) error { + allErrs := field.ErrorList{} + fldPath := field.NewPath(path, morePaths...) + + for _, role := range as.RoleBindings { + if role.Role != RoleBindingRoleAdmin && role.Role != RoleBindingRoleView { + allErrs = append(allErrs, field.Invalid(fldPath.Child("role"), role.Role, "role must be either admin or view")) + } + + fldPath = fldPath.Child("subjects") + + for i, subject := range role.Subjects { + fldPath = fldPath.Index(i) + + if subject.Kind != GroupKind && subject.Kind != UserKind && subject.Kind != ServiceAccountKind { + allErrs = append(allErrs, field.Invalid(fldPath.Child("kind"), subject.Kind, "kind must be either ServiceAccount, User or Group")) + } + + if (subject.Kind == GroupKind || subject.Kind == UserKind) && subject.APIGroup != GroupName { + allErrs = append(allErrs, field.Invalid(fldPath.Child("apiGroup"), subject.APIGroup, "apiGroup must be set to "+GroupName)) + } + + if subject.Name == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("name"), "name must be set")) + } + + if subject.Namespace == "" && subject.Kind == ServiceAccountKind { + allErrs = append(allErrs, field.Required(fldPath.Child("namespace"), "namespace must be set")) + } + } + } + + return allErrs.ToAggregate() +} diff --git a/api/core/v1alpha1/authorization_roles.go b/api/core/v1alpha1/authorization_roles.go new file mode 100644 index 0000000..24e1c94 --- /dev/null +++ b/api/core/v1alpha1/authorization_roles.go @@ -0,0 +1,39 @@ +package v1alpha1 + +// GetClusterRoleNames returns the names of all known cluster roles. +func GetClusterRoleNames() []string { + return []string{ + AdminNamespaceScopeRole, + AdminClusterScopeRole, + AdminNamespaceScopeStandardRulesRole, + AdminClusterScopeStandardRulesRole, + ViewNamespaceScopeRole, + ViewClusterScopeRole, + ViewNamespaceScopeStandardRulesRole, + ViewClusterScopeStandardClusterRole, + } +} + +// IsAggregatedRole returns true if the given role name is an aggregated role. +func IsAggregatedRole(roleName string) bool { + return roleName == AdminNamespaceScopeRole || + roleName == AdminClusterScopeRole || + roleName == ViewNamespaceScopeRole || + roleName == ViewClusterScopeRole +} + +// IsAdminRole returns true if the given role name is an admin role. +func IsAdminRole(roleName string) bool { + return roleName == AdminNamespaceScopeRole || + roleName == AdminClusterScopeRole || + roleName == AdminNamespaceScopeStandardRulesRole || + roleName == AdminClusterScopeStandardRulesRole +} + +// IsClusterScopedRole returns true if the given role name is a cluster scoped role. +func IsClusterScopedRole(roleName string) bool { + return roleName == AdminClusterScopeRole || + roleName == AdminClusterScopeStandardRulesRole || + roleName == ViewClusterScopeRole || + roleName == ViewClusterScopeStandardClusterRole +} diff --git a/api/core/v1alpha1/authorization_types.go b/api/core/v1alpha1/authorization_types.go new file mode 100644 index 0000000..6255e3e --- /dev/null +++ b/api/core/v1alpha1/authorization_types.go @@ -0,0 +1,192 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // RoleBindingRoleAdmin is the role for the admin + RoleBindingRoleAdmin = "admin" + // RoleBindingRoleView is the role for the viewer + RoleBindingRoleView = "view" + + // AdminNamespaceScopeRole is the role for the admin with namespace scope + AdminNamespaceScopeRole = "openmcp:admin" + // AdminClusterScopeRole is the role for the admin with cluster scope + AdminClusterScopeRole = "openmcp:admin:clusterscoped" + // AdminNamespaceScopeStandardRulesRole is the role for the admin with namespace scope and standard rules + AdminNamespaceScopeStandardRulesRole = "openmcp:aggregate-to-admin" + // AdminClusterScopeStandardRulesRole is the role for the admin with cluster scope and standard rules + AdminClusterScopeStandardRulesRole = "openmcp:clusterscoped:aggregate-to-admin" + // AdminNamespaceScopeMatchLabel is the aggregation label for the admin with namespace scope + AdminNamespaceScopeMatchLabel = BaseDomain + "/aggregate-to-admin" + // AdminClusterScopeMatchLabel is the aggregation label for the admin with cluster scope + AdminClusterScopeMatchLabel = BaseDomain + "/aggregate-to-admin-clusterscoped" + + // ViewNamespaceScopeRole is the role for the viewer with namespace scope + ViewNamespaceScopeRole = "openmcp:view" + // ViewClusterScopeRole is the role for the viewer with cluster scope + ViewClusterScopeRole = "openmcp:view:clusterscoped" + // ViewNamespaceScopeStandardRulesRole is the role for the viewer with namespace scope and standard rules + ViewNamespaceScopeStandardRulesRole = "openmcp:aggregate-to-view" + // ViewClusterScopeStandardClusterRole is the role for the viewer with cluster scope and standard rules + ViewClusterScopeStandardClusterRole = "openmcp:clusterscoped:aggregate-to-view" + // ViewNamespaceScopeMatchLabel is the aggregation label for the viewer with namespace scope + ViewNamespaceScopeMatchLabel = BaseDomain + "/aggregate-to-view" + // ViewClusterScopeMatchLabel is the aggregation label for the viewer with cluster scope + ViewClusterScopeMatchLabel = BaseDomain + "/aggregate-to-view-clusterscoped" + + // AdminClusterRoleBinding is the cluster role binding for the admin with cluster scope + AdminClusterRoleBinding = "openmcp:admin" + // AdminRoleBinding is the role binding for the admin with namespace scope + AdminRoleBinding = "openmcp:admin" + // ViewClusterRoleBinding is the cluster role binding for the viewer with cluster scope + ViewClusterRoleBinding = "openmcp:view" + // ViewRoleBinding is the role binding for the viewer with namespace scope + ViewRoleBinding = "openmcp:view" + + // ClusterAdminRoleBinding is the name of the role binding for the cluster admin + ClusterAdminRoleBinding = "openmcp:cluster-admin" + // ClusterAdminRole is the name of the role for the cluster admin + ClusterAdminRole = "cluster-admin" +) + +// AuthorizationConfiguration contains the configuration of the subjects assigned to control plane roles +type AuthorizationConfiguration struct { + // RoleBindings is a list of role bindings + RoleBindings []RoleBinding `json:"roleBindings"` +} + +// GetRoleForName returns the role for the given role name or nil if the role does not exist. +// If multiple roles with the same name exist, their subject lists are aggregated. +func (ac *AuthorizationConfiguration) GetRoleForName(roleName string) *RoleBinding { + var res *RoleBinding + for _, rb := range ac.RoleBindings { + if rb.Role == roleName { + if res == nil { + res = &RoleBinding{ + Role: roleName, + } + } + res.Subjects = append(res.Subjects, rb.Subjects...) + } + } + return res +} + +// RoleBinding contains the role and the subjects assigned to the role +type RoleBinding struct { + // Role is the name of the role + // +kubebuilder:validation:Enum=admin;view + Role string `json:"role"` + // Subjects is a list of subjects assigned to the role + Subjects []Subject `json:"subjects"` +} + +// Subject describes an object that is assigned to a role and +// which can be used to authenticate against the control plane. +type Subject struct { + // Kind is the kind of the subject + // +kubebuilder:validation:Enum=ServiceAccount;User;Group + Kind string `json:"kind"` + // APIGroup is the API group of the subject + // +optional + APIGroup string `json:"apiGroup,omitempty"` + // Name is the name of the subject + // +kubebuilder:validation:MinLength=1 + Name string `json:"name"` + // Namespace is the namespace of the subject + // +optional + Namespace string `json:"namespace,omitempty"` +} + +// AuthorizationSpec contains the specification for the authorization component +type AuthorizationSpec struct { + AuthorizationConfiguration `json:",inline"` +} + +// ExternalAuthorizationStatus contains the status of the external authorization component +type ExternalAuthorizationStatus struct { +} + +// AuthorizationStatus contains the status of the authorization component +type AuthorizationStatus struct { + CommonComponentStatus `json:",inline"` + // ExternalAuthorizationStatus contains the status of the external authorization component + *ExternalAuthorizationStatus `json:",inline"` + + // UserNamespaces is a list of namespaces that have been created by the user and + // must be managed by the authorization component. + UserNamespaces []string `json:"userNamespaces,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// Authorization is the Schema for the authorization API +// +kubebuilder:resource:shortName=authz +// +kubebuilder:printcolumn:name="Successfully_Reconciled",type=string,JSONPath=`.status.conditions[?(@.type=="AuthorizationReconciliation")].status` +// +kubebuilder:printcolumn:name="Deleted",type="date",JSONPath=".metadata.deletionTimestamp" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type Authorization struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AuthorizationSpec `json:"spec,omitempty"` + Status AuthorizationStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// AuthorizationList contains the list of authorizations +type AuthorizationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Authorization `json:"items"` +} + +// ClusterAdminSpec contains the specification for the cluster admin +type ClusterAdminSpec struct { + Subjects []Subject `json:"subjects"` +} + +// ClusterAdminStatus contains the status of the cluster admin +type ClusterAdminStatus struct { + // Active is set to true if the subjects of the cluster admin are assigned the cluster-admin role + Active bool `json:"active"` + // ActivationTime is the time when the cluster admin was activated + // +optional + Activated *metav1.Time `json:"activationTime,omitempty"` + // ExpirationTime is the time when the cluster admin will expire + // +optional + Expiration *metav1.Time `json:"expirationTime,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// ClusterAdmin is the Schema for the cluster admin API +// +kubebuilder:resource:shortName=clas +// +kubebuilder:printcolumn:name="Active",type=string,JSONPath=`.status.active` +// +kubebuilder:printcolumn:name="Activated",type="date",JSONPath=".status.activationTime" +// +kubebuilder:printcolumn:name="Expiration",type="string",JSONPath=".status.expirationTime" +type ClusterAdmin struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterAdminSpec `json:"spec,omitempty"` + Status ClusterAdminStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ClusterAdminList contains the list of cluster admins +type ClusterAdminList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ClusterAdmin `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Authorization{}, &AuthorizationList{}, &ClusterAdmin{}, &ClusterAdminList{}) +} diff --git a/api/core/v1alpha1/cloudorchestrator_component.go b/api/core/v1alpha1/cloudorchestrator_component.go new file mode 100644 index 0000000..1a43172 --- /dev/null +++ b/api/core/v1alpha1/cloudorchestrator_component.go @@ -0,0 +1,50 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/util/sets" + + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +const CloudOrchestratorComponent ComponentType = "CloudOrchestrator" + +// Type implements Component. +func (*CloudOrchestrator) Type() ComponentType { + return CloudOrchestratorComponent +} + +// GetSpec implements Component. +func (o *CloudOrchestrator) GetSpec() any { + return &o.Spec +} + +// SetSpec implements Component. +func (o *CloudOrchestrator) SetSpec(cfg any) error { + cSpec, ok := cfg.(*CloudOrchestratorSpec) + if !ok { + return openmcperrors.ErrWrongComponentConfigType + } + o.Spec = *cSpec + return nil +} + +// GetCommonStatus implements Component. +func (o *CloudOrchestrator) GetCommonStatus() CommonComponentStatus { + return o.Status.CommonComponentStatus +} + +// SetCommonStatus implements Component. +func (o *CloudOrchestrator) SetCommonStatus(status CommonComponentStatus) { + o.Status.CommonComponentStatus = status +} + +// GetExternalStatus implements Component. +func (o *CloudOrchestrator) GetExternalStatus() any { + return o.Status.ExternalCloudOrchestratorStatus +} + +// GetRequiredConditions implements Component. +func (o *CloudOrchestrator) GetRequiredConditions() sets.Set[string] { + // ToDo: Compute a more precise set of expected conditions based on the spec instead of returning a static set. + return sets.New(o.Type().HealthyCondition(), o.Type().ReconciliationCondition()) +} diff --git a/api/core/v1alpha1/cloudorchestrator_defaults.go b/api/core/v1alpha1/cloudorchestrator_defaults.go new file mode 100644 index 0000000..bf5fd8a --- /dev/null +++ b/api/core/v1alpha1/cloudorchestrator_defaults.go @@ -0,0 +1,12 @@ +package v1alpha1 + +// Default sets defaults. +// This modifies the receiver object. +// Note that only the parts which belong to the configured type are defaulted, everything else is ignored. +func (cos *CloudOrchestratorSpec) Default() {} + +// Validate validates the configuration. +// Only the configuration that belongs to the configured type is validated, configuration for other types is ignored. +func (cos *CloudOrchestratorSpec) Validate(path string, morePaths ...string) error { + return nil +} diff --git a/api/core/v1alpha1/cloudorchestrator_types.go b/api/core/v1alpha1/cloudorchestrator_types.go new file mode 100644 index 0000000..be86e6f --- /dev/null +++ b/api/core/v1alpha1/cloudorchestrator_types.go @@ -0,0 +1,128 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// CloudOrchestratorConfiguration contains the configuration for setting up the CloudOrchestrator component in a ManagedControlPlane. +type CloudOrchestratorConfiguration struct { + // Crossplane defines the configuration for setting up the Crossplane component in a ManagedControlPlane. + // +kubebuilder:validation:Optional + Crossplane *CrossplaneConfig `json:"crossplane,omitempty"` + + // BTPServiceOperator defines the configuration for setting up the BTPServiceOperator component in a ManagedControlPlane. + // +kubebuilder:validation:Optional + BTPServiceOperator *BTPServiceOperatorConfig `json:"btpServiceOperator,omitempty"` + + // ExternalSecretsOperator defines the configuration for setting up the ExternalSecretsOperator component in a ManagedControlPlane. + // +kubebuilder:validation:Optional + ExternalSecretsOperator *ExternalSecretsOperatorConfig `json:"externalSecretsOperator,omitempty"` + + // Kyverno defines the configuration for setting up the Kyverno component in a ManagedControlPlane. + // +kubebuilder:validation:Optional + Kyverno *KyvernoConfig `json:"kyverno,omitempty"` + + // Flux defines the configuration for setting up the Flux component in a ManagedControlPlane. + // +kubebuilder:validation:Optional + Flux *FluxConfig `json:"flux,omitempty"` +} + +// CloudOrchestratorSpec defines the desired state of CloudOrchestrator +type CloudOrchestratorSpec struct { + CloudOrchestratorConfiguration `json:",inline"` +} + +// ExternalCloudOrchestratorStatus contains the status of the CloudOrchestrator component. +type ExternalCloudOrchestratorStatus struct { +} + +// CloudOrchestratorStatus defines the observed state of CloudOrchestrator +type CloudOrchestratorStatus struct { + CommonComponentStatus `json:",inline"` + *ExternalCloudOrchestratorStatus `json:",inline"` + + // Number of enabled components. + // +kubebuilder:validation:Optional + ComponentsEnabled int `json:"componentsEnabled"` + + // Number of healthy components. + // +kubebuilder:validation:Optional + ComponentsHealthy int `json:"componentsHealthy"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +// +kubebuilder:resource:shortName=co +// +kubebuilder:printcolumn:name="Successfully_Reconciled",type=string,JSONPath=`.status.conditions[?(@.type=="CloudOrchestratorReconciliation")].status` +// +kubebuilder:printcolumn:name="Deleted",type="date",JSONPath=".metadata.deletionTimestamp" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" + +// CloudOrchestrator is the Schema for the internal CloudOrchestrator API +type CloudOrchestrator struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec CloudOrchestratorSpec `json:"spec,omitempty"` + Status CloudOrchestratorStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// CloudOrchestratorList contains a list of CloudOrchestrator +type CloudOrchestratorList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []CloudOrchestrator `json:"items"` +} + +func init() { + SchemeBuilder.Register(&CloudOrchestrator{}, &CloudOrchestratorList{}) +} + +// CrossplaneConfig defines the configuration of Crossplane +type CrossplaneConfig struct { + // The Version of Crossplane to install. + // +kubebuilder:validation:Required + Version string `json:"version"` + + Providers []*CrossplaneProviderConfig `json:"providers,omitempty"` +} + +// BTPServiceOperatorConfig defines the configuration of BTPServiceOperator +type BTPServiceOperatorConfig struct { + // The Version of BTP Service Operator to install. + // +kubebuilder:validation:Required + Version string `json:"version"` +} + +// ExternalSecretsOperatorConfig defines the configuration of ExternalSecretsOperator +type ExternalSecretsOperatorConfig struct { + // The Version of External Secrets Operator to install. + // +kubebuilder:validation:Required + Version string `json:"version"` +} + +// KyvernoConfig defines the configuration of Kyverno +type KyvernoConfig struct { + // The Version of Kyverno to install. + // +kubebuilder:validation:Required + Version string `json:"version"` +} + +// FluxConfig defines the configuration of Flux +type FluxConfig struct { + // The Version of Flux to install. + // +kubebuilder:validation:Required + Version string `json:"version"` +} + +type CrossplaneProviderConfig struct { + // Name of the provider. + // Using a well-known name will automatically configure the "package" field. + // +kubebuilder:validation:Required + Name string `json:"name"` + + // Version of the provider to install. + // +kubebuilder:validation:Required + Version string `json:"version"` +} diff --git a/api/core/v1alpha1/common_config_types.go b/api/core/v1alpha1/common_config_types.go new file mode 100644 index 0000000..16b5166 --- /dev/null +++ b/api/core/v1alpha1/common_config_types.go @@ -0,0 +1,14 @@ +package v1alpha1 + +// CommonConfig contains configuration that is shared between multiple components. +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.desiredRegion)|| has(self.desiredRegion)",message="desiredRegion is required once set" +type CommonConfig struct { + // DesiredRegion allows customers to specify a desired region proximity. + // +kubebuilder:validation:Optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="RegionSpecification is immutable" + DesiredRegion *RegionSpecification `json:"desiredRegion,omitempty"` +} + +// InternalCommonConfig contains internal configuration that is shared between multiple components. +type InternalCommonConfig struct { +} diff --git a/api/core/v1alpha1/component_types.go b/api/core/v1alpha1/component_types.go new file mode 100644 index 0000000..3c4a4d4 --- /dev/null +++ b/api/core/v1alpha1/component_types.go @@ -0,0 +1,141 @@ +// +kubebuilder:object:generate=true +package v1alpha1 + +import ( + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type ComponentType string + +const ( + ComponentTypeUndefined ComponentType = "Undefined" +) + +type ComponentConditionStatus string + +const ( + // ComponentConditionStatusUnknown represents an unknown status for the condition. + ComponentConditionStatusUnknown ComponentConditionStatus = "Unknown" + // ComponentConditionStatusTrue marks the condition as true. + ComponentConditionStatusTrue ComponentConditionStatus = "True" + // ComponentConditionStatusFalse marks the condition as false. + ComponentConditionStatusFalse ComponentConditionStatus = "False" +) + +type ComponentCondition struct { + // Type is the type of the condition. + // This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + Type string `json:"type"` + + // Status is the status of the condition. + Status ComponentConditionStatus `json:"status"` + + // Reason is expected to contain a CamelCased string that provides further information regarding the condition. + // It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + // It is optional, but should be filled at least when Status is not "True". + // +optional + Reason string `json:"reason,omitempty"` + + // Message contains further details regarding the condition. + // It is meant for human users, Reason should be used for programmatic evaluation instead. + // It is optional, but should be filled at least when Status is not "True". + // +optional + Message string `json:"message,omitempty"` + + // LastTransitionTime specifies the time when this condition's status last changed. + LastTransitionTime metav1.Time `json:"lastTransitionTime,omitempty"` +} + +type ObservedGenerations struct { + // Resource contains the last generation of this resource that has been handled by the controller. + // This refers to metadata.generation of this resource. + Resource int64 `json:"resource"` + + // ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + // Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + // This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + // This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + ManagedControlPlane int64 `json:"managedControlPlane"` + + // InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + // Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + // This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + // If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + // the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + InternalConfiguration int64 `json:"internalConfiguration"` +} + +// CommonComponentStatus contains fields which all component resources' statuses must contain. +type CommonComponentStatus struct { + // Conditions containts the conditions of the component. + // For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + // This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + Conditions ComponentConditionList `json:"conditions,omitempty"` + + // ObservedGenerations contains information about the observed generations of a component. + // This information is required to determine whether a component's controller has already processed some changes or not. + ObservedGenerations ObservedGenerations `json:"observedGenerations,omitempty"` +} + +// ComponentConditionList is a list of ComponentConditions. +type ComponentConditionList []ComponentCondition + +// ComponentConditionStatusFromBoolPtr converts a bool pointer into the corresponding ComponentConditionStatus. +// If nil, "Unknown" is returned. +func ComponentConditionStatusFromBoolPtr(src *bool) ComponentConditionStatus { + if src == nil { + return ComponentConditionStatusUnknown + } + return ComponentConditionStatusFromBool(*src) +} + +// ComponentConditionStatusFromBool converts a bool into the corresponding ComponentConditionStatus. +func ComponentConditionStatusFromBool(src bool) ComponentConditionStatus { + if src { + return ComponentConditionStatusTrue + } + return ComponentConditionStatusFalse +} + +// IsTrue returns true if the ComponentCondition's status is "True". +// Note that the status can be "Unknown", so !IsTrue() is not the same as IsFalse(). +func (cc ComponentCondition) IsTrue() bool { + return cc.Status == ComponentConditionStatusTrue +} + +// IsFalse returns true if the ComponentCondition's status is "False". +// Note that the status can be "Unknown", so !IsFalse() is not the same as IsTrue(). +func (cc ComponentCondition) IsFalse() bool { + return cc.Status == ComponentConditionStatusFalse +} + +// IsUnknown returns true if the ComponentCondition's status is "Unknown". +func (cc ComponentCondition) IsUnknown() bool { + return cc.Status == ComponentConditionStatusUnknown +} + +// Finalizer returns the finalizer this component sets on its own resources. +func (ct ComponentType) Finalizer() string { + return fmt.Sprintf("%s.%s", strings.ToLower(string(ct)), BaseDomain) +} + +// DependencyFinalizer returns the finalizer this component uses to mark its dependencies. +func (ct ComponentType) DependencyFinalizer() string { + return fmt.Sprintf("%s%s", DependencyFinalizerPrefix, strings.ToLower(string(ct))) +} + +// ReconciliationCondition returns the name of the condition that holds the information whether the last +// reconciliation of the component was successful or not. +// It resolves to "Reconciliation". +func (ct ComponentType) ReconciliationCondition() string { + return fmt.Sprintf("%sReconciliation", string(ct)) +} + +// HealthyCondition returns the name of the condition that holds the information whether the component is healthy or not. +// It resolves to "Healthy". +func (ct ComponentType) HealthyCondition() string { + return fmt.Sprintf("%sHealthy", string(ct)) +} diff --git a/api/core/v1alpha1/constants.go b/api/core/v1alpha1/constants.go new file mode 100644 index 0000000..99652a4 --- /dev/null +++ b/api/core/v1alpha1/constants.go @@ -0,0 +1,79 @@ +package v1alpha1 + +const ( + // GENERAL + + // BaseDomain is the CoLa base domain. + // Components should prefix it with their own name. + BaseDomain = "openmcp.cloud" + + // OperationAnnotation is the general operation annotation. + OperationAnnotation = BaseDomain + "/operation" + + // OperationAnnotationValueReconcile is the value of the operation annotation which should cause a reconcile. + OperationAnnotationValueReconcile = "reconcile" + + // OperationAnnotationValueIgnore is the value of the operation annotation which causes the responsible controller to ignore this resource. + OperationAnnotationValueIgnore = "ignore" + + // ManagedControlPlaneBackReferenceLabelName contains the name of the creating ManagedControlPlane resource, in case the ManagedControlPlane's status is lost. + ManagedControlPlaneBackReferenceLabelName = BaseDomain + "/mcp-name" + // ManagedControlPlaneBackReferenceLabelNamespace contains the namespace of the creating ManagedControlPlane resource, in case the ManagedControlPlane's status is lost. + ManagedControlPlaneBackReferenceLabelNamespace = BaseDomain + "/mcp-namespace" + // ManagedControlPlaneBackReferenceLabelProject contains the Project of the ManagedControlPlane resource. + // Note that this is only set if the corresponding project can be extracted from the containing namespace's metadata. + // This label is for user information only and has no internal usage. + ManagedControlPlaneBackReferenceLabelProject = BaseDomain + "/mcp-project" + // ManagedControlPlaneBackReferenceLabelWorkspace contains the Workspace of the ManagedControlPlane resource. + // Note that this is only set if the corresponding workspace can be extracted from the containing namespace's metadata. + // This label is for user information only and has no internal usage. + ManagedControlPlaneBackReferenceLabelWorkspace = BaseDomain + "/mcp-workspace" + + // ManagedControlPlaneGenerationLabel contains the generation of the managedcontrolplane from which this resource was created. + // It is used to check whether component resources are outdated. + ManagedControlPlaneGenerationLabel = BaseDomain + "/mcp-generation" + // InternalConfigurationGenerationLabel contains the generation of the internalconfiguration that was used for this resource, if any. + // It is used to check whether component resources are outdated. + InternalConfigurationGenerationLabel = BaseDomain + "/ic-generation" + + // ManagedByLabel is added to resources created by the operator. + ManagedByLabel = BaseDomain + "/managed-by" + + CreatedByAnnotation = BaseDomain + "/created-by" + + DisplayNameAnnotation = BaseDomain + "/display-name" + + // ComponentTypeLabel is added to the component's specific resources. + // This allows generic functions (working on client.Object) to identify the component the resource belongs to. + ComponentTypeLabel = BaseDomain + "/component" + + DependencyFinalizerPrefix = "dependency." + BaseDomain + "/" + + // SystemNamespace is the name of the system namespace. + // This should be used whenever a namespace is required. + SystemNamespace = "openmcp-system" + + // ProjectWorkspaceOperatorProjectLabel is the label that the PWO attaches to a namespace if that namespace belongs to a project. + // Technically, this should be imported from the PWO, but it is not worth the dependency. + ProjectWorkspaceOperatorProjectLabel = "core.openmcp.cloud/project" + // ProjectWorkspaceOperatorWorkspaceLabel is the label that the PWO attaches to a namespace if that namespace belongs to a workspace. + // Technically, this should be imported from the PWO, but it is not worth the dependency. + ProjectWorkspaceOperatorWorkspaceLabel = "core.openmcp.cloud/workspace" + + // MANAGEDCONTROLPLANE + + // ManagedControlPlaneDomain is the domain for the v1alpha1.ManagedControlPlane controller. + ManagedControlPlaneDomain = "managedcontrolplane." + BaseDomain + + // ManagedControlPlaneFinalizer is the finalizer for the ManagedControlPlane resource. + ManagedControlPlaneFinalizer = "finalizer." + ManagedControlPlaneDomain + + // ManagedControlPlaneDeletionConfirmationAnnotation is the annotation, which needs to be set true before a mcp can be deleted + ManagedControlPlaneDeletionConfirmationAnnotation = "confirmation." + BaseDomain + "/deletion" + + // APIServer + + APIServerDomain = "apiserver." + BaseDomain + + ManagedByAPIServerLabel = APIServerDomain + "/managed" +) diff --git a/api/core/v1alpha1/groupversion_info.go b/api/core/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..94e1f37 --- /dev/null +++ b/api/core/v1alpha1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1alpha1 contains API Schema definitions for the core API group +// +kubebuilder:object:generate=true +// +groupName=core.openmcp.cloud +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "core.openmcp.cloud", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/api/core/v1alpha1/internalconfiguration_types.go b/api/core/v1alpha1/internalconfiguration_types.go new file mode 100644 index 0000000..3022f41 --- /dev/null +++ b/api/core/v1alpha1/internalconfiguration_types.go @@ -0,0 +1,41 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// InternalConfigurationComponents defines the components that are part of the internal configuration. +type InternalConfigurationComponents struct { + APIServer *APIServerInternalConfiguration `json:"apiServer,omitempty"` +} + +// InternalConfigurationSpec defines additional configuration for a managedcontrolplane. +type InternalConfigurationSpec struct { + *InternalCommonConfig `json:",inline"` + + Components InternalConfigurationComponents `json:"components,omitempty"` +} + +//+kubebuilder:object:root=true + +// InternalConfiguration is the Schema for the InternalConfigurations API +// +kubebuilder:resource:shortName=icfg +type InternalConfiguration struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec InternalConfigurationSpec `json:"spec,omitempty"` +} + +//+kubebuilder:object:root=true + +// InternalConfigurationList contains a list of InternalConfiguration +type InternalConfigurationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []InternalConfiguration `json:"items"` +} + +func init() { + SchemeBuilder.Register(&InternalConfiguration{}, &InternalConfigurationList{}) +} diff --git a/api/core/v1alpha1/landscaper_component.go b/api/core/v1alpha1/landscaper_component.go new file mode 100644 index 0000000..355d193 --- /dev/null +++ b/api/core/v1alpha1/landscaper_component.go @@ -0,0 +1,49 @@ +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/util/sets" + + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +const LandscaperComponent ComponentType = "Landscaper" + +// Type implements Component. +func (*Landscaper) Type() ComponentType { + return LandscaperComponent +} + +// GetSpec implements Component. +func (ls *Landscaper) GetSpec() any { + return &ls.Spec +} + +// SetSpec implements Component. +func (ls *Landscaper) SetSpec(cfg any) error { + lsSpec, ok := cfg.(*LandscaperSpec) + if !ok { + return openmcperrors.ErrWrongComponentConfigType + } + ls.Spec = *lsSpec + return nil +} + +func (ls *Landscaper) GetCommonStatus() CommonComponentStatus { + return ls.Status.CommonComponentStatus +} + +// SetCommonStatus implements Component. +func (ls *Landscaper) SetCommonStatus(status CommonComponentStatus) { + ls.Status.CommonComponentStatus = status +} + +// GetExternalStatus implements Component. +func (ls *Landscaper) GetExternalStatus() any { + return ls.Status.ExternalLandscaperStatus +} + +// GetRequiredConditions implements Component. +func (ls *Landscaper) GetRequiredConditions() sets.Set[string] { + // ToDo: Compute a more precise set of expected conditions based on the spec instead of returning a static set. + return sets.New(ls.Type().HealthyCondition(), ls.Type().ReconciliationCondition()) +} diff --git a/api/core/v1alpha1/landscaper_defaults.go b/api/core/v1alpha1/landscaper_defaults.go new file mode 100644 index 0000000..3c06bfc --- /dev/null +++ b/api/core/v1alpha1/landscaper_defaults.go @@ -0,0 +1,20 @@ +package v1alpha1 + +// Default sets defaults. +// This modifies the receiver object. +// Note that only the parts which belong to the configured type are defaulted, everything else is ignored. +func (lss *LandscaperSpec) Default() { + if lss.Deployers == nil { + lss.Deployers = []string{ + "helm", + "manifest", + "container", + } + } +} + +// Validate validates the configuration. +// Only the configuration that belongs to the configured type is validated, configuration for other types is ignored. +func (lss *LandscaperSpec) Validate(path string, morePaths ...string) error { + return nil +} diff --git a/api/core/v1alpha1/landscaper_types.go b/api/core/v1alpha1/landscaper_types.go new file mode 100644 index 0000000..73b342e --- /dev/null +++ b/api/core/v1alpha1/landscaper_types.go @@ -0,0 +1,68 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// LandscaperConfiguration contains the configuration which is required for setting up a LaaS instance. +type LandscaperConfiguration struct { + // Deployers is the list of deployers that should be installed. + // +optional + Deployers []string `json:"deployers,omitempty"` +} + +// ExternalLandscaperStatus contains the status of a LaaS instance. +type ExternalLandscaperStatus struct { +} + +// LandscaperStatus contains the landscaper status and potentially other fields which should not be exposed to the customer. +type LandscaperStatus struct { + CommonComponentStatus `json:",inline"` + *ExternalLandscaperStatus `json:",inline"` + + // LandscaperDeploymentInfo contains information about the corresponding LandscaperDeployment resource. + // +optional + LandscaperDeploymentInfo *LandscaperDeploymentInfo `json:"landscaperDeployment,omitempty"` +} + +// LandscaperDeploymentInfo contains information about the corresponding Landscaper deployment resource. +type LandscaperDeploymentInfo struct { + // Name is the name of the Landscaper deployment. + Name string `json:"name"` + // Namespace is the namespace of the Landscaper deployment. + Namespace string `json:"namespace"` +} + +// LandscaperSpec contains the Landscaper configuration and potentially other fields which should not be exposed to the customer. +type LandscaperSpec struct { + LandscaperConfiguration `json:",inline"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// Landscaper is the Schema for the laasinstances API +// +kubebuilder:resource:shortName=ls +// +kubebuilder:printcolumn:name="Successfully_Reconciled",type=string,JSONPath=`.status.conditions[?(@.type=="LandscaperReconciliation")].status` +// +kubebuilder:printcolumn:name="Deleted",type="date",JSONPath=".metadata.deletionTimestamp" +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +type Landscaper struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec LandscaperSpec `json:"spec,omitempty"` + Status LandscaperStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// LandscaperList contains a list of Landscaper +type LandscaperList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Landscaper `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Landscaper{}, &LandscaperList{}) +} diff --git a/api/core/v1alpha1/managedcomponent_types.go b/api/core/v1alpha1/managedcomponent_types.go new file mode 100644 index 0000000..ecb8d7a --- /dev/null +++ b/api/core/v1alpha1/managedcomponent_types.go @@ -0,0 +1,44 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ManagedComponentSpec defines the desired state of ManagedComponent. +type ManagedComponentSpec struct{} + +// ManagedComponentStatus defines the observed state of ManagedComponent. +type ManagedComponentStatus struct { + Versions []string `json:"versions"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster +// +kubebuilder:printcolumn:name="Name",type="string",JSONPath=".spec.name" +// +kubebuilder:printcolumn:name="Versions",type="string",JSONPath=".status.versions" + +// ManagedComponent is the Schema for the managedcomponents API. +type ManagedComponent struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ManagedComponentSpec `json:"spec,omitempty"` + Status ManagedComponentStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ManagedComponentList contains a list of ManagedComponent. +type ManagedComponentList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ManagedComponent `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ManagedComponent{}, &ManagedComponentList{}) +} diff --git a/api/core/v1alpha1/managedcontrolplane_types.go b/api/core/v1alpha1/managedcontrolplane_types.go new file mode 100644 index 0000000..2803019 --- /dev/null +++ b/api/core/v1alpha1/managedcontrolplane_types.go @@ -0,0 +1,123 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ManagedControlPlaneComponents contains the configuration for the components of a ManagedControlPlane. +// +kubebuilder:validation:XValidation:rule="!has(oldSelf.apiServer)|| has(self.apiServer)",message="apiServer is required once set" +type ManagedControlPlaneComponents struct { + // +kubebuilder:default={"type":"GardenerDedicated"} + APIServer *APIServerConfiguration `json:"apiServer,omitempty"` + + Landscaper *LandscaperConfiguration `json:"landscaper,omitempty"` + + CloudOrchestratorConfiguration `json:",inline"` +} + +// ManagedControlPlaneSpec defines the desired state of ManagedControlPlane. +type ManagedControlPlaneSpec struct { + // DisabledComponents contains a list of component types. + // The resources for these components will still be generated, but they will get the ignore operation annotation, so they should not be processed by their respective controllers. + DisabledComponents []ComponentType `json:"disabledComponents,omitempty"` + + // CommonConfig contains configuration that is passed to all component controllers. + *CommonConfig `json:",inline"` + + // Authentication contains the configuration for the enabled OpenID Connect identity providers + Authentication *AuthenticationConfiguration `json:"authentication,omitempty"` + + // Authorization contains the configuration of the subjects assigned to control plane roles + Authorization *AuthorizationConfiguration `json:"authorization,omitempty"` + + // Components contains the configuration for Components like APIServer, Landscaper, CloudOrchestrator. + Components ManagedControlPlaneComponents `json:"components"` +} + +// ManagedControlPlaneComponentsStatus contains the status of the components of a ManagedControlPlane. +type ManagedControlPlaneComponentsStatus struct { + APIServer *ExternalAPIServerStatus `json:"apiServer,omitempty"` + + Landscaper *ExternalLandscaperStatus `json:"landscaper,omitempty"` + + CloudOrchestrator *ExternalCloudOrchestratorStatus `json:"cloudOrchestrator,omitempty"` + + Authentication *ExternalAuthenticationStatus `json:"authentication,omitempty"` + + Authorization *ExternalAuthorizationStatus `json:"authorization,omitempty"` +} + +// ManagedControlPlaneStatus defines the observed state of ManagedControlPlane. +type ManagedControlPlaneStatus struct { + ManagedControlPlaneMetaStatus `json:",inline"` + + // Conditions collects the conditions of all components. + Conditions []ManagedControlPlaneComponentCondition `json:"conditions,omitempty"` + + Components ManagedControlPlaneComponentsStatus `json:"components,omitempty"` +} + +type ManagedControlPlaneComponentCondition struct { + ComponentCondition `json:",inline"` + + // ManagedBy contains the information which component manages this condition. + ManagedBy ComponentType `json:"managedBy"` +} + +type ManagedControlPlaneMetaStatus struct { + // ObservedGeneration is the last generation of this resource that has successfully been reconciled. + ObservedGeneration int64 `json:"observedGeneration"` + + // Status is the current status of the ManagedControlPlane. + // It is "Deleting" if the ManagedControlPlane is being deleted. + // It is "Ready" if all conditions are true, and "Not Ready" otherwise. + Status MCPStatus `json:"status"` + + // Message contains an optional message. + // +optional + Message string `json:"message,omitempty"` +} + +// MCPStatus is a type for the status of a ManagedControlPlane. +// Use NewMCPStatus to create a new MCPStatus, or use one of the predefined constants. +type MCPStatus string + +const ( + // MCPStatusReady indicates that the ManagedControlPlane is ready. + MCPStatusReady MCPStatus = "Ready" + + // MCPStatusNotReady indicates that the ManagedControlPlane is not ready. + MCPStatusNotReady MCPStatus = "Not Ready" + + // MCPStatusDeleting indicates that the ManagedControlPlane is being deleted. + MCPStatusDeleting MCPStatus = "Deleting" +) + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// ManagedControlPlane is the Schema for the ManagedControlPlane API +// +kubebuilder:resource:shortName=mcp +// +kubebuilder:printcolumn:name="Status",type=string,JSONPath=`.status.status` +// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:validation:XValidation:rule="size(self.metadata.name) <= 36",message="name must not be longer than 36 characters" +type ManagedControlPlane struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ManagedControlPlaneSpec `json:"spec,omitempty"` + Status ManagedControlPlaneStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// ManagedControlPlaneList contains a list of ManagedControlPlane +type ManagedControlPlaneList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ManagedControlPlane `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ManagedControlPlane{}, &ManagedControlPlaneList{}) +} diff --git a/api/core/v1alpha1/managedcontrolplane_webhook.go b/api/core/v1alpha1/managedcontrolplane_webhook.go new file mode 100644 index 0000000..e8d5cba --- /dev/null +++ b/api/core/v1alpha1/managedcontrolplane_webhook.go @@ -0,0 +1,121 @@ +package v1alpha1 + +import ( + "context" + "fmt" + "reflect" + + "k8s.io/apimachinery/pkg/runtime" + apierrors "k8s.io/apimachinery/pkg/util/errors" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +// log is for logging in this package. +var managedcontrolplanelog = logf.Log.WithName("managedcontrolplane-resource") + +// SetupWebhookWithManager will setup the manager to manage the webhooks +func (r *ManagedControlPlane) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + WithValidator(r). + Complete() +} + +// +kubebuilder:webhook:path=/validate-core-openmcp-cloud-v1alpha1-managedcontrolplane,mutating=false,failurePolicy=fail,sideEffects=None,groups=core.openmcp.cloud,resources=managedcontrolplanes,verbs=delete,versions=v1alpha1,name=vmanagedcontrolplane.kb.io,admissionReviewVersions=v1 + +var _ webhook.CustomValidator = &ManagedControlPlane{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *ManagedControlPlane) ValidateCreate(_ context.Context, _ runtime.Object) (admission.Warnings, error) { + managedcontrolplanelog.Info("validate create - this should never be triggered", "name", r.Name) + + // no-op + return nil, nil +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *ManagedControlPlane) ValidateUpdate(_ context.Context, old runtime.Object, newObj runtime.Object) (admission.Warnings, error) { + oldMcp, ok := old.(*ManagedControlPlane) + if !ok { + return nil, fmt.Errorf("object not supported") + } + + newMcp, ok := newObj.(*ManagedControlPlane) + if !ok { + return nil, fmt.Errorf("object not supported") + } + var errorList []error + + updateValidators := []func(*ManagedControlPlane, *ManagedControlPlane) error{ + validateUpdateDesiredRegion, + validateUpdateAPIServerUpdate, + } + + for _, validator := range updateValidators { + if err := validator(newMcp, oldMcp); err != nil { + managedcontrolplanelog.Error(fmt.Errorf("update validation failed"), err.Error()) + errorList = append(errorList, err) + } + } + + return nil, apierrors.NewAggregate(errorList) +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *ManagedControlPlane) ValidateDelete(_ context.Context, obj runtime.Object) (admission.Warnings, error) { + managedcontrolplanelog.Info("validate delete", "name", r.Name) + + mcp, ok := obj.(*ManagedControlPlane) + if !ok { + return nil, fmt.Errorf("object not supported") + } + + if mcp.Annotations[ManagedControlPlaneDeletionConfirmationAnnotation] == "true" { + return nil, nil + } + return nil, fmt.Errorf("ManagedControlPlane %q requires annotation %q to be set to true, before it can be deleted", r.Name, ManagedControlPlaneDeletionConfirmationAnnotation) +} + +// validateUpdateDesiredRegion ensures that DesiredRegion is immutable +func validateUpdateDesiredRegion(newMCP, oldMcp *ManagedControlPlane) error { + if oldMcp.Spec.CommonConfig == nil || oldMcp.Spec.CommonConfig.DesiredRegion == nil { + return nil + } + if !reflect.DeepEqual(newMCP.Spec.DesiredRegion, oldMcp.Spec.DesiredRegion) { + return fmt.Errorf("spec.desiredRegion is immutable") + } + return nil +} + +// validateUpdateAPIServerUpdate ensure that APIServer is immutable while allowing delete +func validateUpdateAPIServerUpdate(newMCP, oldMcp *ManagedControlPlane) error { + if oldMcp.Spec.Components.APIServer == nil { // APIServer is already unset, nothing to do here + return nil + } + + if newMCP.Spec.Components.APIServer == nil { // APIServer is being deleted. + return newMCP.validateUpdateAPIServerRemove(oldMcp) + } + if !reflect.DeepEqual(newMCP.Spec.Components.APIServer, oldMcp.Spec.Components.APIServer) { + return fmt.Errorf("spec.components.apiServer is immutable") + } + return nil +} + +// validateUpdateAPIServerRemove ensures that APIserver is not removed before all other components are removed +func (r *ManagedControlPlane) validateUpdateAPIServerRemove(oldMcp *ManagedControlPlane) error { + if oldMcp.Spec.Components.APIServer != nil && r.Spec.Components.APIServer == nil { + if r.Spec.Components.Landscaper != nil || + r.Spec.Components.CloudOrchestratorConfiguration.Flux != nil || + r.Spec.Components.CloudOrchestratorConfiguration.Kyverno != nil || + r.Spec.Components.CloudOrchestratorConfiguration.Crossplane != nil || + r.Spec.Components.CloudOrchestratorConfiguration.BTPServiceOperator != nil || + r.Spec.Components.CloudOrchestratorConfiguration.ExternalSecretsOperator != nil { + return fmt.Errorf("spec.components.apiServer can't be removed while other components are configured") + } + } + return nil +} diff --git a/api/core/v1alpha1/managedcontrolplane_webhook_test.go b/api/core/v1alpha1/managedcontrolplane_webhook_test.go new file mode 100644 index 0000000..10bfd93 --- /dev/null +++ b/api/core/v1alpha1/managedcontrolplane_webhook_test.go @@ -0,0 +1,129 @@ +package v1alpha1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/uuid" +) + +var _ = Describe("ManagedControlPlane Webhook", func() { + + Context("When deleting a ManagedControlPlane", func() { + It("Should deny if the annotation is not set", func() { + var err error + + namespace := string(uuid.NewUUID()) + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) + Expect(err).ShouldNot(HaveOccurred()) + + mcp := &ManagedControlPlane{ObjectMeta: metav1.ObjectMeta{Name: "mcp", Namespace: namespace}} + err = k8sClient.Create(ctx, mcp) + Expect(err).ShouldNot(HaveOccurred()) + + err = k8sClient.Delete(ctx, mcp) + Expect(apierrors.IsForbidden(err)).Should(BeTrue()) + }) + + It("Should admit the deletion if the annoation was set", func() { + var err error + + namespace := string(uuid.NewUUID()) + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) + Expect(err).ShouldNot(HaveOccurred()) + + annotations := map[string]string{ + ManagedControlPlaneDeletionConfirmationAnnotation: "true", + } + mcp := &ManagedControlPlane{ObjectMeta: metav1.ObjectMeta{Name: "mcp", Namespace: namespace, Annotations: annotations}} + err = k8sClient.Create(ctx, mcp) + Expect(err).ShouldNot(HaveOccurred()) + + err = k8sClient.Delete(ctx, mcp) + Expect(err).ShouldNot(HaveOccurred()) + }) + }) + + Context("When updating a ManagedControlPlane", func() { + + It("Should deny updates to spec.desiredRegion", func() { + var err error + + namespace := string(uuid.NewUUID()) + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) + Expect(err).ShouldNot(HaveOccurred()) + + mcp := &ManagedControlPlane{ObjectMeta: metav1.ObjectMeta{Name: "mcp", Namespace: namespace}} + err = k8sClient.Create(ctx, mcp) + Expect(err).ShouldNot(HaveOccurred()) + + mcp.Spec.CommonConfig = &CommonConfig{ + DesiredRegion: &RegionSpecification{ + Name: "europe", + Direction: "east", + }, + } + + err = k8sClient.Update(ctx, mcp) + Expect(err).ShouldNot(HaveOccurred()) + + mcp.Spec.DesiredRegion.Direction = "west" + + err = k8sClient.Update(ctx, mcp) + Expect(err).To(HaveOccurred()) + // shouldn't be deleted + mcp.Spec.CommonConfig = nil + + err = k8sClient.Update(ctx, mcp) + Expect(err).To(HaveOccurred()) + }) + + It("Should deny update to spec.components.apiServer", func() { + var err error + + namespace := string(uuid.NewUUID()) + err = k8sClient.Create(ctx, &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}}) + Expect(err).ShouldNot(HaveOccurred()) + + mcp := &ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mcp", + Namespace: namespace, + }, + Spec: ManagedControlPlaneSpec{ + Components: ManagedControlPlaneComponents{ + APIServer: &APIServerConfiguration{ + Type: Gardener, + GardenerConfig: &GardenerConfiguration{ + Region: "eu-west-1", + }, + }, + }, + }, + } + err = k8sClient.Create(ctx, mcp) + Expect(err).ShouldNot(HaveOccurred()) + + mcp.Spec.Components.APIServer.Type = GardenerDedicated + + err = k8sClient.Update(ctx, mcp) + Expect(err).To(HaveOccurred()) + + // cover GardnerConfig as well + mcp.Spec.Components.APIServer.GardenerConfig.Region = "eu-east-2" + + err = k8sClient.Update(ctx, mcp) + Expect(err).To(HaveOccurred()) + + // shouldn't be deleted + mcp.Spec.Components.APIServer = nil + + err = k8sClient.Update(ctx, mcp) + Expect(err).To(HaveOccurred()) + }) + + }) + +}) diff --git a/api/core/v1alpha1/region_types.go b/api/core/v1alpha1/region_types.go new file mode 100644 index 0000000..b01b185 --- /dev/null +++ b/api/core/v1alpha1/region_types.go @@ -0,0 +1,44 @@ +package v1alpha1 + +import "fmt" + +// Region represents a supported region. +// +kubebuilder:validation:Enum=northamerica;southamerica;europe;asia;africa;australia +type Region string + +// Direction represents a direction within a region. +// +kubebuilder:validation:Enum=north;east;south;west;central +type Direction string + +const ( + AFRICA Region = "africa" + ASIA Region = "asia" + AUSTRALIA Region = "australia" + EUROPE Region = "europe" + NORTHAMERICA Region = "northamerica" + SOUTHAMERICA Region = "southamerica" +) + +var AllRegions = []Region{AFRICA, ASIA, AUSTRALIA, EUROPE, NORTHAMERICA, SOUTHAMERICA} + +const ( + NORTH Direction = "north" + EAST Direction = "east" + SOUTH Direction = "south" + WEST Direction = "west" + CENTRAL Direction = "central" +) + +var AllDirections = []Direction{NORTH, EAST, SOUTH, WEST, CENTRAL} + +type RegionSpecification struct { + // Name is the name of the region. + Name Region `json:"name,omitempty"` + + // Direction is the direction within the region. + Direction Direction `json:"direction,omitempty"` +} + +func (r RegionSpecification) String() string { + return fmt.Sprintf("%s-%s", r.Name, r.Direction) +} diff --git a/api/core/v1alpha1/shared_types.go b/api/core/v1alpha1/shared_types.go new file mode 100644 index 0000000..e487b31 --- /dev/null +++ b/api/core/v1alpha1/shared_types.go @@ -0,0 +1,33 @@ +package v1alpha1 + +// NamespacedObjectReference is a reference to a namespaced k8s object. +type NamespacedObjectReference struct { + // Name is the object's name. + Name string `json:"name"` + // Namespace is the object's namespace. + Namespace string `json:"namespace"` +} + +// SecretReference is a reference to a specific key inside a secret. +type SecretReference struct { + NamespacedObjectReference `json:",inline"` + // Key is the key inside the secret. + Key string `json:"key"` +} + +// LocalSecretReference is a reference to a specific key inside a secret in the same namespace +// as the object referencing it. +type LocalSecretReference struct { + // Name is the secret name. + Name string `json:"name"` + // Key is the key inside the secret. + Key string `json:"key"` +} + +// SingleOrMultiStringValue is a type that can hold either a single string value or a list of string values. +type SingleOrMultiStringValue struct { + // Value is a single string value. + Value string `json:"value,omitempty"` + // Values is a list of string values. + Values []string `json:"values,omitempty"` +} diff --git a/api/core/v1alpha1/webhook_suite_test.go b/api/core/v1alpha1/webhook_suite_test.go new file mode 100644 index 0000000..9ef4b2d --- /dev/null +++ b/api/core/v1alpha1/webhook_suite_test.go @@ -0,0 +1,130 @@ +package v1alpha1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + // +kubebuilder:scaffold:imports + + envtestutil "github.com/openmcp-project/controller-utils/pkg/envtest" + admissionv1 "k8s.io/api/admission/v1" + corev1 "k8s.io/api/core/v1" + apimachineryruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var ctx context.Context +var cancel context.CancelFunc + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + // make sure all required binaries are installed + var err error + err = envtestutil.Install("latest") + Expect(err).NotTo(HaveOccurred()) + + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + }, + } + + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := apimachineryruntime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:scheme + + err = corev1.AddToScheme(scheme) // add corev1 types to scheme, so we can create namespaces for tests + Expect(err).NotTo(HaveOccurred()) + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + WebhookServer: webhook.NewServer(webhook.Options{ + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + }), + LeaderElection: false, + Metrics: metricsserver.Options{BindAddress: "0"}, + }) + Expect(err).NotTo(HaveOccurred()) + + err = (&ManagedControlPlane{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + // +kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + return conn.Close() + }).Should(Succeed()) + +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + cancel() + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/api/core/v1alpha1/zz_generated.deepcopy.go b/api/core/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..e7aa341 --- /dev/null +++ b/api/core/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,1801 @@ +//go:build !ignore_autogenerated + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServer) DeepCopyInto(out *APIServer) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServer. +func (in *APIServer) DeepCopy() *APIServer { + if in == nil { + return nil + } + out := new(APIServer) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIServer) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerAccess) DeepCopyInto(out *APIServerAccess) { + *out = *in + if in.CreationTimestamp != nil { + in, out := &in.CreationTimestamp, &out.CreationTimestamp + *out = (*in).DeepCopy() + } + if in.ExpirationTimestamp != nil { + in, out := &in.ExpirationTimestamp, &out.ExpirationTimestamp + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerAccess. +func (in *APIServerAccess) DeepCopy() *APIServerAccess { + if in == nil { + return nil + } + out := new(APIServerAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerConfiguration) DeepCopyInto(out *APIServerConfiguration) { + *out = *in + if in.GardenerConfig != nil { + in, out := &in.GardenerConfig, &out.GardenerConfig + *out = new(GardenerConfiguration) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerConfiguration. +func (in *APIServerConfiguration) DeepCopy() *APIServerConfiguration { + if in == nil { + return nil + } + out := new(APIServerConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerInternalConfiguration) DeepCopyInto(out *APIServerInternalConfiguration) { + *out = *in + if in.GardenerConfig != nil { + in, out := &in.GardenerConfig, &out.GardenerConfig + *out = new(GardenerInternalConfiguration) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerInternalConfiguration. +func (in *APIServerInternalConfiguration) DeepCopy() *APIServerInternalConfiguration { + if in == nil { + return nil + } + out := new(APIServerInternalConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerList) DeepCopyInto(out *APIServerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]APIServer, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerList. +func (in *APIServerList) DeepCopy() *APIServerList { + if in == nil { + return nil + } + out := new(APIServerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *APIServerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerSpec) DeepCopyInto(out *APIServerSpec) { + *out = *in + in.APIServerConfiguration.DeepCopyInto(&out.APIServerConfiguration) + if in.Internal != nil { + in, out := &in.Internal, &out.Internal + *out = new(APIServerInternalConfiguration) + (*in).DeepCopyInto(*out) + } + if in.DesiredRegion != nil { + in, out := &in.DesiredRegion, &out.DesiredRegion + *out = new(RegionSpecification) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerSpec. +func (in *APIServerSpec) DeepCopy() *APIServerSpec { + if in == nil { + return nil + } + out := new(APIServerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerStatus) DeepCopyInto(out *APIServerStatus) { + *out = *in + in.CommonComponentStatus.DeepCopyInto(&out.CommonComponentStatus) + if in.ExternalAPIServerStatus != nil { + in, out := &in.ExternalAPIServerStatus, &out.ExternalAPIServerStatus + *out = new(ExternalAPIServerStatus) + **out = **in + } + if in.AdminAccess != nil { + in, out := &in.AdminAccess, &out.AdminAccess + *out = new(APIServerAccess) + (*in).DeepCopyInto(*out) + } + if in.GardenerStatus != nil { + in, out := &in.GardenerStatus, &out.GardenerStatus + *out = new(GardenerStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerStatus. +func (in *APIServerStatus) DeepCopy() *APIServerStatus { + if in == nil { + return nil + } + out := new(APIServerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuditLogConfig) DeepCopyInto(out *AuditLogConfig) { + *out = *in + out.SecretRef = in.SecretRef + out.PolicyRef = in.PolicyRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuditLogConfig. +func (in *AuditLogConfig) DeepCopy() *AuditLogConfig { + if in == nil { + return nil + } + out := new(AuditLogConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authentication) DeepCopyInto(out *Authentication) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authentication. +func (in *Authentication) DeepCopy() *Authentication { + if in == nil { + return nil + } + out := new(Authentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Authentication) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfiguration) { + *out = *in + if in.EnableSystemIdentityProvider != nil { + in, out := &in.EnableSystemIdentityProvider, &out.EnableSystemIdentityProvider + *out = new(bool) + **out = **in + } + if in.IdentityProviders != nil { + in, out := &in.IdentityProviders, &out.IdentityProviders + *out = make([]IdentityProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationConfiguration. +func (in *AuthenticationConfiguration) DeepCopy() *AuthenticationConfiguration { + if in == nil { + return nil + } + out := new(AuthenticationConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationList) DeepCopyInto(out *AuthenticationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Authentication, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationList. +func (in *AuthenticationList) DeepCopy() *AuthenticationList { + if in == nil { + return nil + } + out := new(AuthenticationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AuthenticationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationSpec) DeepCopyInto(out *AuthenticationSpec) { + *out = *in + in.AuthenticationConfiguration.DeepCopyInto(&out.AuthenticationConfiguration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationSpec. +func (in *AuthenticationSpec) DeepCopy() *AuthenticationSpec { + if in == nil { + return nil + } + out := new(AuthenticationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthenticationStatus) DeepCopyInto(out *AuthenticationStatus) { + *out = *in + in.CommonComponentStatus.DeepCopyInto(&out.CommonComponentStatus) + if in.ExternalAuthenticationStatus != nil { + in, out := &in.ExternalAuthenticationStatus, &out.ExternalAuthenticationStatus + *out = new(ExternalAuthenticationStatus) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthenticationStatus. +func (in *AuthenticationStatus) DeepCopy() *AuthenticationStatus { + if in == nil { + return nil + } + out := new(AuthenticationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Authorization) DeepCopyInto(out *Authorization) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Authorization. +func (in *Authorization) DeepCopy() *Authorization { + if in == nil { + return nil + } + out := new(Authorization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Authorization) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationConfiguration) DeepCopyInto(out *AuthorizationConfiguration) { + *out = *in + if in.RoleBindings != nil { + in, out := &in.RoleBindings, &out.RoleBindings + *out = make([]RoleBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationConfiguration. +func (in *AuthorizationConfiguration) DeepCopy() *AuthorizationConfiguration { + if in == nil { + return nil + } + out := new(AuthorizationConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationList) DeepCopyInto(out *AuthorizationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Authorization, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationList. +func (in *AuthorizationList) DeepCopy() *AuthorizationList { + if in == nil { + return nil + } + out := new(AuthorizationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AuthorizationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationSpec) DeepCopyInto(out *AuthorizationSpec) { + *out = *in + in.AuthorizationConfiguration.DeepCopyInto(&out.AuthorizationConfiguration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationSpec. +func (in *AuthorizationSpec) DeepCopy() *AuthorizationSpec { + if in == nil { + return nil + } + out := new(AuthorizationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizationStatus) DeepCopyInto(out *AuthorizationStatus) { + *out = *in + in.CommonComponentStatus.DeepCopyInto(&out.CommonComponentStatus) + if in.ExternalAuthorizationStatus != nil { + in, out := &in.ExternalAuthorizationStatus, &out.ExternalAuthorizationStatus + *out = new(ExternalAuthorizationStatus) + **out = **in + } + if in.UserNamespaces != nil { + in, out := &in.UserNamespaces, &out.UserNamespaces + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizationStatus. +func (in *AuthorizationStatus) DeepCopy() *AuthorizationStatus { + if in == nil { + return nil + } + out := new(AuthorizationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BTPServiceOperatorConfig) DeepCopyInto(out *BTPServiceOperatorConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BTPServiceOperatorConfig. +func (in *BTPServiceOperatorConfig) DeepCopy() *BTPServiceOperatorConfig { + if in == nil { + return nil + } + out := new(BTPServiceOperatorConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClientAuthenticationConfig) DeepCopyInto(out *ClientAuthenticationConfig) { + *out = *in + if in.ClientSecret != nil { + in, out := &in.ClientSecret, &out.ClientSecret + *out = new(LocalSecretReference) + **out = **in + } + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = make(map[string]SingleOrMultiStringValue, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClientAuthenticationConfig. +func (in *ClientAuthenticationConfig) DeepCopy() *ClientAuthenticationConfig { + if in == nil { + return nil + } + out := new(ClientAuthenticationConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudOrchestrator) DeepCopyInto(out *CloudOrchestrator) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudOrchestrator. +func (in *CloudOrchestrator) DeepCopy() *CloudOrchestrator { + if in == nil { + return nil + } + out := new(CloudOrchestrator) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CloudOrchestrator) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudOrchestratorConfiguration) DeepCopyInto(out *CloudOrchestratorConfiguration) { + *out = *in + if in.Crossplane != nil { + in, out := &in.Crossplane, &out.Crossplane + *out = new(CrossplaneConfig) + (*in).DeepCopyInto(*out) + } + if in.BTPServiceOperator != nil { + in, out := &in.BTPServiceOperator, &out.BTPServiceOperator + *out = new(BTPServiceOperatorConfig) + **out = **in + } + if in.ExternalSecretsOperator != nil { + in, out := &in.ExternalSecretsOperator, &out.ExternalSecretsOperator + *out = new(ExternalSecretsOperatorConfig) + **out = **in + } + if in.Kyverno != nil { + in, out := &in.Kyverno, &out.Kyverno + *out = new(KyvernoConfig) + **out = **in + } + if in.Flux != nil { + in, out := &in.Flux, &out.Flux + *out = new(FluxConfig) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudOrchestratorConfiguration. +func (in *CloudOrchestratorConfiguration) DeepCopy() *CloudOrchestratorConfiguration { + if in == nil { + return nil + } + out := new(CloudOrchestratorConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudOrchestratorList) DeepCopyInto(out *CloudOrchestratorList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CloudOrchestrator, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudOrchestratorList. +func (in *CloudOrchestratorList) DeepCopy() *CloudOrchestratorList { + if in == nil { + return nil + } + out := new(CloudOrchestratorList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CloudOrchestratorList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudOrchestratorSpec) DeepCopyInto(out *CloudOrchestratorSpec) { + *out = *in + in.CloudOrchestratorConfiguration.DeepCopyInto(&out.CloudOrchestratorConfiguration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudOrchestratorSpec. +func (in *CloudOrchestratorSpec) DeepCopy() *CloudOrchestratorSpec { + if in == nil { + return nil + } + out := new(CloudOrchestratorSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudOrchestratorStatus) DeepCopyInto(out *CloudOrchestratorStatus) { + *out = *in + in.CommonComponentStatus.DeepCopyInto(&out.CommonComponentStatus) + if in.ExternalCloudOrchestratorStatus != nil { + in, out := &in.ExternalCloudOrchestratorStatus, &out.ExternalCloudOrchestratorStatus + *out = new(ExternalCloudOrchestratorStatus) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudOrchestratorStatus. +func (in *CloudOrchestratorStatus) DeepCopy() *CloudOrchestratorStatus { + if in == nil { + return nil + } + out := new(CloudOrchestratorStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAdmin) DeepCopyInto(out *ClusterAdmin) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAdmin. +func (in *ClusterAdmin) DeepCopy() *ClusterAdmin { + if in == nil { + return nil + } + out := new(ClusterAdmin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterAdmin) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAdminList) DeepCopyInto(out *ClusterAdminList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ClusterAdmin, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAdminList. +func (in *ClusterAdminList) DeepCopy() *ClusterAdminList { + if in == nil { + return nil + } + out := new(ClusterAdminList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterAdminList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAdminSpec) DeepCopyInto(out *ClusterAdminSpec) { + *out = *in + if in.Subjects != nil { + in, out := &in.Subjects, &out.Subjects + *out = make([]Subject, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAdminSpec. +func (in *ClusterAdminSpec) DeepCopy() *ClusterAdminSpec { + if in == nil { + return nil + } + out := new(ClusterAdminSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAdminStatus) DeepCopyInto(out *ClusterAdminStatus) { + *out = *in + if in.Activated != nil { + in, out := &in.Activated, &out.Activated + *out = (*in).DeepCopy() + } + if in.Expiration != nil { + in, out := &in.Expiration, &out.Expiration + *out = (*in).DeepCopy() + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAdminStatus. +func (in *ClusterAdminStatus) DeepCopy() *ClusterAdminStatus { + if in == nil { + return nil + } + out := new(ClusterAdminStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonComponentStatus) DeepCopyInto(out *CommonComponentStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make(ComponentConditionList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.ObservedGenerations = in.ObservedGenerations +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonComponentStatus. +func (in *CommonComponentStatus) DeepCopy() *CommonComponentStatus { + if in == nil { + return nil + } + out := new(CommonComponentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CommonConfig) DeepCopyInto(out *CommonConfig) { + *out = *in + if in.DesiredRegion != nil { + in, out := &in.DesiredRegion, &out.DesiredRegion + *out = new(RegionSpecification) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CommonConfig. +func (in *CommonConfig) DeepCopy() *CommonConfig { + if in == nil { + return nil + } + out := new(CommonConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ComponentCondition) DeepCopyInto(out *ComponentCondition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentCondition. +func (in *ComponentCondition) DeepCopy() *ComponentCondition { + if in == nil { + return nil + } + out := new(ComponentCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in ComponentConditionList) DeepCopyInto(out *ComponentConditionList) { + { + in := &in + *out = make(ComponentConditionList, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ComponentConditionList. +func (in ComponentConditionList) DeepCopy() ComponentConditionList { + if in == nil { + return nil + } + out := new(ComponentConditionList) + in.DeepCopyInto(out) + return *out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CrossplaneConfig) DeepCopyInto(out *CrossplaneConfig) { + *out = *in + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]*CrossplaneProviderConfig, len(*in)) + for i := range *in { + if (*in)[i] != nil { + in, out := &(*in)[i], &(*out)[i] + *out = new(CrossplaneProviderConfig) + **out = **in + } + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossplaneConfig. +func (in *CrossplaneConfig) DeepCopy() *CrossplaneConfig { + if in == nil { + return nil + } + out := new(CrossplaneConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CrossplaneProviderConfig) DeepCopyInto(out *CrossplaneProviderConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CrossplaneProviderConfig. +func (in *CrossplaneProviderConfig) DeepCopy() *CrossplaneProviderConfig { + if in == nil { + return nil + } + out := new(CrossplaneProviderConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EncryptionConfig) DeepCopyInto(out *EncryptionConfig) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EncryptionConfig. +func (in *EncryptionConfig) DeepCopy() *EncryptionConfig { + if in == nil { + return nil + } + out := new(EncryptionConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalAPIServerStatus) DeepCopyInto(out *ExternalAPIServerStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalAPIServerStatus. +func (in *ExternalAPIServerStatus) DeepCopy() *ExternalAPIServerStatus { + if in == nil { + return nil + } + out := new(ExternalAPIServerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalAuthenticationStatus) DeepCopyInto(out *ExternalAuthenticationStatus) { + *out = *in + if in.UserAccess != nil { + in, out := &in.UserAccess, &out.UserAccess + *out = new(SecretReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalAuthenticationStatus. +func (in *ExternalAuthenticationStatus) DeepCopy() *ExternalAuthenticationStatus { + if in == nil { + return nil + } + out := new(ExternalAuthenticationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalAuthorizationStatus) DeepCopyInto(out *ExternalAuthorizationStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalAuthorizationStatus. +func (in *ExternalAuthorizationStatus) DeepCopy() *ExternalAuthorizationStatus { + if in == nil { + return nil + } + out := new(ExternalAuthorizationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalCloudOrchestratorStatus) DeepCopyInto(out *ExternalCloudOrchestratorStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalCloudOrchestratorStatus. +func (in *ExternalCloudOrchestratorStatus) DeepCopy() *ExternalCloudOrchestratorStatus { + if in == nil { + return nil + } + out := new(ExternalCloudOrchestratorStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalLandscaperStatus) DeepCopyInto(out *ExternalLandscaperStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalLandscaperStatus. +func (in *ExternalLandscaperStatus) DeepCopy() *ExternalLandscaperStatus { + if in == nil { + return nil + } + out := new(ExternalLandscaperStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalSecretsOperatorConfig) DeepCopyInto(out *ExternalSecretsOperatorConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalSecretsOperatorConfig. +func (in *ExternalSecretsOperatorConfig) DeepCopy() *ExternalSecretsOperatorConfig { + if in == nil { + return nil + } + out := new(ExternalSecretsOperatorConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FluxConfig) DeepCopyInto(out *FluxConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FluxConfig. +func (in *FluxConfig) DeepCopy() *FluxConfig { + if in == nil { + return nil + } + out := new(FluxConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GardenerConfiguration) DeepCopyInto(out *GardenerConfiguration) { + *out = *in + if in.HighAvailabilityConfig != nil { + in, out := &in.HighAvailabilityConfig, &out.HighAvailabilityConfig + *out = new(HighAvailabilityConfig) + **out = **in + } + if in.AuditLog != nil { + in, out := &in.AuditLog, &out.AuditLog + *out = new(AuditLogConfig) + **out = **in + } + if in.EncryptionConfig != nil { + in, out := &in.EncryptionConfig, &out.EncryptionConfig + *out = new(EncryptionConfig) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GardenerConfiguration. +func (in *GardenerConfiguration) DeepCopy() *GardenerConfiguration { + if in == nil { + return nil + } + out := new(GardenerConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GardenerInternalConfiguration) DeepCopyInto(out *GardenerInternalConfiguration) { + *out = *in + if in.ShootOverwrite != nil { + in, out := &in.ShootOverwrite, &out.ShootOverwrite + *out = new(NamespacedObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GardenerInternalConfiguration. +func (in *GardenerInternalConfiguration) DeepCopy() *GardenerInternalConfiguration { + if in == nil { + return nil + } + out := new(GardenerInternalConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GardenerStatus) DeepCopyInto(out *GardenerStatus) { + *out = *in + if in.Shoot != nil { + in, out := &in.Shoot, &out.Shoot + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GardenerStatus. +func (in *GardenerStatus) DeepCopy() *GardenerStatus { + if in == nil { + return nil + } + out := new(GardenerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HighAvailabilityConfig) DeepCopyInto(out *HighAvailabilityConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HighAvailabilityConfig. +func (in *HighAvailabilityConfig) DeepCopy() *HighAvailabilityConfig { + if in == nil { + return nil + } + out := new(HighAvailabilityConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IdentityProvider) DeepCopyInto(out *IdentityProvider) { + *out = *in + if in.SigningAlgs != nil { + in, out := &in.SigningAlgs, &out.SigningAlgs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.RequiredClaims != nil { + in, out := &in.RequiredClaims, &out.RequiredClaims + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.ClientConfig.DeepCopyInto(&out.ClientConfig) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IdentityProvider. +func (in *IdentityProvider) DeepCopy() *IdentityProvider { + if in == nil { + return nil + } + out := new(IdentityProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalCommonConfig) DeepCopyInto(out *InternalCommonConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalCommonConfig. +func (in *InternalCommonConfig) DeepCopy() *InternalCommonConfig { + if in == nil { + return nil + } + out := new(InternalCommonConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalConfiguration) DeepCopyInto(out *InternalConfiguration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalConfiguration. +func (in *InternalConfiguration) DeepCopy() *InternalConfiguration { + if in == nil { + return nil + } + out := new(InternalConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InternalConfiguration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalConfigurationComponents) DeepCopyInto(out *InternalConfigurationComponents) { + *out = *in + if in.APIServer != nil { + in, out := &in.APIServer, &out.APIServer + *out = new(APIServerInternalConfiguration) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalConfigurationComponents. +func (in *InternalConfigurationComponents) DeepCopy() *InternalConfigurationComponents { + if in == nil { + return nil + } + out := new(InternalConfigurationComponents) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalConfigurationList) DeepCopyInto(out *InternalConfigurationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]InternalConfiguration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalConfigurationList. +func (in *InternalConfigurationList) DeepCopy() *InternalConfigurationList { + if in == nil { + return nil + } + out := new(InternalConfigurationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InternalConfigurationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalConfigurationSpec) DeepCopyInto(out *InternalConfigurationSpec) { + *out = *in + if in.InternalCommonConfig != nil { + in, out := &in.InternalCommonConfig, &out.InternalCommonConfig + *out = new(InternalCommonConfig) + **out = **in + } + in.Components.DeepCopyInto(&out.Components) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalConfigurationSpec. +func (in *InternalConfigurationSpec) DeepCopy() *InternalConfigurationSpec { + if in == nil { + return nil + } + out := new(InternalConfigurationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KyvernoConfig) DeepCopyInto(out *KyvernoConfig) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KyvernoConfig. +func (in *KyvernoConfig) DeepCopy() *KyvernoConfig { + if in == nil { + return nil + } + out := new(KyvernoConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Landscaper) DeepCopyInto(out *Landscaper) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Landscaper. +func (in *Landscaper) DeepCopy() *Landscaper { + if in == nil { + return nil + } + out := new(Landscaper) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Landscaper) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LandscaperConfiguration) DeepCopyInto(out *LandscaperConfiguration) { + *out = *in + if in.Deployers != nil { + in, out := &in.Deployers, &out.Deployers + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LandscaperConfiguration. +func (in *LandscaperConfiguration) DeepCopy() *LandscaperConfiguration { + if in == nil { + return nil + } + out := new(LandscaperConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LandscaperDeploymentInfo) DeepCopyInto(out *LandscaperDeploymentInfo) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LandscaperDeploymentInfo. +func (in *LandscaperDeploymentInfo) DeepCopy() *LandscaperDeploymentInfo { + if in == nil { + return nil + } + out := new(LandscaperDeploymentInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LandscaperList) DeepCopyInto(out *LandscaperList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Landscaper, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LandscaperList. +func (in *LandscaperList) DeepCopy() *LandscaperList { + if in == nil { + return nil + } + out := new(LandscaperList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *LandscaperList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LandscaperSpec) DeepCopyInto(out *LandscaperSpec) { + *out = *in + in.LandscaperConfiguration.DeepCopyInto(&out.LandscaperConfiguration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LandscaperSpec. +func (in *LandscaperSpec) DeepCopy() *LandscaperSpec { + if in == nil { + return nil + } + out := new(LandscaperSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LandscaperStatus) DeepCopyInto(out *LandscaperStatus) { + *out = *in + in.CommonComponentStatus.DeepCopyInto(&out.CommonComponentStatus) + if in.ExternalLandscaperStatus != nil { + in, out := &in.ExternalLandscaperStatus, &out.ExternalLandscaperStatus + *out = new(ExternalLandscaperStatus) + **out = **in + } + if in.LandscaperDeploymentInfo != nil { + in, out := &in.LandscaperDeploymentInfo, &out.LandscaperDeploymentInfo + *out = new(LandscaperDeploymentInfo) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LandscaperStatus. +func (in *LandscaperStatus) DeepCopy() *LandscaperStatus { + if in == nil { + return nil + } + out := new(LandscaperStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LocalSecretReference) DeepCopyInto(out *LocalSecretReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LocalSecretReference. +func (in *LocalSecretReference) DeepCopy() *LocalSecretReference { + if in == nil { + return nil + } + out := new(LocalSecretReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedComponent) DeepCopyInto(out *ManagedComponent) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedComponent. +func (in *ManagedComponent) DeepCopy() *ManagedComponent { + if in == nil { + return nil + } + out := new(ManagedComponent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedComponent) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedComponentList) DeepCopyInto(out *ManagedComponentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ManagedComponent, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedComponentList. +func (in *ManagedComponentList) DeepCopy() *ManagedComponentList { + if in == nil { + return nil + } + out := new(ManagedComponentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedComponentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedComponentSpec) DeepCopyInto(out *ManagedComponentSpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedComponentSpec. +func (in *ManagedComponentSpec) DeepCopy() *ManagedComponentSpec { + if in == nil { + return nil + } + out := new(ManagedComponentSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedComponentStatus) DeepCopyInto(out *ManagedComponentStatus) { + *out = *in + if in.Versions != nil { + in, out := &in.Versions, &out.Versions + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedComponentStatus. +func (in *ManagedComponentStatus) DeepCopy() *ManagedComponentStatus { + if in == nil { + return nil + } + out := new(ManagedComponentStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlane) DeepCopyInto(out *ManagedControlPlane) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlane. +func (in *ManagedControlPlane) DeepCopy() *ManagedControlPlane { + if in == nil { + return nil + } + out := new(ManagedControlPlane) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedControlPlane) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneComponentCondition) DeepCopyInto(out *ManagedControlPlaneComponentCondition) { + *out = *in + in.ComponentCondition.DeepCopyInto(&out.ComponentCondition) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneComponentCondition. +func (in *ManagedControlPlaneComponentCondition) DeepCopy() *ManagedControlPlaneComponentCondition { + if in == nil { + return nil + } + out := new(ManagedControlPlaneComponentCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneComponents) DeepCopyInto(out *ManagedControlPlaneComponents) { + *out = *in + if in.APIServer != nil { + in, out := &in.APIServer, &out.APIServer + *out = new(APIServerConfiguration) + (*in).DeepCopyInto(*out) + } + if in.Landscaper != nil { + in, out := &in.Landscaper, &out.Landscaper + *out = new(LandscaperConfiguration) + (*in).DeepCopyInto(*out) + } + in.CloudOrchestratorConfiguration.DeepCopyInto(&out.CloudOrchestratorConfiguration) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneComponents. +func (in *ManagedControlPlaneComponents) DeepCopy() *ManagedControlPlaneComponents { + if in == nil { + return nil + } + out := new(ManagedControlPlaneComponents) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneComponentsStatus) DeepCopyInto(out *ManagedControlPlaneComponentsStatus) { + *out = *in + if in.APIServer != nil { + in, out := &in.APIServer, &out.APIServer + *out = new(ExternalAPIServerStatus) + **out = **in + } + if in.Landscaper != nil { + in, out := &in.Landscaper, &out.Landscaper + *out = new(ExternalLandscaperStatus) + **out = **in + } + if in.CloudOrchestrator != nil { + in, out := &in.CloudOrchestrator, &out.CloudOrchestrator + *out = new(ExternalCloudOrchestratorStatus) + **out = **in + } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(ExternalAuthenticationStatus) + (*in).DeepCopyInto(*out) + } + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(ExternalAuthorizationStatus) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneComponentsStatus. +func (in *ManagedControlPlaneComponentsStatus) DeepCopy() *ManagedControlPlaneComponentsStatus { + if in == nil { + return nil + } + out := new(ManagedControlPlaneComponentsStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneList) DeepCopyInto(out *ManagedControlPlaneList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ManagedControlPlane, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneList. +func (in *ManagedControlPlaneList) DeepCopy() *ManagedControlPlaneList { + if in == nil { + return nil + } + out := new(ManagedControlPlaneList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ManagedControlPlaneList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneMetaStatus) DeepCopyInto(out *ManagedControlPlaneMetaStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneMetaStatus. +func (in *ManagedControlPlaneMetaStatus) DeepCopy() *ManagedControlPlaneMetaStatus { + if in == nil { + return nil + } + out := new(ManagedControlPlaneMetaStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneSpec) DeepCopyInto(out *ManagedControlPlaneSpec) { + *out = *in + if in.DisabledComponents != nil { + in, out := &in.DisabledComponents, &out.DisabledComponents + *out = make([]ComponentType, len(*in)) + copy(*out, *in) + } + if in.CommonConfig != nil { + in, out := &in.CommonConfig, &out.CommonConfig + *out = new(CommonConfig) + (*in).DeepCopyInto(*out) + } + if in.Authentication != nil { + in, out := &in.Authentication, &out.Authentication + *out = new(AuthenticationConfiguration) + (*in).DeepCopyInto(*out) + } + if in.Authorization != nil { + in, out := &in.Authorization, &out.Authorization + *out = new(AuthorizationConfiguration) + (*in).DeepCopyInto(*out) + } + in.Components.DeepCopyInto(&out.Components) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneSpec. +func (in *ManagedControlPlaneSpec) DeepCopy() *ManagedControlPlaneSpec { + if in == nil { + return nil + } + out := new(ManagedControlPlaneSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ManagedControlPlaneStatus) DeepCopyInto(out *ManagedControlPlaneStatus) { + *out = *in + out.ManagedControlPlaneMetaStatus = in.ManagedControlPlaneMetaStatus + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]ManagedControlPlaneComponentCondition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + in.Components.DeepCopyInto(&out.Components) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedControlPlaneStatus. +func (in *ManagedControlPlaneStatus) DeepCopy() *ManagedControlPlaneStatus { + if in == nil { + return nil + } + out := new(ManagedControlPlaneStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedObjectReference) DeepCopyInto(out *NamespacedObjectReference) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedObjectReference. +func (in *NamespacedObjectReference) DeepCopy() *NamespacedObjectReference { + if in == nil { + return nil + } + out := new(NamespacedObjectReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObservedGenerations) DeepCopyInto(out *ObservedGenerations) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservedGenerations. +func (in *ObservedGenerations) DeepCopy() *ObservedGenerations { + if in == nil { + return nil + } + out := new(ObservedGenerations) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegionSpecification) DeepCopyInto(out *RegionSpecification) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegionSpecification. +func (in *RegionSpecification) DeepCopy() *RegionSpecification { + if in == nil { + return nil + } + out := new(RegionSpecification) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RoleBinding) DeepCopyInto(out *RoleBinding) { + *out = *in + if in.Subjects != nil { + in, out := &in.Subjects, &out.Subjects + *out = make([]Subject, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RoleBinding. +func (in *RoleBinding) DeepCopy() *RoleBinding { + if in == nil { + return nil + } + out := new(RoleBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretReference) DeepCopyInto(out *SecretReference) { + *out = *in + out.NamespacedObjectReference = in.NamespacedObjectReference +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretReference. +func (in *SecretReference) DeepCopy() *SecretReference { + if in == nil { + return nil + } + out := new(SecretReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SingleOrMultiStringValue) DeepCopyInto(out *SingleOrMultiStringValue) { + *out = *in + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SingleOrMultiStringValue. +func (in *SingleOrMultiStringValue) DeepCopy() *SingleOrMultiStringValue { + if in == nil { + return nil + } + out := new(SingleOrMultiStringValue) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Subject) DeepCopyInto(out *Subject) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subject. +func (in *Subject) DeepCopy() *Subject { + if in == nil { + return nil + } + out := new(Subject) + in.DeepCopyInto(out) + return out +} diff --git a/api/crds/crds.go b/api/crds/crds.go new file mode 100644 index 0000000..65cc101 --- /dev/null +++ b/api/crds/crds.go @@ -0,0 +1,35 @@ +package crds + +import ( + "embed" + "fmt" + "path/filepath" + + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "sigs.k8s.io/yaml" +) + +//go:embed manifests +var CRDFS embed.FS + +// CRDs returns the generated CustomResourceDefinitions as go structs. +// Panics if anything goes wrong trying to read the CRD files, as an error here is likely related to a wrong build or invalid generated CRDs. +func CRDs() []*apiextv1.CustomResourceDefinition { + files, err := CRDFS.ReadDir("manifests") + if err != nil { + panic(err) + } + res := []*apiextv1.CustomResourceDefinition{} + for _, f := range files { + data, err := CRDFS.ReadFile(filepath.Join("manifests", f.Name())) + if err != nil { + panic(fmt.Errorf("error reading CRD file '%s': %w", f.Name(), err)) + } + crd := &apiextv1.CustomResourceDefinition{} + if err := yaml.Unmarshal(data, crd); err != nil { + panic(fmt.Errorf("error parsing file '%s' into CRD: %w", f.Name(), err)) + } + res = append(res, crd) + } + return res +} diff --git a/api/crds/manifests/core.openmcp.cloud_apiservers.yaml b/api/crds/manifests/core.openmcp.cloud_apiservers.yaml new file mode 100644 index 0000000..3f8968a --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_apiservers.yaml @@ -0,0 +1,358 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: apiservers.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: APIServer + listKind: APIServerList + plural: apiservers + shortNames: + - as + singular: apiserver + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="APIServerReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: APIServer is the Schema for the APIServer API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: APIServerSpec contains the APIServer configuration and potentially + other fields which should not be exposed to the customer. + properties: + desiredRegion: + description: |- + DesiredRegion is part of the common configuration. + If specified, it will be used to determine the region for the created cluster. + properties: + direction: + description: Direction is the direction within the region. + enum: + - north + - east + - south + - west + - central + type: string + name: + description: Name is the name of the region. + enum: + - northamerica + - southamerica + - europe + - asia + - africa + - australia + type: string + type: object + gardener: + description: |- + GardenerConfig contains configuration for a Gardener APIServer. + Must be set if type is 'Gardener', is ignored otherwise. + properties: + auditLog: + description: AuditLogConfig defines the AuditLog configuration + for the ManagedControlPlane cluster. + properties: + policyRef: + description: PolicyRef is the reference to the policy containing + the configuration for the audit log service. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + secretRef: + description: SecretRef is the reference to the secret containing + the credentials for the audit log service. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + serviceURL: + description: ServiceURL is the URL from the Service Keys. + type: string + tenantID: + description: TenantID is the tenant ID of the BTP Subaccount. + Can be seen in the BTP Cockpit dashboard. + type: string + type: + description: Type is the type of the audit log. + enum: + - standard + type: string + required: + - policyRef + - secretRef + - serviceURL + - tenantID + - type + type: object + encryptionConfig: + description: EncryptionConfig contains customizable encryption + configuration of the API server. + properties: + resources: + description: |- + Resources contains the list of resources that shall be encrypted in addition to secrets. + Each item is a Kubernetes resource name in plural (resource or resource.group) that should be encrypted. + Example: ["configmaps", "statefulsets.apps", "flunders.emxample.com"] + items: + type: string + type: array + type: object + highAvailability: + description: HighAvailabilityConfig specifies the HA configuration + for the API server. + properties: + failureToleranceType: + description: |- + FailureToleranceType specifies failure tolerance mode for the API server. + Allowed values are: node, zone + node: The API server is tolerant to node failures within a single zone. + zone: The API server is tolerant to zone failures. + enum: + - node + - zone + type: string + x-kubernetes-validations: + - message: failureToleranceType is immutable + rule: self == oldSelf + required: + - failureToleranceType + type: object + x-kubernetes-validations: + - message: highAvailability is immutable + rule: self == oldSelf + region: + description: |- + Region is the region to be used for the Shoot cluster. + This is usually derived from the ManagedControlPlane's common configuration, but can be overwritten here. + type: string + x-kubernetes-validations: + - message: region is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: highAvailability is required once set + rule: has(self.highAvailability) == has(oldSelf.highAvailability) + || has(self.highAvailability) + internal: + description: |- + Internal contains the parts of the configuration which are not exposed to the customer. + It would be nice to have this as an inline field, but since both APIServerConfiguration and APIServerInternalConfiguration + contain a field 'gardener', this would clash. + properties: + gardener: + description: GardenerConfig contains internal configuration for + a Gardener APIServer. + properties: + k8sVersionOverwrite: + description: |- + K8SVersionOverwrite is the k8s version for the Shoot cluster. + Will be defaulted if not specified. + type: string + landscapeConfiguration: + description: |- + LandscapeConfiguration is the name of the landscape and the name of the configuration to use. + The expected format is "/". + pattern: ^[a-z0-9-]+/[a-z0-9-]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + shootOverwrite: + description: ShootOverwrite allows to overwrite the shoot + to be used. This could be useful for migration tasks. + properties: + name: + description: Name is the object's name. + type: string + namespace: + description: Namespace is the object's namespace. + type: string + required: + - name + - namespace + type: object + type: object + type: object + type: + default: GardenerDedicated + description: |- + Type is the type of APIServer. This determines which other configuration fields need to be specified. + Valid values are: + - Gardener + - GardenerDedicated + enum: + - Gardener + - GardenerDedicated + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf + required: + - type + type: object + status: + description: APIServerStatus contains the APIServer status and potentially + other fields which should not be exposed to the customer. + properties: + adminAccess: + description: AdminAccess is an admin kubeconfig for accessing the + API server. + properties: + creationTimestamp: + description: CreationTimestamp is the time when this access was + created. + format: date-time + type: string + expirationTimestamp: + description: ExpirationTimestamp is the time until the access + loses its validity. + format: date-time + type: string + kubeconfig: + description: Kubeconfig is the kubeconfig for accessing the APIServer + cluster. + type: string + type: object + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + endpoint: + description: Endpoint represents the Kubernetes API server endpoint + type: string + gardener: + description: GardenerStatus contains status if the type is 'Gardener'. + properties: + shoot: + description: Shoot contains the shoot manifest generated by the + controller. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + serviceAccountIssuer: + description: ServiceAccountIssuer represents the OpenIDConnect issuer + URL that can be used to verify service account tokens. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/api/crds/manifests/core.openmcp.cloud_authentications.yaml b/api/crds/manifests/core.openmcp.cloud_authentications.yaml new file mode 100644 index 0000000..9d5c917 --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_authentications.yaml @@ -0,0 +1,250 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: authentications.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: Authentication + listKind: AuthenticationList + plural: authentications + shortNames: + - auth + singular: authentication + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="AuthenticationReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Authentication is the Schema for the authentication API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: AuthenticationSpec contains the specification for the authentication + component + properties: + enableSystemIdentityProvider: + type: boolean + identityProviders: + items: + description: IdentityProvider contains the configuration for an + OpenID Connect identity provider + properties: + caBundle: + description: |- + CABundle: When set, the OpenID server's certificate will be verified by one of the authorities in the bundle. + Otherwise, the host's root CA set will be used. + type: string + clientConfig: + description: ClientAuthentication contains configuration for + OIDC clients + properties: + clientSecret: + description: |- + ClientSecret is a references to a secret containing the client secret. + The client secret will be added to the generated kubeconfig with the "--oidc-client-secret" flag. + properties: + key: + description: Key is the key inside the secret. + type: string + name: + description: Name is the secret name. + type: string + required: + - key + - name + type: object + extraConfig: + additionalProperties: + description: SingleOrMultiStringValue is a type that can + hold either a single string value or a list of string + values. + properties: + value: + description: Value is a single string value. + type: string + values: + description: Values is a list of string values. + items: + type: string + type: array + type: object + description: |- + ExtraConfig is added to the client configuration in the kubeconfig. + Can either be a single string value, a list of string values or no value. + Must not contain any of the following keys: + - "client-id" + - "client-secret" + - "issuer-url" + type: object + type: object + clientID: + description: ClientID is the client ID of the identity provider. + type: string + groupsClaim: + description: GroupsClaim is the claim that contains the groups. + type: string + issuerURL: + description: IssuerURL is the issuer URL of the identity provider. + type: string + name: + description: |- + Name is the name of the identity provider. + The name must be unique among all identity providers. + The name must only contain lowercase letters. + The length must not exceed 63 characters. + maxLength: 63 + pattern: ^[a-z]+$ + type: string + requiredClaims: + additionalProperties: + type: string + description: RequiredClaims is a map of required claims. If + set, the identity provider must provide these claims in the + ID token. + type: object + signingAlgs: + description: SigningAlgs is the list of allowed JOSE asymmetric + signing algorithms. + items: + type: string + type: array + usernameClaim: + description: UsernameClaim is the claim that contains the username. + type: string + required: + - clientID + - issuerURL + - name + - usernameClaim + type: object + type: array + type: object + status: + description: AuthenticationStatus contains the status of the authentication + component + properties: + access: + description: |- + UserAccess reference the secret containing the kubeconfig + for the APIServer which is to be used by the customer. + properties: + key: + description: Key is the key inside the secret. + type: string + name: + description: Name is the object's name. + type: string + namespace: + description: Namespace is the object's namespace. + type: string + required: + - key + - name + - namespace + type: object + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/api/crds/manifests/core.openmcp.cloud_authorizations.yaml b/api/crds/manifests/core.openmcp.cloud_authorizations.yaml new file mode 100644 index 0000000..c4e70ea --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_authorizations.yaml @@ -0,0 +1,191 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: authorizations.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: Authorization + listKind: AuthorizationList + plural: authorizations + shortNames: + - authz + singular: authorization + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="AuthorizationReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Authorization is the Schema for the authorization API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: AuthorizationSpec contains the specification for the authorization + component + properties: + roleBindings: + description: RoleBindings is a list of role bindings + items: + description: RoleBinding contains the role and the subjects assigned + to the role + properties: + role: + description: Role is the name of the role + enum: + - admin + - view + type: string + subjects: + description: Subjects is a list of subjects assigned to the + role + items: + description: |- + Subject describes an object that is assigned to a role and + which can be used to authenticate against the control plane. + properties: + apiGroup: + description: APIGroup is the API group of the subject + type: string + kind: + description: Kind is the kind of the subject + enum: + - ServiceAccount + - User + - Group + type: string + name: + description: Name is the name of the subject + minLength: 1 + type: string + namespace: + description: Namespace is the namespace of the subject + type: string + required: + - kind + - name + type: object + type: array + required: + - role + - subjects + type: object + type: array + required: + - roleBindings + type: object + status: + description: AuthorizationStatus contains the status of the authorization + component + properties: + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + userNamespaces: + description: |- + UserNamespaces is a list of namespaces that have been created by the user and + must be managed by the authorization component. + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/api/crds/manifests/core.openmcp.cloud_cloudorchestrators.yaml b/api/crds/manifests/core.openmcp.cloud_cloudorchestrators.yaml new file mode 100644 index 0000000..5df84da --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_cloudorchestrators.yaml @@ -0,0 +1,206 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: cloudorchestrators.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: CloudOrchestrator + listKind: CloudOrchestratorList + plural: cloudorchestrators + shortNames: + - co + singular: cloudorchestrator + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="CloudOrchestratorReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: CloudOrchestrator is the Schema for the internal CloudOrchestrator + API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: CloudOrchestratorSpec defines the desired state of CloudOrchestrator + properties: + btpServiceOperator: + description: BTPServiceOperator defines the configuration for setting + up the BTPServiceOperator component in a ManagedControlPlane. + properties: + version: + description: The Version of BTP Service Operator to install. + type: string + required: + - version + type: object + crossplane: + description: Crossplane defines the configuration for setting up the + Crossplane component in a ManagedControlPlane. + properties: + providers: + items: + properties: + name: + description: |- + Name of the provider. + Using a well-known name will automatically configure the "package" field. + type: string + version: + description: Version of the provider to install. + type: string + required: + - name + - version + type: object + type: array + version: + description: The Version of Crossplane to install. + type: string + required: + - version + type: object + externalSecretsOperator: + description: ExternalSecretsOperator defines the configuration for + setting up the ExternalSecretsOperator component in a ManagedControlPlane. + properties: + version: + description: The Version of External Secrets Operator to install. + type: string + required: + - version + type: object + flux: + description: Flux defines the configuration for setting up the Flux + component in a ManagedControlPlane. + properties: + version: + description: The Version of Flux to install. + type: string + required: + - version + type: object + kyverno: + description: Kyverno defines the configuration for setting up the + Kyverno component in a ManagedControlPlane. + properties: + version: + description: The Version of Kyverno to install. + type: string + required: + - version + type: object + type: object + status: + description: CloudOrchestratorStatus defines the observed state of CloudOrchestrator + properties: + componentsEnabled: + description: Number of enabled components. + type: integer + componentsHealthy: + description: Number of healthy components. + type: integer + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/api/crds/manifests/core.openmcp.cloud_clusteradmins.yaml b/api/crds/manifests/core.openmcp.cloud_clusteradmins.yaml new file mode 100644 index 0000000..c4e1bf6 --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_clusteradmins.yaml @@ -0,0 +1,110 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: clusteradmins.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: ClusterAdmin + listKind: ClusterAdminList + plural: clusteradmins + shortNames: + - clas + singular: clusteradmin + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.active + name: Active + type: string + - jsonPath: .status.activationTime + name: Activated + type: date + - jsonPath: .status.expirationTime + name: Expiration + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterAdmin is the Schema for the cluster admin API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: ClusterAdminSpec contains the specification for the cluster + admin + properties: + subjects: + items: + description: |- + Subject describes an object that is assigned to a role and + which can be used to authenticate against the control plane. + properties: + apiGroup: + description: APIGroup is the API group of the subject + type: string + kind: + description: Kind is the kind of the subject + enum: + - ServiceAccount + - User + - Group + type: string + name: + description: Name is the name of the subject + minLength: 1 + type: string + namespace: + description: Namespace is the namespace of the subject + type: string + required: + - kind + - name + type: object + type: array + required: + - subjects + type: object + status: + description: ClusterAdminStatus contains the status of the cluster admin + properties: + activationTime: + description: ActivationTime is the time when the cluster admin was + activated + format: date-time + type: string + active: + description: Active is set to true if the subjects of the cluster + admin are assigned the cluster-admin role + type: boolean + expirationTime: + description: ExpirationTime is the time when the cluster admin will + expire + format: date-time + type: string + required: + - active + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/api/crds/manifests/core.openmcp.cloud_internalconfigurations.yaml b/api/crds/manifests/core.openmcp.cloud_internalconfigurations.yaml new file mode 100644 index 0000000..4ce2abb --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_internalconfigurations.yaml @@ -0,0 +1,90 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: internalconfigurations.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: InternalConfiguration + listKind: InternalConfigurationList + plural: internalconfigurations + shortNames: + - icfg + singular: internalconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InternalConfiguration is the Schema for the InternalConfigurations + API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: InternalConfigurationSpec defines additional configuration + for a managedcontrolplane. + properties: + components: + description: InternalConfigurationComponents defines the components + that are part of the internal configuration. + properties: + apiServer: + properties: + gardener: + description: GardenerConfig contains internal configuration + for a Gardener APIServer. + properties: + k8sVersionOverwrite: + description: |- + K8SVersionOverwrite is the k8s version for the Shoot cluster. + Will be defaulted if not specified. + type: string + landscapeConfiguration: + description: |- + LandscapeConfiguration is the name of the landscape and the name of the configuration to use. + The expected format is "/". + pattern: ^[a-z0-9-]+/[a-z0-9-]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + shootOverwrite: + description: ShootOverwrite allows to overwrite the shoot + to be used. This could be useful for migration tasks. + properties: + name: + description: Name is the object's name. + type: string + namespace: + description: Namespace is the object's namespace. + type: string + required: + - name + - namespace + type: object + type: object + type: object + type: object + type: object + type: object + served: true + storage: true diff --git a/api/crds/manifests/core.openmcp.cloud_landscapers.yaml b/api/crds/manifests/core.openmcp.cloud_landscapers.yaml new file mode 100644 index 0000000..eed123c --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_landscapers.yaml @@ -0,0 +1,154 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: landscapers.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: Landscaper + listKind: LandscaperList + plural: landscapers + shortNames: + - ls + singular: landscaper + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="LandscaperReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Landscaper is the Schema for the laasinstances API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: LandscaperSpec contains the Landscaper configuration and + potentially other fields which should not be exposed to the customer. + properties: + deployers: + description: Deployers is the list of deployers that should be installed. + items: + type: string + type: array + type: object + status: + description: LandscaperStatus contains the landscaper status and potentially + other fields which should not be exposed to the customer. + properties: + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + landscaperDeployment: + description: LandscaperDeploymentInfo contains information about the + corresponding LandscaperDeployment resource. + properties: + name: + description: Name is the name of the Landscaper deployment. + type: string + namespace: + description: Namespace is the namespace of the Landscaper deployment. + type: string + required: + - name + - namespace + type: object + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/api/crds/manifests/core.openmcp.cloud_managedcomponents.yaml b/api/crds/manifests/core.openmcp.cloud_managedcomponents.yaml new file mode 100644 index 0000000..d8c4edb --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_managedcomponents.yaml @@ -0,0 +1,63 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: managedcomponents.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: ManagedComponent + listKind: ManagedComponentList + plural: managedcomponents + singular: managedcomponent + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.name + name: Name + type: string + - jsonPath: .status.versions + name: Versions + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ManagedComponent is the Schema for the managedcomponents API. + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: ManagedComponentSpec defines the desired state of ManagedComponent. + type: object + status: + description: ManagedComponentStatus defines the observed state of ManagedComponent. + properties: + versions: + items: + type: string + type: array + required: + - versions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/api/crds/manifests/core.openmcp.cloud_managedcontrolplanes.yaml b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanes.yaml new file mode 100644 index 0000000..8c00103 --- /dev/null +++ b/api/crds/manifests/core.openmcp.cloud_managedcontrolplanes.yaml @@ -0,0 +1,590 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: managedcontrolplanes.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: ManagedControlPlane + listKind: ManagedControlPlaneList + plural: managedcontrolplanes + shortNames: + - mcp + singular: managedcontrolplane + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ManagedControlPlane is the Schema for the ManagedControlPlane + API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: ManagedControlPlaneSpec defines the desired state of ManagedControlPlane. + properties: + authentication: + description: Authentication contains the configuration for the enabled + OpenID Connect identity providers + properties: + enableSystemIdentityProvider: + type: boolean + identityProviders: + items: + description: IdentityProvider contains the configuration for + an OpenID Connect identity provider + properties: + caBundle: + description: |- + CABundle: When set, the OpenID server's certificate will be verified by one of the authorities in the bundle. + Otherwise, the host's root CA set will be used. + type: string + clientConfig: + description: ClientAuthentication contains configuration + for OIDC clients + properties: + clientSecret: + description: |- + ClientSecret is a references to a secret containing the client secret. + The client secret will be added to the generated kubeconfig with the "--oidc-client-secret" flag. + properties: + key: + description: Key is the key inside the secret. + type: string + name: + description: Name is the secret name. + type: string + required: + - key + - name + type: object + extraConfig: + additionalProperties: + description: SingleOrMultiStringValue is a type that + can hold either a single string value or a list + of string values. + properties: + value: + description: Value is a single string value. + type: string + values: + description: Values is a list of string values. + items: + type: string + type: array + type: object + description: |- + ExtraConfig is added to the client configuration in the kubeconfig. + Can either be a single string value, a list of string values or no value. + Must not contain any of the following keys: + - "client-id" + - "client-secret" + - "issuer-url" + type: object + type: object + clientID: + description: ClientID is the client ID of the identity provider. + type: string + groupsClaim: + description: GroupsClaim is the claim that contains the + groups. + type: string + issuerURL: + description: IssuerURL is the issuer URL of the identity + provider. + type: string + name: + description: |- + Name is the name of the identity provider. + The name must be unique among all identity providers. + The name must only contain lowercase letters. + The length must not exceed 63 characters. + maxLength: 63 + pattern: ^[a-z]+$ + type: string + requiredClaims: + additionalProperties: + type: string + description: RequiredClaims is a map of required claims. + If set, the identity provider must provide these claims + in the ID token. + type: object + signingAlgs: + description: SigningAlgs is the list of allowed JOSE asymmetric + signing algorithms. + items: + type: string + type: array + usernameClaim: + description: UsernameClaim is the claim that contains the + username. + type: string + required: + - clientID + - issuerURL + - name + - usernameClaim + type: object + type: array + type: object + authorization: + description: Authorization contains the configuration of the subjects + assigned to control plane roles + properties: + roleBindings: + description: RoleBindings is a list of role bindings + items: + description: RoleBinding contains the role and the subjects + assigned to the role + properties: + role: + description: Role is the name of the role + enum: + - admin + - view + type: string + subjects: + description: Subjects is a list of subjects assigned to + the role + items: + description: |- + Subject describes an object that is assigned to a role and + which can be used to authenticate against the control plane. + properties: + apiGroup: + description: APIGroup is the API group of the subject + type: string + kind: + description: Kind is the kind of the subject + enum: + - ServiceAccount + - User + - Group + type: string + name: + description: Name is the name of the subject + minLength: 1 + type: string + namespace: + description: Namespace is the namespace of the subject + type: string + required: + - kind + - name + type: object + type: array + required: + - role + - subjects + type: object + type: array + required: + - roleBindings + type: object + components: + description: Components contains the configuration for Components + like APIServer, Landscaper, CloudOrchestrator. + properties: + apiServer: + default: + type: GardenerDedicated + description: APIServerConfiguration contains the configuration + which is required for setting up a k8s cluster to be used as + APIServer. + properties: + gardener: + description: |- + GardenerConfig contains configuration for a Gardener APIServer. + Must be set if type is 'Gardener', is ignored otherwise. + properties: + auditLog: + description: AuditLogConfig defines the AuditLog configuration + for the ManagedControlPlane cluster. + properties: + policyRef: + description: PolicyRef is the reference to the policy + containing the configuration for the audit log service. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + secretRef: + description: SecretRef is the reference to the secret + containing the credentials for the audit log service. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + serviceURL: + description: ServiceURL is the URL from the Service + Keys. + type: string + tenantID: + description: TenantID is the tenant ID of the BTP + Subaccount. Can be seen in the BTP Cockpit dashboard. + type: string + type: + description: Type is the type of the audit log. + enum: + - standard + type: string + required: + - policyRef + - secretRef + - serviceURL + - tenantID + - type + type: object + encryptionConfig: + description: EncryptionConfig contains customizable encryption + configuration of the API server. + properties: + resources: + description: |- + Resources contains the list of resources that shall be encrypted in addition to secrets. + Each item is a Kubernetes resource name in plural (resource or resource.group) that should be encrypted. + Example: ["configmaps", "statefulsets.apps", "flunders.emxample.com"] + items: + type: string + type: array + type: object + highAvailability: + description: HighAvailabilityConfig specifies the HA configuration + for the API server. + properties: + failureToleranceType: + description: |- + FailureToleranceType specifies failure tolerance mode for the API server. + Allowed values are: node, zone + node: The API server is tolerant to node failures within a single zone. + zone: The API server is tolerant to zone failures. + enum: + - node + - zone + type: string + x-kubernetes-validations: + - message: failureToleranceType is immutable + rule: self == oldSelf + required: + - failureToleranceType + type: object + x-kubernetes-validations: + - message: highAvailability is immutable + rule: self == oldSelf + region: + description: |- + Region is the region to be used for the Shoot cluster. + This is usually derived from the ManagedControlPlane's common configuration, but can be overwritten here. + type: string + x-kubernetes-validations: + - message: region is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: highAvailability is required once set + rule: has(self.highAvailability) == has(oldSelf.highAvailability) + || has(self.highAvailability) + type: + default: GardenerDedicated + description: |- + Type is the type of APIServer. This determines which other configuration fields need to be specified. + Valid values are: + - Gardener + - GardenerDedicated + enum: + - Gardener + - GardenerDedicated + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf + required: + - type + type: object + btpServiceOperator: + description: BTPServiceOperator defines the configuration for + setting up the BTPServiceOperator component in a ManagedControlPlane. + properties: + version: + description: The Version of BTP Service Operator to install. + type: string + required: + - version + type: object + crossplane: + description: Crossplane defines the configuration for setting + up the Crossplane component in a ManagedControlPlane. + properties: + providers: + items: + properties: + name: + description: |- + Name of the provider. + Using a well-known name will automatically configure the "package" field. + type: string + version: + description: Version of the provider to install. + type: string + required: + - name + - version + type: object + type: array + version: + description: The Version of Crossplane to install. + type: string + required: + - version + type: object + externalSecretsOperator: + description: ExternalSecretsOperator defines the configuration + for setting up the ExternalSecretsOperator component in a ManagedControlPlane. + properties: + version: + description: The Version of External Secrets Operator to install. + type: string + required: + - version + type: object + flux: + description: Flux defines the configuration for setting up the + Flux component in a ManagedControlPlane. + properties: + version: + description: The Version of Flux to install. + type: string + required: + - version + type: object + kyverno: + description: Kyverno defines the configuration for setting up + the Kyverno component in a ManagedControlPlane. + properties: + version: + description: The Version of Kyverno to install. + type: string + required: + - version + type: object + landscaper: + description: LandscaperConfiguration contains the configuration + which is required for setting up a LaaS instance. + properties: + deployers: + description: Deployers is the list of deployers that should + be installed. + items: + type: string + type: array + type: object + type: object + x-kubernetes-validations: + - message: apiServer is required once set + rule: '!has(oldSelf.apiServer)|| has(self.apiServer)' + desiredRegion: + description: DesiredRegion allows customers to specify a desired region + proximity. + properties: + direction: + description: Direction is the direction within the region. + enum: + - north + - east + - south + - west + - central + type: string + name: + description: Name is the name of the region. + enum: + - northamerica + - southamerica + - europe + - asia + - africa + - australia + type: string + type: object + x-kubernetes-validations: + - message: RegionSpecification is immutable + rule: self == oldSelf + disabledComponents: + description: |- + DisabledComponents contains a list of component types. + The resources for these components will still be generated, but they will get the ignore operation annotation, so they should not be processed by their respective controllers. + items: + type: string + type: array + required: + - components + type: object + x-kubernetes-validations: + - message: desiredRegion is required once set + rule: '!has(oldSelf.desiredRegion)|| has(self.desiredRegion)' + status: + description: ManagedControlPlaneStatus defines the observed state of ManagedControlPlane. + properties: + components: + description: ManagedControlPlaneComponentsStatus contains the status + of the components of a ManagedControlPlane. + properties: + apiServer: + description: |- + ExternalAPIServerStatus contains the status of the API server / ManagedControlPlane cluster. The Kuberenetes can act as an OIDC + compatible provider in a sense that they serve OIDC issuer endpoint URL so that other system can validate tokens that have been + issued by the external party. + properties: + endpoint: + description: Endpoint represents the Kubernetes API server + endpoint + type: string + serviceAccountIssuer: + description: ServiceAccountIssuer represents the OpenIDConnect + issuer URL that can be used to verify service account tokens. + type: string + type: object + authentication: + description: ExternalAuthenticationStatus contains the status + of the authentication component. + properties: + access: + description: |- + UserAccess reference the secret containing the kubeconfig + for the APIServer which is to be used by the customer. + properties: + key: + description: Key is the key inside the secret. + type: string + name: + description: Name is the object's name. + type: string + namespace: + description: Namespace is the object's namespace. + type: string + required: + - key + - name + - namespace + type: object + type: object + authorization: + description: ExternalAuthorizationStatus contains the status of + the external authorization component + type: object + cloudOrchestrator: + description: ExternalCloudOrchestratorStatus contains the status + of the CloudOrchestrator component. + type: object + landscaper: + description: ExternalLandscaperStatus contains the status of a + LaaS instance. + type: object + type: object + conditions: + description: Conditions collects the conditions of all components. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + managedBy: + description: ManagedBy contains the information which component + manages this condition. + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - managedBy + - status + - type + type: object + type: array + message: + description: Message contains an optional message. + type: string + observedGeneration: + description: ObservedGeneration is the last generation of this resource + that has successfully been reconciled. + format: int64 + type: integer + status: + description: |- + Status is the current status of the ManagedControlPlane. + It is "Deleting" if the ManagedControlPlane is being deleted. + It is "Ready" if all conditions are true, and "Not Ready" otherwise. + type: string + required: + - observedGeneration + - status + type: object + type: object + x-kubernetes-validations: + - message: name must not be longer than 36 characters + rule: size(self.metadata.name) <= 36 + served: true + storage: true + subresources: + status: {} diff --git a/api/errors/errors.go b/api/errors/errors.go new file mode 100644 index 0000000..c5bb4aa --- /dev/null +++ b/api/errors/errors.go @@ -0,0 +1,132 @@ +// +kubebuilder:object:generate=false +package errors + +import ( + "errors" + "fmt" + "strings" +) + +var _ ReasonableError = &ErrorWithReason{} + +// ReasonableError enhances an error with a reason. +// The reason is meant to be a CamelCased, machine-readable, enum-like string. +// Use WithReason(err, reason) to wrap a normal error into an ReasonableError. +type ReasonableError interface { + error + Reason() string +} + +// ErrorWithReason wraps an error and adds a reason to it. +// The reason is meant to be a CamelCased, machine-readable, enum-like string. +// Use WithReason(err, reason) to wrap a normal error into an *ErrorWithReason. +type ErrorWithReason struct { + error + reason string +} + +// Reason returns the reason for this error. +func (e *ErrorWithReason) Reason() string { + return e.reason +} + +// WithReason wraps an error together with a reason into ErrorWithReason. +// The reason is meant to be a CamelCased, machine-readable, enum-like string. +// If the given error is nil, nil is returned. +func WithReason(err error, reason string) ReasonableError { + if err == nil { + return nil + } + return &ErrorWithReason{ + error: err, + reason: reason, + } +} + +// Errorf works similarly to fmt.Errorf, with the exception that it requires an ErrorWithReason as second argument and returns nil if that one is nil. +// Otherwise, it calls fmt.Errorf to construct an error and wraps it in an ErrorWithReason, using the reason from the given error. +// This is useful for expanding the error message without losing the reason. +func Errorf(format string, err ReasonableError, a ...any) ReasonableError { + if err == nil { + return nil + } + return WithReason(fmt.Errorf(format, a...), err.Reason()) +} + +// Join joins multiple errors into a single one. +// Returns nil if all given errors are nil. +// This is equivalent to NewErrorList(errs...).Aggregate(). +func Join(errs ...error) ReasonableError { + return NewReasonableErrorList(errs...).Aggregate() +} + +// ReasonableErrorList is a helper struct for situations in which multiple errors (with or without reasons) should be returned as a single one. +type ReasonableErrorList struct { + Errs []error + Reasons []string +} + +// NewReasonableErrorList creates a new *ErrorListWithReasons containing the provided errors. +func NewReasonableErrorList(errs ...error) *ReasonableErrorList { + res := &ReasonableErrorList{ + Errs: []error{}, + Reasons: []string{}, + } + return res.Append(errs...) +} + +// Aggregate aggregates all errors in the list into a single ErrorWithReason. +// Returns nil if the list is either nil or empty. +// If the list contains a single error, that error is returned. +// Otherwise, a new error is constructed by appending all contained errors' messages. +// The reason in the returned error is the first reason that was added to the list, +// or the empty string if none of the contained errors was an ErrorWithReason. +func (el *ReasonableErrorList) Aggregate() ReasonableError { + if el == nil || len(el.Errs) == 0 { + return nil + } + reason := "" + if len(el.Reasons) > 0 { + reason = el.Reasons[0] + } + if len(el.Errs) == 1 { + if ewr, ok := el.Errs[0].(ReasonableError); ok { + return ewr + } + return WithReason(el.Errs[0], reason) + } + sb := strings.Builder{} + sb.WriteString("multiple errors occurred:") + for _, e := range el.Errs { + sb.WriteString("\n") + sb.WriteString(e.Error()) + } + return WithReason(errors.New(sb.String()), reason) +} + +// Append appends all given errors to the ErrorListWithReasons. +// This modifies the receiver object. +// If a given error is of type ErrorWithReason, its reason is added to the list of reasons. +// nil pointers in the arguments are ignored. +// Returns the receiver for chaining. +func (el *ReasonableErrorList) Append(errs ...error) *ReasonableErrorList { + for _, e := range errs { + if e != nil { + el.Errs = append(el.Errs, e) + if ewr, ok := e.(ReasonableError); ok { + el.Reasons = append(el.Reasons, ewr.Reason()) + } + } + } + return el +} + +// Reason returns the first reason from the list of reasons contained in this error list. +// If the list is nil or no reasons are contained, the empty string is returned. +// This is equivalent to el.Aggregate().Reason(), except that it also works for an empty error list. +func (el *ReasonableErrorList) Reason() string { + if el == nil || len(el.Reasons) == 0 { + return "" + } + return el.Reasons[0] +} diff --git a/api/errors/predefined.go b/api/errors/predefined.go new file mode 100644 index 0000000..6a17a52 --- /dev/null +++ b/api/errors/predefined.go @@ -0,0 +1,6 @@ +package errors + +import "fmt" + +var ErrWrongComponentConfigType = fmt.Errorf("the given configuration has the wrong type for this component's spec") +var ErrWrongComponentStatusType = fmt.Errorf("the given status has the wrong type for this component's status field in the ManagedControlPlane") diff --git a/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/register.go b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/register.go new file mode 100644 index 0000000..c72b1ab --- /dev/null +++ b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/register.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the group name use in this package +const GroupName = "aws.provider.extensions.gardener.cloud" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + // SchemeBuilder used to register the Shoot resource. + SchemeBuilder runtime.SchemeBuilder + localSchemeBuilder = &SchemeBuilder + // AddToScheme is a pointer to SchemeBuilder.AddToScheme. + AddToScheme = localSchemeBuilder.AddToScheme +) + +func init() { + // We only register manually written functions here. The registration of the + // generated functions takes place in the generated files. The separation + // makes the code compile even when the generated files are missing. + localSchemeBuilder.Register(addKnownTypes) +} + +// Adds the list of known types to api.Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &CloudProfileConfig{}, + &InfrastructureConfig{}, + &InfrastructureStatus{}, + &InfrastructureState{}, + &ControlPlaneConfig{}, + &WorkerConfig{}, + &WorkerStatus{}, + ) + return nil +} diff --git a/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_cloudprofile.go b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_cloudprofile.go new file mode 100644 index 0000000..877b070 --- /dev/null +++ b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_cloudprofile.go @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CloudProfileConfig contains provider-specific configuration that is embedded into Gardener's `CloudProfile` +// resource. +type CloudProfileConfig struct { + metav1.TypeMeta `json:",inline"` + // MachineImages is the list of machine images that are understood by the controller. It maps + // logical names and versions to provider-specific identifiers. + MachineImages []MachineImages `json:"machineImages"` +} + +// MachineImages is a mapping from logical names and versions to provider-specific identifiers. +type MachineImages struct { + // Name is the logical name of the machine image. + Name string `json:"name"` + // Versions contains versions and a provider-specific identifier. + Versions []MachineImageVersion `json:"versions"` +} + +// MachineImageVersion contains a version and a provider-specific identifier. +type MachineImageVersion struct { + // Version is the version of the image. + Version string `json:"version"` + // Regions is a mapping to the correct AMI for the machine image in the supported regions. + Regions []RegionAMIMapping `json:"regions"` +} + +// RegionAMIMapping is a mapping to the correct AMI for the machine image in the given region. +type RegionAMIMapping struct { + // Name is the name of the region. + Name string `json:"name"` + // AMI is the AMI for the machine image. + AMI string `json:"ami"` + // Architecture is the CPU architecture of the machine image. + // +optional + Architecture *string `json:"architecture,omitempty"` +} diff --git a/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_controlplane.go b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_controlplane.go new file mode 100644 index 0000000..d078729 --- /dev/null +++ b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_controlplane.go @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControlPlaneConfig contains configuration settings for the control plane. +type ControlPlaneConfig struct { + metav1.TypeMeta `json:",inline"` + + // CloudControllerManager contains configuration settings for the cloud-controller-manager. + // +optional + CloudControllerManager *CloudControllerManagerConfig `json:"cloudControllerManager,omitempty"` + + // LoadBalancerController contains configuration settings for the optional aws-load-balancer-controller (ALB). + // +optional + LoadBalancerController *LoadBalancerControllerConfig `json:"loadBalancerController,omitempty"` + + // Storage contains configuration for storage in the cluster. + // +optional + Storage *Storage `json:"storage,omitempty"` +} + +// CloudControllerManagerConfig contains configuration settings for the cloud-controller-manager. +type CloudControllerManagerConfig struct { + // FeatureGates contains information about enabled feature gates. + // +optional + FeatureGates map[string]bool `json:"featureGates,omitempty"` + + // UseCustomRouteController controls if custom route controller should be used. + // Defaults to false. + // +optional + UseCustomRouteController *bool `json:"useCustomRouteController,omitempty"` +} + +// LoadBalancerControllerConfig contains configuration settings for the optional aws-load-balancer-controller (ALB). +type LoadBalancerControllerConfig struct { + // Enabled controls if the ALB should be deployed. + Enabled bool `json:"enabled"` + + // IngressClassName is the name of the ingress class the ALB controller will target. Default value is 'alb'. + // If empty string is specified, it will match all ingresses without ingress class annotation and ingresses of type alb + // +optional + IngressClassName *string `json:"ingressClassName,omitempty"` +} + +// Storage contains configuration for storage in the cluster. +type Storage struct { + // ManagedDefaultClass controls if the 'default' StorageClass and 'default' VolumeSnapshotClass + // would be marked as default. Set to false to manually set the default to another class not + // managed by Gardener. + // Defaults to true. + // +optional + ManagedDefaultClass *bool `json:"managedDefaultClass,omitempty"` +} diff --git a/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_infrastructure.go b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_infrastructure.go new file mode 100644 index 0000000..0abd480 --- /dev/null +++ b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_infrastructure.go @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// InfrastructureConfig infrastructure configuration resource +type InfrastructureConfig struct { + metav1.TypeMeta `json:",inline"` + + // EnableECRAccess specifies whether the IAM role policy for the worker nodes shall contain + // permissions to access the ECR. + // default: true + // +optional + EnableECRAccess *bool `json:"enableECRAccess,omitempty"` + + // DualStack specifies whether dual-stack or IPv4-only should be supported. + DualStack *DualStack `json:"dualStack,omitempty"` + + // Networks is the AWS specific network configuration (VPC, subnets, etc.) + Networks Networks `json:"networks"` + + // IgnoreTags allows to configure which resource tags on resources managed by Gardener should be ignored during + // infrastructure reconciliation. By default, all tags that are added outside of Gardener's / terraform's + // reconciliation will be removed during the next reconciliation. This field allows users and automation to add + // custom tags on resources created and managed by Gardener without loosing them on the next reconciliation. + // See https://registry.terraform.io/providers/hashicorp/aws/latest/docs/guides/resource-tagging#ignoring-changes-in-all-resources + // for details of the underlying terraform implementation. + // +optional + IgnoreTags *IgnoreTags `json:"ignoreTags,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// InfrastructureStatus contains information about created infrastructure resources. +type InfrastructureStatus struct { + metav1.TypeMeta `json:",inline"` + // EC2 contains information about the created AWS EC2 resources. + EC2 EC2 `json:"ec2"` + // IAM contains information about the created AWS IAM resources. + IAM IAM `json:"iam"` + // VPC contains information about the created AWS VPC and some related resources. + VPC VPCStatus `json:"vpc"` +} + +// Networks holds information about the Kubernetes and infrastructure networks. +type Networks struct { + // VPC indicates whether to use an existing VPC or create a new one. + VPC VPC `json:"vpc"` + // Zones belonging to the same region + Zones []Zone `json:"zones"` +} + +// IgnoreTags holds information about ignored resource tags. +type IgnoreTags struct { + // Keys is a list of individual tag keys, that should be ignored during infrastructure reconciliation. + // +optional + Keys []string `json:"keys,omitempty"` + // KeyPrefixes is a list of tag key prefixes, that should be ignored during infrastructure reconciliation. + // +optional + KeyPrefixes []string `json:"keyPrefixes,omitempty"` +} + +// Zone describes the properties of a zone. +type Zone struct { + // Name is the name for this zone. + Name string `json:"name"` + // Internal is the private subnet range to create (used for internal load balancers). + Internal string `json:"internal"` + // Public is the public subnet range to create (used for bastion and load balancers). + Public string `json:"public"` + // Workers is the workers subnet range to create (used for the VMs). + Workers string `json:"workers"` + // ElasticIPAllocationID contains the allocation ID of an Elastic IP that will be attached to the NAT gateway in + // this zone (e.g., `eipalloc-123456`). If it's not provided then a new Elastic IP will be automatically created + // and attached. + // Important: If this field is changed then the already attached Elastic IP will be disassociated from the NAT gateway + // (and potentially removed if it was created by this extension). Also, the NAT gateway will be deleted. This will + // disrupt egress traffic for a while. + // +optional + ElasticIPAllocationID *string `json:"elasticIPAllocationID,omitempty"` +} + +// EC2 contains information about the AWS EC2 resources. +type EC2 struct { + // KeyName is the name of the SSH key. + KeyName string `json:"keyName"` +} + +// IAM contains information about the AWS IAM resources. +type IAM struct { + // InstanceProfiles is a list of AWS IAM instance profiles. + InstanceProfiles []InstanceProfile `json:"instanceProfiles"` + // Roles is a list of AWS IAM roles. + Roles []Role `json:"roles"` +} + +// VPC contains information about the AWS VPC and some related resources. +type VPC struct { + // ID is the VPC id. + // +optional + ID *string `json:"id,omitempty"` + // CIDR is the VPC CIDR. + // +optional + CIDR *string `json:"cidr,omitempty"` + // GatewayEndpoints service names to configure as gateway endpoints in the VPC. + // +optional + GatewayEndpoints []string `json:"gatewayEndpoints,omitempty"` +} + +// VPCStatus contains information about a generated VPC or resources inside an existing VPC. +type VPCStatus struct { + // ID is the VPC id. + ID string `json:"id"` + // Subnets is a list of subnets that have been created. + Subnets []Subnet `json:"subnets"` + // SecurityGroups is a list of security groups that have been created. + SecurityGroups []SecurityGroup `json:"securityGroups"` +} + +const ( + // PurposeNodes is a constant describing that the respective resource is used for nodes. + PurposeNodes string = "nodes" + // PurposePublic is a constant describing that the respective resource is used for public load balancers. + PurposePublic string = "public" + // PurposeInternal is a constant describing that the respective resource is used for internal load balancers. + PurposeInternal string = "internal" +) + +// InstanceProfile is an AWS IAM instance profile. +type InstanceProfile struct { + // Purpose is a logical description of the instance profile. + Purpose string `json:"purpose"` + // Name is the name for this instance profile. + Name string `json:"name"` +} + +// Role is an AWS IAM role. +type Role struct { + // Purpose is a logical description of the role. + Purpose string `json:"purpose"` + // ARN is the AWS Resource Name for this role. + ARN string `json:"arn"` +} + +// Subnet is an AWS subnet related to a VPC. +type Subnet struct { + // Purpose is a logical description of the subnet. + Purpose string `json:"purpose"` + // ID is the subnet id. + ID string `json:"id"` + // Zone is the availability zone into which the subnet has been created. + Zone string `json:"zone"` +} + +// SecurityGroup is an AWS security group related to a VPC. +type SecurityGroup struct { + // Purpose is a logical description of the security group. + Purpose string `json:"purpose"` + // ID is the subnet id. + ID string `json:"id"` +} + +// DualStack specifies whether dual-stack or IPv4-only should be supported. +type DualStack struct { + // Enabled specifies if dual-stack is enabled or not. + Enabled bool `json:"enabled"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// InfrastructureState is the state which is persisted as part of the infrastructure status. +type InfrastructureState struct { + metav1.TypeMeta + + Data map[string]string `json:"data"` +} diff --git a/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_worker.go b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_worker.go new file mode 100644 index 0000000..e56c933 --- /dev/null +++ b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/types_worker.go @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + extensionsv1alpha1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/extensions/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// WorkerConfig contains configuration settings for the worker nodes. +type WorkerConfig struct { + metav1.TypeMeta `json:",inline"` + + // NodeTemplate contains resource information of the machine which is used by Cluster Autoscaler to generate nodeTemplate during scaling a nodeGroup from zero. + // +optional + NodeTemplate *extensionsv1alpha1.NodeTemplate `json:"nodeTemplate,omitempty"` + // Volume contains configuration for the root disks attached to VMs. + // +optional + Volume *Volume `json:"volume,omitempty"` + // DataVolumes contains configuration for the additional disks attached to VMs. + // +optional + DataVolumes []DataVolume `json:"dataVolumes,omitempty"` + // IAMInstanceProfile contains configuration for the IAM instance profile that should be used for the VMs of this + // worker pool. + // +optional + IAMInstanceProfile *IAMInstanceProfile `json:"iamInstanceProfile,omitempty"` + // InstanceMetadataOptions contains configuration for controlling access to the metadata API. + InstanceMetadataOptions *InstanceMetadataOptions `json:"instanceMetadataOptions,omitempty"` + // CpuOptions contains detailed configuration for the number of cores and threads for the instance. + CpuOptions *CpuOptions `json:"cpuOptions,omitempty"` +} + +// Volume contains configuration for the root disks attached to VMs. +type Volume struct { + // IOPS is the number of I/O operations per second (IOPS) that the volume supports. + // For io1 volume type, this represents the number of IOPS that are provisioned for the + // volume. For gp2 volume type, this represents the baseline performance of the volume and + // the rate at which the volume accumulates I/O credits for bursting. For more + // information about General Purpose SSD baseline performance, I/O credits, + // and bursting, see Amazon EBS Volume Types (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html) + // in the Amazon Elastic Compute Cloud User Guide. + // + // Constraint: Range is 100-20000 IOPS for io1 volumes and 100-10000 IOPS for + // gp2 volumes. + // + // Condition: This parameter is required for requests to create io1 volumes; + // it is not used in requests to create gp2, st1, sc1, or standard volumes. + // +optional + IOPS *int64 `json:"iops,omitempty"` + + // The throughput that the volume supports, in MiB/s. + // + // This parameter is valid only for gp3 volumes. + // + // Valid Range: The range as of 16th Aug 2022 is from 125 MiB/s to 1000 MiB/s. For more info refer (http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSVolumeTypes.html) + Throughput *int64 `json:"throughput,omitempty"` +} + +// DataVolume contains configuration for data volumes attached to VMs. +type DataVolume struct { + // Name is the name of the data volume this configuration applies to. + Name string `json:"name"` + // Volume contains configuration for the volume. + Volume `json:",inline"` + // SnapshotID is the ID of the snapshot. + // +optional + SnapshotID *string `json:"snapshotID,omitempty"` +} + +// IAMInstanceProfile contains configuration for the IAM instance profile that should be used for the VMs of this +// worker pool. Either 'Name" or 'ARN' must be specified. +type IAMInstanceProfile struct { + // Name is the name of the instance profile. + // +optional + Name *string `json:"name,omitempty"` + // ARN is the ARN of the instance profile. + // +optional + ARN *string `json:"arn,omitempty"` +} + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// WorkerStatus contains information about created worker resources. +type WorkerStatus struct { + metav1.TypeMeta `json:",inline"` + + // MachineImages is a list of machine images that have been used in this worker. Usually, the extension controller + // gets the mapping from name/version to the provider-specific machine image data in its componentconfig. However, if + // a version that is still in use gets removed from this componentconfig it cannot reconcile anymore existing `Worker` + // resources that are still using this version. Hence, it stores the used versions in the provider status to ensure + // reconciliation is possible. + // +optional + MachineImages []MachineImage `json:"machineImages,omitempty"` +} + +// MachineImage is a mapping from logical names and versions to provider-specific machine image data. +type MachineImage struct { + // Name is the logical name of the machine image. + Name string `json:"name"` + // Version is the logical version of the machine image. + Version string `json:"version"` + // AMI is the AMI for the machine image. + AMI string `json:"ami"` + // Architecture is the CPU architecture of the machine image. + // +optional + Architecture *string `json:"architecture,omitempty"` +} + +// VolumeType is a constant for volume types. +type VolumeType string + +const ( + // VolumeTypeIO1 is a constant for the io1 volume type. + VolumeTypeIO1 VolumeType = "io1" + // VolumeTypeGP2 is a constant for the gp2 volume type. + VolumeTypeGP2 VolumeType = "gp2" + // VolumeTypeGP3 is a constant for the gp3 volume type. + VolumeTypeGP3 VolumeType = "gp3" +) + +// HTTPTokensValue is a constant for HTTPTokens values. +type HTTPTokensValue string + +const ( + // HTTPTokensRequired is a constant for requiring the use of tokens to access IMDS. Effectively disables access via + // the IMDSv1 endpoints. + HTTPTokensRequired HTTPTokensValue = "required" + // HTTPTokensOptional that makes the use of tokens for IMDS optional. Effectively allows access via both IMDSv1 and + // IMDSv2 endpoints. + HTTPTokensOptional HTTPTokensValue = "optional" +) + +// InstanceMetadataOptions contains configuration for controlling access to the metadata API. +type InstanceMetadataOptions struct { + // HTTPTokens enforces the use of metadata v2 API. + HTTPTokens *HTTPTokensValue `json:"httpTokens,omitempty"` + // HTTPPutResponseHopLimit is the response hop limit for instance metadata requests. + // Valid values are between 1 and 64. + HTTPPutResponseHopLimit *int64 `json:"httpPutResponseHopLimit,omitempty"` +} + +// CpuOptions contains detailed configuration for the number of cores and threads for the instance. +type CpuOptions struct { + // CoreCount specifies the number of CPU cores per instance. + CoreCount *int64 `json:"coreCount"` + // ThreadsPerCore sets the number of threads per core. Must be either '1' (disable multi-threading) or '2'. + ThreadsPerCore *int64 `json:"threadsPerCore"` +} diff --git a/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/zz_generated.deepcopy.go b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..39d8cad --- /dev/null +++ b/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,811 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + extensionsv1alpha1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/extensions/v1alpha1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudControllerManagerConfig) DeepCopyInto(out *CloudControllerManagerConfig) { + *out = *in + if in.FeatureGates != nil { + in, out := &in.FeatureGates, &out.FeatureGates + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.UseCustomRouteController != nil { + in, out := &in.UseCustomRouteController, &out.UseCustomRouteController + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudControllerManagerConfig. +func (in *CloudControllerManagerConfig) DeepCopy() *CloudControllerManagerConfig { + if in == nil { + return nil + } + out := new(CloudControllerManagerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudProfileConfig) DeepCopyInto(out *CloudProfileConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.MachineImages != nil { + in, out := &in.MachineImages, &out.MachineImages + *out = make([]MachineImages, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudProfileConfig. +func (in *CloudProfileConfig) DeepCopy() *CloudProfileConfig { + if in == nil { + return nil + } + out := new(CloudProfileConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CloudProfileConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneConfig) DeepCopyInto(out *ControlPlaneConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.CloudControllerManager != nil { + in, out := &in.CloudControllerManager, &out.CloudControllerManager + *out = new(CloudControllerManagerConfig) + (*in).DeepCopyInto(*out) + } + if in.LoadBalancerController != nil { + in, out := &in.LoadBalancerController, &out.LoadBalancerController + *out = new(LoadBalancerControllerConfig) + (*in).DeepCopyInto(*out) + } + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(Storage) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneConfig. +func (in *ControlPlaneConfig) DeepCopy() *ControlPlaneConfig { + if in == nil { + return nil + } + out := new(ControlPlaneConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControlPlaneConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CpuOptions) DeepCopyInto(out *CpuOptions) { + *out = *in + if in.CoreCount != nil { + in, out := &in.CoreCount, &out.CoreCount + *out = new(int64) + **out = **in + } + if in.ThreadsPerCore != nil { + in, out := &in.ThreadsPerCore, &out.ThreadsPerCore + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CpuOptions. +func (in *CpuOptions) DeepCopy() *CpuOptions { + if in == nil { + return nil + } + out := new(CpuOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataVolume) DeepCopyInto(out *DataVolume) { + *out = *in + in.Volume.DeepCopyInto(&out.Volume) + if in.SnapshotID != nil { + in, out := &in.SnapshotID, &out.SnapshotID + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataVolume. +func (in *DataVolume) DeepCopy() *DataVolume { + if in == nil { + return nil + } + out := new(DataVolume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DualStack) DeepCopyInto(out *DualStack) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DualStack. +func (in *DualStack) DeepCopy() *DualStack { + if in == nil { + return nil + } + out := new(DualStack) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EC2) DeepCopyInto(out *EC2) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EC2. +func (in *EC2) DeepCopy() *EC2 { + if in == nil { + return nil + } + out := new(EC2) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IAM) DeepCopyInto(out *IAM) { + *out = *in + if in.InstanceProfiles != nil { + in, out := &in.InstanceProfiles, &out.InstanceProfiles + *out = make([]InstanceProfile, len(*in)) + copy(*out, *in) + } + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]Role, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IAM. +func (in *IAM) DeepCopy() *IAM { + if in == nil { + return nil + } + out := new(IAM) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IAMInstanceProfile) DeepCopyInto(out *IAMInstanceProfile) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.ARN != nil { + in, out := &in.ARN, &out.ARN + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IAMInstanceProfile. +func (in *IAMInstanceProfile) DeepCopy() *IAMInstanceProfile { + if in == nil { + return nil + } + out := new(IAMInstanceProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IgnoreTags) DeepCopyInto(out *IgnoreTags) { + *out = *in + if in.Keys != nil { + in, out := &in.Keys, &out.Keys + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.KeyPrefixes != nil { + in, out := &in.KeyPrefixes, &out.KeyPrefixes + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IgnoreTags. +func (in *IgnoreTags) DeepCopy() *IgnoreTags { + if in == nil { + return nil + } + out := new(IgnoreTags) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureConfig) DeepCopyInto(out *InfrastructureConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.EnableECRAccess != nil { + in, out := &in.EnableECRAccess, &out.EnableECRAccess + *out = new(bool) + **out = **in + } + if in.DualStack != nil { + in, out := &in.DualStack, &out.DualStack + *out = new(DualStack) + **out = **in + } + in.Networks.DeepCopyInto(&out.Networks) + if in.IgnoreTags != nil { + in, out := &in.IgnoreTags, &out.IgnoreTags + *out = new(IgnoreTags) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureConfig. +func (in *InfrastructureConfig) DeepCopy() *InfrastructureConfig { + if in == nil { + return nil + } + out := new(InfrastructureConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfrastructureConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureState) DeepCopyInto(out *InfrastructureState) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureState. +func (in *InfrastructureState) DeepCopy() *InfrastructureState { + if in == nil { + return nil + } + out := new(InfrastructureState) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfrastructureState) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureStatus) DeepCopyInto(out *InfrastructureStatus) { + *out = *in + out.TypeMeta = in.TypeMeta + out.EC2 = in.EC2 + in.IAM.DeepCopyInto(&out.IAM) + in.VPC.DeepCopyInto(&out.VPC) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureStatus. +func (in *InfrastructureStatus) DeepCopy() *InfrastructureStatus { + if in == nil { + return nil + } + out := new(InfrastructureStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfrastructureStatus) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceMetadataOptions) DeepCopyInto(out *InstanceMetadataOptions) { + *out = *in + if in.HTTPTokens != nil { + in, out := &in.HTTPTokens, &out.HTTPTokens + *out = new(HTTPTokensValue) + **out = **in + } + if in.HTTPPutResponseHopLimit != nil { + in, out := &in.HTTPPutResponseHopLimit, &out.HTTPPutResponseHopLimit + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceMetadataOptions. +func (in *InstanceMetadataOptions) DeepCopy() *InstanceMetadataOptions { + if in == nil { + return nil + } + out := new(InstanceMetadataOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InstanceProfile) DeepCopyInto(out *InstanceProfile) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceProfile. +func (in *InstanceProfile) DeepCopy() *InstanceProfile { + if in == nil { + return nil + } + out := new(InstanceProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LoadBalancerControllerConfig) DeepCopyInto(out *LoadBalancerControllerConfig) { + *out = *in + if in.IngressClassName != nil { + in, out := &in.IngressClassName, &out.IngressClassName + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerControllerConfig. +func (in *LoadBalancerControllerConfig) DeepCopy() *LoadBalancerControllerConfig { + if in == nil { + return nil + } + out := new(LoadBalancerControllerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineImage) DeepCopyInto(out *MachineImage) { + *out = *in + if in.Architecture != nil { + in, out := &in.Architecture, &out.Architecture + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineImage. +func (in *MachineImage) DeepCopy() *MachineImage { + if in == nil { + return nil + } + out := new(MachineImage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineImageVersion) DeepCopyInto(out *MachineImageVersion) { + *out = *in + if in.Regions != nil { + in, out := &in.Regions, &out.Regions + *out = make([]RegionAMIMapping, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineImageVersion. +func (in *MachineImageVersion) DeepCopy() *MachineImageVersion { + if in == nil { + return nil + } + out := new(MachineImageVersion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineImages) DeepCopyInto(out *MachineImages) { + *out = *in + if in.Versions != nil { + in, out := &in.Versions, &out.Versions + *out = make([]MachineImageVersion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineImages. +func (in *MachineImages) DeepCopy() *MachineImages { + if in == nil { + return nil + } + out := new(MachineImages) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Networks) DeepCopyInto(out *Networks) { + *out = *in + in.VPC.DeepCopyInto(&out.VPC) + if in.Zones != nil { + in, out := &in.Zones, &out.Zones + *out = make([]Zone, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Networks. +func (in *Networks) DeepCopy() *Networks { + if in == nil { + return nil + } + out := new(Networks) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegionAMIMapping) DeepCopyInto(out *RegionAMIMapping) { + *out = *in + if in.Architecture != nil { + in, out := &in.Architecture, &out.Architecture + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegionAMIMapping. +func (in *RegionAMIMapping) DeepCopy() *RegionAMIMapping { + if in == nil { + return nil + } + out := new(RegionAMIMapping) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Role) DeepCopyInto(out *Role) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Role. +func (in *Role) DeepCopy() *Role { + if in == nil { + return nil + } + out := new(Role) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecurityGroup) DeepCopyInto(out *SecurityGroup) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityGroup. +func (in *SecurityGroup) DeepCopy() *SecurityGroup { + if in == nil { + return nil + } + out := new(SecurityGroup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Storage) DeepCopyInto(out *Storage) { + *out = *in + if in.ManagedDefaultClass != nil { + in, out := &in.ManagedDefaultClass, &out.ManagedDefaultClass + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Storage. +func (in *Storage) DeepCopy() *Storage { + if in == nil { + return nil + } + out := new(Storage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Subnet) DeepCopyInto(out *Subnet) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Subnet. +func (in *Subnet) DeepCopy() *Subnet { + if in == nil { + return nil + } + out := new(Subnet) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPC) DeepCopyInto(out *VPC) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.CIDR != nil { + in, out := &in.CIDR, &out.CIDR + *out = new(string) + **out = **in + } + if in.GatewayEndpoints != nil { + in, out := &in.GatewayEndpoints, &out.GatewayEndpoints + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPC. +func (in *VPC) DeepCopy() *VPC { + if in == nil { + return nil + } + out := new(VPC) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VPCStatus) DeepCopyInto(out *VPCStatus) { + *out = *in + if in.Subnets != nil { + in, out := &in.Subnets, &out.Subnets + *out = make([]Subnet, len(*in)) + copy(*out, *in) + } + if in.SecurityGroups != nil { + in, out := &in.SecurityGroups, &out.SecurityGroups + *out = make([]SecurityGroup, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VPCStatus. +func (in *VPCStatus) DeepCopy() *VPCStatus { + if in == nil { + return nil + } + out := new(VPCStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Volume) DeepCopyInto(out *Volume) { + *out = *in + if in.IOPS != nil { + in, out := &in.IOPS, &out.IOPS + *out = new(int64) + **out = **in + } + if in.Throughput != nil { + in, out := &in.Throughput, &out.Throughput + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Volume. +func (in *Volume) DeepCopy() *Volume { + if in == nil { + return nil + } + out := new(Volume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerConfig) DeepCopyInto(out *WorkerConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.NodeTemplate != nil { + in, out := &in.NodeTemplate, &out.NodeTemplate + *out = new(extensionsv1alpha1.NodeTemplate) + (*in).DeepCopyInto(*out) + } + if in.Volume != nil { + in, out := &in.Volume, &out.Volume + *out = new(Volume) + (*in).DeepCopyInto(*out) + } + if in.DataVolumes != nil { + in, out := &in.DataVolumes, &out.DataVolumes + *out = make([]DataVolume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.IAMInstanceProfile != nil { + in, out := &in.IAMInstanceProfile, &out.IAMInstanceProfile + *out = new(IAMInstanceProfile) + (*in).DeepCopyInto(*out) + } + if in.InstanceMetadataOptions != nil { + in, out := &in.InstanceMetadataOptions, &out.InstanceMetadataOptions + *out = new(InstanceMetadataOptions) + (*in).DeepCopyInto(*out) + } + if in.CpuOptions != nil { + in, out := &in.CpuOptions, &out.CpuOptions + *out = new(CpuOptions) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerConfig. +func (in *WorkerConfig) DeepCopy() *WorkerConfig { + if in == nil { + return nil + } + out := new(WorkerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WorkerConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerStatus) DeepCopyInto(out *WorkerStatus) { + *out = *in + out.TypeMeta = in.TypeMeta + if in.MachineImages != nil { + in, out := &in.MachineImages, &out.MachineImages + *out = make([]MachineImage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerStatus. +func (in *WorkerStatus) DeepCopy() *WorkerStatus { + if in == nil { + return nil + } + out := new(WorkerStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WorkerStatus) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Zone) DeepCopyInto(out *Zone) { + *out = *in + if in.ElasticIPAllocationID != nil { + in, out := &in.ElasticIPAllocationID, &out.ElasticIPAllocationID + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Zone. +func (in *Zone) DeepCopy() *Zone { + if in == nil { + return nil + } + out := new(Zone) + in.DeepCopyInto(out) + return out +} diff --git a/api/external/gardener/pkg/apis/authentication/v1alpha1/register.go b/api/external/gardener/pkg/apis/authentication/v1alpha1/register.go new file mode 100644 index 0000000..d89ee2b --- /dev/null +++ b/api/external/gardener/pkg/apis/authentication/v1alpha1/register.go @@ -0,0 +1,44 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the name of the authentication API group. +// "authentication.gardener.cloud/v1alpha1" API is already used for CRD registration and must not be served by the API server. +const GroupName = "authentication.gardener.cloud" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"} + +// Kind takes an unqualified kind and returns a Group qualified GroupKind. +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + // SchemeBuilder is a new Scheme Builder which registers our API. + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + localSchemeBuilder = &SchemeBuilder + // AddToScheme is a reference to the Scheme Builder's AddToScheme function. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &AdminKubeconfigRequest{}, + &ViewerKubeconfigRequest{}, + ) + + return nil +} diff --git a/api/external/gardener/pkg/apis/authentication/v1alpha1/types_adminkubeconfigrequest.go b/api/external/gardener/pkg/apis/authentication/v1alpha1/types_adminkubeconfigrequest.go new file mode 100644 index 0000000..d6b3ef2 --- /dev/null +++ b/api/external/gardener/pkg/apis/authentication/v1alpha1/types_adminkubeconfigrequest.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// AdminKubeconfigRequest can be used to request a kubeconfig with admin credentials +// for a Shoot cluster. +type AdminKubeconfigRequest struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec is the specification of the AdminKubeconfigRequest. + Spec AdminKubeconfigRequestSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + // Status is the status of the AdminKubeconfigRequest. + Status AdminKubeconfigRequestStatus `json:"status" protobuf:"bytes,3,opt,name=status"` +} + +// AdminKubeconfigRequestStatus is the status of the AdminKubeconfigRequest containing +// the kubeconfig and expiration of the credential. +type AdminKubeconfigRequestStatus struct { + // Kubeconfig contains the kubeconfig with cluster-admin privileges for the shoot cluster. + Kubeconfig []byte `json:"kubeconfig" protobuf:"bytes,1,opt,name=kubeconfig"` + // ExpirationTimestamp is the expiration timestamp of the returned credential. + ExpirationTimestamp metav1.Time `json:"expirationTimestamp" protobuf:"bytes,2,opt,name=expirationTimestamp"` +} + +// AdminKubeconfigRequestSpec contains the expiration time of the kubeconfig. +type AdminKubeconfigRequestSpec struct { + // ExpirationSeconds is the requested validity duration of the credential. The + // credential issuer may return a credential with a different validity duration so a + // client needs to check the 'expirationTimestamp' field in a response. + // Defaults to 1 hour. + // +optional + ExpirationSeconds *int64 `json:"expirationSeconds,omitempty" protobuf:"varint,1,opt,name=expirationSeconds"` +} diff --git a/api/external/gardener/pkg/apis/authentication/v1alpha1/types_viewerkubeconfigrequest.go b/api/external/gardener/pkg/apis/authentication/v1alpha1/types_viewerkubeconfigrequest.go new file mode 100644 index 0000000..147997c --- /dev/null +++ b/api/external/gardener/pkg/apis/authentication/v1alpha1/types_viewerkubeconfigrequest.go @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ViewerKubeconfigRequest can be used to request a kubeconfig with viewer credentials (excluding Secrets) +// for a Shoot cluster. +type ViewerKubeconfigRequest struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec is the specification of the ViewerKubeconfigRequest. + Spec ViewerKubeconfigRequestSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + // Status is the status of the ViewerKubeconfigRequest. + Status ViewerKubeconfigRequestStatus `json:"status" protobuf:"bytes,3,opt,name=status"` +} + +// ViewerKubeconfigRequestStatus is the status of the ViewerKubeconfigRequest containing +// the kubeconfig and expiration of the credential. +type ViewerKubeconfigRequestStatus struct { + // Kubeconfig contains the kubeconfig with viewer privileges (excluding Secrets) for the shoot cluster. + Kubeconfig []byte `json:"kubeconfig" protobuf:"bytes,1,opt,name=kubeconfig"` + // ExpirationTimestamp is the expiration timestamp of the returned credential. + ExpirationTimestamp metav1.Time `json:"expirationTimestamp" protobuf:"bytes,2,opt,name=expirationTimestamp"` +} + +// ViewerKubeconfigRequestSpec contains the expiration time of the kubeconfig. +type ViewerKubeconfigRequestSpec struct { + // ExpirationSeconds is the requested validity duration of the credential. The + // credential issuer may return a credential with a different validity duration so a + // client needs to check the 'expirationTimestamp' field in a response. + // Defaults to 1 hour. + // +optional + ExpirationSeconds *int64 `json:"expirationSeconds,omitempty" protobuf:"varint,1,opt,name=expirationSeconds"` +} diff --git a/api/external/gardener/pkg/apis/authentication/v1alpha1/zz_generated.deepcopy.go b/api/external/gardener/pkg/apis/authentication/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..57e5fb9 --- /dev/null +++ b/api/external/gardener/pkg/apis/authentication/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,156 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdminKubeconfigRequest) DeepCopyInto(out *AdminKubeconfigRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdminKubeconfigRequest. +func (in *AdminKubeconfigRequest) DeepCopy() *AdminKubeconfigRequest { + if in == nil { + return nil + } + out := new(AdminKubeconfigRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AdminKubeconfigRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdminKubeconfigRequestSpec) DeepCopyInto(out *AdminKubeconfigRequestSpec) { + *out = *in + if in.ExpirationSeconds != nil { + in, out := &in.ExpirationSeconds, &out.ExpirationSeconds + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdminKubeconfigRequestSpec. +func (in *AdminKubeconfigRequestSpec) DeepCopy() *AdminKubeconfigRequestSpec { + if in == nil { + return nil + } + out := new(AdminKubeconfigRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdminKubeconfigRequestStatus) DeepCopyInto(out *AdminKubeconfigRequestStatus) { + *out = *in + if in.Kubeconfig != nil { + in, out := &in.Kubeconfig, &out.Kubeconfig + *out = make([]byte, len(*in)) + copy(*out, *in) + } + in.ExpirationTimestamp.DeepCopyInto(&out.ExpirationTimestamp) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdminKubeconfigRequestStatus. +func (in *AdminKubeconfigRequestStatus) DeepCopy() *AdminKubeconfigRequestStatus { + if in == nil { + return nil + } + out := new(AdminKubeconfigRequestStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ViewerKubeconfigRequest) DeepCopyInto(out *ViewerKubeconfigRequest) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ViewerKubeconfigRequest. +func (in *ViewerKubeconfigRequest) DeepCopy() *ViewerKubeconfigRequest { + if in == nil { + return nil + } + out := new(ViewerKubeconfigRequest) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ViewerKubeconfigRequest) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ViewerKubeconfigRequestSpec) DeepCopyInto(out *ViewerKubeconfigRequestSpec) { + *out = *in + if in.ExpirationSeconds != nil { + in, out := &in.ExpirationSeconds, &out.ExpirationSeconds + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ViewerKubeconfigRequestSpec. +func (in *ViewerKubeconfigRequestSpec) DeepCopy() *ViewerKubeconfigRequestSpec { + if in == nil { + return nil + } + out := new(ViewerKubeconfigRequestSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ViewerKubeconfigRequestStatus) DeepCopyInto(out *ViewerKubeconfigRequestStatus) { + *out = *in + if in.Kubeconfig != nil { + in, out := &in.Kubeconfig, &out.Kubeconfig + *out = make([]byte, len(*in)) + copy(*out, *in) + } + in.ExpirationTimestamp.DeepCopyInto(&out.ExpirationTimestamp) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ViewerKubeconfigRequestStatus. +func (in *ViewerKubeconfigRequestStatus) DeepCopy() *ViewerKubeconfigRequestStatus { + if in == nil { + return nil + } + out := new(ViewerKubeconfigRequestStatus) + in.DeepCopyInto(out) + return out +} diff --git a/api/external/gardener/pkg/apis/core/types.go b/api/external/gardener/pkg/apis/core/types.go new file mode 100644 index 0000000..ec9a7dc --- /dev/null +++ b/api/external/gardener/pkg/apis/core/types.go @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package core + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // GardenerSeedLeaseNamespace is the namespace in which Gardenlet will report Seeds' + // status using Lease resources for each Seed + GardenerSeedLeaseNamespace = "gardener-system-seed-lease" +) + +// Object is a core object resource. +type Object interface { + metav1.Object +} + +// IPFamily is a type for specifying an IP protocol version to use in Gardener clusters. +type IPFamily string + +const ( + // IPFamilyIPv4 is the IPv4 IP family. + IPFamilyIPv4 IPFamily = "IPv4" + // IPFamilyIPv6 is the IPv6 IP family. + IPFamilyIPv6 IPFamily = "IPv6" +) + +// IsIPv4SingleStack determines whether the given list of IP families specifies IPv4 single-stack networking. +func IsIPv4SingleStack(ipFamilies []IPFamily) bool { + return len(ipFamilies) == 0 || (len(ipFamilies) == 1 && ipFamilies[0] == IPFamilyIPv4) +} + +// IsIPv6SingleStack determines whether the given list of IP families specifies IPv6 single-stack networking. +func IsIPv6SingleStack(ipFamilies []IPFamily) bool { + return len(ipFamilies) == 1 && ipFamilies[0] == IPFamilyIPv6 +} + +// AccessRestriction describes an access restriction for a Kubernetes cluster (e.g., EU access-only). +type AccessRestriction struct { + // Name is the name of the restriction. + Name string +} + +// AccessRestrictionWithOptions describes an access restriction for a Kubernetes cluster (e.g., EU access-only) and +// allows to specify additional options. +type AccessRestrictionWithOptions struct { + AccessRestriction + // Options is a map of additional options for the access restriction. + // +optional + Options map[string]string +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/constants/types_constants.go b/api/external/gardener/pkg/apis/core/v1beta1/constants/types_constants.go new file mode 100644 index 0000000..604a5cb --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/constants/types_constants.go @@ -0,0 +1,972 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package constants + +import ( + "time" +) + +const ( + // SecretManagerIdentityControllerManager is the identity for the secret manager used inside controller-manager. + SecretManagerIdentityControllerManager = "controller-manager" + // SecretManagerIdentityGardenlet is the identity for the secret manager used inside gardenlet. + SecretManagerIdentityGardenlet = "gardenlet" + + // SecretNameCACluster is a constant for the name of a Kubernetes secret object that contains the CA + // certificate of a shoot cluster. + SecretNameCACluster = "ca" + // SecretNameCAClient is a constant for the name of a Kubernetes secret object that contains the client CA + // certificate of a shoot cluster. + SecretNameCAClient = "ca-client" + // SecretNameCAETCD is a constant for the name of a Kubernetes secret object that contains the CA + // certificate of the etcd of a shoot cluster. + SecretNameCAETCD = "ca-etcd" + // SecretNameCAETCDPeer is a constant for the name of a Kubernetes secret object that contains the CA + // certificate of the etcd peer network of a shoot cluster. + SecretNameCAETCDPeer = "ca-etcd-peer" // #nosec G101 -- No credential. + // SecretNameCAFrontProxy is a constant for the name of a Kubernetes secret object that contains the CA + // certificate of the kube-aggregator a shoot cluster. + SecretNameCAFrontProxy = "ca-front-proxy" + // SecretNameCAKubelet is a constant for the name of a Kubernetes secret object that contains the CA + // certificate of the kubelet of a shoot cluster. + SecretNameCAKubelet = "ca-kubelet" + // SecretNameCAMetricsServer is a constant for the name of a Kubernetes secret object that contains the CA + // certificate of the metrics-server of a shoot cluster. + SecretNameCAMetricsServer = "ca-metrics-server" // #nosec G101 -- No credential. + // SecretNameCAVPN is a constant for the name of a Kubernetes secret object that contains the CA + // certificate of the VPN components of a shoot cluster. + SecretNameCAVPN = "ca-vpn" + // SecretNameCASeed is a constant for the name of a Kubernetes secret object that contains the CA + // certificate generated for a seed cluster. + SecretNameCASeed = "ca-seed" + + // SecretNameCloudProvider is a constant for the name of a Kubernetes secret object that contains the provider + // specific credentials that shall be used to create/delete the shoot. + SecretNameCloudProvider = "cloudprovider" + // SecretNameSSHKeyPair is a constant for the name of a Kubernetes secret object that contains the SSH key pair + // (public and private key) that can be used to SSH into the shoot nodes. + SecretNameSSHKeyPair = "ssh-keypair" // #nosec G101 -- No credential. + // SecretNameServiceAccountKey is a constant for the name of a Kubernetes secret object that contains a + // PEM-encoded private RSA or ECDSA key used by the Kube Controller Manager to sign service account tokens. + SecretNameServiceAccountKey = "service-account-key" + // SecretNameObservabilityIngress is a constant for the name of a Kubernetes secret object that contains the ingress + // credentials for observability components. + SecretNameObservabilityIngress = "observability-ingress" // #nosec G101 -- No credential. + // SecretNameObservabilityIngressUsers is a constant for the name of a Kubernetes secret object that contains the + // user's ingress credentials for observability components. + SecretNameObservabilityIngressUsers = "observability-ingress-users" // #nosec G101 -- No credential. + // SecretNameETCDEncryptionKey is a constant for the name of a Kubernetes secret object that contains the key + // for encryption data in ETCD. + SecretNameETCDEncryptionKey = "kube-apiserver-etcd-encryption-key" // #nosec G101 -- No credential. + // SecretNamePrefixETCDEncryptionConfiguration is a constant for the name prefix of a Kubernetes secret object that + // contains the configuration for encryption data in ETCD. + SecretNamePrefixETCDEncryptionConfiguration = "kube-apiserver-etcd-encryption-configuration" // #nosec G101 -- No credential. + // SecretNameGardenerETCDEncryptionKey is a constant for the name of a Kubernetes secret object that contains the + // key for encryption data in ETCD for gardener-apiserver. + SecretNameGardenerETCDEncryptionKey = "gardener-apiserver-etcd-encryption-key" + // SecretNamePrefixGardenerETCDEncryptionConfiguration is a constant for the name prefix of a Kubernetes secret + // object that contains the configuration for encryption data in ETCD for gardener-apiserver. + SecretNamePrefixGardenerETCDEncryptionConfiguration = "gardener-apiserver-etcd-encryption-configuration" + + // SecretNameGardener is a constant for the name of a Kubernetes secret object that contains the client + // certificate and a kubeconfig for a shoot cluster. It is used by Gardener and can be used by extension + // controllers in order to communicate with the shoot's API server. The client certificate has administrator + // privileges. + SecretNameGardener = "gardener" + // SecretNameGardenerInternal is a constant for the name of a Kubernetes secret object that contains the client + // certificate and a kubeconfig for a shoot cluster. It is used by Gardener and can be used by extension + // controllers in order to communicate with the shoot's API server. The client certificate has administrator + // privileges. The difference to the "gardener" secret is that is contains the in-cluster endpoint as address to + // for the shoot API server instead the DNS name or load balancer address. + SecretNameGardenerInternal = "gardener-internal" + + // SecretPrefixGeneratedBackupBucket is a constant for the prefix of a secret name in the garden cluster related to + // BackpuBuckets. + SecretPrefixGeneratedBackupBucket = "generated-bucket-" + + // SecretNameGenericTokenKubeconfig is a constant for the name of the kubeconfig used by the shoot controlplane + // components to authenticate against the shoot Kubernetes API server. + // Use `pkg/extensions.GenericTokenKubeconfigSecretNameFromCluster` instead. + SecretNameGenericTokenKubeconfig = "generic-token-kubeconfig" + // SecretNameGenericGardenKubeconfig is a constant for the name of the kubeconfig used by the extension + // components to authenticate against the garden Kubernetes API server. + SecretNameGenericGardenKubeconfig = "generic-garden-kubeconfig" + // AnnotationKeyGenericTokenKubeconfigSecretName is a constant for the key of an annotation on + // extensions.gardener.cloud/v1alpha1.Cluster resources whose value contains the name of the generic token + // kubeconfig secret in the seed cluster. + AnnotationKeyGenericTokenKubeconfigSecretName = "generic-token-kubeconfig.secret.gardener.cloud/name" + + // ExtensionGardenServiceAccountPrefix is the prefix of the default garden ServiceAccount generated for each + // ControllerInstallation. + ExtensionGardenServiceAccountPrefix = "extension-" + + // ReferenceProtectionFinalizerName is the name of the finalizer used for the reference protection. + ReferenceProtectionFinalizerName = "gardener.cloud/reference-protection" + + // DeploymentNameClusterAutoscaler is a constant for the name of a Kubernetes deployment object that contains + // the cluster-autoscaler pod. + DeploymentNameClusterAutoscaler = "cluster-autoscaler" + // DeploymentNameKubeAPIServer is a constant for the name of a Kubernetes deployment object that contains + // the kube-apiserver pod. + DeploymentNameKubeAPIServer = "kube-apiserver" + // DeploymentNameKubeControllerManager is a constant for the name of a Kubernetes deployment object that contains + // the kube-controller-manager pod. + DeploymentNameKubeControllerManager = "kube-controller-manager" + // DeploymentNameDependencyWatchdogProber is a constant for the name of a Kubernetes deployment object that contains + // the dependency-watchdog-prober pod. + DeploymentNameDependencyWatchdogProber = "dependency-watchdog-prober" + // DeploymentNameDependencyWatchdogWeeder is a constant for the name of a Kubernetes deployment object that contains + // the dependency-watchdog-weeder pod. + DeploymentNameDependencyWatchdogWeeder = "dependency-watchdog-weeder" + // DeploymentNameGardenlet is a constant for the name of a Kubernetes deployment object that contains + // the Gardenlet pod. + DeploymentNameGardenlet = "gardenlet" + // DeploymentNameGardenerOperator is a constant for the name of a Kubernetes deployment object that contains + // the gardener-operator pod. + DeploymentNameGardenerOperator = "gardener-operator" + + // DeploymentNameVPNSeedServer is a constant for the name of a Kubernetes deployment object that contains + // the vpn-seed-server pod. + DeploymentNameVPNSeedServer = "vpn-seed-server" + + // DeploymentNameKubeScheduler is a constant for the name of a Kubernetes deployment object that contains + // the kube-scheduler pod. + DeploymentNameKubeScheduler = "kube-scheduler" + // DeploymentNameGardenerResourceManager is a constant for the name of a Kubernetes deployment object that contains + // the gardener-resource-manager pod. + DeploymentNameGardenerResourceManager = "gardener-resource-manager" + // DeploymentNamePlutono is a constant for the name of a Kubernetes deployment object that contains the plutono pod. + DeploymentNamePlutono = "plutono" + // DeploymentNameEventLogger is a constant for the name of a Kubernetes deployment object that contains + // the event-logger pod. + DeploymentNameEventLogger = "event-logger" + // DeploymentNameFluentOperator is a constant for the name of a Kubernetes deployment object that contains + // the fluent-operator pod. + DeploymentNameFluentOperator = "fluent-operator" + // DaemonSetNameFluentBit is a constant for the name of a Kubernetes Daemonset object that contains + // the fluent-bit pod. + DaemonSetNameFluentBit = "fluent-bit" + // DeploymentNameKubeStateMetrics is a constant for the name of a Kubernetes deployment object that contains + // the kube-state-metrics pod. + DeploymentNameKubeStateMetrics = "kube-state-metrics" + // DeploymentNameGardenerMetricsExporter is a constant for the name of a Kubernetes deployment object that contains + // the gardener-metrics-exporter pod. + DeploymentNameGardenerMetricsExporter = "gardener-metrics-exporter" + + // DeploymentNameVPAAdmissionController is a constant for the name of the VPA admission controller deployment. + DeploymentNameVPAAdmissionController = "vpa-admission-controller" + // DeploymentNameVPARecommender is a constant for the name of the VPA recommender deployment. + DeploymentNameVPARecommender = "vpa-recommender" + // DeploymentNameVPAUpdater is a constant for the name of the VPA updater deployment. + DeploymentNameVPAUpdater = "vpa-updater" + + // DeploymentNameKubernetesDashboard is a constant for the name of the kubernetes dashboard deployment. + DeploymentNameKubernetesDashboard = "kubernetes-dashboard" + // DeploymentNameDashboardMetricsScraper is a constant for the name of the dashboard metrics scraper deployment. + DeploymentNameDashboardMetricsScraper = "dashboard-metrics-scraper" + + // DeploymentNameMachineControllerManager is a constant for the name of a Kubernetes deployment object that contains + // the machine-controller-manager pod. + DeploymentNameMachineControllerManager = "machine-controller-manager" + + // ConfigMapNameShootInfo is the name of a ConfigMap in the kube-system namespace of shoot clusters which contains + // information about the shoot cluster. + ConfigMapNameShootInfo = "shoot-info" + + // StatefulSetNameAlertManager is a constant for the name of a Kubernetes stateful set object that contains + // the alertmanager pod. + StatefulSetNameAlertManager = "alertmanager" + // ETCDRoleMain is a constant for the main etcd role. + ETCDRoleMain = "main" + // ETCDRoleEvents is a constant for the events etcd role. + ETCDRoleEvents = "events" + // ETCDMain is a constant for the name of etcd-main Etcd object. + ETCDMain = "etcd-" + ETCDRoleMain + // ETCDEvents is a constant for the name of etcd-events Etcd object. + ETCDEvents = "etcd-" + ETCDRoleEvents + // StatefulSetNameVali is a constant for the name of a Kubernetes stateful set object that contains + // the vali pod. + StatefulSetNameVali = "vali" + + // GardenerPurpose is a constant for the key in a label describing the purpose of the respective object. + GardenerPurpose = "gardener.cloud/purpose" + // GardenerDescription is a constant for a key in an annotation describing what the resource is used for. + GardenerDescription = "gardener.cloud/description" + // GardenerWarning is a constant for a key in an annotation containing a warning message. + GardenerWarning = "gardener.cloud/warning" + + // GardenCreatedBy is the key for an annotation of a Shoot cluster whose value indicates contains the username + // of the user that created the resource. + GardenCreatedBy = "gardener.cloud/created-by" + // GardenerOperation is a constant for an annotation on a resource that describes a desired operation. + GardenerOperation = "gardener.cloud/operation" + // GardenerMaintenanceOperation is a constant for an annotation on a Shoot that describes a desired operation which + // will be performed during maintenance. + GardenerMaintenanceOperation = "maintenance.gardener.cloud/operation" + // GardenerOperationReconcile is a constant for the value of the operation annotation describing a reconcile + // operation. + GardenerOperationReconcile = "reconcile" + // GardenerTimestamp is a constant for an annotation on a resource that describes the timestamp when a reconciliation has been requested. + // It is only used to guarantee an update event for watching clients in case the operation-annotation is already present. + GardenerTimestamp = "gardener.cloud/timestamp" + // GardenerOperationMigrate is a constant for the value of the operation annotation describing a migration + // operation. + GardenerOperationMigrate = "migrate" + // GardenerOperationRestore is a constant for the value of the operation annotation describing a restoration + // operation. + GardenerOperationRestore = "restore" + // GardenerOperationWaitForState is a constant for the value of the operation annotation describing a wait + // operation. + GardenerOperationWaitForState = "wait-for-state" + // GardenerOperationKeepalive is a constant for the value of the operation annotation describing an + // operation that extends the lifetime of the object having the operation annotation. + GardenerOperationKeepalive = "keepalive" + // GardenerOperationRenewKubeconfig is a constant for the value of the operation annotation to renew the gardenlet's + // kubeconfig secret. + GardenerOperationRenewKubeconfig = "renew-kubeconfig" + + // GardenRole is a constant for a label that describes a role. + GardenRole = "gardener.cloud/role" + // GardenRoleExtension is a constant for a label that describes the 'extensions' role. + GardenRoleExtension = "extension" + // GardenRoleGarden is the value of the GardenRole key indicating type 'garden'. + GardenRoleGarden = "garden" + // GardenRoleSeed is the value of the GardenRole key indicating type 'seed'. + GardenRoleSeed = "seed" + // GardenRoleShoot is the value of the GardenRole key indicating type 'shoot'. + GardenRoleShoot = "shoot" + // GardenRoleLogging is the value of the GardenRole key indicating type 'logging'. + GardenRoleLogging = "logging" + // GardenRoleIstioSystem is the value of the GardenRole key indicating type 'istio-system'. + GardenRoleIstioSystem = "istio-system" + // GardenRoleIstioIngress is the value of the GardenRole key indicating type 'istio-ingress'. + GardenRoleIstioIngress = "istio-ingress" + // GardenRoleProject is the value of GardenRole key indicating type 'project'. + GardenRoleProject = "project" + // GardenRoleControlPlane is the value of the GardenRole key indicating type 'controlplane'. + GardenRoleControlPlane = "controlplane" + // GardenRoleSystemComponent is the value of the GardenRole key indicating type 'system-component'. + GardenRoleSystemComponent = "system-component" + // GardenRoleSeedSystemComponent is the value of the GardenRole key indicating type 'seed-system-component'. + GardenRoleSeedSystemComponent = "seed-system-component" + // GardenRoleMonitoring is the value of the GardenRole key indicating type 'monitoring'. + GardenRoleMonitoring = "monitoring" + // GardenRoleOptionalAddon is the value of the GardenRole key indicating type 'optional-addon'. + GardenRoleOptionalAddon = "optional-addon" + // GardenRoleOperatingSystemConfig is the value of the GardenRole key indicating type 'operating-system-config'. + GardenRoleOperatingSystemConfig = "operating-system-config" + // GardenRoleKubeconfig is the value of the GardenRole key indicating type 'kubeconfig'. + GardenRoleKubeconfig = "kubeconfig" + // GardenRoleCACluster is the value of the GardenRole key indicating type 'ca-cluster'. + GardenRoleCACluster = "ca-cluster" + // GardenRoleCAClient is the value of the GardenRole key indicating type 'ca-client'. + GardenRoleCAClient = "ca-client" + // GardenRoleSSHKeyPair is the value of the GardenRole key indicating type 'ssh-keypair'. + GardenRoleSSHKeyPair = "ssh-keypair" + // GardenRoleDefaultDomain is the value of the GardenRole key indicating type 'default-domain'. + GardenRoleDefaultDomain = "default-domain" + // GardenRoleInternalDomain is the value of the GardenRole key indicating type 'internal-domain'. + GardenRoleInternalDomain = "internal-domain" + // GardenRoleGlobalMonitoring is the value of the GardenRole key indicating type 'global-monitoring' + GardenRoleGlobalMonitoring = "global-monitoring" + // GardenRoleGlobalShootRemoteWriteMonitoring is the value of the GardenRole key indicating type 'global-shoot-remote-write-monitoring' + GardenRoleGlobalShootRemoteWriteMonitoring = "global-shoot-remote-write-monitoring" + // GardenRoleAlerting is the value of GardenRole key indicating type 'alerting'. + GardenRoleAlerting = "alerting" + // GardenRoleControlPlaneWildcardCert is the value of the GardenRole key indicating type 'controlplane-cert'. + // It refers to a wildcard TLS certificate which can be used for seed services exposed under the corresponding domain. + GardenRoleControlPlaneWildcardCert = "controlplane-cert" + // GardenRoleGardenWildcardCert is the value of the GardenRole key indicating type 'garden-cert'. + // It refers to a wildcard TLS certificate which can be used for Garden runtime services exposed under the corresponding domain. + GardenRoleGardenWildcardCert = "garden-cert" + // GardenRoleExposureClassHandler is the value of the GardenRole key indicating type 'exposureclass-handler'. + GardenRoleExposureClassHandler = "exposureclass-handler" + // GardenRoleShootServiceAccountIssuer is the value of the GardenRole key indicating type 'shoot-service-account-issuer'. + GardenRoleShootServiceAccountIssuer = "shoot-service-account-issuer" + + // ShootUID is an annotation key for the shoot namespace in the seed cluster, + // which value will be the value of `shoot.status.uid` + ShootUID = "shoot.gardener.cloud/uid" + // ShootPurpose is a constant for the shoot purpose. + ShootPurpose = "shoot.gardener.cloud/purpose" + // ShootSyncPeriod is a constant for an annotation on a Shoot which may be used to overwrite the global Shoot controller sync period. + // The value must be a duration. It can also be used to disable the reconciliation at all by setting it to 0m. Disabling the reconciliation + // does only mean that the period reconciliation is disabled. However, when the Gardener is restarted/redeployed or the specification is + // changed then the reconciliation flow will be executed. + ShootSyncPeriod = "shoot.gardener.cloud/sync-period" + // ShootIgnore is a constant for an annotation on a Shoot which may be used to tell the Gardener that the Shoot with this name should be + // ignored completely. That means that the Shoot will never reach the reconciliation flow (independent of the operation (create/update/ + // delete)). + ShootIgnore = "shoot.gardener.cloud/ignore" + // ShootNoCleanup is a constant for a label on a resource indicating that the Gardener cleaner should not delete this + // resource when cleaning a shoot during the deletion flow. + ShootNoCleanup = "shoot.gardener.cloud/no-cleanup" + + // ShootAlphaControlPlaneScaleDownDisabled is a constant for an annotation on the Shoot resource stating that the + // automatic scale-down shall be disabled for the etcd, kube-apiserver, kube-controller-manager. + // Note that this annotation is alpha and can be removed anytime without further notice. Only use it if you know + // what you do. + ShootAlphaControlPlaneScaleDownDisabled = "alpha.control-plane.scaling.shoot.gardener.cloud/scale-down-disabled" + + // ShootAlphaControlPlaneHAVPN is a constant for an annotation on the Shoot resource to enforce + // enabling/disabling the high availability setup for the VPN connection. + // By default, the HA setup for VPN connections is activated automatically if the control plane high availability is enabled. + // Note that this annotation is alpha and can be removed anytime without further notice. Only use it if you know + // what you do. + ShootAlphaControlPlaneHAVPN = "alpha.control-plane.shoot.gardener.cloud/high-availability-vpn" + // ShootAlphaControlPlaneVPNVPAUpdateDisabled is a constant for an annotation on the Shoot resource to enforce + // disabling the vertical pod autoscaler update resources related to the VPN connection. + ShootAlphaControlPlaneVPNVPAUpdateDisabled = "alpha.control-plane.shoot.gardener.cloud/vpn-vpa-update-disabled" + // ShootAlphaControlPlaneDisableNewVPN is a constant for an annotation on the Shoot resource to disabling the + // new Go implementation of VPN. + // TODO(MartinWeindel) Remove after feature gate `NewVPN` gets promoted to GA. + ShootAlphaControlPlaneDisableNewVPN = "alpha.control-plane.shoot.gardener.cloud/disable-new-vpn" + // ShootExpirationTimestamp is an annotation on a Shoot resource whose value represents the time when the Shoot lifetime + // is expired. The lifetime can be extended, but at most by the minimal value of the 'clusterLifetimeDays' property + // of referenced quotas. + ShootExpirationTimestamp = "shoot.gardener.cloud/expiration-timestamp" + // ShootStatus is a constant for a label on a Shoot resource indicating that the Shoot's health. + ShootStatus = "shoot.gardener.cloud/status" + // FailedShootNeedsRetryOperation is a constant for an annotation on a Shoot in a failed state indicating that a retry operation should be triggered during the next maintenance time window. + FailedShootNeedsRetryOperation = "maintenance.shoot.gardener.cloud/needs-retry-operation" + // LabelExcludeWebhookFromRemediation is a constant for a label on a webhook in the shoot which makes it being + // excluded from automatic remediation. + LabelExcludeWebhookFromRemediation = "remediation.webhook.shoot.gardener.cloud/exclude" + + // ShootTasks is a constant for an annotation on a Shoot which states that certain tasks should be done. + ShootTasks = "shoot.gardener.cloud/tasks" + // ShootTaskDeployInfrastructure is a name for a Shoot's infrastructure deployment task. It indicates that the + // Infrastructure extension resource shall be reconciled. + ShootTaskDeployInfrastructure = "deployInfrastructure" + // ShootTaskDeployDNSRecordInternal is a name for a Shoot's internal DNS record deployment task. It indicates that + // the internal DNSRecord extension resources shall be reconciled. + ShootTaskDeployDNSRecordInternal = "deployDNSRecordInternal" + // ShootTaskDeployDNSRecordExternal is a name for a Shoot's external DNS record deployment task. It indicates that + // the external DNSRecord extension resources shall be reconciled. + ShootTaskDeployDNSRecordExternal = "deployDNSRecordExternal" + // ShootTaskDeployDNSRecordIngress is a name for a Shoot's ingress DNS record deployment task. It indicates that + // the ingress DNSRecord extension resources shall be reconciled. + ShootTaskDeployDNSRecordIngress = "deployDNSRecordIngress" + // ShootTaskRestartControlPlanePods is a name for a Shoot task which is dedicated to restart related control plane pods. + ShootTaskRestartControlPlanePods = "restartControlPlanePods" + // ShootTaskRestartCoreAddons is a name for a Shoot task which is dedicated to restart some core addons. + ShootTaskRestartCoreAddons = "restartCoreAddons" + // ShootOperationMaintain is a constant for an annotation on a Shoot indicating that the Shoot maintenance shall be + // executed as soon as possible. + ShootOperationMaintain = "maintain" + // ShootOperationRetry is a constant for an annotation on a Shoot indicating that a failed Shoot reconciliation shall be + // retried. + ShootOperationRetry = "retry" + // OperationRotateCredentialsStart is a constant for an annotation indicating that the rotation of all credentials + // shall be started. This includes CAs, certificates, kubeconfigs, SSH keypairs, observability credentials, and + // ServiceAccount signing key. + OperationRotateCredentialsStart = "rotate-credentials-start" // #nosec G101 -- No credential. + // OperationRotateCredentialsStartWithoutWorkersRollout is a constant for an annotation indicating that the rotation + // of all credentials shall be started without rolling out the workers. This includes CAs, certificates, + // kubeconfigs, SSH keypairs, observability credentials, and ServiceAccount signing key. + OperationRotateCredentialsStartWithoutWorkersRollout = "rotate-credentials-start-without-workers-rollout" // #nosec G101 -- No credential. + // OperationRotateCredentialsComplete is a constant for an annotation indicating that the rotation of the + // credentials shall be completed. + OperationRotateCredentialsComplete = "rotate-credentials-complete" // #nosec G101 -- No credential. + // ShootOperationRotateKubeconfigCredentials is a constant for an annotation on a Shoot indicating that the + // credentialscontained in the kubeconfig that is handed out to the user shall be rotated. + ShootOperationRotateKubeconfigCredentials = "rotate-kubeconfig-credentials" // #nosec G101 -- No credential. + // ShootOperationRotateSSHKeypair is a constant for an annotation on a Shoot indicating that the SSH keypair for the + // shoot nodes shall be rotated. + ShootOperationRotateSSHKeypair = "rotate-ssh-keypair" + // OperationRotateCAStart is a constant for an annotation indicating that the rotation of the certificate + // authorities shall be started. + OperationRotateCAStart = "rotate-ca-start" + // OperationRotateCAStartWithoutWorkersRollout is a constant for an annotation indicating that the rotation of the + // certificate authorities shall be started without rolling out the workers. + OperationRotateCAStartWithoutWorkersRollout = "rotate-ca-start-without-workers-rollout" + // OperationRotateCAComplete is a constant for an annotation indicating that the rotation of the certificate + // authorities shall be completed. + OperationRotateCAComplete = "rotate-ca-complete" + // OperationRotateObservabilityCredentials is a constant for an annotation indicating that the + // credentials for the observability stack secret shall be rotated. Note that this only affects the user credentials + // since the operator credentials are rotated automatically each `30d`. + OperationRotateObservabilityCredentials = "rotate-observability-credentials" // #nosec G101 -- No credential. + // OperationRotateServiceAccountKeyStart is a constant for an annotation on a Shoot indicating that the + // rotation of the service account signing key shall be started. + OperationRotateServiceAccountKeyStart = "rotate-serviceaccount-key-start" + // OperationRotateServiceAccountKeyStartWithoutWorkersRollout is a constant for an annotation on a Shoot indicating that the + // rotation of the service account signing key shall be started without rolling out the workers. + OperationRotateServiceAccountKeyStartWithoutWorkersRollout = "rotate-serviceaccount-key-start-without-workers-rollout" + // OperationRotateServiceAccountKeyComplete is a constant for an annotation on a Shoot indicating that the + // rotation of the service account signing key shall be completed. + OperationRotateServiceAccountKeyComplete = "rotate-serviceaccount-key-complete" + // OperationRotateETCDEncryptionKeyStart is a constant for an annotation on a Shoot indicating that the + // rotation of the ETCD encryption key shall be started. + OperationRotateETCDEncryptionKeyStart = "rotate-etcd-encryption-key-start" + // OperationRotateETCDEncryptionKeyComplete is a constant for an annotation on a Shoot indicating that the + // rotation of the ETCD encryption key shall be completed. + OperationRotateETCDEncryptionKeyComplete = "rotate-etcd-encryption-key-complete" + // OperationRotateRolloutWorkers is a constant for an annotation triggering the rollout of one or more worker pools + // (comma-separated) when the certificate authorities or service account signing key credentials rotation is in + // WaitingForWorkersRollout phase. + OperationRotateRolloutWorkers = "rotate-rollout-workers" + // SeedOperationRenewGardenAccessSecrets is a constant for an annotation on a Seed indicating that + // all garden access secrets on the seed shall be renewed. + SeedOperationRenewGardenAccessSecrets = "renew-garden-access-secrets" // #nosec G101 -- No credential. + // SeedOperationRenewWorkloadIdentityTokens is a constant for an annotation on a Seed indicating that + // all workload identity tokens on the seed shall be renewed. + SeedOperationRenewWorkloadIdentityTokens = "renew-workload-identity-tokens" + // KubeconfigSecretOperationRenew is a constant for an annotation on the secret in a Seed containing the garden + // cluster kubeconfig of a gardenlet indicating that it should be renewed. + KubeconfigSecretOperationRenew = "renew" + + // ConfirmationDeletion is an annotation on a Shoot, Project, and ShootState resources whose value must be set to + // "true" in order to allow deleting the resource (if the annotation is not set any DELETE request will be denied). + ConfirmationDeletion = "confirmation.gardener.cloud/deletion" + // DeletionConfirmedBy is an annotation on a resource whose value is the subject which confirmed the deletion. + DeletionConfirmedBy = "deletion.gardener.cloud/confirmed-by" + + // SeedResourceManagerClass is the resource-class managed by the Gardener-Resource-Manager + // instance in the garden namespace on the seeds. + SeedResourceManagerClass = "seed" + // LabelBackupProvider is used to identify the backup provider. + LabelBackupProvider = "backup.gardener.cloud/provider" + // LabelSeedProvider is used to identify the seed provider. + LabelSeedProvider = "seed.gardener.cloud/provider" + // LabelShootProvider is used to identify the shoot provider. + LabelShootProvider = "shoot.gardener.cloud/provider" + // LabelShootProviderPrefix is used to prefix label that indicates the provider type. + // The label key is in the form provider.shoot.gardener.cloud/. + LabelShootProviderPrefix = "provider.shoot.gardener.cloud/" + // LabelNetworkingProvider is used to identify the networking provider for the cni plugin. + LabelNetworkingProvider = "networking.shoot.gardener.cloud/provider" + // LabelExtensionPrefix is used to prefix extension specific labels. + LabelExtensionPrefix = "extensions.gardener.cloud/" + // LabelLogging is a constant for a label for logging stack configurations + LabelLogging = "logging" + // LabelMonitoring is a constant for a label for monitoring stack configurations + LabelMonitoring = "monitoring" + // LabelPrefixMonitoringDashboard is the prefix of a label key on ConfigMaps for indicating that the data contains a + // dashboard. + LabelPrefixMonitoringDashboard = "dashboard.monitoring.gardener.cloud/" + // LabelKeyCustomLoggingResource is the key of the label which is used from the operator to select the CustomResources which will be imported in the FluentBit configuration. + // TODO(nickytd): the label key has to be migrated to "fluentbit.gardener.cloud/type". + LabelKeyCustomLoggingResource = "fluentbit.gardener/type" + // LabelValueCustomLoggingResource is the value of the label which is used from the operator to select the CustomResources which will be imported in the FluentBit configuration. + LabelValueCustomLoggingResource = "seed" + // LabelSeedNetwork is used to specify whether the seed is reachable from the garden cluster. + LabelSeedNetwork = "seed.gardener.cloud/network" + // LabelSeedNetworkPrivate is used to specify that the seed is in private networks and not reachable from the garden + // cluster. + LabelSeedNetworkPrivate = "private" + // LabelKeyAggregateToProjectMember is a constant for a label on ClusterRoles that are aggregated to the project + // member ClusterRole. + LabelKeyAggregateToProjectMember = "rbac.gardener.cloud/aggregate-to-project-member" + + // LabelSecretBindingReference is used to identify secrets which are referred by a SecretBinding (not necessarily in the same namespace). + LabelSecretBindingReference = "reference.gardener.cloud/secretbinding" + // LabelCredentialsBindingReference is used to identify credentials which are referred by a CredentialsBinding (not necessarily in the same namespace). + LabelCredentialsBindingReference = "reference.gardener.cloud/credentialsbinding" + // LabelPrefixSeedName is the prefix for the label key describing the name of a seed, e.g. seed.gardener.cloud/my-seed=true. + LabelPrefixSeedName = "seed.gardener.cloud/" + + // LabelExtensionExtensionTypePrefix is used to prefix extension label for extension types. + LabelExtensionExtensionTypePrefix = "extensions.extensions.gardener.cloud/" + // LabelExtensionProviderTypePrefix is used to prefix extension label for cloud provider types. + LabelExtensionProviderTypePrefix = "provider.extensions.gardener.cloud/" + // LabelExtensionDNSRecordTypePrefix is used to prefix extension label for DNS types. + LabelExtensionDNSRecordTypePrefix = "dnsrecord.extensions.gardener.cloud/" + // LabelExtensionNetworkingTypePrefix is used to prefix extension label for networking plugin types. + LabelExtensionNetworkingTypePrefix = "networking.extensions.gardener.cloud/" + // LabelExtensionOperatingSystemConfigTypePrefix is used to prefix extension label for OperatingSystemConfig types. + LabelExtensionOperatingSystemConfigTypePrefix = "operatingsystemconfig.extensions.gardener.cloud/" + // LabelExtensionContainerRuntimeTypePrefix is used to prefix extension label for ContainerRuntime types. + LabelExtensionContainerRuntimeTypePrefix = "containerruntime.extensions.gardener.cloud/" + + // LabelExtensionProviderMutatedByControlplaneWebhook is used to specify extension provider controlplane webhook targets + LabelExtensionProviderMutatedByControlplaneWebhook = LabelExtensionProviderTypePrefix + "mutated-by-controlplane-webhook" + + // LabelNetworkPolicyToBlockedCIDRs allows Egress from pods labeled with 'networking.gardener.cloud/to-blocked-cidrs=allowed'. + LabelNetworkPolicyToBlockedCIDRs = "networking.gardener.cloud/to-blocked-cidrs" + // LabelNetworkPolicyToDNS allows Egress from pods labeled with 'networking.gardener.cloud/to-dns=allowed' to DNS running in 'kube-system'. + // In practice, most of the Pods which require network Egress need this label. + LabelNetworkPolicyToDNS = "networking.gardener.cloud/to-dns" + // LabelNetworkPolicyToPrivateNetworks allows Egress from pods labeled with 'networking.gardener.cloud/to-private-networks=allowed' to the + // private networks (RFC1918), Carrier-grade NAT (RFC6598) except for cloudProvider's specific metadata service IP, seed networks, + // shoot networks. + LabelNetworkPolicyToPrivateNetworks = "networking.gardener.cloud/to-private-networks" + // LabelNetworkPolicyToPublicNetworks allows Egress from pods labeled with 'networking.gardener.cloud/to-public-networks=allowed' to all public + // network IPs, except for private networks (RFC1918), carrier-grade NAT (RFC6598), cloudProvider's specific metadata service IP. + // In practice, this blocks Egress traffic to all networks in the Seed cluster and only traffic to public IPv4 addresses. + LabelNetworkPolicyToPublicNetworks = "networking.gardener.cloud/to-public-networks" + // LabelNetworkPolicyToSeedAPIServer allows Egress from pods labeled with 'networking.gardener.cloud/to-seed-apiserver=allowed' to Seed's Kubernetes + // API Server. + // + // Deprecated: Use LabelNetworkPolicyToRuntimeAPIServer instead. + LabelNetworkPolicyToSeedAPIServer = "networking.gardener.cloud/to-seed-apiserver" + // LabelNetworkPolicyToRuntimeAPIServer allows Egress from pods labeled with 'networking.gardener.cloud/to-runtime-apiserver=allowed' to runtime Kubernetes + // API Server. + LabelNetworkPolicyToRuntimeAPIServer = "networking.gardener.cloud/to-runtime-apiserver" + // LabelNetworkPolicyFromPrometheus allows Ingress from Prometheus to pods labeled with 'networking.gardener.cloud/from-prometheus=allowed' and ports + // named 'metrics' in the PodSpecification. + // + // Deprecated: This label is deprecated and will be removed in a future version. Components in shoot namespaces + // which need to be scraped by Prometheus need to annotate their Services with + // `networking.resources.gardener.cloud/from-policy-pod-label-selector=all-scrape-targets` and + // `networking.resources.gardener.cloud/from-policy-allowed-ports=[{"protocol":,"port":}]`. + LabelNetworkPolicyFromPrometheus = "networking.gardener.cloud/from-prometheus" + // LabelNetworkPolicyShootFromSeed allows Ingress traffic from the seed cluster (where the shoot's kube-apiserver + // runs). + LabelNetworkPolicyShootFromSeed = "networking.gardener.cloud/from-seed" + // LabelNetworkPolicyShootToAPIServer allows Egress traffic to the shoot's API server. + LabelNetworkPolicyShootToAPIServer = "networking.gardener.cloud/to-apiserver" + // LabelNetworkPolicyShootToKubelet allows Egress traffic to the kubelets. + LabelNetworkPolicyShootToKubelet = "networking.gardener.cloud/to-kubelet" + // LabelNetworkPolicyAllowed is a constant for allowing a network policy. + LabelNetworkPolicyAllowed = "allowed" + // LabelNetworkPolicyScrapeTargets is a constant for pod selector label which can be used on Services for components + // which should be scraped by Prometheus. + // See https://github.com/gardener/gardener/blob/master/docs/concepts/resource-manager.md#overwriting-the-pod-selector-label. + LabelNetworkPolicyScrapeTargets = "all-scrape-targets" + // LabelNetworkPolicyGardenScrapeTargets is a constant for pod selector label which can be used on Services for + // garden system components or extensions which should be scraped by Prometheus. + // See https://github.com/gardener/gardener/blob/master/docs/concepts/resource-manager.md#overwriting-the-pod-selector-label. + LabelNetworkPolicyGardenScrapeTargets = "all-garden-scrape-targets" + // LabelNetworkPolicySeedScrapeTargets is a constant for pod selector label which can be used on Services for + // seed system components or extensions which should be scraped by Prometheus. + // See https://github.com/gardener/gardener/blob/master/docs/concepts/resource-manager.md#overwriting-the-pod-selector-label. + LabelNetworkPolicySeedScrapeTargets = "all-seed-scrape-targets" + // LabelNetworkPolicyWebhookTargets is a constant for pod selector label which can be used on Services for + // garden or shoot components which serve a webhook endpoint that must be reachable by the kube-apiserver. + // See https://github.com/gardener/gardener/blob/master/docs/concepts/resource-manager.md#overwriting-the-pod-selector-label. + LabelNetworkPolicyWebhookTargets = "all-webhook-targets" + // LabelNetworkPolicyShootNamespaceAlias is a constant for the alias for shoot namespaces used in NetworkPolicy + // labels. + LabelNetworkPolicyShootNamespaceAlias = "all-shoots" + // LabelNetworkPolicyExtensionsNamespaceAlias is a constant for the alias for extension namespaces used in + // NetworkPolicy labels. + LabelNetworkPolicyExtensionsNamespaceAlias = "extensions" + // LabelNetworkPolicyIstioIngressNamespaceAlias is a constant for the alias for shoot namespaces used in + // NetworkPolicy labels. + LabelNetworkPolicyIstioIngressNamespaceAlias = "all-istio-ingresses" + // LabelNetworkPolicyAccessTargetAPIServer is a constant for the alias for a namespace which runs components that + // need to initiate the communication with a target API server (e.g., shoot API server or virtual garden API + // server). + LabelNetworkPolicyAccessTargetAPIServer = "networking.gardener.cloud/access-target-apiserver" + + // LabelAuthorizationExtensionsServiceAccountSelector is a constant for an annotation key on ClusterRoles in the + // garden cluster which can be used to describe a selector for labels on ServiceAccounts which are allowed to get + // bound to this ClusterRole. + LabelAuthorizationExtensionsServiceAccountSelector = "authorization.gardener.cloud/extensions-serviceaccount-selector" + // LabelAuthorizationCustomExtensionsPermissions is a constant for a label key on ClusterRoles in the garden + // cluster which can be used to describe that this ClusterRole contains custom permissions for extensions. + LabelAuthorizationCustomExtensionsPermissions = "authorization.gardener.cloud/custom-extensions-permissions" + + // LabelObservabilityApplication is a constant for a label key set to all observability applications in gardener exposing a public endpoint. + LabelObservabilityApplication = "observability.gardener.cloud/app" + + // LabelApp is a constant for a label key. + LabelApp = "app" + // LabelRole is a constant for a label key. + LabelRole = "role" + // LabelKubernetes is a constant for a label for Kubernetes workload. + LabelKubernetes = "kubernetes" + // LabelGardener is a constant for a label for Gardener workload. + LabelGardener = "gardener" + // LabelAPIServer is a constant for a label for the kube-apiserver. + LabelAPIServer = "apiserver" + // LabelControllerManager is a constant for a label for the kube-controller-manager. + LabelControllerManager = "controller-manager" + // LabelScheduler is a constant for a label for the kube-scheduler. + LabelScheduler = "scheduler" + // LabelProxy is a constant for a label for the kube-proxy. + LabelProxy = "proxy" + // LabelExtensionProjectRole is a constant for a label value for extension project roles + LabelExtensionProjectRole = "extension-project-role" + + // LabelShootNamespace is a constant for a label key that indicates a relationship to a shoot in the specified namespace. + LabelShootNamespace = "shoot.gardener.cloud/namespace" + // LabelShootName is a constant for a label key that indicates a relationship to a shoot with the specified name. + LabelShootName = "shoot.gardener.cloud/name" + // LabelShootUID is a constant for a label key that indicates a relationship to a shoot with the specified UID. + LabelShootUID = "shoot.gardener.cloud/uid" + + // LabelPublicKeys is a constant for a label key that indicates that a resource contains public keys. + // Deprecated: Use LabelDiscoveryPublic instead. + LabelPublicKeys = "authentication.gardener.cloud/public-keys" // TODO(dimityrmirchev): Deprecate in favour of LabelDiscoveryPublic + // LabelPublicKeysServiceAccount is a constant for a label value that indicates that a resource contains service account public keys. + LabelPublicKeysServiceAccount = "serviceaccount" + + // LabelDiscoveryPublic is a constant for a label key that indicates that the labeled resource is of interest to the Gardener Discovery Server. + LabelDiscoveryPublic = "discovery.gardener.cloud/public" + // DiscoveryShootCA is a constant for a label value that indicates that the labeled resource contains shoot cluster certificate authority. + DiscoveryShootCA = "shoot-ca" + + // LabelExposureClassHandlerName is the label key for exposure class handler names. + LabelExposureClassHandlerName = "handler.exposureclass.gardener.cloud/name" + + // LabelNodeLocalDNS is a constant for a label key, which the provider extensions set on the nodes. + // The value can be true or false. + LabelNodeLocalDNS = "networking.gardener.cloud/node-local-dns-enabled" + + // LabelVPAEvictionRequirementsController is a constant for a label indicating that a VPA resource is under control + // of the VPAEvictionRequirementsController. + LabelVPAEvictionRequirementsController = "autoscaling.gardener.cloud/eviction-requirements" + // EvictionRequirementManagedByController is a constant to be used as a value for the label LabelVPAEvictionRequirementsController + // to indicate that the vpa-eviction-requirements-controller manages all EvictionRequirements on a VPA object. + EvictionRequirementManagedByController = "managed-by-controller" + + // AnnotationVPAEvictionRequirementDownscaleRestriction is a constant for an annotation key on a VPA object indicating that + // the VPAEvictionRequirementsController should add an EvictionRestriction that prevents downscaling. + // Possible values are "in-maintenance-window-only" and "never", available as constants below. + AnnotationVPAEvictionRequirementDownscaleRestriction = "eviction-requirements.autoscaling.gardener.cloud/downscale-restriction" + // EvictionRequirementInMaintenanceWindowOnly is a constant to be used as a value for the annotation AnnotationVPAEvictionRequirementDownscaleRestriction, + // indicating that downscaling should be restricted to the Shoot's maintenance window. + EvictionRequirementInMaintenanceWindowOnly = "in-maintenance-window-only" + // EvictionRequirementNever is a constant to be used as a value for the annotation AnnotationVPAEvictionRequirementDownscaleRestriction, + // indicating that downscaling should never be allowed. + EvictionRequirementNever = "never" + // AnnotationShootMaintenanceWindow is a constant for an annotation key used on VPA objects to hold the Shoot's maintenance window start and end. + AnnotationShootMaintenanceWindow = "shoot.gardener.cloud/maintenance-window" + + // GardenNamespace is the namespace in which the configuration and secrets for + // the Gardener controller manager will be stored (e.g., secrets for the Seed clusters). + // It is also used by the gardener-apiserver. + GardenNamespace = "garden" + // IstioSystemNamespace is the istio-system namespace. + IstioSystemNamespace = "istio-system" + // KubernetesDashboardNamespace is the kubernetes-dashboard namespace. + KubernetesDashboardNamespace = "kubernetes-dashboard" + + // DefaultSNIIngressNamespace is the default sni ingress namespace. + DefaultSNIIngressNamespace = "istio-ingress" + // DefaultSNIIngressServiceName is the default sni ingress service name. + DefaultSNIIngressServiceName = "istio-ingressgateway" + // DefaultIngressGatewayAppLabelValue is the ingress gateway value for the app label. + DefaultIngressGatewayAppLabelValue = "istio-ingressgateway" + + // DataTypeSecret is a constant for a value of the 'Type' field in 'GardenerResourceData' structs describing that + // the data is a secret. + DataTypeSecret = "secret" + // DataTypeMachineState is a constant for a value of the 'Type' field in 'GardenerResourceData' structs describing + // that the data is machine state. + DataTypeMachineState = "machine-state" + + // DefaultSchedulerName is the name of the default scheduler. + DefaultSchedulerName = "default-scheduler" + // SchedulingPurpose is a constant for the key in a label describing the purpose of the scheduler related object. + SchedulingPurpose = "scheduling.gardener.cloud/purpose" + // SchedulingPurposeRegionConfig is a constant for a label value indicating that the object should be considered as a region config. + SchedulingPurposeRegionConfig = "region-config" + // AnnotationSchedulingCloudProfiles is a constant for an annotation key on a configmap which denotes + // the linked cloudprofiles containing the region distances. + AnnotationSchedulingCloudProfiles = "scheduling.gardener.cloud/cloudprofiles" + + // AnnotationConfirmationForceDeletion is a constant for an annotation on a Shoot resource whose value must be set to "true" in order to + // trigger force-deletion of the cluster. It can only be set if the Shoot has a deletion timestamp and contains an ErrorCode in the Shoot Status. + AnnotationConfirmationForceDeletion = "confirmation.gardener.cloud/force-deletion" + // AnnotationManagedSeedAPIServer is a constant for an annotation on a Shoot resource containing the API server settings for a managed seed. + AnnotationManagedSeedAPIServer = "shoot.gardener.cloud/managed-seed-api-server" + // AnnotationShootIgnoreAlerts is the key for an annotation of a Shoot cluster whose value indicates + // if alerts for this cluster should be ignored + AnnotationShootIgnoreAlerts = "shoot.gardener.cloud/ignore-alerts" + // AnnotationShootSkipCleanup is a key for an annotation on a Shoot resource that declares that the clean up steps should be skipped when the + // cluster is deleted. Concretely, this will skip everything except the deletion of (load balancer) services and persistent volume resources. + AnnotationShootSkipCleanup = "shoot.gardener.cloud/skip-cleanup" + // AnnotationShootSkipReadiness is a key for an annotation on a Shoot resource that instructs the shoot flow to skip readiness steps during reconciliation. + AnnotationShootSkipReadiness = "shoot.gardener.cloud/skip-readiness" + // AnnotationShootCleanupWebhooksFinalizeGracePeriodSeconds is a key for an annotation on a Shoot resource that + // declares the grace period in seconds for finalizing the resources handled in the 'cleanup webhooks' step. + // Concretely, after the specified seconds, all the finalizers of the affected resources are forcefully removed. + AnnotationShootCleanupWebhooksFinalizeGracePeriodSeconds = "shoot.gardener.cloud/cleanup-webhooks-finalize-grace-period-seconds" + // AnnotationShootCleanupExtendedAPIsFinalizeGracePeriodSeconds is a key for an annotation on a Shoot resource that + // declares the grace period in seconds for finalizing the resources handled in the 'cleanup extended APIs' step. + // Concretely, after the specified seconds, all the finalizers of the affected resources are forcefully removed. + AnnotationShootCleanupExtendedAPIsFinalizeGracePeriodSeconds = "shoot.gardener.cloud/cleanup-extended-apis-finalize-grace-period-seconds" + // AnnotationShootCleanupKubernetesResourcesFinalizeGracePeriodSeconds is a key for an annotation on a Shoot + // resource that declares the grace period in seconds for finalizing the resources handled in the 'cleanup + // Kubernetes resources' step. Concretely, after the specified seconds, all the finalizers of the affected resources + // are forcefully removed. + AnnotationShootCleanupKubernetesResourcesFinalizeGracePeriodSeconds = "shoot.gardener.cloud/cleanup-kubernetes-resources-finalize-grace-period-seconds" + // AnnotationShootCloudConfigExecutionMaxDelaySeconds is a key for an annotation on a Shoot resource that declares + // the maximum delay in seconds when potentially updated cloud-config user data is executed on the worker nodes. + // Concretely, the gardener-node-agent systemd service running on all worker nodes will wait + // for a random duration based on the configured value before executing the user data (default value is 300) plus an + // additional offset of 30s. If set to 0 then no random delay will be applied and the minimum delay (30s) applies. + // Any value above 1800 is ignored (in this case the default value is used). + // Note that changing this value only applies to new nodes. Existing nodes which already computed their individual + // delays will not recompute it. + AnnotationShootCloudConfigExecutionMaxDelaySeconds = "shoot.gardener.cloud/cloud-config-execution-max-delay-seconds" + + // AnnotationAuthenticationIssuer is the key for an annotation applied to a Shoot which specifies + // if the shoot's issuer is managed by Gardener. + AnnotationAuthenticationIssuer = "authentication.gardener.cloud/issuer" + // AnnotationAuthenticationIssuerManaged is the value for [AnnotationAuthenticationIssuer] annotation that indicates that + // a shoot's issuer should be managed by Gardener. + AnnotationAuthenticationIssuerManaged = "managed" + + // AnnotationPodSecurityEnforce is a constant for an annotation on `ControllerRegistration`s and `ControllerInstallation`s. When set the + // `extension` namespace is created with "pod-security.kubernetes.io/enforce" label set to AnnotationPodSecurityEnforce's value. + AnnotationPodSecurityEnforce = "security.gardener.cloud/pod-security-enforce" + // OperatingSystemConfigUnitNameKubeletService is a constant for a unit in the operating system config that contains the kubelet service. + OperatingSystemConfigUnitNameKubeletService = "kubelet.service" + // OperatingSystemConfigUnitNameContainerDService is a constant for a unit in the operating system config that contains the containerd service. + OperatingSystemConfigUnitNameContainerDService = "containerd.service" + // OperatingSystemConfigFilePathKernelSettings is a constant for a path to a file in the operating system config that contains some general kernel settings. + OperatingSystemConfigFilePathKernelSettings = "/etc/sysctl.d/99-k8s-general.conf" + // OperatingSystemConfigFilePathKubeletConfig is a constant for a path to a file in the operating system config that contains the kubelet configuration. + OperatingSystemConfigFilePathKubeletConfig = "/var/lib/kubelet/config/kubelet" + // OperatingSystemConfigUnitNameValitailService is a constant for a unit in the operating system config that contains the valitail service. + OperatingSystemConfigUnitNameValitailService = "valitail.service" + // OperatingSystemConfigFilePathValitailConfig is a constant for a path to a file in the operating system config that contains the kubelet configuration. + OperatingSystemConfigFilePathValitailConfig = "/var/lib/valitail/config/config" + // OperatingSystemConfigFilePathBinaries is a constant for a path to a directory in the operating system config that contains the binaries. + OperatingSystemConfigFilePathBinaries = "/opt/bin" + + // FluentBitConfigMapKubernetesFilter is a constant for the Fluent Bit ConfigMap's section regarding Kubernetes filters + FluentBitConfigMapKubernetesFilter = "filter-kubernetes.conf" + // FluentBitConfigMapParser is a constant for the Fluent Bit ConfigMap's section regarding Parsers for common container types + FluentBitConfigMapParser = "parsers.conf" + + // LabelControllerRegistrationName is the key of a label on extension namespaces that indicates the controller registration name. + LabelControllerRegistrationName = "controllerregistration.core.gardener.cloud/name" + // LabelPodMaintenanceRestart is a constant for a label that describes that a pod should be restarted during maintenance. + LabelPodMaintenanceRestart = "maintenance.gardener.cloud/restart" + // LabelCareConditionType is a key for a label on a ManagedResource indicating to which condition type its status + // should be aggregated. + LabelCareConditionType = "care.gardener.cloud/condition-type" + // ObservabilityComponentsHealthy is a constant for a condition type indicating the health of observability components. + ObservabilityComponentsHealthy = "ObservabilityComponentsHealthy" + + // LabelWorkerPool is a constant for a label that indicates the worker pool the node belongs to + LabelWorkerPool = "worker.gardener.cloud/pool" + // LabelWorkerKubernetesVersion is a constant for a label that indicates the Kubernetes version used for the worker pool nodes. + LabelWorkerKubernetesVersion = "worker.gardener.cloud/kubernetes-version" + // LabelWorkerPoolDeprecated is a deprecated constant for a label that indicates the worker pool the node belongs to + LabelWorkerPoolDeprecated = "worker.garden.sapcloud.io/group" + // LabelWorkerPoolSystemComponents is a constant that indicates whether the worker pool should host system components + LabelWorkerPoolSystemComponents = "worker.gardener.cloud/system-components" + // LabelWorkerPoolGardenerNodeAgentSecretName is the name of the secret used by the gardener node agent + LabelWorkerPoolGardenerNodeAgentSecretName = "worker.gardener.cloud/gardener-node-agent-secret-name" + + // LabelUpdateRestriction is a constant for a label key that indicates + // that a resource must be only updated by the gardenlet. + LabelUpdateRestriction = "gardener.cloud/update-restriction" + + // EventResourceReferenced indicates that the resource deletion is in waiting mode because the resource is still + // being referenced by at least one other resource (e.g. a SecretBinding is still referenced by a Shoot) + EventResourceReferenced = "ResourceReferenced" + + // ReferencedResourcesPrefix is the prefix used when copying referenced resources to the Shoot namespace in the Seed, + // to avoid naming collisions with resources managed by Gardener. + ReferencedResourcesPrefix = "ref-" + + // ClusterIdentity is a constant equal to the name and data key (that stores the identity) of the cluster-identity ConfigMap + ClusterIdentity = "cluster-identity" + // ClusterIdentityOrigin is a constant equal to the data key that stores the identity origin of the cluster-identity ConfigMap + ClusterIdentityOrigin = "origin" + // ClusterIdentityOriginGardenerAPIServer defines a cluster-identity ConfigMap originated from gardener-apiserver + ClusterIdentityOriginGardenerAPIServer = "gardener-apiserver" + // ClusterIdentityOriginSeed defines a cluster-identity ConfigMap originated from seed + ClusterIdentityOriginSeed = "seed" + // ClusterIdentityOriginShoot defines a cluster-identity ConfigMap originated from shoot + ClusterIdentityOriginShoot = "shoot" + + // SeedNginxIngressClass defines the ingress class for the seed nginx ingress controller + SeedNginxIngressClass = "nginx-ingress-gardener" + // ShootNginxIngressClass defines the ingress class for the shoot nginx ingress controller addon. + ShootNginxIngressClass = "nginx" + // IngressKindNginx defines nginx as kind as managed Seed ingress + IngressKindNginx = "nginx" + + // SeedsGroup is the identity group for gardenlets when authenticating to the API server. + SeedsGroup = "gardener.cloud:system:seeds" + // SeedUserNamePrefix is the identity user name prefix for gardenlets when authenticating to the API server. + SeedUserNamePrefix = "gardener.cloud:system:seed:" + + // ShootGroupViewers is a constant for a group name in shoot clusters whose users get read-only privileges (except + // for core/v1.Secrets). + ShootGroupViewers = "gardener.cloud:system:viewers" + // ClusterRoleNameGardenerAdministrators is the name of a cluster role in the garden cluster defining privileges + // for administrators. + ClusterRoleNameGardenerAdministrators = "gardener.cloud:system:administrators" + + // ProjectName is the key of a label on namespaces whose value holds the project name. + ProjectName = "project.gardener.cloud/name" + // ProjectSkipStaleCheck is the key of an annotation on a project namespace that marks the associated Project to be + // skipped by the stale project controller. If the project has already configured stale timestamps in its status + // then they will be reset. + ProjectSkipStaleCheck = "project.gardener.cloud/skip-stale-check" + // NamespaceProject is the key of an annotation on namespace whose value holds the project uid. + NamespaceProject = "namespace.gardener.cloud/project" + // NamespaceKeepAfterProjectDeletion is a constant for an annotation on a `Namespace` resource that states that it + // should not be deleted if the corresponding `Project` gets deleted. Please note that all project related labels + // from the namespace will be removed when the project is being deleted. + NamespaceKeepAfterProjectDeletion = "namespace.gardener.cloud/keep-after-project-deletion" + // NamespaceCreatedByProjectController is a constant for annotation on a `Namespace` resource that states that it + // was created by the project controller because either the Project's `spec.namespace` field was not specified + // or the specified namespace was not present. + NamespaceCreatedByProjectController = "namespace.gardener.cloud/created-by-project-controller" + + // DefaultVPNRange is the default IPv4 network range for the VPN between seed and shoot cluster. + DefaultVPNRange = "192.168.123.0/24" + // DefaultVPNRangeV6 is the default IPv6 network range for the VPN between seed and shoot cluster. + DefaultVPNRangeV6 = "fd8f:6d53:b97a:1::/96" + // ReservedKubeApiServerMappingRange is the IPv4 network range for the "kubernetes" service used by apiserver-proxy + ReservedKubeApiServerMappingRange = "240.0.0.0/8" + + // BackupSecretName is the name of secret having credentials for etcd backups. + BackupSecretName string = "etcd-backup" + // DataKeyBackupBucketName is the name of a data key whose value contains the backup bucket name. + DataKeyBackupBucketName string = "bucketName" + // BackupSourcePrefix is the prefix for names of resources related to source backupentries when copying backups. + BackupSourcePrefix = "source" + + // GardenerAudience is the identifier for Gardener controllers when interacting with the API Server + GardenerAudience = "gardener" + + // DNSRecordInternalName is a constant for DNSRecord objects used for the internal domain name. + DNSRecordInternalName = "internal" + // DNSRecordExternalName is a constant for DNSRecord objects used for the external domain name. + DNSRecordExternalName = "external" + + // ArchitectureAMD64 is a constant for the 'amd64' architecture. + ArchitectureAMD64 = "amd64" + // ArchitectureARM64 is a constant for the 'arm64' architecture. + ArchitectureARM64 = "arm64" + + // EnvGenericGardenKubeconfig is a constant for the environment variable which holds the path to the generic garden kubeconfig. + EnvGenericGardenKubeconfig = "GARDEN_KUBECONFIG" + // EnvSeedName is a constant for the environment variable which holds the name of the Seed that the extension + // controller is running on. + EnvSeedName = "SEED_NAME" + + // IngressTLSCertificateValidity is the default validity for ingress TLS certificates. + IngressTLSCertificateValidity = 730 * 24 * time.Hour // ~2 years, see https://support.apple.com/en-us/HT210176 + // IngressDomainPrefixPrometheusAggregate is the prefix of a domain exposing prometheus-aggregate in seed clusters. + IngressDomainPrefixPrometheusAggregate = "p-seed" + + // VPNTunnel dictates that VPN is used as a tunnel between seed and shoot networks. + VPNTunnel string = "vpn-shoot" + + // AdvertisedAddressExternal is a constant that represents the name of the external kube-apiserver address. + AdvertisedAddressExternal = "external" + // AdvertisedAddressInternal is a constant that represents the name of the internal kube-apiserver address. + AdvertisedAddressInternal = "internal" + // AdvertisedAddressUnmanaged is a constant that represents the name of the unmanaged kube-apiserver address. + AdvertisedAddressUnmanaged = "unmanaged" + // AdvertisedAddressServiceAccountIssuer is a constant that represents the name of the address + // that is used as a service account issuer for the kube-apiserver. + AdvertisedAddressServiceAccountIssuer = "service-account-issuer" + + // CloudProfileReferenceKindCloudProfile is a constant for the CloudProfile kind reference. + CloudProfileReferenceKindCloudProfile = "CloudProfile" + // CloudProfileReferenceKindNamespacedCloudProfile is a constant for the NamespacedCloudProfile kind reference. + CloudProfileReferenceKindNamespacedCloudProfile = "NamespacedCloudProfile" +) + +var ( + // ControlPlaneSecretRoles contains all role values used for control plane secrets synced to the Garden cluster. + ControlPlaneSecretRoles = []string{ + GardenRoleKubeconfig, + GardenRoleSSHKeyPair, + GardenRoleMonitoring, + } + + // ValidArchitectures contains all CPU architectures which are supported by the Shoot. + ValidArchitectures = []string{ + ArchitectureAMD64, + ArchitectureARM64, + } +) + +// constants for well-known PriorityClass names +const ( + // PriorityClassNameGardenSystemCritical is the name of a PriorityClass for Garden system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameGardenSystemCritical = "gardener-garden-system-critical" + // PriorityClassNameGardenSystem500 is the name of a PriorityClass for Garden system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameGardenSystem500 = "gardener-garden-system-500" + // PriorityClassNameGardenSystem400 is the name of a PriorityClass for Garden system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameGardenSystem400 = "gardener-garden-system-400" + // PriorityClassNameGardenSystem300 is the name of a PriorityClass for Garden system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameGardenSystem300 = "gardener-garden-system-300" + // PriorityClassNameGardenSystem200 is the name of a PriorityClass for Garden system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameGardenSystem200 = "gardener-garden-system-200" + // PriorityClassNameGardenSystem100 is the name of a PriorityClass for Garden system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameGardenSystem100 = "gardener-garden-system-100" + + // PriorityClassNameShootSystem900 is the name of a PriorityClass for Shoot system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootSystem900 = "gardener-shoot-system-900" + // PriorityClassNameShootSystem800 is the name of a PriorityClass for Shoot system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootSystem800 = "gardener-shoot-system-800" + // PriorityClassNameShootSystem700 is the name of a PriorityClass for Shoot system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootSystem700 = "gardener-shoot-system-700" + // PriorityClassNameShootSystem600 is the name of a PriorityClass for Shoot system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootSystem600 = "gardener-shoot-system-600" + + // PriorityClassNameSeedSystemCritical is the name of a PriorityClass for Seed system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameSeedSystemCritical = "gardener-system-critical" + // PriorityClassNameSeedSystem900 is the name of a PriorityClass for Seed system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameSeedSystem900 = "gardener-system-900" + // PriorityClassNameSeedSystem800 is the name of a PriorityClass for Seed system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameSeedSystem800 = "gardener-system-800" + // PriorityClassNameSeedSystem700 is the name of a PriorityClass for Seed system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameSeedSystem700 = "gardener-system-700" + // PriorityClassNameSeedSystem600 is the name of a PriorityClass for Seed system components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameSeedSystem600 = "gardener-system-600" + // PriorityClassNameReserveExcessCapacity is the name of a PriorityClass for reserving excess capacity on a Seed cluster. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameReserveExcessCapacity = "gardener-reserve-excess-capacity" + + // PriorityClassNameShootControlPlane500 is the name of a PriorityClass for Shoot control plane components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootControlPlane500 = "gardener-system-500" + // PriorityClassNameShootControlPlane400 is the name of a PriorityClass for Shoot control plane components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootControlPlane400 = "gardener-system-400" + // PriorityClassNameShootControlPlane300 is the name of a PriorityClass for Shoot control plane components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootControlPlane300 = "gardener-system-300" + // PriorityClassNameShootControlPlane200 is the name of a PriorityClass for Shoot control plane components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootControlPlane200 = "gardener-system-200" + // PriorityClassNameShootControlPlane100 is the name of a PriorityClass for Shoot control plane components. + // Please consider the documentation in https://github.com/gardener/gardener/blob/master/docs/development/priority-classes.md + PriorityClassNameShootControlPlane100 = "gardener-system-100" + + // TechnicalIDPrefix is a prefix used for a shoot's technical id. For historic reasons, there is only one 'dash' + // while nowadays we always use two dashes after "shoot". + TechnicalIDPrefix = "shoot-" + + // TaintNodeCriticalComponentsNotReady is the key for the gardener-managed node components taint. + TaintNodeCriticalComponentsNotReady = "node.gardener.cloud/critical-components-not-ready" + // LabelNodeCriticalComponent is the label key for marking node-critical component pods. + LabelNodeCriticalComponent = "node.gardener.cloud/critical-component" + // AnnotationPrefixWaitForCSINode is the annotation key for csi-driver-node pods, indicating they use the driver + // specified in the value. + AnnotationPrefixWaitForCSINode = "node.gardener.cloud/wait-for-csi-node-" + // AnnotationNodeAgentReconciliationDelay is the annotation key for specifying how long the gardener-node-agent + // should wait with reconciliation of the operating system config (to prevent too many node-agents from restarting + // kubelet or other critical units at the same time). + AnnotationNodeAgentReconciliationDelay = "node-agent.gardener.cloud/reconciliation-delay" + // NodeAgentsGroup is the identity group for gardener-node-agents when authenticating to the API server. + NodeAgentsGroup = "gardener.cloud:node-agents" + // NodeAgentUserNamePrefix is the identity username prefix for gardener-node-agent when authenticating to the API server. + NodeAgentUserNamePrefix = "gardener.cloud:node-agent:machine:" + + // GardenPurposeMachineClass is a constant for the 'machineclass' value in a label. + GardenPurposeMachineClass = "machineclass" +) diff --git a/api/external/gardener/pkg/apis/core/v1beta1/register.go b/api/external/gardener/pkg/apis/core/v1beta1/register.go new file mode 100644 index 0000000..02b5ea4 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/register.go @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +// GroupName is the name of the core API group. +const GroupName = "core.gardener.cloud" + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1beta1"} + +// Kind takes an unqualified kind and returns a Group qualified GroupKind. +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource. +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + // SchemeBuilder is a new Scheme Builder which registers our API. + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + localSchemeBuilder = &SchemeBuilder + // AddToScheme is a reference to the Scheme Builder's AddToScheme function. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to the given scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &BackupBucket{}, + &BackupBucketList{}, + &BackupEntry{}, + &BackupEntryList{}, + &CloudProfile{}, + &CloudProfileList{}, + &ControllerRegistration{}, + &ControllerRegistrationList{}, + &ControllerDeployment{}, + &ControllerDeploymentList{}, + &ControllerInstallation{}, + &ControllerInstallationList{}, + &ExposureClass{}, + &ExposureClassList{}, + &InternalSecret{}, + &InternalSecretList{}, + &NamespacedCloudProfile{}, + &NamespacedCloudProfileList{}, + &Project{}, + &ProjectList{}, + &Quota{}, + &QuotaList{}, + &SecretBinding{}, + &SecretBindingList{}, + &Seed{}, + &SeedList{}, + &Shoot{}, + &ShootList{}, + &ShootState{}, + &ShootStateList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + + return nil +} + +func addDefaultingFuncs(scheme *runtime.Scheme) error { + return nil +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types.go b/api/external/gardener/pkg/apis/core/v1beta1/types.go new file mode 100644 index 0000000..441b83d --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types.go @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +const ( + // GardenerSeedLeaseNamespace is the namespace in which Gardenlet will report Seeds' + // status using Lease resources for each Seed + GardenerSeedLeaseNamespace = "gardener-system-seed-lease" + // GardenerShootIssuerNamespace is the namespace in which Gardenlet + // will sync service account issuer discovery documents + // of Shoot clusters which require managed issuer + GardenerShootIssuerNamespace = "gardener-system-shoot-issuer" +) + +// IPFamily is a type for specifying an IP protocol version to use in Gardener clusters. +type IPFamily string + +const ( + // IPFamilyIPv4 is the IPv4 IP family. + IPFamilyIPv4 IPFamily = "IPv4" + // IPFamilyIPv6 is the IPv6 IP family. + IPFamilyIPv6 IPFamily = "IPv6" +) + +// IsIPv4SingleStack determines whether the given list of IP families specifies IPv4 single-stack networking. +func IsIPv4SingleStack(ipFamilies []IPFamily) bool { + return len(ipFamilies) == 0 || (len(ipFamilies) == 1 && ipFamilies[0] == IPFamilyIPv4) +} + +// IsIPv6SingleStack determines whether the given list of IP families specifies IPv6 single-stack networking. +func IsIPv6SingleStack(ipFamilies []IPFamily) bool { + return len(ipFamilies) == 1 && ipFamilies[0] == IPFamilyIPv6 +} + +// AccessRestriction describes an access restriction for a Kubernetes cluster (e.g., EU access-only). +type AccessRestriction struct { + // Name is the name of the restriction. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` +} + +// AccessRestrictionWithOptions describes an access restriction for a Kubernetes cluster (e.g., EU access-only) and +// allows to specify additional options. +type AccessRestrictionWithOptions struct { + AccessRestriction `json:",inline" protobuf:"bytes,1,opt,name=accessRestriction"` + // Options is a map of additional options for the access restriction. + // +optional + Options map[string]string `json:"options,omitempty" protobuf:"bytes,2,rep,name=options"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_backupbucket.go b/api/external/gardener/pkg/apis/core/v1beta1/types_backupbucket.go new file mode 100644 index 0000000..e78d02c --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_backupbucket.go @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BackupBucket holds details about backup bucket +type BackupBucket struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the Backup Bucket. + Spec BackupBucketSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"` + // Most recently observed status of the Backup Bucket. + Status BackupBucketStatus `json:"status" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BackupBucketList is a list of BackupBucket objects. +type BackupBucketList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of BackupBucket. + Items []BackupBucket `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// BackupBucketSpec is the specification of a Backup Bucket. +type BackupBucketSpec struct { + // Provider holds the details of cloud provider of the object store. This field is immutable. + Provider BackupBucketProvider `json:"provider" protobuf:"bytes,1,opt,name=provider"` + // ProviderConfig is the configuration passed to BackupBucket resource. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,2,opt,name=providerConfig"` + // SecretRef is a reference to a secret that contains the credentials to access object store. + SecretRef corev1.SecretReference `json:"secretRef" protobuf:"bytes,3,opt,name=secretRef"` + // SeedName holds the name of the seed allocated to BackupBucket for running controller. + // This field is immutable. + // +optional + SeedName *string `json:"seedName,omitempty" protobuf:"bytes,4,opt,name=seedName"` +} + +// BackupBucketStatus holds the most recently observed status of the Backup Bucket. +type BackupBucketStatus struct { + // ProviderStatus is the configuration passed to BackupBucket resource. + // +optional + ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty" protobuf:"bytes,1,opt,name=providerStatus"` + // LastOperation holds information about the last operation on the BackupBucket. + // +optional + LastOperation *LastOperation `json:"lastOperation,omitempty" protobuf:"bytes,2,opt,name=lastOperation"` + // LastError holds information about the last occurred error during an operation. + // +optional + LastError *LastError `json:"lastError,omitempty" protobuf:"bytes,3,opt,name=lastError"` + // ObservedGeneration is the most recent generation observed for this BackupBucket. It corresponds to the + // BackupBucket's generation, which is updated on mutation by the API Server. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,4,opt,name=observedGeneration"` + // GeneratedSecretRef is reference to the secret generated by backup bucket, which + // will have object store specific credentials. + // +optional + GeneratedSecretRef *corev1.SecretReference `json:"generatedSecretRef,omitempty" protobuf:"bytes,5,opt,name=generatedSecretRef"` +} + +// BackupBucketProvider holds the details of cloud provider of the object store. +type BackupBucketProvider struct { + // Type is the type of provider. + Type string `json:"type" protobuf:"bytes,1,opt,name=type"` + // Region is the region of the bucket. + Region string `json:"region" protobuf:"bytes,2,opt,name=region"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_backupentry.go b/api/external/gardener/pkg/apis/core/v1beta1/types_backupentry.go new file mode 100644 index 0000000..c8bd74c --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_backupentry.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // BackupEntryForceDeletion is a constant for an annotation on a BackupEntry indicating that it should be force deleted. + BackupEntryForceDeletion = "backupentry.core.gardener.cloud/force-deletion" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BackupEntry holds details about shoot backup. +type BackupEntry struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata" protobuf:"bytes,1,opt,name=metadata"` + // Spec contains the specification of the Backup Entry. + // +optional + Spec BackupEntrySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + // Status contains the most recently observed status of the Backup Entry. + // +optional + Status BackupEntryStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BackupEntryList is a list of BackupEntry objects. +type BackupEntryList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of BackupEntry. + Items []BackupEntry `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// BackupEntrySpec is the specification of a Backup Entry. +type BackupEntrySpec struct { + // BucketName is the name of backup bucket for this Backup Entry. + BucketName string `json:"bucketName" protobuf:"bytes,1,opt,name=bucketName"` + // SeedName holds the name of the seed to which this BackupEntry is scheduled + // +optional + SeedName *string `json:"seedName,omitempty" protobuf:"bytes,2,opt,name=seedName"` +} + +// BackupEntryStatus holds the most recently observed status of the Backup Entry. +type BackupEntryStatus struct { + // LastOperation holds information about the last operation on the BackupEntry. + // +optional + LastOperation *LastOperation `json:"lastOperation,omitempty" protobuf:"bytes,1,opt,name=lastOperation"` + // LastError holds information about the last occurred error during an operation. + // +optional + LastError *LastError `json:"lastError,omitempty" protobuf:"bytes,2,opt,name=lastError"` + // ObservedGeneration is the most recent generation observed for this BackupEntry. It corresponds to the + // BackupEntry's generation, which is updated on mutation by the API Server. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,3,opt,name=observedGeneration"` + // SeedName is the name of the seed to which this BackupEntry is currently scheduled. This field is populated + // at the beginning of a create/reconcile operation. It is used when moving the BackupEntry between seeds. + // +optional + SeedName *string `json:"seedName,omitempty" protobuf:"bytes,4,opt,name=seedName"` + // MigrationStartTime is the time when a migration to a different seed was initiated. + // +optional + MigrationStartTime *metav1.Time `json:"migrationStartTime,omitempty" protobuf:"bytes,5,opt,name=migrationStartTime"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_cloudprofile.go b/api/external/gardener/pkg/apis/core/v1beta1/types_cloudprofile.go new file mode 100644 index 0000000..90858fb --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_cloudprofile.go @@ -0,0 +1,288 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CloudProfile represents certain properties about a provider environment. +type CloudProfile struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec defines the provider environment properties. + // +optional + Spec CloudProfileSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// CloudProfileList is a collection of CloudProfiles. +type CloudProfileList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of CloudProfiles. + Items []CloudProfile `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// CloudProfileSpec is the specification of a CloudProfile. +// It must contain exactly one of its defined keys. +type CloudProfileSpec struct { + // CABundle is a certificate bundle which will be installed onto every host machine of shoot cluster targeting this profile. + // +optional + CABundle *string `json:"caBundle,omitempty" protobuf:"bytes,1,opt,name=caBundle"` + // Kubernetes contains constraints regarding allowed values of the 'kubernetes' block in the Shoot specification. + Kubernetes KubernetesSettings `json:"kubernetes" protobuf:"bytes,2,opt,name=kubernetes"` + // MachineImages contains constraints regarding allowed values for machine images in the Shoot specification. + // +patchMergeKey=name + // +patchStrategy=merge + MachineImages []MachineImage `json:"machineImages" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,3,rep,name=machineImages"` + // MachineTypes contains constraints regarding allowed values for machine types in the 'workers' block in the Shoot specification. + // +patchMergeKey=name + // +patchStrategy=merge + MachineTypes []MachineType `json:"machineTypes" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,4,rep,name=machineTypes"` + // ProviderConfig contains provider-specific configuration for the profile. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,5,opt,name=providerConfig"` + // Regions contains constraints regarding allowed values for regions and zones. + // +patchMergeKey=name + // +patchStrategy=merge + Regions []Region `json:"regions" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,6,rep,name=regions"` + // SeedSelector contains an optional list of labels on `Seed` resources that marks those seeds whose shoots may use this provider profile. + // An empty list means that all seeds of the same provider type are supported. + // This is useful for environments that are of the same type (like openstack) but may have different "instances"/landscapes. + // Optionally a list of possible providers can be added to enable cross-provider scheduling. By default, the provider + // type of the seed must match the shoot's provider. + // +optional + SeedSelector *SeedSelector `json:"seedSelector,omitempty" protobuf:"bytes,7,opt,name=seedSelector"` + // Type is the name of the provider. + Type string `json:"type" protobuf:"bytes,8,opt,name=type"` + // VolumeTypes contains constraints regarding allowed values for volume types in the 'workers' block in the Shoot specification. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + VolumeTypes []VolumeType `json:"volumeTypes,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,9,rep,name=volumeTypes"` + // Bastion contains the machine and image properties + // +optional + Bastion *Bastion `json:"bastion,omitempty" protobuf:"bytes,10,opt,name=bastion"` +} + +// SeedSelector contains constraints for selecting seed to be usable for shoots using a profile +type SeedSelector struct { + // LabelSelector is optional and can be used to select seeds by their label settings + // +optional + metav1.LabelSelector `json:",inline,omitempty" protobuf:"bytes,1,opt,name=labelSelector"` + // Providers is optional and can be used by restricting seeds by their provider type. '*' can be used to enable seeds regardless of their provider type. + // +optional + ProviderTypes []string `json:"providerTypes,omitempty" protobuf:"bytes,2,rep,name=providerTypes"` +} + +// KubernetesSettings contains constraints regarding allowed values of the 'kubernetes' block in the Shoot specification. +type KubernetesSettings struct { + // Versions is the list of allowed Kubernetes versions with optional expiration dates for Shoot clusters. + // +patchMergeKey=version + // +patchStrategy=merge + // +optional + Versions []ExpirableVersion `json:"versions,omitempty" patchStrategy:"merge" patchMergeKey:"version" protobuf:"bytes,1,rep,name=versions"` +} + +// MachineImage defines the name and multiple versions of the machine image in any environment. +type MachineImage struct { + // Name is the name of the image. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Versions contains versions, expiration dates and container runtimes of the machine image + // +patchMergeKey=version + // +patchStrategy=merge + Versions []MachineImageVersion `json:"versions" patchStrategy:"merge" patchMergeKey:"version" protobuf:"bytes,2,rep,name=versions"` + // UpdateStrategy is the update strategy to use for the machine image. Possible values are: + // - patch: update to the latest patch version of the current minor version. + // - minor: update to the latest minor and patch version. + // - major: always update to the overall latest version (default). + // +optional + UpdateStrategy *MachineImageUpdateStrategy `json:"updateStrategy,omitempty" protobuf:"bytes,3,opt,name=updateStrategy,casttype=MachineImageUpdateStrategy"` +} + +// MachineImageVersion is an expirable version with list of supported container runtimes and interfaces +type MachineImageVersion struct { + ExpirableVersion `json:",inline" protobuf:"bytes,1,opt,name=expirableVersion"` + // CRI list of supported container runtime and interfaces supported by this version + // +optional + CRI []CRI `json:"cri,omitempty" protobuf:"bytes,2,rep,name=cri"` + // Architectures is the list of CPU architectures of the machine image in this version. + // +optional + Architectures []string `json:"architectures,omitempty" protobuf:"bytes,3,opt,name=architectures"` + // KubeletVersionConstraint is a constraint describing the supported kubelet versions by the machine image in this version. + // If the field is not specified, it is assumed that the machine image in this version supports all kubelet versions. + // Examples: + // - '>= 1.26' - supports only kubelet versions greater than or equal to 1.26 + // - '< 1.26' - supports only kubelet versions less than 1.26 + // +optional + KubeletVersionConstraint *string `json:"kubeletVersionConstraint,omitempty" protobuf:"bytes,4,opt,name=kubeletVersionConstraint"` +} + +// ExpirableVersion contains a version and an expiration date. +type ExpirableVersion struct { + // Version is the version identifier. + Version string `json:"version" protobuf:"bytes,1,opt,name=version"` + // ExpirationDate defines the time at which this version expires. + // +optional + ExpirationDate *metav1.Time `json:"expirationDate,omitempty" protobuf:"bytes,2,opt,name=expirationDate"` + // Classification defines the state of a version (preview, supported, deprecated) + // +optional + Classification *VersionClassification `json:"classification,omitempty" protobuf:"bytes,3,opt,name=classification,casttype=VersionClassification"` +} + +// MachineType contains certain properties of a machine type. +type MachineType struct { + // CPU is the number of CPUs for this machine type. + CPU resource.Quantity `json:"cpu" protobuf:"bytes,1,opt,name=cpu"` + // GPU is the number of GPUs for this machine type. + GPU resource.Quantity `json:"gpu" protobuf:"bytes,2,opt,name=gpu"` + // Memory is the amount of memory for this machine type. + Memory resource.Quantity `json:"memory" protobuf:"bytes,3,opt,name=memory"` + // Name is the name of the machine type. + Name string `json:"name" protobuf:"bytes,4,opt,name=name"` + // Storage is the amount of storage associated with the root volume of this machine type. + // +optional + Storage *MachineTypeStorage `json:"storage,omitempty" protobuf:"bytes,5,opt,name=storage"` + // Usable defines if the machine type can be used for shoot clusters. + // +optional + Usable *bool `json:"usable,omitempty" protobuf:"varint,6,opt,name=usable"` + // Architecture is the CPU architecture of this machine type. + // +optional + Architecture *string `json:"architecture,omitempty" protobuf:"bytes,7,opt,name=architecture"` +} + +// MachineTypeStorage is the amount of storage associated with the root volume of this machine type. +type MachineTypeStorage struct { + // Class is the class of the storage type. + Class string `json:"class" protobuf:"bytes,1,opt,name=class"` + // StorageSize is the storage size. + // +optional + StorageSize *resource.Quantity `json:"size,omitempty" protobuf:"bytes,2,opt,name=size"` + // Type is the type of the storage. + Type string `json:"type" protobuf:"bytes,3,opt,name=type"` + // MinSize is the minimal supported storage size. + // This overrides any other common minimum size configuration from `spec.volumeTypes[*].minSize`. + // +optional + MinSize *resource.Quantity `json:"minSize,omitempty" protobuf:"bytes,4,opt,name=minSize"` +} + +// Region contains certain properties of a region. +type Region struct { + // Name is a region name. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Zones is a list of availability zones in this region. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + Zones []AvailabilityZone `json:"zones,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=zones"` + // Labels is an optional set of key-value pairs that contain certain administrator-controlled labels for this region. + // It can be used by Gardener administrators/operators to provide additional information about a region, e.g. wrt + // quality, reliability, etc. + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,3,rep,name=labels"` + // AccessRestrictions describe a list of access restrictions that can be used for Shoots using this region. + // +optional + AccessRestrictions []AccessRestriction `json:"accessRestrictions,omitempty" protobuf:"bytes,4,rep,name=accessRestrictions"` +} + +// AvailabilityZone is an availability zone. +type AvailabilityZone struct { + // Name is an availability zone name. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // UnavailableMachineTypes is a list of machine type names that are not availability in this zone. + // +optional + UnavailableMachineTypes []string `json:"unavailableMachineTypes,omitempty" protobuf:"bytes,2,rep,name=unavailableMachineTypes"` + // UnavailableVolumeTypes is a list of volume type names that are not availability in this zone. + // +optional + UnavailableVolumeTypes []string `json:"unavailableVolumeTypes,omitempty" protobuf:"bytes,3,rep,name=unavailableVolumeTypes"` +} + +// VolumeType contains certain properties of a volume type. +type VolumeType struct { + // Class is the class of the volume type. + Class string `json:"class" protobuf:"bytes,1,opt,name=class"` + // Name is the name of the volume type. + Name string `json:"name" protobuf:"bytes,2,opt,name=name"` + // Usable defines if the volume type can be used for shoot clusters. + // +optional + Usable *bool `json:"usable,omitempty" protobuf:"varint,3,opt,name=usable"` + // MinSize is the minimal supported storage size. + // +optional + MinSize *resource.Quantity `json:"minSize,omitempty" protobuf:"bytes,4,opt,name=minSize"` +} + +// Bastion contains the bastions creation info +type Bastion struct { + // MachineImage contains the bastions machine image properties + // +optional + MachineImage *BastionMachineImage `json:"machineImage,omitempty" protobuf:"bytes,1,opt,name=machineImage"` + // MachineType contains the bastions machine type properties + // +optional + MachineType *BastionMachineType `json:"machineType,omitempty" protobuf:"bytes,2,opt,name=machineType"` +} + +// BastionMachineImage contains the bastions machine image properties +type BastionMachineImage struct { + // Name of the machine image + Name string `json:"name" protobuf:"bytes,1,name=name"` + // Version of the machine image + // +optional + Version *string `json:"version,omitempty" protobuf:"bytes,2,opt,name=version"` +} + +// BastionMachineType contains the bastions machine type properties +type BastionMachineType struct { + // Name of the machine type + Name string `json:"name" protobuf:"bytes,1,name=name"` +} + +const ( + // VolumeClassStandard is a constant for the standard volume class. + VolumeClassStandard string = "standard" + // VolumeClassPremium is a constant for the premium volume class. + VolumeClassPremium string = "premium" +) + +// VersionClassification is the logical state of a version. +type VersionClassification string + +const ( + // ClassificationPreview indicates that a version has recently been added and not promoted to "Supported" yet. + // ClassificationPreview versions will not be considered for automatic Kubernetes and Machine Image patch version updates. + ClassificationPreview VersionClassification = "preview" + // ClassificationSupported indicates that a patch version is the recommended version for a shoot. + // Only one "supported" version is allowed per minor version. + // Supported versions are eligible for the automated Kubernetes and Machine image patch version update for shoot clusters in Gardener. + ClassificationSupported VersionClassification = "supported" + // ClassificationDeprecated indicates that a patch version should not be used anymore, should be updated to a new version + // and will eventually expire. + ClassificationDeprecated VersionClassification = "deprecated" +) + +// MachineImageUpdateStrategy is the update strategy to use for a machine image +type MachineImageUpdateStrategy string + +const ( + // UpdateStrategyPatch indicates that auto-updates are performed to the latest patch version of the current minor version. + // When using an expired version during the maintenance window, force updates to the latest patch of the next (not necessarily consecutive) minor when using an expired version. + UpdateStrategyPatch MachineImageUpdateStrategy = "patch" + // UpdateStrategyMinor indicates that auto-updates are performed to the latest patch and minor version of the current major version. + // When using an expired version during the maintenance window, force updates to the latest minor and patch of the next (not necessarily consecutive) major version. + UpdateStrategyMinor MachineImageUpdateStrategy = "minor" + // UpdateStrategyMajor indicates that auto-updates are performed always to the overall latest version. + UpdateStrategyMajor MachineImageUpdateStrategy = "major" +) diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_common.go b/api/external/gardener/pkg/apis/core/v1beta1/types_common.go new file mode 100644 index 0000000..e4b8ba2 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_common.go @@ -0,0 +1,174 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ErrorCode is a string alias. +type ErrorCode string + +const ( + // ErrorInfraUnauthenticated indicates that the last error occurred due to the client request not being completed because it lacks valid authentication credentials for the requested resource. + // It is classified as a non-retryable error code. + ErrorInfraUnauthenticated ErrorCode = "ERR_INFRA_UNAUTHENTICATED" + // ErrorInfraUnauthorized indicates that the last error occurred due to the server understanding the request but refusing to authorize it. + // It is classified as a non-retryable error code. + ErrorInfraUnauthorized ErrorCode = "ERR_INFRA_UNAUTHORIZED" + // ErrorInfraQuotaExceeded indicates that the last error occurred due to infrastructure quota limits. + // It is classified as a non-retryable error code. + ErrorInfraQuotaExceeded ErrorCode = "ERR_INFRA_QUOTA_EXCEEDED" + // ErrorInfraRateLimitsExceeded indicates that the last error occurred due to exceeded infrastructure request rate limits. + ErrorInfraRateLimitsExceeded ErrorCode = "ERR_INFRA_RATE_LIMITS_EXCEEDED" + // ErrorInfraDependencies indicates that the last error occurred due to dependent objects on the infrastructure level. + // It is classified as a non-retryable error code. + ErrorInfraDependencies ErrorCode = "ERR_INFRA_DEPENDENCIES" + // ErrorRetryableInfraDependencies indicates that the last error occurred due to dependent objects on the infrastructure level, but operation should be retried. + ErrorRetryableInfraDependencies ErrorCode = "ERR_RETRYABLE_INFRA_DEPENDENCIES" + // ErrorInfraResourcesDepleted indicates that the last error occurred due to depleted resource in the infrastructure. + ErrorInfraResourcesDepleted ErrorCode = "ERR_INFRA_RESOURCES_DEPLETED" + // ErrorCleanupClusterResources indicates that the last error occurred due to resources in the cluster that are stuck in deletion. + ErrorCleanupClusterResources ErrorCode = "ERR_CLEANUP_CLUSTER_RESOURCES" + // ErrorConfigurationProblem indicates that the last error occurred due to a configuration problem. + // It is classified as a non-retryable error code. + ErrorConfigurationProblem ErrorCode = "ERR_CONFIGURATION_PROBLEM" + // ErrorRetryableConfigurationProblem indicates that the last error occurred due to a retryable configuration problem. + ErrorRetryableConfigurationProblem ErrorCode = "ERR_RETRYABLE_CONFIGURATION_PROBLEM" + // ErrorProblematicWebhook indicates that the last error occurred due to a webhook not following the Kubernetes + // best practices (https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#best-practices-and-warnings). + // It is classified as a non-retryable error code. + ErrorProblematicWebhook ErrorCode = "ERR_PROBLEMATIC_WEBHOOK" +) + +// LastError indicates the last occurred error for an operation on a resource. +type LastError struct { + // A human readable message indicating details about the last error. + Description string `json:"description" protobuf:"bytes,1,opt,name=description"` + // ID of the task which caused this last error + // +optional + TaskID *string `json:"taskID,omitempty" protobuf:"bytes,2,opt,name=taskID"` + // Well-defined error codes of the last error(s). + // +optional + Codes []ErrorCode `json:"codes,omitempty" protobuf:"bytes,3,rep,name=codes,casttype=ErrorCode"` + // Last time the error was reported + // +optional + LastUpdateTime *metav1.Time `json:"lastUpdateTime,omitempty" protobuf:"bytes,4,opt,name=lastUpdateTime"` +} + +// LastOperationType is a string alias. +type LastOperationType string + +const ( + // LastOperationTypeCreate indicates a 'create' operation. + LastOperationTypeCreate LastOperationType = "Create" + // LastOperationTypeReconcile indicates a 'reconcile' operation. + LastOperationTypeReconcile LastOperationType = "Reconcile" + // LastOperationTypeDelete indicates a 'delete' operation. + LastOperationTypeDelete LastOperationType = "Delete" + // LastOperationTypeMigrate indicates a 'migrate' operation. + LastOperationTypeMigrate LastOperationType = "Migrate" + // LastOperationTypeRestore indicates a 'restore' operation. + LastOperationTypeRestore LastOperationType = "Restore" +) + +// LastOperationState is a string alias. +type LastOperationState string + +const ( + // LastOperationStateProcessing indicates that an operation is ongoing. + LastOperationStateProcessing LastOperationState = "Processing" + // LastOperationStateSucceeded indicates that an operation has completed successfully. + LastOperationStateSucceeded LastOperationState = "Succeeded" + // LastOperationStateError indicates that an operation is completed with errors and will be retried. + LastOperationStateError LastOperationState = "Error" + // LastOperationStateFailed indicates that an operation is completed with errors and won't be retried. + LastOperationStateFailed LastOperationState = "Failed" + // LastOperationStatePending indicates that an operation cannot be done now, but will be tried in future. + LastOperationStatePending LastOperationState = "Pending" + // LastOperationStateAborted indicates that an operation has been aborted. + LastOperationStateAborted LastOperationState = "Aborted" +) + +// LastOperation indicates the type and the state of the last operation, along with a description +// message and a progress indicator. +type LastOperation struct { + // A human readable message indicating details about the last operation. + Description string `json:"description" protobuf:"bytes,1,opt,name=description"` + // Last time the operation state transitioned from one to another. + LastUpdateTime metav1.Time `json:"lastUpdateTime" protobuf:"bytes,2,opt,name=lastUpdateTime"` + // The progress in percentage (0-100) of the last operation. + Progress int32 `json:"progress" protobuf:"varint,3,opt,name=progress"` + // Status of the last operation, one of Aborted, Processing, Succeeded, Error, Failed. + State LastOperationState `json:"state" protobuf:"bytes,4,opt,name=state,casttype=LastOperationState"` + // Type of the last operation, one of Create, Reconcile, Delete, Migrate, Restore. + Type LastOperationType `json:"type" protobuf:"bytes,5,opt,name=type,casttype=LastOperationType"` +} + +// Gardener holds the information about the Gardener version that operated a resource. +type Gardener struct { + // ID is the container id of the Gardener which last acted on a resource. + ID string `json:"id" protobuf:"bytes,1,opt,name=id"` + // Name is the hostname (pod name) of the Gardener which last acted on a resource. + Name string `json:"name" protobuf:"bytes,2,opt,name=name"` + // Version is the version of the Gardener which last acted on a resource. + Version string `json:"version" protobuf:"bytes,3,opt,name=version"` +} + +const ( + // GardenerName is the value in a Garden resource's `.metadata.finalizers[]` array on which the Gardener will react + // when performing a delete request on a resource. + GardenerName = "gardener" + // ExternalGardenerName is the value in a Kubernetes core resources `.metadata.finalizers[]` array on which the + // Gardener will react when performing a delete request on a resource. + ExternalGardenerName = "gardener.cloud/gardener" +) + +const ( + // EventReconciling indicates that the Reconcile operation started. + EventReconciling = "Reconciling" + // EventReconciled indicates that the Reconcile operation was successful. + EventReconciled = "Reconciled" + // EventReconcileError indicates that the Reconcile operation failed. + EventReconcileError = "ReconcileError" + // EventDeleting indicates that the Delete operation started. + EventDeleting = "Deleting" + // EventDeleted indicates that the Delete operation was successful. + EventDeleted = "Deleted" + // EventDeleteError indicates that the Delete operation failed. + EventDeleteError = "DeleteError" + // EventPrepareMigration indicates that the Prepare Migration operation started. + EventPrepareMigration = "PrepareMigration" + // EventMigrationPrepared indicates that the Migration preparation was successful. + EventMigrationPrepared = "MigrationPrepared" + // EventMigrationPreparationFailed indicates that the Migration preparation failed. + EventMigrationPreparationFailed = "MigrationPreparationFailed" +) + +// HighAvailability specifies the configuration settings for high availability for a resource. Typical +// usages could be to configure HA for shoot control plane or for seed system components. +type HighAvailability struct { + // FailureTolerance holds information about failure tolerance level of a highly available resource. + FailureTolerance FailureTolerance `json:"failureTolerance" protobuf:"bytes,1,name=failureTolerance"` +} + +// FailureTolerance describes information about failure tolerance level of a highly available resource. +type FailureTolerance struct { + // Type specifies the type of failure that the highly available resource can tolerate + Type FailureToleranceType `json:"type" protobuf:"bytes,1,name=type"` +} + +// FailureToleranceType specifies the type of failure that a highly available +// shoot control plane that can tolerate. +type FailureToleranceType string + +const ( + // FailureToleranceTypeNode specifies that a highly available resource can tolerate the + // failure of one or more nodes within a single-zone setup and still be available. + FailureToleranceTypeNode FailureToleranceType = "node" + // FailureToleranceTypeZone specifies that a highly available resource can tolerate the + // failure of one or more zones within a multi-zone setup and still be available. + FailureToleranceTypeZone FailureToleranceType = "zone" +) diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_controllerdeployment.go b/api/external/gardener/pkg/apis/core/v1beta1/types_controllerdeployment.go new file mode 100644 index 0000000..61edb2e --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_controllerdeployment.go @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControllerDeployment contains information about how this controller is deployed. +type ControllerDeployment struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Type is the deployment type. + Type string `json:"type" protobuf:"bytes,2,opt,name=type"` + // ProviderConfig contains type-specific configuration. It contains assets that deploy the controller. + ProviderConfig runtime.RawExtension `json:"providerConfig" protobuf:"bytes,3,opt,name=providerConfig"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControllerDeploymentList is a collection of ControllerDeployments. +type ControllerDeploymentList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of ControllerDeployments. + Items []ControllerDeployment `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +const ( + // ControllerDeploymentTypeHelm is the type for instructing the extension controller deployment using helm. + // The ControllerDeployment.ProviderConfig is expected to hold a HelmControllerDeployment object. + ControllerDeploymentTypeHelm = "helm" +) + +// HelmControllerDeployment configures how an extension controller is deployed using helm. +// This is the legacy structure that used to be defined in gardenlet's ControllerInstallation controller for +// ControllerDeployment's with type=helm. +// While this is not a proper API type, we need to define the structure in the API package so that we can convert it +// to the internal API version in the new representation. +type HelmControllerDeployment struct { + // Chart is a Helm chart tarball. + Chart []byte `json:"chart,omitempty" protobuf:"bytes,1,opt,name=chart"` + // Values is a map of values for the given chart. + Values *apiextensionsv1.JSON `json:"values,omitempty" protobuf:"bytes,2,opt,name=values"` + // OCIRepository defines where to pull the chart. + // +optional + OCIRepository *OCIRepository `json:"ociRepository,omitempty" protobuf:"bytes,3,opt,name=ociRepository"` +} + +// OCIRepository configures where to pull an OCI Artifact, that could contain for example a Helm Chart. +type OCIRepository struct { + // Ref is the full artifact Ref and takes precedence over all other fields. + // +optional + Ref *string `json:"ref,omitempty" protobuf:"bytes,1,name=ref"` + // Repository is a reference to an OCI artifact repository. + // +optional + Repository *string `json:"repository,omitempty" protobuf:"bytes,2,name=repository"` + // Tag is the image tag to pull. + // +optional + Tag *string `json:"tag,omitempty" protobuf:"bytes,3,opt,name=tag"` + // Digest of the image to pull, takes precedence over tag. + // +optional + Digest *string `json:"digest,omitempty" protobuf:"bytes,4,opt,name=digest"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_controllerinstallation.go b/api/external/gardener/pkg/apis/core/v1beta1/types_controllerinstallation.go new file mode 100644 index 0000000..ac9d28c --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_controllerinstallation.go @@ -0,0 +1,77 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControllerInstallation represents an installation request for an external controller. +type ControllerInstallation struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec contains the specification of this installation. + // If the object's deletion timestamp is set, this field is immutable. + Spec ControllerInstallationSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + // Status contains the status of this installation. + Status ControllerInstallationStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControllerInstallationList is a collection of ControllerInstallations. +type ControllerInstallationList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of ControllerInstallations. + Items []ControllerInstallation `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// ControllerInstallationSpec is the specification of a ControllerInstallation. +type ControllerInstallationSpec struct { + // RegistrationRef is used to reference a ControllerRegistration resource. + // The name field of the RegistrationRef is immutable. + RegistrationRef corev1.ObjectReference `json:"registrationRef" protobuf:"bytes,1,opt,name=registrationRef"` + // SeedRef is used to reference a Seed resource. The name field of the SeedRef is immutable. + SeedRef corev1.ObjectReference `json:"seedRef" protobuf:"bytes,2,opt,name=seedRef"` + // DeploymentRef is used to reference a ControllerDeployment resource. + // +optional + DeploymentRef *corev1.ObjectReference `json:"deploymentRef,omitempty" protobuf:"bytes,3,opt,name=deploymentRef"` +} + +// ControllerInstallationStatus is the status of a ControllerInstallation. +type ControllerInstallationStatus struct { + // Conditions represents the latest available observations of a ControllerInstallations's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +optional + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + // ProviderStatus contains type-specific status. + // +optional + ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty" protobuf:"bytes,2,opt,name=providerStatus"` +} + +const ( + // ControllerInstallationHealthy is a condition type for indicating whether the controller is healthy. + ControllerInstallationHealthy ConditionType = "Healthy" + // ControllerInstallationInstalled is a condition type for indicating whether the controller has been installed. + ControllerInstallationInstalled ConditionType = "Installed" + // ControllerInstallationProgressing is a condition type for indicating whether the controller is progressing. + ControllerInstallationProgressing ConditionType = "Progressing" + // ControllerInstallationValid is a condition type for indicating whether the installation request is valid. + ControllerInstallationValid ConditionType = "Valid" + // ControllerInstallationRequired is a condition type for indicating that the respective extension controller is + // still required on the seed cluster as corresponding extension resources still exist. + ControllerInstallationRequired ConditionType = "Required" +) diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_controllerregistration.go b/api/external/gardener/pkg/apis/core/v1beta1/types_controllerregistration.go new file mode 100644 index 0000000..0d91882 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_controllerregistration.go @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControllerRegistration represents a registration of an external controller. +type ControllerRegistration struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec contains the specification of this registration. + // If the object's deletion timestamp is set, this field is immutable. + Spec ControllerRegistrationSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControllerRegistrationList is a collection of ControllerRegistrations. +type ControllerRegistrationList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of ControllerRegistrations. + Items []ControllerRegistration `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// ControllerRegistrationSpec is the specification of a ControllerRegistration. +type ControllerRegistrationSpec struct { + // Resources is a list of combinations of kinds (DNSProvider, Infrastructure, Generic, ...) and their actual types + // (aws-route53, gcp, auditlog, ...). + // +optional + Resources []ControllerResource `json:"resources,omitempty" protobuf:"bytes,1,opt,name=resources"` + // Deployment contains information for how this controller is deployed. + // +optional + Deployment *ControllerRegistrationDeployment `json:"deployment,omitempty" protobuf:"bytes,2,opt,name=deployment"` +} + +// ControllerResource is a combination of a kind (DNSProvider, Infrastructure, Generic, ...) and the actual type for this +// kind (aws-route53, gcp, auditlog, ...). +type ControllerResource struct { + // Kind is the resource kind, for example "OperatingSystemConfig". + Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"` + // Type is the resource type, for example "coreos" or "ubuntu". + Type string `json:"type" protobuf:"bytes,2,opt,name=type"` + // GloballyEnabled determines if this ControllerResource is required by all Shoot clusters. + // This field is defaulted to false when kind is "Extension". + // +optional + GloballyEnabled *bool `json:"globallyEnabled,omitempty" protobuf:"varint,3,opt,name=globallyEnabled"` + // ReconcileTimeout defines how long Gardener should wait for the resource reconciliation. + // This field is defaulted to 3m0s when kind is "Extension". + // +optional + ReconcileTimeout *metav1.Duration `json:"reconcileTimeout,omitempty" protobuf:"bytes,4,opt,name=reconcileTimeout"` + // Primary determines if the controller backed by this ControllerRegistration is responsible for the extension + // resource's lifecycle. This field defaults to true. There must be exactly one primary controller for this kind/type + // combination. This field is immutable. + // +optional + Primary *bool `json:"primary,omitempty" protobuf:"varint,5,opt,name=primary"` + // Lifecycle defines a strategy that determines when different operations on a ControllerResource should be performed. + // This field is defaulted in the following way when kind is "Extension". + // Reconcile: "AfterKubeAPIServer" + // Delete: "BeforeKubeAPIServer" + // Migrate: "BeforeKubeAPIServer" + // +optional + Lifecycle *ControllerResourceLifecycle `json:"lifecycle,omitempty" protobuf:"bytes,6,opt,name=lifecycle"` + // WorkerlessSupported specifies whether this ControllerResource supports Workerless Shoot clusters. + // This field is only relevant when kind is "Extension". + // +optional + WorkerlessSupported *bool `json:"workerlessSupported,omitempty" protobuf:"varint,7,opt,name=workerlessSupported"` +} + +// DeploymentRef contains information about `ControllerDeployment` references. +type DeploymentRef struct { + // Name is the name of the `ControllerDeployment` that is being referred to. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` +} + +// ControllerRegistrationDeployment contains information for how this controller is deployed. +type ControllerRegistrationDeployment struct { + // Policy controls how the controller is deployed. It defaults to 'OnDemand'. + // +optional + Policy *ControllerDeploymentPolicy `json:"policy,omitempty" protobuf:"bytes,3,opt,name=policy"` + // SeedSelector contains an optional label selector for seeds. Only if the labels match then this controller will be + // considered for a deployment. + // An empty list means that all seeds are selected. + // +optional + SeedSelector *metav1.LabelSelector `json:"seedSelector,omitempty" protobuf:"bytes,4,opt,name=seedSelector"` + // DeploymentRefs holds references to `ControllerDeployments`. Only one element is supported currently. + // +optional + DeploymentRefs []DeploymentRef `json:"deploymentRefs,omitempty" protobuf:"bytes,5,opt,name=deploymentRefs"` +} + +// ControllerDeploymentPolicy is a string alias. +type ControllerDeploymentPolicy string + +const ( + // ControllerDeploymentPolicyOnDemand specifies that the controller shall be only deployed if required by another + // resource. If nothing requires it then the controller shall not be deployed. + ControllerDeploymentPolicyOnDemand ControllerDeploymentPolicy = "OnDemand" + // ControllerDeploymentPolicyAlways specifies that the controller shall be deployed always, independent of whether + // another resource requires it or the respective seed has shoots. + ControllerDeploymentPolicyAlways ControllerDeploymentPolicy = "Always" + // ControllerDeploymentPolicyAlwaysExceptNoShoots specifies that the controller shall be deployed always, independent of + // whether another resource requires it, but only when the respective seed has at least one shoot. + ControllerDeploymentPolicyAlwaysExceptNoShoots ControllerDeploymentPolicy = "AlwaysExceptNoShoots" +) + +// ControllerResourceLifecycleStrategy is a string alias. +type ControllerResourceLifecycleStrategy string + +const ( + // BeforeKubeAPIServer specifies that a resource should be handled before the kube-apiserver. + BeforeKubeAPIServer ControllerResourceLifecycleStrategy = "BeforeKubeAPIServer" + // AfterKubeAPIServer specifies that a resource should be handled after the kube-apiserver. + AfterKubeAPIServer ControllerResourceLifecycleStrategy = "AfterKubeAPIServer" + // AfterWorker specifies that a resource should be handled after workers. This is only available during reconcile. + AfterWorker ControllerResourceLifecycleStrategy = "AfterWorker" +) + +// ControllerResourceLifecycle defines the lifecycle of a controller resource. +type ControllerResourceLifecycle struct { + // Reconcile defines the strategy during reconciliation. + // +optional + Reconcile *ControllerResourceLifecycleStrategy `json:"reconcile,omitempty" protobuf:"bytes,1,opt,name=reconcile,casttype=ControllerResourceLifecycleStrategy"` + // Delete defines the strategy during deletion. + // +optional + Delete *ControllerResourceLifecycleStrategy `json:"delete,omitempty" protobuf:"bytes,2,opt,name=delete,casttype=ControllerResourceLifecycleStrategy"` + // Migrate defines the strategy during migration. + // +optional + Migrate *ControllerResourceLifecycleStrategy `json:"migrate,omitempty" protobuf:"bytes,3,opt,name=migrate,casttype=ControllerResourceLifecycleStrategy"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_exposureclass.go b/api/external/gardener/pkg/apis/core/v1beta1/types_exposureclass.go new file mode 100644 index 0000000..a2e6d51 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_exposureclass.go @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ExposureClass represents a control plane endpoint exposure strategy. +type ExposureClass struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Handler is the name of the handler which applies the control plane endpoint exposure strategy. + // This field is immutable. + Handler string `json:"handler" protobuf:"bytes,2,opt,name=handler"` + // Scheduling holds information how to select applicable Seed's for ExposureClass usage. + // This field is immutable. + // +optional + Scheduling *ExposureClassScheduling `json:"scheduling,omitempty" protobuf:"bytes,3,opt,name=scheduling"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ExposureClassList is a collection of ExposureClass. +type ExposureClassList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of ExposureClasses. + Items []ExposureClass `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// ExposureClassScheduling holds information to select applicable Seed's for ExposureClass usage. +type ExposureClassScheduling struct { + // SeedSelector is an optional label selector for Seed's which are suitable to use the ExposureClass. + // +optional + SeedSelector *SeedSelector `json:"seedSelector,omitempty" protobuf:"bytes,1,opt,name=seedSelector"` + // Tolerations contains the tolerations for taints on Seed clusters. + // +patchMergeKey=key + // +patchStrategy=merge + // +optional + Tolerations []Toleration `json:"tolerations,omitempty" patchStrategy:"merge" patchMergeKey:"key" protobuf:"bytes,2,rep,name=tolerations"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_internalsecret.go b/api/external/gardener/pkg/apis/core/v1beta1/types_internalsecret.go new file mode 100644 index 0000000..f9e85d8 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_internalsecret.go @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// InternalSecret holds secret data of a certain type. The total bytes of the values in +// the Data field must be less than MaxSecretSize bytes. +type InternalSecret struct { + metav1.TypeMeta `json:",inline"` + // Standard object's metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Immutable, if set to true, ensures that data stored in the Secret cannot + // be updated (only object metadata can be modified). + // If not set to true, the field can be modified at any time. + // Defaulted to nil. + // +optional + Immutable *bool `json:"immutable,omitempty" protobuf:"varint,5,opt,name=immutable"` + + // Data contains the secret data. Each key must consist of alphanumeric + // characters, '-', '_' or '.'. The serialized form of the secret data is a + // base64 encoded string, representing the arbitrary (possibly non-string) + // data value here. Described in https://tools.ietf.org/html/rfc4648#section-4 + // +optional + Data map[string][]byte `json:"data,omitempty" protobuf:"bytes,2,rep,name=data"` + + // stringData allows specifying non-binary secret data in string form. + // It is provided as a write-only input field for convenience. + // All keys and values are merged into the data field on write, overwriting any existing values. + // The stringData field is never output when reading from the API. + // +k8s:conversion-gen=false + // +optional + StringData map[string]string `json:"stringData,omitempty" protobuf:"bytes,4,rep,name=stringData"` + + // Used to facilitate programmatic handling of secret data. + // More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types + // +optional + Type corev1.SecretType `json:"type,omitempty" protobuf:"bytes,3,opt,name=type,casttype=SecretType"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// InternalSecretList is a list of InternalSecret. +type InternalSecretList struct { + metav1.TypeMeta `json:",inline"` + // Standard list metadata. + // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + + // Items is a list of secret objects. + // More info: https://kubernetes.io/docs/concepts/configuration/secret + Items []InternalSecret `json:"items" protobuf:"bytes,2,rep,name=items"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_namespacedcloudprofile.go b/api/external/gardener/pkg/apis/core/v1beta1/types_namespacedcloudprofile.go new file mode 100644 index 0000000..fdaca65 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_namespacedcloudprofile.go @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// NamespacedCloudProfile represents certain properties about a provider environment. +type NamespacedCloudProfile struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec defines the provider environment properties. + Spec NamespacedCloudProfileSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + // Most recently observed status of the NamespacedCloudProfile. + Status NamespacedCloudProfileStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// NamespacedCloudProfileList is a collection of NamespacedCloudProfiles. +type NamespacedCloudProfileList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of NamespacedCloudProfiles. + Items []NamespacedCloudProfile `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// NamespacedCloudProfileSpec is the specification of a NamespacedCloudProfile. +type NamespacedCloudProfileSpec struct { + // CABundle is a certificate bundle which will be installed onto every host machine of shoot cluster targeting this profile. + // +optional + CABundle *string `json:"caBundle,omitempty" protobuf:"bytes,1,opt,name=caBundle"` + // Kubernetes contains constraints regarding allowed values of the 'kubernetes' block in the Shoot specification. + // +optional + Kubernetes *KubernetesSettings `json:"kubernetes,omitempty" protobuf:"bytes,2,opt,name=kubernetes"` + // MachineImages contains constraints regarding allowed values for machine images in the Shoot specification. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + MachineImages []MachineImage `json:"machineImages,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,3,opt,name=machineImages"` + // MachineTypes contains constraints regarding allowed values for machine types in the 'workers' block in the Shoot specification. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + MachineTypes []MachineType `json:"machineTypes,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,4,opt,name=machineTypes"` + // VolumeTypes contains constraints regarding allowed values for volume types in the 'workers' block in the Shoot specification. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + VolumeTypes []VolumeType `json:"volumeTypes,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,6,opt,name=volumeTypes"` + // Parent contains a reference to a CloudProfile it inherits from. + Parent CloudProfileReference `json:"parent" protobuf:"bytes,7,req,name=parent"` + // ProviderConfig contains provider-specific configuration for the profile. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,8,opt,name=providerConfig"` +} + +// NamespacedCloudProfileStatus holds the most recently observed status of the NamespacedCloudProfile. +type NamespacedCloudProfileStatus struct { + // CloudProfile is the most recently generated CloudProfile of the NamespacedCloudProfile. + CloudProfileSpec CloudProfileSpec `json:"cloudProfileSpec,omitempty" protobuf:"bytes,1,req,name=cloudProfileSpec"` + // ObservedGeneration is the most recent generation observed for this NamespacedCloudProfile. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,2,opt,name=observedGeneration"` +} + +// CloudProfileReference holds the information about a CloudProfile or a NamespacedCloudProfile. +type CloudProfileReference struct { + // Kind contains a CloudProfile kind. + Kind string `json:"kind" protobuf:"bytes,1,req,name=kind"` + // Name contains the name of the referenced CloudProfile. + Name string `json:"name" protobuf:"bytes,2,req,name=name"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_project.go b/api/external/gardener/pkg/apis/core/v1beta1/types_project.go new file mode 100644 index 0000000..56887c7 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_project.go @@ -0,0 +1,187 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Project holds certain properties about a Gardener project. +type Project struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec defines the project properties. + // +optional + Spec ProjectSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + // Most recently observed status of the Project. + // +optional + Status ProjectStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ProjectList is a collection of Projects. +type ProjectList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of Projects. + Items []Project `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// ProjectSpec is the specification of a Project. +type ProjectSpec struct { + // CreatedBy is a subject representing a user name, an email address, or any other identifier of a user + // who created the project. This field is immutable. + // +optional + CreatedBy *rbacv1.Subject `json:"createdBy,omitempty" protobuf:"bytes,1,opt,name=createdBy"` + // Description is a human-readable description of what the project is used for. + // +optional + Description *string `json:"description,omitempty" protobuf:"bytes,2,opt,name=description"` + // Owner is a subject representing a user name, an email address, or any other identifier of a user owning + // the project. + // IMPORTANT: Be aware that this field will be removed in the `v1` version of this API in favor of the `owner` + // role. The only way to change the owner will be by moving the `owner` role. In this API version the only way + // to change the owner is to use this field. + // +optional + // TODO: Remove this field in favor of the `owner` role in `v1`. + Owner *rbacv1.Subject `json:"owner,omitempty" protobuf:"bytes,3,opt,name=owner"` + // Purpose is a human-readable explanation of the project's purpose. + // +optional + Purpose *string `json:"purpose,omitempty" protobuf:"bytes,4,opt,name=purpose"` + // Members is a list of subjects representing a user name, an email address, or any other identifier of a user, + // group, or service account that has a certain role. + // +optional + Members []ProjectMember `json:"members,omitempty" protobuf:"bytes,5,rep,name=members"` + // Namespace is the name of the namespace that has been created for the Project object. + // A nil value means that Gardener will determine the name of the namespace. + // This field is immutable. + // +optional + Namespace *string `json:"namespace,omitempty" protobuf:"bytes,6,opt,name=namespace"` + // Tolerations contains the tolerations for taints on seed clusters. + // +optional + Tolerations *ProjectTolerations `json:"tolerations,omitempty" protobuf:"bytes,7,opt,name=tolerations"` + // DualApprovalForDeletion contains configuration for the dual approval concept for resource deletion. + // +optional + DualApprovalForDeletion []DualApprovalForDeletion `json:"dualApprovalForDeletion,omitempty" protobuf:"bytes,8,opt,name=dualApprovalForDeletion"` +} + +// ProjectStatus holds the most recently observed status of the project. +type ProjectStatus struct { + // ObservedGeneration is the most recent generation observed for this project. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,1,opt,name=observedGeneration"` + // Phase is the current phase of the project. + Phase ProjectPhase `json:"phase,omitempty" protobuf:"bytes,2,opt,name=phase,casttype=ProjectPhase"` + // StaleSinceTimestamp contains the timestamp when the project was first discovered to be stale/unused. + // +optional + StaleSinceTimestamp *metav1.Time `json:"staleSinceTimestamp,omitempty" protobuf:"bytes,3,opt,name=staleSinceTimestamp"` + // StaleAutoDeleteTimestamp contains the timestamp when the project will be garbage-collected/automatically deleted + // because it's stale/unused. + // +optional + StaleAutoDeleteTimestamp *metav1.Time `json:"staleAutoDeleteTimestamp,omitempty" protobuf:"bytes,4,opt,name=staleAutoDeleteTimestamp"` + // LastActivityTimestamp contains the timestamp from the last activity performed in this project. + // +optional + LastActivityTimestamp *metav1.Time `json:"lastActivityTimestamp,omitempty" protobuf:"bytes,5,opt,name=lastActivityTimestamp"` +} + +// ProjectMember is a member of a project. +type ProjectMember struct { + // Subject is representing a user name, an email address, or any other identifier of a user, group, or service + // account that has a certain role. + rbacv1.Subject `json:",inline" protobuf:"bytes,1,opt,name=subject"` + // Role represents the role of this member. + // IMPORTANT: Be aware that this field will be removed in the `v1` version of this API in favor of the `roles` + // list. + // TODO: Remove this field in favor of the `roles` list in `v1`. + Role string `json:"role" protobuf:"bytes,2,opt,name=role"` + // Roles represents the list of roles of this member. + // +optional + Roles []string `json:"roles,omitempty" protobuf:"bytes,3,rep,name=roles"` +} + +// ProjectTolerations contains the tolerations for taints on seed clusters. +type ProjectTolerations struct { + // Defaults contains a list of tolerations that are added to the shoots in this project by default. + // +patchMergeKey=key + // +patchStrategy=merge + // +optional + Defaults []Toleration `json:"defaults,omitempty" patchStrategy:"merge" patchMergeKey:"key" protobuf:"bytes,1,rep,name=defaults"` + // Whitelist contains a list of tolerations that are allowed to be added to the shoots in this project. Please note + // that this list may only be added by users having the `spec-tolerations-whitelist` verb for project resources. + // +patchMergeKey=key + // +patchStrategy=merge + // +optional + Whitelist []Toleration `json:"whitelist,omitempty" patchStrategy:"merge" patchMergeKey:"key" protobuf:"bytes,2,rep,name=whitelist"` +} + +// Toleration is a toleration for a seed taint. +type Toleration struct { + // Key is the toleration key to be applied to a project or shoot. + Key string `json:"key" protobuf:"bytes,1,opt,name=key"` + // Value is the toleration value corresponding to the toleration key. + // +optional + Value *string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` +} + +// DualApprovalForDeletion contains configuration for the dual approval concept for resource deletion. +type DualApprovalForDeletion struct { + // Resource is the name of the resource this applies to. + Resource string `json:"resource" protobuf:"bytes,1,opt,name=resource"` + // Selector is the label selector for the resources. + Selector metav1.LabelSelector `json:"selector" protobuf:"bytes,2,opt,name=selector"` + // IncludeServiceAccounts specifies whether the concept also applies when deletion is triggered by ServiceAccounts. + // Defaults to true. + // +optional + IncludeServiceAccounts *bool `json:"includeServiceAccounts,omitempty" protobuf:"varint,3,opt,name=includeServiceAccounts"` +} + +const ( + // ProjectMemberAdmin is a const for a role that provides full admin access. + ProjectMemberAdmin = "admin" + // ProjectMemberOwner is a const for a role that provides full owner access. + ProjectMemberOwner = "owner" + // ProjectMemberUserAccessManager is a const for a role that provides permissions to manage human user(s, (groups)). + ProjectMemberUserAccessManager = "uam" + // ProjectMemberServiceAccountManager is a const for a role that provides permissions to manage service accounts and request tokens for them. + ProjectMemberServiceAccountManager = "serviceaccountmanager" + // ProjectMemberViewer is a const for a role that provides limited permissions to only view some resources. + ProjectMemberViewer = "viewer" + // ProjectMemberExtensionPrefix is a prefix for custom roles that are not known by Gardener. + ProjectMemberExtensionPrefix = "extension:" +) + +// ProjectPhase is a label for the condition of a project at the current time. +type ProjectPhase string + +const ( + // ProjectPending indicates that the project reconciliation is pending. + ProjectPending ProjectPhase = "Pending" + // ProjectReady indicates that the project reconciliation was successful. + ProjectReady ProjectPhase = "Ready" + // ProjectFailed indicates that the project reconciliation failed. + ProjectFailed ProjectPhase = "Failed" + // ProjectTerminating indicates that the project is in termination process. + ProjectTerminating ProjectPhase = "Terminating" + + // ProjectEventNamespaceReconcileFailed indicates that the namespace reconciliation has failed. + ProjectEventNamespaceReconcileFailed = "NamespaceReconcileFailed" + // ProjectEventNamespaceReconcileSuccessful indicates that the namespace reconciliation has succeeded. + ProjectEventNamespaceReconcileSuccessful = "NamespaceReconcileSuccessful" + // ProjectEventNamespaceNotEmpty indicates that the namespace cannot be released because it is not empty. + ProjectEventNamespaceNotEmpty = "NamespaceNotEmpty" + // ProjectEventNamespaceDeletionFailed indicates that the namespace deletion failed. + ProjectEventNamespaceDeletionFailed = "NamespaceDeletionFailed" + // ProjectEventNamespaceMarkedForDeletion indicates that the namespace has been successfully marked for deletion. + ProjectEventNamespaceMarkedForDeletion = "NamespaceMarkedForDeletion" +) diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_quota.go b/api/external/gardener/pkg/apis/core/v1beta1/types_quota.go new file mode 100644 index 0000000..c7609a6 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_quota.go @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Quota represents a quota on resources consumed by shoot clusters either per project or per provider secret. +type Quota struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec defines the Quota constraints. + // +optional + Spec QuotaSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// QuotaList is a collection of Quotas. +type QuotaList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of Quotas. + Items []Quota `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// QuotaSpec is the specification of a Quota. +type QuotaSpec struct { + // ClusterLifetimeDays is the lifetime of a Shoot cluster in days before it will be terminated automatically. + // +optional + ClusterLifetimeDays *int32 `json:"clusterLifetimeDays,omitempty" protobuf:"varint,1,opt,name=clusterLifetimeDays"` + // Metrics is a list of resources which will be put under constraints. + Metrics corev1.ResourceList `json:"metrics" protobuf:"bytes,2,rep,name=metrics,casttype=k8s.io/api/core/v1.ResourceList,castkey=k8s.io/api/core/v1.ResourceName"` + // Scope is the scope of the Quota object, either 'project', 'secret' or 'workloadidentity'. This field is immutable. + Scope corev1.ObjectReference `json:"scope" protobuf:"bytes,3,opt,name=scope"` // TODO: When graduating the API to v1 consider reworking this field as described in https://github.com/gardener/gardener/issues/9773#issuecomment-2293340267 +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_secretbinding.go b/api/external/gardener/pkg/apis/core/v1beta1/types_secretbinding.go new file mode 100644 index 0000000..c8e12f4 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_secretbinding.go @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SecretBinding represents a binding to a secret in the same or another namespace. +type SecretBinding struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // SecretRef is a reference to a secret object in the same or another namespace. + // This field is immutable. + SecretRef corev1.SecretReference `json:"secretRef" protobuf:"bytes,2,opt,name=secretRef"` + // Quotas is a list of references to Quota objects in the same or another namespace. + // This field is immutable. + // +optional + Quotas []corev1.ObjectReference `json:"quotas,omitempty" protobuf:"bytes,3,rep,name=quotas"` + // Provider defines the provider type of the SecretBinding. + // This field is immutable. + // +optional + Provider *SecretBindingProvider `json:"provider,omitempty" protobuf:"bytes,4,opt,name=provider"` +} + +// SecretBindingProvider defines the provider type of the SecretBinding. +type SecretBindingProvider struct { + // Type is the type of the provider. + // + // For backwards compatibility, the field can contain multiple providers separated by a comma. + // However the usage of single SecretBinding (hence Secret) for different cloud providers is strongly discouraged. + Type string `json:"type" protobuf:"bytes,1,opt,name=type"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SecretBindingList is a collection of SecretBindings. +type SecretBindingList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of SecretBindings. + Items []SecretBinding `json:"items" protobuf:"bytes,2,rep,name=items"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_seed.go b/api/external/gardener/pkg/apis/core/v1beta1/types_seed.go new file mode 100644 index 0000000..2f0a5a5 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_seed.go @@ -0,0 +1,438 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Seed represents an installation request for an external controller. +type Seed struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Spec contains the specification of this installation. + Spec SeedSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + // Status contains the status of this installation. + Status SeedStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// SeedList is a collection of Seeds. +type SeedList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of Seeds. + Items []Seed `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// SeedTemplate is a template for creating a Seed object. +type SeedTemplate struct { + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the desired behavior of the Seed. + // +optional + Spec SeedSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// SeedSpec is the specification of a Seed. +type SeedSpec struct { + // Backup holds the object store configuration for the backups of shoot (currently only etcd). + // If it is not specified, then there won't be any backups taken for shoots associated with this seed. + // If backup field is present in seed, then backups of the etcd from shoot control plane will be stored + // under the configured object store. + // +optional + Backup *SeedBackup `json:"backup,omitempty" protobuf:"bytes,1,opt,name=backup"` + // DNS contains DNS-relevant information about this seed cluster. + DNS SeedDNS `json:"dns" protobuf:"bytes,2,opt,name=dns"` + // Networks defines the pod, service and worker network of the Seed cluster. + Networks SeedNetworks `json:"networks" protobuf:"bytes,3,opt,name=networks"` + // Provider defines the provider type and region for this Seed cluster. + Provider SeedProvider `json:"provider" protobuf:"bytes,4,opt,name=provider"` + + // SecretRef is tombstoned to show why 5 is reserved protobuf tag. + // SecretRef *corev1.SecretReference `json:"secretRef,omitempty" protobuf:"bytes,5,opt,name=secretRef"` + + // Taints describes taints on the seed. + // +optional + Taints []SeedTaint `json:"taints,omitempty" protobuf:"bytes,6,rep,name=taints"` + // Volume contains settings for persistentvolumes created in the seed cluster. + // +optional + Volume *SeedVolume `json:"volume,omitempty" protobuf:"bytes,7,opt,name=volume"` + // Settings contains certain settings for this seed cluster. + // +optional + Settings *SeedSettings `json:"settings,omitempty" protobuf:"bytes,8,opt,name=settings"` + // Ingress configures Ingress specific settings of the Seed cluster. This field is immutable. + // +optional + Ingress *Ingress `json:"ingress,omitempty" protobuf:"bytes,9,opt,name=ingress"` + // AccessRestrictions describe a list of access restrictions for this seed cluster. + // +optional + AccessRestrictions []AccessRestriction `json:"accessRestrictions,omitempty" protobuf:"bytes,10,rep,name=accessRestrictions"` +} + +// SeedStatus is the status of a Seed. +type SeedStatus struct { + // Gardener holds information about the Gardener which last acted on the Shoot. + // +optional + Gardener *Gardener `json:"gardener,omitempty" protobuf:"bytes,1,opt,name=gardener"` + // KubernetesVersion is the Kubernetes version of the seed cluster. + // +optional + KubernetesVersion *string `json:"kubernetesVersion,omitempty" protobuf:"bytes,2,opt,name=kubernetesVersion"` + // Conditions represents the latest available observations of a Seed's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +optional + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,3,rep,name=conditions"` + // ObservedGeneration is the most recent generation observed for this Seed. It corresponds to the + // Seed's generation, which is updated on mutation by the API Server. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,4,opt,name=observedGeneration"` + // ClusterIdentity is the identity of the Seed cluster. This field is immutable. + // +optional + ClusterIdentity *string `json:"clusterIdentity,omitempty" protobuf:"bytes,5,opt,name=clusterIdentity"` + // Capacity represents the total resources of a seed. + // +optional + Capacity corev1.ResourceList `json:"capacity,omitempty" protobuf:"bytes,6,rep,name=capacity"` + // Allocatable represents the resources of a seed that are available for scheduling. + // Defaults to Capacity. + // +optional + Allocatable corev1.ResourceList `json:"allocatable,omitempty" protobuf:"bytes,7,rep,name=allocatable"` + // ClientCertificateExpirationTimestamp is the timestamp at which gardenlet's client certificate expires. + // +optional + ClientCertificateExpirationTimestamp *metav1.Time `json:"clientCertificateExpirationTimestamp,omitempty" protobuf:"bytes,8,opt,name=clientCertificateExpirationTimestamp"` + // LastOperation holds information about the last operation on the Seed. + // +optional + LastOperation *LastOperation `json:"lastOperation,omitempty" protobuf:"bytes,9,opt,name=lastOperation"` +} + +// SeedBackup contains the object store configuration for backups for shoot (currently only etcd). +type SeedBackup struct { + // Provider is a provider name. This field is immutable. + Provider string `json:"provider" protobuf:"bytes,1,opt,name=provider"` + // ProviderConfig is the configuration passed to BackupBucket resource. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,2,opt,name=providerConfig"` + // Region is a region name. This field is immutable. + // +optional + Region *string `json:"region,omitempty" protobuf:"bytes,3,opt,name=region"` + // SecretRef is a reference to a Secret object containing the cloud provider credentials for + // the object store where backups should be stored. It should have enough privileges to manipulate + // the objects as well as buckets. + SecretRef corev1.SecretReference `json:"secretRef" protobuf:"bytes,4,opt,name=secretRef"` +} + +// SeedDNS contains DNS-relevant information about this seed cluster. +type SeedDNS struct { + // IngressDomain is tombstoned to show why 1 is reserved protobuf tag. + // IngressDomain *string `json:"ingressDomain,omitempty" protobuf:"bytes,1,opt,name=ingressDomain"` + + // Provider configures a DNSProvider + // +optional + Provider *SeedDNSProvider `json:"provider,omitempty" protobuf:"bytes,2,opt,name=provider"` +} + +// SeedDNSProvider configures a DNSProvider for Seeds +type SeedDNSProvider struct { + // Type describes the type of the dns-provider, for example `aws-route53` + Type string `json:"type" protobuf:"bytes,1,opt,name=type"` + // SecretRef is a reference to a Secret object containing cloud provider credentials used for registering external domains. + SecretRef corev1.SecretReference `json:"secretRef" protobuf:"bytes,2,opt,name=secretRef"` + + // Domains is tombstoned to show why 3 is reserved protobuf tag. + // Domains *DNSIncludeExclude `json:"domains,omitempty" protobuf:"bytes,3,opt,name=domains"` + + // Zones is tombstoned to show why 4 is reserved protobuf tag. + // Zones *DNSIncludeExclude `json:"zones,omitempty" protobuf:"bytes,4,opt,name=zones"` +} + +// Ingress configures the Ingress specific settings of the cluster +type Ingress struct { + // Domain specifies the IngressDomain of the cluster pointing to the ingress controller endpoint. It will be used + // to construct ingress URLs for system applications running in Shoot/Garden clusters. Once set this field is immutable. + // +kubebuilder:validation:MaxLength=253 + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // +kubebuilder:validation:Pattern="^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$" + Domain string `json:"domain" protobuf:"bytes,1,name=domain"` + // Controller configures a Gardener managed Ingress Controller listening on the ingressDomain + Controller IngressController `json:"controller" protobuf:"bytes,2,name=controller"` +} + +// IngressController enables a Gardener managed Ingress Controller listening on the ingressDomain +type IngressController struct { + // Kind defines which kind of IngressController to use. At the moment only `nginx` is supported + // +kubebuilder:validation:Enum="nginx" + Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"` + // ProviderConfig specifies infrastructure specific configuration for the ingressController + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,2,opt,name=providerConfig"` +} + +// SeedNetworks contains CIDRs for the pod, service and node networks of a Kubernetes cluster. +type SeedNetworks struct { + // Nodes is the CIDR of the node network. This field is immutable. + // +optional + Nodes *string `json:"nodes,omitempty" protobuf:"bytes,1,opt,name=nodes"` + // Pods is the CIDR of the pod network. This field is immutable. + Pods string `json:"pods" protobuf:"bytes,2,opt,name=pods"` + // Services is the CIDR of the service network. This field is immutable. + Services string `json:"services" protobuf:"bytes,3,opt,name=services"` + // ShootDefaults contains the default networks CIDRs for shoots. + // +optional + ShootDefaults *ShootNetworks `json:"shootDefaults,omitempty" protobuf:"bytes,4,opt,name=shootDefaults"` + // BlockCIDRs is a list of network addresses that should be blocked for shoot control plane components running + // in the seed cluster. + // +optional + BlockCIDRs []string `json:"blockCIDRs,omitempty" protobuf:"bytes,5,rep,name=blockCIDRs"` + // IPFamilies specifies the IP protocol versions to use for seed networking. This field is immutable. + // See https://github.com/gardener/gardener/blob/master/docs/development/ipv6.md. + // Defaults to ["IPv4"]. + // +optional + IPFamilies []IPFamily `json:"ipFamilies,omitempty" protobuf:"bytes,6,rep,name=ipFamilies,casttype=IPFamily"` +} + +// ShootNetworks contains the default networks CIDRs for shoots. +type ShootNetworks struct { + // Pods is the CIDR of the pod network. + // +optional + Pods *string `json:"pods,omitempty" protobuf:"bytes,1,opt,name=pods"` + // Services is the CIDR of the service network. + // +optional + Services *string `json:"services,omitempty" protobuf:"bytes,2,opt,name=services"` +} + +// SeedProvider defines the provider-specific information of this Seed cluster. +type SeedProvider struct { + // Type is the name of the provider. + Type string `json:"type" protobuf:"bytes,1,opt,name=type"` + // ProviderConfig is the configuration passed to Seed resource. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,2,opt,name=providerConfig"` + // Region is a name of a region. + Region string `json:"region" protobuf:"bytes,3,opt,name=region"` + // Zones is the list of availability zones the seed cluster is deployed to. + // +optional + Zones []string `json:"zones,omitempty" protobuf:"bytes,4,rep,name=zones"` +} + +// SeedSettings contains certain settings for this seed cluster. +type SeedSettings struct { + // ExcessCapacityReservation controls the excess capacity reservation for shoot control planes in the seed. + // +optional + ExcessCapacityReservation *SeedSettingExcessCapacityReservation `json:"excessCapacityReservation,omitempty" protobuf:"bytes,1,opt,name=excessCapacityReservation"` + // Scheduling controls settings for scheduling decisions for the seed. + // +optional + Scheduling *SeedSettingScheduling `json:"scheduling,omitempty" protobuf:"bytes,2,opt,name=scheduling"` + + // ShootDNS is tombstoned to show why 3 is reserved protobuf tag. + // ShootDNS *SeedSettingShootDNS `json:"shootDNS,omitempty" protobuf:"bytes,3,opt,name=shootDNS"` + + // LoadBalancerServices controls certain settings for services of type load balancer that are created in the seed. + // +optional + LoadBalancerServices *SeedSettingLoadBalancerServices `json:"loadBalancerServices,omitempty" protobuf:"bytes,4,opt,name=loadBalancerServices"` + // VerticalPodAutoscaler controls certain settings for the vertical pod autoscaler components deployed in the seed. + // +optional + VerticalPodAutoscaler *SeedSettingVerticalPodAutoscaler `json:"verticalPodAutoscaler,omitempty" protobuf:"bytes,5,opt,name=verticalPodAutoscaler"` + + // OwnerChecks is tombstoned to show why 6 is reserved protobuf tag. + // OwnerChecks *SeedSettingOwnerChecks `json:"ownerChecks,omitempty" protobuf:"bytes,6,opt,name=ownerChecks"` + + // DependencyWatchdog controls certain settings for the dependency-watchdog components deployed in the seed. + // +optional + DependencyWatchdog *SeedSettingDependencyWatchdog `json:"dependencyWatchdog,omitempty" protobuf:"bytes,7,opt,name=dependencyWatchdog"` + // TopologyAwareRouting controls certain settings for topology-aware traffic routing in the seed. + // See https://github.com/gardener/gardener/blob/master/docs/operations/topology_aware_routing.md. + // +optional + TopologyAwareRouting *SeedSettingTopologyAwareRouting `json:"topologyAwareRouting,omitempty" protobuf:"bytes,8,opt,name=topologyAwareRouting"` +} + +// SeedSettingExcessCapacityReservation controls the excess capacity reservation for shoot control planes in the seed. +type SeedSettingExcessCapacityReservation struct { + // Enabled controls whether the default excess capacity reservation should be enabled. When not specified, the functionality is enabled. + // +optional + Enabled *bool `json:"enabled,omitempty" protobuf:"bytes,1,opt,name=enabled"` + // Configs configures excess capacity reservation deployments for shoot control planes in the seed. + // +optional + Configs []SeedSettingExcessCapacityReservationConfig `json:"configs,omitempty" protobuf:"bytes,2,rep,name=configs"` +} + +// SeedSettingExcessCapacityReservationConfig configures excess capacity reservation deployments for shoot control planes in the seed. +type SeedSettingExcessCapacityReservationConfig struct { + // Resources specify the resource requests and limits of the excess-capacity-reservation pod. + Resources corev1.ResourceList `json:"resources" protobuf:"bytes,1,rep,name=resources,casttype=k8s.io/api/core/v1.ResourceList,castkey=k8s.io/api/core/v1.ResourceName"` + // NodeSelector specifies the node where the excess-capacity-reservation pod should run. + // +optional + NodeSelector map[string]string `json:"nodeSelector,omitempty" protobuf:"bytes,2,rep,name=nodeSelector"` + // Tolerations specify the tolerations for the the excess-capacity-reservation pod. + // +optional + Tolerations []corev1.Toleration `json:"tolerations,omitempty" protobuf:"bytes,3,rep,name=tolerations"` +} + +// SeedSettingScheduling controls settings for scheduling decisions for the seed. +type SeedSettingScheduling struct { + // Visible controls whether the gardener-scheduler shall consider this seed when scheduling shoots. Invisible seeds + // are not considered by the scheduler. + Visible bool `json:"visible" protobuf:"bytes,1,opt,name=visible"` +} + +// SeedSettingLoadBalancerServices controls certain settings for services of type load balancer that are created in the +// seed. +type SeedSettingLoadBalancerServices struct { + // Annotations is a map of annotations that will be injected/merged into every load balancer service object. + // +optional + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,1,rep,name=annotations"` + // ExternalTrafficPolicy describes how nodes distribute service traffic they + // receive on one of the service's "externally-facing" addresses. + // Defaults to "Cluster". + // +optional + ExternalTrafficPolicy *corev1.ServiceExternalTrafficPolicy `json:"externalTrafficPolicy,omitempty" protobuf:"bytes,2,opt,name=externalTrafficPolicy"` + // Zones controls settings, which are specific to the single-zone load balancers in a multi-zonal setup. + // Can be empty for single-zone seeds. Each specified zone has to relate to one of the zones in seed.spec.provider.zones. + // +optional + Zones []SeedSettingLoadBalancerServicesZones `json:"zones,omitempty" protobuf:"bytes,3,rep,name=zones"` + // ProxyProtocol controls whether ProxyProtocol is (optionally) allowed for the load balancer services. + // Defaults to nil, which is equivalent to not allowing ProxyProtocol. + // +optional + ProxyProtocol *LoadBalancerServicesProxyProtocol `json:"proxyProtocol,omitempty" protobuf:"bytes,4,opt,name=proxyProtocol"` +} + +// SeedSettingLoadBalancerServicesZones controls settings, which are specific to the single-zone load balancers in a +// multi-zonal setup. +type SeedSettingLoadBalancerServicesZones struct { + // Name is the name of the zone as specified in seed.spec.provider.zones. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Annotations is a map of annotations that will be injected/merged into the zone-specific load balancer service object. + // +optional + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,2,rep,name=annotations"` + // ExternalTrafficPolicy describes how nodes distribute service traffic they + // receive on one of the service's "externally-facing" addresses. + // Defaults to "Cluster". + // +optional + ExternalTrafficPolicy *corev1.ServiceExternalTrafficPolicy `json:"externalTrafficPolicy,omitempty" protobuf:"bytes,3,opt,name=externalTrafficPolicy"` + // ProxyProtocol controls whether ProxyProtocol is (optionally) allowed for the load balancer services. + // Defaults to nil, which is equivalent to not allowing ProxyProtocol. + // +optional + ProxyProtocol *LoadBalancerServicesProxyProtocol `json:"proxyProtocol,omitempty" protobuf:"bytes,4,opt,name=proxyProtocol"` +} + +// LoadBalancerServicesProxyProtocol controls whether ProxyProtocol is (optionally) allowed for the load balancer services. +type LoadBalancerServicesProxyProtocol struct { + // Allowed controls whether the ProxyProtocol is optionally allowed for the load balancer services. + // This should only be enabled if the load balancer services are already using ProxyProtocol or will be reconfigured to use it soon. + // Until the load balancers are configured with ProxyProtocol, enabling this setting may allow clients to spoof their source IP addresses. + // The option allows a migration from non-ProxyProtocol to ProxyProtocol without downtime (depending on the infrastructure). + // Defaults to false. + Allowed bool `json:"allowed" protobuf:"bytes,1,opt,name=allowed"` +} + +// SeedSettingVerticalPodAutoscaler controls certain settings for the vertical pod autoscaler components deployed in the +// seed. +type SeedSettingVerticalPodAutoscaler struct { + // Enabled controls whether the VPA components shall be deployed into the garden namespace in the seed cluster. It + // is enabled by default because Gardener heavily relies on a VPA being deployed. You should only disable this if + // your seed cluster already has another, manually/custom managed VPA deployment. + Enabled bool `json:"enabled" protobuf:"bytes,1,opt,name=enabled"` +} + +// SeedSettingDependencyWatchdog controls the dependency-watchdog settings for the seed. +type SeedSettingDependencyWatchdog struct { + // Endpoint is tombstoned to show why 1 is reserved protobuf tag. + // Endpoint *SeedSettingDependencyWatchdogEndpoint `json:"endpoint,omitempty" protobuf:"bytes,1,opt,name=endpoint"` + // Probe is tombstoned to show why 2 is reserved protobuf tag. + // Probe *SeedSettingDependencyWatchdogProbe `json:"probe,omitempty" protobuf:"bytes,2,opt,name=probe"` + + // Weeder controls the weeder settings for the dependency-watchdog for the seed. + // +optional + Weeder *SeedSettingDependencyWatchdogWeeder `json:"weeder,omitempty" protobuf:"bytes,3,opt,name=weeder"` + // Prober controls the prober settings for the dependency-watchdog for the seed. + // +optional + Prober *SeedSettingDependencyWatchdogProber `json:"prober,omitempty" protobuf:"bytes,4,opt,name=prober"` +} + +// SeedSettingDependencyWatchdogWeeder controls the weeder settings for the dependency-watchdog for the seed. +type SeedSettingDependencyWatchdogWeeder struct { + // Enabled controls whether the endpoint controller(weeder) of the dependency-watchdog should be enabled. This controller + // helps to alleviate the delay where control plane components remain unavailable by finding the respective pods in + // CrashLoopBackoff status and restarting them once their dependants become ready and available again. + Enabled bool `json:"enabled" protobuf:"bytes,1,opt,name=enabled"` +} + +// SeedSettingDependencyWatchdogProber controls the prober settings for the dependency-watchdog for the seed. +type SeedSettingDependencyWatchdogProber struct { + // Enabled controls whether the probe controller(prober) of the dependency-watchdog should be enabled. This controller + // scales down the kube-controller-manager, machine-controller-manager and cluster-autoscaler of shoot clusters in case their respective kube-apiserver is not + // reachable via its external ingress in order to avoid melt-down situations. + Enabled bool `json:"enabled" protobuf:"bytes,1,opt,name=enabled"` +} + +// SeedSettingTopologyAwareRouting controls certain settings for topology-aware traffic routing in the seed. +// See https://github.com/gardener/gardener/blob/master/docs/operations/topology_aware_routing.md. +type SeedSettingTopologyAwareRouting struct { + // Enabled controls whether certain Services deployed in the seed cluster should be topology-aware. + // These Services are etcd-main-client, etcd-events-client, kube-apiserver, gardener-resource-manager and vpa-webhook. + Enabled bool `json:"enabled" protobuf:"bytes,1,opt,name=enabled"` +} + +// SeedTaint describes a taint on a seed. +type SeedTaint struct { + // Key is the taint key to be applied to a seed. + Key string `json:"key" protobuf:"bytes,1,opt,name=key"` + // Value is the taint value corresponding to the taint key. + // +optional + Value *string `json:"value,omitempty" protobuf:"bytes,2,opt,name=value"` +} + +const ( + // SeedTaintProtected is a constant for a taint key on a seed that marks it as protected. Protected seeds + // may only be used by shoots in the `garden` namespace. + SeedTaintProtected = "seed.gardener.cloud/protected" +) + +// SeedVolume contains settings for persistentvolumes created in the seed cluster. +type SeedVolume struct { + // MinimumSize defines the minimum size that should be used for PVCs in the seed. + // +optional + MinimumSize *resource.Quantity `json:"minimumSize,omitempty" protobuf:"bytes,1,opt,name=minimumSize"` + // Providers is a list of storage class provisioner types for the seed. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + Providers []SeedVolumeProvider `json:"providers,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=providers"` +} + +// SeedVolumeProvider is a storage class provisioner type. +type SeedVolumeProvider struct { + // Purpose is the purpose of this provider. + Purpose string `json:"purpose" protobuf:"bytes,1,opt,name=purpose"` + // Name is the name of the storage class provisioner type. + Name string `json:"name" protobuf:"bytes,2,opt,name=name"` +} + +const ( + // SeedBackupBucketsReady is a constant for a condition type indicating that associated BackupBuckets are ready. + SeedBackupBucketsReady ConditionType = "BackupBucketsReady" + // SeedExtensionsReady is a constant for a condition type indicating that the extensions are ready. + SeedExtensionsReady ConditionType = "ExtensionsReady" + // SeedGardenletReady is a constant for a condition type indicating that the Gardenlet is ready. + SeedGardenletReady ConditionType = "GardenletReady" + // SeedSystemComponentsHealthy is a constant for a condition type indicating the system components health. + SeedSystemComponentsHealthy ConditionType = "SeedSystemComponentsHealthy" +) + +// Resource constants for Gardener object types +const ( + // ResourceShoots is a resource constant for the number of shoots. + ResourceShoots corev1.ResourceName = "shoots" +) diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_shoot.go b/api/external/gardener/pkg/apis/core/v1beta1/types_shoot.go new file mode 100644 index 0000000..5defed2 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_shoot.go @@ -0,0 +1,1910 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + "time" + + autoscalingv1 "k8s.io/api/autoscaling/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/intstr" + + v1beta1constants "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1/constants" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// Shoot represents a Shoot cluster created and managed by Gardener. +type Shoot struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the Shoot cluster. + // If the object's deletion timestamp is set, this field is immutable. + // +optional + Spec ShootSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` + // Most recently observed status of the Shoot cluster. + // +optional + Status ShootStatus `json:"status,omitempty" protobuf:"bytes,3,opt,name=status"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ShootList is a list of Shoot objects. +type ShootList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of Shoots. + Items []Shoot `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// ShootTemplate is a template for creating a Shoot object. +type ShootTemplate struct { + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the desired behavior of the Shoot. + // +optional + Spec ShootSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// ShootSpec is the specification of a Shoot. +type ShootSpec struct { + // Addons contains information about enabled/disabled addons and their configuration. + // +optional + Addons *Addons `json:"addons,omitempty" protobuf:"bytes,1,opt,name=addons"` + // CloudProfileName is a name of a CloudProfile object. + // Deprecated: This field will be removed in a future version of Gardener. Use `CloudProfile` instead. + // Until removed, this field is synced with the `CloudProfile` field. + // +optional + CloudProfileName *string `json:"cloudProfileName,omitempty" protobuf:"bytes,2,opt,name=cloudProfileName"` + // DNS contains information about the DNS settings of the Shoot. + // +optional + DNS *DNS `json:"dns,omitempty" protobuf:"bytes,3,opt,name=dns"` + // Extensions contain type and provider information for Shoot extensions. + // +optional + Extensions []Extension `json:"extensions,omitempty" protobuf:"bytes,4,rep,name=extensions"` + // Hibernation contains information whether the Shoot is suspended or not. + // +optional + Hibernation *Hibernation `json:"hibernation,omitempty" protobuf:"bytes,5,opt,name=hibernation"` + // Kubernetes contains the version and configuration settings of the control plane components. + Kubernetes Kubernetes `json:"kubernetes" protobuf:"bytes,6,opt,name=kubernetes"` + // Networking contains information about cluster networking such as CNI Plugin type, CIDRs, ...etc. + // +optional + Networking *Networking `json:"networking,omitempty" protobuf:"bytes,7,opt,name=networking"` + // Maintenance contains information about the time window for maintenance operations and which + // operations should be performed. + // +optional + Maintenance *Maintenance `json:"maintenance,omitempty" protobuf:"bytes,8,opt,name=maintenance"` + // Monitoring contains information about custom monitoring configurations for the shoot. + // +optional + Monitoring *Monitoring `json:"monitoring,omitempty" protobuf:"bytes,9,opt,name=monitoring"` + // Provider contains all provider-specific and provider-relevant information. + Provider Provider `json:"provider" protobuf:"bytes,10,opt,name=provider"` + // Purpose is the purpose class for this cluster. + // +optional + Purpose *ShootPurpose `json:"purpose,omitempty" protobuf:"bytes,11,opt,name=purpose,casttype=ShootPurpose"` + // Region is a name of a region. This field is immutable. + Region string `json:"region" protobuf:"bytes,12,opt,name=region"` + // SecretBindingName is the name of a SecretBinding that has a reference to the provider secret. + // The credentials inside the provider secret will be used to create the shoot in the respective account. + // The field is mutually exclusive with CredentialsBindingName. + // This field is immutable. + // +optional + SecretBindingName *string `json:"secretBindingName,omitempty" protobuf:"bytes,13,opt,name=secretBindingName"` + // SeedName is the name of the seed cluster that runs the control plane of the Shoot. + // +optional + SeedName *string `json:"seedName,omitempty" protobuf:"bytes,14,opt,name=seedName"` + // SeedSelector is an optional selector which must match a seed's labels for the shoot to be scheduled on that seed. + // +optional + SeedSelector *SeedSelector `json:"seedSelector,omitempty" protobuf:"bytes,15,opt,name=seedSelector"` + // Resources holds a list of named resource references that can be referred to in extension configs by their names. + // +optional + Resources []NamedResourceReference `json:"resources,omitempty" protobuf:"bytes,16,rep,name=resources"` + // Tolerations contains the tolerations for taints on seed clusters. + // +patchMergeKey=key + // +patchStrategy=merge + // +optional + Tolerations []Toleration `json:"tolerations,omitempty" patchStrategy:"merge" patchMergeKey:"key" protobuf:"bytes,17,rep,name=tolerations"` + // ExposureClassName is the optional name of an exposure class to apply a control plane endpoint exposure strategy. + // This field is immutable. + // +optional + ExposureClassName *string `json:"exposureClassName,omitempty" protobuf:"bytes,18,opt,name=exposureClassName"` + // SystemComponents contains the settings of system components in the control or data plane of the Shoot cluster. + // +optional + SystemComponents *SystemComponents `json:"systemComponents,omitempty" protobuf:"bytes,19,opt,name=systemComponents"` + // ControlPlane contains general settings for the control plane of the shoot. + // +optional + ControlPlane *ControlPlane `json:"controlPlane,omitempty" protobuf:"bytes,20,opt,name=controlPlane"` + // SchedulerName is the name of the responsible scheduler which schedules the shoot. + // If not specified, the default scheduler takes over. + // This field is immutable. + // +optional + SchedulerName *string `json:"schedulerName,omitempty" protobuf:"bytes,21,opt,name=schedulerName"` + // CloudProfile contains a reference to a CloudProfile or a NamespacedCloudProfile. + // +optional + CloudProfile *CloudProfileReference `json:"cloudProfile,omitempty" protobuf:"bytes,22,opt,name=cloudProfile"` + // CredentialsBindingName is the name of a CredentialsBinding that has a reference to the provider credentials. + // The credentials will be used to create the shoot in the respective account. The field is mutually exclusive with SecretBindingName. + // +optional + CredentialsBindingName *string `json:"credentialsBindingName,omitempty" protobuf:"bytes,23,opt,name=credentialsBindingName"` + // AccessRestrictions describe a list of access restrictions for this shoot cluster. + // +optional + AccessRestrictions []AccessRestrictionWithOptions `json:"accessRestrictions,omitempty" protobuf:"bytes,24,rep,name=accessRestrictions"` +} + +// ShootStatus holds the most recently observed status of the Shoot cluster. +type ShootStatus struct { + // Conditions represents the latest available observations of a Shoots's current state. + // +patchMergeKey=type + // +patchStrategy=merge + // +optional + Conditions []Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,1,rep,name=conditions"` + // Constraints represents conditions of a Shoot's current state that constraint some operations on it. + // +patchMergeKey=type + // +patchStrategy=merge + // +optional + Constraints []Condition `json:"constraints,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,rep,name=constraints"` + // Gardener holds information about the Gardener which last acted on the Shoot. + Gardener Gardener `json:"gardener" protobuf:"bytes,3,opt,name=gardener"` + // IsHibernated indicates whether the Shoot is currently hibernated. + IsHibernated bool `json:"hibernated" protobuf:"varint,4,opt,name=hibernated"` + // LastOperation holds information about the last operation on the Shoot. + // +optional + LastOperation *LastOperation `json:"lastOperation,omitempty" protobuf:"bytes,5,opt,name=lastOperation"` + // LastErrors holds information about the last occurred error(s) during an operation. + // +optional + LastErrors []LastError `json:"lastErrors,omitempty" protobuf:"bytes,6,rep,name=lastErrors"` + // ObservedGeneration is the most recent generation observed for this Shoot. It corresponds to the + // Shoot's generation, which is updated on mutation by the API Server. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty" protobuf:"varint,7,opt,name=observedGeneration"` + // RetryCycleStartTime is the start time of the last retry cycle (used to determine how often an operation + // must be retried until we give up). + // +optional + RetryCycleStartTime *metav1.Time `json:"retryCycleStartTime,omitempty" protobuf:"bytes,8,opt,name=retryCycleStartTime"` + // SeedName is the name of the seed cluster that runs the control plane of the Shoot. This value is only written + // after a successful create/reconcile operation. It will be used when control planes are moved between Seeds. + // +optional + SeedName *string `json:"seedName,omitempty" protobuf:"bytes,9,opt,name=seedName"` + // TechnicalID is the name that is used for creating the Seed namespace, the infrastructure resources, and + // basically everything that is related to this particular Shoot. This field is immutable. + TechnicalID string `json:"technicalID" protobuf:"bytes,10,opt,name=technicalID"` + // UID is a unique identifier for the Shoot cluster to avoid portability between Kubernetes clusters. + // It is used to compute unique hashes. This field is immutable. + UID types.UID `json:"uid" protobuf:"bytes,11,opt,name=uid,casttype=k8s.io/apimachinery/pkg/types.UID"` + // ClusterIdentity is the identity of the Shoot cluster. This field is immutable. + // +optional + ClusterIdentity *string `json:"clusterIdentity,omitempty" protobuf:"bytes,12,opt,name=clusterIdentity"` + // List of addresses that are relevant to the shoot. + // These include the Kube API server address and also the service account issuer. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + AdvertisedAddresses []ShootAdvertisedAddress `json:"advertisedAddresses,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,13,rep,name=advertisedAddresses"` + // MigrationStartTime is the time when a migration to a different seed was initiated. + // +optional + MigrationStartTime *metav1.Time `json:"migrationStartTime,omitempty" protobuf:"bytes,14,opt,name=migrationStartTime"` + // Credentials contains information about the shoot credentials. + // +optional + Credentials *ShootCredentials `json:"credentials,omitempty" protobuf:"bytes,15,opt,name=credentials"` + // LastHibernationTriggerTime indicates the last time when the hibernation controller + // managed to change the hibernation settings of the cluster + // +optional + LastHibernationTriggerTime *metav1.Time `json:"lastHibernationTriggerTime,omitempty" protobuf:"bytes,16,opt,name=lastHibernationTriggerTime"` + // LastMaintenance holds information about the last maintenance operations on the Shoot. + // +optional + LastMaintenance *LastMaintenance `json:"lastMaintenance,omitempty" protobuf:"bytes,17,opt,name=lastMaintenance"` + // EncryptedResources is the list of resources in the Shoot which are currently encrypted. + // Secrets are encrypted by default and are not part of the list. + // See https://github.com/gardener/gardener/blob/master/docs/usage/security/etcd_encryption_config.md for more details. + // +optional + EncryptedResources []string `json:"encryptedResources,omitempty" protobuf:"bytes,18,rep,name=encryptedResources"` + // Networking contains information about cluster networking such as CIDRs. + // +optional + Networking *NetworkingStatus `json:"networking,omitempty" protobuf:"bytes,19,opt,name=networking"` +} + +// LastMaintenance holds information about a maintenance operation on the Shoot. +type LastMaintenance struct { + // A human-readable message containing details about the operations performed in the last maintenance. + Description string `json:"description" protobuf:"bytes,1,opt,name=description"` + // TriggeredTime is the time when maintenance was triggered. + TriggeredTime metav1.Time `json:"triggeredTime" protobuf:"bytes,2,opt,name=triggeredTime"` + // Status of the last maintenance operation, one of Processing, Succeeded, Error. + State LastOperationState `json:"state" protobuf:"bytes,3,opt,name=state,casttype=LastOperationState"` + // FailureReason holds the information about the last maintenance operation failure reason. + // +optional + FailureReason *string `json:"failureReason,omitempty" protobuf:"bytes,4,opt,name=failureReason"` +} + +// NetworkingStatus contains information about cluster networking such as CIDRs. +type NetworkingStatus struct { + // Pods are the CIDRs of the pod network. + // +optional + Pods []string `json:"pods,omitempty" protobuf:"bytes,1,rep,name=pods"` + // Nodes are the CIDRs of the node network. + // +optional + Nodes []string `json:"nodes,omitempty" protobuf:"bytes,2,rep,name=nodes"` + // Services are the CIDRs of the service network. + // +optional + Services []string `json:"services,omitempty" protobuf:"bytes,3,rep,name=services"` + // EgressCIDRs is a list of CIDRs used by the shoot as the source IP for egress traffic as reported by the used + // Infrastructure extension controller. For certain environments the egress IPs may not be stable in which case the + // extension controller may opt to not populate this field. + // +optional + EgressCIDRs []string `json:"egressCIDRs,omitempty" protobuf:"bytes,4,rep,name=egressCIDRs"` +} + +// ShootCredentials contains information about the shoot credentials. +type ShootCredentials struct { + // Rotation contains information about the credential rotations. + // +optional + Rotation *ShootCredentialsRotation `json:"rotation,omitempty" protobuf:"bytes,1,opt,name=rotation"` +} + +// ShootCredentialsRotation contains information about the rotation of credentials. +type ShootCredentialsRotation struct { + // CertificateAuthorities contains information about the certificate authority credential rotation. + // +optional + CertificateAuthorities *CARotation `json:"certificateAuthorities,omitempty" protobuf:"bytes,1,opt,name=certificateAuthorities"` + // Kubeconfig contains information about the kubeconfig credential rotation. + // +optional + Kubeconfig *ShootKubeconfigRotation `json:"kubeconfig,omitempty" protobuf:"bytes,2,opt,name=kubeconfig"` + // SSHKeypair contains information about the ssh-keypair credential rotation. + // +optional + SSHKeypair *ShootSSHKeypairRotation `json:"sshKeypair,omitempty" protobuf:"bytes,3,opt,name=sshKeypair"` + // Observability contains information about the observability credential rotation. + // +optional + Observability *ObservabilityRotation `json:"observability,omitempty" protobuf:"bytes,4,opt,name=observability"` + // ServiceAccountKey contains information about the service account key credential rotation. + // +optional + ServiceAccountKey *ServiceAccountKeyRotation `json:"serviceAccountKey,omitempty" protobuf:"bytes,5,opt,name=serviceAccountKey"` + // ETCDEncryptionKey contains information about the ETCD encryption key credential rotation. + // +optional + ETCDEncryptionKey *ETCDEncryptionKeyRotation `json:"etcdEncryptionKey,omitempty" protobuf:"bytes,6,opt,name=etcdEncryptionKey"` +} + +// CARotation contains information about the certificate authority credential rotation. +type CARotation struct { + // Phase describes the phase of the certificate authority credential rotation. + Phase CredentialsRotationPhase `json:"phase" protobuf:"bytes,1,opt,name=phase"` + // LastCompletionTime is the most recent time when the certificate authority credential rotation was successfully + // completed. + // +optional + LastCompletionTime *metav1.Time `json:"lastCompletionTime,omitempty" protobuf:"bytes,2,opt,name=lastCompletionTime"` + // LastInitiationTime is the most recent time when the certificate authority credential rotation was initiated. + // +optional + LastInitiationTime *metav1.Time `json:"lastInitiationTime,omitempty" protobuf:"bytes,3,opt,name=lastInitiationTime"` + // LastInitiationFinishedTime is the recent time when the certificate authority credential rotation initiation was + // completed. + // +optional + LastInitiationFinishedTime *metav1.Time `json:"lastInitiationFinishedTime,omitempty" protobuf:"bytes,4,opt,name=lastInitiationFinishedTime"` + // LastCompletionTriggeredTime is the recent time when the certificate authority credential rotation completion was + // triggered. + // +optional + LastCompletionTriggeredTime *metav1.Time `json:"lastCompletionTriggeredTime,omitempty" protobuf:"bytes,5,opt,name=lastCompletionTriggeredTime"` + // PendingWorkersRollouts contains the name of a worker pool and the initiation time of their last rollout due to + // credentials rotation. + // +optional + PendingWorkersRollouts []PendingWorkersRollout `json:"pendingWorkersRollouts,omitempty" protobuf:"bytes,6,rep,name=pendingWorkersRollouts"` +} + +// ShootKubeconfigRotation contains information about the kubeconfig credential rotation. +type ShootKubeconfigRotation struct { + // LastInitiationTime is the most recent time when the kubeconfig credential rotation was initiated. + // +optional + LastInitiationTime *metav1.Time `json:"lastInitiationTime,omitempty" protobuf:"bytes,1,opt,name=lastInitiationTime"` + // LastCompletionTime is the most recent time when the kubeconfig credential rotation was successfully completed. + // +optional + LastCompletionTime *metav1.Time `json:"lastCompletionTime,omitempty" protobuf:"bytes,2,opt,name=lastCompletionTime"` +} + +// ShootSSHKeypairRotation contains information about the ssh-keypair credential rotation. +type ShootSSHKeypairRotation struct { + // LastInitiationTime is the most recent time when the ssh-keypair credential rotation was initiated. + // +optional + LastInitiationTime *metav1.Time `json:"lastInitiationTime,omitempty" protobuf:"bytes,1,opt,name=lastInitiationTime"` + // LastCompletionTime is the most recent time when the ssh-keypair credential rotation was successfully completed. + // +optional + LastCompletionTime *metav1.Time `json:"lastCompletionTime,omitempty" protobuf:"bytes,2,opt,name=lastCompletionTime"` +} + +// ObservabilityRotation contains information about the observability credential rotation. +type ObservabilityRotation struct { + // LastInitiationTime is the most recent time when the observability credential rotation was initiated. + // +optional + LastInitiationTime *metav1.Time `json:"lastInitiationTime,omitempty" protobuf:"bytes,1,opt,name=lastInitiationTime"` + // LastCompletionTime is the most recent time when the observability credential rotation was successfully completed. + // +optional + LastCompletionTime *metav1.Time `json:"lastCompletionTime,omitempty" protobuf:"bytes,2,opt,name=lastCompletionTime"` +} + +// ServiceAccountKeyRotation contains information about the service account key credential rotation. +type ServiceAccountKeyRotation struct { + // Phase describes the phase of the service account key credential rotation. + Phase CredentialsRotationPhase `json:"phase" protobuf:"bytes,1,opt,name=phase"` + // LastCompletionTime is the most recent time when the service account key credential rotation was successfully + // completed. + // +optional + LastCompletionTime *metav1.Time `json:"lastCompletionTime,omitempty" protobuf:"bytes,2,opt,name=lastCompletionTime"` + // LastInitiationTime is the most recent time when the service account key credential rotation was initiated. + // +optional + LastInitiationTime *metav1.Time `json:"lastInitiationTime,omitempty" protobuf:"bytes,3,opt,name=lastInitiationTime"` + // LastInitiationFinishedTime is the recent time when the service account key credential rotation initiation was + // completed. + // +optional + LastInitiationFinishedTime *metav1.Time `json:"lastInitiationFinishedTime,omitempty" protobuf:"bytes,4,opt,name=lastInitiationFinishedTime"` + // LastCompletionTriggeredTime is the recent time when the service account key credential rotation completion was + // triggered. + // +optional + LastCompletionTriggeredTime *metav1.Time `json:"lastCompletionTriggeredTime,omitempty" protobuf:"bytes,5,opt,name=lastCompletionTriggeredTime"` + // PendingWorkersRollouts contains the name of a worker pool and the initiation time of their last rollout due to + // credentials rotation. + // +optional + PendingWorkersRollouts []PendingWorkersRollout `json:"pendingWorkersRollouts,omitempty" protobuf:"bytes,6,rep,name=pendingWorkersRollouts"` +} + +// ETCDEncryptionKeyRotation contains information about the ETCD encryption key credential rotation. +type ETCDEncryptionKeyRotation struct { + // Phase describes the phase of the ETCD encryption key credential rotation. + Phase CredentialsRotationPhase `json:"phase" protobuf:"bytes,1,opt,name=phase"` + // LastCompletionTime is the most recent time when the ETCD encryption key credential rotation was successfully + // completed. + // +optional + LastCompletionTime *metav1.Time `json:"lastCompletionTime,omitempty" protobuf:"bytes,2,opt,name=lastCompletionTime"` + // LastInitiationTime is the most recent time when the ETCD encryption key credential rotation was initiated. + // +optional + LastInitiationTime *metav1.Time `json:"lastInitiationTime,omitempty" protobuf:"bytes,3,opt,name=lastInitiationTime"` + // LastInitiationFinishedTime is the recent time when the ETCD encryption key credential rotation initiation was + // completed. + // +optional + LastInitiationFinishedTime *metav1.Time `json:"lastInitiationFinishedTime,omitempty" protobuf:"bytes,4,opt,name=lastInitiationFinishedTime"` + // LastCompletionTriggeredTime is the recent time when the ETCD encryption key credential rotation completion was + // triggered. + // +optional + LastCompletionTriggeredTime *metav1.Time `json:"lastCompletionTriggeredTime,omitempty" protobuf:"bytes,5,opt,name=lastCompletionTriggeredTime"` +} + +// CredentialsRotationPhase is a string alias. +type CredentialsRotationPhase string + +const ( + // RotationPreparing is a constant for the credentials rotation phase describing that the procedure is being prepared. + RotationPreparing CredentialsRotationPhase = "Preparing" + // RotationPreparingWithoutWorkersRollout is a constant for the credentials rotation phase describing that the + // procedure is being prepared without triggering a worker pool rollout. + RotationPreparingWithoutWorkersRollout CredentialsRotationPhase = "PreparingWithoutWorkersRollout" + // RotationWaitingForWorkersRollout is a constant for the credentials rotation phase describing that the procedure + // was prepared but is still waiting for the workers to roll out. + RotationWaitingForWorkersRollout CredentialsRotationPhase = "WaitingForWorkersRollout" + // RotationPrepared is a constant for the credentials rotation phase describing that the procedure was prepared. + RotationPrepared CredentialsRotationPhase = "Prepared" + // RotationCompleting is a constant for the credentials rotation phase describing that the procedure is being + // completed. + RotationCompleting CredentialsRotationPhase = "Completing" + // RotationCompleted is a constant for the credentials rotation phase describing that the procedure was completed. + RotationCompleted CredentialsRotationPhase = "Completed" +) + +// PendingWorkersRollout contains the name of a worker pool and the initiation time of their last rollout due to +// credentials rotation. +type PendingWorkersRollout struct { + // Name is the name of a worker pool. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // LastInitiationTime is the most recent time when the credential rotation was initiated. + // +optional + LastInitiationTime *metav1.Time `json:"lastInitiationTime,omitempty" protobuf:"bytes,2,opt,name=lastInitiationTime"` +} + +// ShootAdvertisedAddress contains information for the shoot's Kube API server. +type ShootAdvertisedAddress struct { + // Name of the advertised address. e.g. external + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // The URL of the API Server. e.g. https://api.foo.bar or https://1.2.3.4 + URL string `json:"url" protobuf:"bytes,2,opt,name=url"` +} + +// Addons is a collection of configuration for specific addons which are managed by the Gardener. +type Addons struct { + // KubernetesDashboard holds configuration settings for the kubernetes dashboard addon. + // +optional + KubernetesDashboard *KubernetesDashboard `json:"kubernetesDashboard,omitempty" protobuf:"bytes,1,opt,name=kubernetesDashboard"` + // NginxIngress holds configuration settings for the nginx-ingress addon. + // +optional + NginxIngress *NginxIngress `json:"nginxIngress,omitempty" protobuf:"bytes,2,opt,name=nginxIngress"` +} + +// Addon allows enabling or disabling a specific addon and is used to derive from. +type Addon struct { + // Enabled indicates whether the addon is enabled or not. + Enabled bool `json:"enabled" protobuf:"varint,1,opt,name=enabled"` +} + +// KubernetesDashboard describes configuration values for the kubernetes-dashboard addon. +type KubernetesDashboard struct { + Addon `json:",inline" protobuf:"bytes,2,opt,name=addon"` + // AuthenticationMode defines the authentication mode for the kubernetes-dashboard. + // +optional + AuthenticationMode *string `json:"authenticationMode,omitempty" protobuf:"bytes,1,opt,name=authenticationMode"` +} + +const ( + // KubernetesDashboardAuthModeToken uses token-based mode for auth. + KubernetesDashboardAuthModeToken = "token" +) + +// NginxIngress describes configuration values for the nginx-ingress addon. +type NginxIngress struct { + Addon `json:",inline" protobuf:"bytes,1,opt,name=addon"` + // LoadBalancerSourceRanges is list of allowed IP sources for NginxIngress + // +optional + LoadBalancerSourceRanges []string `json:"loadBalancerSourceRanges,omitempty" protobuf:"bytes,2,rep,name=loadBalancerSourceRanges"` + // Config contains custom configuration for the nginx-ingress-controller configuration. + // See https://github.com/kubernetes/ingress-nginx/blob/master/docs/user-guide/nginx-configuration/configmap.md#configuration-options + // +optional + Config map[string]string `json:"config,omitempty" protobuf:"bytes,3,rep,name=config"` + // ExternalTrafficPolicy controls the `.spec.externalTrafficPolicy` value of the load balancer `Service` + // exposing the nginx-ingress. Defaults to `Cluster`. + // +optional + ExternalTrafficPolicy *corev1.ServiceExternalTrafficPolicy `json:"externalTrafficPolicy,omitempty" protobuf:"bytes,4,opt,name=externalTrafficPolicy,casttype=k8s.io/api/core/v1.ServiceExternalTrafficPolicy"` +} + +// ControlPlane holds information about the general settings for the control plane of a shoot. +type ControlPlane struct { + // HighAvailability holds the configuration settings for high availability of the + // control plane of a shoot. + // +optional + HighAvailability *HighAvailability `json:"highAvailability,omitempty" protobuf:"bytes,1,name=highAvailability"` +} + +// DNS holds information about the provider, the hosted zone id and the domain. +type DNS struct { + // Domain is the external available domain of the Shoot cluster. This domain will be written into the + // kubeconfig that is handed out to end-users. This field is immutable. + // +optional + Domain *string `json:"domain,omitempty" protobuf:"bytes,1,opt,name=domain"` + // Providers is a list of DNS providers that shall be enabled for this shoot cluster. Only relevant if + // not a default domain is used. + // + // Deprecated: Configuring multiple DNS providers is deprecated and will be forbidden in a future release. + // Please use the DNS extension provider config (e.g. shoot-dns-service) for additional providers. + // +patchMergeKey=type + // +patchStrategy=merge + // +optional + Providers []DNSProvider `json:"providers,omitempty" patchStrategy:"merge" patchMergeKey:"type" protobuf:"bytes,2,rep,name=providers"` +} + +// TODO(timuthy): Rework the 'DNSProvider' struct and deprecated fields in the scope of https://github.com/gardener/gardener/issues/9176. + +// DNSProvider contains information about a DNS provider. +type DNSProvider struct { + // Domains contains information about which domains shall be included/excluded for this provider. + // + // Deprecated: This field is deprecated and will be removed in a future release. + // Please use the DNS extension provider config (e.g. shoot-dns-service) for additional configuration. + // +optional + Domains *DNSIncludeExclude `json:"domains,omitempty" protobuf:"bytes,1,opt,name=domains"` + // Primary indicates that this DNSProvider is used for shoot related domains. + // + // Deprecated: This field is deprecated and will be removed in a future release. + // Please use the DNS extension provider config (e.g. shoot-dns-service) for additional and non-primary providers. + // +optional + Primary *bool `json:"primary,omitempty" protobuf:"varint,2,opt,name=primary"` + // SecretName is a name of a secret containing credentials for the stated domain and the + // provider. When not specified, the Gardener will use the cloud provider credentials referenced + // by the Shoot and try to find respective credentials there (primary provider only). Specifying this field may override + // this behavior, i.e. forcing the Gardener to only look into the given secret. + // +optional + SecretName *string `json:"secretName,omitempty" protobuf:"bytes,3,opt,name=secretName"` + // Type is the DNS provider type. + // +optional + Type *string `json:"type,omitempty" protobuf:"bytes,4,opt,name=type"` + + // Zones contains information about which hosted zones shall be included/excluded for this provider. + // + // Deprecated: This field is deprecated and will be removed in a future release. + // Please use the DNS extension provider config (e.g. shoot-dns-service) for additional configuration. + // +optional + Zones *DNSIncludeExclude `json:"zones,omitempty" protobuf:"bytes,5,opt,name=zones"` +} + +// DNSIncludeExclude contains information about which domains shall be included/excluded. +type DNSIncludeExclude struct { + // Include is a list of domains that shall be included. + // +optional + Include []string `json:"include,omitempty" protobuf:"bytes,1,rep,name=include"` + // Exclude is a list of domains that shall be excluded. + // +optional + Exclude []string `json:"exclude,omitempty" protobuf:"bytes,2,rep,name=exclude"` +} + +// DefaultDomain is the default value in the Shoot's '.spec.dns.domain' when '.spec.dns.provider' is 'unmanaged' +const DefaultDomain = "cluster.local" + +// Extension contains type and provider information for Shoot extensions. +type Extension struct { + // Type is the type of the extension resource. + Type string `json:"type" protobuf:"bytes,1,opt,name=type"` + // ProviderConfig is the configuration passed to extension resource. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,2,opt,name=providerConfig"` + // Disabled allows to disable extensions that were marked as 'globally enabled' by Gardener administrators. + // +optional + Disabled *bool `json:"disabled,omitempty" protobuf:"varint,3,opt,name=disabled"` +} + +// NamedResourceReference is a named reference to a resource. +type NamedResourceReference struct { + // Name of the resource reference. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // ResourceRef is a reference to a resource. + ResourceRef autoscalingv1.CrossVersionObjectReference `json:"resourceRef" protobuf:"bytes,2,opt,name=resourceRef"` +} + +// Hibernation contains information whether the Shoot is suspended or not. +type Hibernation struct { + // Enabled specifies whether the Shoot needs to be hibernated or not. If it is true, the Shoot's desired state is to be hibernated. + // If it is false or nil, the Shoot's desired state is to be awakened. + // +optional + Enabled *bool `json:"enabled,omitempty" protobuf:"varint,1,opt,name=enabled"` + // Schedules determine the hibernation schedules. + // +optional + Schedules []HibernationSchedule `json:"schedules,omitempty" protobuf:"bytes,2,rep,name=schedules"` +} + +// HibernationSchedule determines the hibernation schedule of a Shoot. +// A Shoot will be regularly hibernated at each start time and will be woken up at each end time. +// Start or End can be omitted, though at least one of each has to be specified. +type HibernationSchedule struct { + // Start is a Cron spec at which time a Shoot will be hibernated. + // +optional + Start *string `json:"start,omitempty" protobuf:"bytes,1,opt,name=start"` + // End is a Cron spec at which time a Shoot will be woken up. + // +optional + End *string `json:"end,omitempty" protobuf:"bytes,2,opt,name=end"` + // Location is the time location in which both start and shall be evaluated. + // +optional + Location *string `json:"location,omitempty" protobuf:"bytes,3,opt,name=location"` +} + +// Kubernetes contains the version and configuration variables for the Shoot control plane. +type Kubernetes struct { + // AllowPrivilegedContainers is tombstoned to show why 1 is reserved protobuf tag. + // AllowPrivilegedContainers *bool `json:"allowPrivilegedContainers,omitempty" protobuf:"varint,1,opt,name=allowPrivilegedContainers"` + + // ClusterAutoscaler contains the configuration flags for the Kubernetes cluster autoscaler. + // +optional + ClusterAutoscaler *ClusterAutoscaler `json:"clusterAutoscaler,omitempty" protobuf:"bytes,2,opt,name=clusterAutoscaler"` + // KubeAPIServer contains configuration settings for the kube-apiserver. + // +optional + KubeAPIServer *KubeAPIServerConfig `json:"kubeAPIServer,omitempty" protobuf:"bytes,3,opt,name=kubeAPIServer"` + // KubeControllerManager contains configuration settings for the kube-controller-manager. + // +optional + KubeControllerManager *KubeControllerManagerConfig `json:"kubeControllerManager,omitempty" protobuf:"bytes,4,opt,name=kubeControllerManager"` + // KubeScheduler contains configuration settings for the kube-scheduler. + // +optional + KubeScheduler *KubeSchedulerConfig `json:"kubeScheduler,omitempty" protobuf:"bytes,5,opt,name=kubeScheduler"` + // KubeProxy contains configuration settings for the kube-proxy. + // +optional + KubeProxy *KubeProxyConfig `json:"kubeProxy,omitempty" protobuf:"bytes,6,opt,name=kubeProxy"` + // Kubelet contains configuration settings for the kubelet. + // +optional + Kubelet *KubeletConfig `json:"kubelet,omitempty" protobuf:"bytes,7,opt,name=kubelet"` + // Note: Even though 'Version' is an optional field for users, we deliberately chose to not make it a pointer + // because the field is guaranteed to be not-empty after the admission plugin processed the shoot object. + // Thus, pointer handling for this field is not beneficial and would make things more cumbersome. + + // Version is the semantic Kubernetes version to use for the Shoot cluster. + // Defaults to the highest supported minor and patch version given in the referenced cloud profile. + // The version can be omitted completely or partially specified, e.g. `.`. + // +optional + Version string `json:"version,omitempty" protobuf:"bytes,8,opt,name=version"` + // VerticalPodAutoscaler contains the configuration flags for the Kubernetes vertical pod autoscaler. + // +optional + VerticalPodAutoscaler *VerticalPodAutoscaler `json:"verticalPodAutoscaler,omitempty" protobuf:"bytes,9,opt,name=verticalPodAutoscaler"` + // EnableStaticTokenKubeconfig indicates whether static token kubeconfig secret will be created for the Shoot cluster. + // Defaults to true for Shoots with Kubernetes versions < 1.26. Defaults to false for Shoots with Kubernetes versions >= 1.26. + // Starting Kubernetes 1.27 the field will be locked to false. + // +optional + EnableStaticTokenKubeconfig *bool `json:"enableStaticTokenKubeconfig,omitempty" protobuf:"varint,10,opt,name=enableStaticTokenKubeconfig"` +} + +// ClusterAutoscaler contains the configuration flags for the Kubernetes cluster autoscaler. +type ClusterAutoscaler struct { + // ScaleDownDelayAfterAdd defines how long after scale up that scale down evaluation resumes (default: 1 hour). + // +optional + ScaleDownDelayAfterAdd *metav1.Duration `json:"scaleDownDelayAfterAdd,omitempty" protobuf:"bytes,1,opt,name=scaleDownDelayAfterAdd"` + // ScaleDownDelayAfterDelete how long after node deletion that scale down evaluation resumes, defaults to scanInterval (default: 0 secs). + // +optional + ScaleDownDelayAfterDelete *metav1.Duration `json:"scaleDownDelayAfterDelete,omitempty" protobuf:"bytes,2,opt,name=scaleDownDelayAfterDelete"` + // ScaleDownDelayAfterFailure how long after scale down failure that scale down evaluation resumes (default: 3 mins). + // +optional + ScaleDownDelayAfterFailure *metav1.Duration `json:"scaleDownDelayAfterFailure,omitempty" protobuf:"bytes,3,opt,name=scaleDownDelayAfterFailure"` + // ScaleDownUnneededTime defines how long a node should be unneeded before it is eligible for scale down (default: 30 mins). + // +optional + ScaleDownUnneededTime *metav1.Duration `json:"scaleDownUnneededTime,omitempty" protobuf:"bytes,4,opt,name=scaleDownUnneededTime"` + // ScaleDownUtilizationThreshold defines the threshold in fraction (0.0 - 1.0) under which a node is being removed (default: 0.5). + // +optional + ScaleDownUtilizationThreshold *float64 `json:"scaleDownUtilizationThreshold,omitempty" protobuf:"fixed64,5,opt,name=scaleDownUtilizationThreshold"` + // ScanInterval how often cluster is reevaluated for scale up or down (default: 10 secs). + // +optional + ScanInterval *metav1.Duration `json:"scanInterval,omitempty" protobuf:"bytes,6,opt,name=scanInterval"` + // Expander defines the algorithm to use during scale up (default: least-waste). + // See: https://github.com/gardener/autoscaler/blob/machine-controller-manager-provider/cluster-autoscaler/FAQ.md#what-are-expanders. + // +optional + Expander *ExpanderMode `json:"expander,omitempty" protobuf:"bytes,7,opt,name=expander"` + // MaxNodeProvisionTime defines how long CA waits for node to be provisioned (default: 20 mins). + // +optional + MaxNodeProvisionTime *metav1.Duration `json:"maxNodeProvisionTime,omitempty" protobuf:"bytes,8,opt,name=maxNodeProvisionTime"` + // MaxGracefulTerminationSeconds is the number of seconds CA waits for pod termination when trying to scale down a node (default: 600). + // +optional + MaxGracefulTerminationSeconds *int32 `json:"maxGracefulTerminationSeconds,omitempty" protobuf:"varint,9,opt,name=maxGracefulTerminationSeconds"` + // IgnoreTaints specifies a list of taint keys to ignore in node templates when considering to scale a node group. + // Deprecated: Ignore taints are deprecated as of K8S 1.29 and treated as startup taints + // +optional + IgnoreTaints []string `json:"ignoreTaints,omitempty" protobuf:"bytes,10,opt,name=ignoreTaints"` + + // NewPodScaleUpDelay specifies how long CA should ignore newly created pods before they have to be considered for scale-up (default: 0s). + // +optional + NewPodScaleUpDelay *metav1.Duration `json:"newPodScaleUpDelay,omitempty" protobuf:"bytes,11,opt,name=newPodScaleUpDelay"` + // MaxEmptyBulkDelete specifies the maximum number of empty nodes that can be deleted at the same time (default: 10). + // +optional + MaxEmptyBulkDelete *int32 `json:"maxEmptyBulkDelete,omitempty" protobuf:"varint,12,opt,name=maxEmptyBulkDelete"` + // IgnoreDaemonsetsUtilization allows CA to ignore DaemonSet pods when calculating resource utilization for scaling down (default: false). + // +optional + IgnoreDaemonsetsUtilization *bool `json:"ignoreDaemonsetsUtilization,omitempty" protobuf:"varint,13,opt,name=ignoreDaemonsetsUtilization"` + // Verbosity allows CA to modify its log level (default: 2). + // +optional + Verbosity *int32 `json:"verbosity,omitempty" protobuf:"varint,14,opt,name=verbosity"` + // StartupTaints specifies a list of taint keys to ignore in node templates when considering to scale a node group. + // Cluster Autoscaler treats nodes tainted with startup taints as unready, but taken into account during scale up logic, assuming they will become ready shortly. + // +optional + StartupTaints []string `json:"startupTaints,omitempty" protobuf:"bytes,15,opt,name=startupTaints"` + // StatusTaints specifies a list of taint keys to ignore in node templates when considering to scale a node group. + // Cluster Autoscaler internally treats nodes tainted with status taints as ready, but filtered out during scale up logic. + // +optional + StatusTaints []string `json:"statusTaints,omitempty" protobuf:"bytes,16,opt,name=statusTaints"` +} + +// ExpanderMode is type used for Expander values +type ExpanderMode string + +const ( + // ClusterAutoscalerExpanderLeastWaste selects the node group that will have the least idle CPU (if tied, unused memory) after scale-up. + // This is useful when you have different classes of nodes, for example, high CPU or high memory nodes, and + // only want to expand those when there are pending pods that need a lot of those resources. + // This is the default value. + ClusterAutoscalerExpanderLeastWaste ExpanderMode = "least-waste" + // ClusterAutoscalerExpanderMostPods selects the node group that would be able to schedule the most pods when scaling up. + // This is useful when you are using nodeSelector to make sure certain pods land on certain nodes. + // Note that this won't cause the autoscaler to select bigger nodes vs. smaller, as it can add multiple smaller nodes at once. + ClusterAutoscalerExpanderMostPods ExpanderMode = "most-pods" + // ClusterAutoscalerExpanderPriority selects the node group that has the highest priority assigned by the user. For configurations, + // See: https://github.com/gardener/autoscaler/blob/machine-controller-manager-provider/cluster-autoscaler/expander/priority/readme.md + ClusterAutoscalerExpanderPriority ExpanderMode = "priority" + // ClusterAutoscalerExpanderRandom should be used when you don't have a particular need + // for the node groups to scale differently. + ClusterAutoscalerExpanderRandom ExpanderMode = "random" +) + +// VerticalPodAutoscaler contains the configuration flags for the Kubernetes vertical pod autoscaler. +type VerticalPodAutoscaler struct { + // Enabled specifies whether the Kubernetes VPA shall be enabled for the shoot cluster. + Enabled bool `json:"enabled" protobuf:"varint,1,opt,name=enabled"` + // EvictAfterOOMThreshold defines the threshold that will lead to pod eviction in case it OOMed in less than the given + // threshold since its start and if it has only one container (default: 10m0s). + // +optional + EvictAfterOOMThreshold *metav1.Duration `json:"evictAfterOOMThreshold,omitempty" protobuf:"bytes,2,opt,name=evictAfterOOMThreshold"` + // EvictionRateBurst defines the burst of pods that can be evicted (default: 1) + // +optional + EvictionRateBurst *int32 `json:"evictionRateBurst,omitempty" protobuf:"varint,3,opt,name=evictionRateBurst"` + // EvictionRateLimit defines the number of pods that can be evicted per second. A rate limit set to 0 or -1 will + // disable the rate limiter (default: -1). + // +optional + EvictionRateLimit *float64 `json:"evictionRateLimit,omitempty" protobuf:"fixed64,4,opt,name=evictionRateLimit"` + // EvictionTolerance defines the fraction of replica count that can be evicted for update in case more than one + // pod can be evicted (default: 0.5). + // +optional + EvictionTolerance *float64 `json:"evictionTolerance,omitempty" protobuf:"fixed64,5,opt,name=evictionTolerance"` + // RecommendationMarginFraction is the fraction of usage added as the safety margin to the recommended request + // (default: 0.15). + // +optional + RecommendationMarginFraction *float64 `json:"recommendationMarginFraction,omitempty" protobuf:"fixed64,6,opt,name=recommendationMarginFraction"` + // UpdaterInterval is the interval how often the updater should run (default: 1m0s). + // +optional + UpdaterInterval *metav1.Duration `json:"updaterInterval,omitempty" protobuf:"bytes,7,opt,name=updaterInterval"` + // RecommenderInterval is the interval how often metrics should be fetched (default: 1m0s). + // +optional + RecommenderInterval *metav1.Duration `json:"recommenderInterval,omitempty" protobuf:"bytes,8,opt,name=recommenderInterval"` + // TargetCPUPercentile is the usage percentile that will be used as a base for CPU target recommendation. + // Doesn't affect CPU lower bound, CPU upper bound nor memory recommendations. + // (default: 0.9) + // +optional + TargetCPUPercentile *float64 `json:"targetCPUPercentile,omitempty" protobuf:"fixed64,9,opt,name=targetCPUPercentile"` + // RecommendationLowerBoundCPUPercentile is the usage percentile that will be used for the lower bound on CPU recommendation. + // (default: 0.5) + // +optional + RecommendationLowerBoundCPUPercentile *float64 `json:"recommendationLowerBoundCPUPercentile,omitempty" protobuf:"fixed64,10,opt,name=recommendationLowerBoundCPUPercentile"` + // RecommendationUpperBoundCPUPercentile is the usage percentile that will be used for the upper bound on CPU recommendation. + // (default: 0.95) + // +optional + RecommendationUpperBoundCPUPercentile *float64 `json:"recommendationUpperBoundCPUPercentile,omitempty" protobuf:"fixed64,11,opt,name=recommendationUpperBoundCPUPercentile"` + // TargetMemoryPercentile is the usage percentile that will be used as a base for memory target recommendation. + // Doesn't affect memory lower bound nor memory upper bound. + // (default: 0.9) + // +optional + TargetMemoryPercentile *float64 `json:"targetMemoryPercentile,omitempty" protobuf:"fixed64,12,opt,name=targetMemoryPercentile"` + // RecommendationLowerBoundMemoryPercentile is the usage percentile that will be used for the lower bound on memory recommendation. + // (default: 0.5) + // +optional + RecommendationLowerBoundMemoryPercentile *float64 `json:"recommendationLowerBoundMemoryPercentile,omitempty" protobuf:"fixed64,13,opt,name=recommendationLowerBoundMemoryPercentile"` + // RecommendationUpperBoundMemoryPercentile is the usage percentile that will be used for the upper bound on memory recommendation. + // (default: 0.95) + // +optional + RecommendationUpperBoundMemoryPercentile *float64 `json:"recommendationUpperBoundMemoryPercentile,omitempty" protobuf:"fixed64,14,opt,name=recommendationUpperBoundMemoryPercentile"` + // CPUHistogramDecayHalfLife is the amount of time it takes a historical CPU usage sample to lose half of its weight. + // (default: 24h) + // +optional + CPUHistogramDecayHalfLife *metav1.Duration `json:"cpuHistogramDecayHalfLife,omitempty" protobuf:"bytes,15,opt,name=cpuHistogramDecayHalfLife"` + // MemoryHistogramDecayHalfLife is the amount of time it takes a historical memory usage sample to lose half of its weight. + // (default: 24h) + // +optional + MemoryHistogramDecayHalfLife *metav1.Duration `json:"memoryHistogramDecayHalfLife,omitempty" protobuf:"bytes,16,opt,name=memoryHistogramDecayHalfLife"` + // MemoryAggregationInterval is the length of a single interval, for which the peak memory usage is computed. + // (default: 24h) + // +optional + MemoryAggregationInterval *metav1.Duration `json:"memoryAggregationInterval,omitempty" protobuf:"bytes,17,opt,name=memoryAggregationInterval"` + // MemoryAggregationIntervalCount is the number of consecutive memory-aggregation-intervals which make up the + // MemoryAggregationWindowLength which in turn is the period for memory usage aggregation by VPA. In other words, + // `MemoryAggregationWindowLength = memory-aggregation-interval * memory-aggregation-interval-count`. + // (default: 8) + // +optional + MemoryAggregationIntervalCount *int64 `json:"memoryAggregationIntervalCount,omitempty" protobuf:"varint,18,opt,name=memoryAggregationIntervalCount"` +} + +const ( + // DefaultEvictionRateBurst is the default value for the EvictionRateBurst field in the VPA configuration. + DefaultEvictionRateBurst int32 = 1 + // DefaultEvictionRateLimit is the default value for the EvictionRateLimit field in the VPA configuration. + DefaultEvictionRateLimit float64 = -1 + // DefaultEvictionTolerance is the default value for the EvictionTolerance field in the VPA configuration. + DefaultEvictionTolerance = 0.5 + // DefaultRecommendationMarginFraction is the default value for the RecommendationMarginFraction field in the VPA configuration. + DefaultRecommendationMarginFraction = 0.15 + // DefaultTargetCPUPercentile is the default value for the TargetCPUPercentile field in the VPA configuration. + DefaultTargetCPUPercentile = 0.9 + // DefaultRecommendationLowerBoundCPUPercentile is the default value for the RecommendationLowerBoundCPUPercentile field in the VPA configuration. + DefaultRecommendationLowerBoundCPUPercentile = 0.5 + // DefaultRecommendationUpperBoundCPUPercentile is the default value for the RecommendationUpperBoundCPUPercentile field in the VPA configuration. + DefaultRecommendationUpperBoundCPUPercentile = 0.95 + // DefaultTargetMemoryPercentile is the default value for the TargetMemoryPercentile field in the VPA configuration. + DefaultTargetMemoryPercentile = 0.9 + // DefaultRecommendationLowerBoundMemoryPercentile is the default value for the RecommendationLowerBoundMemoryPercentile field in the VPA configuration. + DefaultRecommendationLowerBoundMemoryPercentile = 0.5 + // DefaultRecommendationUpperBoundMemoryPercentile is the default value for the RecommendationUpperBoundMemoryPercentile field in the VPA configuration. + DefaultRecommendationUpperBoundMemoryPercentile = 0.95 +) + +var ( + // DefaultEvictAfterOOMThreshold is the default value for the EvictAfterOOMThreshold field in the VPA configuration. + DefaultEvictAfterOOMThreshold = metav1.Duration{Duration: 10 * time.Minute} + // DefaultUpdaterInterval is the default value for the UpdaterInterval field in the VPA configuration. + DefaultUpdaterInterval = metav1.Duration{Duration: time.Minute} + // DefaultRecommenderInterval is the default value for the RecommenderInterval field in the VPA configuration. + DefaultRecommenderInterval = metav1.Duration{Duration: time.Minute} + // DefaultCPUHistogramDecayHalfLife is the default value for the CPUHistogramDecayHalfLife field in the VPA configuration. + DefaultCPUHistogramDecayHalfLife = metav1.Duration{Duration: 24 * time.Hour} + // DefaultMemoryHistogramDecayHalfLife is the default value for the MemoryHistogramDecayHalfLife field in the VPA configuration. + DefaultMemoryHistogramDecayHalfLife = metav1.Duration{Duration: 24 * time.Hour} + // DefaultMemoryAggregationInterval is the default value for the MemoryAggregationInterval field in the VPA configuration. + DefaultMemoryAggregationInterval = metav1.Duration{Duration: 24 * time.Hour} + // DefaultMemoryAggregationIntervalCount is the default value for the MemoryAggregationIntervalCount field in the VPA configuration. + DefaultMemoryAggregationIntervalCount = int64(8) +) + +// KubernetesConfig contains common configuration fields for the control plane components. +type KubernetesConfig struct { + // FeatureGates contains information about enabled feature gates. + // +optional + FeatureGates map[string]bool `json:"featureGates,omitempty" protobuf:"bytes,1,rep,name=featureGates"` +} + +// KubeAPIServerConfig contains configuration settings for the kube-apiserver. +type KubeAPIServerConfig struct { + KubernetesConfig `json:",inline" protobuf:"bytes,1,opt,name=kubernetesConfig"` + // AdmissionPlugins contains the list of user-defined admission plugins (additional to those managed by Gardener), and, if desired, the corresponding + // configuration. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + AdmissionPlugins []AdmissionPlugin `json:"admissionPlugins,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=admissionPlugins"` + // APIAudiences are the identifiers of the API. The service account token authenticator will + // validate that tokens used against the API are bound to at least one of these audiences. + // Defaults to ["kubernetes"]. + // +optional + APIAudiences []string `json:"apiAudiences,omitempty" protobuf:"bytes,3,rep,name=apiAudiences"` + // AuditConfig contains configuration settings for the audit of the kube-apiserver. + // +optional + AuditConfig *AuditConfig `json:"auditConfig,omitempty" protobuf:"bytes,4,opt,name=auditConfig"` + + // EnableBasicAuthentication is tombstoned to show why 5 is reserved protobuf tag. + // EnableBasicAuthentication *bool `json:"enableBasicAuthentication,omitempty" protobuf:"varint,5,opt,name=enableBasicAuthentication"` + + // OIDCConfig contains configuration settings for the OIDC provider. + // + // Deprecated: This field is deprecated and will be forbidden starting from Kubernetes 1.32. + // Please configure and use structured authentication instead of oidc flags. + // For more information check https://github.com/gardener/gardener/issues/9858 + // TODO(AleksandarSavchev): Drop this field after support for Kubernetes 1.31 is dropped. + // +optional + OIDCConfig *OIDCConfig `json:"oidcConfig,omitempty" protobuf:"bytes,6,opt,name=oidcConfig"` + // RuntimeConfig contains information about enabled or disabled APIs. + // +optional + RuntimeConfig map[string]bool `json:"runtimeConfig,omitempty" protobuf:"bytes,7,rep,name=runtimeConfig"` + // ServiceAccountConfig contains configuration settings for the service account handling + // of the kube-apiserver. + // +optional + ServiceAccountConfig *ServiceAccountConfig `json:"serviceAccountConfig,omitempty" protobuf:"bytes,8,opt,name=serviceAccountConfig"` + // WatchCacheSizes contains configuration of the API server's watch cache sizes. + // Configuring these flags might be useful for large-scale Shoot clusters with a lot of parallel update requests + // and a lot of watching controllers (e.g. large ManagedSeed clusters). When the API server's watch cache's + // capacity is too small to cope with the amount of update requests and watchers for a particular resource, it + // might happen that controller watches are permanently stopped with `too old resource version` errors. + // Starting from kubernetes v1.19, the API server's watch cache size is adapted dynamically and setting the watch + // cache size flags will have no effect, except when setting it to 0 (which disables the watch cache). + // +optional + WatchCacheSizes *WatchCacheSizes `json:"watchCacheSizes,omitempty" protobuf:"bytes,9,opt,name=watchCacheSizes"` + // Requests contains configuration for request-specific settings for the kube-apiserver. + // +optional + Requests *APIServerRequests `json:"requests,omitempty" protobuf:"bytes,10,opt,name=requests"` + // EnableAnonymousAuthentication defines whether anonymous requests to the secure port + // of the API server should be allowed (flag `--anonymous-auth`). + // See: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/ + // +optional + EnableAnonymousAuthentication *bool `json:"enableAnonymousAuthentication,omitempty" protobuf:"varint,11,opt,name=enableAnonymousAuthentication"` + // EventTTL controls the amount of time to retain events. + // Defaults to 1h. + // +optional + EventTTL *metav1.Duration `json:"eventTTL,omitempty" protobuf:"bytes,12,opt,name=eventTTL"` + // Logging contains configuration for the log level and HTTP access logs. + // +optional + Logging *APIServerLogging `json:"logging,omitempty" protobuf:"bytes,13,opt,name=logging"` + // DefaultNotReadyTolerationSeconds indicates the tolerationSeconds of the toleration for notReady:NoExecute + // that is added by default to every pod that does not already have such a toleration (flag `--default-not-ready-toleration-seconds`). + // The field has effect only when the `DefaultTolerationSeconds` admission plugin is enabled. + // Defaults to 300. + // +optional + DefaultNotReadyTolerationSeconds *int64 `json:"defaultNotReadyTolerationSeconds,omitempty" protobuf:"varint,14,opt,name=defaultNotReadyTolerationSeconds"` + // DefaultUnreachableTolerationSeconds indicates the tolerationSeconds of the toleration for unreachable:NoExecute + // that is added by default to every pod that does not already have such a toleration (flag `--default-unreachable-toleration-seconds`). + // The field has effect only when the `DefaultTolerationSeconds` admission plugin is enabled. + // Defaults to 300. + // +optional + DefaultUnreachableTolerationSeconds *int64 `json:"defaultUnreachableTolerationSeconds,omitempty" protobuf:"varint,15,opt,name=defaultUnreachableTolerationSeconds"` + // EncryptionConfig contains customizable encryption configuration of the Kube API server. + // +optional + EncryptionConfig *EncryptionConfig `json:"encryptionConfig,omitempty" protobuf:"bytes,16,opt,name=encryptionConfig"` + // StructuredAuthentication contains configuration settings for structured authentication for the kube-apiserver. + // This field is only available for Kubernetes v1.30 or later. + // +optional + StructuredAuthentication *StructuredAuthentication `json:"structuredAuthentication,omitempty" protobuf:"bytes,17,opt,name=structuredAuthentication"` + // StructuredAuthorization contains configuration settings for structured authorization for the kube-apiserver. + // This field is only available for Kubernetes v1.30 or later. + // +optional + StructuredAuthorization *StructuredAuthorization `json:"structuredAuthorization,omitempty" protobuf:"bytes,18,opt,name=structuredAuthorization"` +} + +// APIServerLogging contains configuration for the logs level and http access logs +type APIServerLogging struct { + // Verbosity is the kube-apiserver log verbosity level + // Defaults to 2. + // +optional + Verbosity *int32 `json:"verbosity,omitempty" protobuf:"varint,1,opt,name=verbosity"` + // HTTPAccessVerbosity is the kube-apiserver access logs level + // +optional + HTTPAccessVerbosity *int32 `json:"httpAccessVerbosity,omitempty" protobuf:"varint,2,opt,name=httpAccessVerbosity"` +} + +// APIServerRequests contains configuration for request-specific settings for the kube-apiserver. +type APIServerRequests struct { + // MaxNonMutatingInflight is the maximum number of non-mutating requests in flight at a given time. When the server + // exceeds this, it rejects requests. + // +optional + MaxNonMutatingInflight *int32 `json:"maxNonMutatingInflight,omitempty" protobuf:"bytes,1,name=maxNonMutatingInflight"` + // MaxMutatingInflight is the maximum number of mutating requests in flight at a given time. When the server + // exceeds this, it rejects requests. + // +optional + MaxMutatingInflight *int32 `json:"maxMutatingInflight,omitempty" protobuf:"bytes,2,name=maxMutatingInflight"` +} + +// EncryptionConfig contains customizable encryption configuration of the API server. +type EncryptionConfig struct { + // Resources contains the list of resources that shall be encrypted in addition to secrets. + // Each item is a Kubernetes resource name in plural (resource or resource.group) that should be encrypted. + // Note that configuring a custom resource is only supported for versions >= 1.26. + // Wildcards are not supported for now. + // See https://github.com/gardener/gardener/blob/master/docs/usage/security/etcd_encryption_config.md for more details. + Resources []string `json:"resources" protobuf:"bytes,1,rep,name=resources"` +} + +// ServiceAccountConfig is the kube-apiserver configuration for service accounts. +type ServiceAccountConfig struct { + // Issuer is the identifier of the service account token issuer. The issuer will assert this + // identifier in "iss" claim of issued tokens. This value is used to generate new service account tokens. + // This value is a string or URI. Defaults to URI of the API server. + // +optional + Issuer *string `json:"issuer,omitempty" protobuf:"bytes,1,opt,name=issuer"` + + // SigningKeySecret is tombstoned to show why 2 is reserved protobuf tag. + // SigningKeySecret *corev1.LocalObjectReference `json:"signingKeySecretName,omitempty" protobuf:"bytes,2,opt,name=signingKeySecretName"` + + // ExtendTokenExpiration turns on projected service account expiration extension during token generation, which + // helps safe transition from legacy token to bound service account token feature. If this flag is enabled, + // admission injected tokens would be extended up to 1 year to prevent unexpected failure during transition, + // ignoring value of service-account-max-token-expiration. + // +optional + ExtendTokenExpiration *bool `json:"extendTokenExpiration,omitempty" protobuf:"bytes,3,opt,name=extendTokenExpiration"` + // MaxTokenExpiration is the maximum validity duration of a token created by the service account token issuer. If an + // otherwise valid TokenRequest with a validity duration larger than this value is requested, a token will be issued + // with a validity duration of this value. + // This field must be within [30d,90d]. + // +optional + MaxTokenExpiration *metav1.Duration `json:"maxTokenExpiration,omitempty" protobuf:"bytes,4,opt,name=maxTokenExpiration"` + // AcceptedIssuers is an additional set of issuers that are used to determine which service account tokens are accepted. + // These values are not used to generate new service account tokens. Only useful when service account tokens are also + // issued by another external system or a change of the current issuer that is used for generating tokens is being performed. + // +optional + AcceptedIssuers []string `json:"acceptedIssuers,omitempty" protobuf:"bytes,5,opt,name=acceptedIssuers"` +} + +// AuditConfig contains settings for audit of the api server +type AuditConfig struct { + // AuditPolicy contains configuration settings for audit policy of the kube-apiserver. + // +optional + AuditPolicy *AuditPolicy `json:"auditPolicy,omitempty" protobuf:"bytes,1,opt,name=auditPolicy"` +} + +// AuditPolicy contains audit policy for kube-apiserver +type AuditPolicy struct { + // ConfigMapRef is a reference to a ConfigMap object in the same namespace, + // which contains the audit policy for the kube-apiserver. + // +optional + ConfigMapRef *corev1.ObjectReference `json:"configMapRef,omitempty" protobuf:"bytes,1,opt,name=configMapRef"` +} + +// StructuredAuthentication contains authentication config for kube-apiserver. +type StructuredAuthentication struct { + // ConfigMapName is the name of the ConfigMap in the project namespace which contains AuthenticationConfiguration + // for the kube-apiserver. + ConfigMapName string `json:"configMapName" protobuf:"bytes,1,opt,name=configMapName"` +} + +// StructuredAuthorization contains authorization config for kube-apiserver. +type StructuredAuthorization struct { + // ConfigMapName is the name of the ConfigMap in the project namespace which contains AuthorizationConfiguration for + // the kube-apiserver. + ConfigMapName string `json:"configMapName" protobuf:"bytes,1,opt,name=configMapName"` + // Kubeconfigs is a list of references for kubeconfigs for the authorization webhooks. + Kubeconfigs []AuthorizerKubeconfigReference `json:"kubeconfigs" protobuf:"bytes,2,rep,name=kubeconfigs"` +} + +// AuthorizerKubeconfigReference is a reference for a kubeconfig for a authorization webhook. +type AuthorizerKubeconfigReference struct { + // AuthorizerName is the name of a webhook authorizer. + AuthorizerName string `json:"authorizerName" protobuf:"bytes,1,opt,name=authorizerName"` + // SecretName is the name of a secret containing the kubeconfig. + SecretName string `json:"secretName" protobuf:"bytes,2,opt,name=secretName"` +} + +// OIDCConfig contains configuration settings for the OIDC provider. +// Note: Descriptions were taken from the Kubernetes documentation. +type OIDCConfig struct { + // If set, the OpenID server's certificate will be verified by one of the authorities in the oidc-ca-file, otherwise the host's root CA set will be used. + // +optional + CABundle *string `json:"caBundle,omitempty" protobuf:"bytes,1,opt,name=caBundle"` + // ClientAuthentication can optionally contain client configuration used for kubeconfig generation. + // + // Deprecated: This field has no implemented use and will be forbidden starting from Kubernetes 1.31. + // It's use was planned for genereting OIDC kubeconfig https://github.com/gardener/gardener/issues/1433 + // TODO(AleksandarSavchev): Drop this field after support for Kubernetes 1.30 is dropped. + // +optional + ClientAuthentication *OpenIDConnectClientAuthentication `json:"clientAuthentication,omitempty" protobuf:"bytes,2,opt,name=clientAuthentication"` + // The client ID for the OpenID Connect client, must be set. + // +optional + ClientID *string `json:"clientID,omitempty" protobuf:"bytes,3,opt,name=clientID"` + // If provided, the name of a custom OpenID Connect claim for specifying user groups. The claim value is expected to be a string or array of strings. This flag is experimental, please see the authentication documentation for further details. + // +optional + GroupsClaim *string `json:"groupsClaim,omitempty" protobuf:"bytes,4,opt,name=groupsClaim"` + // If provided, all groups will be prefixed with this value to prevent conflicts with other authentication strategies. + // +optional + GroupsPrefix *string `json:"groupsPrefix,omitempty" protobuf:"bytes,5,opt,name=groupsPrefix"` + // The URL of the OpenID issuer, only HTTPS scheme will be accepted. Used to verify the OIDC JSON Web Token (JWT). + // +optional + IssuerURL *string `json:"issuerURL,omitempty" protobuf:"bytes,6,opt,name=issuerURL"` + // key=value pairs that describes a required claim in the ID Token. If set, the claim is verified to be present in the ID Token with a matching value. + // +optional + RequiredClaims map[string]string `json:"requiredClaims,omitempty" protobuf:"bytes,7,rep,name=requiredClaims"` + // List of allowed JOSE asymmetric signing algorithms. JWTs with a 'alg' header value not in this list will be rejected. Values are defined by RFC 7518 https://tools.ietf.org/html/rfc7518#section-3.1 + // +optional + SigningAlgs []string `json:"signingAlgs,omitempty" protobuf:"bytes,8,rep,name=signingAlgs"` + // The OpenID claim to use as the user name. Note that claims other than the default ('sub') is not guaranteed to be unique and immutable. This flag is experimental, please see the authentication documentation for further details. (default "sub") + // +optional + UsernameClaim *string `json:"usernameClaim,omitempty" protobuf:"bytes,9,opt,name=usernameClaim"` + // If provided, all usernames will be prefixed with this value. If not provided, username claims other than 'email' are prefixed by the issuer URL to avoid clashes. To skip any prefixing, provide the value '-'. + // +optional + UsernamePrefix *string `json:"usernamePrefix,omitempty" protobuf:"bytes,10,opt,name=usernamePrefix"` +} + +// OpenIDConnectClientAuthentication contains configuration for OIDC clients. +type OpenIDConnectClientAuthentication struct { + // Extra configuration added to kubeconfig's auth-provider. + // Must not be any of idp-issuer-url, client-id, client-secret, idp-certificate-authority, idp-certificate-authority-data, id-token or refresh-token + // +optional + ExtraConfig map[string]string `json:"extraConfig,omitempty" protobuf:"bytes,1,rep,name=extraConfig"` + // The client Secret for the OpenID Connect client. + // +optional + Secret *string `json:"secret,omitempty" protobuf:"bytes,2,opt,name=secret"` +} + +// AdmissionPlugin contains information about a specific admission plugin and its corresponding configuration. +type AdmissionPlugin struct { + // Name is the name of the plugin. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Config is the configuration of the plugin. + // +optional + Config *runtime.RawExtension `json:"config,omitempty" protobuf:"bytes,2,opt,name=config"` + // Disabled specifies whether this plugin should be disabled. + // +optional + Disabled *bool `json:"disabled,omitempty" protobuf:"varint,3,opt,name=disabled"` + // KubeconfigSecretName specifies the name of a secret containing the kubeconfig for this admission plugin. + // +optional + KubeconfigSecretName *string `json:"kubeconfigSecretName,omitempty" protobuf:"bytes,4,opt,name=kubeconfigSecretName"` +} + +// WatchCacheSizes contains configuration of the API server's watch cache sizes. +type WatchCacheSizes struct { + // Default configures the default watch cache size of the kube-apiserver + // (flag `--default-watch-cache-size`, defaults to 100). + // See: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/ + // +optional + Default *int32 `json:"default,omitempty" protobuf:"varint,1,opt,name=default"` + // Resources configures the watch cache size of the kube-apiserver per resource + // (flag `--watch-cache-sizes`). + // See: https://kubernetes.io/docs/reference/command-line-tools-reference/kube-apiserver/ + // +optional + Resources []ResourceWatchCacheSize `json:"resources,omitempty" protobuf:"bytes,2,rep,name=resources"` +} + +// ResourceWatchCacheSize contains configuration of the API server's watch cache size for one specific resource. +type ResourceWatchCacheSize struct { + // APIGroup is the API group of the resource for which the watch cache size should be configured. + // An unset value is used to specify the legacy core API (e.g. for `secrets`). + // +optional + APIGroup *string `json:"apiGroup,omitempty" protobuf:"bytes,1,opt,name=apiGroup"` + // Resource is the name of the resource for which the watch cache size should be configured + // (in lowercase plural form, e.g. `secrets`). + Resource string `json:"resource" protobuf:"bytes,2,opt,name=resource"` + // CacheSize specifies the watch cache size that should be configured for the specified resource. + CacheSize int32 `json:"size" protobuf:"varint,3,opt,name=size"` +} + +// KubeControllerManagerConfig contains configuration settings for the kube-controller-manager. +type KubeControllerManagerConfig struct { + KubernetesConfig `json:",inline" protobuf:"bytes,1,opt,name=kubernetesConfig"` + // HorizontalPodAutoscalerConfig contains horizontal pod autoscaler configuration settings for the kube-controller-manager. + // +optional + HorizontalPodAutoscalerConfig *HorizontalPodAutoscalerConfig `json:"horizontalPodAutoscaler,omitempty" protobuf:"bytes,2,opt,name=horizontalPodAutoscaler"` + // NodeCIDRMaskSize defines the mask size for node cidr in cluster (default is 24). This field is immutable. + // +optional + NodeCIDRMaskSize *int32 `json:"nodeCIDRMaskSize,omitempty" protobuf:"varint,3,opt,name=nodeCIDRMaskSize"` + // PodEvictionTimeout defines the grace period for deleting pods on failed nodes. Defaults to 2m. + // + // Deprecated: The corresponding kube-controller-manager flag `--pod-eviction-timeout` is deprecated + // in favor of the kube-apiserver flags `--default-not-ready-toleration-seconds` and `--default-unreachable-toleration-seconds`. + // The `--pod-eviction-timeout` flag does not have effect when the taint based eviction is enabled. The taint + // based eviction is beta (enabled by default) since Kubernetes 1.13 and GA since Kubernetes 1.18. Hence, + // instead of setting this field, set the `spec.kubernetes.kubeAPIServer.defaultNotReadyTolerationSeconds` and + // `spec.kubernetes.kubeAPIServer.defaultUnreachableTolerationSeconds`. + // +optional + PodEvictionTimeout *metav1.Duration `json:"podEvictionTimeout,omitempty" protobuf:"bytes,4,opt,name=podEvictionTimeout"` + // NodeMonitorGracePeriod defines the grace period before an unresponsive node is marked unhealthy. + // +optional + NodeMonitorGracePeriod *metav1.Duration `json:"nodeMonitorGracePeriod,omitempty" protobuf:"bytes,5,opt,name=nodeMonitorGracePeriod"` +} + +// HorizontalPodAutoscalerConfig contains horizontal pod autoscaler configuration settings for the kube-controller-manager. +// Note: Descriptions were taken from the Kubernetes documentation. +type HorizontalPodAutoscalerConfig struct { + // The period after which a ready pod transition is considered to be the first. + // +optional + CPUInitializationPeriod *metav1.Duration `json:"cpuInitializationPeriod,omitempty" protobuf:"bytes,1,opt,name=cpuInitializationPeriod"` + // The configurable window at which the controller will choose the highest recommendation for autoscaling. + // +optional + DownscaleStabilization *metav1.Duration `json:"downscaleStabilization,omitempty" protobuf:"bytes,3,opt,name=downscaleStabilization"` + // The configurable period at which the horizontal pod autoscaler considers a Pod “not yet ready” given that it’s unready and it has transitioned to unready during that time. + // +optional + InitialReadinessDelay *metav1.Duration `json:"initialReadinessDelay,omitempty" protobuf:"bytes,4,opt,name=initialReadinessDelay"` + // The period for syncing the number of pods in horizontal pod autoscaler. + // +optional + SyncPeriod *metav1.Duration `json:"syncPeriod,omitempty" protobuf:"bytes,5,opt,name=syncPeriod"` + // The minimum change (from 1.0) in the desired-to-actual metrics ratio for the horizontal pod autoscaler to consider scaling. + // +optional + Tolerance *float64 `json:"tolerance,omitempty" protobuf:"fixed64,6,opt,name=tolerance"` +} + +const ( + // DefaultHPASyncPeriod is a constant for the default HPA sync period for a Shoot cluster. + DefaultHPASyncPeriod = 30 * time.Second + // DefaultHPATolerance is a constant for the default HPA tolerance for a Shoot cluster. + DefaultHPATolerance = 0.1 + // DefaultDownscaleStabilization is the default HPA downscale stabilization window for a Shoot cluster + DefaultDownscaleStabilization = 5 * time.Minute + // DefaultInitialReadinessDelay is for the default HPA ReadinessDelay value in the Shoot cluster + DefaultInitialReadinessDelay = 30 * time.Second + // DefaultCPUInitializationPeriod is the for the default value of the CPUInitializationPeriod in the Shoot cluster + DefaultCPUInitializationPeriod = 5 * time.Minute +) + +// KubeSchedulerConfig contains configuration settings for the kube-scheduler. +type KubeSchedulerConfig struct { + KubernetesConfig `json:",inline" protobuf:"bytes,1,opt,name=kubernetesConfig"` + // KubeMaxPDVols allows to configure the `KUBE_MAX_PD_VOLS` environment variable for the kube-scheduler. + // Please find more information here: https://kubernetes.io/docs/concepts/storage/storage-limits/#custom-limits + // Note that using this field is considered alpha-/experimental-level and is on your own risk. You should be aware + // of all the side-effects and consequences when changing it. + // +optional + KubeMaxPDVols *string `json:"kubeMaxPDVols,omitempty" protobuf:"bytes,2,opt,name=kubeMaxPDVols"` + // Profile configures the scheduling profile for the cluster. + // If not specified, the used profile is "balanced" (provides the default kube-scheduler behavior). + // +optional + Profile *SchedulingProfile `json:"profile,omitempty" protobuf:"bytes,3,opt,name=profile,casttype=SchedulingProfile"` +} + +// SchedulingProfile is a string alias used for scheduling profile values. +type SchedulingProfile string + +const ( + // SchedulingProfileBalanced is a scheduling profile that attempts to spread Pods evenly across Nodes + // to obtain a more balanced resource usage. This profile provides the default kube-scheduler behavior. + SchedulingProfileBalanced SchedulingProfile = "balanced" + // SchedulingProfileBinPacking is a scheduling profile that scores Nodes based on the allocation of resources. + // It prioritizes Nodes with most allocated resources. This leads the Node count in the cluster to be minimized and + // the Node resource utilization to be increased. + SchedulingProfileBinPacking SchedulingProfile = "bin-packing" +) + +// KubeProxyConfig contains configuration settings for the kube-proxy. +type KubeProxyConfig struct { + KubernetesConfig `json:",inline" protobuf:"bytes,1,opt,name=kubernetesConfig"` + // Mode specifies which proxy mode to use. + // defaults to IPTables. + // +optional + Mode *ProxyMode `json:"mode,omitempty" protobuf:"bytes,2,opt,name=mode,casttype=ProxyMode"` + // Enabled indicates whether kube-proxy should be deployed or not. + // Depending on the networking extensions switching kube-proxy off might be rejected. Consulting the respective documentation of the used networking extension is recommended before using this field. + // defaults to true if not specified. + // +optional + Enabled *bool `json:"enabled,omitempty" protobuf:"varint,3,opt,name=enabled"` +} + +// ProxyMode available in Linux platform: 'userspace' (older, going to be EOL), 'iptables' +// (newer, faster), 'ipvs' (newest, better in performance and scalability). +// As of now only 'iptables' and 'ipvs' is supported by Gardener. +// In Linux platform, if the iptables proxy is selected, regardless of how, but the system's kernel or iptables versions are +// insufficient, this always falls back to the userspace proxy. IPVS mode will be enabled when proxy mode is set to 'ipvs', +// and the fall back path is firstly iptables and then userspace. +type ProxyMode string + +const ( + // ProxyModeIPTables uses iptables as proxy implementation. + ProxyModeIPTables ProxyMode = "IPTables" + // ProxyModeIPVS uses ipvs as proxy implementation. + ProxyModeIPVS ProxyMode = "IPVS" +) + +// KubeletConfig contains configuration settings for the kubelet. +type KubeletConfig struct { + KubernetesConfig `json:",inline" protobuf:"bytes,1,opt,name=kubernetesConfig"` + // CPUCFSQuota allows you to disable/enable CPU throttling for Pods. + // +optional + CPUCFSQuota *bool `json:"cpuCFSQuota,omitempty" protobuf:"varint,2,opt,name=cpuCFSQuota"` + // CPUManagerPolicy allows to set alternative CPU management policies (default: none). + // +optional + CPUManagerPolicy *string `json:"cpuManagerPolicy,omitempty" protobuf:"bytes,3,opt,name=cpuManagerPolicy"` + // EvictionHard describes a set of eviction thresholds (e.g. memory.available<1Gi) that if met would trigger a Pod eviction. + // +optional + // Default: + // memory.available: "100Mi/1Gi/5%" + // nodefs.available: "5%" + // nodefs.inodesFree: "5%" + // imagefs.available: "5%" + // imagefs.inodesFree: "5%" + EvictionHard *KubeletConfigEviction `json:"evictionHard,omitempty" protobuf:"bytes,4,opt,name=evictionHard"` + // EvictionMaxPodGracePeriod describes the maximum allowed grace period (in seconds) to use when terminating pods in response to a soft eviction threshold being met. + // +optional + // Default: 90 + EvictionMaxPodGracePeriod *int32 `json:"evictionMaxPodGracePeriod,omitempty" protobuf:"varint,5,opt,name=evictionMaxPodGracePeriod"` + // EvictionMinimumReclaim configures the amount of resources below the configured eviction threshold that the kubelet attempts to reclaim whenever the kubelet observes resource pressure. + // +optional + // Default: 0 for each resource + EvictionMinimumReclaim *KubeletConfigEvictionMinimumReclaim `json:"evictionMinimumReclaim,omitempty" protobuf:"bytes,6,opt,name=evictionMinimumReclaim"` + // EvictionPressureTransitionPeriod is the duration for which the kubelet has to wait before transitioning out of an eviction pressure condition. + // +optional + // Default: 4m0s + EvictionPressureTransitionPeriod *metav1.Duration `json:"evictionPressureTransitionPeriod,omitempty" protobuf:"bytes,7,opt,name=evictionPressureTransitionPeriod"` + // EvictionSoft describes a set of eviction thresholds (e.g. memory.available<1.5Gi) that if met over a corresponding grace period would trigger a Pod eviction. + // +optional + // Default: + // memory.available: "200Mi/1.5Gi/10%" + // nodefs.available: "10%" + // nodefs.inodesFree: "10%" + // imagefs.available: "10%" + // imagefs.inodesFree: "10%" + EvictionSoft *KubeletConfigEviction `json:"evictionSoft,omitempty" protobuf:"bytes,8,opt,name=evictionSoft"` + // EvictionSoftGracePeriod describes a set of eviction grace periods (e.g. memory.available=1m30s) that correspond to how long a soft eviction threshold must hold before triggering a Pod eviction. + // +optional + // Default: + // memory.available: 1m30s + // nodefs.available: 1m30s + // nodefs.inodesFree: 1m30s + // imagefs.available: 1m30s + // imagefs.inodesFree: 1m30s + EvictionSoftGracePeriod *KubeletConfigEvictionSoftGracePeriod `json:"evictionSoftGracePeriod,omitempty" protobuf:"bytes,9,opt,name=evictionSoftGracePeriod"` + // MaxPods is the maximum number of Pods that are allowed by the Kubelet. + // +optional + // Default: 110 + MaxPods *int32 `json:"maxPods,omitempty" protobuf:"varint,10,opt,name=maxPods"` + // PodPIDsLimit is the maximum number of process IDs per pod allowed by the kubelet. + // +optional + PodPIDsLimit *int64 `json:"podPidsLimit,omitempty" protobuf:"varint,11,opt,name=podPidsLimit"` + + // ImagePullProgressDeadline is tombstoned to show why 12 is reserved protobuf tag. + // ImagePullProgressDeadline *metav1.Duration `json:"imagePullProgressDeadline,omitempty" protobuf:"bytes,12,opt,name=imagePullProgressDeadline"` + + // FailSwapOn makes the Kubelet fail to start if swap is enabled on the node. (default true). + // +optional + FailSwapOn *bool `json:"failSwapOn,omitempty" protobuf:"varint,13,opt,name=failSwapOn"` + // KubeReserved is the configuration for resources reserved for kubernetes node components (mainly kubelet and container runtime). + // When updating these values, be aware that cgroup resizes may not succeed on active worker nodes. Look for the NodeAllocatableEnforced event to determine if the configuration was applied. + // +optional + // Default: cpu=80m,memory=1Gi,pid=20k + KubeReserved *KubeletConfigReserved `json:"kubeReserved,omitempty" protobuf:"bytes,14,opt,name=kubeReserved"` + // SystemReserved is the configuration for resources reserved for system processes not managed by kubernetes (e.g. journald). + // When updating these values, be aware that cgroup resizes may not succeed on active worker nodes. Look for the NodeAllocatableEnforced event to determine if the configuration was applied. + // + // Deprecated: Separately configuring resource reservations for system processes is deprecated in Gardener and will be forbidden starting from Kubernetes 1.31. + // Please merge existing resource reservations into the kubeReserved field. + // TODO(MichaelEischer): Drop this field after support for Kubernetes 1.30 is dropped. + // +optional + SystemReserved *KubeletConfigReserved `json:"systemReserved,omitempty" protobuf:"bytes,15,opt,name=systemReserved"` + // ImageGCHighThresholdPercent describes the percent of the disk usage which triggers image garbage collection. + // +optional + // Default: 50 + ImageGCHighThresholdPercent *int32 `json:"imageGCHighThresholdPercent,omitempty" protobuf:"bytes,16,opt,name=imageGCHighThresholdPercent"` + // ImageGCLowThresholdPercent describes the percent of the disk to which garbage collection attempts to free. + // +optional + // Default: 40 + ImageGCLowThresholdPercent *int32 `json:"imageGCLowThresholdPercent,omitempty" protobuf:"bytes,17,opt,name=imageGCLowThresholdPercent"` + // SerializeImagePulls describes whether the images are pulled one at a time. + // +optional + // Default: true + SerializeImagePulls *bool `json:"serializeImagePulls,omitempty" protobuf:"varint,18,opt,name=serializeImagePulls"` + // RegistryPullQPS is the limit of registry pulls per second. The value must not be a negative number. + // Setting it to 0 means no limit. + // Default: 5 + // +optional + RegistryPullQPS *int32 `json:"registryPullQPS,omitempty" protobuf:"varint,19,opt,name=registryPullQPS"` + // RegistryBurst is the maximum size of bursty pulls, temporarily allows pulls to burst to this number, + // while still not exceeding registryPullQPS. The value must not be a negative number. + // Only used if registryPullQPS is greater than 0. + // Default: 10 + // +optional + RegistryBurst *int32 `json:"registryBurst,omitempty" protobuf:"varint,20,opt,name=registryBurst"` + // SeccompDefault enables the use of `RuntimeDefault` as the default seccomp profile for all workloads. + // This requires the corresponding SeccompDefault feature gate to be enabled as well. + // This field is only available for Kubernetes v1.25 or later. + // +optional + SeccompDefault *bool `json:"seccompDefault,omitempty" protobuf:"varint,21,opt,name=seccompDefault"` + // A quantity defines the maximum size of the container log file before it is rotated. For example: "5Mi" or "256Ki". + // +optional + // Default: 100Mi + ContainerLogMaxSize *resource.Quantity `json:"containerLogMaxSize,omitempty" protobuf:"bytes,22,opt,name=containerLogMaxSize"` + // Maximum number of container log files that can be present for a container. + // +optional + ContainerLogMaxFiles *int32 `json:"containerLogMaxFiles,omitempty" protobuf:"bytes,23,opt,name=containerLogMaxFiles"` + // ProtectKernelDefaults ensures that the kernel tunables are equal to the kubelet defaults. + // Defaults to true for Kubernetes v1.26 or later. + // +optional + ProtectKernelDefaults *bool `json:"protectKernelDefaults,omitempty" protobuf:"varint,24,opt,name=protectKernelDefaults"` + // StreamingConnectionIdleTimeout is the maximum time a streaming connection can be idle before the connection is automatically closed. + // This field cannot be set lower than "30s" or greater than "4h". + // Default: + // "4h" for Kubernetes < v1.26. + // "5m" for Kubernetes >= v1.26. + // +optional + StreamingConnectionIdleTimeout *metav1.Duration `json:"streamingConnectionIdleTimeout,omitempty" protobuf:"bytes,25,opt,name=streamingConnectionIdleTimeout"` + // MemorySwap configures swap memory available to container workloads. + // +optional + MemorySwap *MemorySwapConfiguration `json:"memorySwap,omitempty" protobuf:"bytes,26,opt,name=memorySwap"` +} + +// KubeletConfigEviction contains kubelet eviction thresholds supporting either a resource.Quantity or a percentage based value. +type KubeletConfigEviction struct { + // MemoryAvailable is the threshold for the free memory on the host server. + // +optional + MemoryAvailable *string `json:"memoryAvailable,omitempty" protobuf:"bytes,1,opt,name=memoryAvailable"` + // ImageFSAvailable is the threshold for the free disk space in the imagefs filesystem (docker images and container writable layers). + // +optional + ImageFSAvailable *string `json:"imageFSAvailable,omitempty" protobuf:"bytes,2,opt,name=imageFSAvailable"` + // ImageFSInodesFree is the threshold for the available inodes in the imagefs filesystem. + // +optional + ImageFSInodesFree *string `json:"imageFSInodesFree,omitempty" protobuf:"bytes,3,opt,name=imageFSInodesFree"` + // NodeFSAvailable is the threshold for the free disk space in the nodefs filesystem (docker volumes, logs, etc). + // +optional + NodeFSAvailable *string `json:"nodeFSAvailable,omitempty" protobuf:"bytes,4,opt,name=nodeFSAvailable"` + // NodeFSInodesFree is the threshold for the available inodes in the nodefs filesystem. + // +optional + NodeFSInodesFree *string `json:"nodeFSInodesFree,omitempty" protobuf:"bytes,5,opt,name=nodeFSInodesFree"` +} + +// KubeletConfigEvictionMinimumReclaim contains configuration for the kubelet eviction minimum reclaim. +type KubeletConfigEvictionMinimumReclaim struct { + // MemoryAvailable is the threshold for the memory reclaim on the host server. + // +optional + MemoryAvailable *resource.Quantity `json:"memoryAvailable,omitempty" protobuf:"bytes,1,opt,name=memoryAvailable"` + // ImageFSAvailable is the threshold for the disk space reclaim in the imagefs filesystem (docker images and container writable layers). + // +optional + ImageFSAvailable *resource.Quantity `json:"imageFSAvailable,omitempty" protobuf:"bytes,2,opt,name=imageFSAvailable"` + // ImageFSInodesFree is the threshold for the inodes reclaim in the imagefs filesystem. + // +optional + ImageFSInodesFree *resource.Quantity `json:"imageFSInodesFree,omitempty" protobuf:"bytes,3,opt,name=imageFSInodesFree"` + // NodeFSAvailable is the threshold for the disk space reclaim in the nodefs filesystem (docker volumes, logs, etc). + // +optional + NodeFSAvailable *resource.Quantity `json:"nodeFSAvailable,omitempty" protobuf:"bytes,4,opt,name=nodeFSAvailable"` + // NodeFSInodesFree is the threshold for the inodes reclaim in the nodefs filesystem. + // +optional + NodeFSInodesFree *resource.Quantity `json:"nodeFSInodesFree,omitempty" protobuf:"bytes,5,opt,name=nodeFSInodesFree"` +} + +// KubeletConfigEvictionSoftGracePeriod contains grace periods for kubelet eviction thresholds. +type KubeletConfigEvictionSoftGracePeriod struct { + // MemoryAvailable is the grace period for the MemoryAvailable eviction threshold. + // +optional + MemoryAvailable *metav1.Duration `json:"memoryAvailable,omitempty" protobuf:"bytes,1,opt,name=memoryAvailable"` + // ImageFSAvailable is the grace period for the ImageFSAvailable eviction threshold. + // +optional + ImageFSAvailable *metav1.Duration `json:"imageFSAvailable,omitempty" protobuf:"bytes,2,opt,name=imageFSAvailable"` + // ImageFSInodesFree is the grace period for the ImageFSInodesFree eviction threshold. + // +optional + ImageFSInodesFree *metav1.Duration `json:"imageFSInodesFree,omitempty" protobuf:"bytes,3,opt,name=imageFSInodesFree"` + // NodeFSAvailable is the grace period for the NodeFSAvailable eviction threshold. + // +optional + NodeFSAvailable *metav1.Duration `json:"nodeFSAvailable,omitempty" protobuf:"bytes,4,opt,name=nodeFSAvailable"` + // NodeFSInodesFree is the grace period for the NodeFSInodesFree eviction threshold. + // +optional + NodeFSInodesFree *metav1.Duration `json:"nodeFSInodesFree,omitempty" protobuf:"bytes,5,opt,name=nodeFSInodesFree"` +} + +// KubeletConfigReserved contains reserved resources for daemons +type KubeletConfigReserved struct { + // CPU is the reserved cpu. + // +optional + CPU *resource.Quantity `json:"cpu,omitempty" protobuf:"bytes,1,opt,name=cpu"` + // Memory is the reserved memory. + // +optional + Memory *resource.Quantity `json:"memory,omitempty" protobuf:"bytes,2,opt,name=memory"` + // EphemeralStorage is the reserved ephemeral-storage. + // +optional + EphemeralStorage *resource.Quantity `json:"ephemeralStorage,omitempty" protobuf:"bytes,3,opt,name=ephemeralStorage"` + // PID is the reserved process-ids. + // +optional + PID *resource.Quantity `json:"pid,omitempty" protobuf:"bytes,4,opt,name=pid"` +} + +// SwapBehavior configures swap memory available to container workloads +type SwapBehavior string + +const ( + // NoSwap is a constant for the kubelet's swap behavior restricting Kubernetes workloads to not use swap. + // Only available for Kubernetes versions >= v1.30. + NoSwap SwapBehavior = "NoSwap" + // LimitedSwap is a constant for the kubelet's swap behavior limiting the amount of swap usable for Kubernetes workloads. Workloads on the node not managed by Kubernetes can still swap. + // - cgroupsv1 host: Kubernetes workloads can use any combination of memory and swap, up to the pod's memory limit + // - cgroupsv2 host: swap is managed independently from memory. Kubernetes workloads cannot use swap memory. + LimitedSwap SwapBehavior = "LimitedSwap" + // UnlimitedSwap is a constant for the kubelet's swap behavior enabling Kubernetes workloads to use as much swap memory as required, up to the system limit (not limited by pod or container memory limits). + // Only available for Kubernetes versions < v1.30. + UnlimitedSwap SwapBehavior = "UnlimitedSwap" +) + +// MemorySwapConfiguration contains kubelet swap configuration +// For more information, please see KEP: 2400-node-swap +type MemorySwapConfiguration struct { + // SwapBehavior configures swap memory available to container workloads. May be one of {"LimitedSwap", "UnlimitedSwap"} + // defaults to: LimitedSwap + // +optional + SwapBehavior *SwapBehavior `json:"swapBehavior,omitempty" protobuf:"bytes,1,opt,name=swapBehavior"` +} + +// Networking defines networking parameters for the shoot cluster. +type Networking struct { + // Type identifies the type of the networking plugin. This field is immutable. + // +optional + Type *string `json:"type,omitempty" protobuf:"bytes,1,opt,name=type"` + // ProviderConfig is the configuration passed to network resource. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,2,opt,name=providerConfig"` + // Pods is the CIDR of the pod network. This field is immutable. + // +optional + Pods *string `json:"pods,omitempty" protobuf:"bytes,3,opt,name=pods"` + // Nodes is the CIDR of the entire node network. + // This field is mutable. + // +optional + Nodes *string `json:"nodes,omitempty" protobuf:"bytes,4,opt,name=nodes"` + // Services is the CIDR of the service network. This field is immutable. + // +optional + Services *string `json:"services,omitempty" protobuf:"bytes,5,opt,name=services"` + // IPFamilies specifies the IP protocol versions to use for shoot networking. This field is immutable. + // See https://github.com/gardener/gardener/blob/master/docs/development/ipv6.md. + // Defaults to ["IPv4"]. + // +optional + IPFamilies []IPFamily `json:"ipFamilies,omitempty" protobuf:"bytes,6,rep,name=ipFamilies,casttype=IPFamily"` +} + +const ( + // DefaultPodNetworkCIDR is a constant for the default pod network CIDR of a Shoot cluster. + DefaultPodNetworkCIDR = "100.96.0.0/11" + // DefaultServiceNetworkCIDR is a constant for the default service network CIDR of a Shoot cluster. + DefaultServiceNetworkCIDR = "100.64.0.0/13" +) + +const ( + // MaintenanceTimeWindowDurationMinimum is the minimum duration for a maintenance time window. + MaintenanceTimeWindowDurationMinimum = 30 * time.Minute + // MaintenanceTimeWindowDurationMaximum is the maximum duration for a maintenance time window. + MaintenanceTimeWindowDurationMaximum = 6 * time.Hour +) + +// Maintenance contains information about the time window for maintenance operations and which +// operations should be performed. +type Maintenance struct { + // AutoUpdate contains information about which constraints should be automatically updated. + // +optional + AutoUpdate *MaintenanceAutoUpdate `json:"autoUpdate,omitempty" protobuf:"bytes,1,opt,name=autoUpdate"` + // TimeWindow contains information about the time window for maintenance operations. + // +optional + TimeWindow *MaintenanceTimeWindow `json:"timeWindow,omitempty" protobuf:"bytes,2,opt,name=timeWindow"` + // ConfineSpecUpdateRollout prevents that changes/updates to the shoot specification will be rolled out immediately. + // Instead, they are rolled out during the shoot's maintenance time window. There is one exception that will trigger + // an immediate roll out which is changes to the Spec.Hibernation.Enabled field. + // +optional + ConfineSpecUpdateRollout *bool `json:"confineSpecUpdateRollout,omitempty" protobuf:"varint,3,opt,name=confineSpecUpdateRollout"` +} + +// MaintenanceAutoUpdate contains information about which constraints should be automatically updated. +type MaintenanceAutoUpdate struct { + // KubernetesVersion indicates whether the patch Kubernetes version may be automatically updated (default: true). + KubernetesVersion bool `json:"kubernetesVersion" protobuf:"varint,1,opt,name=kubernetesVersion"` + // MachineImageVersion indicates whether the machine image version may be automatically updated (default: true). + // +optional + MachineImageVersion *bool `json:"machineImageVersion,omitempty" protobuf:"varint,2,opt,name=machineImageVersion"` +} + +// MaintenanceTimeWindow contains information about the time window for maintenance operations. +type MaintenanceTimeWindow struct { + // Begin is the beginning of the time window in the format HHMMSS+ZONE, e.g. "220000+0100". + // If not present, a random value will be computed. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`([0-1][0-9]|2[0-3])[0-5][0-9][0-5][0-9]\+[0-1][0-4]00` + Begin string `json:"begin" protobuf:"bytes,1,opt,name=begin"` + // End is the end of the time window in the format HHMMSS+ZONE, e.g. "220000+0100". + // If not present, the value will be computed based on the "Begin" value. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern=`([0-1][0-9]|2[0-3])[0-5][0-9][0-5][0-9]\+[0-1][0-4]00` + End string `json:"end" protobuf:"bytes,2,opt,name=end"` +} + +// Monitoring contains information about the monitoring configuration for the shoot. +type Monitoring struct { + // Alerting contains information about the alerting configuration for the shoot cluster. + // +optional + Alerting *Alerting `json:"alerting,omitempty" protobuf:"bytes,1,opt,name=alerting"` +} + +// Alerting contains information about how alerting will be done (i.e. who will receive alerts and how). +type Alerting struct { + // MonitoringEmailReceivers is a list of recipients for alerts + // +optional + EmailReceivers []string `json:"emailReceivers,omitempty" protobuf:"bytes,1,rep,name=emailReceivers"` +} + +// Provider contains provider-specific information that are handed-over to the provider-specific +// extension controller. +type Provider struct { + // Type is the type of the provider. This field is immutable. + Type string `json:"type" protobuf:"bytes,1,opt,name=type"` + // ControlPlaneConfig contains the provider-specific control plane config blob. Please look up the concrete + // definition in the documentation of your provider extension. + // +optional + ControlPlaneConfig *runtime.RawExtension `json:"controlPlaneConfig,omitempty" protobuf:"bytes,2,opt,name=controlPlaneConfig"` + // InfrastructureConfig contains the provider-specific infrastructure config blob. Please look up the concrete + // definition in the documentation of your provider extension. + // +optional + InfrastructureConfig *runtime.RawExtension `json:"infrastructureConfig,omitempty" protobuf:"bytes,3,opt,name=infrastructureConfig"` + // Workers is a list of worker groups. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + Workers []Worker `json:"workers,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,4,rep,name=workers"` + // WorkersSettings contains settings for all workers. + // +optional + WorkersSettings *WorkersSettings `json:"workersSettings,omitempty" protobuf:"bytes,5,opt,name=workersSettings"` +} + +// Worker is the base definition of a worker group. +type Worker struct { + // Annotations is a map of key/value pairs for annotations for all the `Node` objects in this worker pool. + // +optional + Annotations map[string]string `json:"annotations,omitempty" protobuf:"bytes,1,rep,name=annotations"` + // CABundle is a certificate bundle which will be installed onto every machine of this worker pool. + // +optional + CABundle *string `json:"caBundle,omitempty" protobuf:"bytes,2,opt,name=caBundle"` + // CRI contains configurations of CRI support of every machine in the worker pool. + // Defaults to a CRI with name `containerd`. + // +optional + CRI *CRI `json:"cri,omitempty" protobuf:"bytes,3,opt,name=cri"` + // Kubernetes contains configuration for Kubernetes components related to this worker pool. + // +optional + Kubernetes *WorkerKubernetes `json:"kubernetes,omitempty" protobuf:"bytes,4,opt,name=kubernetes"` + // Labels is a map of key/value pairs for labels for all the `Node` objects in this worker pool. + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,5,rep,name=labels"` + // Name is the name of the worker group. + Name string `json:"name" protobuf:"bytes,6,opt,name=name"` + // Machine contains information about the machine type and image. + Machine Machine `json:"machine" protobuf:"bytes,7,opt,name=machine"` + // Maximum is the maximum number of machines to create. + // This value is divided by the number of configured zones for a fair distribution. + Maximum int32 `json:"maximum" protobuf:"varint,8,opt,name=maximum"` + // Minimum is the minimum number of machines to create. + // This value is divided by the number of configured zones for a fair distribution. + Minimum int32 `json:"minimum" protobuf:"varint,9,opt,name=minimum"` + // MaxSurge is maximum number of machines that are created during an update. + // This value is divided by the number of configured zones for a fair distribution. + // +optional + MaxSurge *intstr.IntOrString `json:"maxSurge,omitempty" protobuf:"bytes,10,opt,name=maxSurge"` + // MaxUnavailable is the maximum number of machines that can be unavailable during an update. + // This value is divided by the number of configured zones for a fair distribution. + // +optional + MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty" protobuf:"bytes,11,opt,name=maxUnavailable"` + // ProviderConfig is the provider-specific configuration for this worker pool. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,12,opt,name=providerConfig"` + // Taints is a list of taints for all the `Node` objects in this worker pool. + // +optional + Taints []corev1.Taint `json:"taints,omitempty" protobuf:"bytes,13,rep,name=taints"` + // Volume contains information about the volume type and size. + // +optional + Volume *Volume `json:"volume,omitempty" protobuf:"bytes,14,opt,name=volume"` + // DataVolumes contains a list of additional worker volumes. + // +optional + DataVolumes []DataVolume `json:"dataVolumes,omitempty" protobuf:"bytes,15,rep,name=dataVolumes"` + // KubeletDataVolumeName contains the name of a dataVolume that should be used for storing kubelet state. + // +optional + KubeletDataVolumeName *string `json:"kubeletDataVolumeName,omitempty" protobuf:"bytes,16,opt,name=kubeletDataVolumeName"` + // Zones is a list of availability zones that are used to evenly distribute this worker pool. Optional + // as not every provider may support availability zones. + // +optional + Zones []string `json:"zones,omitempty" protobuf:"bytes,17,rep,name=zones"` + // SystemComponents contains configuration for system components related to this worker pool + // +optional + SystemComponents *WorkerSystemComponents `json:"systemComponents,omitempty" protobuf:"bytes,18,opt,name=systemComponents"` + // MachineControllerManagerSettings contains configurations for different worker-pools. Eg. MachineDrainTimeout, MachineHealthTimeout. + // +optional + MachineControllerManagerSettings *MachineControllerManagerSettings `json:"machineControllerManager,omitempty" protobuf:"bytes,19,opt,name=machineControllerManager"` + // Sysctls is a map of kernel settings to apply on all machines in this worker pool. + // +optional + Sysctls map[string]string `json:"sysctls,omitempty" protobuf:"bytes,20,rep,name=sysctls"` + // ClusterAutoscaler contains the cluster autoscaler configurations for the worker pool. + // +optional + ClusterAutoscaler *ClusterAutoscalerOptions `json:"clusterAutoscaler,omitempty" protobuf:"bytes,21,opt,name=clusterAutoscaler"` + // Priority (or weight) is the importance by which this worker group will be scaled by cluster autoscaling. + // +optional + Priority *int32 `json:"priority,omitempty" protobuf:"varint,22,opt,name=priority"` +} + +// ClusterAutoscalerOptions contains the cluster autoscaler configurations for a worker pool. +type ClusterAutoscalerOptions struct { + // ScaleDownUtilizationThreshold defines the threshold in fraction (0.0 - 1.0) under which a node is being removed. + // +optional + ScaleDownUtilizationThreshold *float64 `json:"scaleDownUtilizationThreshold,omitempty" protobuf:"fixed64,1,opt,name=scaleDownUtilizationThreshold"` + // ScaleDownGpuUtilizationThreshold defines the threshold in fraction (0.0 - 1.0) of gpu resources under which a node is being removed. + // +optional + ScaleDownGpuUtilizationThreshold *float64 `json:"scaleDownGpuUtilizationThreshold,omitempty" protobuf:"fixed64,2,opt,name=scaleDownGpuUtilizationThreshold"` + // ScaleDownUnneededTime defines how long a node should be unneeded before it is eligible for scale down. + // +optional + ScaleDownUnneededTime *metav1.Duration `json:"scaleDownUnneededTime,omitempty" protobuf:"bytes,3,opt,name=scaleDownUnneededTime"` + // ScaleDownUnreadyTime defines how long an unready node should be unneeded before it is eligible for scale down. + // +optional + ScaleDownUnreadyTime *metav1.Duration `json:"scaleDownUnreadyTime,omitempty" protobuf:"bytes,4,opt,name=scaleDownUnreadyTime"` + // MaxNodeProvisionTime defines how long CA waits for node to be provisioned. + // +optional + MaxNodeProvisionTime *metav1.Duration `json:"maxNodeProvisionTime,omitempty" protobuf:"bytes,5,opt,name=maxNodeProvisionTime"` +} + +// MachineControllerManagerSettings contains configurations for different worker-pools. Eg. MachineDrainTimeout, MachineHealthTimeout. +type MachineControllerManagerSettings struct { + // MachineDrainTimeout is the period after which machine is forcefully deleted. + // +optional + MachineDrainTimeout *metav1.Duration `json:"machineDrainTimeout,omitempty" protobuf:"bytes,1,name=machineDrainTimeout"` + // MachineHealthTimeout is the period after which machine is declared failed. + // +optional + MachineHealthTimeout *metav1.Duration `json:"machineHealthTimeout,omitempty" protobuf:"bytes,2,name=machineHealthTimeout"` + // MachineCreationTimeout is the period after which creation of the machine is declared failed. + // +optional + MachineCreationTimeout *metav1.Duration `json:"machineCreationTimeout,omitempty" protobuf:"bytes,3,name=machineCreationTimeout"` + // MaxEvictRetries are the number of eviction retries on a pod after which drain is declared failed, and forceful deletion is triggered. + // +optional + MaxEvictRetries *int32 `json:"maxEvictRetries,omitempty" protobuf:"bytes,4,name=maxEvictRetries"` + // NodeConditions are the set of conditions if set to true for the period of MachineHealthTimeout, machine will be declared failed. + // +optional + NodeConditions []string `json:"nodeConditions,omitempty" protobuf:"bytes,5,name=nodeConditions"` +} + +// WorkerSystemComponents contains configuration for system components related to this worker pool +type WorkerSystemComponents struct { + // Allow determines whether the pool should be allowed to host system components or not (defaults to true) + Allow bool `json:"allow" protobuf:"bytes,1,name=allow"` +} + +// WorkerKubernetes contains configuration for Kubernetes components related to this worker pool. +type WorkerKubernetes struct { + // Kubelet contains configuration settings for all kubelets of this worker pool. + // If set, all `spec.kubernetes.kubelet` settings will be overwritten for this worker pool (no merge of settings). + // +optional + Kubelet *KubeletConfig `json:"kubelet,omitempty" protobuf:"bytes,1,opt,name=kubelet"` + // Version is the semantic Kubernetes version to use for the Kubelet in this Worker Group. + // If not specified the kubelet version is derived from the global shoot cluster kubernetes version. + // version must be equal or lower than the version of the shoot kubernetes version. + // Only one minor version difference to other worker groups and global kubernetes version is allowed. + // +optional + Version *string `json:"version,omitempty" protobuf:"bytes,2,opt,name=version"` +} + +// Machine contains information about the machine type and image. +type Machine struct { + // Type is the machine type of the worker group. + Type string `json:"type" protobuf:"bytes,1,opt,name=type"` + // Image holds information about the machine image to use for all nodes of this pool. It will default to the + // latest version of the first image stated in the referenced CloudProfile if no value has been provided. + // +optional + Image *ShootMachineImage `json:"image,omitempty" protobuf:"bytes,2,opt,name=image"` + // Architecture is CPU architecture of machines in this worker pool. + // +optional + Architecture *string `json:"architecture,omitempty" protobuf:"bytes,3,opt,name=architecture"` +} + +// ShootMachineImage defines the name and the version of the shoot's machine image in any environment. Has to be +// defined in the respective CloudProfile. +type ShootMachineImage struct { + // Name is the name of the image. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // ProviderConfig is the shoot's individual configuration passed to an extension resource. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,2,opt,name=providerConfig"` + // Version is the version of the shoot's image. + // If version is not provided, it will be defaulted to the latest version from the CloudProfile. + // +optional + Version *string `json:"version,omitempty" protobuf:"bytes,3,opt,name=version"` +} + +// Volume contains information about the volume type, size, and encryption. +type Volume struct { + // Name of the volume to make it referenceable. + // +optional + Name *string `json:"name,omitempty" protobuf:"bytes,1,opt,name=name"` + // Type is the type of the volume. + // +optional + Type *string `json:"type,omitempty" protobuf:"bytes,2,opt,name=type"` + // VolumeSize is the size of the volume. + VolumeSize string `json:"size" protobuf:"bytes,3,opt,name=size"` + // Encrypted determines if the volume should be encrypted. + // +optional + Encrypted *bool `json:"encrypted,omitempty" protobuf:"varint,4,opt,name=encrypted"` +} + +// DataVolume contains information about a data volume. +type DataVolume struct { + // Name of the volume to make it referenceable. + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Type is the type of the volume. + // +optional + Type *string `json:"type,omitempty" protobuf:"bytes,2,opt,name=type"` + // VolumeSize is the size of the volume. + VolumeSize string `json:"size" protobuf:"bytes,3,opt,name=size"` + // Encrypted determines if the volume should be encrypted. + // +optional + Encrypted *bool `json:"encrypted,omitempty" protobuf:"varint,4,opt,name=encrypted"` +} + +// CRI contains information about the Container Runtimes. +type CRI struct { + // The name of the CRI library. Supported values are `containerd`. + Name CRIName `json:"name" protobuf:"bytes,1,opt,name=name,casttype=CRIName"` + // ContainerRuntimes is the list of the required container runtimes supported for a worker pool. + // +optional + ContainerRuntimes []ContainerRuntime `json:"containerRuntimes,omitempty" protobuf:"bytes,2,rep,name=containerRuntimes"` +} + +// CRIName is a type alias for the CRI name string. +type CRIName string + +const ( + // CRINameContainerD is a constant for ContainerD CRI name. + CRINameContainerD CRIName = "containerd" +) + +// ContainerRuntime contains information about worker's available container runtime +type ContainerRuntime struct { + // Type is the type of the Container Runtime. + Type string `json:"type" protobuf:"bytes,1,opt,name=type"` + + // ProviderConfig is the configuration passed to container runtime resource. + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty" protobuf:"bytes,2,opt,name=providerConfig"` +} + +// WorkersSettings contains settings for all workers. +type WorkersSettings struct { + // SSHAccess contains settings regarding ssh access to the worker nodes. + // +optional + SSHAccess *SSHAccess `json:"sshAccess,omitempty" protobuf:"bytes,1,opt,name=sshAccess"` +} + +// SSHAccess contains settings regarding ssh access to the worker nodes. +type SSHAccess struct { + // Enabled indicates whether the SSH access to the worker nodes is ensured to be enabled or disabled in systemd. + // Defaults to true. + Enabled bool `json:"enabled" protobuf:"varint,1,opt,name=enabled"` +} + +var ( + // DefaultWorkerMaxSurge is the default value for Worker MaxSurge. + DefaultWorkerMaxSurge = intstr.FromInt32(1) + // DefaultWorkerMaxUnavailable is the default value for Worker MaxUnavailable. + DefaultWorkerMaxUnavailable = intstr.FromInt32(0) + // DefaultWorkerSystemComponentsAllow is the default value for Worker AllowSystemComponents + DefaultWorkerSystemComponentsAllow = true +) + +// SystemComponents contains the settings of system components in the control or data plane of the Shoot cluster. +type SystemComponents struct { + // CoreDNS contains the settings of the Core DNS components running in the data plane of the Shoot cluster. + // +optional + CoreDNS *CoreDNS `json:"coreDNS,omitempty" protobuf:"bytes,1,opt,name=coreDNS"` + // NodeLocalDNS contains the settings of the node local DNS components running in the data plane of the Shoot cluster. + // +optional + NodeLocalDNS *NodeLocalDNS `json:"nodeLocalDNS,omitempty" protobuf:"bytes,2,opt,name=nodeLocalDNS"` +} + +// CoreDNS contains the settings of the Core DNS components running in the data plane of the Shoot cluster. +type CoreDNS struct { + // Autoscaling contains the settings related to autoscaling of the Core DNS components running in the data plane of the Shoot cluster. + // +optional + Autoscaling *CoreDNSAutoscaling `json:"autoscaling,omitempty" protobuf:"bytes,1,opt,name=autoscaling"` + // Rewriting contains the setting related to rewriting of requests, which are obviously incorrect due to the unnecessary application of the search path. + // +optional + Rewriting *CoreDNSRewriting `json:"rewriting,omitempty" protobuf:"bytes,2,opt,name=rewriting"` +} + +// CoreDNSAutoscaling contains the settings related to autoscaling of the Core DNS components running in the data plane of the Shoot cluster. +type CoreDNSAutoscaling struct { + // The mode of the autoscaling to be used for the Core DNS components running in the data plane of the Shoot cluster. + // Supported values are `horizontal` and `cluster-proportional`. + Mode CoreDNSAutoscalingMode `json:"mode" protobuf:"bytes,1,opt,name=mode"` +} + +// CoreDNSAutoscalingMode is a type alias for the Core DNS autoscaling mode string. +type CoreDNSAutoscalingMode string + +const ( + // CoreDNSAutoscalingModeHorizontal is a constant for horizontal Core DNS autoscaling mode. + CoreDNSAutoscalingModeHorizontal CoreDNSAutoscalingMode = "horizontal" + // CoreDNSAutoscalingModeClusterProportional is a constant for cluster-proportional Core DNS autoscaling mode. + CoreDNSAutoscalingModeClusterProportional CoreDNSAutoscalingMode = "cluster-proportional" +) + +// CoreDNSRewriting contains the setting related to rewriting requests, which are obviously incorrect due to the unnecessary application of the search path. +type CoreDNSRewriting struct { + // CommonSuffixes are expected to be the suffix of a fully qualified domain name. Each suffix should contain at least one or two dots ('.') to prevent accidental clashes. + // +optional + CommonSuffixes []string `json:"commonSuffixes,omitempty" protobuf:"bytes,1,rep,name=commonSuffixes"` +} + +// NodeLocalDNS contains the settings of the node local DNS components running in the data plane of the Shoot cluster. +type NodeLocalDNS struct { + // Enabled indicates whether node local DNS is enabled or not. + Enabled bool `json:"enabled" protobuf:"varint,1,opt,name=enabled"` + // ForceTCPToClusterDNS indicates whether the connection from the node local DNS to the cluster DNS (Core DNS) will be forced to TCP or not. + // Default, if unspecified, is to enforce TCP. + // +optional + ForceTCPToClusterDNS *bool `json:"forceTCPToClusterDNS,omitempty" protobuf:"varint,2,opt,name=forceTCPToClusterDNS"` + // ForceTCPToUpstreamDNS indicates whether the connection from the node local DNS to the upstream DNS (infrastructure DNS) will be forced to TCP or not. + // Default, if unspecified, is to enforce TCP. + // +optional + ForceTCPToUpstreamDNS *bool `json:"forceTCPToUpstreamDNS,omitempty" protobuf:"varint,3,opt,name=forceTCPToUpstreamDNS"` + // DisableForwardToUpstreamDNS indicates whether requests from node local DNS to upstream DNS should be disabled. + // Default, if unspecified, is to forward requests for external domains to upstream DNS + // +optional + DisableForwardToUpstreamDNS *bool `json:"disableForwardToUpstreamDNS,omitempty" protobuf:"varint,4,opt,name=disableForwardToUpstreamDNS"` +} + +const ( + // ShootMaintenanceFailed indicates that a shoot maintenance operation failed. + ShootMaintenanceFailed = "MaintenanceFailed" + // ShootEventImageVersionMaintenance indicates that a maintenance operation regarding the image version has been performed. + ShootEventImageVersionMaintenance = "MachineImageVersionMaintenance" + // ShootEventK8sVersionMaintenance indicates that a maintenance operation regarding the K8s version has been performed. + ShootEventK8sVersionMaintenance = "KubernetesVersionMaintenance" + // ShootEventHibernationEnabled indicates that hibernation started. + ShootEventHibernationEnabled = "Hibernated" + // ShootEventHibernationDisabled indicates that hibernation ended. + ShootEventHibernationDisabled = "WokenUp" + // ShootEventSchedulingSuccessful indicates that a scheduling decision was taken successfully. + ShootEventSchedulingSuccessful = "SchedulingSuccessful" + // ShootEventSchedulingFailed indicates that a scheduling decision failed. + ShootEventSchedulingFailed = "SchedulingFailed" +) + +const ( + // ShootAPIServerAvailable is a constant for a condition type indicating that the Shoot cluster's API server is available. + ShootAPIServerAvailable ConditionType = "APIServerAvailable" + // ShootControlPlaneHealthy is a constant for a condition type indicating the health of core control plane components. + ShootControlPlaneHealthy ConditionType = "ControlPlaneHealthy" + // ShootObservabilityComponentsHealthy is a constant for a condition type indicating the health of observability components. + ShootObservabilityComponentsHealthy ConditionType = v1beta1constants.ObservabilityComponentsHealthy + // ShootEveryNodeReady is a constant for a condition type indicating the node health. + ShootEveryNodeReady ConditionType = "EveryNodeReady" + // ShootSystemComponentsHealthy is a constant for a condition type indicating the system components health. + ShootSystemComponentsHealthy ConditionType = "SystemComponentsHealthy" + // ShootHibernationPossible is a constant for a condition type indicating whether the Shoot can be hibernated. + ShootHibernationPossible ConditionType = "HibernationPossible" + // ShootMaintenancePreconditionsSatisfied is a constant for a condition type indicating whether all preconditions + // for a shoot maintenance operation are satisfied. + ShootMaintenancePreconditionsSatisfied ConditionType = "MaintenancePreconditionsSatisfied" + // ShootCACertificateValiditiesAcceptable is a constant for a condition type indicating that the validities of all + // CA certificates is long enough. + ShootCACertificateValiditiesAcceptable ConditionType = "CACertificateValiditiesAcceptable" + // ShootCRDsWithProblematicConversionWebhooks is a constant for a condition type indicating that the Shoot cluster has + // CRDs with conversion webhooks and multiple stored versions which can break the reconciliation flow of the cluster. + ShootCRDsWithProblematicConversionWebhooks ConditionType = "CRDsWithProblematicConversionWebhooks" + // ShootReadyForMigration is a constant for a condition type indicating whether the Shoot can be migrated. + ShootReadyForMigration ConditionType = "ReadyForMigration" +) + +// ShootPurpose is a type alias for string. +type ShootPurpose string + +const ( + // ShootPurposeEvaluation is a constant for the evaluation purpose. + ShootPurposeEvaluation ShootPurpose = "evaluation" + // ShootPurposeTesting is a constant for the testing purpose. + ShootPurposeTesting ShootPurpose = "testing" + // ShootPurposeDevelopment is a constant for the development purpose. + ShootPurposeDevelopment ShootPurpose = "development" + // ShootPurposeProduction is a constant for the production purpose. + ShootPurposeProduction ShootPurpose = "production" + // ShootPurposeInfrastructure is a constant for the infrastructure purpose. + ShootPurposeInfrastructure ShootPurpose = "infrastructure" +) diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_shootstate.go b/api/external/gardener/pkg/apis/core/v1beta1/types_shootstate.go new file mode 100644 index 0000000..f5c1152 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_shootstate.go @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + autoscalingv1 "k8s.io/api/autoscaling/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ShootState contains a snapshot of the Shoot's state required to migrate the Shoot's control plane to a new Seed. +type ShootState struct { + metav1.TypeMeta `json:",inline"` + // Standard object metadata. + // +optional + metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Specification of the ShootState. + // +optional + Spec ShootStateSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ShootStateList is a list of ShootState objects. +type ShootStateList struct { + metav1.TypeMeta `json:",inline"` + // Standard list object metadata. + // +optional + metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"` + // Items is the list of ShootStates. + Items []ShootState `json:"items" protobuf:"bytes,2,rep,name=items"` +} + +// ShootStateSpec is the specification of the ShootState. +type ShootStateSpec struct { + // Gardener holds the data required to generate resources deployed by the gardenlet + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + Gardener []GardenerResourceData `json:"gardener,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,1,rep,name=gardener"` + // Extensions holds the state of custom resources reconciled by extension controllers in the seed + // +optional + Extensions []ExtensionResourceState `json:"extensions,omitempty" protobuf:"bytes,2,rep,name=extensions"` + // Resources holds the data of resources referred to by extension controller states + // +optional + Resources []ResourceData `json:"resources,omitempty" protobuf:"bytes,3,rep,name=resources"` +} + +// GardenerResourceData holds the data which is used to generate resources, deployed in the Shoot's control plane. +type GardenerResourceData struct { + // Name of the object required to generate resources + Name string `json:"name" protobuf:"bytes,1,opt,name=name"` + // Type of the object + Type string `json:"type" protobuf:"bytes,2,opt,name=type"` + // Data contains the payload required to generate resources + Data runtime.RawExtension `json:"data" protobuf:"bytes,3,opt,name=data"` + // Labels are labels of the object + // +optional + Labels map[string]string `json:"labels,omitempty" protobuf:"bytes,4,opt,name=labels"` +} + +// ExtensionResourceState contains the kind of the extension custom resource and its last observed state in the Shoot's +// namespace on the Seed cluster. +type ExtensionResourceState struct { + // Kind (type) of the extension custom resource + Kind string `json:"kind" protobuf:"bytes,1,opt,name=kind"` + // Name of the extension custom resource + // +optional + Name *string `json:"name,omitempty" protobuf:"bytes,2,opt,name=name"` + // Purpose of the extension custom resource + // +optional + Purpose *string `json:"purpose,omitempty" protobuf:"bytes,3,opt,name=purpose"` + // State of the extension resource + // +optional + State *runtime.RawExtension `json:"state,omitempty" protobuf:"bytes,4,opt,name=state"` + // Resources holds a list of named resource references that can be referred to in the state by their names. + // +optional + Resources []NamedResourceReference `json:"resources,omitempty" protobuf:"bytes,5,rep,name=resources"` +} + +// ResourceData holds the data of a resource referred to by an extension controller state. +type ResourceData struct { + autoscalingv1.CrossVersionObjectReference `json:",inline" protobuf:"bytes,1,opt,name=ref"` + // Data of the resource + Data runtime.RawExtension `json:"data" protobuf:"bytes,2,opt,name=data"` +} diff --git a/api/external/gardener/pkg/apis/core/v1beta1/types_utils.go b/api/external/gardener/pkg/apis/core/v1beta1/types_utils.go new file mode 100644 index 0000000..fa1b4d3 --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/types_utils.go @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1beta1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + // EventSchedulingSuccessful is an event reason for successful scheduling. + EventSchedulingSuccessful = "SchedulingSuccessful" + // EventSchedulingFailed is an event reason for failed scheduling. + EventSchedulingFailed = "SchedulingFailed" +) + +// ConditionStatus is the status of a condition. +type ConditionStatus string + +// ConditionType is a string alias. +type ConditionType string + +// Condition holds the information about the state of a resource. +type Condition struct { + // Type of the condition. + Type ConditionType `json:"type" protobuf:"bytes,1,opt,name=type,casttype=ConditionType"` + // Status of the condition, one of True, False, Unknown. + Status ConditionStatus `json:"status" protobuf:"bytes,2,opt,name=status,casttype=ConditionStatus"` + // Last time the condition transitioned from one status to another. + LastTransitionTime metav1.Time `json:"lastTransitionTime" protobuf:"bytes,3,opt,name=lastTransitionTime"` + // Last time the condition was updated. + LastUpdateTime metav1.Time `json:"lastUpdateTime" protobuf:"bytes,4,opt,name=lastUpdateTime"` + // The reason for the condition's last transition. + Reason string `json:"reason" protobuf:"bytes,5,opt,name=reason"` + // A human readable message indicating details about the transition. + Message string `json:"message" protobuf:"bytes,6,opt,name=message"` + // Well-defined error codes in case the condition reports a problem. + // +optional + Codes []ErrorCode `json:"codes,omitempty" protobuf:"bytes,7,rep,name=codes,casttype=ErrorCode"` +} + +const ( + // ConditionTrue means a resource is in the condition. + ConditionTrue ConditionStatus = "True" + // ConditionFalse means a resource is not in the condition. + ConditionFalse ConditionStatus = "False" + // ConditionUnknown means Gardener can't decide if a resource is in the condition or not. + ConditionUnknown ConditionStatus = "Unknown" + // ConditionProgressing means the condition was seen true, failed but stayed within a predefined failure threshold. + // In the future, we could add other intermediate conditions, e.g. ConditionDegraded. + ConditionProgressing ConditionStatus = "Progressing" + + // ConditionCheckError is a constant for a reason in condition. + ConditionCheckError = "ConditionCheckError" + // ManagedResourceMissingConditionError is a constant for a reason in a condition that indicates + // one or multiple missing conditions in the observed managed resource. + ManagedResourceMissingConditionError = "MissingManagedResourceCondition" + // OutdatedStatusError is a constant for a reason in a condition that indicates + // that the observed generation in a status is outdated. + OutdatedStatusError = "OutdatedStatus" + // ManagedResourceProgressingRolloutStuck is a constant for a reason in a condition that indicates + // managed resource progressing condition is stuck in the true state for more than the threshold time. + ManagedResourceProgressingRolloutStuck = "ProgressingRolloutStuck" +) diff --git a/api/external/gardener/pkg/apis/core/v1beta1/zz_generated.deepcopy.go b/api/external/gardener/pkg/apis/core/v1beta1/zz_generated.deepcopy.go new file mode 100644 index 0000000..c4fd64a --- /dev/null +++ b/api/external/gardener/pkg/apis/core/v1beta1/zz_generated.deepcopy.go @@ -0,0 +1,5927 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1beta1 + +import ( + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" + intstr "k8s.io/apimachinery/pkg/util/intstr" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerLogging) DeepCopyInto(out *APIServerLogging) { + *out = *in + if in.Verbosity != nil { + in, out := &in.Verbosity, &out.Verbosity + *out = new(int32) + **out = **in + } + if in.HTTPAccessVerbosity != nil { + in, out := &in.HTTPAccessVerbosity, &out.HTTPAccessVerbosity + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerLogging. +func (in *APIServerLogging) DeepCopy() *APIServerLogging { + if in == nil { + return nil + } + out := new(APIServerLogging) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *APIServerRequests) DeepCopyInto(out *APIServerRequests) { + *out = *in + if in.MaxNonMutatingInflight != nil { + in, out := &in.MaxNonMutatingInflight, &out.MaxNonMutatingInflight + *out = new(int32) + **out = **in + } + if in.MaxMutatingInflight != nil { + in, out := &in.MaxMutatingInflight, &out.MaxMutatingInflight + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new APIServerRequests. +func (in *APIServerRequests) DeepCopy() *APIServerRequests { + if in == nil { + return nil + } + out := new(APIServerRequests) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessRestriction) DeepCopyInto(out *AccessRestriction) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessRestriction. +func (in *AccessRestriction) DeepCopy() *AccessRestriction { + if in == nil { + return nil + } + out := new(AccessRestriction) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AccessRestrictionWithOptions) DeepCopyInto(out *AccessRestrictionWithOptions) { + *out = *in + out.AccessRestriction = in.AccessRestriction + if in.Options != nil { + in, out := &in.Options, &out.Options + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AccessRestrictionWithOptions. +func (in *AccessRestrictionWithOptions) DeepCopy() *AccessRestrictionWithOptions { + if in == nil { + return nil + } + out := new(AccessRestrictionWithOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Addon) DeepCopyInto(out *Addon) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Addon. +func (in *Addon) DeepCopy() *Addon { + if in == nil { + return nil + } + out := new(Addon) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Addons) DeepCopyInto(out *Addons) { + *out = *in + if in.KubernetesDashboard != nil { + in, out := &in.KubernetesDashboard, &out.KubernetesDashboard + *out = new(KubernetesDashboard) + (*in).DeepCopyInto(*out) + } + if in.NginxIngress != nil { + in, out := &in.NginxIngress, &out.NginxIngress + *out = new(NginxIngress) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Addons. +func (in *Addons) DeepCopy() *Addons { + if in == nil { + return nil + } + out := new(Addons) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdmissionPlugin) DeepCopyInto(out *AdmissionPlugin) { + *out = *in + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Disabled != nil { + in, out := &in.Disabled, &out.Disabled + *out = new(bool) + **out = **in + } + if in.KubeconfigSecretName != nil { + in, out := &in.KubeconfigSecretName, &out.KubeconfigSecretName + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdmissionPlugin. +func (in *AdmissionPlugin) DeepCopy() *AdmissionPlugin { + if in == nil { + return nil + } + out := new(AdmissionPlugin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Alerting) DeepCopyInto(out *Alerting) { + *out = *in + if in.EmailReceivers != nil { + in, out := &in.EmailReceivers, &out.EmailReceivers + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Alerting. +func (in *Alerting) DeepCopy() *Alerting { + if in == nil { + return nil + } + out := new(Alerting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuditConfig) DeepCopyInto(out *AuditConfig) { + *out = *in + if in.AuditPolicy != nil { + in, out := &in.AuditPolicy, &out.AuditPolicy + *out = new(AuditPolicy) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuditConfig. +func (in *AuditConfig) DeepCopy() *AuditConfig { + if in == nil { + return nil + } + out := new(AuditConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuditPolicy) DeepCopyInto(out *AuditPolicy) { + *out = *in + if in.ConfigMapRef != nil { + in, out := &in.ConfigMapRef, &out.ConfigMapRef + *out = new(v1.ObjectReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuditPolicy. +func (in *AuditPolicy) DeepCopy() *AuditPolicy { + if in == nil { + return nil + } + out := new(AuditPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AuthorizerKubeconfigReference) DeepCopyInto(out *AuthorizerKubeconfigReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AuthorizerKubeconfigReference. +func (in *AuthorizerKubeconfigReference) DeepCopy() *AuthorizerKubeconfigReference { + if in == nil { + return nil + } + out := new(AuthorizerKubeconfigReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AvailabilityZone) DeepCopyInto(out *AvailabilityZone) { + *out = *in + if in.UnavailableMachineTypes != nil { + in, out := &in.UnavailableMachineTypes, &out.UnavailableMachineTypes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UnavailableVolumeTypes != nil { + in, out := &in.UnavailableVolumeTypes, &out.UnavailableVolumeTypes + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AvailabilityZone. +func (in *AvailabilityZone) DeepCopy() *AvailabilityZone { + if in == nil { + return nil + } + out := new(AvailabilityZone) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucket) DeepCopyInto(out *BackupBucket) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucket. +func (in *BackupBucket) DeepCopy() *BackupBucket { + if in == nil { + return nil + } + out := new(BackupBucket) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupBucket) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucketList) DeepCopyInto(out *BackupBucketList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BackupBucket, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucketList. +func (in *BackupBucketList) DeepCopy() *BackupBucketList { + if in == nil { + return nil + } + out := new(BackupBucketList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupBucketList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucketProvider) DeepCopyInto(out *BackupBucketProvider) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucketProvider. +func (in *BackupBucketProvider) DeepCopy() *BackupBucketProvider { + if in == nil { + return nil + } + out := new(BackupBucketProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucketSpec) DeepCopyInto(out *BackupBucketSpec) { + *out = *in + out.Provider = in.Provider + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + out.SecretRef = in.SecretRef + if in.SeedName != nil { + in, out := &in.SeedName, &out.SeedName + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucketSpec. +func (in *BackupBucketSpec) DeepCopy() *BackupBucketSpec { + if in == nil { + return nil + } + out := new(BackupBucketSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucketStatus) DeepCopyInto(out *BackupBucketStatus) { + *out = *in + if in.ProviderStatus != nil { + in, out := &in.ProviderStatus, &out.ProviderStatus + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.LastOperation != nil { + in, out := &in.LastOperation, &out.LastOperation + *out = new(LastOperation) + (*in).DeepCopyInto(*out) + } + if in.LastError != nil { + in, out := &in.LastError, &out.LastError + *out = new(LastError) + (*in).DeepCopyInto(*out) + } + if in.GeneratedSecretRef != nil { + in, out := &in.GeneratedSecretRef, &out.GeneratedSecretRef + *out = new(v1.SecretReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucketStatus. +func (in *BackupBucketStatus) DeepCopy() *BackupBucketStatus { + if in == nil { + return nil + } + out := new(BackupBucketStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupEntry) DeepCopyInto(out *BackupEntry) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupEntry. +func (in *BackupEntry) DeepCopy() *BackupEntry { + if in == nil { + return nil + } + out := new(BackupEntry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupEntry) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupEntryList) DeepCopyInto(out *BackupEntryList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BackupEntry, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupEntryList. +func (in *BackupEntryList) DeepCopy() *BackupEntryList { + if in == nil { + return nil + } + out := new(BackupEntryList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupEntryList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupEntrySpec) DeepCopyInto(out *BackupEntrySpec) { + *out = *in + if in.SeedName != nil { + in, out := &in.SeedName, &out.SeedName + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupEntrySpec. +func (in *BackupEntrySpec) DeepCopy() *BackupEntrySpec { + if in == nil { + return nil + } + out := new(BackupEntrySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupEntryStatus) DeepCopyInto(out *BackupEntryStatus) { + *out = *in + if in.LastOperation != nil { + in, out := &in.LastOperation, &out.LastOperation + *out = new(LastOperation) + (*in).DeepCopyInto(*out) + } + if in.LastError != nil { + in, out := &in.LastError, &out.LastError + *out = new(LastError) + (*in).DeepCopyInto(*out) + } + if in.SeedName != nil { + in, out := &in.SeedName, &out.SeedName + *out = new(string) + **out = **in + } + if in.MigrationStartTime != nil { + in, out := &in.MigrationStartTime, &out.MigrationStartTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupEntryStatus. +func (in *BackupEntryStatus) DeepCopy() *BackupEntryStatus { + if in == nil { + return nil + } + out := new(BackupEntryStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Bastion) DeepCopyInto(out *Bastion) { + *out = *in + if in.MachineImage != nil { + in, out := &in.MachineImage, &out.MachineImage + *out = new(BastionMachineImage) + (*in).DeepCopyInto(*out) + } + if in.MachineType != nil { + in, out := &in.MachineType, &out.MachineType + *out = new(BastionMachineType) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bastion. +func (in *Bastion) DeepCopy() *Bastion { + if in == nil { + return nil + } + out := new(Bastion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BastionMachineImage) DeepCopyInto(out *BastionMachineImage) { + *out = *in + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BastionMachineImage. +func (in *BastionMachineImage) DeepCopy() *BastionMachineImage { + if in == nil { + return nil + } + out := new(BastionMachineImage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BastionMachineType) DeepCopyInto(out *BastionMachineType) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BastionMachineType. +func (in *BastionMachineType) DeepCopy() *BastionMachineType { + if in == nil { + return nil + } + out := new(BastionMachineType) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CARotation) DeepCopyInto(out *CARotation) { + *out = *in + if in.LastCompletionTime != nil { + in, out := &in.LastCompletionTime, &out.LastCompletionTime + *out = (*in).DeepCopy() + } + if in.LastInitiationTime != nil { + in, out := &in.LastInitiationTime, &out.LastInitiationTime + *out = (*in).DeepCopy() + } + if in.LastInitiationFinishedTime != nil { + in, out := &in.LastInitiationFinishedTime, &out.LastInitiationFinishedTime + *out = (*in).DeepCopy() + } + if in.LastCompletionTriggeredTime != nil { + in, out := &in.LastCompletionTriggeredTime, &out.LastCompletionTriggeredTime + *out = (*in).DeepCopy() + } + if in.PendingWorkersRollouts != nil { + in, out := &in.PendingWorkersRollouts, &out.PendingWorkersRollouts + *out = make([]PendingWorkersRollout, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CARotation. +func (in *CARotation) DeepCopy() *CARotation { + if in == nil { + return nil + } + out := new(CARotation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CRI) DeepCopyInto(out *CRI) { + *out = *in + if in.ContainerRuntimes != nil { + in, out := &in.ContainerRuntimes, &out.ContainerRuntimes + *out = make([]ContainerRuntime, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRI. +func (in *CRI) DeepCopy() *CRI { + if in == nil { + return nil + } + out := new(CRI) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudProfile) DeepCopyInto(out *CloudProfile) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudProfile. +func (in *CloudProfile) DeepCopy() *CloudProfile { + if in == nil { + return nil + } + out := new(CloudProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CloudProfile) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudProfileList) DeepCopyInto(out *CloudProfileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]CloudProfile, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudProfileList. +func (in *CloudProfileList) DeepCopy() *CloudProfileList { + if in == nil { + return nil + } + out := new(CloudProfileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *CloudProfileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudProfileReference) DeepCopyInto(out *CloudProfileReference) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudProfileReference. +func (in *CloudProfileReference) DeepCopy() *CloudProfileReference { + if in == nil { + return nil + } + out := new(CloudProfileReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudProfileSpec) DeepCopyInto(out *CloudProfileSpec) { + *out = *in + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = new(string) + **out = **in + } + in.Kubernetes.DeepCopyInto(&out.Kubernetes) + if in.MachineImages != nil { + in, out := &in.MachineImages, &out.MachineImages + *out = make([]MachineImage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.MachineTypes != nil { + in, out := &in.MachineTypes, &out.MachineTypes + *out = make([]MachineType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Regions != nil { + in, out := &in.Regions, &out.Regions + *out = make([]Region, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.SeedSelector != nil { + in, out := &in.SeedSelector, &out.SeedSelector + *out = new(SeedSelector) + (*in).DeepCopyInto(*out) + } + if in.VolumeTypes != nil { + in, out := &in.VolumeTypes, &out.VolumeTypes + *out = make([]VolumeType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Bastion != nil { + in, out := &in.Bastion, &out.Bastion + *out = new(Bastion) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudProfileSpec. +func (in *CloudProfileSpec) DeepCopy() *CloudProfileSpec { + if in == nil { + return nil + } + out := new(CloudProfileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAutoscaler) DeepCopyInto(out *ClusterAutoscaler) { + *out = *in + if in.ScaleDownDelayAfterAdd != nil { + in, out := &in.ScaleDownDelayAfterAdd, &out.ScaleDownDelayAfterAdd + *out = new(metav1.Duration) + **out = **in + } + if in.ScaleDownDelayAfterDelete != nil { + in, out := &in.ScaleDownDelayAfterDelete, &out.ScaleDownDelayAfterDelete + *out = new(metav1.Duration) + **out = **in + } + if in.ScaleDownDelayAfterFailure != nil { + in, out := &in.ScaleDownDelayAfterFailure, &out.ScaleDownDelayAfterFailure + *out = new(metav1.Duration) + **out = **in + } + if in.ScaleDownUnneededTime != nil { + in, out := &in.ScaleDownUnneededTime, &out.ScaleDownUnneededTime + *out = new(metav1.Duration) + **out = **in + } + if in.ScaleDownUtilizationThreshold != nil { + in, out := &in.ScaleDownUtilizationThreshold, &out.ScaleDownUtilizationThreshold + *out = new(float64) + **out = **in + } + if in.ScanInterval != nil { + in, out := &in.ScanInterval, &out.ScanInterval + *out = new(metav1.Duration) + **out = **in + } + if in.Expander != nil { + in, out := &in.Expander, &out.Expander + *out = new(ExpanderMode) + **out = **in + } + if in.MaxNodeProvisionTime != nil { + in, out := &in.MaxNodeProvisionTime, &out.MaxNodeProvisionTime + *out = new(metav1.Duration) + **out = **in + } + if in.MaxGracefulTerminationSeconds != nil { + in, out := &in.MaxGracefulTerminationSeconds, &out.MaxGracefulTerminationSeconds + *out = new(int32) + **out = **in + } + if in.IgnoreTaints != nil { + in, out := &in.IgnoreTaints, &out.IgnoreTaints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.NewPodScaleUpDelay != nil { + in, out := &in.NewPodScaleUpDelay, &out.NewPodScaleUpDelay + *out = new(metav1.Duration) + **out = **in + } + if in.MaxEmptyBulkDelete != nil { + in, out := &in.MaxEmptyBulkDelete, &out.MaxEmptyBulkDelete + *out = new(int32) + **out = **in + } + if in.IgnoreDaemonsetsUtilization != nil { + in, out := &in.IgnoreDaemonsetsUtilization, &out.IgnoreDaemonsetsUtilization + *out = new(bool) + **out = **in + } + if in.Verbosity != nil { + in, out := &in.Verbosity, &out.Verbosity + *out = new(int32) + **out = **in + } + if in.StartupTaints != nil { + in, out := &in.StartupTaints, &out.StartupTaints + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.StatusTaints != nil { + in, out := &in.StatusTaints, &out.StatusTaints + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAutoscaler. +func (in *ClusterAutoscaler) DeepCopy() *ClusterAutoscaler { + if in == nil { + return nil + } + out := new(ClusterAutoscaler) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAutoscalerOptions) DeepCopyInto(out *ClusterAutoscalerOptions) { + *out = *in + if in.ScaleDownUtilizationThreshold != nil { + in, out := &in.ScaleDownUtilizationThreshold, &out.ScaleDownUtilizationThreshold + *out = new(float64) + **out = **in + } + if in.ScaleDownGpuUtilizationThreshold != nil { + in, out := &in.ScaleDownGpuUtilizationThreshold, &out.ScaleDownGpuUtilizationThreshold + *out = new(float64) + **out = **in + } + if in.ScaleDownUnneededTime != nil { + in, out := &in.ScaleDownUnneededTime, &out.ScaleDownUnneededTime + *out = new(metav1.Duration) + **out = **in + } + if in.ScaleDownUnreadyTime != nil { + in, out := &in.ScaleDownUnreadyTime, &out.ScaleDownUnreadyTime + *out = new(metav1.Duration) + **out = **in + } + if in.MaxNodeProvisionTime != nil { + in, out := &in.MaxNodeProvisionTime, &out.MaxNodeProvisionTime + *out = new(metav1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAutoscalerOptions. +func (in *ClusterAutoscalerOptions) DeepCopy() *ClusterAutoscalerOptions { + if in == nil { + return nil + } + out := new(ClusterAutoscalerOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Condition) DeepCopyInto(out *Condition) { + *out = *in + in.LastTransitionTime.DeepCopyInto(&out.LastTransitionTime) + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + if in.Codes != nil { + in, out := &in.Codes, &out.Codes + *out = make([]ErrorCode, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Condition. +func (in *Condition) DeepCopy() *Condition { + if in == nil { + return nil + } + out := new(Condition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerRuntime) DeepCopyInto(out *ContainerRuntime) { + *out = *in + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerRuntime. +func (in *ContainerRuntime) DeepCopy() *ContainerRuntime { + if in == nil { + return nil + } + out := new(ContainerRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlane) DeepCopyInto(out *ControlPlane) { + *out = *in + if in.HighAvailability != nil { + in, out := &in.HighAvailability, &out.HighAvailability + *out = new(HighAvailability) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlane. +func (in *ControlPlane) DeepCopy() *ControlPlane { + if in == nil { + return nil + } + out := new(ControlPlane) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerDeployment) DeepCopyInto(out *ControllerDeployment) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.ProviderConfig.DeepCopyInto(&out.ProviderConfig) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerDeployment. +func (in *ControllerDeployment) DeepCopy() *ControllerDeployment { + if in == nil { + return nil + } + out := new(ControllerDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerDeployment) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerDeploymentList) DeepCopyInto(out *ControllerDeploymentList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ControllerDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerDeploymentList. +func (in *ControllerDeploymentList) DeepCopy() *ControllerDeploymentList { + if in == nil { + return nil + } + out := new(ControllerDeploymentList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerDeploymentList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerInstallation) DeepCopyInto(out *ControllerInstallation) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerInstallation. +func (in *ControllerInstallation) DeepCopy() *ControllerInstallation { + if in == nil { + return nil + } + out := new(ControllerInstallation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerInstallation) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerInstallationList) DeepCopyInto(out *ControllerInstallationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ControllerInstallation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerInstallationList. +func (in *ControllerInstallationList) DeepCopy() *ControllerInstallationList { + if in == nil { + return nil + } + out := new(ControllerInstallationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerInstallationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerInstallationSpec) DeepCopyInto(out *ControllerInstallationSpec) { + *out = *in + out.RegistrationRef = in.RegistrationRef + out.SeedRef = in.SeedRef + if in.DeploymentRef != nil { + in, out := &in.DeploymentRef, &out.DeploymentRef + *out = new(v1.ObjectReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerInstallationSpec. +func (in *ControllerInstallationSpec) DeepCopy() *ControllerInstallationSpec { + if in == nil { + return nil + } + out := new(ControllerInstallationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerInstallationStatus) DeepCopyInto(out *ControllerInstallationStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProviderStatus != nil { + in, out := &in.ProviderStatus, &out.ProviderStatus + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerInstallationStatus. +func (in *ControllerInstallationStatus) DeepCopy() *ControllerInstallationStatus { + if in == nil { + return nil + } + out := new(ControllerInstallationStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerRegistration) DeepCopyInto(out *ControllerRegistration) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerRegistration. +func (in *ControllerRegistration) DeepCopy() *ControllerRegistration { + if in == nil { + return nil + } + out := new(ControllerRegistration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerRegistration) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerRegistrationDeployment) DeepCopyInto(out *ControllerRegistrationDeployment) { + *out = *in + if in.Policy != nil { + in, out := &in.Policy, &out.Policy + *out = new(ControllerDeploymentPolicy) + **out = **in + } + if in.SeedSelector != nil { + in, out := &in.SeedSelector, &out.SeedSelector + *out = new(metav1.LabelSelector) + (*in).DeepCopyInto(*out) + } + if in.DeploymentRefs != nil { + in, out := &in.DeploymentRefs, &out.DeploymentRefs + *out = make([]DeploymentRef, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerRegistrationDeployment. +func (in *ControllerRegistrationDeployment) DeepCopy() *ControllerRegistrationDeployment { + if in == nil { + return nil + } + out := new(ControllerRegistrationDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerRegistrationList) DeepCopyInto(out *ControllerRegistrationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ControllerRegistration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerRegistrationList. +func (in *ControllerRegistrationList) DeepCopy() *ControllerRegistrationList { + if in == nil { + return nil + } + out := new(ControllerRegistrationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControllerRegistrationList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerRegistrationSpec) DeepCopyInto(out *ControllerRegistrationSpec) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ControllerResource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Deployment != nil { + in, out := &in.Deployment, &out.Deployment + *out = new(ControllerRegistrationDeployment) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerRegistrationSpec. +func (in *ControllerRegistrationSpec) DeepCopy() *ControllerRegistrationSpec { + if in == nil { + return nil + } + out := new(ControllerRegistrationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerResource) DeepCopyInto(out *ControllerResource) { + *out = *in + if in.GloballyEnabled != nil { + in, out := &in.GloballyEnabled, &out.GloballyEnabled + *out = new(bool) + **out = **in + } + if in.ReconcileTimeout != nil { + in, out := &in.ReconcileTimeout, &out.ReconcileTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.Primary != nil { + in, out := &in.Primary, &out.Primary + *out = new(bool) + **out = **in + } + if in.Lifecycle != nil { + in, out := &in.Lifecycle, &out.Lifecycle + *out = new(ControllerResourceLifecycle) + (*in).DeepCopyInto(*out) + } + if in.WorkerlessSupported != nil { + in, out := &in.WorkerlessSupported, &out.WorkerlessSupported + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerResource. +func (in *ControllerResource) DeepCopy() *ControllerResource { + if in == nil { + return nil + } + out := new(ControllerResource) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControllerResourceLifecycle) DeepCopyInto(out *ControllerResourceLifecycle) { + *out = *in + if in.Reconcile != nil { + in, out := &in.Reconcile, &out.Reconcile + *out = new(ControllerResourceLifecycleStrategy) + **out = **in + } + if in.Delete != nil { + in, out := &in.Delete, &out.Delete + *out = new(ControllerResourceLifecycleStrategy) + **out = **in + } + if in.Migrate != nil { + in, out := &in.Migrate, &out.Migrate + *out = new(ControllerResourceLifecycleStrategy) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControllerResourceLifecycle. +func (in *ControllerResourceLifecycle) DeepCopy() *ControllerResourceLifecycle { + if in == nil { + return nil + } + out := new(ControllerResourceLifecycle) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CoreDNS) DeepCopyInto(out *CoreDNS) { + *out = *in + if in.Autoscaling != nil { + in, out := &in.Autoscaling, &out.Autoscaling + *out = new(CoreDNSAutoscaling) + **out = **in + } + if in.Rewriting != nil { + in, out := &in.Rewriting, &out.Rewriting + *out = new(CoreDNSRewriting) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreDNS. +func (in *CoreDNS) DeepCopy() *CoreDNS { + if in == nil { + return nil + } + out := new(CoreDNS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CoreDNSAutoscaling) DeepCopyInto(out *CoreDNSAutoscaling) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreDNSAutoscaling. +func (in *CoreDNSAutoscaling) DeepCopy() *CoreDNSAutoscaling { + if in == nil { + return nil + } + out := new(CoreDNSAutoscaling) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CoreDNSRewriting) DeepCopyInto(out *CoreDNSRewriting) { + *out = *in + if in.CommonSuffixes != nil { + in, out := &in.CommonSuffixes, &out.CommonSuffixes + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CoreDNSRewriting. +func (in *CoreDNSRewriting) DeepCopy() *CoreDNSRewriting { + if in == nil { + return nil + } + out := new(CoreDNSRewriting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNS) DeepCopyInto(out *DNS) { + *out = *in + if in.Domain != nil { + in, out := &in.Domain, &out.Domain + *out = new(string) + **out = **in + } + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]DNSProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNS. +func (in *DNS) DeepCopy() *DNS { + if in == nil { + return nil + } + out := new(DNS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSIncludeExclude) DeepCopyInto(out *DNSIncludeExclude) { + *out = *in + if in.Include != nil { + in, out := &in.Include, &out.Include + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Exclude != nil { + in, out := &in.Exclude, &out.Exclude + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSIncludeExclude. +func (in *DNSIncludeExclude) DeepCopy() *DNSIncludeExclude { + if in == nil { + return nil + } + out := new(DNSIncludeExclude) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSProvider) DeepCopyInto(out *DNSProvider) { + *out = *in + if in.Domains != nil { + in, out := &in.Domains, &out.Domains + *out = new(DNSIncludeExclude) + (*in).DeepCopyInto(*out) + } + if in.Primary != nil { + in, out := &in.Primary, &out.Primary + *out = new(bool) + **out = **in + } + if in.SecretName != nil { + in, out := &in.SecretName, &out.SecretName + *out = new(string) + **out = **in + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + if in.Zones != nil { + in, out := &in.Zones, &out.Zones + *out = new(DNSIncludeExclude) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSProvider. +func (in *DNSProvider) DeepCopy() *DNSProvider { + if in == nil { + return nil + } + out := new(DNSProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataVolume) DeepCopyInto(out *DataVolume) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + if in.Encrypted != nil { + in, out := &in.Encrypted, &out.Encrypted + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataVolume. +func (in *DataVolume) DeepCopy() *DataVolume { + if in == nil { + return nil + } + out := new(DataVolume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DeploymentRef) DeepCopyInto(out *DeploymentRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DeploymentRef. +func (in *DeploymentRef) DeepCopy() *DeploymentRef { + if in == nil { + return nil + } + out := new(DeploymentRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DualApprovalForDeletion) DeepCopyInto(out *DualApprovalForDeletion) { + *out = *in + in.Selector.DeepCopyInto(&out.Selector) + if in.IncludeServiceAccounts != nil { + in, out := &in.IncludeServiceAccounts, &out.IncludeServiceAccounts + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DualApprovalForDeletion. +func (in *DualApprovalForDeletion) DeepCopy() *DualApprovalForDeletion { + if in == nil { + return nil + } + out := new(DualApprovalForDeletion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ETCDEncryptionKeyRotation) DeepCopyInto(out *ETCDEncryptionKeyRotation) { + *out = *in + if in.LastCompletionTime != nil { + in, out := &in.LastCompletionTime, &out.LastCompletionTime + *out = (*in).DeepCopy() + } + if in.LastInitiationTime != nil { + in, out := &in.LastInitiationTime, &out.LastInitiationTime + *out = (*in).DeepCopy() + } + if in.LastInitiationFinishedTime != nil { + in, out := &in.LastInitiationFinishedTime, &out.LastInitiationFinishedTime + *out = (*in).DeepCopy() + } + if in.LastCompletionTriggeredTime != nil { + in, out := &in.LastCompletionTriggeredTime, &out.LastCompletionTriggeredTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ETCDEncryptionKeyRotation. +func (in *ETCDEncryptionKeyRotation) DeepCopy() *ETCDEncryptionKeyRotation { + if in == nil { + return nil + } + out := new(ETCDEncryptionKeyRotation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *EncryptionConfig) DeepCopyInto(out *EncryptionConfig) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EncryptionConfig. +func (in *EncryptionConfig) DeepCopy() *EncryptionConfig { + if in == nil { + return nil + } + out := new(EncryptionConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExpirableVersion) DeepCopyInto(out *ExpirableVersion) { + *out = *in + if in.ExpirationDate != nil { + in, out := &in.ExpirationDate, &out.ExpirationDate + *out = (*in).DeepCopy() + } + if in.Classification != nil { + in, out := &in.Classification, &out.Classification + *out = new(VersionClassification) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExpirableVersion. +func (in *ExpirableVersion) DeepCopy() *ExpirableVersion { + if in == nil { + return nil + } + out := new(ExpirableVersion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExposureClass) DeepCopyInto(out *ExposureClass) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Scheduling != nil { + in, out := &in.Scheduling, &out.Scheduling + *out = new(ExposureClassScheduling) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExposureClass. +func (in *ExposureClass) DeepCopy() *ExposureClass { + if in == nil { + return nil + } + out := new(ExposureClass) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExposureClass) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExposureClassList) DeepCopyInto(out *ExposureClassList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ExposureClass, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExposureClassList. +func (in *ExposureClassList) DeepCopy() *ExposureClassList { + if in == nil { + return nil + } + out := new(ExposureClassList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExposureClassList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExposureClassScheduling) DeepCopyInto(out *ExposureClassScheduling) { + *out = *in + if in.SeedSelector != nil { + in, out := &in.SeedSelector, &out.SeedSelector + *out = new(SeedSelector) + (*in).DeepCopyInto(*out) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExposureClassScheduling. +func (in *ExposureClassScheduling) DeepCopy() *ExposureClassScheduling { + if in == nil { + return nil + } + out := new(ExposureClassScheduling) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Extension) DeepCopyInto(out *Extension) { + *out = *in + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Disabled != nil { + in, out := &in.Disabled, &out.Disabled + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extension. +func (in *Extension) DeepCopy() *Extension { + if in == nil { + return nil + } + out := new(Extension) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionResourceState) DeepCopyInto(out *ExtensionResourceState) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Purpose != nil { + in, out := &in.Purpose, &out.Purpose + *out = new(string) + **out = **in + } + if in.State != nil { + in, out := &in.State, &out.State + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]NamedResourceReference, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionResourceState. +func (in *ExtensionResourceState) DeepCopy() *ExtensionResourceState { + if in == nil { + return nil + } + out := new(ExtensionResourceState) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FailureTolerance) DeepCopyInto(out *FailureTolerance) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FailureTolerance. +func (in *FailureTolerance) DeepCopy() *FailureTolerance { + if in == nil { + return nil + } + out := new(FailureTolerance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Gardener) DeepCopyInto(out *Gardener) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Gardener. +func (in *Gardener) DeepCopy() *Gardener { + if in == nil { + return nil + } + out := new(Gardener) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GardenerResourceData) DeepCopyInto(out *GardenerResourceData) { + *out = *in + in.Data.DeepCopyInto(&out.Data) + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GardenerResourceData. +func (in *GardenerResourceData) DeepCopy() *GardenerResourceData { + if in == nil { + return nil + } + out := new(GardenerResourceData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HelmControllerDeployment) DeepCopyInto(out *HelmControllerDeployment) { + *out = *in + if in.Chart != nil { + in, out := &in.Chart, &out.Chart + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } + if in.OCIRepository != nil { + in, out := &in.OCIRepository, &out.OCIRepository + *out = new(OCIRepository) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HelmControllerDeployment. +func (in *HelmControllerDeployment) DeepCopy() *HelmControllerDeployment { + if in == nil { + return nil + } + out := new(HelmControllerDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Hibernation) DeepCopyInto(out *Hibernation) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Schedules != nil { + in, out := &in.Schedules, &out.Schedules + *out = make([]HibernationSchedule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Hibernation. +func (in *Hibernation) DeepCopy() *Hibernation { + if in == nil { + return nil + } + out := new(Hibernation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HibernationSchedule) DeepCopyInto(out *HibernationSchedule) { + *out = *in + if in.Start != nil { + in, out := &in.Start, &out.Start + *out = new(string) + **out = **in + } + if in.End != nil { + in, out := &in.End, &out.End + *out = new(string) + **out = **in + } + if in.Location != nil { + in, out := &in.Location, &out.Location + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HibernationSchedule. +func (in *HibernationSchedule) DeepCopy() *HibernationSchedule { + if in == nil { + return nil + } + out := new(HibernationSchedule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HighAvailability) DeepCopyInto(out *HighAvailability) { + *out = *in + out.FailureTolerance = in.FailureTolerance + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HighAvailability. +func (in *HighAvailability) DeepCopy() *HighAvailability { + if in == nil { + return nil + } + out := new(HighAvailability) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *HorizontalPodAutoscalerConfig) DeepCopyInto(out *HorizontalPodAutoscalerConfig) { + *out = *in + if in.CPUInitializationPeriod != nil { + in, out := &in.CPUInitializationPeriod, &out.CPUInitializationPeriod + *out = new(metav1.Duration) + **out = **in + } + if in.DownscaleStabilization != nil { + in, out := &in.DownscaleStabilization, &out.DownscaleStabilization + *out = new(metav1.Duration) + **out = **in + } + if in.InitialReadinessDelay != nil { + in, out := &in.InitialReadinessDelay, &out.InitialReadinessDelay + *out = new(metav1.Duration) + **out = **in + } + if in.SyncPeriod != nil { + in, out := &in.SyncPeriod, &out.SyncPeriod + *out = new(metav1.Duration) + **out = **in + } + if in.Tolerance != nil { + in, out := &in.Tolerance, &out.Tolerance + *out = new(float64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new HorizontalPodAutoscalerConfig. +func (in *HorizontalPodAutoscalerConfig) DeepCopy() *HorizontalPodAutoscalerConfig { + if in == nil { + return nil + } + out := new(HorizontalPodAutoscalerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Ingress) DeepCopyInto(out *Ingress) { + *out = *in + in.Controller.DeepCopyInto(&out.Controller) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Ingress. +func (in *Ingress) DeepCopy() *Ingress { + if in == nil { + return nil + } + out := new(Ingress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *IngressController) DeepCopyInto(out *IngressController) { + *out = *in + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IngressController. +func (in *IngressController) DeepCopy() *IngressController { + if in == nil { + return nil + } + out := new(IngressController) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalSecret) DeepCopyInto(out *InternalSecret) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + if in.Immutable != nil { + in, out := &in.Immutable, &out.Immutable + *out = new(bool) + **out = **in + } + if in.Data != nil { + in, out := &in.Data, &out.Data + *out = make(map[string][]byte, len(*in)) + for key, val := range *in { + var outVal []byte + if val == nil { + (*out)[key] = nil + } else { + in, out := &val, &outVal + *out = make([]byte, len(*in)) + copy(*out, *in) + } + (*out)[key] = outVal + } + } + if in.StringData != nil { + in, out := &in.StringData, &out.StringData + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalSecret. +func (in *InternalSecret) DeepCopy() *InternalSecret { + if in == nil { + return nil + } + out := new(InternalSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InternalSecret) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InternalSecretList) DeepCopyInto(out *InternalSecretList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]InternalSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InternalSecretList. +func (in *InternalSecretList) DeepCopy() *InternalSecretList { + if in == nil { + return nil + } + out := new(InternalSecretList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InternalSecretList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeAPIServerConfig) DeepCopyInto(out *KubeAPIServerConfig) { + *out = *in + in.KubernetesConfig.DeepCopyInto(&out.KubernetesConfig) + if in.AdmissionPlugins != nil { + in, out := &in.AdmissionPlugins, &out.AdmissionPlugins + *out = make([]AdmissionPlugin, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.APIAudiences != nil { + in, out := &in.APIAudiences, &out.APIAudiences + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AuditConfig != nil { + in, out := &in.AuditConfig, &out.AuditConfig + *out = new(AuditConfig) + (*in).DeepCopyInto(*out) + } + if in.OIDCConfig != nil { + in, out := &in.OIDCConfig, &out.OIDCConfig + *out = new(OIDCConfig) + (*in).DeepCopyInto(*out) + } + if in.RuntimeConfig != nil { + in, out := &in.RuntimeConfig, &out.RuntimeConfig + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ServiceAccountConfig != nil { + in, out := &in.ServiceAccountConfig, &out.ServiceAccountConfig + *out = new(ServiceAccountConfig) + (*in).DeepCopyInto(*out) + } + if in.WatchCacheSizes != nil { + in, out := &in.WatchCacheSizes, &out.WatchCacheSizes + *out = new(WatchCacheSizes) + (*in).DeepCopyInto(*out) + } + if in.Requests != nil { + in, out := &in.Requests, &out.Requests + *out = new(APIServerRequests) + (*in).DeepCopyInto(*out) + } + if in.EnableAnonymousAuthentication != nil { + in, out := &in.EnableAnonymousAuthentication, &out.EnableAnonymousAuthentication + *out = new(bool) + **out = **in + } + if in.EventTTL != nil { + in, out := &in.EventTTL, &out.EventTTL + *out = new(metav1.Duration) + **out = **in + } + if in.Logging != nil { + in, out := &in.Logging, &out.Logging + *out = new(APIServerLogging) + (*in).DeepCopyInto(*out) + } + if in.DefaultNotReadyTolerationSeconds != nil { + in, out := &in.DefaultNotReadyTolerationSeconds, &out.DefaultNotReadyTolerationSeconds + *out = new(int64) + **out = **in + } + if in.DefaultUnreachableTolerationSeconds != nil { + in, out := &in.DefaultUnreachableTolerationSeconds, &out.DefaultUnreachableTolerationSeconds + *out = new(int64) + **out = **in + } + if in.EncryptionConfig != nil { + in, out := &in.EncryptionConfig, &out.EncryptionConfig + *out = new(EncryptionConfig) + (*in).DeepCopyInto(*out) + } + if in.StructuredAuthentication != nil { + in, out := &in.StructuredAuthentication, &out.StructuredAuthentication + *out = new(StructuredAuthentication) + **out = **in + } + if in.StructuredAuthorization != nil { + in, out := &in.StructuredAuthorization, &out.StructuredAuthorization + *out = new(StructuredAuthorization) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeAPIServerConfig. +func (in *KubeAPIServerConfig) DeepCopy() *KubeAPIServerConfig { + if in == nil { + return nil + } + out := new(KubeAPIServerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeControllerManagerConfig) DeepCopyInto(out *KubeControllerManagerConfig) { + *out = *in + in.KubernetesConfig.DeepCopyInto(&out.KubernetesConfig) + if in.HorizontalPodAutoscalerConfig != nil { + in, out := &in.HorizontalPodAutoscalerConfig, &out.HorizontalPodAutoscalerConfig + *out = new(HorizontalPodAutoscalerConfig) + (*in).DeepCopyInto(*out) + } + if in.NodeCIDRMaskSize != nil { + in, out := &in.NodeCIDRMaskSize, &out.NodeCIDRMaskSize + *out = new(int32) + **out = **in + } + if in.PodEvictionTimeout != nil { + in, out := &in.PodEvictionTimeout, &out.PodEvictionTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.NodeMonitorGracePeriod != nil { + in, out := &in.NodeMonitorGracePeriod, &out.NodeMonitorGracePeriod + *out = new(metav1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeControllerManagerConfig. +func (in *KubeControllerManagerConfig) DeepCopy() *KubeControllerManagerConfig { + if in == nil { + return nil + } + out := new(KubeControllerManagerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeProxyConfig) DeepCopyInto(out *KubeProxyConfig) { + *out = *in + in.KubernetesConfig.DeepCopyInto(&out.KubernetesConfig) + if in.Mode != nil { + in, out := &in.Mode, &out.Mode + *out = new(ProxyMode) + **out = **in + } + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeProxyConfig. +func (in *KubeProxyConfig) DeepCopy() *KubeProxyConfig { + if in == nil { + return nil + } + out := new(KubeProxyConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeSchedulerConfig) DeepCopyInto(out *KubeSchedulerConfig) { + *out = *in + in.KubernetesConfig.DeepCopyInto(&out.KubernetesConfig) + if in.KubeMaxPDVols != nil { + in, out := &in.KubeMaxPDVols, &out.KubeMaxPDVols + *out = new(string) + **out = **in + } + if in.Profile != nil { + in, out := &in.Profile, &out.Profile + *out = new(SchedulingProfile) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeSchedulerConfig. +func (in *KubeSchedulerConfig) DeepCopy() *KubeSchedulerConfig { + if in == nil { + return nil + } + out := new(KubeSchedulerConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeletConfig) DeepCopyInto(out *KubeletConfig) { + *out = *in + in.KubernetesConfig.DeepCopyInto(&out.KubernetesConfig) + if in.CPUCFSQuota != nil { + in, out := &in.CPUCFSQuota, &out.CPUCFSQuota + *out = new(bool) + **out = **in + } + if in.CPUManagerPolicy != nil { + in, out := &in.CPUManagerPolicy, &out.CPUManagerPolicy + *out = new(string) + **out = **in + } + if in.EvictionHard != nil { + in, out := &in.EvictionHard, &out.EvictionHard + *out = new(KubeletConfigEviction) + (*in).DeepCopyInto(*out) + } + if in.EvictionMaxPodGracePeriod != nil { + in, out := &in.EvictionMaxPodGracePeriod, &out.EvictionMaxPodGracePeriod + *out = new(int32) + **out = **in + } + if in.EvictionMinimumReclaim != nil { + in, out := &in.EvictionMinimumReclaim, &out.EvictionMinimumReclaim + *out = new(KubeletConfigEvictionMinimumReclaim) + (*in).DeepCopyInto(*out) + } + if in.EvictionPressureTransitionPeriod != nil { + in, out := &in.EvictionPressureTransitionPeriod, &out.EvictionPressureTransitionPeriod + *out = new(metav1.Duration) + **out = **in + } + if in.EvictionSoft != nil { + in, out := &in.EvictionSoft, &out.EvictionSoft + *out = new(KubeletConfigEviction) + (*in).DeepCopyInto(*out) + } + if in.EvictionSoftGracePeriod != nil { + in, out := &in.EvictionSoftGracePeriod, &out.EvictionSoftGracePeriod + *out = new(KubeletConfigEvictionSoftGracePeriod) + (*in).DeepCopyInto(*out) + } + if in.MaxPods != nil { + in, out := &in.MaxPods, &out.MaxPods + *out = new(int32) + **out = **in + } + if in.PodPIDsLimit != nil { + in, out := &in.PodPIDsLimit, &out.PodPIDsLimit + *out = new(int64) + **out = **in + } + if in.FailSwapOn != nil { + in, out := &in.FailSwapOn, &out.FailSwapOn + *out = new(bool) + **out = **in + } + if in.KubeReserved != nil { + in, out := &in.KubeReserved, &out.KubeReserved + *out = new(KubeletConfigReserved) + (*in).DeepCopyInto(*out) + } + if in.SystemReserved != nil { + in, out := &in.SystemReserved, &out.SystemReserved + *out = new(KubeletConfigReserved) + (*in).DeepCopyInto(*out) + } + if in.ImageGCHighThresholdPercent != nil { + in, out := &in.ImageGCHighThresholdPercent, &out.ImageGCHighThresholdPercent + *out = new(int32) + **out = **in + } + if in.ImageGCLowThresholdPercent != nil { + in, out := &in.ImageGCLowThresholdPercent, &out.ImageGCLowThresholdPercent + *out = new(int32) + **out = **in + } + if in.SerializeImagePulls != nil { + in, out := &in.SerializeImagePulls, &out.SerializeImagePulls + *out = new(bool) + **out = **in + } + if in.RegistryPullQPS != nil { + in, out := &in.RegistryPullQPS, &out.RegistryPullQPS + *out = new(int32) + **out = **in + } + if in.RegistryBurst != nil { + in, out := &in.RegistryBurst, &out.RegistryBurst + *out = new(int32) + **out = **in + } + if in.SeccompDefault != nil { + in, out := &in.SeccompDefault, &out.SeccompDefault + *out = new(bool) + **out = **in + } + if in.ContainerLogMaxSize != nil { + in, out := &in.ContainerLogMaxSize, &out.ContainerLogMaxSize + x := (*in).DeepCopy() + *out = &x + } + if in.ContainerLogMaxFiles != nil { + in, out := &in.ContainerLogMaxFiles, &out.ContainerLogMaxFiles + *out = new(int32) + **out = **in + } + if in.ProtectKernelDefaults != nil { + in, out := &in.ProtectKernelDefaults, &out.ProtectKernelDefaults + *out = new(bool) + **out = **in + } + if in.StreamingConnectionIdleTimeout != nil { + in, out := &in.StreamingConnectionIdleTimeout, &out.StreamingConnectionIdleTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.MemorySwap != nil { + in, out := &in.MemorySwap, &out.MemorySwap + *out = new(MemorySwapConfiguration) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeletConfig. +func (in *KubeletConfig) DeepCopy() *KubeletConfig { + if in == nil { + return nil + } + out := new(KubeletConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeletConfigEviction) DeepCopyInto(out *KubeletConfigEviction) { + *out = *in + if in.MemoryAvailable != nil { + in, out := &in.MemoryAvailable, &out.MemoryAvailable + *out = new(string) + **out = **in + } + if in.ImageFSAvailable != nil { + in, out := &in.ImageFSAvailable, &out.ImageFSAvailable + *out = new(string) + **out = **in + } + if in.ImageFSInodesFree != nil { + in, out := &in.ImageFSInodesFree, &out.ImageFSInodesFree + *out = new(string) + **out = **in + } + if in.NodeFSAvailable != nil { + in, out := &in.NodeFSAvailable, &out.NodeFSAvailable + *out = new(string) + **out = **in + } + if in.NodeFSInodesFree != nil { + in, out := &in.NodeFSInodesFree, &out.NodeFSInodesFree + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeletConfigEviction. +func (in *KubeletConfigEviction) DeepCopy() *KubeletConfigEviction { + if in == nil { + return nil + } + out := new(KubeletConfigEviction) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeletConfigEvictionMinimumReclaim) DeepCopyInto(out *KubeletConfigEvictionMinimumReclaim) { + *out = *in + if in.MemoryAvailable != nil { + in, out := &in.MemoryAvailable, &out.MemoryAvailable + x := (*in).DeepCopy() + *out = &x + } + if in.ImageFSAvailable != nil { + in, out := &in.ImageFSAvailable, &out.ImageFSAvailable + x := (*in).DeepCopy() + *out = &x + } + if in.ImageFSInodesFree != nil { + in, out := &in.ImageFSInodesFree, &out.ImageFSInodesFree + x := (*in).DeepCopy() + *out = &x + } + if in.NodeFSAvailable != nil { + in, out := &in.NodeFSAvailable, &out.NodeFSAvailable + x := (*in).DeepCopy() + *out = &x + } + if in.NodeFSInodesFree != nil { + in, out := &in.NodeFSInodesFree, &out.NodeFSInodesFree + x := (*in).DeepCopy() + *out = &x + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeletConfigEvictionMinimumReclaim. +func (in *KubeletConfigEvictionMinimumReclaim) DeepCopy() *KubeletConfigEvictionMinimumReclaim { + if in == nil { + return nil + } + out := new(KubeletConfigEvictionMinimumReclaim) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeletConfigEvictionSoftGracePeriod) DeepCopyInto(out *KubeletConfigEvictionSoftGracePeriod) { + *out = *in + if in.MemoryAvailable != nil { + in, out := &in.MemoryAvailable, &out.MemoryAvailable + *out = new(metav1.Duration) + **out = **in + } + if in.ImageFSAvailable != nil { + in, out := &in.ImageFSAvailable, &out.ImageFSAvailable + *out = new(metav1.Duration) + **out = **in + } + if in.ImageFSInodesFree != nil { + in, out := &in.ImageFSInodesFree, &out.ImageFSInodesFree + *out = new(metav1.Duration) + **out = **in + } + if in.NodeFSAvailable != nil { + in, out := &in.NodeFSAvailable, &out.NodeFSAvailable + *out = new(metav1.Duration) + **out = **in + } + if in.NodeFSInodesFree != nil { + in, out := &in.NodeFSInodesFree, &out.NodeFSInodesFree + *out = new(metav1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeletConfigEvictionSoftGracePeriod. +func (in *KubeletConfigEvictionSoftGracePeriod) DeepCopy() *KubeletConfigEvictionSoftGracePeriod { + if in == nil { + return nil + } + out := new(KubeletConfigEvictionSoftGracePeriod) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubeletConfigReserved) DeepCopyInto(out *KubeletConfigReserved) { + *out = *in + if in.CPU != nil { + in, out := &in.CPU, &out.CPU + x := (*in).DeepCopy() + *out = &x + } + if in.Memory != nil { + in, out := &in.Memory, &out.Memory + x := (*in).DeepCopy() + *out = &x + } + if in.EphemeralStorage != nil { + in, out := &in.EphemeralStorage, &out.EphemeralStorage + x := (*in).DeepCopy() + *out = &x + } + if in.PID != nil { + in, out := &in.PID, &out.PID + x := (*in).DeepCopy() + *out = &x + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubeletConfigReserved. +func (in *KubeletConfigReserved) DeepCopy() *KubeletConfigReserved { + if in == nil { + return nil + } + out := new(KubeletConfigReserved) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Kubernetes) DeepCopyInto(out *Kubernetes) { + *out = *in + if in.ClusterAutoscaler != nil { + in, out := &in.ClusterAutoscaler, &out.ClusterAutoscaler + *out = new(ClusterAutoscaler) + (*in).DeepCopyInto(*out) + } + if in.KubeAPIServer != nil { + in, out := &in.KubeAPIServer, &out.KubeAPIServer + *out = new(KubeAPIServerConfig) + (*in).DeepCopyInto(*out) + } + if in.KubeControllerManager != nil { + in, out := &in.KubeControllerManager, &out.KubeControllerManager + *out = new(KubeControllerManagerConfig) + (*in).DeepCopyInto(*out) + } + if in.KubeScheduler != nil { + in, out := &in.KubeScheduler, &out.KubeScheduler + *out = new(KubeSchedulerConfig) + (*in).DeepCopyInto(*out) + } + if in.KubeProxy != nil { + in, out := &in.KubeProxy, &out.KubeProxy + *out = new(KubeProxyConfig) + (*in).DeepCopyInto(*out) + } + if in.Kubelet != nil { + in, out := &in.Kubelet, &out.Kubelet + *out = new(KubeletConfig) + (*in).DeepCopyInto(*out) + } + if in.VerticalPodAutoscaler != nil { + in, out := &in.VerticalPodAutoscaler, &out.VerticalPodAutoscaler + *out = new(VerticalPodAutoscaler) + (*in).DeepCopyInto(*out) + } + if in.EnableStaticTokenKubeconfig != nil { + in, out := &in.EnableStaticTokenKubeconfig, &out.EnableStaticTokenKubeconfig + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Kubernetes. +func (in *Kubernetes) DeepCopy() *Kubernetes { + if in == nil { + return nil + } + out := new(Kubernetes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesConfig) DeepCopyInto(out *KubernetesConfig) { + *out = *in + if in.FeatureGates != nil { + in, out := &in.FeatureGates, &out.FeatureGates + *out = make(map[string]bool, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesConfig. +func (in *KubernetesConfig) DeepCopy() *KubernetesConfig { + if in == nil { + return nil + } + out := new(KubernetesConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesDashboard) DeepCopyInto(out *KubernetesDashboard) { + *out = *in + out.Addon = in.Addon + if in.AuthenticationMode != nil { + in, out := &in.AuthenticationMode, &out.AuthenticationMode + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesDashboard. +func (in *KubernetesDashboard) DeepCopy() *KubernetesDashboard { + if in == nil { + return nil + } + out := new(KubernetesDashboard) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *KubernetesSettings) DeepCopyInto(out *KubernetesSettings) { + *out = *in + if in.Versions != nil { + in, out := &in.Versions, &out.Versions + *out = make([]ExpirableVersion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new KubernetesSettings. +func (in *KubernetesSettings) DeepCopy() *KubernetesSettings { + if in == nil { + return nil + } + out := new(KubernetesSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LastError) DeepCopyInto(out *LastError) { + *out = *in + if in.TaskID != nil { + in, out := &in.TaskID, &out.TaskID + *out = new(string) + **out = **in + } + if in.Codes != nil { + in, out := &in.Codes, &out.Codes + *out = make([]ErrorCode, len(*in)) + copy(*out, *in) + } + if in.LastUpdateTime != nil { + in, out := &in.LastUpdateTime, &out.LastUpdateTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastError. +func (in *LastError) DeepCopy() *LastError { + if in == nil { + return nil + } + out := new(LastError) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LastMaintenance) DeepCopyInto(out *LastMaintenance) { + *out = *in + in.TriggeredTime.DeepCopyInto(&out.TriggeredTime) + if in.FailureReason != nil { + in, out := &in.FailureReason, &out.FailureReason + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastMaintenance. +func (in *LastMaintenance) DeepCopy() *LastMaintenance { + if in == nil { + return nil + } + out := new(LastMaintenance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LastOperation) DeepCopyInto(out *LastOperation) { + *out = *in + in.LastUpdateTime.DeepCopyInto(&out.LastUpdateTime) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LastOperation. +func (in *LastOperation) DeepCopy() *LastOperation { + if in == nil { + return nil + } + out := new(LastOperation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *LoadBalancerServicesProxyProtocol) DeepCopyInto(out *LoadBalancerServicesProxyProtocol) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new LoadBalancerServicesProxyProtocol. +func (in *LoadBalancerServicesProxyProtocol) DeepCopy() *LoadBalancerServicesProxyProtocol { + if in == nil { + return nil + } + out := new(LoadBalancerServicesProxyProtocol) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Machine) DeepCopyInto(out *Machine) { + *out = *in + if in.Image != nil { + in, out := &in.Image, &out.Image + *out = new(ShootMachineImage) + (*in).DeepCopyInto(*out) + } + if in.Architecture != nil { + in, out := &in.Architecture, &out.Architecture + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Machine. +func (in *Machine) DeepCopy() *Machine { + if in == nil { + return nil + } + out := new(Machine) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineControllerManagerSettings) DeepCopyInto(out *MachineControllerManagerSettings) { + *out = *in + if in.MachineDrainTimeout != nil { + in, out := &in.MachineDrainTimeout, &out.MachineDrainTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.MachineHealthTimeout != nil { + in, out := &in.MachineHealthTimeout, &out.MachineHealthTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.MachineCreationTimeout != nil { + in, out := &in.MachineCreationTimeout, &out.MachineCreationTimeout + *out = new(metav1.Duration) + **out = **in + } + if in.MaxEvictRetries != nil { + in, out := &in.MaxEvictRetries, &out.MaxEvictRetries + *out = new(int32) + **out = **in + } + if in.NodeConditions != nil { + in, out := &in.NodeConditions, &out.NodeConditions + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineControllerManagerSettings. +func (in *MachineControllerManagerSettings) DeepCopy() *MachineControllerManagerSettings { + if in == nil { + return nil + } + out := new(MachineControllerManagerSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineImage) DeepCopyInto(out *MachineImage) { + *out = *in + if in.Versions != nil { + in, out := &in.Versions, &out.Versions + *out = make([]MachineImageVersion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.UpdateStrategy != nil { + in, out := &in.UpdateStrategy, &out.UpdateStrategy + *out = new(MachineImageUpdateStrategy) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineImage. +func (in *MachineImage) DeepCopy() *MachineImage { + if in == nil { + return nil + } + out := new(MachineImage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineImageVersion) DeepCopyInto(out *MachineImageVersion) { + *out = *in + in.ExpirableVersion.DeepCopyInto(&out.ExpirableVersion) + if in.CRI != nil { + in, out := &in.CRI, &out.CRI + *out = make([]CRI, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Architectures != nil { + in, out := &in.Architectures, &out.Architectures + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.KubeletVersionConstraint != nil { + in, out := &in.KubeletVersionConstraint, &out.KubeletVersionConstraint + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineImageVersion. +func (in *MachineImageVersion) DeepCopy() *MachineImageVersion { + if in == nil { + return nil + } + out := new(MachineImageVersion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineType) DeepCopyInto(out *MachineType) { + *out = *in + out.CPU = in.CPU.DeepCopy() + out.GPU = in.GPU.DeepCopy() + out.Memory = in.Memory.DeepCopy() + if in.Storage != nil { + in, out := &in.Storage, &out.Storage + *out = new(MachineTypeStorage) + (*in).DeepCopyInto(*out) + } + if in.Usable != nil { + in, out := &in.Usable, &out.Usable + *out = new(bool) + **out = **in + } + if in.Architecture != nil { + in, out := &in.Architecture, &out.Architecture + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineType. +func (in *MachineType) DeepCopy() *MachineType { + if in == nil { + return nil + } + out := new(MachineType) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineTypeStorage) DeepCopyInto(out *MachineTypeStorage) { + *out = *in + if in.StorageSize != nil { + in, out := &in.StorageSize, &out.StorageSize + x := (*in).DeepCopy() + *out = &x + } + if in.MinSize != nil { + in, out := &in.MinSize, &out.MinSize + x := (*in).DeepCopy() + *out = &x + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineTypeStorage. +func (in *MachineTypeStorage) DeepCopy() *MachineTypeStorage { + if in == nil { + return nil + } + out := new(MachineTypeStorage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Maintenance) DeepCopyInto(out *Maintenance) { + *out = *in + if in.AutoUpdate != nil { + in, out := &in.AutoUpdate, &out.AutoUpdate + *out = new(MaintenanceAutoUpdate) + (*in).DeepCopyInto(*out) + } + if in.TimeWindow != nil { + in, out := &in.TimeWindow, &out.TimeWindow + *out = new(MaintenanceTimeWindow) + **out = **in + } + if in.ConfineSpecUpdateRollout != nil { + in, out := &in.ConfineSpecUpdateRollout, &out.ConfineSpecUpdateRollout + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Maintenance. +func (in *Maintenance) DeepCopy() *Maintenance { + if in == nil { + return nil + } + out := new(Maintenance) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MaintenanceAutoUpdate) DeepCopyInto(out *MaintenanceAutoUpdate) { + *out = *in + if in.MachineImageVersion != nil { + in, out := &in.MachineImageVersion, &out.MachineImageVersion + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MaintenanceAutoUpdate. +func (in *MaintenanceAutoUpdate) DeepCopy() *MaintenanceAutoUpdate { + if in == nil { + return nil + } + out := new(MaintenanceAutoUpdate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MaintenanceTimeWindow) DeepCopyInto(out *MaintenanceTimeWindow) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MaintenanceTimeWindow. +func (in *MaintenanceTimeWindow) DeepCopy() *MaintenanceTimeWindow { + if in == nil { + return nil + } + out := new(MaintenanceTimeWindow) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MemorySwapConfiguration) DeepCopyInto(out *MemorySwapConfiguration) { + *out = *in + if in.SwapBehavior != nil { + in, out := &in.SwapBehavior, &out.SwapBehavior + *out = new(SwapBehavior) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MemorySwapConfiguration. +func (in *MemorySwapConfiguration) DeepCopy() *MemorySwapConfiguration { + if in == nil { + return nil + } + out := new(MemorySwapConfiguration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Monitoring) DeepCopyInto(out *Monitoring) { + *out = *in + if in.Alerting != nil { + in, out := &in.Alerting, &out.Alerting + *out = new(Alerting) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Monitoring. +func (in *Monitoring) DeepCopy() *Monitoring { + if in == nil { + return nil + } + out := new(Monitoring) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamedResourceReference) DeepCopyInto(out *NamedResourceReference) { + *out = *in + out.ResourceRef = in.ResourceRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamedResourceReference. +func (in *NamedResourceReference) DeepCopy() *NamedResourceReference { + if in == nil { + return nil + } + out := new(NamedResourceReference) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedCloudProfile) DeepCopyInto(out *NamespacedCloudProfile) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedCloudProfile. +func (in *NamespacedCloudProfile) DeepCopy() *NamespacedCloudProfile { + if in == nil { + return nil + } + out := new(NamespacedCloudProfile) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NamespacedCloudProfile) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedCloudProfileList) DeepCopyInto(out *NamespacedCloudProfileList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NamespacedCloudProfile, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedCloudProfileList. +func (in *NamespacedCloudProfileList) DeepCopy() *NamespacedCloudProfileList { + if in == nil { + return nil + } + out := new(NamespacedCloudProfileList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NamespacedCloudProfileList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedCloudProfileSpec) DeepCopyInto(out *NamespacedCloudProfileSpec) { + *out = *in + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = new(string) + **out = **in + } + if in.Kubernetes != nil { + in, out := &in.Kubernetes, &out.Kubernetes + *out = new(KubernetesSettings) + (*in).DeepCopyInto(*out) + } + if in.MachineImages != nil { + in, out := &in.MachineImages, &out.MachineImages + *out = make([]MachineImage, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.MachineTypes != nil { + in, out := &in.MachineTypes, &out.MachineTypes + *out = make([]MachineType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.VolumeTypes != nil { + in, out := &in.VolumeTypes, &out.VolumeTypes + *out = make([]VolumeType, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.Parent = in.Parent + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedCloudProfileSpec. +func (in *NamespacedCloudProfileSpec) DeepCopy() *NamespacedCloudProfileSpec { + if in == nil { + return nil + } + out := new(NamespacedCloudProfileSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespacedCloudProfileStatus) DeepCopyInto(out *NamespacedCloudProfileStatus) { + *out = *in + in.CloudProfileSpec.DeepCopyInto(&out.CloudProfileSpec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespacedCloudProfileStatus. +func (in *NamespacedCloudProfileStatus) DeepCopy() *NamespacedCloudProfileStatus { + if in == nil { + return nil + } + out := new(NamespacedCloudProfileStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Networking) DeepCopyInto(out *Networking) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Pods != nil { + in, out := &in.Pods, &out.Pods + *out = new(string) + **out = **in + } + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = new(string) + **out = **in + } + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = new(string) + **out = **in + } + if in.IPFamilies != nil { + in, out := &in.IPFamilies, &out.IPFamilies + *out = make([]IPFamily, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Networking. +func (in *Networking) DeepCopy() *Networking { + if in == nil { + return nil + } + out := new(Networking) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkingStatus) DeepCopyInto(out *NetworkingStatus) { + *out = *in + if in.Pods != nil { + in, out := &in.Pods, &out.Pods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.EgressCIDRs != nil { + in, out := &in.EgressCIDRs, &out.EgressCIDRs + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkingStatus. +func (in *NetworkingStatus) DeepCopy() *NetworkingStatus { + if in == nil { + return nil + } + out := new(NetworkingStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NginxIngress) DeepCopyInto(out *NginxIngress) { + *out = *in + out.Addon = in.Addon + if in.LoadBalancerSourceRanges != nil { + in, out := &in.LoadBalancerSourceRanges, &out.LoadBalancerSourceRanges + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Config != nil { + in, out := &in.Config, &out.Config + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ExternalTrafficPolicy != nil { + in, out := &in.ExternalTrafficPolicy, &out.ExternalTrafficPolicy + *out = new(v1.ServiceExternalTrafficPolicy) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxIngress. +func (in *NginxIngress) DeepCopy() *NginxIngress { + if in == nil { + return nil + } + out := new(NginxIngress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeLocalDNS) DeepCopyInto(out *NodeLocalDNS) { + *out = *in + if in.ForceTCPToClusterDNS != nil { + in, out := &in.ForceTCPToClusterDNS, &out.ForceTCPToClusterDNS + *out = new(bool) + **out = **in + } + if in.ForceTCPToUpstreamDNS != nil { + in, out := &in.ForceTCPToUpstreamDNS, &out.ForceTCPToUpstreamDNS + *out = new(bool) + **out = **in + } + if in.DisableForwardToUpstreamDNS != nil { + in, out := &in.DisableForwardToUpstreamDNS, &out.DisableForwardToUpstreamDNS + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeLocalDNS. +func (in *NodeLocalDNS) DeepCopy() *NodeLocalDNS { + if in == nil { + return nil + } + out := new(NodeLocalDNS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OCIRepository) DeepCopyInto(out *OCIRepository) { + *out = *in + if in.Ref != nil { + in, out := &in.Ref, &out.Ref + *out = new(string) + **out = **in + } + if in.Repository != nil { + in, out := &in.Repository, &out.Repository + *out = new(string) + **out = **in + } + if in.Tag != nil { + in, out := &in.Tag, &out.Tag + *out = new(string) + **out = **in + } + if in.Digest != nil { + in, out := &in.Digest, &out.Digest + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OCIRepository. +func (in *OCIRepository) DeepCopy() *OCIRepository { + if in == nil { + return nil + } + out := new(OCIRepository) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDCConfig) DeepCopyInto(out *OIDCConfig) { + *out = *in + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = new(string) + **out = **in + } + if in.ClientAuthentication != nil { + in, out := &in.ClientAuthentication, &out.ClientAuthentication + *out = new(OpenIDConnectClientAuthentication) + (*in).DeepCopyInto(*out) + } + if in.ClientID != nil { + in, out := &in.ClientID, &out.ClientID + *out = new(string) + **out = **in + } + if in.GroupsClaim != nil { + in, out := &in.GroupsClaim, &out.GroupsClaim + *out = new(string) + **out = **in + } + if in.GroupsPrefix != nil { + in, out := &in.GroupsPrefix, &out.GroupsPrefix + *out = new(string) + **out = **in + } + if in.IssuerURL != nil { + in, out := &in.IssuerURL, &out.IssuerURL + *out = new(string) + **out = **in + } + if in.RequiredClaims != nil { + in, out := &in.RequiredClaims, &out.RequiredClaims + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.SigningAlgs != nil { + in, out := &in.SigningAlgs, &out.SigningAlgs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.UsernameClaim != nil { + in, out := &in.UsernameClaim, &out.UsernameClaim + *out = new(string) + **out = **in + } + if in.UsernamePrefix != nil { + in, out := &in.UsernamePrefix, &out.UsernamePrefix + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCConfig. +func (in *OIDCConfig) DeepCopy() *OIDCConfig { + if in == nil { + return nil + } + out := new(OIDCConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ObservabilityRotation) DeepCopyInto(out *ObservabilityRotation) { + *out = *in + if in.LastInitiationTime != nil { + in, out := &in.LastInitiationTime, &out.LastInitiationTime + *out = (*in).DeepCopy() + } + if in.LastCompletionTime != nil { + in, out := &in.LastCompletionTime, &out.LastCompletionTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ObservabilityRotation. +func (in *ObservabilityRotation) DeepCopy() *ObservabilityRotation { + if in == nil { + return nil + } + out := new(ObservabilityRotation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenIDConnectClientAuthentication) DeepCopyInto(out *OpenIDConnectClientAuthentication) { + *out = *in + if in.ExtraConfig != nil { + in, out := &in.ExtraConfig, &out.ExtraConfig + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Secret != nil { + in, out := &in.Secret, &out.Secret + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenIDConnectClientAuthentication. +func (in *OpenIDConnectClientAuthentication) DeepCopy() *OpenIDConnectClientAuthentication { + if in == nil { + return nil + } + out := new(OpenIDConnectClientAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PendingWorkersRollout) DeepCopyInto(out *PendingWorkersRollout) { + *out = *in + if in.LastInitiationTime != nil { + in, out := &in.LastInitiationTime, &out.LastInitiationTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PendingWorkersRollout. +func (in *PendingWorkersRollout) DeepCopy() *PendingWorkersRollout { + if in == nil { + return nil + } + out := new(PendingWorkersRollout) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Project) DeepCopyInto(out *Project) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Project. +func (in *Project) DeepCopy() *Project { + if in == nil { + return nil + } + out := new(Project) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Project) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectList) DeepCopyInto(out *ProjectList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Project, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectList. +func (in *ProjectList) DeepCopy() *ProjectList { + if in == nil { + return nil + } + out := new(ProjectList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ProjectList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectMember) DeepCopyInto(out *ProjectMember) { + *out = *in + out.Subject = in.Subject + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectMember. +func (in *ProjectMember) DeepCopy() *ProjectMember { + if in == nil { + return nil + } + out := new(ProjectMember) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectSpec) DeepCopyInto(out *ProjectSpec) { + *out = *in + if in.CreatedBy != nil { + in, out := &in.CreatedBy, &out.CreatedBy + *out = new(rbacv1.Subject) + **out = **in + } + if in.Description != nil { + in, out := &in.Description, &out.Description + *out = new(string) + **out = **in + } + if in.Owner != nil { + in, out := &in.Owner, &out.Owner + *out = new(rbacv1.Subject) + **out = **in + } + if in.Purpose != nil { + in, out := &in.Purpose, &out.Purpose + *out = new(string) + **out = **in + } + if in.Members != nil { + in, out := &in.Members, &out.Members + *out = make([]ProjectMember, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Namespace != nil { + in, out := &in.Namespace, &out.Namespace + *out = new(string) + **out = **in + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = new(ProjectTolerations) + (*in).DeepCopyInto(*out) + } + if in.DualApprovalForDeletion != nil { + in, out := &in.DualApprovalForDeletion, &out.DualApprovalForDeletion + *out = make([]DualApprovalForDeletion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectSpec. +func (in *ProjectSpec) DeepCopy() *ProjectSpec { + if in == nil { + return nil + } + out := new(ProjectSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectStatus) DeepCopyInto(out *ProjectStatus) { + *out = *in + if in.StaleSinceTimestamp != nil { + in, out := &in.StaleSinceTimestamp, &out.StaleSinceTimestamp + *out = (*in).DeepCopy() + } + if in.StaleAutoDeleteTimestamp != nil { + in, out := &in.StaleAutoDeleteTimestamp, &out.StaleAutoDeleteTimestamp + *out = (*in).DeepCopy() + } + if in.LastActivityTimestamp != nil { + in, out := &in.LastActivityTimestamp, &out.LastActivityTimestamp + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectStatus. +func (in *ProjectStatus) DeepCopy() *ProjectStatus { + if in == nil { + return nil + } + out := new(ProjectStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProjectTolerations) DeepCopyInto(out *ProjectTolerations) { + *out = *in + if in.Defaults != nil { + in, out := &in.Defaults, &out.Defaults + *out = make([]Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Whitelist != nil { + in, out := &in.Whitelist, &out.Whitelist + *out = make([]Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProjectTolerations. +func (in *ProjectTolerations) DeepCopy() *ProjectTolerations { + if in == nil { + return nil + } + out := new(ProjectTolerations) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Provider) DeepCopyInto(out *Provider) { + *out = *in + if in.ControlPlaneConfig != nil { + in, out := &in.ControlPlaneConfig, &out.ControlPlaneConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.InfrastructureConfig != nil { + in, out := &in.InfrastructureConfig, &out.InfrastructureConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Workers != nil { + in, out := &in.Workers, &out.Workers + *out = make([]Worker, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.WorkersSettings != nil { + in, out := &in.WorkersSettings, &out.WorkersSettings + *out = new(WorkersSettings) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Provider. +func (in *Provider) DeepCopy() *Provider { + if in == nil { + return nil + } + out := new(Provider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Quota) DeepCopyInto(out *Quota) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Quota. +func (in *Quota) DeepCopy() *Quota { + if in == nil { + return nil + } + out := new(Quota) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Quota) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *QuotaList) DeepCopyInto(out *QuotaList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Quota, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuotaList. +func (in *QuotaList) DeepCopy() *QuotaList { + if in == nil { + return nil + } + out := new(QuotaList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *QuotaList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *QuotaSpec) DeepCopyInto(out *QuotaSpec) { + *out = *in + if in.ClusterLifetimeDays != nil { + in, out := &in.ClusterLifetimeDays, &out.ClusterLifetimeDays + *out = new(int32) + **out = **in + } + if in.Metrics != nil { + in, out := &in.Metrics, &out.Metrics + *out = make(v1.ResourceList, len(*in)) + for key, val := range *in { + (*out)[key] = val.DeepCopy() + } + } + out.Scope = in.Scope + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new QuotaSpec. +func (in *QuotaSpec) DeepCopy() *QuotaSpec { + if in == nil { + return nil + } + out := new(QuotaSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Region) DeepCopyInto(out *Region) { + *out = *in + if in.Zones != nil { + in, out := &in.Zones, &out.Zones + *out = make([]AvailabilityZone, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.AccessRestrictions != nil { + in, out := &in.AccessRestrictions, &out.AccessRestrictions + *out = make([]AccessRestriction, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Region. +func (in *Region) DeepCopy() *Region { + if in == nil { + return nil + } + out := new(Region) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceData) DeepCopyInto(out *ResourceData) { + *out = *in + out.CrossVersionObjectReference = in.CrossVersionObjectReference + in.Data.DeepCopyInto(&out.Data) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceData. +func (in *ResourceData) DeepCopy() *ResourceData { + if in == nil { + return nil + } + out := new(ResourceData) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ResourceWatchCacheSize) DeepCopyInto(out *ResourceWatchCacheSize) { + *out = *in + if in.APIGroup != nil { + in, out := &in.APIGroup, &out.APIGroup + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceWatchCacheSize. +func (in *ResourceWatchCacheSize) DeepCopy() *ResourceWatchCacheSize { + if in == nil { + return nil + } + out := new(ResourceWatchCacheSize) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SSHAccess) DeepCopyInto(out *SSHAccess) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SSHAccess. +func (in *SSHAccess) DeepCopy() *SSHAccess { + if in == nil { + return nil + } + out := new(SSHAccess) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretBinding) DeepCopyInto(out *SecretBinding) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.SecretRef = in.SecretRef + if in.Quotas != nil { + in, out := &in.Quotas, &out.Quotas + *out = make([]v1.ObjectReference, len(*in)) + copy(*out, *in) + } + if in.Provider != nil { + in, out := &in.Provider, &out.Provider + *out = new(SecretBindingProvider) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretBinding. +func (in *SecretBinding) DeepCopy() *SecretBinding { + if in == nil { + return nil + } + out := new(SecretBinding) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SecretBinding) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretBindingList) DeepCopyInto(out *SecretBindingList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]SecretBinding, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretBindingList. +func (in *SecretBindingList) DeepCopy() *SecretBindingList { + if in == nil { + return nil + } + out := new(SecretBindingList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SecretBindingList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretBindingProvider) DeepCopyInto(out *SecretBindingProvider) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretBindingProvider. +func (in *SecretBindingProvider) DeepCopy() *SecretBindingProvider { + if in == nil { + return nil + } + out := new(SecretBindingProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Seed) DeepCopyInto(out *Seed) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Seed. +func (in *Seed) DeepCopy() *Seed { + if in == nil { + return nil + } + out := new(Seed) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Seed) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedBackup) DeepCopyInto(out *SeedBackup) { + *out = *in + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Region != nil { + in, out := &in.Region, &out.Region + *out = new(string) + **out = **in + } + out.SecretRef = in.SecretRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedBackup. +func (in *SeedBackup) DeepCopy() *SeedBackup { + if in == nil { + return nil + } + out := new(SeedBackup) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedDNS) DeepCopyInto(out *SeedDNS) { + *out = *in + if in.Provider != nil { + in, out := &in.Provider, &out.Provider + *out = new(SeedDNSProvider) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedDNS. +func (in *SeedDNS) DeepCopy() *SeedDNS { + if in == nil { + return nil + } + out := new(SeedDNS) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedDNSProvider) DeepCopyInto(out *SeedDNSProvider) { + *out = *in + out.SecretRef = in.SecretRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedDNSProvider. +func (in *SeedDNSProvider) DeepCopy() *SeedDNSProvider { + if in == nil { + return nil + } + out := new(SeedDNSProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedList) DeepCopyInto(out *SeedList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Seed, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedList. +func (in *SeedList) DeepCopy() *SeedList { + if in == nil { + return nil + } + out := new(SeedList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *SeedList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedNetworks) DeepCopyInto(out *SeedNetworks) { + *out = *in + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = new(string) + **out = **in + } + if in.ShootDefaults != nil { + in, out := &in.ShootDefaults, &out.ShootDefaults + *out = new(ShootNetworks) + (*in).DeepCopyInto(*out) + } + if in.BlockCIDRs != nil { + in, out := &in.BlockCIDRs, &out.BlockCIDRs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.IPFamilies != nil { + in, out := &in.IPFamilies, &out.IPFamilies + *out = make([]IPFamily, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedNetworks. +func (in *SeedNetworks) DeepCopy() *SeedNetworks { + if in == nil { + return nil + } + out := new(SeedNetworks) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedProvider) DeepCopyInto(out *SeedProvider) { + *out = *in + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Zones != nil { + in, out := &in.Zones, &out.Zones + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedProvider. +func (in *SeedProvider) DeepCopy() *SeedProvider { + if in == nil { + return nil + } + out := new(SeedProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSelector) DeepCopyInto(out *SeedSelector) { + *out = *in + in.LabelSelector.DeepCopyInto(&out.LabelSelector) + if in.ProviderTypes != nil { + in, out := &in.ProviderTypes, &out.ProviderTypes + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSelector. +func (in *SeedSelector) DeepCopy() *SeedSelector { + if in == nil { + return nil + } + out := new(SeedSelector) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingDependencyWatchdog) DeepCopyInto(out *SeedSettingDependencyWatchdog) { + *out = *in + if in.Weeder != nil { + in, out := &in.Weeder, &out.Weeder + *out = new(SeedSettingDependencyWatchdogWeeder) + **out = **in + } + if in.Prober != nil { + in, out := &in.Prober, &out.Prober + *out = new(SeedSettingDependencyWatchdogProber) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingDependencyWatchdog. +func (in *SeedSettingDependencyWatchdog) DeepCopy() *SeedSettingDependencyWatchdog { + if in == nil { + return nil + } + out := new(SeedSettingDependencyWatchdog) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingDependencyWatchdogProber) DeepCopyInto(out *SeedSettingDependencyWatchdogProber) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingDependencyWatchdogProber. +func (in *SeedSettingDependencyWatchdogProber) DeepCopy() *SeedSettingDependencyWatchdogProber { + if in == nil { + return nil + } + out := new(SeedSettingDependencyWatchdogProber) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingDependencyWatchdogWeeder) DeepCopyInto(out *SeedSettingDependencyWatchdogWeeder) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingDependencyWatchdogWeeder. +func (in *SeedSettingDependencyWatchdogWeeder) DeepCopy() *SeedSettingDependencyWatchdogWeeder { + if in == nil { + return nil + } + out := new(SeedSettingDependencyWatchdogWeeder) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingExcessCapacityReservation) DeepCopyInto(out *SeedSettingExcessCapacityReservation) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.Configs != nil { + in, out := &in.Configs, &out.Configs + *out = make([]SeedSettingExcessCapacityReservationConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingExcessCapacityReservation. +func (in *SeedSettingExcessCapacityReservation) DeepCopy() *SeedSettingExcessCapacityReservation { + if in == nil { + return nil + } + out := new(SeedSettingExcessCapacityReservation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingExcessCapacityReservationConfig) DeepCopyInto(out *SeedSettingExcessCapacityReservationConfig) { + *out = *in + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make(v1.ResourceList, len(*in)) + for key, val := range *in { + (*out)[key] = val.DeepCopy() + } + } + if in.NodeSelector != nil { + in, out := &in.NodeSelector, &out.NodeSelector + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]v1.Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingExcessCapacityReservationConfig. +func (in *SeedSettingExcessCapacityReservationConfig) DeepCopy() *SeedSettingExcessCapacityReservationConfig { + if in == nil { + return nil + } + out := new(SeedSettingExcessCapacityReservationConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingLoadBalancerServices) DeepCopyInto(out *SeedSettingLoadBalancerServices) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ExternalTrafficPolicy != nil { + in, out := &in.ExternalTrafficPolicy, &out.ExternalTrafficPolicy + *out = new(v1.ServiceExternalTrafficPolicy) + **out = **in + } + if in.Zones != nil { + in, out := &in.Zones, &out.Zones + *out = make([]SeedSettingLoadBalancerServicesZones, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ProxyProtocol != nil { + in, out := &in.ProxyProtocol, &out.ProxyProtocol + *out = new(LoadBalancerServicesProxyProtocol) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingLoadBalancerServices. +func (in *SeedSettingLoadBalancerServices) DeepCopy() *SeedSettingLoadBalancerServices { + if in == nil { + return nil + } + out := new(SeedSettingLoadBalancerServices) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingLoadBalancerServicesZones) DeepCopyInto(out *SeedSettingLoadBalancerServicesZones) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ExternalTrafficPolicy != nil { + in, out := &in.ExternalTrafficPolicy, &out.ExternalTrafficPolicy + *out = new(v1.ServiceExternalTrafficPolicy) + **out = **in + } + if in.ProxyProtocol != nil { + in, out := &in.ProxyProtocol, &out.ProxyProtocol + *out = new(LoadBalancerServicesProxyProtocol) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingLoadBalancerServicesZones. +func (in *SeedSettingLoadBalancerServicesZones) DeepCopy() *SeedSettingLoadBalancerServicesZones { + if in == nil { + return nil + } + out := new(SeedSettingLoadBalancerServicesZones) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingScheduling) DeepCopyInto(out *SeedSettingScheduling) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingScheduling. +func (in *SeedSettingScheduling) DeepCopy() *SeedSettingScheduling { + if in == nil { + return nil + } + out := new(SeedSettingScheduling) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingTopologyAwareRouting) DeepCopyInto(out *SeedSettingTopologyAwareRouting) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingTopologyAwareRouting. +func (in *SeedSettingTopologyAwareRouting) DeepCopy() *SeedSettingTopologyAwareRouting { + if in == nil { + return nil + } + out := new(SeedSettingTopologyAwareRouting) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettingVerticalPodAutoscaler) DeepCopyInto(out *SeedSettingVerticalPodAutoscaler) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettingVerticalPodAutoscaler. +func (in *SeedSettingVerticalPodAutoscaler) DeepCopy() *SeedSettingVerticalPodAutoscaler { + if in == nil { + return nil + } + out := new(SeedSettingVerticalPodAutoscaler) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSettings) DeepCopyInto(out *SeedSettings) { + *out = *in + if in.ExcessCapacityReservation != nil { + in, out := &in.ExcessCapacityReservation, &out.ExcessCapacityReservation + *out = new(SeedSettingExcessCapacityReservation) + (*in).DeepCopyInto(*out) + } + if in.Scheduling != nil { + in, out := &in.Scheduling, &out.Scheduling + *out = new(SeedSettingScheduling) + **out = **in + } + if in.LoadBalancerServices != nil { + in, out := &in.LoadBalancerServices, &out.LoadBalancerServices + *out = new(SeedSettingLoadBalancerServices) + (*in).DeepCopyInto(*out) + } + if in.VerticalPodAutoscaler != nil { + in, out := &in.VerticalPodAutoscaler, &out.VerticalPodAutoscaler + *out = new(SeedSettingVerticalPodAutoscaler) + **out = **in + } + if in.DependencyWatchdog != nil { + in, out := &in.DependencyWatchdog, &out.DependencyWatchdog + *out = new(SeedSettingDependencyWatchdog) + (*in).DeepCopyInto(*out) + } + if in.TopologyAwareRouting != nil { + in, out := &in.TopologyAwareRouting, &out.TopologyAwareRouting + *out = new(SeedSettingTopologyAwareRouting) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSettings. +func (in *SeedSettings) DeepCopy() *SeedSettings { + if in == nil { + return nil + } + out := new(SeedSettings) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedSpec) DeepCopyInto(out *SeedSpec) { + *out = *in + if in.Backup != nil { + in, out := &in.Backup, &out.Backup + *out = new(SeedBackup) + (*in).DeepCopyInto(*out) + } + in.DNS.DeepCopyInto(&out.DNS) + in.Networks.DeepCopyInto(&out.Networks) + in.Provider.DeepCopyInto(&out.Provider) + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]SeedTaint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Volume != nil { + in, out := &in.Volume, &out.Volume + *out = new(SeedVolume) + (*in).DeepCopyInto(*out) + } + if in.Settings != nil { + in, out := &in.Settings, &out.Settings + *out = new(SeedSettings) + (*in).DeepCopyInto(*out) + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(Ingress) + (*in).DeepCopyInto(*out) + } + if in.AccessRestrictions != nil { + in, out := &in.AccessRestrictions, &out.AccessRestrictions + *out = make([]AccessRestriction, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedSpec. +func (in *SeedSpec) DeepCopy() *SeedSpec { + if in == nil { + return nil + } + out := new(SeedSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedStatus) DeepCopyInto(out *SeedStatus) { + *out = *in + if in.Gardener != nil { + in, out := &in.Gardener, &out.Gardener + *out = new(Gardener) + **out = **in + } + if in.KubernetesVersion != nil { + in, out := &in.KubernetesVersion, &out.KubernetesVersion + *out = new(string) + **out = **in + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ClusterIdentity != nil { + in, out := &in.ClusterIdentity, &out.ClusterIdentity + *out = new(string) + **out = **in + } + if in.Capacity != nil { + in, out := &in.Capacity, &out.Capacity + *out = make(v1.ResourceList, len(*in)) + for key, val := range *in { + (*out)[key] = val.DeepCopy() + } + } + if in.Allocatable != nil { + in, out := &in.Allocatable, &out.Allocatable + *out = make(v1.ResourceList, len(*in)) + for key, val := range *in { + (*out)[key] = val.DeepCopy() + } + } + if in.ClientCertificateExpirationTimestamp != nil { + in, out := &in.ClientCertificateExpirationTimestamp, &out.ClientCertificateExpirationTimestamp + *out = (*in).DeepCopy() + } + if in.LastOperation != nil { + in, out := &in.LastOperation, &out.LastOperation + *out = new(LastOperation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedStatus. +func (in *SeedStatus) DeepCopy() *SeedStatus { + if in == nil { + return nil + } + out := new(SeedStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedTaint) DeepCopyInto(out *SeedTaint) { + *out = *in + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedTaint. +func (in *SeedTaint) DeepCopy() *SeedTaint { + if in == nil { + return nil + } + out := new(SeedTaint) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedTemplate) DeepCopyInto(out *SeedTemplate) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedTemplate. +func (in *SeedTemplate) DeepCopy() *SeedTemplate { + if in == nil { + return nil + } + out := new(SeedTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedVolume) DeepCopyInto(out *SeedVolume) { + *out = *in + if in.MinimumSize != nil { + in, out := &in.MinimumSize, &out.MinimumSize + x := (*in).DeepCopy() + *out = &x + } + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]SeedVolumeProvider, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedVolume. +func (in *SeedVolume) DeepCopy() *SeedVolume { + if in == nil { + return nil + } + out := new(SeedVolume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeedVolumeProvider) DeepCopyInto(out *SeedVolumeProvider) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeedVolumeProvider. +func (in *SeedVolumeProvider) DeepCopy() *SeedVolumeProvider { + if in == nil { + return nil + } + out := new(SeedVolumeProvider) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountConfig) DeepCopyInto(out *ServiceAccountConfig) { + *out = *in + if in.Issuer != nil { + in, out := &in.Issuer, &out.Issuer + *out = new(string) + **out = **in + } + if in.ExtendTokenExpiration != nil { + in, out := &in.ExtendTokenExpiration, &out.ExtendTokenExpiration + *out = new(bool) + **out = **in + } + if in.MaxTokenExpiration != nil { + in, out := &in.MaxTokenExpiration, &out.MaxTokenExpiration + *out = new(metav1.Duration) + **out = **in + } + if in.AcceptedIssuers != nil { + in, out := &in.AcceptedIssuers, &out.AcceptedIssuers + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountConfig. +func (in *ServiceAccountConfig) DeepCopy() *ServiceAccountConfig { + if in == nil { + return nil + } + out := new(ServiceAccountConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ServiceAccountKeyRotation) DeepCopyInto(out *ServiceAccountKeyRotation) { + *out = *in + if in.LastCompletionTime != nil { + in, out := &in.LastCompletionTime, &out.LastCompletionTime + *out = (*in).DeepCopy() + } + if in.LastInitiationTime != nil { + in, out := &in.LastInitiationTime, &out.LastInitiationTime + *out = (*in).DeepCopy() + } + if in.LastInitiationFinishedTime != nil { + in, out := &in.LastInitiationFinishedTime, &out.LastInitiationFinishedTime + *out = (*in).DeepCopy() + } + if in.LastCompletionTriggeredTime != nil { + in, out := &in.LastCompletionTriggeredTime, &out.LastCompletionTriggeredTime + *out = (*in).DeepCopy() + } + if in.PendingWorkersRollouts != nil { + in, out := &in.PendingWorkersRollouts, &out.PendingWorkersRollouts + *out = make([]PendingWorkersRollout, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ServiceAccountKeyRotation. +func (in *ServiceAccountKeyRotation) DeepCopy() *ServiceAccountKeyRotation { + if in == nil { + return nil + } + out := new(ServiceAccountKeyRotation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Shoot) DeepCopyInto(out *Shoot) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Shoot. +func (in *Shoot) DeepCopy() *Shoot { + if in == nil { + return nil + } + out := new(Shoot) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Shoot) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootAdvertisedAddress) DeepCopyInto(out *ShootAdvertisedAddress) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootAdvertisedAddress. +func (in *ShootAdvertisedAddress) DeepCopy() *ShootAdvertisedAddress { + if in == nil { + return nil + } + out := new(ShootAdvertisedAddress) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootCredentials) DeepCopyInto(out *ShootCredentials) { + *out = *in + if in.Rotation != nil { + in, out := &in.Rotation, &out.Rotation + *out = new(ShootCredentialsRotation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootCredentials. +func (in *ShootCredentials) DeepCopy() *ShootCredentials { + if in == nil { + return nil + } + out := new(ShootCredentials) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootCredentialsRotation) DeepCopyInto(out *ShootCredentialsRotation) { + *out = *in + if in.CertificateAuthorities != nil { + in, out := &in.CertificateAuthorities, &out.CertificateAuthorities + *out = new(CARotation) + (*in).DeepCopyInto(*out) + } + if in.Kubeconfig != nil { + in, out := &in.Kubeconfig, &out.Kubeconfig + *out = new(ShootKubeconfigRotation) + (*in).DeepCopyInto(*out) + } + if in.SSHKeypair != nil { + in, out := &in.SSHKeypair, &out.SSHKeypair + *out = new(ShootSSHKeypairRotation) + (*in).DeepCopyInto(*out) + } + if in.Observability != nil { + in, out := &in.Observability, &out.Observability + *out = new(ObservabilityRotation) + (*in).DeepCopyInto(*out) + } + if in.ServiceAccountKey != nil { + in, out := &in.ServiceAccountKey, &out.ServiceAccountKey + *out = new(ServiceAccountKeyRotation) + (*in).DeepCopyInto(*out) + } + if in.ETCDEncryptionKey != nil { + in, out := &in.ETCDEncryptionKey, &out.ETCDEncryptionKey + *out = new(ETCDEncryptionKeyRotation) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootCredentialsRotation. +func (in *ShootCredentialsRotation) DeepCopy() *ShootCredentialsRotation { + if in == nil { + return nil + } + out := new(ShootCredentialsRotation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootKubeconfigRotation) DeepCopyInto(out *ShootKubeconfigRotation) { + *out = *in + if in.LastInitiationTime != nil { + in, out := &in.LastInitiationTime, &out.LastInitiationTime + *out = (*in).DeepCopy() + } + if in.LastCompletionTime != nil { + in, out := &in.LastCompletionTime, &out.LastCompletionTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootKubeconfigRotation. +func (in *ShootKubeconfigRotation) DeepCopy() *ShootKubeconfigRotation { + if in == nil { + return nil + } + out := new(ShootKubeconfigRotation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootList) DeepCopyInto(out *ShootList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Shoot, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootList. +func (in *ShootList) DeepCopy() *ShootList { + if in == nil { + return nil + } + out := new(ShootList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ShootList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootMachineImage) DeepCopyInto(out *ShootMachineImage) { + *out = *in + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootMachineImage. +func (in *ShootMachineImage) DeepCopy() *ShootMachineImage { + if in == nil { + return nil + } + out := new(ShootMachineImage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootNetworks) DeepCopyInto(out *ShootNetworks) { + *out = *in + if in.Pods != nil { + in, out := &in.Pods, &out.Pods + *out = new(string) + **out = **in + } + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootNetworks. +func (in *ShootNetworks) DeepCopy() *ShootNetworks { + if in == nil { + return nil + } + out := new(ShootNetworks) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootSSHKeypairRotation) DeepCopyInto(out *ShootSSHKeypairRotation) { + *out = *in + if in.LastInitiationTime != nil { + in, out := &in.LastInitiationTime, &out.LastInitiationTime + *out = (*in).DeepCopy() + } + if in.LastCompletionTime != nil { + in, out := &in.LastCompletionTime, &out.LastCompletionTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootSSHKeypairRotation. +func (in *ShootSSHKeypairRotation) DeepCopy() *ShootSSHKeypairRotation { + if in == nil { + return nil + } + out := new(ShootSSHKeypairRotation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootSpec) DeepCopyInto(out *ShootSpec) { + *out = *in + if in.Addons != nil { + in, out := &in.Addons, &out.Addons + *out = new(Addons) + (*in).DeepCopyInto(*out) + } + if in.CloudProfileName != nil { + in, out := &in.CloudProfileName, &out.CloudProfileName + *out = new(string) + **out = **in + } + if in.DNS != nil { + in, out := &in.DNS, &out.DNS + *out = new(DNS) + (*in).DeepCopyInto(*out) + } + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]Extension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Hibernation != nil { + in, out := &in.Hibernation, &out.Hibernation + *out = new(Hibernation) + (*in).DeepCopyInto(*out) + } + in.Kubernetes.DeepCopyInto(&out.Kubernetes) + if in.Networking != nil { + in, out := &in.Networking, &out.Networking + *out = new(Networking) + (*in).DeepCopyInto(*out) + } + if in.Maintenance != nil { + in, out := &in.Maintenance, &out.Maintenance + *out = new(Maintenance) + (*in).DeepCopyInto(*out) + } + if in.Monitoring != nil { + in, out := &in.Monitoring, &out.Monitoring + *out = new(Monitoring) + (*in).DeepCopyInto(*out) + } + in.Provider.DeepCopyInto(&out.Provider) + if in.Purpose != nil { + in, out := &in.Purpose, &out.Purpose + *out = new(ShootPurpose) + **out = **in + } + if in.SecretBindingName != nil { + in, out := &in.SecretBindingName, &out.SecretBindingName + *out = new(string) + **out = **in + } + if in.SeedName != nil { + in, out := &in.SeedName, &out.SeedName + *out = new(string) + **out = **in + } + if in.SeedSelector != nil { + in, out := &in.SeedSelector, &out.SeedSelector + *out = new(SeedSelector) + (*in).DeepCopyInto(*out) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]NamedResourceReference, len(*in)) + copy(*out, *in) + } + if in.Tolerations != nil { + in, out := &in.Tolerations, &out.Tolerations + *out = make([]Toleration, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ExposureClassName != nil { + in, out := &in.ExposureClassName, &out.ExposureClassName + *out = new(string) + **out = **in + } + if in.SystemComponents != nil { + in, out := &in.SystemComponents, &out.SystemComponents + *out = new(SystemComponents) + (*in).DeepCopyInto(*out) + } + if in.ControlPlane != nil { + in, out := &in.ControlPlane, &out.ControlPlane + *out = new(ControlPlane) + (*in).DeepCopyInto(*out) + } + if in.SchedulerName != nil { + in, out := &in.SchedulerName, &out.SchedulerName + *out = new(string) + **out = **in + } + if in.CloudProfile != nil { + in, out := &in.CloudProfile, &out.CloudProfile + *out = new(CloudProfileReference) + **out = **in + } + if in.CredentialsBindingName != nil { + in, out := &in.CredentialsBindingName, &out.CredentialsBindingName + *out = new(string) + **out = **in + } + if in.AccessRestrictions != nil { + in, out := &in.AccessRestrictions, &out.AccessRestrictions + *out = make([]AccessRestrictionWithOptions, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootSpec. +func (in *ShootSpec) DeepCopy() *ShootSpec { + if in == nil { + return nil + } + out := new(ShootSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootState) DeepCopyInto(out *ShootState) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootState. +func (in *ShootState) DeepCopy() *ShootState { + if in == nil { + return nil + } + out := new(ShootState) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ShootState) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootStateList) DeepCopyInto(out *ShootStateList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ShootState, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootStateList. +func (in *ShootStateList) DeepCopy() *ShootStateList { + if in == nil { + return nil + } + out := new(ShootStateList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ShootStateList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootStateSpec) DeepCopyInto(out *ShootStateSpec) { + *out = *in + if in.Gardener != nil { + in, out := &in.Gardener, &out.Gardener + *out = make([]GardenerResourceData, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Extensions != nil { + in, out := &in.Extensions, &out.Extensions + *out = make([]ExtensionResourceState, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ResourceData, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootStateSpec. +func (in *ShootStateSpec) DeepCopy() *ShootStateSpec { + if in == nil { + return nil + } + out := new(ShootStateSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootStatus) DeepCopyInto(out *ShootStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Constraints != nil { + in, out := &in.Constraints, &out.Constraints + *out = make([]Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.Gardener = in.Gardener + if in.LastOperation != nil { + in, out := &in.LastOperation, &out.LastOperation + *out = new(LastOperation) + (*in).DeepCopyInto(*out) + } + if in.LastErrors != nil { + in, out := &in.LastErrors, &out.LastErrors + *out = make([]LastError, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.RetryCycleStartTime != nil { + in, out := &in.RetryCycleStartTime, &out.RetryCycleStartTime + *out = (*in).DeepCopy() + } + if in.SeedName != nil { + in, out := &in.SeedName, &out.SeedName + *out = new(string) + **out = **in + } + if in.ClusterIdentity != nil { + in, out := &in.ClusterIdentity, &out.ClusterIdentity + *out = new(string) + **out = **in + } + if in.AdvertisedAddresses != nil { + in, out := &in.AdvertisedAddresses, &out.AdvertisedAddresses + *out = make([]ShootAdvertisedAddress, len(*in)) + copy(*out, *in) + } + if in.MigrationStartTime != nil { + in, out := &in.MigrationStartTime, &out.MigrationStartTime + *out = (*in).DeepCopy() + } + if in.Credentials != nil { + in, out := &in.Credentials, &out.Credentials + *out = new(ShootCredentials) + (*in).DeepCopyInto(*out) + } + if in.LastHibernationTriggerTime != nil { + in, out := &in.LastHibernationTriggerTime, &out.LastHibernationTriggerTime + *out = (*in).DeepCopy() + } + if in.LastMaintenance != nil { + in, out := &in.LastMaintenance, &out.LastMaintenance + *out = new(LastMaintenance) + (*in).DeepCopyInto(*out) + } + if in.EncryptedResources != nil { + in, out := &in.EncryptedResources, &out.EncryptedResources + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Networking != nil { + in, out := &in.Networking, &out.Networking + *out = new(NetworkingStatus) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootStatus. +func (in *ShootStatus) DeepCopy() *ShootStatus { + if in == nil { + return nil + } + out := new(ShootStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ShootTemplate) DeepCopyInto(out *ShootTemplate) { + *out = *in + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ShootTemplate. +func (in *ShootTemplate) DeepCopy() *ShootTemplate { + if in == nil { + return nil + } + out := new(ShootTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StructuredAuthentication) DeepCopyInto(out *StructuredAuthentication) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StructuredAuthentication. +func (in *StructuredAuthentication) DeepCopy() *StructuredAuthentication { + if in == nil { + return nil + } + out := new(StructuredAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *StructuredAuthorization) DeepCopyInto(out *StructuredAuthorization) { + *out = *in + if in.Kubeconfigs != nil { + in, out := &in.Kubeconfigs, &out.Kubeconfigs + *out = make([]AuthorizerKubeconfigReference, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new StructuredAuthorization. +func (in *StructuredAuthorization) DeepCopy() *StructuredAuthorization { + if in == nil { + return nil + } + out := new(StructuredAuthorization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SystemComponents) DeepCopyInto(out *SystemComponents) { + *out = *in + if in.CoreDNS != nil { + in, out := &in.CoreDNS, &out.CoreDNS + *out = new(CoreDNS) + (*in).DeepCopyInto(*out) + } + if in.NodeLocalDNS != nil { + in, out := &in.NodeLocalDNS, &out.NodeLocalDNS + *out = new(NodeLocalDNS) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SystemComponents. +func (in *SystemComponents) DeepCopy() *SystemComponents { + if in == nil { + return nil + } + out := new(SystemComponents) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Toleration) DeepCopyInto(out *Toleration) { + *out = *in + if in.Value != nil { + in, out := &in.Value, &out.Value + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Toleration. +func (in *Toleration) DeepCopy() *Toleration { + if in == nil { + return nil + } + out := new(Toleration) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VerticalPodAutoscaler) DeepCopyInto(out *VerticalPodAutoscaler) { + *out = *in + if in.EvictAfterOOMThreshold != nil { + in, out := &in.EvictAfterOOMThreshold, &out.EvictAfterOOMThreshold + *out = new(metav1.Duration) + **out = **in + } + if in.EvictionRateBurst != nil { + in, out := &in.EvictionRateBurst, &out.EvictionRateBurst + *out = new(int32) + **out = **in + } + if in.EvictionRateLimit != nil { + in, out := &in.EvictionRateLimit, &out.EvictionRateLimit + *out = new(float64) + **out = **in + } + if in.EvictionTolerance != nil { + in, out := &in.EvictionTolerance, &out.EvictionTolerance + *out = new(float64) + **out = **in + } + if in.RecommendationMarginFraction != nil { + in, out := &in.RecommendationMarginFraction, &out.RecommendationMarginFraction + *out = new(float64) + **out = **in + } + if in.UpdaterInterval != nil { + in, out := &in.UpdaterInterval, &out.UpdaterInterval + *out = new(metav1.Duration) + **out = **in + } + if in.RecommenderInterval != nil { + in, out := &in.RecommenderInterval, &out.RecommenderInterval + *out = new(metav1.Duration) + **out = **in + } + if in.TargetCPUPercentile != nil { + in, out := &in.TargetCPUPercentile, &out.TargetCPUPercentile + *out = new(float64) + **out = **in + } + if in.RecommendationLowerBoundCPUPercentile != nil { + in, out := &in.RecommendationLowerBoundCPUPercentile, &out.RecommendationLowerBoundCPUPercentile + *out = new(float64) + **out = **in + } + if in.RecommendationUpperBoundCPUPercentile != nil { + in, out := &in.RecommendationUpperBoundCPUPercentile, &out.RecommendationUpperBoundCPUPercentile + *out = new(float64) + **out = **in + } + if in.TargetMemoryPercentile != nil { + in, out := &in.TargetMemoryPercentile, &out.TargetMemoryPercentile + *out = new(float64) + **out = **in + } + if in.RecommendationLowerBoundMemoryPercentile != nil { + in, out := &in.RecommendationLowerBoundMemoryPercentile, &out.RecommendationLowerBoundMemoryPercentile + *out = new(float64) + **out = **in + } + if in.RecommendationUpperBoundMemoryPercentile != nil { + in, out := &in.RecommendationUpperBoundMemoryPercentile, &out.RecommendationUpperBoundMemoryPercentile + *out = new(float64) + **out = **in + } + if in.CPUHistogramDecayHalfLife != nil { + in, out := &in.CPUHistogramDecayHalfLife, &out.CPUHistogramDecayHalfLife + *out = new(metav1.Duration) + **out = **in + } + if in.MemoryHistogramDecayHalfLife != nil { + in, out := &in.MemoryHistogramDecayHalfLife, &out.MemoryHistogramDecayHalfLife + *out = new(metav1.Duration) + **out = **in + } + if in.MemoryAggregationInterval != nil { + in, out := &in.MemoryAggregationInterval, &out.MemoryAggregationInterval + *out = new(metav1.Duration) + **out = **in + } + if in.MemoryAggregationIntervalCount != nil { + in, out := &in.MemoryAggregationIntervalCount, &out.MemoryAggregationIntervalCount + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VerticalPodAutoscaler. +func (in *VerticalPodAutoscaler) DeepCopy() *VerticalPodAutoscaler { + if in == nil { + return nil + } + out := new(VerticalPodAutoscaler) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Volume) DeepCopyInto(out *Volume) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + if in.Encrypted != nil { + in, out := &in.Encrypted, &out.Encrypted + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Volume. +func (in *Volume) DeepCopy() *Volume { + if in == nil { + return nil + } + out := new(Volume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VolumeType) DeepCopyInto(out *VolumeType) { + *out = *in + if in.Usable != nil { + in, out := &in.Usable, &out.Usable + *out = new(bool) + **out = **in + } + if in.MinSize != nil { + in, out := &in.MinSize, &out.MinSize + x := (*in).DeepCopy() + *out = &x + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VolumeType. +func (in *VolumeType) DeepCopy() *VolumeType { + if in == nil { + return nil + } + out := new(VolumeType) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WatchCacheSizes) DeepCopyInto(out *WatchCacheSizes) { + *out = *in + if in.Default != nil { + in, out := &in.Default, &out.Default + *out = new(int32) + **out = **in + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]ResourceWatchCacheSize, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WatchCacheSizes. +func (in *WatchCacheSizes) DeepCopy() *WatchCacheSizes { + if in == nil { + return nil + } + out := new(WatchCacheSizes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Worker) DeepCopyInto(out *Worker) { + *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.CABundle != nil { + in, out := &in.CABundle, &out.CABundle + *out = new(string) + **out = **in + } + if in.CRI != nil { + in, out := &in.CRI, &out.CRI + *out = new(CRI) + (*in).DeepCopyInto(*out) + } + if in.Kubernetes != nil { + in, out := &in.Kubernetes, &out.Kubernetes + *out = new(WorkerKubernetes) + (*in).DeepCopyInto(*out) + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + in.Machine.DeepCopyInto(&out.Machine) + if in.MaxSurge != nil { + in, out := &in.MaxSurge, &out.MaxSurge + *out = new(intstr.IntOrString) + **out = **in + } + if in.MaxUnavailable != nil { + in, out := &in.MaxUnavailable, &out.MaxUnavailable + *out = new(intstr.IntOrString) + **out = **in + } + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]v1.Taint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Volume != nil { + in, out := &in.Volume, &out.Volume + *out = new(Volume) + (*in).DeepCopyInto(*out) + } + if in.DataVolumes != nil { + in, out := &in.DataVolumes, &out.DataVolumes + *out = make([]DataVolume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.KubeletDataVolumeName != nil { + in, out := &in.KubeletDataVolumeName, &out.KubeletDataVolumeName + *out = new(string) + **out = **in + } + if in.Zones != nil { + in, out := &in.Zones, &out.Zones + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SystemComponents != nil { + in, out := &in.SystemComponents, &out.SystemComponents + *out = new(WorkerSystemComponents) + **out = **in + } + if in.MachineControllerManagerSettings != nil { + in, out := &in.MachineControllerManagerSettings, &out.MachineControllerManagerSettings + *out = new(MachineControllerManagerSettings) + (*in).DeepCopyInto(*out) + } + if in.Sysctls != nil { + in, out := &in.Sysctls, &out.Sysctls + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.ClusterAutoscaler != nil { + in, out := &in.ClusterAutoscaler, &out.ClusterAutoscaler + *out = new(ClusterAutoscalerOptions) + (*in).DeepCopyInto(*out) + } + if in.Priority != nil { + in, out := &in.Priority, &out.Priority + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Worker. +func (in *Worker) DeepCopy() *Worker { + if in == nil { + return nil + } + out := new(Worker) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerKubernetes) DeepCopyInto(out *WorkerKubernetes) { + *out = *in + if in.Kubelet != nil { + in, out := &in.Kubelet, &out.Kubelet + *out = new(KubeletConfig) + (*in).DeepCopyInto(*out) + } + if in.Version != nil { + in, out := &in.Version, &out.Version + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerKubernetes. +func (in *WorkerKubernetes) DeepCopy() *WorkerKubernetes { + if in == nil { + return nil + } + out := new(WorkerKubernetes) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerSystemComponents) DeepCopyInto(out *WorkerSystemComponents) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerSystemComponents. +func (in *WorkerSystemComponents) DeepCopy() *WorkerSystemComponents { + if in == nil { + return nil + } + out := new(WorkerSystemComponents) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkersSettings) DeepCopyInto(out *WorkersSettings) { + *out = *in + if in.SSHAccess != nil { + in, out := &in.SSHAccess, &out.SSHAccess + *out = new(SSHAccess) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkersSettings. +func (in *WorkersSettings) DeepCopy() *WorkersSettings { + if in == nil { + return nil + } + out := new(WorkersSettings) + in.DeepCopyInto(out) + return out +} diff --git a/api/external/gardener/pkg/apis/extensions/register.go b/api/external/gardener/pkg/apis/extensions/register.go new file mode 100644 index 0000000..6d0ee5c --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/register.go @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package extensions + +const ( + // GroupName is the name of the extensions API group. + GroupName = "extensions.gardener.cloud" +) diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/register.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/register.go new file mode 100644 index 0000000..b24f76f --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/register.go @@ -0,0 +1,66 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + + "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/extensions" +) + +// SchemeGroupVersion is group version used to register these objects +var SchemeGroupVersion = schema.GroupVersion{Group: extensions.GroupName, Version: "v1alpha1"} + +// Kind takes an unqualified kind and returns back a Group qualified GroupKind +func Kind(kind string) schema.GroupKind { + return SchemeGroupVersion.WithKind(kind).GroupKind() +} + +// Resource takes an unqualified resource and returns a Group qualified GroupResource +func Resource(resource string) schema.GroupResource { + return SchemeGroupVersion.WithResource(resource).GroupResource() +} + +var ( + // SchemeBuilder is a new Scheme Builder which registers our API. + SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes) + // AddToScheme is a reference to the Scheme Builder's AddToScheme function. + AddToScheme = SchemeBuilder.AddToScheme +) + +// Adds the list of known types to Scheme. +func addKnownTypes(scheme *runtime.Scheme) error { + scheme.AddKnownTypes(SchemeGroupVersion, + &BackupBucket{}, + &BackupBucketList{}, + &BackupEntry{}, + &BackupEntryList{}, + &Bastion{}, + &BastionList{}, + &Cluster{}, + &ClusterList{}, + &ContainerRuntime{}, + &ContainerRuntimeList{}, + &ControlPlane{}, + &ControlPlaneList{}, + &DNSRecord{}, + &DNSRecordList{}, + &Extension{}, + &ExtensionList{}, + &Infrastructure{}, + &InfrastructureList{}, + &Network{}, + &NetworkList{}, + &OperatingSystemConfig{}, + &OperatingSystemConfigList{}, + &Worker{}, + &WorkerList{}, + ) + metav1.AddToGroupVersion(scheme, SchemeGroupVersion) + + return nil +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types.go new file mode 100644 index 0000000..eefbfca --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + gardencorev1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" +) + +// Status is the status of an Object. +type Status interface { + // GetProviderStatus retrieves the provider status. + GetProviderStatus() *runtime.RawExtension + // GetConditions retrieves the Conditions of a status. + // Conditions may be nil. + GetConditions() []gardencorev1beta1.Condition + // SetConditions sets the Conditions of a status. + SetConditions([]gardencorev1beta1.Condition) + // GetLastOperation retrieves the LastOperation of a status. + // LastOperation may be nil. + GetLastOperation() *gardencorev1beta1.LastOperation + // SetLastOperation sets the LastOperation of a status. + SetLastOperation(*gardencorev1beta1.LastOperation) + // GetObservedGeneration retrieves the last generation observed by the extension controller. + GetObservedGeneration() int64 + // SetObservedGeneration sets the ObservedGeneration of a status. + SetObservedGeneration(int64) + // GetLastError retrieves the LastError of a status. + // LastError may be nil. + GetLastError() *gardencorev1beta1.LastError + // SetLastError sets the LastError of a status. + SetLastError(*gardencorev1beta1.LastError) + // GetState retrieves the State of the extension + GetState() *runtime.RawExtension + // SetState sets the State of the extension + SetState(state *runtime.RawExtension) + // GetResources retrieves the list of named resource references referred to in the State by their names. + GetResources() []gardencorev1beta1.NamedResourceReference + // SetResources sets a list of named resource references in the Status, that are referred by + // their names in the State. + SetResources(namedResourceReferences []gardencorev1beta1.NamedResourceReference) +} + +// Spec is the spec section of an Object. +type Spec interface { + // GetExtensionType retrieves the extension type. + GetExtensionType() string + // GetExtensionClass retrieves the extension class. + GetExtensionClass() *ExtensionClass + // GetExtensionPurpose retrieves the extension purpose. + GetExtensionPurpose() *string + // GetProviderConfig retrieves the provider config. + GetProviderConfig() *runtime.RawExtension +} + +// Object is an extension object resource. +type Object interface { + metav1.Object + runtime.Object + + // GetExtensionSpec retrieves the object's spec. + GetExtensionSpec() Spec + // GetExtensionStatus retrieves the object's status. + GetExtensionStatus() Status +} + +// ShootAlphaCSIMigrationKubernetesVersion is a constant for an annotation on the Shoot resource stating the Kubernetes +// version for which the CSI migration shall be enabled. +// Note that this annotation is alpha and can be removed anytime without further notice. Only use it if you know +// what you do. +const ShootAlphaCSIMigrationKubernetesVersion = "alpha.csimigration.shoot.extensions.gardener.cloud/kubernetes-version" + +// IPFamily is a type for specifying an IP protocol version to use in Gardener clusters. +type IPFamily string + +const ( + // IPFamilyIPv4 is the IPv4 IP family. + IPFamilyIPv4 IPFamily = "IPv4" + // IPFamilyIPv6 is the IPv6 IP family. + IPFamilyIPv6 IPFamily = "IPv6" +) diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_backupbucket.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_backupbucket.go new file mode 100644 index 0000000..3595fa7 --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_backupbucket.go @@ -0,0 +1,79 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ Object = (*BackupBucket)(nil) + +// BackupBucketResource is a constant for the name of the BackupBucket resource. +const BackupBucketResource = "BackupBucket" + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,path=backupbuckets,shortName=bb,singular=backupbucket +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The type of the cloud provider for this resource." +// +kubebuilder:printcolumn:name=Region,JSONPath=".spec.region",type=string,description="The region into which the backup bucket should be created." +// +kubebuilder:printcolumn:name=State,JSONPath=".status.lastOperation.state",type=string,description="status of the last operation, one of Aborted, Processing, Succeeded, Error, Failed" +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// BackupBucket is a specification for backup bucket. +type BackupBucket struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the BackupBucket. + // If the object's deletion timestamp is set, this field is immutable. + Spec BackupBucketSpec `json:"spec"` + // +optional + Status BackupBucketStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (i *BackupBucket) GetExtensionSpec() Spec { + return &i.Spec +} + +// GetExtensionStatus implements Object. +func (i *BackupBucket) GetExtensionStatus() Status { + return &i.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BackupBucketList is a list of BackupBucket resources. +type BackupBucketList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // Items is the list of BackupBucket. + Items []BackupBucket `json:"items"` +} + +// BackupBucketSpec is the spec for an BackupBucket resource. +type BackupBucketSpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + // Region is the region of this bucket. This field is immutable. + Region string `json:"region"` + // SecretRef is a reference to a secret that contains the credentials to access object store. + SecretRef corev1.SecretReference `json:"secretRef"` +} + +// BackupBucketStatus is the status for an BackupBucket resource. +type BackupBucketStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` + // GeneratedSecretRef is reference to the secret generated by backup bucket, which + // will have object store specific credentials. + // +optional + GeneratedSecretRef *corev1.SecretReference `json:"generatedSecretRef,omitempty"` +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_backupentry.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_backupentry.go new file mode 100644 index 0000000..bed09ff --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_backupentry.go @@ -0,0 +1,85 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ Object = (*BackupEntry)(nil) + +// BackupEntryResource is a constant for the name of the BackupEntry resource. +const BackupEntryResource = "BackupEntry" + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,path=backupentries,shortName=be,singular=backupentry +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The type of the cloud provider for this resource." +// +kubebuilder:printcolumn:name=Region,JSONPath=".spec.region",type=string,description="The region into which the backup entry should be created." +// +kubebuilder:printcolumn:name=Bucket,JSONPath=".spec.bucketName",type=string,description="The name of the bucket into which the backup entry should be created." +// +kubebuilder:printcolumn:name=State,JSONPath=".status.lastOperation.state",type=string,description="status of the last operation, one of Aborted, Processing, Succeeded, Error, Failed" +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// BackupEntry is a specification for backup Entry. +type BackupEntry struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the BackupEntry. + // If the object's deletion timestamp is set, this field is immutable. + Spec BackupEntrySpec `json:"spec"` + // +optional + Status BackupEntryStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (i *BackupEntry) GetExtensionSpec() Spec { + return &i.Spec +} + +// GetExtensionStatus implements Object. +func (i *BackupEntry) GetExtensionStatus() Status { + return &i.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BackupEntryList is a list of BackupEntry resources. +type BackupEntryList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // Items is the list of BackupEntry. + Items []BackupEntry `json:"items"` +} + +// BackupEntrySpec is the spec for an BackupEntry resource. +type BackupEntrySpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + // BackupBucketProviderStatus contains the provider status that has + // been generated by the controller responsible for the `BackupBucket` resource. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + BackupBucketProviderStatus *runtime.RawExtension `json:"backupBucketProviderStatus,omitempty"` + // Region is the region of this Entry. This field is immutable. + Region string `json:"region"` + // BucketName is the name of backup bucket for this Backup Entry. + BucketName string `json:"bucketName"` + // SecretRef is a reference to a secret that contains the credentials to access object store. + SecretRef corev1.SecretReference `json:"secretRef"` +} + +// BackupEntryStatus is the status for an BackupEntry resource. +type BackupEntryStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_bastion.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_bastion.go new file mode 100644 index 0000000..ed0c32d --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_bastion.go @@ -0,0 +1,86 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ Object = (*Bastion)(nil) + +// BastionResource is a constant for the name of the Bastion resource. +const BastionResource = "Bastion" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=bastions,singular=bastion +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=IP,JSONPath=".status.ingress.ip",type=string,description="The public IP address of the temporary bastion host" +// +kubebuilder:printcolumn:name=Hostname,JSONPath=".status.ingress.hostname",type=string,description="The public hostname of the temporary bastion host" +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="The bastion's age." + +// Bastion is a bastion or jump host that is dynamically created +// to provide SSH access to shoot nodes. +type Bastion struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Spec is the specification of this Bastion. + // If the object's deletion timestamp is set, this field is immutable. + Spec BastionSpec `json:"spec"` + // Status is the bastion's status. + // +optional + Status BastionStatus `json:"status,omitempty"` +} + +// GetExtensionSpec implements Object. +func (b *Bastion) GetExtensionSpec() Spec { + return &b.Spec +} + +// GetExtensionStatus implements Object. +func (b *Bastion) GetExtensionStatus() Status { + return &b.Status +} + +// BastionSpec contains the specification for an SSH bastion host. +type BastionSpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + // UserData is the base64-encoded user data for the bastion instance. This should + // contain code to provision the SSH key on the bastion instance. + // This field is immutable. + UserData []byte `json:"userData"` + // Ingress controls from where the created bastion host should be reachable. + Ingress []BastionIngressPolicy `json:"ingress"` +} + +// BastionIngressPolicy represents an ingress policy for SSH bastion hosts. +type BastionIngressPolicy struct { + // IPBlock defines an IP block that is allowed to access the bastion. + IPBlock networkingv1.IPBlock `json:"ipBlock"` +} + +// BastionStatus holds the most recently observed status of the Bastion. +type BastionStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` + // Ingress is the external IP and/or hostname of the bastion host. + // +optional + Ingress *corev1.LoadBalancerIngress `json:"ingress,omitempty"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// BastionList is a collection of Bastions. +type BastionList struct { + metav1.TypeMeta + // Standard list object metadata. + metav1.ListMeta + // Items is the list of Bastions. + Items []Bastion +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_cluster.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_cluster.go new file mode 100644 index 0000000..639a439 --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_cluster.go @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// ClusterResource is a constant for the name of the Cluster resource. +const ClusterResource = "Cluster" + +// +genclient +// +genclient:nonNamespaced +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Cluster,path=clusters,singular=cluster +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// Cluster is a specification for a Cluster resource. +type Cluster struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ClusterSpec `json:"spec"` +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ClusterList is a list of Cluster resources. +type ClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + // Items is the list of Cluster. + Items []Cluster `json:"items"` +} + +// ClusterSpec is the spec for a Cluster resource. +type ClusterSpec struct { + // CloudProfile is a raw extension field that contains the cloudprofile resource referenced + // by the shoot that has to be reconciled. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + CloudProfile runtime.RawExtension `json:"cloudProfile"` + // Seed is a raw extension field that contains the seed resource referenced by the shoot that + // has to be reconciled. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + Seed runtime.RawExtension `json:"seed"` + // Shoot is a raw extension field that contains the shoot resource that has to be reconciled. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + Shoot runtime.RawExtension `json:"shoot"` +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_containerruntime.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_containerruntime.go new file mode 100644 index 0000000..840c59b --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_containerruntime.go @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ Object = (*ContainerRuntime)(nil) + +const ( + // ContainerRuntimeResource is a constant for the name of the Container Runtime Extension resource. + ContainerRuntimeResource = "ContainerRuntime" + // CRINameWorkerLabel is the name of the label describing the CRI name used in this node. + CRINameWorkerLabel = "worker.gardener.cloud/cri-name" + // ContainerRuntimeNameWorkerLabel is a label describing a Container Runtime which should be supported on the node. + ContainerRuntimeNameWorkerLabel = "containerruntime.worker.gardener.cloud/%s" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=containerruntimes,shortName=cr,singular=containerruntime +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The type of the Container Runtime resource." +// +kubebuilder:printcolumn:name=Status,JSONPath=".status.lastOperation.state",type=string,description="status of the last operation, one of Aborted, Processing, Succeeded, Error, Failed" +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// ContainerRuntime is a specification for a container runtime resource. +type ContainerRuntime struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the ContainerRuntime. + // If the object's deletion timestamp is set, this field is immutable. + Spec ContainerRuntimeSpec `json:"spec"` + // +optional + Status ContainerRuntimeStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (i *ContainerRuntime) GetExtensionSpec() Spec { + return &i.Spec +} + +// GetExtensionStatus implements Object. +func (i *ContainerRuntime) GetExtensionStatus() Status { + return &i.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ContainerRuntimeList is a list of ContainerRuntime resources. +type ContainerRuntimeList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + Items []ContainerRuntime `json:"items"` +} + +// ContainerRuntimeSpec is the spec for a ContainerRuntime resource. +type ContainerRuntimeSpec struct { + // BinaryPath is the Worker's machine path where container runtime extensions should copy the binaries to. + BinaryPath string `json:"binaryPath"` + // WorkerPool identifies the worker pool of the Shoot. + // For each worker pool and type, Gardener deploys a ContainerRuntime CRD. + WorkerPool ContainerRuntimeWorkerPool `json:"workerPool"` + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` +} + +// ContainerRuntimeWorkerPool identifies a Shoot worker pool by its name and selector. +type ContainerRuntimeWorkerPool struct { + // Name specifies the name of the worker pool the container runtime should be available for. + // This field is immutable. + Name string `json:"name"` + // Selector is the label selector used by the extension to match the nodes belonging to the worker pool. + Selector metav1.LabelSelector `json:"selector"` +} + +// ContainerRuntimeStatus is the status for a ContainerRuntime resource. +type ContainerRuntimeStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_controlplane.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_controlplane.go new file mode 100644 index 0000000..b94be4c --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_controlplane.go @@ -0,0 +1,98 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ Object = (*ControlPlane)(nil) + +// ControlPlaneResource is a constant for the name of the ControlPlane resource. +const ControlPlaneResource = "ControlPlane" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=controlplanes,shortName=cp,singular=controlplane +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The control plane type." +// +kubebuilder:printcolumn:name=Purpose,JSONPath=".spec.purpose",type=string,description="Purpose of control plane resource." +// +kubebuilder:printcolumn:name=Status,JSONPath=".status.lastOperation.state",type=string,description="Status of control plane resource." +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// ControlPlane is a specification for a ControlPlane resource. +type ControlPlane struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the ControlPlane. + // If the object's deletion timestamp is set, this field is immutable. + Spec ControlPlaneSpec `json:"spec"` + // +optional + Status ControlPlaneStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (i *ControlPlane) GetExtensionSpec() Spec { + return &i.Spec +} + +// GetExtensionStatus implements Object. +func (i *ControlPlane) GetExtensionStatus() Status { + return &i.Status +} + +// GetExtensionPurpose implements Object. +func (i *ControlPlaneSpec) GetExtensionPurpose() *string { + return (*string)(i.Purpose) +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ControlPlaneList is a list of ControlPlane resources. +type ControlPlaneList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + // Items is the list of ControlPlanes. + Items []ControlPlane `json:"items"` +} + +// ControlPlaneSpec is the spec of a ControlPlane resource. +type ControlPlaneSpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + // Purpose contains the data if a cloud provider needs additional components in order to expose the control plane. + // This field is immutable. + // +optional + Purpose *Purpose `json:"purpose,omitempty"` + // InfrastructureProviderStatus contains the provider status that has + // been generated by the controller responsible for the `Infrastructure` resource. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + InfrastructureProviderStatus *runtime.RawExtension `json:"infrastructureProviderStatus,omitempty"` + // Region is the region of this control plane. This field is immutable. + Region string `json:"region"` + // SecretRef is a reference to a secret that contains the cloud provider specific credentials. + SecretRef corev1.SecretReference `json:"secretRef"` +} + +// ControlPlaneStatus is the status of a ControlPlane resource. +type ControlPlaneStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` +} + +// Purpose is a string alias. +type Purpose string + +const ( + // Normal triggers the ControlPlane controllers for the shoot provider. + Normal Purpose = "normal" + // Exposure triggers the ControlPlane controllers for the exposure settings. + Exposure Purpose = "exposure" +) diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_defaults.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_defaults.go new file mode 100644 index 0000000..23288a3 --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_defaults.go @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime" + + gardencorev1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" +) + +// DefaultSpec contains common status fields for every extension resource. +type DefaultSpec struct { + // Type contains the instance of the resource's kind. + Type string `json:"type"` + // Class holds the extension class used to control the responsibility for multiple provider extensions. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // +optional + Class *ExtensionClass `json:"class,omitempty"` + // ProviderConfig is the provider specific configuration. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty"` +} + +// ExtensionClass is a string alias for an extension class. +type ExtensionClass string + +const ( + // ExtensionClassShoot is the extension class responsible for shoot clusters. + ExtensionClassShoot ExtensionClass = "shoot" + // ExtensionClassGarden is the extension class responsible for the garden cluster. + ExtensionClassGarden ExtensionClass = "garden" +) + +// GetExtensionType implements Spec. +func (d *DefaultSpec) GetExtensionType() string { + return d.Type +} + +// GetExtensionClass implements Spec. +func (d *DefaultSpec) GetExtensionClass() *ExtensionClass { return d.Class } + +// GetProviderConfig implements Spec. +func (d *DefaultSpec) GetProviderConfig() *runtime.RawExtension { + return d.ProviderConfig +} + +// GetExtensionPurpose implements Spec. +func (d *DefaultSpec) GetExtensionPurpose() *string { + return nil +} + +// DefaultStatus contains common status fields for every extension resource. +type DefaultStatus struct { + // ProviderStatus contains provider-specific status. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + ProviderStatus *runtime.RawExtension `json:"providerStatus,omitempty"` + // Conditions represents the latest available observations of a Seed's current state. + // +optional + Conditions []gardencorev1beta1.Condition `json:"conditions,omitempty"` + // LastError holds information about the last occurred error during an operation. + // +optional + LastError *gardencorev1beta1.LastError `json:"lastError,omitempty"` + // LastOperation holds information about the last operation on the resource. + // +optional + LastOperation *gardencorev1beta1.LastOperation `json:"lastOperation,omitempty"` + // ObservedGeneration is the most recent generation observed for this resource. + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + // State can be filled by the operating controller with what ever data it needs. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + State *runtime.RawExtension `json:"state,omitempty"` + // Resources holds a list of named resource references that can be referred to in the state by their names. + // +optional + Resources []gardencorev1beta1.NamedResourceReference `json:"resources,omitempty"` +} + +// GetProviderStatus implements Status. +func (d *DefaultStatus) GetProviderStatus() *runtime.RawExtension { + return d.ProviderStatus +} + +// GetConditions implements Status. +func (d *DefaultStatus) GetConditions() []gardencorev1beta1.Condition { + return d.Conditions +} + +// SetConditions implements Status. +func (d *DefaultStatus) SetConditions(c []gardencorev1beta1.Condition) { + d.Conditions = c +} + +// GetLastOperation implements Status. +func (d *DefaultStatus) GetLastOperation() *gardencorev1beta1.LastOperation { + return d.LastOperation +} + +// SetLastOperation implements Status. +func (d *DefaultStatus) SetLastOperation(lastOp *gardencorev1beta1.LastOperation) { + d.LastOperation = lastOp +} + +// GetLastError implements Status. +func (d *DefaultStatus) GetLastError() *gardencorev1beta1.LastError { + return d.LastError +} + +// SetLastError implements Status. +func (d *DefaultStatus) SetLastError(lastErr *gardencorev1beta1.LastError) { + d.LastError = lastErr +} + +// GetObservedGeneration implements Status. +func (d *DefaultStatus) GetObservedGeneration() int64 { + return d.ObservedGeneration +} + +// SetObservedGeneration implements Status. +func (d *DefaultStatus) SetObservedGeneration(generation int64) { + d.ObservedGeneration = generation +} + +// GetState implements Status. +func (d *DefaultStatus) GetState() *runtime.RawExtension { + return d.State +} + +// SetState implements Status. +func (d *DefaultStatus) SetState(state *runtime.RawExtension) { + d.State = state +} + +// GetResources implements Status. +func (d *DefaultStatus) GetResources() []gardencorev1beta1.NamedResourceReference { + return d.Resources +} + +// SetResources implements Status. +func (d *DefaultStatus) SetResources(namedResourceReference []gardencorev1beta1.NamedResourceReference) { + d.Resources = namedResourceReference +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_dnsrecord.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_dnsrecord.go new file mode 100644 index 0000000..7266aee --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_dnsrecord.go @@ -0,0 +1,111 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ Object = (*DNSRecord)(nil) + +// DNSRecordResource is a constant for the name of the DNSRecord resource. +const DNSRecordResource = "DNSRecord" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=dnsrecords,shortName=dns,singular=dnsrecord +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The DNS record provider type." +// +kubebuilder:printcolumn:name="Domain Name",JSONPath=".spec.name",type=string,description="The DNS record domain name." +// +kubebuilder:printcolumn:name="Record Type",JSONPath=".spec.recordType",type=string,description="The DNS record type (A, CNAME, or TXT)." +// +kubebuilder:printcolumn:name=Status,JSONPath=".status.lastOperation.state",type=string,description="" +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="" + +// DNSRecord is a specification for a DNSRecord resource. +type DNSRecord struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the DNSRecord. + // If the object's deletion timestamp is set, this field is immutable. + Spec DNSRecordSpec `json:"spec"` + // +optional + Status DNSRecordStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (i *DNSRecord) GetExtensionSpec() Spec { + return &i.Spec +} + +// GetExtensionStatus implements Object. +func (i *DNSRecord) GetExtensionStatus() Status { + return &i.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// DNSRecordList is a list of DNSRecord resources. +type DNSRecordList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata"` + + // Items is the list of DNSRecords. + Items []DNSRecord `json:"items"` +} + +// DNSRecordSpec is the spec of a DNSRecord resource. +type DNSRecordSpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + // SecretRef is a reference to a secret that contains the cloud provider specific credentials. + SecretRef corev1.SecretReference `json:"secretRef"` + // Region is the region of this DNS record. If not specified, the region specified in SecretRef will be used. + // If that is also not specified, the extension controller will use its default region. + // +optional + Region *string `json:"region,omitempty"` + // Zone is the DNS hosted zone of this DNS record. If not specified, it will be determined automatically by + // getting all hosted zones of the account and searching for the longest zone name that is a suffix of Name. + // +optional + Zone *string `json:"zone,omitempty"` + // Name is the fully qualified domain name, e.g. "api.". This field is immutable. + Name string `json:"name"` + // RecordType is the DNS record type. Only A, CNAME, and TXT records are currently supported. This field is immutable. + RecordType DNSRecordType `json:"recordType"` + // Values is a list of IP addresses for A records, a single hostname for CNAME records, or a list of texts for TXT records. + Values []string `json:"values"` + // TTL is the time to live in seconds. Defaults to 120. + // +optional + TTL *int64 `json:"ttl,omitempty"` +} + +// DNSRecordStatus is the status of a DNSRecord resource. +type DNSRecordStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` + // Zone is the DNS hosted zone of this DNS record. + // +optional + Zone *string `json:"zone,omitempty"` +} + +// DNSRecordType is a string alias. +type DNSRecordType string + +const ( + // DNSRecordTypeA specifies that the DNSRecord is of type A. + DNSRecordTypeA DNSRecordType = "A" + // DNSRecordTypeAAAA specifies that the DNSRecord is of type AAAA. + DNSRecordTypeAAAA DNSRecordType = "AAAA" + // DNSRecordTypeCNAME specifies that the DNSRecord is of type CNAME. + DNSRecordTypeCNAME DNSRecordType = "CNAME" + // DNSRecordTypeTXT specifies that the DNSRecord is of type TXT. + DNSRecordTypeTXT DNSRecordType = "TXT" +) + +const ( + // ConditionTypeCreated specifies the condition type "Created" used as marker if record creation + // on infrastructure was performed successfully at least once. + ConditionTypeCreated = "Created" +) diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_extension.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_extension.go new file mode 100644 index 0000000..a8c5364 --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_extension.go @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ Object = (*Extension)(nil) + +// ExtensionResource is a constant for the name of the Extension resource. +const ExtensionResource = "Extension" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=extensions,shortName=ext,singular=extension +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The type of the Extension resource." +// +kubebuilder:printcolumn:name=Status,JSONPath=".status.lastOperation.state",type=string,description="Status of Extension resource." +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// Extension is a specification for a Extension resource. +type Extension struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the Extension. + // If the object's deletion timestamp is set, this field is immutable. + Spec ExtensionSpec `json:"spec"` + // +optional + Status ExtensionStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (i *Extension) GetExtensionSpec() Spec { + return &i.Spec +} + +// GetExtensionStatus implements Object. +func (i *Extension) GetExtensionStatus() Status { + return &i.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// ExtensionList is a list of Extension resources. +type ExtensionList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + Items []Extension `json:"items"` +} + +// ExtensionSpec is the spec for a Extension resource. +type ExtensionSpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` +} + +// ExtensionStatus is the status for a Extension resource. +type ExtensionStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_infrastructure.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_infrastructure.go new file mode 100644 index 0000000..c515d71 --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_infrastructure.go @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ Object = (*Infrastructure)(nil) + +// InfrastructureResource is a constant for the name of the Infrastructure resource. +const InfrastructureResource = "Infrastructure" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=infrastructures,shortName=infra,singular=infrastructure +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The type of the cloud provider for this resource." +// +kubebuilder:printcolumn:name=Region,JSONPath=".spec.region",type=string,description="The region into which the infrastructure should be deployed." +// +kubebuilder:printcolumn:name=Status,JSONPath=".status.lastOperation.state",type=string,description="Status of infrastructure resource." +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// Infrastructure is a specification for cloud provider infrastructure. +type Infrastructure struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the Infrastructure. + // If the object's deletion timestamp is set, this field is immutable. + Spec InfrastructureSpec `json:"spec"` + // +optional + Status InfrastructureStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (i *Infrastructure) GetExtensionSpec() Spec { + return &i.Spec +} + +// GetExtensionStatus implements Object. +func (i *Infrastructure) GetExtensionStatus() Status { + return &i.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// InfrastructureList is a list of Infrastructure resources. +type InfrastructureList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // Items is the list of Infrastructures. + Items []Infrastructure `json:"items"` +} + +// InfrastructureSpec is the spec for an Infrastructure resource. +type InfrastructureSpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + // Region is the region of this infrastructure. This field is immutable. + Region string `json:"region"` + // SecretRef is a reference to a secret that contains the cloud provider credentials. + SecretRef corev1.SecretReference `json:"secretRef"` + // SSHPublicKey is the public SSH key that should be used with this infrastructure. + // +optional + SSHPublicKey []byte `json:"sshPublicKey,omitempty"` +} + +// InfrastructureStatus is the status for an Infrastructure resource. +type InfrastructureStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` + // NodesCIDR is the CIDR of the node network that was optionally created by the acting extension controller. + // This might be needed in environments in which the CIDR for the network for the shoot worker node cannot + // be statically defined in the Shoot resource but must be computed dynamically. + // +optional + NodesCIDR *string `json:"nodesCIDR,omitempty"` + // EgressCIDRs is a list of CIDRs used by the shoot as the source IP for egress traffic. For certain environments the egress + // IPs may not be stable in which case the extension controller may opt to not populate this field. + // +optional + EgressCIDRs []string `json:"egressCIDRs,omitempty"` + // Networking contains information about cluster networking such as CIDRs. + // +optional + Networking *InfrastructureStatusNetworking `json:"networking,omitempty"` +} + +// InfrastructureStatusNetworking is a structure containing information about the node, service and pod network ranges. +type InfrastructureStatusNetworking struct { + // Pods are the CIDRs of the pod network. + // +optional + Pods []string `json:"pods,omitempty"` + // Nodes are the CIDRs of the node network. + // +optional + Nodes []string `json:"nodes,omitempty"` + // Services are the CIDRs of the service network. + // +optional + Services []string `json:"services,omitempty"` +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_network.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_network.go new file mode 100644 index 0000000..e162dcb --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_network.go @@ -0,0 +1,83 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ Object = (*Network)(nil) + +// NetworkResource is a constant for the name of the Network resource. +const NetworkResource = "Network" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=networks,singular=network +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The type of the network provider for this resource." +// +kubebuilder:printcolumn:name=Pod CIDR,JSONPath=".spec.podCIDR",type=string,description="The CIDR that will be used for pods." +// +kubebuilder:printcolumn:name=Service CIDR,JSONPath=".spec.serviceCIDR",type=string,description="The CIDR that will be used for services." +// +kubebuilder:printcolumn:name=Status,JSONPath=".status.lastOperation.state",type=string,description="Status of network resource." +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// Network is the specification for cluster networking. +type Network struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the Network. + // If the object's deletion timestamp is set, this field is immutable. + Spec NetworkSpec `json:"spec"` + // +optional + Status NetworkStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (n *Network) GetExtensionSpec() Spec { + return &n.Spec +} + +// GetExtensionStatus implements Object. +func (n *Network) GetExtensionStatus() Status { + return &n.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// NetworkList is a list of Network resources. +type NetworkList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // Items is the list of Networks. + Items []Network `json:"items"` +} + +// NetworkSpec is the spec for an Network resource. +type NetworkSpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + // PodCIDR defines the CIDR that will be used for pods. This field is immutable. + PodCIDR string `json:"podCIDR"` + // ServiceCIDR defines the CIDR that will be used for services. This field is immutable. + ServiceCIDR string `json:"serviceCIDR"` + // IPFamilies specifies the IP protocol versions to use for shoot networking. This field is immutable. + // See https://github.com/gardener/gardener/blob/master/docs/development/ipv6.md + // +optional + IPFamilies []IPFamily `json:"ipFamilies,omitempty"` +} + +// NetworkStatus is the status for an Network resource. +type NetworkStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` +} + +// GetExtensionType returns the type of this Network resource. +func (n *Network) GetExtensionType() string { + return n.Spec.Type +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_operatingsystemconfig.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_operatingsystemconfig.go new file mode 100644 index 0000000..fd0ceca --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_operatingsystemconfig.go @@ -0,0 +1,352 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ Object = (*OperatingSystemConfig)(nil) + +// OperatingSystemConfigResource is a constant for the name of the OperatingSystemConfig resource. +const OperatingSystemConfigResource = "OperatingSystemConfig" + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=operatingsystemconfigs,shortName=osc,singular=operatingsystemconfig +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The type of the operating system configuration." +// +kubebuilder:printcolumn:name=Purpose,JSONPath=".spec.purpose",type=string,description="The purpose of the operating system configuration." +// +kubebuilder:printcolumn:name=Status,JSONPath=".status.lastOperation.state",type=string,description="Status of operating system configuration." +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// OperatingSystemConfig is a specification for a OperatingSystemConfig resource +type OperatingSystemConfig struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the OperatingSystemConfig. + // If the object's deletion timestamp is set, this field is immutable. + Spec OperatingSystemConfigSpec `json:"spec"` + // +optional + Status OperatingSystemConfigStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (o *OperatingSystemConfig) GetExtensionSpec() Spec { + return &o.Spec +} + +// GetExtensionPurpose implements Object. +func (o *OperatingSystemConfigSpec) GetExtensionPurpose() *string { + return (*string)(&o.Purpose) +} + +// GetExtensionStatus implements Object. +func (o *OperatingSystemConfig) GetExtensionStatus() Status { + return &o.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// OperatingSystemConfigList is a list of OperatingSystemConfig resources. +type OperatingSystemConfigList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // Items is the list of OperatingSystemConfigs. + Items []OperatingSystemConfig `json:"items"` +} + +// OperatingSystemConfigSpec is the spec for a OperatingSystemConfig resource. +type OperatingSystemConfigSpec struct { + // CRI config is a structure contains configurations of the CRI library + // +optional + CRIConfig *CRIConfig `json:"criConfig,omitempty"` + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + // Purpose describes how the result of this OperatingSystemConfig is used by Gardener. Either it + // gets sent to the `Worker` extension controller to bootstrap a VM, or it is downloaded by the + // gardener-node-agent already running on a bootstrapped VM. + // This field is immutable. + Purpose OperatingSystemConfigPurpose `json:"purpose"` + // Units is a list of unit for the operating system configuration (usually, a systemd unit). + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + Units []Unit `json:"units,omitempty" patchStrategy:"merge" patchMergeKey:"name"` + // Files is a list of files that should get written to the host's file system. + // +patchMergeKey=path + // +patchStrategy=merge + // +optional + Files []File `json:"files,omitempty" patchStrategy:"merge" patchMergeKey:"path"` +} + +// Unit is a unit for the operating system configuration (usually, a systemd unit). +type Unit struct { + // Name is the name of a unit. + Name string `json:"name"` + // Command is the unit's command. + // +optional + Command *UnitCommand `json:"command,omitempty"` + // Enable describes whether the unit is enabled or not. + // +optional + Enable *bool `json:"enable,omitempty"` + // Content is the unit's content. + // +optional + Content *string `json:"content,omitempty"` + // DropIns is a list of drop-ins for this unit. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + DropIns []DropIn `json:"dropIns,omitempty" patchStrategy:"merge" patchMergeKey:"name"` + // FilePaths is a list of files the unit depends on. If any file changes a restart of the dependent unit will be + // triggered. For each FilePath there must exist a File with matching Path in OperatingSystemConfig.Spec.Files. + FilePaths []string `json:"filePaths,omitempty"` +} + +// UnitCommand is a string alias. +type UnitCommand string + +const ( + // CommandStart is the 'start' command for a unit. + CommandStart UnitCommand = "start" + // CommandRestart is the 'restart' command for a unit. + CommandRestart UnitCommand = "restart" + // CommandStop is the 'stop' command for a unit. + CommandStop UnitCommand = "stop" +) + +// DropIn is a drop-in configuration for a systemd unit. +type DropIn struct { + // Name is the name of the drop-in. + Name string `json:"name"` + // Content is the content of the drop-in. + Content string `json:"content"` +} + +// File is a file that should get written to the host's file system. The content can either be inlined or +// referenced from a secret in the same namespace. +type File struct { + // Path is the path of the file system where the file should get written to. + Path string `json:"path"` + // Permissions describes with which permissions the file should get written to the file system. + // If no permissions are set, the operating system's defaults are used. + // +optional + Permissions *uint32 `json:"permissions,omitempty"` + // Content describe the file's content. + Content FileContent `json:"content"` +} + +// FileContent can either reference a secret or contain inline configuration. +type FileContent struct { + // SecretRef is a struct that contains information about the referenced secret. + // +optional + SecretRef *FileContentSecretRef `json:"secretRef,omitempty"` + // Inline is a struct that contains information about the inlined data. + // +optional + Inline *FileContentInline `json:"inline,omitempty"` + // TransmitUnencoded set to true will ensure that the os-extension does not encode the file content when sent to the node. + // This for example can be used to manipulate the clear-text content before it reaches the node. + // +optional + TransmitUnencoded *bool `json:"transmitUnencoded,omitempty"` + // ImageRef describes a container image which contains a file. + // +optional + ImageRef *FileContentImageRef `json:"imageRef,omitempty"` +} + +// FileContentSecretRef contains keys for referencing a file content's data from a secret in the same namespace. +type FileContentSecretRef struct { + // Name is the name of the secret. + Name string `json:"name"` + // DataKey is the key in the secret's `.data` field that should be read. + DataKey string `json:"dataKey"` +} + +// FileContentInline contains keys for inlining a file content's data and encoding. +type FileContentInline struct { + // Encoding is the file's encoding (e.g. base64). + Encoding string `json:"encoding"` + // Data is the file's data. + Data string `json:"data"` +} + +// FileContentImageRef describes a container image which contains a file +type FileContentImageRef struct { + // Image contains the container image repository with tag. + Image string `json:"image"` + // FilePathInImage contains the path in the image to the file that should be extracted. + FilePathInImage string `json:"filePathInImage"` +} + +// OperatingSystemConfigStatus is the status for a OperatingSystemConfig resource. +type OperatingSystemConfigStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` + // ExtensionUnits is a list of additional systemd units provided by the extension. + // +patchMergeKey=name + // +patchStrategy=merge + // +optional + ExtensionUnits []Unit `json:"extensionUnits,omitempty" patchStrategy:"merge" patchMergeKey:"name"` + // ExtensionFiles is a list of additional files provided by the extension. + // +patchMergeKey=path + // +patchStrategy=merge + // +optional + ExtensionFiles []File `json:"extensionFiles,omitempty" patchStrategy:"merge" patchMergeKey:"path"` + // CloudConfig is a structure for containing the generated output for the given operating system + // config spec. It contains a reference to a secret as the result may contain confidential data. + // After Gardener v1.112, this will be only set for OperatingSystemConfigs with purpose 'provision'. + // +optional + CloudConfig *CloudConfig `json:"cloudConfig,omitempty"` +} + +// CloudConfig contains the generated output for the given operating system +// config spec. It contains a reference to a secret as the result may contain confidential data. +type CloudConfig struct { + // SecretRef is a reference to a secret that contains the actual result of the generated cloud config. + SecretRef corev1.SecretReference `json:"secretRef"` +} + +// OperatingSystemConfigPurpose is a string alias. +type OperatingSystemConfigPurpose string + +const ( + // OperatingSystemConfigPurposeProvision describes that the operating system configuration is used to bootstrap a + // new VM. + OperatingSystemConfigPurposeProvision OperatingSystemConfigPurpose = "provision" + // OperatingSystemConfigPurposeReconcile describes that the operating system configuration is executed on an already + // provisioned VM by the gardener-node-agent. + OperatingSystemConfigPurposeReconcile OperatingSystemConfigPurpose = "reconcile" + + // OperatingSystemConfigSecretDataKey is a constant for the key in a secret's `.data` field containing the + // results of a computed cloud config. + OperatingSystemConfigSecretDataKey = "cloud_config" // #nosec G101 -- No credential. +) + +// CgroupDriverName is a string denoting the CRI cgroup driver. +type CgroupDriverName string + +const ( + // CgroupDriverCgroupfs is the name of the 'cgroupfs' cgroup driver. + CgroupDriverCgroupfs CgroupDriverName = "cgroupfs" + // CgroupDriverSystemd is the name of the 'systemd' cgroup driver. + CgroupDriverSystemd CgroupDriverName = "systemd" +) + +// CRIConfig contains configurations of the CRI library. +type CRIConfig struct { + // Name is a mandatory string containing the name of the CRI library. Supported values are `containerd`. + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="Value is immutable" + // +kubebuilder:validation:Enum="containerd" + Name CRIName `json:"name"` + // CgroupDriver configures the CRI's cgroup driver. Supported values are `cgroupfs` or `systemd`. + // +optional + CgroupDriver *CgroupDriverName `json:"cgroupDriver,omitempty"` + // ContainerdConfig is the containerd configuration. + // Only to be set for OperatingSystemConfigs with purpose 'reconcile'. + // +optional + Containerd *ContainerdConfig `json:"containerd,omitempty"` +} + +// ContainerdConfig contains configuration options for containerd. +type ContainerdConfig struct { + // Registries configures the registry hosts for containerd. + // +optional + Registries []RegistryConfig `json:"registries,omitempty"` + // SandboxImage configures the sandbox image for containerd. + SandboxImage string `json:"sandboxImage"` + // Plugins configures the plugins section in containerd's config.toml. + // +optional + Plugins []PluginConfig `json:"plugins,omitempty"` +} + +// PluginPathOperation is a type alias for operations at containerd's plugin configuration. +type PluginPathOperation string + +const ( + // AddPluginPathOperation is the name of the 'add' operation. + AddPluginPathOperation PluginPathOperation = "add" + // RemovePluginPathOperation is the name of the 'remove' operation. + RemovePluginPathOperation PluginPathOperation = "remove" +) + +// PluginConfig contains configuration values for the containerd plugins section. +type PluginConfig struct { + // Op is the operation for the given path. Possible values are 'add' and 'remove', defaults to 'add'. + // +optional + Op *PluginPathOperation `json:"op,omitempty"` + // Path is a list of elements that construct the path in the plugins section. + Path []string `json:"path"` + // Values are the values configured at the given path. If defined, it is expected as json format: + // - A given json object will be put to the given path. + // - If not configured, only the table entry to be created. + // +optional + Values *apiextensionsv1.JSON `json:"values,omitempty"` +} + +// RegistryConfig contains registry configuration options. +type RegistryConfig struct { + // Upstream is the upstream name of the registry. + Upstream string `json:"upstream"` + // Server is the URL to registry server of this upstream. + // It corresponds to the server field in the `hosts.toml` file, see https://github.com/containerd/containerd/blob/c51463010e0682f76dfdc10edc095e6596e2764b/docs/hosts.md#server-field for more information. + // +optional + Server *string `json:"server,omitempty"` + // Hosts are the registry hosts. + // It corresponds to the host fields in the `hosts.toml` file, see https://github.com/containerd/containerd/blob/c51463010e0682f76dfdc10edc095e6596e2764b/docs/hosts.md#host-fields-in-the-toml-table-format for more information. + Hosts []RegistryHost `json:"hosts,omitempty"` + // ReadinessProbe determines if host registry endpoints should be probed before they are added to the containerd config. + // +optional + ReadinessProbe *bool `json:"readinessProbe,omitempty"` +} + +// RegistryCapability specifies an action a client can perform against a registry. +type RegistryCapability string + +const ( + // PullCapability defines the 'pull' capability. + PullCapability RegistryCapability = "pull" + // ResolveCapability defines the 'resolve' capability. + ResolveCapability RegistryCapability = "resolve" + // PushCapability defines the 'push' capability. + PushCapability RegistryCapability = "push" +) + +// RegistryHost contains configuration values for a registry host. +type RegistryHost struct { + // URL is the endpoint address of the registry mirror. + URL string `json:"url"` + // Capabilities determine what operations a host is + // capable of performing. Defaults to + // - pull + // - resolve + Capabilities []RegistryCapability `json:"capabilities,omitempty"` + // CACerts are paths to public key certificates used for TLS. + CACerts []string `json:"caCerts,omitempty"` +} + +// CRIName is a type alias for the CRI name string. +type CRIName string + +const ( + // CRINameContainerD is a constant for ContainerD CRI name + CRINameContainerD CRIName = "containerd" +) + +// ContainerDRuntimeContainersBinFolder is the folder where Container Runtime binaries should be saved for ContainerD usage +const ContainerDRuntimeContainersBinFolder = "/var/bin/containerruntimes" + +// FileCodecID is the id of a FileCodec for cloud-init scripts. +type FileCodecID string + +const ( + // PlainFileCodecID is the plain file codec id. + PlainFileCodecID FileCodecID = "" + // B64FileCodecID is the base64 file codec id. + B64FileCodecID FileCodecID = "b64" +) diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/types_worker.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_worker.go new file mode 100644 index 0000000..891bd32 --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/types_worker.go @@ -0,0 +1,261 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package v1alpha1 + +import ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" + + gardencorev1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" +) + +var _ Object = (*Worker)(nil) + +// WorkerResource is a constant for the name of the Worker resource. +const WorkerResource = "Worker" + +const ( + // ScaleDownUtilizationThresholdAnnotation is the annotation key for the value of NodeGroupAutoscalingOptions.ScaleDownUtilizationThreshold of cluster-autoscaler + ScaleDownUtilizationThresholdAnnotation = "autoscaler.gardener.cloud/scale-down-utilization-threshold" + // ScaleDownGpuUtilizationThresholdAnnotation is the annotation key for the value of NodeGroupAutoscalingOptions.ScaleDownGpuUtilizationThreshold of cluster-autoscaler + ScaleDownGpuUtilizationThresholdAnnotation = "autoscaler.gardener.cloud/scale-down-gpu-utilization-threshold" + // ScaleDownUnneededTimeAnnotation is the annotation key for the value of NodeGroupAutoscalingOptions.ScaleDownUnneededTime of cluster-autoscaler + ScaleDownUnneededTimeAnnotation = "autoscaler.gardener.cloud/scale-down-unneeded-time" + // ScaleDownUnreadyTimeAnnotation is the annotation key for the value of NodeGroupAutoscalingOptions.ScaleDownUnreadyTime of cluster-autoscaler + ScaleDownUnreadyTimeAnnotation = "autoscaler.gardener.cloud/scale-down-unready-time" + // MaxNodeProvisionTimeAnnotation is the annotation key for the value of NodeGroupAutoscalingOptions.MaxNodeProvisionTime of cluster-autoscaler + MaxNodeProvisionTimeAnnotation = "autoscaler.gardener.cloud/max-node-provision-time" +) + +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,path=workers,singular=worker +// +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name=Type,JSONPath=".spec.type",type=string,description="The type of the cloud provider for this resource." +// +kubebuilder:printcolumn:name=Region,JSONPath=".spec.region",type=string,description="The region into which the worker should be deployed." +// +kubebuilder:printcolumn:name=Status,JSONPath=".status.lastOperation.state",type=string,description="Status of the worker." +// +kubebuilder:printcolumn:name=Age,JSONPath=".metadata.creationTimestamp",type=date,description="creation timestamp" + +// Worker is a specification for a Worker resource. +type Worker struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ObjectMeta `json:"metadata,omitempty"` + // Specification of the Worker. + // If the object's deletion timestamp is set, this field is immutable. + Spec WorkerSpec `json:"spec"` + // +optional + Status WorkerStatus `json:"status"` +} + +// GetExtensionSpec implements Object. +func (i *Worker) GetExtensionSpec() Spec { + return &i.Spec +} + +// GetExtensionStatus implements Object. +func (i *Worker) GetExtensionStatus() Status { + return &i.Status +} + +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object + +// WorkerList is a list of Worker resources. +type WorkerList struct { + metav1.TypeMeta `json:",inline"` + // +optional + metav1.ListMeta `json:"metadata,omitempty"` + + // Items is the list of Worker. + Items []Worker `json:"items"` +} + +// WorkerSpec is the spec for a Worker resource. +type WorkerSpec struct { + // DefaultSpec is a structure containing common fields used by all extension resources. + DefaultSpec `json:",inline"` + + // InfrastructureProviderStatus is a raw extension field that contains the provider status that has + // been generated by the controller responsible for the `Infrastructure` resource. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + InfrastructureProviderStatus *runtime.RawExtension `json:"infrastructureProviderStatus,omitempty"` + // Region is the name of the region where the worker pool should be deployed to. This field is immutable. + Region string `json:"region"` + // SecretRef is a reference to a secret that contains the cloud provider specific credentials. + SecretRef corev1.SecretReference `json:"secretRef"` + // SSHPublicKey is the public SSH key that should be used with these workers. + // +optional + SSHPublicKey []byte `json:"sshPublicKey,omitempty"` + // Pools is a list of worker pools. + // +patchMergeKey=name + // +patchStrategy=merge + Pools []WorkerPool `json:"pools" patchStrategy:"merge" patchMergeKey:"name"` +} + +// WorkerPool is the definition of a specific worker pool. +type WorkerPool struct { + // MachineType contains information about the machine type that should be used for this worker pool. + MachineType string `json:"machineType"` + // Maximum is the maximum size of the worker pool. + Maximum int32 `json:"maximum"` + // MaxSurge is maximum number of VMs that are created during an update. + MaxSurge intstr.IntOrString `json:"maxSurge"` + // MaxUnavailable is the maximum number of VMs that can be unavailable during an update. + MaxUnavailable intstr.IntOrString `json:"maxUnavailable"` + // Annotations is a map of key/value pairs for annotations for all the `Node` objects in this worker pool. + // +optional + Annotations map[string]string `json:"annotations,omitempty"` + // Labels is a map of key/value pairs for labels for all the `Node` objects in this worker pool. + // +optional + Labels map[string]string `json:"labels,omitempty"` + // Taints is a list of taints for all the `Node` objects in this worker pool. + // +optional + Taints []corev1.Taint `json:"taints,omitempty"` + // MachineImage contains logical information about the name and the version of the machie image that + // should be used. The logical information must be mapped to the provider-specific information (e.g., + // AMIs, ...) by the provider itself. + MachineImage MachineImage `json:"machineImage,omitempty"` + // Minimum is the minimum size of the worker pool. + Minimum int32 `json:"minimum"` + // Name is the name of this worker pool. + Name string `json:"name"` + // NodeAgentSecretName is uniquely identifying selected aspects of the OperatingSystemConfig. If it changes, then the + // worker pool must be rolled. + // +optional + NodeAgentSecretName *string `json:"nodeAgentSecretName,omitempty"` + // ProviderConfig is a provider specific configuration for the worker pool. + // +kubebuilder:validation:XPreserveUnknownFields + // +kubebuilder:pruning:PreserveUnknownFields + // +optional + ProviderConfig *runtime.RawExtension `json:"providerConfig,omitempty"` + // UserDataSecretRef references a Secret and a data key containing the data that is sent to the provider's APIs when + // a new machine/VM that is part of this worker pool shall be spawned. + UserDataSecretRef corev1.SecretKeySelector `json:"userDataSecretRef"` + // Volume contains information about the root disks that should be used for this worker pool. + // +optional + Volume *Volume `json:"volume,omitempty"` + // DataVolumes contains a list of additional worker volumes. + // +optional + DataVolumes []DataVolume `json:"dataVolumes,omitempty"` + // KubeletDataVolumeName contains the name of a dataVolume that should be used for storing kubelet state. + // +optional + KubeletDataVolumeName *string `json:"kubeletDataVolumeName,omitempty"` + // Zones contains information about availability zones for this worker pool. + // +optional + Zones []string `json:"zones,omitempty"` + // MachineControllerManagerSettings contains configurations for different worker-pools. Eg. MachineDrainTimeout, MachineHealthTimeout. + // +optional + MachineControllerManagerSettings *gardencorev1beta1.MachineControllerManagerSettings `json:"machineControllerManager,omitempty"` + // KubernetesVersion is the kubernetes version in this worker pool + // +optional + KubernetesVersion *string `json:"kubernetesVersion,omitempty"` + // NodeTemplate contains resource information of the machine which is used by Cluster Autoscaler to generate nodeTemplate during scaling a nodeGroup from zero + // +optional + NodeTemplate *NodeTemplate `json:"nodeTemplate,omitempty"` + // Architecture is the CPU architecture of the worker pool machines and machine image. + // +optional + Architecture *string `json:"architecture,omitempty"` + // ClusterAutoscaler contains the cluster autoscaler configurations for the worker pool. + // +optional + ClusterAutoscaler *ClusterAutoscalerOptions `json:"clusterAutoscaler,omitempty"` + // Priority (or weight) is the importance by which this worker pool will be scaled by cluster autoscaling. + // +optional + Priority *int32 `json:"priority,omitempty"` +} + +// ClusterAutoscalerOptions contains the cluster autoscaler configurations for a worker pool. +type ClusterAutoscalerOptions struct { + // ScaleDownUtilizationThreshold defines the threshold in fraction (0.0 - 1.0) under which a node is being removed. + // +optional + ScaleDownUtilizationThreshold *string `json:"scaleDownUtilizationThreshold,omitempty"` + // ScaleDownGpuUtilizationThreshold defines the threshold in fraction (0.0 - 1.0) of gpu resources under which a node is being removed. + // +optional + ScaleDownGpuUtilizationThreshold *string `json:"scaleDownGpuUtilizationThreshold,omitempty"` + // ScaleDownUnneededTime defines how long a node should be unneeded before it is eligible for scale down. + // +optional + ScaleDownUnneededTime *metav1.Duration `json:"scaleDownUnneededTime,omitempty"` + // ScaleDownUnreadyTime defines how long an unready node should be unneeded before it is eligible for scale down. + // +optional + ScaleDownUnreadyTime *metav1.Duration `json:"scaleDownUnreadyTime,omitempty"` + // MaxNodeProvisionTime defines how long cluster autoscaler should wait for a node to be provisioned. + // +optional + MaxNodeProvisionTime *metav1.Duration `json:"maxNodeProvisionTime,omitempty"` +} + +// NodeTemplate contains information about the expected node properties. +type NodeTemplate struct { + // Capacity represents the expected Node capacity. + Capacity corev1.ResourceList `json:"capacity"` +} + +// MachineImage contains logical information about the name and the version of the machie image that +// should be used. The logical information must be mapped to the provider-specific information (e.g., +// AMIs, ...) by the provider itself. +type MachineImage struct { + // Name is the logical name of the machine image. + Name string `json:"name"` + // Version is the version of the machine image. + Version string `json:"version"` +} + +// Volume contains information about the root disks that should be used for worker pools. +type Volume struct { + // Name of the volume to make it referenceable. + // +optional + Name *string `json:"name,omitempty"` + // Type is the type of the volume. + // +optional + Type *string `json:"type,omitempty"` + // Size is the of the root volume. + Size string `json:"size"` + // Encrypted determines if the volume should be encrypted. + // +optional + Encrypted *bool `json:"encrypted,omitempty"` +} + +// DataVolume contains information about a data volume. +type DataVolume struct { + // Name of the volume to make it referenceable. + Name string `json:"name"` + // Type is the type of the volume. + // +optional + Type *string `json:"type,omitempty"` + // Size is the of the root volume. + Size string `json:"size"` + // Encrypted determines if the volume should be encrypted. + // +optional + Encrypted *bool `json:"encrypted,omitempty"` +} + +// WorkerStatus is the status for a Worker resource. +type WorkerStatus struct { + // DefaultStatus is a structure containing common fields used by all extension resources. + DefaultStatus `json:",inline"` + // MachineDeployments is a list of created machine deployments. It will be used to e.g. configure + // the cluster-autoscaler properly. + // +patchMergeKey=name + // +patchStrategy=merge + MachineDeployments []MachineDeployment `json:"machineDeployments,omitempty" patchStrategy:"merge" patchMergeKey:"name"` + // MachineDeploymentsLastUpdateTime is the timestamp when the status.MachineDeployments slice was last updated. + // +optional + MachineDeploymentsLastUpdateTime *metav1.Time `json:"machineDeploymentsLastUpdateTime,omitempty"` +} + +// MachineDeployment is a created machine deployment. +type MachineDeployment struct { + // Name is the name of the `MachineDeployment` resource. + Name string `json:"name"` + // Minimum is the minimum number for this machine deployment. + Minimum int32 `json:"minimum"` + // Maximum is the maximum number for this machine deployment. + Maximum int32 `json:"maximum"` + // Priority (or weight) is the importance by which this machine deployment will be scaled by cluster autoscaling. + // +optional + Priority *int32 `json:"priority,omitempty"` +} diff --git a/api/external/gardener/pkg/apis/extensions/v1alpha1/zz_generated.deepcopy.go b/api/external/gardener/pkg/apis/extensions/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..b8f0e35 --- /dev/null +++ b/api/external/gardener/pkg/apis/extensions/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,2028 @@ +//go:build !ignore_autogenerated +// +build !ignore_autogenerated + +// SPDX-FileCopyrightText: SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +// Code generated by deepcopy-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + v1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" + v1 "k8s.io/api/core/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucket) DeepCopyInto(out *BackupBucket) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucket. +func (in *BackupBucket) DeepCopy() *BackupBucket { + if in == nil { + return nil + } + out := new(BackupBucket) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupBucket) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucketList) DeepCopyInto(out *BackupBucketList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BackupBucket, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucketList. +func (in *BackupBucketList) DeepCopy() *BackupBucketList { + if in == nil { + return nil + } + out := new(BackupBucketList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupBucketList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucketSpec) DeepCopyInto(out *BackupBucketSpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + out.SecretRef = in.SecretRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucketSpec. +func (in *BackupBucketSpec) DeepCopy() *BackupBucketSpec { + if in == nil { + return nil + } + out := new(BackupBucketSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupBucketStatus) DeepCopyInto(out *BackupBucketStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + if in.GeneratedSecretRef != nil { + in, out := &in.GeneratedSecretRef, &out.GeneratedSecretRef + *out = new(v1.SecretReference) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupBucketStatus. +func (in *BackupBucketStatus) DeepCopy() *BackupBucketStatus { + if in == nil { + return nil + } + out := new(BackupBucketStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupEntry) DeepCopyInto(out *BackupEntry) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupEntry. +func (in *BackupEntry) DeepCopy() *BackupEntry { + if in == nil { + return nil + } + out := new(BackupEntry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupEntry) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupEntryList) DeepCopyInto(out *BackupEntryList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]BackupEntry, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupEntryList. +func (in *BackupEntryList) DeepCopy() *BackupEntryList { + if in == nil { + return nil + } + out := new(BackupEntryList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BackupEntryList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupEntrySpec) DeepCopyInto(out *BackupEntrySpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + if in.BackupBucketProviderStatus != nil { + in, out := &in.BackupBucketProviderStatus, &out.BackupBucketProviderStatus + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + out.SecretRef = in.SecretRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupEntrySpec. +func (in *BackupEntrySpec) DeepCopy() *BackupEntrySpec { + if in == nil { + return nil + } + out := new(BackupEntrySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BackupEntryStatus) DeepCopyInto(out *BackupEntryStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BackupEntryStatus. +func (in *BackupEntryStatus) DeepCopy() *BackupEntryStatus { + if in == nil { + return nil + } + out := new(BackupEntryStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Bastion) DeepCopyInto(out *Bastion) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Bastion. +func (in *Bastion) DeepCopy() *Bastion { + if in == nil { + return nil + } + out := new(Bastion) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Bastion) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BastionIngressPolicy) DeepCopyInto(out *BastionIngressPolicy) { + *out = *in + in.IPBlock.DeepCopyInto(&out.IPBlock) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BastionIngressPolicy. +func (in *BastionIngressPolicy) DeepCopy() *BastionIngressPolicy { + if in == nil { + return nil + } + out := new(BastionIngressPolicy) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BastionList) DeepCopyInto(out *BastionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Bastion, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BastionList. +func (in *BastionList) DeepCopy() *BastionList { + if in == nil { + return nil + } + out := new(BastionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *BastionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BastionSpec) DeepCopyInto(out *BastionSpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + if in.UserData != nil { + in, out := &in.UserData, &out.UserData + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = make([]BastionIngressPolicy, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BastionSpec. +func (in *BastionSpec) DeepCopy() *BastionSpec { + if in == nil { + return nil + } + out := new(BastionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *BastionStatus) DeepCopyInto(out *BastionStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + if in.Ingress != nil { + in, out := &in.Ingress, &out.Ingress + *out = new(v1.LoadBalancerIngress) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BastionStatus. +func (in *BastionStatus) DeepCopy() *BastionStatus { + if in == nil { + return nil + } + out := new(BastionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CRIConfig) DeepCopyInto(out *CRIConfig) { + *out = *in + if in.CgroupDriver != nil { + in, out := &in.CgroupDriver, &out.CgroupDriver + *out = new(CgroupDriverName) + **out = **in + } + if in.Containerd != nil { + in, out := &in.Containerd, &out.Containerd + *out = new(ContainerdConfig) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CRIConfig. +func (in *CRIConfig) DeepCopy() *CRIConfig { + if in == nil { + return nil + } + out := new(CRIConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CloudConfig) DeepCopyInto(out *CloudConfig) { + *out = *in + out.SecretRef = in.SecretRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CloudConfig. +func (in *CloudConfig) DeepCopy() *CloudConfig { + if in == nil { + return nil + } + out := new(CloudConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Cluster) DeepCopyInto(out *Cluster) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Cluster. +func (in *Cluster) DeepCopy() *Cluster { + if in == nil { + return nil + } + out := new(Cluster) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Cluster) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterAutoscalerOptions) DeepCopyInto(out *ClusterAutoscalerOptions) { + *out = *in + if in.ScaleDownUtilizationThreshold != nil { + in, out := &in.ScaleDownUtilizationThreshold, &out.ScaleDownUtilizationThreshold + *out = new(string) + **out = **in + } + if in.ScaleDownGpuUtilizationThreshold != nil { + in, out := &in.ScaleDownGpuUtilizationThreshold, &out.ScaleDownGpuUtilizationThreshold + *out = new(string) + **out = **in + } + if in.ScaleDownUnneededTime != nil { + in, out := &in.ScaleDownUnneededTime, &out.ScaleDownUnneededTime + *out = new(metav1.Duration) + **out = **in + } + if in.ScaleDownUnreadyTime != nil { + in, out := &in.ScaleDownUnreadyTime, &out.ScaleDownUnreadyTime + *out = new(metav1.Duration) + **out = **in + } + if in.MaxNodeProvisionTime != nil { + in, out := &in.MaxNodeProvisionTime, &out.MaxNodeProvisionTime + *out = new(metav1.Duration) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterAutoscalerOptions. +func (in *ClusterAutoscalerOptions) DeepCopy() *ClusterAutoscalerOptions { + if in == nil { + return nil + } + out := new(ClusterAutoscalerOptions) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterList) DeepCopyInto(out *ClusterList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Cluster, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterList. +func (in *ClusterList) DeepCopy() *ClusterList { + if in == nil { + return nil + } + out := new(ClusterList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ClusterList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ClusterSpec) DeepCopyInto(out *ClusterSpec) { + *out = *in + in.CloudProfile.DeepCopyInto(&out.CloudProfile) + in.Seed.DeepCopyInto(&out.Seed) + in.Shoot.DeepCopyInto(&out.Shoot) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterSpec. +func (in *ClusterSpec) DeepCopy() *ClusterSpec { + if in == nil { + return nil + } + out := new(ClusterSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerRuntime) DeepCopyInto(out *ContainerRuntime) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerRuntime. +func (in *ContainerRuntime) DeepCopy() *ContainerRuntime { + if in == nil { + return nil + } + out := new(ContainerRuntime) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ContainerRuntime) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerRuntimeList) DeepCopyInto(out *ContainerRuntimeList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ContainerRuntime, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerRuntimeList. +func (in *ContainerRuntimeList) DeepCopy() *ContainerRuntimeList { + if in == nil { + return nil + } + out := new(ContainerRuntimeList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ContainerRuntimeList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerRuntimeSpec) DeepCopyInto(out *ContainerRuntimeSpec) { + *out = *in + in.WorkerPool.DeepCopyInto(&out.WorkerPool) + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerRuntimeSpec. +func (in *ContainerRuntimeSpec) DeepCopy() *ContainerRuntimeSpec { + if in == nil { + return nil + } + out := new(ContainerRuntimeSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerRuntimeStatus) DeepCopyInto(out *ContainerRuntimeStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerRuntimeStatus. +func (in *ContainerRuntimeStatus) DeepCopy() *ContainerRuntimeStatus { + if in == nil { + return nil + } + out := new(ContainerRuntimeStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerRuntimeWorkerPool) DeepCopyInto(out *ContainerRuntimeWorkerPool) { + *out = *in + in.Selector.DeepCopyInto(&out.Selector) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerRuntimeWorkerPool. +func (in *ContainerRuntimeWorkerPool) DeepCopy() *ContainerRuntimeWorkerPool { + if in == nil { + return nil + } + out := new(ContainerRuntimeWorkerPool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerdConfig) DeepCopyInto(out *ContainerdConfig) { + *out = *in + if in.Registries != nil { + in, out := &in.Registries, &out.Registries + *out = make([]RegistryConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Plugins != nil { + in, out := &in.Plugins, &out.Plugins + *out = make([]PluginConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerdConfig. +func (in *ContainerdConfig) DeepCopy() *ContainerdConfig { + if in == nil { + return nil + } + out := new(ContainerdConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlane) DeepCopyInto(out *ControlPlane) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlane. +func (in *ControlPlane) DeepCopy() *ControlPlane { + if in == nil { + return nil + } + out := new(ControlPlane) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControlPlane) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneList) DeepCopyInto(out *ControlPlaneList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]ControlPlane, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneList. +func (in *ControlPlaneList) DeepCopy() *ControlPlaneList { + if in == nil { + return nil + } + out := new(ControlPlaneList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ControlPlaneList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneSpec) DeepCopyInto(out *ControlPlaneSpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + if in.Purpose != nil { + in, out := &in.Purpose, &out.Purpose + *out = new(Purpose) + **out = **in + } + if in.InfrastructureProviderStatus != nil { + in, out := &in.InfrastructureProviderStatus, &out.InfrastructureProviderStatus + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + out.SecretRef = in.SecretRef + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneSpec. +func (in *ControlPlaneSpec) DeepCopy() *ControlPlaneSpec { + if in == nil { + return nil + } + out := new(ControlPlaneSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ControlPlaneStatus) DeepCopyInto(out *ControlPlaneStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ControlPlaneStatus. +func (in *ControlPlaneStatus) DeepCopy() *ControlPlaneStatus { + if in == nil { + return nil + } + out := new(ControlPlaneStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecord) DeepCopyInto(out *DNSRecord) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecord. +func (in *DNSRecord) DeepCopy() *DNSRecord { + if in == nil { + return nil + } + out := new(DNSRecord) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRecord) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordList) DeepCopyInto(out *DNSRecordList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]DNSRecord, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordList. +func (in *DNSRecordList) DeepCopy() *DNSRecordList { + if in == nil { + return nil + } + out := new(DNSRecordList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DNSRecordList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordSpec) DeepCopyInto(out *DNSRecordSpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + out.SecretRef = in.SecretRef + if in.Region != nil { + in, out := &in.Region, &out.Region + *out = new(string) + **out = **in + } + if in.Zone != nil { + in, out := &in.Zone, &out.Zone + *out = new(string) + **out = **in + } + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.TTL != nil { + in, out := &in.TTL, &out.TTL + *out = new(int64) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordSpec. +func (in *DNSRecordSpec) DeepCopy() *DNSRecordSpec { + if in == nil { + return nil + } + out := new(DNSRecordSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DNSRecordStatus) DeepCopyInto(out *DNSRecordStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + if in.Zone != nil { + in, out := &in.Zone, &out.Zone + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DNSRecordStatus. +func (in *DNSRecordStatus) DeepCopy() *DNSRecordStatus { + if in == nil { + return nil + } + out := new(DNSRecordStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DataVolume) DeepCopyInto(out *DataVolume) { + *out = *in + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + if in.Encrypted != nil { + in, out := &in.Encrypted, &out.Encrypted + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DataVolume. +func (in *DataVolume) DeepCopy() *DataVolume { + if in == nil { + return nil + } + out := new(DataVolume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultSpec) DeepCopyInto(out *DefaultSpec) { + *out = *in + if in.Class != nil { + in, out := &in.Class, &out.Class + *out = new(ExtensionClass) + **out = **in + } + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultSpec. +func (in *DefaultSpec) DeepCopy() *DefaultSpec { + if in == nil { + return nil + } + out := new(DefaultSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DefaultStatus) DeepCopyInto(out *DefaultStatus) { + *out = *in + if in.ProviderStatus != nil { + in, out := &in.ProviderStatus, &out.ProviderStatus + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1beta1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.LastError != nil { + in, out := &in.LastError, &out.LastError + *out = new(v1beta1.LastError) + (*in).DeepCopyInto(*out) + } + if in.LastOperation != nil { + in, out := &in.LastOperation, &out.LastOperation + *out = new(v1beta1.LastOperation) + (*in).DeepCopyInto(*out) + } + if in.State != nil { + in, out := &in.State, &out.State + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + if in.Resources != nil { + in, out := &in.Resources, &out.Resources + *out = make([]v1beta1.NamedResourceReference, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DefaultStatus. +func (in *DefaultStatus) DeepCopy() *DefaultStatus { + if in == nil { + return nil + } + out := new(DefaultStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DropIn) DeepCopyInto(out *DropIn) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DropIn. +func (in *DropIn) DeepCopy() *DropIn { + if in == nil { + return nil + } + out := new(DropIn) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Extension) DeepCopyInto(out *Extension) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Extension. +func (in *Extension) DeepCopy() *Extension { + if in == nil { + return nil + } + out := new(Extension) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Extension) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionList) DeepCopyInto(out *ExtensionList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Extension, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionList. +func (in *ExtensionList) DeepCopy() *ExtensionList { + if in == nil { + return nil + } + out := new(ExtensionList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *ExtensionList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionSpec) DeepCopyInto(out *ExtensionSpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionSpec. +func (in *ExtensionSpec) DeepCopy() *ExtensionSpec { + if in == nil { + return nil + } + out := new(ExtensionSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExtensionStatus) DeepCopyInto(out *ExtensionStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionStatus. +func (in *ExtensionStatus) DeepCopy() *ExtensionStatus { + if in == nil { + return nil + } + out := new(ExtensionStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *File) DeepCopyInto(out *File) { + *out = *in + if in.Permissions != nil { + in, out := &in.Permissions, &out.Permissions + *out = new(uint32) + **out = **in + } + in.Content.DeepCopyInto(&out.Content) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new File. +func (in *File) DeepCopy() *File { + if in == nil { + return nil + } + out := new(File) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileContent) DeepCopyInto(out *FileContent) { + *out = *in + if in.SecretRef != nil { + in, out := &in.SecretRef, &out.SecretRef + *out = new(FileContentSecretRef) + **out = **in + } + if in.Inline != nil { + in, out := &in.Inline, &out.Inline + *out = new(FileContentInline) + **out = **in + } + if in.TransmitUnencoded != nil { + in, out := &in.TransmitUnencoded, &out.TransmitUnencoded + *out = new(bool) + **out = **in + } + if in.ImageRef != nil { + in, out := &in.ImageRef, &out.ImageRef + *out = new(FileContentImageRef) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileContent. +func (in *FileContent) DeepCopy() *FileContent { + if in == nil { + return nil + } + out := new(FileContent) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileContentImageRef) DeepCopyInto(out *FileContentImageRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileContentImageRef. +func (in *FileContentImageRef) DeepCopy() *FileContentImageRef { + if in == nil { + return nil + } + out := new(FileContentImageRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileContentInline) DeepCopyInto(out *FileContentInline) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileContentInline. +func (in *FileContentInline) DeepCopy() *FileContentInline { + if in == nil { + return nil + } + out := new(FileContentInline) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FileContentSecretRef) DeepCopyInto(out *FileContentSecretRef) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FileContentSecretRef. +func (in *FileContentSecretRef) DeepCopy() *FileContentSecretRef { + if in == nil { + return nil + } + out := new(FileContentSecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Infrastructure) DeepCopyInto(out *Infrastructure) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Infrastructure. +func (in *Infrastructure) DeepCopy() *Infrastructure { + if in == nil { + return nil + } + out := new(Infrastructure) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Infrastructure) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureList) DeepCopyInto(out *InfrastructureList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Infrastructure, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureList. +func (in *InfrastructureList) DeepCopy() *InfrastructureList { + if in == nil { + return nil + } + out := new(InfrastructureList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfrastructureList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureSpec) DeepCopyInto(out *InfrastructureSpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + out.SecretRef = in.SecretRef + if in.SSHPublicKey != nil { + in, out := &in.SSHPublicKey, &out.SSHPublicKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureSpec. +func (in *InfrastructureSpec) DeepCopy() *InfrastructureSpec { + if in == nil { + return nil + } + out := new(InfrastructureSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureStatus) DeepCopyInto(out *InfrastructureStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + if in.NodesCIDR != nil { + in, out := &in.NodesCIDR, &out.NodesCIDR + *out = new(string) + **out = **in + } + if in.EgressCIDRs != nil { + in, out := &in.EgressCIDRs, &out.EgressCIDRs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Networking != nil { + in, out := &in.Networking, &out.Networking + *out = new(InfrastructureStatusNetworking) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureStatus. +func (in *InfrastructureStatus) DeepCopy() *InfrastructureStatus { + if in == nil { + return nil + } + out := new(InfrastructureStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfrastructureStatusNetworking) DeepCopyInto(out *InfrastructureStatusNetworking) { + *out = *in + if in.Pods != nil { + in, out := &in.Pods, &out.Pods + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Nodes != nil { + in, out := &in.Nodes, &out.Nodes + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Services != nil { + in, out := &in.Services, &out.Services + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfrastructureStatusNetworking. +func (in *InfrastructureStatusNetworking) DeepCopy() *InfrastructureStatusNetworking { + if in == nil { + return nil + } + out := new(InfrastructureStatusNetworking) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineDeployment) DeepCopyInto(out *MachineDeployment) { + *out = *in + if in.Priority != nil { + in, out := &in.Priority, &out.Priority + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineDeployment. +func (in *MachineDeployment) DeepCopy() *MachineDeployment { + if in == nil { + return nil + } + out := new(MachineDeployment) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MachineImage) DeepCopyInto(out *MachineImage) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MachineImage. +func (in *MachineImage) DeepCopy() *MachineImage { + if in == nil { + return nil + } + out := new(MachineImage) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Network) DeepCopyInto(out *Network) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Network. +func (in *Network) DeepCopy() *Network { + if in == nil { + return nil + } + out := new(Network) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Network) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkList) DeepCopyInto(out *NetworkList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Network, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkList. +func (in *NetworkList) DeepCopy() *NetworkList { + if in == nil { + return nil + } + out := new(NetworkList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NetworkList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkSpec) DeepCopyInto(out *NetworkSpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + if in.IPFamilies != nil { + in, out := &in.IPFamilies, &out.IPFamilies + *out = make([]IPFamily, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkSpec. +func (in *NetworkSpec) DeepCopy() *NetworkSpec { + if in == nil { + return nil + } + out := new(NetworkSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkStatus) DeepCopyInto(out *NetworkStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkStatus. +func (in *NetworkStatus) DeepCopy() *NetworkStatus { + if in == nil { + return nil + } + out := new(NetworkStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NodeTemplate) DeepCopyInto(out *NodeTemplate) { + *out = *in + if in.Capacity != nil { + in, out := &in.Capacity, &out.Capacity + *out = make(v1.ResourceList, len(*in)) + for key, val := range *in { + (*out)[key] = val.DeepCopy() + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NodeTemplate. +func (in *NodeTemplate) DeepCopy() *NodeTemplate { + if in == nil { + return nil + } + out := new(NodeTemplate) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatingSystemConfig) DeepCopyInto(out *OperatingSystemConfig) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatingSystemConfig. +func (in *OperatingSystemConfig) DeepCopy() *OperatingSystemConfig { + if in == nil { + return nil + } + out := new(OperatingSystemConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatingSystemConfig) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatingSystemConfigList) DeepCopyInto(out *OperatingSystemConfigList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OperatingSystemConfig, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatingSystemConfigList. +func (in *OperatingSystemConfigList) DeepCopy() *OperatingSystemConfigList { + if in == nil { + return nil + } + out := new(OperatingSystemConfigList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OperatingSystemConfigList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatingSystemConfigSpec) DeepCopyInto(out *OperatingSystemConfigSpec) { + *out = *in + if in.CRIConfig != nil { + in, out := &in.CRIConfig, &out.CRIConfig + *out = new(CRIConfig) + (*in).DeepCopyInto(*out) + } + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + if in.Units != nil { + in, out := &in.Units, &out.Units + *out = make([]Unit, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.Files != nil { + in, out := &in.Files, &out.Files + *out = make([]File, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatingSystemConfigSpec. +func (in *OperatingSystemConfigSpec) DeepCopy() *OperatingSystemConfigSpec { + if in == nil { + return nil + } + out := new(OperatingSystemConfigSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OperatingSystemConfigStatus) DeepCopyInto(out *OperatingSystemConfigStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + if in.ExtensionUnits != nil { + in, out := &in.ExtensionUnits, &out.ExtensionUnits + *out = make([]Unit, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ExtensionFiles != nil { + in, out := &in.ExtensionFiles, &out.ExtensionFiles + *out = make([]File, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.CloudConfig != nil { + in, out := &in.CloudConfig, &out.CloudConfig + *out = new(CloudConfig) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OperatingSystemConfigStatus. +func (in *OperatingSystemConfigStatus) DeepCopy() *OperatingSystemConfigStatus { + if in == nil { + return nil + } + out := new(OperatingSystemConfigStatus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PluginConfig) DeepCopyInto(out *PluginConfig) { + *out = *in + if in.Op != nil { + in, out := &in.Op, &out.Op + *out = new(PluginPathOperation) + **out = **in + } + if in.Path != nil { + in, out := &in.Path, &out.Path + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Values != nil { + in, out := &in.Values, &out.Values + *out = new(apiextensionsv1.JSON) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PluginConfig. +func (in *PluginConfig) DeepCopy() *PluginConfig { + if in == nil { + return nil + } + out := new(PluginConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryConfig) DeepCopyInto(out *RegistryConfig) { + *out = *in + if in.Server != nil { + in, out := &in.Server, &out.Server + *out = new(string) + **out = **in + } + if in.Hosts != nil { + in, out := &in.Hosts, &out.Hosts + *out = make([]RegistryHost, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ReadinessProbe != nil { + in, out := &in.ReadinessProbe, &out.ReadinessProbe + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryConfig. +func (in *RegistryConfig) DeepCopy() *RegistryConfig { + if in == nil { + return nil + } + out := new(RegistryConfig) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RegistryHost) DeepCopyInto(out *RegistryHost) { + *out = *in + if in.Capabilities != nil { + in, out := &in.Capabilities, &out.Capabilities + *out = make([]RegistryCapability, len(*in)) + copy(*out, *in) + } + if in.CACerts != nil { + in, out := &in.CACerts, &out.CACerts + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RegistryHost. +func (in *RegistryHost) DeepCopy() *RegistryHost { + if in == nil { + return nil + } + out := new(RegistryHost) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Unit) DeepCopyInto(out *Unit) { + *out = *in + if in.Command != nil { + in, out := &in.Command, &out.Command + *out = new(UnitCommand) + **out = **in + } + if in.Enable != nil { + in, out := &in.Enable, &out.Enable + *out = new(bool) + **out = **in + } + if in.Content != nil { + in, out := &in.Content, &out.Content + *out = new(string) + **out = **in + } + if in.DropIns != nil { + in, out := &in.DropIns, &out.DropIns + *out = make([]DropIn, len(*in)) + copy(*out, *in) + } + if in.FilePaths != nil { + in, out := &in.FilePaths, &out.FilePaths + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Unit. +func (in *Unit) DeepCopy() *Unit { + if in == nil { + return nil + } + out := new(Unit) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Volume) DeepCopyInto(out *Volume) { + *out = *in + if in.Name != nil { + in, out := &in.Name, &out.Name + *out = new(string) + **out = **in + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + if in.Encrypted != nil { + in, out := &in.Encrypted, &out.Encrypted + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Volume. +func (in *Volume) DeepCopy() *Volume { + if in == nil { + return nil + } + out := new(Volume) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Worker) DeepCopyInto(out *Worker) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Worker. +func (in *Worker) DeepCopy() *Worker { + if in == nil { + return nil + } + out := new(Worker) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Worker) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerList) DeepCopyInto(out *WorkerList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Worker, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerList. +func (in *WorkerList) DeepCopy() *WorkerList { + if in == nil { + return nil + } + out := new(WorkerList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *WorkerList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerPool) DeepCopyInto(out *WorkerPool) { + *out = *in + out.MaxSurge = in.MaxSurge + out.MaxUnavailable = in.MaxUnavailable + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Taints != nil { + in, out := &in.Taints, &out.Taints + *out = make([]v1.Taint, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + out.MachineImage = in.MachineImage + if in.NodeAgentSecretName != nil { + in, out := &in.NodeAgentSecretName, &out.NodeAgentSecretName + *out = new(string) + **out = **in + } + if in.ProviderConfig != nil { + in, out := &in.ProviderConfig, &out.ProviderConfig + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + in.UserDataSecretRef.DeepCopyInto(&out.UserDataSecretRef) + if in.Volume != nil { + in, out := &in.Volume, &out.Volume + *out = new(Volume) + (*in).DeepCopyInto(*out) + } + if in.DataVolumes != nil { + in, out := &in.DataVolumes, &out.DataVolumes + *out = make([]DataVolume, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.KubeletDataVolumeName != nil { + in, out := &in.KubeletDataVolumeName, &out.KubeletDataVolumeName + *out = new(string) + **out = **in + } + if in.Zones != nil { + in, out := &in.Zones, &out.Zones + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.MachineControllerManagerSettings != nil { + in, out := &in.MachineControllerManagerSettings, &out.MachineControllerManagerSettings + *out = new(v1beta1.MachineControllerManagerSettings) + (*in).DeepCopyInto(*out) + } + if in.KubernetesVersion != nil { + in, out := &in.KubernetesVersion, &out.KubernetesVersion + *out = new(string) + **out = **in + } + if in.NodeTemplate != nil { + in, out := &in.NodeTemplate, &out.NodeTemplate + *out = new(NodeTemplate) + (*in).DeepCopyInto(*out) + } + if in.Architecture != nil { + in, out := &in.Architecture, &out.Architecture + *out = new(string) + **out = **in + } + if in.ClusterAutoscaler != nil { + in, out := &in.ClusterAutoscaler, &out.ClusterAutoscaler + *out = new(ClusterAutoscalerOptions) + (*in).DeepCopyInto(*out) + } + if in.Priority != nil { + in, out := &in.Priority, &out.Priority + *out = new(int32) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerPool. +func (in *WorkerPool) DeepCopy() *WorkerPool { + if in == nil { + return nil + } + out := new(WorkerPool) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerSpec) DeepCopyInto(out *WorkerSpec) { + *out = *in + in.DefaultSpec.DeepCopyInto(&out.DefaultSpec) + if in.InfrastructureProviderStatus != nil { + in, out := &in.InfrastructureProviderStatus, &out.InfrastructureProviderStatus + *out = new(runtime.RawExtension) + (*in).DeepCopyInto(*out) + } + out.SecretRef = in.SecretRef + if in.SSHPublicKey != nil { + in, out := &in.SSHPublicKey, &out.SSHPublicKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Pools != nil { + in, out := &in.Pools, &out.Pools + *out = make([]WorkerPool, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerSpec. +func (in *WorkerSpec) DeepCopy() *WorkerSpec { + if in == nil { + return nil + } + out := new(WorkerSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *WorkerStatus) DeepCopyInto(out *WorkerStatus) { + *out = *in + in.DefaultStatus.DeepCopyInto(&out.DefaultStatus) + if in.MachineDeployments != nil { + in, out := &in.MachineDeployments, &out.MachineDeployments + *out = make([]MachineDeployment, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.MachineDeploymentsLastUpdateTime != nil { + in, out := &in.MachineDeploymentsLastUpdateTime, &out.MachineDeploymentsLastUpdateTime + *out = (*in).DeepCopy() + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new WorkerStatus. +func (in *WorkerStatus) DeepCopy() *WorkerStatus { + if in == nil { + return nil + } + out := new(WorkerStatus) + in.DeepCopyInto(out) + return out +} diff --git a/api/go.mod b/api/go.mod new file mode 100644 index 0000000..6514354 --- /dev/null +++ b/api/go.mod @@ -0,0 +1,75 @@ +module github.com/openmcp-project/mcp-operator/api + +go 1.23.0 + +toolchain go1.23.6 + +require ( + github.com/onsi/ginkgo/v2 v2.22.2 + github.com/onsi/gomega v1.36.2 + github.com/openmcp-project/controller-utils v0.4.2 + k8s.io/api v0.32.2 + k8s.io/apiextensions-apiserver v0.32.2 + k8s.io/apimachinery v0.32.2 + k8s.io/client-go v0.32.2 + k8s.io/utils v0.0.0-20241210054802-24370beab758 + sigs.k8s.io/controller-runtime v0.20.2 + sigs.k8s.io/yaml v1.4.0 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/pflag v1.0.6 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.10.0 // indirect + golang.org/x/tools v0.28.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect +) diff --git a/api/go.sum b/api/go.sum new file mode 100644 index 0000000..67e7d61 --- /dev/null +++ b/api/go.sum @@ -0,0 +1,184 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v4.12.0+incompatible h1:4onqiflcdA9EOZ4RxV643DvftH5pOlLGNtQ5lPWQu84= +github.com/evanphx/json-patch v4.12.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/openmcp-project/controller-utils v0.4.2 h1:8viXmZZdBLanN1ummLr05ceCd7XaEBToflm1hU17ecQ= +github.com/openmcp-project/controller-utils v0.4.2/go.mod h1:6BeRpSZK/FkJbelqGA4pv10FEtNerT2RvWb3Eg1Z9j0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8= +golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= +k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= +k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= +k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= +k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= +k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= +k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= +k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.2 h1:/439OZVxoEc02psi1h4QO3bHzTgu49bb347Xp4gW1pc= +sigs.k8s.io/controller-runtime v0.20.2/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/api/install/install.go b/api/install/install.go new file mode 100644 index 0000000..9d88bc9 --- /dev/null +++ b/api/install/install.go @@ -0,0 +1,15 @@ +package install + +import ( + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// Install installs all APIs in the scheme. +func Install(scheme *runtime.Scheme) { + utilruntime.Must(v1alpha1.AddToScheme(scheme)) + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) +} diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..45d9545 --- /dev/null +++ b/api/v1alpha1/groupversion_info.go @@ -0,0 +1,20 @@ +// Package v1alpha1 contains API Schema definitions for the v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=openmcp.cloud +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +var ( + // GroupVersion is group version used to register these objects + GroupVersion = schema.GroupVersion{Group: "openmcp.cloud", Version: "v1alpha1"} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} + + // AddToScheme adds the types in this group-version to the given scheme. + AddToScheme = SchemeBuilder.AddToScheme +) diff --git a/blueprints/mcp-operator/blueprint.yaml b/blueprints/mcp-operator/blueprint.yaml new file mode 100644 index 0000000..8418a6a --- /dev/null +++ b/blueprints/mcp-operator/blueprint.yaml @@ -0,0 +1,184 @@ +apiVersion: landscaper.gardener.cloud/v1alpha1 +kind: Blueprint +jsonSchemaVersion: "https://json-schema.org/draft/2019-09/schema" + +imports: +- name: release-name + type: data + schema: + type: string + # helm release name + +- name: release-namespace + type: data + schema: + type: string + # release namespace + +- name: helm-values + type: data + required: false + schema: {} + # additional helm values + # will not overwrite the ones that can be specifically specified below + +- name: image-repo + type: data + required: false + schema: + type: string + # image repository + +- name: image-version + type: data + required: false + schema: + type: string + # image tag + +- name: image-pull-secret + type: data + required: false + schema: + type: string + # image pull secret (the actual secret, base64 encoded) + +- name: image-pull-secret-ref + type: data + required: false + schema: + type: string + # image pull secret (a reference to a secret in the cluster (in the release namespace) containing the pull secret) + +- name: mcp-controllers + type: data + required: false + schema: + type: array + items: + type: string + # a list of mcp controllers that should be active + +- name: apiserver-config + type: data + required: false + schema: + type: object + # the config for the APIServer (what would be put under 'apiserver.config' in the helm values otherwise) + +- name: authentication-config + type: data + required: false + schema: + type: object + # the config for the Authentication controller (what would be put under 'authentication.config' in the helm values otherwise) + +- name: authorization-config + type: data + required: false + schema: + type: object + # the config for the Authorization controller (what would be put under 'authorization.config' in the helm values otherwise) + +- name: mcp-system-cluster + type: target + targetType: landscaper.gardener.cloud/kubernetes-cluster + # kubeconfig for the cluster the MCPO should be deployed into + +- name: mcp-crate-cluster + type: target + required: false + targetType: landscaper.gardener.cloud/kubernetes-cluster # either kubeconfig or oidc trust config + # kubeconfig for the cluster that should be watched by the MCP operator + # MCPO watches host cluster if not specified + +- name: laas-core-cluster + type: target + required: false + targetType: landscaper.gardener.cloud/kubernetes-cluster # either kubeconfig or oidc trust config + # kubeconfig for the cluster where the LandscaperDeployments should be created + # host cluster is used if not specified + +- name: cloud-orchestrator-core-cluster + type: target + required: false + targetType: landscaper.gardener.cloud/kubernetes-cluster # either kubeconfig or oidc trust config + # kubeconfig for the cluster where the CO resources should be created + # host cluster is used if not specified + +deployExecutions: +- name: default + type: Spiff + template: + constants: + <<<: (( &temporary )) + pull-secret-name: (( imports.release-name "-imagepull" )) + resources: + <<<: (( &temporary )) + chart: (( getResource(cd, "name", "mcp-operator-chart") )) + image: (( getResource(cd, "name", "mcp-operator-image") )) + deployItems: + - <<<: (( valid(imports.image-pull-secret) ? ~ :~~ )) # this line removes this entry if no pull secret is given + name: pull-secret + type: landscaper.gardener.cloud/kubernetes-manifest + target: + import: mcp-system-cluster + config: + apiVersion: manifest.deployer.landscaper.gardener.cloud/v1alpha2 + kind: ProviderConfiguration + updateStrategy: update + manifests: + - policy: manage + manifest: + apiVersion: v1 + kind: Secret + type: kubernetes.io/dockerconfigjson + metadata: + name: (( constants.pull-secret-name )) + namespace: (( imports.release-namespace )) + data: + .dockerconfigjson: (( imports.image-pull-secret )) + - name: controller + type: landscaper.gardener.cloud/helm + dependsOn: + - (( valid(imports.image-pull-secret) ? "pull-secret" :~~ )) + target: + import: mcp-system-cluster + config: + apiVersion: helm.deployer.landscaper.gardener.cloud/v1alpha1 + kind: ProviderConfiguration + updateStrategy: update + name: (( imports.release-name )) + namespace: (( imports.release-namespace )) + helmDeployment: false + chart: + ref: (( resources.chart.access.imageReference )) + values: + <<<: (( imports.helm-values || ~ )) + image: + repository: (( imports.image-repo || ociRefRepo(resources.image.access.imageReference) )) + tag: (( imports.image-version || ociRefVersion(resources.image.access.imageReference) )) + pullSecrets: + - (( imports.image-pull-secret-ref || ~~ )) + - (( valid(imports.image-pull-secret) ? constants.pull-secret-name :~~ )) + clusters: + crate: (( valid(imports.mcp-crate-cluster) ? ( imports.mcp-crate-cluster.spec.config || imports.mcp-crate-cluster.spec ) :~~ )) + managedcontrolplane: + disabled: (( valid(imports.mcp-controllers) ? ! contains(imports.mcp-controllers, "managedcontrolplane") :~~ )) + apiserver: + disabled: (( valid(imports.mcp-controllers) ? ! contains(imports.mcp-controllers, "apiserver") :~~ )) + config: (( valid(imports.apiserver-config) ? imports.apiserver-config :~~ )) + authentication: + disabled: (( valid(imports.mcp-controllers) ? ! contains(imports.mcp-controllers, "authentication") :~~ )) + config: (( valid(imports.authentication-config) ? imports.authentication-config :~~ )) + authorization: + disabled: (( valid(imports.mcp-controllers) ? ! contains(imports.mcp-controllers, "authorization") :~~ )) + config: (( valid(imports.authorization-config) ? imports.authorization-config :~~ )) + landscaper: + disabled: (( valid(imports.mcp-controllers) ? ! contains(imports.mcp-controllers, "landscaper") :~~ )) + clusters: + core: (( valid(imports.laas-core-cluster) ? ( imports.laas-core-cluster.spec.config || imports.laas-core-cluster.spec ) :~~ )) + cloudOrchestrator: + disabled: (( valid(imports.mcp-controllers) ? ! ( contains(imports.mcp-controllers, "cloudOrchestrator") -or contains(imports.mcp-controllers, "cloudorchestrator") ) :~~ )) + clusters: + core: (( valid(imports.cloud-orchestrator-core-cluster) ? ( imports.cloud-orchestrator-core-cluster.spec.config || imports.cloud-orchestrator-core-cluster.spec ) :~~ )) diff --git a/charts/mcp-operator/Chart.yaml b/charts/mcp-operator/Chart.yaml new file mode 100644 index 0000000..e05e9fc --- /dev/null +++ b/charts/mcp-operator/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: mcp-operator +description: A Helm chart for the mcp-operator +type: application +version: v0.26.0 +appVersion: v0.26.0 +home: https://github.com/openmcp-project/mcp-operator +sources: + - https://github.com/openmcp-project/mcp-operator \ No newline at end of file diff --git a/charts/mcp-operator/templates/_helpers.tpl b/charts/mcp-operator/templates/_helpers.tpl new file mode 100644 index 0000000..a3be82f --- /dev/null +++ b/charts/mcp-operator/templates/_helpers.tpl @@ -0,0 +1,104 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "mcp-operator.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "mcp-operator.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Name of the clusterrole(binding) if in-cluster config is used for the crate cluster. +*/}} +{{- define "mcp-operator.clusterrole" -}} +{{- print "openmcp.cloud:" ( include "mcp-operator.fullname" . ) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Name of the clusterrole(binding) if in-cluster config is used for the laas cluster. +*/}} +{{- define "mcp-operator.landscaper.clusterrole" -}} +{{- print "openmcp.cloud:laas:" ( include "mcp-operator.fullname" . ) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Name of the clusterrole(binding) if in-cluster config is used for the cloudorchestrator cluster. +*/}} +{{- define "mcp-operator.cloudorchestrator.clusterrole" -}} +{{- print "openmcp.cloud:co:" ( include "mcp-operator.fullname" . ) | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Use : or @, depending on which is given. +*/}} +{{- define "image" -}} +{{- if hasPrefix "sha256:" (required "$.tag is required" $.tag) -}} +{{ required "$.repository is required" $.repository }}@{{ required "$.tag is required" $.tag }} +{{- else -}} +{{ required "$.repository is required" $.repository }}:{{ required "$.tag is required" $.tag }} +{{- end -}} +{{- end -}} + +{{/* +Renders a list of controllers that have not been deactivated. +If all controllers are active, the result of this is: +- managedcontrolplane +- apiserver +- landscaper +- cloudorchestrator +- authentication +- authorization +*/}} +{{- define "mcp-operator.activeControllers" -}} +{{- range tuple "managedcontrolplane" "apiserver" "landscaper" "cloudOrchestrator" "authentication" "authorization"}} +{{- if not ( "disabled" | get ( . | get $ )) }} +- {{ . | lower }} +{{- end -}} +{{- end -}} +{{- end -}} + +{{/* +Same as 'mcp-operator.activeControllers', but as a comma-separated string. +If all controllers are active, the result of this is: +managedcontrolplane,apiserver,landscaper,cloudorchestrator,authentication +*/}} +{{- define "mcp-operator.activeControllersString" -}} +{{ join "," ( include "mcp-operator.activeControllers" $ | fromYamlArray ) }} +{{- end -}} + +{{/* +Common labels +*/}} +{{- define "mcp-operator.labels" -}} +helm.sh/chart-name: {{ .Chart.Name }} +helm.sh/chart-version: {{ .Chart.Version | quote }} +{{ include "mcp-operator.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "mcp-operator.selectorLabels" -}} +app.kubernetes.io/name: {{ include "mcp-operator.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/charts/mcp-operator/templates/_versions.tpl b/charts/mcp-operator/templates/_versions.tpl new file mode 100644 index 0000000..b73b01e --- /dev/null +++ b/charts/mcp-operator/templates/_versions.tpl @@ -0,0 +1,7 @@ +{{- define "rbacversion" -}} +rbac.authorization.k8s.io/v1 +{{- end -}} + +{{- define "deploymentversion" -}} +apps/v1 +{{- end -}} \ No newline at end of file diff --git a/charts/mcp-operator/templates/deployment.yaml b/charts/mcp-operator/templates/deployment.yaml new file mode 100644 index 0000000..e14962c --- /dev/null +++ b/charts/mcp-operator/templates/deployment.yaml @@ -0,0 +1,306 @@ +apiVersion: {{ include "deploymentversion" . }} +kind: Deployment +metadata: + name: mcp-operator + namespace: {{ .Release.Namespace }} + labels: + app: cola-onboarding + role: mcp-operator + chart-name: "{{ .Chart.Name }}" + chart-version: "{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" +spec: + replicas: {{ .Values.deployment.replicaCount }} + minReadySeconds: {{ .Values.deployment.minReadySeconds }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: {{ .Values.deployment.maxSurge }} + maxUnavailable: {{ .Values.deployment.maxUnavailable }} + selector: + matchLabels: + {{- include "mcp-operator.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/apiserver-config: {{ include (print $.Template.BasePath "/secret-apiserver-config.yaml") . | sha256sum }} + checksum/common-clusters: {{ include (print $.Template.BasePath "/secrets-common-clusters.yaml") . | sha256sum }} + checksum/laas-clusters: {{ include (print $.Template.BasePath "/secrets-landscaper-clusters.yaml") . | sha256sum }} + checksum/co-clusters: {{ include (print $.Template.BasePath "/secrets-cloudorchestrator-clusters.yaml") . | sha256sum }} + checksum/auth-config: {{ include (print $.Template.BasePath "/secret-auth-config.yaml") . | sha256sum }} + checksum/authz-config: {{ include (print $.Template.BasePath "/secret-authz-config.yaml") . | sha256sum }} + {{- with .Values.podAnnotations }} + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + openmcp.cloud/topology: mcp-operator + openmcp.cloud/topology-ns: {{ .Release.Namespace }} + {{- include "mcp-operator.labels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: mcp-operator + containers: + - name: mcp-operator + image: "{{ include "image" .Values.image }}" + imagePullPolicy: "{{.Values.image.pullPolicy}}" + command: + - /mcp-operator + - --controllers={{ include "mcp-operator.activeControllersString" .Values }} + {{- if .Values.deployment.leaderElection.enabled }} + - --leader-elect + - --lease-namespace={{ .Values.deployment.leaderElection.leaseNamespace }} + {{- end }} + {{- if .Values.webhooks.manage }} + - --install-webhooks + {{- end }} + - --metrics-bind-address=:{{ .Values.metrics.listen.port }} + {{- if has "apiserver" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - --apiserver-config=/etc/config/apiserver/config.yaml + {{- end }} + {{- if has "authentication" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - --auth-config=/etc/config/authentication/config.yaml + {{- end }} + {{- if has "authorization" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - --authz-config=/etc/config/authorization/config.yaml + {{- end }} + {{- if has "landscaper" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + {{- if and .Values.landscaper.clusters .Values.landscaper.clusters.core }} + - --laas-cluster=/etc/config/landscaper/clusters/core + {{- end }} + {{- end }} + {{- if has "cloudorchestrator" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + {{- if and .Values.cloudOrchestrator.clusters .Values.cloudOrchestrator.clusters.core }} + - --co-cluster=/etc/config/cloudorchestrator/clusters/core + {{- end }} + {{- end }} + {{- if and .Values.clusters .Values.clusters.crate }} + - --crate-cluster=/etc/config/common/clusters/crate + {{- end }} + {{- if and .Values.logging .Values.logging.verbosity }} + - -v={{ .Values.logging.verbosity }} + {{- end }} + {{- if .Values.apiserver.worker.maxWorkers }} + - --apiserver-workers={{ .Values.apiserver.worker.maxWorkers }} + {{- end }} + {{- if .Values.apiserver.worker.intervalTime }} + - --apiserver-worker-interval={{ .Values.apiserver.worker.intervalTime }} + {{- end }} + ports: + {{- if not .Values.webhooks.disabled }} + - name: webhooks-https + containerPort: 9443 + {{- end }} + - name: metrics-http + containerPort: {{ .Values.metrics.listen.port }} + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: 8081 + initialDelaySeconds: 15 + periodSeconds: 20 + readinessProbe: + httpGet: + path: /readyz + port: 8081 + initialDelaySeconds: 5 + periodSeconds: 10 + volumeMounts: + {{- if has "apiserver" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: apiserver + mountPath: /etc/config/apiserver + readOnly: true + {{- end }} + {{- if has "authentication" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: authentication + mountPath: /etc/config/authentication + readOnly: true + {{- end }} + {{- if has "authorization" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: authorization + mountPath: /etc/config/authorization + readOnly: true + {{- end }} + {{- if has "landscaper" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: landscaper + mountPath: /etc/config/landscaper + readOnly: true + {{- end }} + {{- if has "cloudorchestrator" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: cloudorchestrator + mountPath: /etc/config/cloudorchestrator + readOnly: true + {{- end }} + {{- if not .Values.webhooks.disabled }} + - name: {{ include "mcp-operator.fullname" . }}-webhooks-tls + mountPath: /tmp/k8s-webhook-server/serving-certs/ + readOnly: true + {{- end }} + - name: common + mountPath: /etc/config/common + readOnly: true + resources: + requests: + cpu: {{ .Values.resources.requests.cpu | default "100m" }} + memory: {{ .Values.resources.requests.memory | default "256Mi" }} + {{- if .Values.resources.limits }} + limits: + {{- .Values.resources.limits | toYaml | nindent 12 }} + {{- end }} + volumes: + {{- if has "apiserver" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: apiserver + projected: + sources: + - secret: + name: apiserver-provider-config + {{- end }} + {{- if has "authentication" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: authentication + projected: + sources: + - secret: + name: authentication-provider-config + {{- end }} + {{- if has "authorization" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: authorization + projected: + sources: + - secret: + name: authorization-provider-config + {{- end }} + {{- if has "landscaper" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: landscaper + projected: + sources: + {{- if and .Values.landscaper.clusters }} + {{- range $cname, $cvalues := .Values.landscaper.clusters }} + {{- if $cvalues.kubeconfig }} + - secret: + name: landscaper-{{ $cname }}-cluster + items: + - key: kubeconfig + path: clusters/{{ $cname }}/kubeconfig + {{- else }} + - secret: + name: landscaper-{{ $cname }}-cluster + items: + - key: host + path: clusters/{{ $cname }}/host + {{- if $cvalues.caData }} + - key: caData + path: clusters/{{ $cname }}/ca.crt + {{- end }} + - serviceAccountToken: + path: clusters/{{ $cname }}/token + expirationSeconds: 7200 + audience: {{ $cvalues.audience }} + {{- if $cvalues.caConfigMapName }} + - configMap: + name: {{ $cvalues.caConfigMapName }} + items: + - key: ca.crt + path: clusters/{{ $cname }}/ca.crt + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- if has "cloudorchestrator" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} + - name: cloudorchestrator + projected: + sources: + {{- if and .Values.cloudOrchestrator.clusters }} + {{- range $cname, $cvalues := .Values.cloudOrchestrator.clusters }} + {{- if $cvalues.kubeconfig }} + - secret: + name: cloudorchestrator-{{ $cname }}-cluster + items: + - key: kubeconfig + path: clusters/{{ $cname }}/kubeconfig + {{- else }} + - secret: + name: cloudorchestrator-{{ $cname }}-cluster + items: + - key: host + path: clusters/{{ $cname }}/host + {{- if $cvalues.caData }} + - key: caData + path: clusters/{{ $cname }}/ca.crt + {{- end }} + - serviceAccountToken: + path: clusters/{{ $cname }}/token + expirationSeconds: 7200 + audience: {{ $cvalues.audience }} + {{- if $cvalues.caConfigMapName }} + - configMap: + name: {{ $cvalues.caConfigMapName }} + items: + - key: ca.crt + path: clusters/{{ $cname }}/ca.crt + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- end }} + - name: common + projected: + sources: + {{- if and .Values.clusters }} + {{- range $cname, $cvalues := .Values.clusters }} + {{- if $cvalues.kubeconfig }} + - secret: + name: {{ $cname }}-cluster + items: + - key: kubeconfig + path: clusters/{{ $cname }}/kubeconfig + {{- else }} + - secret: + name: {{ $cname }}-cluster + items: + - key: host + path: clusters/{{ $cname }}/host + {{- if $cvalues.caData }} + - key: caData + path: clusters/{{ $cname }}/ca.crt + {{- end }} + - serviceAccountToken: + path: clusters/{{ $cname }}/token + expirationSeconds: 7200 + audience: {{ $cvalues.audience }} + {{- if $cvalues.caConfigMapName }} + - configMap: + name: {{ $cvalues.caConfigMapName }} + items: + - key: ca.crt + path: clusters/{{ $cname }}/ca.crt + {{- end }} + {{- end }} + {{- end }} + {{- end }} + {{- if not .Values.webhooks.disabled }} + - name: {{ include "mcp-operator.fullname" . }}-webhooks-tls + secret: + secretName: {{ include "mcp-operator.fullname" . }}-webhooks-tls + {{- end }} + {{- if .Values.deployment.topologySpreadConstraints.enabled }} + topologySpreadConstraints: + - maxSkew: {{ .Values.deployment.topologySpreadConstraints.maxSkew }} + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + openmcp.cloud/topology: mcp-operator + openmcp.cloud/topology-ns: {{ .Release.Namespace }} + - maxSkew: {{ .Values.deployment.topologySpreadConstraints.maxSkew }} + topologyKey: kubernetes.io/hostname + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + openmcp.cloud/topology: mcp-operator + openmcp.cloud/topology-ns: {{ .Release.Namespace }} + {{- end }} \ No newline at end of file diff --git a/charts/mcp-operator/templates/init-job.yaml b/charts/mcp-operator/templates/init-job.yaml new file mode 100644 index 0000000..cd092df --- /dev/null +++ b/charts/mcp-operator/templates/init-job.yaml @@ -0,0 +1,111 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: mcp-operator-init + labels: + app: cola-onboarding + role: mcp-operator + chart-name: "{{ .Chart.Name }}" + chart-version: "{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" + annotations: + helm.sh/hook: post-install,post-upgrade + helm.sh/hook-delete-policy: before-hook-creation +spec: + template: + metadata: + name: mcp-operator-init + labels: + app: cola-onboarding + role: mcp-operator + chart-name: "{{ .Chart.Name }}" + chart-version: "{{ .Chart.Version }}" + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: mcp-operator + restartPolicy: Never + containers: + - name: mcp-operator-init + image: "{{ include "image" .Values.image }}" + imagePullPolicy: "{{.Values.image.pullPolicy}}" + command: + - /mcp-operator + - --init + - --controllers="" + {{- if .Values.crds.manage }} + - --install-crds + {{- end }} + {{- if .Values.webhooks.manage }} + - --install-webhooks + {{- if .Values.webhooks.url }} + - --webhooks-base-url={{ .Values.webhooks.url }} + - --webhooks-without-ca + {{- end }} + {{- end }} + {{- if and .Values.clusters .Values.clusters.crate }} + - --crate-cluster=/etc/config/common/clusters/crate + {{- end }} + {{- if and .Values.logging .Values.logging.verbosity }} + - -v={{ .Values.logging.verbosity }} + {{- end }} + env: + {{- if not .Values.webhooks.disabled }} + - name: WEBHOOK_SECRET_NAME + value: {{ include "mcp-operator.fullname" . }}-webhooks-tls + - name: WEBHOOK_SECRET_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: WEBHOOK_SERVICE_NAME + value: {{ include "mcp-operator.fullname" . }}-webhooks + - name: WEBHOOK_SERVICE_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + {{- end }} + volumeMounts: + - name: common + mountPath: /etc/config/common + readOnly: true + volumes: + - name: common + projected: + sources: + {{- if and .Values.clusters }} + {{- range $cname, $cvalues := .Values.clusters }} + {{- if $cvalues.kubeconfig }} + - secret: + name: {{ $cname }}-cluster + items: + - key: kubeconfig + path: clusters/{{ $cname }}/kubeconfig + {{- else }} + - secret: + name: {{ $cname }}-cluster + items: + - key: host + path: clusters/{{ $cname }}/host + {{- if $cvalues.caData }} + - key: caData + path: clusters/{{ $cname }}/ca.crt + {{- end }} + - serviceAccountToken: + path: clusters/{{ $cname }}/token + expirationSeconds: 7200 + audience: {{ $cvalues.audience }} + {{- if $cvalues.caConfigMapName }} + - configMap: + name: {{ $cvalues.caConfigMapName }} + items: + - key: ca.crt + path: clusters/{{ $cname }}/ca.crt + {{- end }} + {{- end }} + {{- end }} + {{- end }} \ No newline at end of file diff --git a/charts/mcp-operator/templates/rbac.yaml b/charts/mcp-operator/templates/rbac.yaml new file mode 100644 index 0000000..fbd0b92 --- /dev/null +++ b/charts/mcp-operator/templates/rbac.yaml @@ -0,0 +1,177 @@ +{{- if not ( and .Values.clusters .Values.clusters.crate ) }} +apiVersion: {{ include "rbacversion" . }} +kind: ClusterRole +metadata: + name: {{ include "mcp-operator.clusterrole" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - core.openmcp.cloud + resources: + - managedcontrolplanes + - managedcontrolplanes/status + - internalconfigurations + - apiservers + - landscapers + - cloudorchestrators + - authentications + - authorizations + - clusteradmin + verbs: + - "*" +- apiGroups: + - "" + resources: + - "namespaces" + verbs: + - get + - list + - watch +- apiGroups: + - coordination.k8s.io + resources: + - leases + - leases/status + verbs: + - "*" +- apiGroups: + - "" + resources: + - events + verbs: + - "*" +{{- if not .Values.webhooks.disabled }} +- apiGroups: ["admissionregistration.k8s.io"] + resources: + - validatingwebhookconfigurations + # - mutatingwebhookconfigurations for now we are only using validatingwebhooks + verbs: ["*"] +{{- end }} +{{- if not (and .Values.crds .Values.crds.disabled) }} +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - '*' +--- +{{- end }} +kind: ClusterRoleBinding +apiVersion: {{ include "rbacversion" . }} +metadata: + name: {{ include "mcp-operator.clusterrole" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + name: mcp-operator + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ include "mcp-operator.clusterrole" . }} + apiGroup: rbac.authorization.k8s.io +--- +{{- end }} +{{- if has "landscaper" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} +{{- if not ( and .Values.landscaper.clusters .Values.landscaper.clusters.core ) }} +apiVersion: {{ include "rbacversion" . }} +kind: ClusterRole +metadata: + name: {{ include "mcp-operator.landscaper.clusterrole" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - "" + resources: + - "namespaces" + verbs: + - "*" +- apiGroups: + - landscaper-service.gardener.cloud + resources: + - landscaperdeployments + verbs: + - "*" +--- +kind: ClusterRoleBinding +apiVersion: {{ include "rbacversion" . }} +metadata: + name: {{ include "mcp-operator.landscaper.clusterrole" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + name: mcp-operator + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ include "mcp-operator.landscaper.clusterrole" . }} + apiGroup: rbac.authorization.k8s.io +--- +{{- end }} +{{- end }} +{{- if has "cloudorchestrator" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} +{{- if not ( and .Values.cloudOrchestrator.clusters .Values.cloudOrchestrator.clusters.core ) }} +apiVersion: {{ include "rbacversion" . }} +kind: ClusterRole +metadata: + name: {{ include "mcp-operator.cloudorchestrator.clusterrole" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +rules: +- apiGroups: + - core.orchestrate.cloud.sap + resources: + - managedcontrolplanes + verbs: + - "*" +--- +kind: ClusterRoleBinding +apiVersion: {{ include "rbacversion" . }} +metadata: + name: {{ include "mcp-operator.cloudorchestrator.clusterrole" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + name: mcp-operator + namespace: {{ .Release.Namespace }} +roleRef: + kind: ClusterRole + name: {{ include "mcp-operator.cloudorchestrator.clusterrole" . }} + apiGroup: rbac.authorization.k8s.io +--- +{{- end }} +{{- end }} +{{- if not .Values.webhooks.disabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: {{ include "mcp-operator.fullname" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["*"] + resourceNames: + - {{ include "mcp-operator.fullname" . }}-webhooks-tls +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: {{ include "mcp-operator.fullname" . }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "mcp-operator.fullname" . }} +subjects: +- kind: ServiceAccount + name: mcp-operator + namespace: {{ .Release.Namespace }} +--- +{{- end }} \ No newline at end of file diff --git a/charts/mcp-operator/templates/secret-apiserver-config.yaml b/charts/mcp-operator/templates/secret-apiserver-config.yaml new file mode 100644 index 0000000..eb3c6e2 --- /dev/null +++ b/charts/mcp-operator/templates/secret-apiserver-config.yaml @@ -0,0 +1,11 @@ +{{- if has "apiserver" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} +apiVersion: v1 +kind: Secret +metadata: + name: apiserver-provider-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +data: + config.yaml: {{ .Values.apiserver.config | toYaml | b64enc }} +{{- end }} diff --git a/charts/mcp-operator/templates/secret-auth-config.yaml b/charts/mcp-operator/templates/secret-auth-config.yaml new file mode 100644 index 0000000..7991711 --- /dev/null +++ b/charts/mcp-operator/templates/secret-auth-config.yaml @@ -0,0 +1,11 @@ +{{- if has "authentication" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} +apiVersion: v1 +kind: Secret +metadata: + name: authentication-provider-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +data: + config.yaml: {{ .Values.authentication.config | toYaml | b64enc }} +{{- end }} diff --git a/charts/mcp-operator/templates/secret-authz-config.yaml b/charts/mcp-operator/templates/secret-authz-config.yaml new file mode 100644 index 0000000..3df18d2 --- /dev/null +++ b/charts/mcp-operator/templates/secret-authz-config.yaml @@ -0,0 +1,11 @@ +{{- if has "authorization" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} +apiVersion: v1 +kind: Secret +metadata: + name: authorization-provider-config + namespace: {{ .Release.Namespace }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +data: + config.yaml: {{ .Values.authorization.config | toYaml | b64enc }} +{{- end }} diff --git a/charts/mcp-operator/templates/secret-webhooks.yaml b/charts/mcp-operator/templates/secret-webhooks.yaml new file mode 100644 index 0000000..4b746aa --- /dev/null +++ b/charts/mcp-operator/templates/secret-webhooks.yaml @@ -0,0 +1,9 @@ +{{- if .Values.webhooks.listen }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "mcp-operator.fullname" . }}-webhooks-tls + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +type: Opaque +{{- end }} diff --git a/charts/mcp-operator/templates/secrets-cloudorchestrator-clusters.yaml b/charts/mcp-operator/templates/secrets-cloudorchestrator-clusters.yaml new file mode 100644 index 0000000..d3c7461 --- /dev/null +++ b/charts/mcp-operator/templates/secrets-cloudorchestrator-clusters.yaml @@ -0,0 +1,18 @@ +{{- if has "cloudorchestrator" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} +{{- if .Values.cloudOrchestrator.clusters }} +{{- range $cname, $cvalues := .Values.cloudOrchestrator.clusters }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: cloudorchestrator-{{ $cname }}-cluster + namespace: {{ $.Release.Namespace }} + labels: + {{- include "mcp-operator.labels" $ | nindent 4 }} +data: + {{- range $k, $v := $cvalues }} + {{ $k }}: {{ $v | b64enc }} + {{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/mcp-operator/templates/secrets-common-clusters.yaml b/charts/mcp-operator/templates/secrets-common-clusters.yaml new file mode 100644 index 0000000..7fa232b --- /dev/null +++ b/charts/mcp-operator/templates/secrets-common-clusters.yaml @@ -0,0 +1,16 @@ +{{- if .Values.clusters }} +{{- range $cname, $cvalues := .Values.clusters }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ $cname }}-cluster + namespace: {{ $.Release.Namespace }} + labels: + {{- include "mcp-operator.labels" $ | nindent 4 }} +data: + {{- range $k, $v := $cvalues }} + {{ $k }}: {{ $v | b64enc }} + {{- end }} +{{- end }} +{{- end }} diff --git a/charts/mcp-operator/templates/secrets-landscaper-clusters.yaml b/charts/mcp-operator/templates/secrets-landscaper-clusters.yaml new file mode 100644 index 0000000..db74b22 --- /dev/null +++ b/charts/mcp-operator/templates/secrets-landscaper-clusters.yaml @@ -0,0 +1,18 @@ +{{- if has "landscaper" ( include "mcp-operator.activeControllers" .Values | fromYamlArray ) }} +{{- if .Values.landscaper.clusters }} +{{- range $cname, $cvalues := .Values.landscaper.clusters }} +--- +apiVersion: v1 +kind: Secret +metadata: + name: landscaper-{{ $cname }}-cluster + namespace: {{ $.Release.Namespace }} + labels: + {{- include "mcp-operator.labels" $ | nindent 4 }} +data: + {{- range $k, $v := $cvalues }} + {{ $k }}: {{ $v | b64enc }} + {{- end }} +{{- end }} +{{- end }} +{{- end }} diff --git a/charts/mcp-operator/templates/service-metrics.yaml b/charts/mcp-operator/templates/service-metrics.yaml new file mode 100644 index 0000000..6dd96f3 --- /dev/null +++ b/charts/mcp-operator/templates/service-metrics.yaml @@ -0,0 +1,17 @@ +{{- if .Values.metrics.service.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mcp-operator.fullname" . }}-metrics + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.metrics.service.type }} + ports: + - port: {{ .Values.metrics.service.port }} + targetPort: metrics-http + protocol: TCP + name: http + selector: + {{- include "mcp-operator.selectorLabels" . | nindent 4 }} +{{- end -}} diff --git a/charts/mcp-operator/templates/service-webhooks.yaml b/charts/mcp-operator/templates/service-webhooks.yaml new file mode 100644 index 0000000..1d330c3 --- /dev/null +++ b/charts/mcp-operator/templates/service-webhooks.yaml @@ -0,0 +1,17 @@ +{{- if .Values.webhooks.service.enabled -}} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "mcp-operator.fullname" . }}-webhooks + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} +spec: + type: {{ .Values.webhooks.service.type }} + ports: + - port: {{ .Values.webhooks.service.port }} + targetPort: webhooks-https + protocol: TCP + name: https + selector: + {{- include "mcp-operator.selectorLabels" . | nindent 4 }} +{{- end -}} diff --git a/charts/mcp-operator/templates/serviceaccount.yaml b/charts/mcp-operator/templates/serviceaccount.yaml new file mode 100644 index 0000000..a0dcd39 --- /dev/null +++ b/charts/mcp-operator/templates/serviceaccount.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: mcp-operator + namespace: {{ .Release.Namespace }} + labels: + {{- include "mcp-operator.labels" . | nindent 4 }} diff --git a/charts/mcp-operator/values.yaml b/charts/mcp-operator/values.yaml new file mode 100644 index 0000000..dba82e9 --- /dev/null +++ b/charts/mcp-operator/values.yaml @@ -0,0 +1,256 @@ +deployment: + replicaCount: 1 + minReadySeconds: 5 + maxSurge: 1 + maxUnavailable: 0 + + topologySpreadConstraints: + enabled: false + maxSkew: 1 + + leaderElection: + enabled: false + leaseNamespace: default + +image: + repository: ghcr.io/openmcp-project/github.com/openmcp-project/mcp-operator/images/mcp-operator + tag: v0.26.0 + pullPolicy: IfNotPresent + +imagePullSecrets: [] + # - name: pull-secret + +clusters: + # crate: + # # specify either kubeconfig or host, audience, and one of caData or caConfigMapName. + # kubeconfig: | + # apiVersion: v1 + # clusters: + # - cluster: ... + # host: https://api.mycluster.com + # audience: ... + # caData: ... + # caConfigMapName: ... + + + +crds: + manage: true + +webhooks: + manage: true + url: "" + listen: + port: 9443 + service: + enabled: true + port: 443 + type: ClusterIP + annotations: {} + +managedcontrolplane: + disabled: false + +apiserver: + disabled: false + worker: + maxWorkers: 10 + intervalTime: 10s + + config: + #gardener: + # project: my-project + # cloudProfile: gcp + # regions: + # - name: europe-west1 + # - name: us-east1 + # - name: us-west1 + # defaultRegion: europe-west1 + # shootTemplate: + # spec: + # networking: + # type: "calico" + # nodes: "10.180.0.0/16" + # provider: + # type: gcp + # infrastructureConfig: + # apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + # kind: InfrastructureConfig + # networks: + # workers: 10.180.0.0/16 + # controlPlaneConfig: + # apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + # kind: ControlPlaneConfig + # zone: "" + # workers: + # - name: worker-0 + # machine: + # type: n1-standard-2 + # image: + # name: gardenlinux + # version: 1312.3.0 + # architecture: amd64 + # maximum: 2 + # minimum: 1 + # volume: + # type: pd-standard + # size: 50Gi + # secretBindingName: trial-secretbinding-gcp + # kubeconfig: | + # apiVersion: v1 + # kind: Config + # ... + +landscaper: + disabled: false + clusters: + # core: + # # specify either kubeconfig or host, audience, and one of caData or caConfigMapName. + # kubeconfig: | + # apiVersion: v1 + # clusters: + # - cluster: ... + # host: https://api.mycluster.com + # audience: ... + # caData: ... + # caConfigMapName: ... + +cloudOrchestrator: + disabled: false + clusters: + # core: + # # specify either kubeconfig or host, audience, and one of caData or caConfigMapName. + # kubeconfig: | + # apiVersion: v1 + # clusters: + # - cluster: ... + # host: https://api.mycluster.com + # audience: ... + # caData: ... + # caConfigMapName: ... + +authentication: + disabled: false + config: + # systemIdentityProvider: + # name: example + # issuerURL: https://accounts.example.com + # clientID: foo + # groupsClaim: groups + # usernameClaim: email + +authorization: + disabled: false + config: + protectedNamespaces: + - prefix: "kube-" + - postfix: "-system" + admin: + namespaceScoped: + rules: + - apiGroups: + - "" + resources: + - configmaps + - secrets + verbs: + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - update + - patch + - delete + - impersonate + - apiGroups: + - rbac.authorization.k8s.io + resources: + - roles + - rolebindings + verbs: + - create + - update + - patch + - delete + - apiGroups: + - "" + resources: + - serviceaccounts/token + verbs: + - create + + clusterScoped: + rules: + - apiGroups: + - "" + resources: + - namespaces + verbs: + - create + + view: + namespaceScoped: + rules: + - apiGroups: [ "" ] + resources: + - configmaps + - secrets + - serviceaccounts + verbs: + - get + - list + - watch + + clusterScoped: + rules: + - apiGroups: [ "" ] + resources: + - namespaces + verbs: + - get + - list + - watch + - apiGroups: + - rbac.authorization.k8s.io + resources: + - "*" + verbs: + - get + - list + - watch + - apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - get + - list + - watch + +resources: + requests: + cpu: 100m + memory: 256Mi +# limits: +# cpu: 500m +# memory: 2Gi + +# logging: +# verbosity: info # error, info, or debug + +metrics: + listen: + port: 8080 + service: + enabled: false + port: 8080 + type: ClusterIP + annotations: {} + +podAnnotations: {} diff --git a/cmd/mcp-operator/app/app.go b/cmd/mcp-operator/app/app.go new file mode 100644 index 0000000..d10240a --- /dev/null +++ b/cmd/mcp-operator/app/app.go @@ -0,0 +1,327 @@ +package app + +import ( + "context" + "fmt" + "os" + + "github.com/openmcp-project/mcp-operator/internal/components" + "github.com/openmcp-project/mcp-operator/internal/releasechannel" + "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" + + apiservercontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver" + authenticationcontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication" + authorizationcontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization" + clusteradmincontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/clusteradmin" + cloudorchestratorcontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/cloudorchestrator" + landscapercontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper" + mcpcontroller "github.com/openmcp-project/mcp-operator/internal/controller/core/managedcontrolplane" + + "sigs.k8s.io/controller-runtime/pkg/cluster" + + laasinstall "github.com/gardener/landscaper-service/pkg/apis/core/install" + cocorev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" + "github.com/openmcp-project/controller-utils/pkg/init/webhooks" + "github.com/openmcp-project/controller-utils/pkg/logging" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/utils/ptr" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + _ "k8s.io/client-go/plugin/pkg/client/auth" + + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrlcfg "sigs.k8s.io/controller-runtime/pkg/config" + "sigs.k8s.io/controller-runtime/pkg/healthz" + "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + crdinstall "github.com/openmcp-project/mcp-operator/api/crds" + openmcpinstall "github.com/openmcp-project/mcp-operator/api/install" +) + +func NewMCPOperatorCommand(ctx context.Context) *cobra.Command { + options := NewOptions() + + cmd := &cobra.Command{ + Use: "mcpo", + Short: "mcpo handles ManagedControlPlanes.", + + Run: func(cmd *cobra.Command, args []string) { + if err := options.Complete(); err != nil { + fmt.Print(err) + os.Exit(1) + } + ctx = logging.NewContext(ctx, options.Log) + if options.Init { + if err := options.runInit(ctx); err != nil { + options.Log.Error(err, "unable to run mcpo init") + os.Exit(1) + } + } else { + if err := options.run(ctx); err != nil { + options.Log.Error(err, "unable to run mcpo controller") + os.Exit(1) + } + } + }, + } + + options.AddFlags(cmd.Flags()) + + return cmd +} + +func (o *Options) runInit(ctx context.Context) error { + log := o.Log + setupLog := log.WithName("setup") + + if o.DryRun { + setupLog.Info("Exiting now because this is a dry run") + return nil + } + + setupLog.Info("Initializing mcpo") + + var err error + + sc := runtime.NewScheme() + openmcpinstall.Install(sc) + utilruntime.Must(clientgoscheme.AddToScheme(sc)) + + hostClient, err := client.New(o.HostClusterConfig, client.Options{Scheme: components.Registry.Scheme()}) + if err != nil { + return fmt.Errorf("error building host client: %w", err) + } + crateClient, err := client.New(o.CrateClusterConfig, client.Options{Scheme: components.Registry.Scheme()}) + if err != nil { + return fmt.Errorf("error building crate client: %w", err) + } + + if o.CRDFlags.Install { + if err != nil { + return fmt.Errorf("error building setup client: %w", err) + } + setupLog.Info("CRD installation configured, deploying CRDs ...") + crds := crdinstall.CRDs() + for _, crd := range crds { + setupLog.Info("Deploying CRD", "name", crd.Name) + desired := crd.DeepCopy() + if _, err := ctrl.CreateOrUpdate(ctx, crateClient, crd, func() error { + crd.Spec = desired.Spec + return nil + }); err != nil { + return fmt.Errorf("error trying to apply CRD '%s' into cluster: %w", crd.Name, err) + } + } + } + + // install WebHooks if configured + if o.WebhooksFlags.Install { + // Generate webhook certificate + if err := webhooks.GenerateCertificate(ctx, hostClient, o.WebhooksFlags.CertOptions...); err != nil { + return fmt.Errorf("error generating webhook certificate: %w", err) + } + + installOptions := o.WebhooksFlags.InstallOptions + installOptions = append(installOptions, webhooks.WithRemoteClient{Client: crateClient}) + + // Install webhooks + err = webhooks.Install( + ctx, + hostClient, + sc, + []client.Object{ + &openmcpv1alpha1.ManagedControlPlane{}, + }, + installOptions..., + ) + if err != nil { + return fmt.Errorf("error installing webhooks: %w", err) + } + } + + return nil +} + +func (o *Options) run(ctx context.Context) error { + log := o.Log + // ctx = logging.NewContext(ctx, log) + setupLog := log.WithName("setup") + + if o.DryRun { + setupLog.Info("Exiting now because this is a dry run") + return nil + } + if len(o.ActiveControllers) == 0 { + setupLog.Info("All controllers deactivated, nothing to do") + return nil + } + + setupLog.Info("Starting controllers") + sc := runtime.NewScheme() + openmcpinstall.Install(sc) + utilruntime.Must(clientgoscheme.AddToScheme(sc)) + mgr, err := ctrl.NewManager(o.CrateClusterConfig, ctrl.Options{ + Scheme: sc, + Metrics: server.Options{ + BindAddress: o.MetricsAddr, + }, + Controller: ctrlcfg.Controller{ + RecoverPanic: ptr.To(true), + }, + HealthProbeBindAddress: o.ProbeAddr, + LeaderElection: o.EnableLeaderElection, + LeaderElectionID: "mcpo.openmcp.cloud", + LeaderElectionNamespace: o.LeaseNamespace, + // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily + // when the Manager ends. This requires the binary to immediately end when the + // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly + // speeds up voluntary leader transitions as the new leader don't have to wait + // LeaseDuration time first. + // + // In the default scaffold provided, the program ends immediately after + // the manager stops, so would be fine to enable this option. However, + // if you are doing or is intended to do any operation such as perform cleanups + // after the manager stops then its usage might be unsafe. + LeaderElectionReleaseOnCancel: true, + }) + if err != nil { + return fmt.Errorf("unable to start manager: %w", err) + } + + apiServerWorkerOptions := &apiserver.Options{ + MaxWorkers: ptr.To(o.APIServerWorkerCount), + Interval: ptr.To(o.APIServerWorkerInterval), + } + apiServerWorker, err := apiserver.NewWorker(mgr.GetClient(), apiServerWorkerOptions) + if err != nil { + return fmt.Errorf("unable to create APIServer worker: %w", err) + } + + // run WebHooks if configured + if o.WebhooksFlags.Install { + if err := (&openmcpv1alpha1.ManagedControlPlane{}).SetupWebhookWithManager(mgr); err != nil { + return fmt.Errorf("failed to setup webhook: %w", err) + } + } + + if o.ActiveControllers.Has(ControllerIDManagedControlPlane) { + // ManagedControlPlane controller + cpc := mcpcontroller.NewManagedControlPlaneController(mgr.GetClient()) + if err := cpc.SetupWithManager(mgr); err != nil { + return fmt.Errorf("error adding controller '%s' to manager: %w", mcpcontroller.ControllerName, err) + } + } + + if o.ActiveControllers.Has(ControllerIDAPIServer) { + // APIServer controller + apiServerProvider, err := apiservercontroller.NewAPIServerProvider(ctx, mgr.GetClient(), o.APIServerConfig) + if err != nil { + return fmt.Errorf("error creating %s: %w", apiservercontroller.ControllerName, err) + } + if err := apiServerProvider.SetupWithManager(mgr); err != nil { + return fmt.Errorf("error adding controller '%s' to manager: %w", apiservercontroller.ControllerName, err) + } + } + + if o.ActiveControllers.Has(ControllerIDLandscaper) { + // Landscaper controller + // build laas scheme + laasScheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(laasScheme)) + laasinstall.Install(laasScheme) + // build laas cluster client + laasClient, err := client.New(o.LaaSClusterConfig, client.Options{ + Scheme: laasScheme, + }) + if err != nil { + return fmt.Errorf("error creating LaaS cluster client: %w", err) + } + // add controller + if err := landscapercontroller.NewLandscaperConnector(mgr.GetClient(), laasClient).SetupWithManager(mgr); err != nil { + return fmt.Errorf("error adding controller '%s' to manager: %w", landscapercontroller.ControllerName, err) + } + } + + if o.ActiveControllers.Has(ControllerIDCloudOrchestrator) { + // CloudOrchestrator controller + // build cloudOrchestrator cluster client + coScheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(coScheme)) + utilruntime.Must(cocorev1beta1.AddToScheme(coScheme)) + cloudOrchestratorClient, err := client.New(o.CloudOrchestratorClusterConfig, client.Options{ + Scheme: coScheme, + }) + if err != nil { + return fmt.Errorf("error creating cloudOrchestrator cluster client: %w", err) + } + coreCluster, err := cluster.New(o.CloudOrchestratorClusterConfig, func(o *cluster.Options) { + o.Scheme = coScheme + }) + if err != nil { + return fmt.Errorf("error creating core cluster: %w", err) + } + if err := mgr.Add(coreCluster); err != nil { + return fmt.Errorf("error adding core cluster to manager: %w", err) + } + // add controller + if err := cloudorchestratorcontroller.NewCloudOrchestratorController(mgr.GetClient(), cloudOrchestratorClient, coreCluster).SetupWithManager(mgr); err != nil { + return fmt.Errorf("error adding controller '%s' to manager: %w", cloudorchestratorcontroller.ControllerName, err) + } + + // Add releasechannel sync runnable + runnable := releasechannel.NewReleasechannelRunnable(mgr.GetClient(), cloudOrchestratorClient) + if err := mgr.Add(&runnable); err != nil { + return fmt.Errorf("unable to add releasechannel sync runnable: %w", err) + } + } + + if o.ActiveControllers.Has(ControllerIDAuthentication) { + // Authentication controller + // add controller + if err := authenticationcontroller.NewAuthenticationReconciler(mgr.GetClient(), o.AuthConfig).SetupWithManager(mgr); err != nil { + return fmt.Errorf("error adding controller '%s' to manager: %w", authenticationcontroller.ControllerName, err) + } + } + + if o.ActiveControllers.Has(ControllerIDAuthorization) { + // Authorization controller + // add controller + if err := authorizationcontroller.NewAuthorizationReconciler(mgr.GetClient(), o.AuthzConfig).RegisterTasks(apiServerWorker).SetupWithManager(mgr); err != nil { + return fmt.Errorf("error adding controller '%s' to manager: %w", authorizationcontroller.ControllerName, err) + } + + if err := clusteradmincontroller.NewClusterAdminReconciler(mgr.GetClient(), o.AuthzConfig).SetupWithManager(mgr); err != nil { + return fmt.Errorf("error adding controller '%s' to manager: %w", clusteradmincontroller.ControllerName, err) + } + } + + if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { + return fmt.Errorf("unable to set up health check: %w", err) + } + if err := mgr.AddReadyzCheck("readyz", healthz.Ping); err != nil { + return fmt.Errorf("unable to set up ready check: %w", err) + } + + signalHandler := ctrl.SetupSignalHandler() + signalHandler = logging.NewContext(signalHandler, log.WithName("apiServerWorker")) + + setupLog.Info("Starting APIServer worker") + if err := apiServerWorker.Start(signalHandler, nil, nil, mgr.Elected()); err != nil { + return fmt.Errorf("problem running APIServer worker: %w", err) + } + + setupLog.Info("Starting manager") + + if err = mgr.Start(signalHandler); err != nil { + return fmt.Errorf("problem running manager: %w", err) + } + + return nil +} diff --git a/cmd/mcp-operator/app/options.go b/cmd/mcp-operator/app/options.go new file mode 100644 index 0000000..5683f41 --- /dev/null +++ b/cmd/mcp-operator/app/options.go @@ -0,0 +1,324 @@ +package app + +import ( + goflag "flag" + "fmt" + "strings" + "time" + + "github.com/openmcp-project/mcp-operator/internal/components" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/config" + configauthn "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication/config" + configauthz "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/config" + + colactrlutil "github.com/openmcp-project/controller-utils/pkg/controller" + "github.com/openmcp-project/controller-utils/pkg/init/crds" + "github.com/openmcp-project/controller-utils/pkg/init/webhooks" + "github.com/openmcp-project/controller-utils/pkg/logging" + flag "github.com/spf13/pflag" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/rest" + ctrlrun "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/yaml" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +const ( + ControllerIDManagedControlPlane = "managedcontrolplane" +) + +var ( + ControllerIDAPIServer = strings.ToLower(string(openmcpv1alpha1.APIServerComponent)) + ControllerIDLandscaper = strings.ToLower(string(openmcpv1alpha1.LandscaperComponent)) + ControllerIDCloudOrchestrator = strings.ToLower(string(openmcpv1alpha1.CloudOrchestratorComponent)) + ControllerIDAuthentication = strings.ToLower(string(openmcpv1alpha1.AuthenticationComponent)) + ControllerIDAuthorization = strings.ToLower(string(openmcpv1alpha1.AuthorizationComponent)) +) + +// rawOptions contains the options specified directly via the command line. +// The Options struct then contains these as embedded struct and additionally some options that were derived from the raw options (e.g. by loading files or interpreting raw options). +type rawOptions struct { + // init + Init bool `json:"init"` + + // controller-runtime stuff + MetricsAddr string `json:"metricsAddress"` + EnableLeaderElection bool `json:"enableLeaderElection"` + LeaseNamespace string `json:"leaseNamespace"` + ProbeAddr string `json:"healthProbeAddress"` + + // raw options that need to be evaluated + APIServerConfigPath string `json:"apiServerConfigPath"` + LaaSClusterPath string `json:"laasClusterConfigPath"` + CrateClusterPath string `json:"crateClusterConfigPath"` + CloudOrchestratorClusterPath string `json:"cloudOrchestratorClusterConfigPath"` + AuthConfigPath string `json:"authConfigPath"` + AuthzConfigPath string `json:"authzConfigPath"` + ControllerList string `json:"controllers"` + + APIServerWorkerCount int `json:"apiServerWorkerCount"` + APIServerWorkerInterval time.Duration `json:"apiServerWorkerInterval"` + + // raw options that are final + DryRun bool `json:"dryRun"` +} + +func writeHeader(sb *strings.Builder, includeHeader bool, header string) { + if includeHeader { + sb.WriteString("########## ") + sb.WriteString(header) + sb.WriteString(" ##########\n") + } +} + +func (ro *rawOptions) String(includeHeader bool) (string, error) { + sb := strings.Builder{} + writeHeader(&sb, includeHeader, "RAW OPTIONS") + printableRawOptions, err := yaml.Marshal(ro) + if err != nil { + return "", fmt.Errorf("unable to marshal raw options to yaml: %w", err) + } + sb.WriteString(string(printableRawOptions)) + writeHeader(&sb, includeHeader, "END RAW OPTIONS") + return sb.String(), nil +} + +// Options describes the options to configure the Landscaper controller. +type Options struct { + rawOptions + + // completed options from raw options + APIServerConfig *config.APIServerProviderConfiguration + Log logging.Logger + LaaSClusterConfig *rest.Config + CrateClusterConfig *rest.Config + CloudOrchestratorClusterConfig *rest.Config + HostClusterConfig *rest.Config + AuthConfig *configauthn.AuthenticationConfig + AuthzConfig *configauthz.AuthorizationConfig + ActiveControllers sets.Set[string] + WebhooksFlags *webhooks.Flags + CRDFlags *crds.Flags +} + +func NewOptions() *Options { + return &Options{} +} + +func (o *Options) String(includeHeader bool, includeRawOptions bool) (string, error) { + sb := strings.Builder{} + writeHeader(&sb, includeHeader, "OPTIONS") + + if includeRawOptions { + rawOpts, err := o.rawOptions.String(false) + if err != nil { + return "", err + } + sb.WriteString(rawOpts) + } + + opts := map[string]any{} + // API server config + opts["apiServerConfig"] = o.APIServerConfig + + // clusters + opts["crateClusterHost"] = nil + if o.CrateClusterConfig != nil { + opts["crateClusterHost"] = o.CrateClusterConfig.Host + } + opts["laasClusterHost"] = nil + if o.LaaSClusterConfig != nil { + opts["laasClusterHost"] = o.LaaSClusterConfig.Host + } + opts["cloudOrchestratorClusterHost"] = nil + if o.CloudOrchestratorClusterConfig != nil { + opts["cloudOrchestratorClusterHost"] = o.CloudOrchestratorClusterConfig.Host + } + + hostClusterConfig, err := ctrlrun.GetConfig() + if err != nil { + return "", fmt.Errorf("error getting host cluster config: %w", err) + } + o.HostClusterConfig = hostClusterConfig + + opts["authConfig"] = o.AuthConfig + opts["authzConfig"] = o.AuthzConfig + + // controllers + opts["activeControllers"] = sets.List(o.ActiveControllers) + + // convert to yaml + optsString, err := yaml.Marshal(opts) + if err != nil { + return "", fmt.Errorf("error converting options map to yaml: %w", err) + } + sb.WriteString(string(optsString)) + + webhooksString, err := yaml.Marshal(o.WebhooksFlags) + if err != nil { + return "", fmt.Errorf("error converting webhooks flags to yaml: %w", err) + } + + writeHeader(&sb, includeHeader, "WEBHOOKS") + sb.WriteString(string(webhooksString)) + + crdsString, err := yaml.Marshal(o.CRDFlags) + if err != nil { + return "", fmt.Errorf("error converting CRD flags to yaml: %w", err) + } + + writeHeader(&sb, includeHeader, "CRDs") + sb.WriteString(string(crdsString)) + + writeHeader(&sb, includeHeader, "END OPTIONS") + + return sb.String(), nil +} + +func (o *Options) AddFlags(fs *flag.FlagSet) { + // decide if init process or main controller + fs.BoolVar(&o.Init, "init", false, "If true, the init process is started, which creates the necessary CRDs and webhooks configuration.") + + // standard stuff + fs.StringVar(&o.MetricsAddr, "metrics-bind-address", ":8080", "The address the metrics endpoint binds to.") + fs.StringVar(&o.ProbeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") + fs.BoolVar(&o.EnableLeaderElection, "leader-elect", false, "Enable leader election for controller manager. Enabling this will ensure there is only one active controller manager.") + fs.StringVar(&o.LeaseNamespace, "lease-namespace", "default", "Namespace in which the controller manager's leader election lease will be created.") + + // APIServer + fs.StringVar(&o.APIServerConfigPath, "apiserver-config", "", "Path to the APIServer provider config file.") + fs.IntVar(&o.APIServerWorkerCount, "apiserver-workers", 10, "Number of max workers in the APIServer worker pool.") + fs.DurationVar(&o.APIServerWorkerInterval, "apiserver-worker-interval", time.Second*1, "Interval at which the APIServer worker runs the tasks.") + + // landscaper + fs.StringVar(&o.LaaSClusterPath, "laas-cluster", "", "Path to the LaaS core cluster kubeconfig file or directory containing either a kubeconfig or host, token, and ca file. Leave empty to use in-cluster config.") + + // cloudorchestrator + fs.StringVar(&o.CloudOrchestratorClusterPath, "co-cluster", "", "Path to the CloudOrchestrator core cluster kubeconfig file or directory containing either a kubeconfig or host, token, and ca file. Leave empty to use in-cluster config.") + + // authentication + fs.StringVar(&o.AuthConfigPath, "auth-config", "", "Path to the authentication config file.") + + // authorization + fs.StringVar(&o.AuthzConfigPath, "authz-config", "", "Path to the authorization config file.") + + // common + fs.BoolVar(&o.DryRun, "dry-run", false, "If true, the CLI args are evaluated as usual, but the program exits before the controllers are started.") + fs.StringVar(&o.CrateClusterPath, "crate-cluster", "", "Path to the crate cluster kubeconfig file or directory containing either a kubeconfig or host, token, and ca file. Leave empty to use in-cluster config.") + fs.StringVar(&o.ControllerList, "controllers", strings.Join([]string{ControllerIDManagedControlPlane, ControllerIDAPIServer, ControllerIDLandscaper, ControllerIDCloudOrchestrator}, ","), "Comma-separated list of controllers that should be active.") + logging.InitFlags(fs) + // because webhooks.BindFlags only supports Go's native flag package, we have to convert + fsHelper := &goflag.FlagSet{} + o.WebhooksFlags = webhooks.BindFlags(fsHelper) + o.CRDFlags = crds.BindFlags(fsHelper) + fs.AddGoFlagSet(fsHelper) +} + +// Complete parses all Options and flags and initializes the basic functions +func (o *Options) Complete() error { + // build logger + log, err := logging.GetLogger() + if err != nil { + return err + } + o.Log = log + ctrlrun.SetLogger(o.Log.Logr()) + olog := log.WithName("options") + + // print raw options + rawOptsString, err := o.rawOptions.String(true) + if err != nil { + olog.Error(err, "error computing raw options string for printing") + } else { + fmt.Print(rawOptsString) + } + + // determine active controllers + o.ActiveControllers = sets.New(strings.Split(o.ControllerList, ",")...) + // remove empty string, if part of active controllers + delete(o.ActiveControllers, "") + // unregister components for inactive controllers + for ct := range components.Registry.GetKnownComponents() { + if !o.ActiveControllers.Has(strings.ToLower(string(ct))) { + // controller for component is not active, remove registration + components.Registry.Register(ct, nil) + } + } + + // load kubeconfigs + o.CrateClusterConfig, err = colactrlutil.LoadKubeconfig(o.CrateClusterPath) + if err != nil { + return fmt.Errorf("unable to load crate cluster kubeconfig: %w", err) + } + if o.ActiveControllers.Has(ControllerIDLandscaper) { + o.LaaSClusterConfig, err = colactrlutil.LoadKubeconfig(o.LaaSClusterPath) + if err != nil { + return fmt.Errorf("unable to load laas cluster kubeconfig: %w", err) + } + } + if o.ActiveControllers.Has(ControllerIDCloudOrchestrator) { + o.CloudOrchestratorClusterConfig, err = colactrlutil.LoadKubeconfig(o.CloudOrchestratorClusterPath) + if err != nil { + return fmt.Errorf("unable to load core cluster kubeconfig: %w", err) + } + } + + // load APIServer provider config + if o.ActiveControllers.Has(ControllerIDAPIServer) { + if o.APIServerConfigPath == "" { + return fmt.Errorf("no (or empty) path to API server config file given, please specify --apiserver-config argument") + } + o.APIServerConfig, err = config.LoadConfig(o.APIServerConfigPath) + if err != nil { + return err + } + err = config.Validate(o.APIServerConfig) + if err != nil { + return fmt.Errorf("invalid config: %w", err) + } + } + + // load authentication config + if o.ActiveControllers.Has(ControllerIDAuthentication) { + if o.AuthConfigPath == "" { + return fmt.Errorf("no (or empty) path to authentication config file given, please specify --auth-config argument") + } + o.AuthConfig, err = configauthn.LoadConfig(o.AuthConfigPath) + if err != nil { + return err + } + + err = configauthn.Validate(o.AuthConfig) + if err != nil { + return fmt.Errorf("invalid authentication config: %w", err) + } + } + + // load authorization config + if o.ActiveControllers.Has(ControllerIDAuthorization) { + if o.AuthzConfigPath == "" { + return fmt.Errorf("no (or empty) path to authorization config file given, please specify --authz-config argument") + } + o.AuthzConfig, err = configauthz.LoadConfig(o.AuthzConfigPath) + if err != nil { + return err + } + + err = configauthz.Validate(o.AuthzConfig) + if err != nil { + return fmt.Errorf("invalid authorization config: %w", err) + } + } + + // print options + optsString, err := o.String(true, false) + if err != nil { + olog.Error(err, "error computing options string for printing") + } else { + fmt.Print(optsString) + } + + return nil +} diff --git a/cmd/mcp-operator/main.go b/cmd/mcp-operator/main.go new file mode 100644 index 0000000..b022f83 --- /dev/null +++ b/cmd/mcp-operator/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/openmcp-project/mcp-operator/cmd/mcp-operator/app" +) + +func main() { + ctx := context.Background() + defer ctx.Done() + cmd := app.NewMCPOperatorCommand(ctx) + + if err := cmd.Execute(); err != nil { + fmt.Print(err) + os.Exit(1) + } +} diff --git a/components/components.yaml b/components/components.yaml new file mode 100644 index 0000000..3048aa4 --- /dev/null +++ b/components/components.yaml @@ -0,0 +1,109 @@ +# Usage: +# +# Feed arguments into ocm CLI like this: +# ocm add componentversions ... -- CHART_REGISTRY=... IMG_REGISTRY=... +# +# Required values: +# - VERSION (set via the ocm CLI's --version flag) +# - Used as version for the source element pointing to this repo. +# - Used as referenced GitHub release, if it does not contain a '-dev'. +# - Used as fallback value for other versions. +# - COMMIT +# - Commit hash of the git commit used to generate this component descriptor. +# - Used for the source element pointing to this repo. +# - CHART_REGISTRY +# - URL of the OCI registry used for the helm charts. +# - IMG_REGISTRY +# - URL of the OCI registry used for the container images. +# - COMPONENTS +# - Comma-separated list of components for which resources should be added to the component descriptor, e.g. "apiserver-controller,managedcontrolplane-controller,landscaper-connector". +# - Not required if all of BP_COMPONENTS, CHART_COMPONENTS, and IMG_COMPONENTS are specified instead. +# +# Optional values: +# - CD_VERSION +# - Version used for the component descriptor. +# - Defaults to VERSION if not specified. +# - CHART_VERSION +# - Default version for referenced helm charts. +# - Defaults to VERSION if not specified. +# - IMG_VERSION +# - Default version for referenced container images. +# - Defaults to VERSION if not specified. +# - BP_COMPONENTS +# - Comma-separated list of components for which the blueprint should be added to the component descriptor, e.g. "apiserver-controller,managedcontrolplane-controller,landscaper-connector" +# - Each element will result in a resource entry of type 'landscaper.gardener.cloud/blueprint' named '-blueprint'. The corresponding blueprint is expected at '../blueprints/' (relative to this file). +# - Defaults to COMPONENTS if not specified. +# - CHART_COMPONENTS +# - Comma-separated list of components for which helm charts should be referenced in the component descriptor, optionally with version (separated by ":"). +# - Example: "apiserver-controller:v0.1.0,managedcontrolplane-controller:v0.2.0,landscaper-connector" +# - Each element will result in a resource entry of type 'helmChart' named '-chart'. The chart is expected in the OCI registry at '/:'. +# - Defaults to COMPONENTS if not specified. +# - Each chart's version defaults to CHART_VERSION if not specified. +# - IMG_COMPONENTS +# - Comma-separated list of components for which container images should be referenced in the component descriptor, optionally with version (separated by ":"). +# - Example: "apiserver-controller:v0.1.0,managedcontrolplane-controller:v0.2.0,landscaper-connector" +# - Each element will result in a resource entry of type 'ociImage' named '-image'. The image is expected in the OCI registry at '/:'. +# - Defaults to COMPONENTS if not specified. +# - Each image's version defaults to IMG_VERSION if not specified. + + +name: github.com/openmcp-project/mcp-operator +version: (( defaults.CD_VERSION )) +provider: + name: openmcp-project + +sources: +- name: mcp-operator + type: blob + version: (( values.VERSION )) + access: + type: gitHub + repoUrl: https://github.com/openmcp-project/mcp-operator + commit: (( values.COMMIT )) + ref: (( contains(values.VERSION, "-dev") ? ~~ :"refs/tags/" values.VERSION )) +resources: +- <<<: (( sum[split(",", defaults.BP_COMPONENTS)|[]|s,comp|-> s *templates.blueprint] )) +- <<<: (( sum[split(",", defaults.CHART_COMPONENTS)|[]|s,cv|-> ("cvs" = split(":", cv)) ("comp" = cvs[0], "chart_version" = (cvs[1] || defaults.CHART_VERSION)) s *templates.chart] )) +- <<<: (( sum[split(",", defaults.IMG_COMPONENTS)|[]|s,cv|-> ("cvs" = split(":", cv)) ("comp" = cvs[0], "img_version" = (cvs[1] || defaults.IMG_VERSION)) s *templates.image] )) + + +########################################################################## +# Everything below this is temporary stuff only required during rendering and will not be part of the generated component descriptor. + +defaults: + <<<: (( &temporary )) + CD_VERSION: (( values.CD_VERSION || values.VERSION )) + CHART_VERSION: (( values.CHART_VERSION || values.VERSION )) + IMG_VERSION: (( values.IMG_VERSION || values.VERSION )) + BP_COMPONENTS: (( values.BP_COMPONENTS || values.COMPONENTS )) + CHART_COMPONENTS: (( values.CHART_COMPONENTS || values.COMPONENTS )) + IMG_COMPONENTS: (( values.IMG_COMPONENTS || values.COMPONENTS )) + +templates: + <<<: (( &temporary )) + blueprint: + <<<: (( &template )) + name: (( comp "-blueprint" )) + type: landscaper.gardener.cloud/blueprint + input: + path: (( "../blueprints/" comp )) + type: dir + chart: + <<<: (( &template )) + name: (( comp "-chart" )) + type: helmChart + version: (( chart_version )) + access: + type: ociArtifact + imageReference: (( values.CHART_REGISTRY "/" comp ":" chart_version )) + image: + <<<: (( &template )) + name: (( comp "-image" )) + type: ociImage + version: (( img_version )) + access: + imageReference: (( values.IMG_REGISTRY "/" comp ":" img_version )) + type: ociArtifact + + + diff --git a/config/crd/bases/core.openmcp.cloud_apiservers.yaml b/config/crd/bases/core.openmcp.cloud_apiservers.yaml new file mode 100644 index 0000000..3f8968a --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_apiservers.yaml @@ -0,0 +1,358 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: apiservers.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: APIServer + listKind: APIServerList + plural: apiservers + shortNames: + - as + singular: apiserver + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="APIServerReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: APIServer is the Schema for the APIServer API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: APIServerSpec contains the APIServer configuration and potentially + other fields which should not be exposed to the customer. + properties: + desiredRegion: + description: |- + DesiredRegion is part of the common configuration. + If specified, it will be used to determine the region for the created cluster. + properties: + direction: + description: Direction is the direction within the region. + enum: + - north + - east + - south + - west + - central + type: string + name: + description: Name is the name of the region. + enum: + - northamerica + - southamerica + - europe + - asia + - africa + - australia + type: string + type: object + gardener: + description: |- + GardenerConfig contains configuration for a Gardener APIServer. + Must be set if type is 'Gardener', is ignored otherwise. + properties: + auditLog: + description: AuditLogConfig defines the AuditLog configuration + for the ManagedControlPlane cluster. + properties: + policyRef: + description: PolicyRef is the reference to the policy containing + the configuration for the audit log service. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + secretRef: + description: SecretRef is the reference to the secret containing + the credentials for the audit log service. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + serviceURL: + description: ServiceURL is the URL from the Service Keys. + type: string + tenantID: + description: TenantID is the tenant ID of the BTP Subaccount. + Can be seen in the BTP Cockpit dashboard. + type: string + type: + description: Type is the type of the audit log. + enum: + - standard + type: string + required: + - policyRef + - secretRef + - serviceURL + - tenantID + - type + type: object + encryptionConfig: + description: EncryptionConfig contains customizable encryption + configuration of the API server. + properties: + resources: + description: |- + Resources contains the list of resources that shall be encrypted in addition to secrets. + Each item is a Kubernetes resource name in plural (resource or resource.group) that should be encrypted. + Example: ["configmaps", "statefulsets.apps", "flunders.emxample.com"] + items: + type: string + type: array + type: object + highAvailability: + description: HighAvailabilityConfig specifies the HA configuration + for the API server. + properties: + failureToleranceType: + description: |- + FailureToleranceType specifies failure tolerance mode for the API server. + Allowed values are: node, zone + node: The API server is tolerant to node failures within a single zone. + zone: The API server is tolerant to zone failures. + enum: + - node + - zone + type: string + x-kubernetes-validations: + - message: failureToleranceType is immutable + rule: self == oldSelf + required: + - failureToleranceType + type: object + x-kubernetes-validations: + - message: highAvailability is immutable + rule: self == oldSelf + region: + description: |- + Region is the region to be used for the Shoot cluster. + This is usually derived from the ManagedControlPlane's common configuration, but can be overwritten here. + type: string + x-kubernetes-validations: + - message: region is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: highAvailability is required once set + rule: has(self.highAvailability) == has(oldSelf.highAvailability) + || has(self.highAvailability) + internal: + description: |- + Internal contains the parts of the configuration which are not exposed to the customer. + It would be nice to have this as an inline field, but since both APIServerConfiguration and APIServerInternalConfiguration + contain a field 'gardener', this would clash. + properties: + gardener: + description: GardenerConfig contains internal configuration for + a Gardener APIServer. + properties: + k8sVersionOverwrite: + description: |- + K8SVersionOverwrite is the k8s version for the Shoot cluster. + Will be defaulted if not specified. + type: string + landscapeConfiguration: + description: |- + LandscapeConfiguration is the name of the landscape and the name of the configuration to use. + The expected format is "/". + pattern: ^[a-z0-9-]+/[a-z0-9-]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + shootOverwrite: + description: ShootOverwrite allows to overwrite the shoot + to be used. This could be useful for migration tasks. + properties: + name: + description: Name is the object's name. + type: string + namespace: + description: Namespace is the object's namespace. + type: string + required: + - name + - namespace + type: object + type: object + type: object + type: + default: GardenerDedicated + description: |- + Type is the type of APIServer. This determines which other configuration fields need to be specified. + Valid values are: + - Gardener + - GardenerDedicated + enum: + - Gardener + - GardenerDedicated + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf + required: + - type + type: object + status: + description: APIServerStatus contains the APIServer status and potentially + other fields which should not be exposed to the customer. + properties: + adminAccess: + description: AdminAccess is an admin kubeconfig for accessing the + API server. + properties: + creationTimestamp: + description: CreationTimestamp is the time when this access was + created. + format: date-time + type: string + expirationTimestamp: + description: ExpirationTimestamp is the time until the access + loses its validity. + format: date-time + type: string + kubeconfig: + description: Kubeconfig is the kubeconfig for accessing the APIServer + cluster. + type: string + type: object + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + endpoint: + description: Endpoint represents the Kubernetes API server endpoint + type: string + gardener: + description: GardenerStatus contains status if the type is 'Gardener'. + properties: + shoot: + description: Shoot contains the shoot manifest generated by the + controller. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + serviceAccountIssuer: + description: ServiceAccountIssuer represents the OpenIDConnect issuer + URL that can be used to verify service account tokens. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openmcp.cloud_authentications.yaml b/config/crd/bases/core.openmcp.cloud_authentications.yaml new file mode 100644 index 0000000..9d5c917 --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_authentications.yaml @@ -0,0 +1,250 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: authentications.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: Authentication + listKind: AuthenticationList + plural: authentications + shortNames: + - auth + singular: authentication + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="AuthenticationReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Authentication is the Schema for the authentication API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: AuthenticationSpec contains the specification for the authentication + component + properties: + enableSystemIdentityProvider: + type: boolean + identityProviders: + items: + description: IdentityProvider contains the configuration for an + OpenID Connect identity provider + properties: + caBundle: + description: |- + CABundle: When set, the OpenID server's certificate will be verified by one of the authorities in the bundle. + Otherwise, the host's root CA set will be used. + type: string + clientConfig: + description: ClientAuthentication contains configuration for + OIDC clients + properties: + clientSecret: + description: |- + ClientSecret is a references to a secret containing the client secret. + The client secret will be added to the generated kubeconfig with the "--oidc-client-secret" flag. + properties: + key: + description: Key is the key inside the secret. + type: string + name: + description: Name is the secret name. + type: string + required: + - key + - name + type: object + extraConfig: + additionalProperties: + description: SingleOrMultiStringValue is a type that can + hold either a single string value or a list of string + values. + properties: + value: + description: Value is a single string value. + type: string + values: + description: Values is a list of string values. + items: + type: string + type: array + type: object + description: |- + ExtraConfig is added to the client configuration in the kubeconfig. + Can either be a single string value, a list of string values or no value. + Must not contain any of the following keys: + - "client-id" + - "client-secret" + - "issuer-url" + type: object + type: object + clientID: + description: ClientID is the client ID of the identity provider. + type: string + groupsClaim: + description: GroupsClaim is the claim that contains the groups. + type: string + issuerURL: + description: IssuerURL is the issuer URL of the identity provider. + type: string + name: + description: |- + Name is the name of the identity provider. + The name must be unique among all identity providers. + The name must only contain lowercase letters. + The length must not exceed 63 characters. + maxLength: 63 + pattern: ^[a-z]+$ + type: string + requiredClaims: + additionalProperties: + type: string + description: RequiredClaims is a map of required claims. If + set, the identity provider must provide these claims in the + ID token. + type: object + signingAlgs: + description: SigningAlgs is the list of allowed JOSE asymmetric + signing algorithms. + items: + type: string + type: array + usernameClaim: + description: UsernameClaim is the claim that contains the username. + type: string + required: + - clientID + - issuerURL + - name + - usernameClaim + type: object + type: array + type: object + status: + description: AuthenticationStatus contains the status of the authentication + component + properties: + access: + description: |- + UserAccess reference the secret containing the kubeconfig + for the APIServer which is to be used by the customer. + properties: + key: + description: Key is the key inside the secret. + type: string + name: + description: Name is the object's name. + type: string + namespace: + description: Namespace is the object's namespace. + type: string + required: + - key + - name + - namespace + type: object + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openmcp.cloud_authorizations.yaml b/config/crd/bases/core.openmcp.cloud_authorizations.yaml new file mode 100644 index 0000000..c4e70ea --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_authorizations.yaml @@ -0,0 +1,191 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: authorizations.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: Authorization + listKind: AuthorizationList + plural: authorizations + shortNames: + - authz + singular: authorization + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="AuthorizationReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Authorization is the Schema for the authorization API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: AuthorizationSpec contains the specification for the authorization + component + properties: + roleBindings: + description: RoleBindings is a list of role bindings + items: + description: RoleBinding contains the role and the subjects assigned + to the role + properties: + role: + description: Role is the name of the role + enum: + - admin + - view + type: string + subjects: + description: Subjects is a list of subjects assigned to the + role + items: + description: |- + Subject describes an object that is assigned to a role and + which can be used to authenticate against the control plane. + properties: + apiGroup: + description: APIGroup is the API group of the subject + type: string + kind: + description: Kind is the kind of the subject + enum: + - ServiceAccount + - User + - Group + type: string + name: + description: Name is the name of the subject + minLength: 1 + type: string + namespace: + description: Namespace is the namespace of the subject + type: string + required: + - kind + - name + type: object + type: array + required: + - role + - subjects + type: object + type: array + required: + - roleBindings + type: object + status: + description: AuthorizationStatus contains the status of the authorization + component + properties: + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + userNamespaces: + description: |- + UserNamespaces is a list of namespaces that have been created by the user and + must be managed by the authorization component. + items: + type: string + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openmcp.cloud_cloudorchestrators.yaml b/config/crd/bases/core.openmcp.cloud_cloudorchestrators.yaml new file mode 100644 index 0000000..5df84da --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_cloudorchestrators.yaml @@ -0,0 +1,206 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: cloudorchestrators.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: CloudOrchestrator + listKind: CloudOrchestratorList + plural: cloudorchestrators + shortNames: + - co + singular: cloudorchestrator + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="CloudOrchestratorReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: CloudOrchestrator is the Schema for the internal CloudOrchestrator + API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: CloudOrchestratorSpec defines the desired state of CloudOrchestrator + properties: + btpServiceOperator: + description: BTPServiceOperator defines the configuration for setting + up the BTPServiceOperator component in a ManagedControlPlane. + properties: + version: + description: The Version of BTP Service Operator to install. + type: string + required: + - version + type: object + crossplane: + description: Crossplane defines the configuration for setting up the + Crossplane component in a ManagedControlPlane. + properties: + providers: + items: + properties: + name: + description: |- + Name of the provider. + Using a well-known name will automatically configure the "package" field. + type: string + version: + description: Version of the provider to install. + type: string + required: + - name + - version + type: object + type: array + version: + description: The Version of Crossplane to install. + type: string + required: + - version + type: object + externalSecretsOperator: + description: ExternalSecretsOperator defines the configuration for + setting up the ExternalSecretsOperator component in a ManagedControlPlane. + properties: + version: + description: The Version of External Secrets Operator to install. + type: string + required: + - version + type: object + flux: + description: Flux defines the configuration for setting up the Flux + component in a ManagedControlPlane. + properties: + version: + description: The Version of Flux to install. + type: string + required: + - version + type: object + kyverno: + description: Kyverno defines the configuration for setting up the + Kyverno component in a ManagedControlPlane. + properties: + version: + description: The Version of Kyverno to install. + type: string + required: + - version + type: object + type: object + status: + description: CloudOrchestratorStatus defines the observed state of CloudOrchestrator + properties: + componentsEnabled: + description: Number of enabled components. + type: integer + componentsHealthy: + description: Number of healthy components. + type: integer + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openmcp.cloud_clusteradmins.yaml b/config/crd/bases/core.openmcp.cloud_clusteradmins.yaml new file mode 100644 index 0000000..c4e1bf6 --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_clusteradmins.yaml @@ -0,0 +1,110 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: clusteradmins.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: ClusterAdmin + listKind: ClusterAdminList + plural: clusteradmins + shortNames: + - clas + singular: clusteradmin + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.active + name: Active + type: string + - jsonPath: .status.activationTime + name: Activated + type: date + - jsonPath: .status.expirationTime + name: Expiration + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ClusterAdmin is the Schema for the cluster admin API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: ClusterAdminSpec contains the specification for the cluster + admin + properties: + subjects: + items: + description: |- + Subject describes an object that is assigned to a role and + which can be used to authenticate against the control plane. + properties: + apiGroup: + description: APIGroup is the API group of the subject + type: string + kind: + description: Kind is the kind of the subject + enum: + - ServiceAccount + - User + - Group + type: string + name: + description: Name is the name of the subject + minLength: 1 + type: string + namespace: + description: Namespace is the namespace of the subject + type: string + required: + - kind + - name + type: object + type: array + required: + - subjects + type: object + status: + description: ClusterAdminStatus contains the status of the cluster admin + properties: + activationTime: + description: ActivationTime is the time when the cluster admin was + activated + format: date-time + type: string + active: + description: Active is set to true if the subjects of the cluster + admin are assigned the cluster-admin role + type: boolean + expirationTime: + description: ExpirationTime is the time when the cluster admin will + expire + format: date-time + type: string + required: + - active + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openmcp.cloud_internalconfigurations.yaml b/config/crd/bases/core.openmcp.cloud_internalconfigurations.yaml new file mode 100644 index 0000000..4ce2abb --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_internalconfigurations.yaml @@ -0,0 +1,90 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: internalconfigurations.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: InternalConfiguration + listKind: InternalConfigurationList + plural: internalconfigurations + shortNames: + - icfg + singular: internalconfiguration + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InternalConfiguration is the Schema for the InternalConfigurations + API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: InternalConfigurationSpec defines additional configuration + for a managedcontrolplane. + properties: + components: + description: InternalConfigurationComponents defines the components + that are part of the internal configuration. + properties: + apiServer: + properties: + gardener: + description: GardenerConfig contains internal configuration + for a Gardener APIServer. + properties: + k8sVersionOverwrite: + description: |- + K8SVersionOverwrite is the k8s version for the Shoot cluster. + Will be defaulted if not specified. + type: string + landscapeConfiguration: + description: |- + LandscapeConfiguration is the name of the landscape and the name of the configuration to use. + The expected format is "/". + pattern: ^[a-z0-9-]+/[a-z0-9-]+$ + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + shootOverwrite: + description: ShootOverwrite allows to overwrite the shoot + to be used. This could be useful for migration tasks. + properties: + name: + description: Name is the object's name. + type: string + namespace: + description: Namespace is the object's namespace. + type: string + required: + - name + - namespace + type: object + type: object + type: object + type: object + type: object + type: object + served: true + storage: true diff --git a/config/crd/bases/core.openmcp.cloud_landscapers.yaml b/config/crd/bases/core.openmcp.cloud_landscapers.yaml new file mode 100644 index 0000000..eed123c --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_landscapers.yaml @@ -0,0 +1,154 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: landscapers.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: Landscaper + listKind: LandscaperList + plural: landscapers + shortNames: + - ls + singular: landscaper + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=="LandscaperReconciliation")].status + name: Successfully_Reconciled + type: string + - jsonPath: .metadata.deletionTimestamp + name: Deleted + type: date + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: Landscaper is the Schema for the laasinstances API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: LandscaperSpec contains the Landscaper configuration and + potentially other fields which should not be exposed to the customer. + properties: + deployers: + description: Deployers is the list of deployers that should be installed. + items: + type: string + type: array + type: object + status: + description: LandscaperStatus contains the landscaper status and potentially + other fields which should not be exposed to the customer. + properties: + conditions: + description: |- + Conditions containts the conditions of the component. + For each component, this is expected to contain at least one condition per top-level node that component has in the ManagedControlPlane's spec. + This condition is expected to be named "Healthy" and to describe the general availability of the functionality configured by that top-level node. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - status + - type + type: object + type: array + landscaperDeployment: + description: LandscaperDeploymentInfo contains information about the + corresponding LandscaperDeployment resource. + properties: + name: + description: Name is the name of the Landscaper deployment. + type: string + namespace: + description: Namespace is the namespace of the Landscaper deployment. + type: string + required: + - name + - namespace + type: object + observedGenerations: + description: |- + ObservedGenerations contains information about the observed generations of a component. + This information is required to determine whether a component's controller has already processed some changes or not. + properties: + internalConfiguration: + description: |- + InternalConfiguration contains the last generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane that has been seen by the controller. + Note that the component's controller does not read the InternalConfiguration itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the InternalConfiguration belonging to the owning v1alpha1.ManagedControlPlane, if any. + If the resource does not have a label containing the generation of the corresponding InternalConfiguration, this means that no InternalConfiguration exists for + the owning v1alpha1.ManagedControlPlane. In that case, the value of this field is expected to be -1. + format: int64 + type: integer + managedControlPlane: + description: |- + ManagedControlPlane contains the last generation of the owning v1alpha1.ManagedControlPlane that has been by the controller. + Note that the component's controller does not read the ManagedControlPlane resource itself, but fetches this information from a label which is populated by the v1alpha1.ManagedControlPlane controller. + This refers to metadata.generation of the owning v1alpha1.ManagedControlPlane resource. + This value is probably identical to the one in 'Resource', unless something else than the v1alpha1.ManagedControlPlane controller touched the spec of this resource. + format: int64 + type: integer + resource: + description: |- + Resource contains the last generation of this resource that has been handled by the controller. + This refers to metadata.generation of this resource. + format: int64 + type: integer + required: + - internalConfiguration + - managedControlPlane + - resource + type: object + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openmcp.cloud_managedcomponents.yaml b/config/crd/bases/core.openmcp.cloud_managedcomponents.yaml new file mode 100644 index 0000000..d8c4edb --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_managedcomponents.yaml @@ -0,0 +1,63 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: managedcomponents.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: ManagedComponent + listKind: ManagedComponentList + plural: managedcomponents + singular: managedcomponent + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .spec.name + name: Name + type: string + - jsonPath: .status.versions + name: Versions + type: string + name: v1alpha1 + schema: + openAPIV3Schema: + description: ManagedComponent is the Schema for the managedcomponents API. + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: ManagedComponentSpec defines the desired state of ManagedComponent. + type: object + status: + description: ManagedComponentStatus defines the observed state of ManagedComponent. + properties: + versions: + items: + type: string + type: array + required: + - versions + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/core.openmcp.cloud_managedcontrolplanes.yaml b/config/crd/bases/core.openmcp.cloud_managedcontrolplanes.yaml new file mode 100644 index 0000000..8c00103 --- /dev/null +++ b/config/crd/bases/core.openmcp.cloud_managedcontrolplanes.yaml @@ -0,0 +1,590 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.4 + name: managedcontrolplanes.core.openmcp.cloud +spec: + group: core.openmcp.cloud + names: + kind: ManagedControlPlane + listKind: ManagedControlPlaneList + plural: managedcontrolplanes + shortNames: + - mcp + singular: managedcontrolplane + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .status.status + name: Status + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ManagedControlPlane is the Schema for the ManagedControlPlane + API + properties: + apiVersion: + 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 + type: string + kind: + 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 + type: string + metadata: + type: object + spec: + description: ManagedControlPlaneSpec defines the desired state of ManagedControlPlane. + properties: + authentication: + description: Authentication contains the configuration for the enabled + OpenID Connect identity providers + properties: + enableSystemIdentityProvider: + type: boolean + identityProviders: + items: + description: IdentityProvider contains the configuration for + an OpenID Connect identity provider + properties: + caBundle: + description: |- + CABundle: When set, the OpenID server's certificate will be verified by one of the authorities in the bundle. + Otherwise, the host's root CA set will be used. + type: string + clientConfig: + description: ClientAuthentication contains configuration + for OIDC clients + properties: + clientSecret: + description: |- + ClientSecret is a references to a secret containing the client secret. + The client secret will be added to the generated kubeconfig with the "--oidc-client-secret" flag. + properties: + key: + description: Key is the key inside the secret. + type: string + name: + description: Name is the secret name. + type: string + required: + - key + - name + type: object + extraConfig: + additionalProperties: + description: SingleOrMultiStringValue is a type that + can hold either a single string value or a list + of string values. + properties: + value: + description: Value is a single string value. + type: string + values: + description: Values is a list of string values. + items: + type: string + type: array + type: object + description: |- + ExtraConfig is added to the client configuration in the kubeconfig. + Can either be a single string value, a list of string values or no value. + Must not contain any of the following keys: + - "client-id" + - "client-secret" + - "issuer-url" + type: object + type: object + clientID: + description: ClientID is the client ID of the identity provider. + type: string + groupsClaim: + description: GroupsClaim is the claim that contains the + groups. + type: string + issuerURL: + description: IssuerURL is the issuer URL of the identity + provider. + type: string + name: + description: |- + Name is the name of the identity provider. + The name must be unique among all identity providers. + The name must only contain lowercase letters. + The length must not exceed 63 characters. + maxLength: 63 + pattern: ^[a-z]+$ + type: string + requiredClaims: + additionalProperties: + type: string + description: RequiredClaims is a map of required claims. + If set, the identity provider must provide these claims + in the ID token. + type: object + signingAlgs: + description: SigningAlgs is the list of allowed JOSE asymmetric + signing algorithms. + items: + type: string + type: array + usernameClaim: + description: UsernameClaim is the claim that contains the + username. + type: string + required: + - clientID + - issuerURL + - name + - usernameClaim + type: object + type: array + type: object + authorization: + description: Authorization contains the configuration of the subjects + assigned to control plane roles + properties: + roleBindings: + description: RoleBindings is a list of role bindings + items: + description: RoleBinding contains the role and the subjects + assigned to the role + properties: + role: + description: Role is the name of the role + enum: + - admin + - view + type: string + subjects: + description: Subjects is a list of subjects assigned to + the role + items: + description: |- + Subject describes an object that is assigned to a role and + which can be used to authenticate against the control plane. + properties: + apiGroup: + description: APIGroup is the API group of the subject + type: string + kind: + description: Kind is the kind of the subject + enum: + - ServiceAccount + - User + - Group + type: string + name: + description: Name is the name of the subject + minLength: 1 + type: string + namespace: + description: Namespace is the namespace of the subject + type: string + required: + - kind + - name + type: object + type: array + required: + - role + - subjects + type: object + type: array + required: + - roleBindings + type: object + components: + description: Components contains the configuration for Components + like APIServer, Landscaper, CloudOrchestrator. + properties: + apiServer: + default: + type: GardenerDedicated + description: APIServerConfiguration contains the configuration + which is required for setting up a k8s cluster to be used as + APIServer. + properties: + gardener: + description: |- + GardenerConfig contains configuration for a Gardener APIServer. + Must be set if type is 'Gardener', is ignored otherwise. + properties: + auditLog: + description: AuditLogConfig defines the AuditLog configuration + for the ManagedControlPlane cluster. + properties: + policyRef: + description: PolicyRef is the reference to the policy + containing the configuration for the audit log service. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + secretRef: + description: SecretRef is the reference to the secret + containing the credentials for the audit log service. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + serviceURL: + description: ServiceURL is the URL from the Service + Keys. + type: string + tenantID: + description: TenantID is the tenant ID of the BTP + Subaccount. Can be seen in the BTP Cockpit dashboard. + type: string + type: + description: Type is the type of the audit log. + enum: + - standard + type: string + required: + - policyRef + - secretRef + - serviceURL + - tenantID + - type + type: object + encryptionConfig: + description: EncryptionConfig contains customizable encryption + configuration of the API server. + properties: + resources: + description: |- + Resources contains the list of resources that shall be encrypted in addition to secrets. + Each item is a Kubernetes resource name in plural (resource or resource.group) that should be encrypted. + Example: ["configmaps", "statefulsets.apps", "flunders.emxample.com"] + items: + type: string + type: array + type: object + highAvailability: + description: HighAvailabilityConfig specifies the HA configuration + for the API server. + properties: + failureToleranceType: + description: |- + FailureToleranceType specifies failure tolerance mode for the API server. + Allowed values are: node, zone + node: The API server is tolerant to node failures within a single zone. + zone: The API server is tolerant to zone failures. + enum: + - node + - zone + type: string + x-kubernetes-validations: + - message: failureToleranceType is immutable + rule: self == oldSelf + required: + - failureToleranceType + type: object + x-kubernetes-validations: + - message: highAvailability is immutable + rule: self == oldSelf + region: + description: |- + Region is the region to be used for the Shoot cluster. + This is usually derived from the ManagedControlPlane's common configuration, but can be overwritten here. + type: string + x-kubernetes-validations: + - message: region is immutable + rule: self == oldSelf + type: object + x-kubernetes-validations: + - message: highAvailability is required once set + rule: has(self.highAvailability) == has(oldSelf.highAvailability) + || has(self.highAvailability) + type: + default: GardenerDedicated + description: |- + Type is the type of APIServer. This determines which other configuration fields need to be specified. + Valid values are: + - Gardener + - GardenerDedicated + enum: + - Gardener + - GardenerDedicated + type: string + x-kubernetes-validations: + - message: type is immutable + rule: self == oldSelf + required: + - type + type: object + btpServiceOperator: + description: BTPServiceOperator defines the configuration for + setting up the BTPServiceOperator component in a ManagedControlPlane. + properties: + version: + description: The Version of BTP Service Operator to install. + type: string + required: + - version + type: object + crossplane: + description: Crossplane defines the configuration for setting + up the Crossplane component in a ManagedControlPlane. + properties: + providers: + items: + properties: + name: + description: |- + Name of the provider. + Using a well-known name will automatically configure the "package" field. + type: string + version: + description: Version of the provider to install. + type: string + required: + - name + - version + type: object + type: array + version: + description: The Version of Crossplane to install. + type: string + required: + - version + type: object + externalSecretsOperator: + description: ExternalSecretsOperator defines the configuration + for setting up the ExternalSecretsOperator component in a ManagedControlPlane. + properties: + version: + description: The Version of External Secrets Operator to install. + type: string + required: + - version + type: object + flux: + description: Flux defines the configuration for setting up the + Flux component in a ManagedControlPlane. + properties: + version: + description: The Version of Flux to install. + type: string + required: + - version + type: object + kyverno: + description: Kyverno defines the configuration for setting up + the Kyverno component in a ManagedControlPlane. + properties: + version: + description: The Version of Kyverno to install. + type: string + required: + - version + type: object + landscaper: + description: LandscaperConfiguration contains the configuration + which is required for setting up a LaaS instance. + properties: + deployers: + description: Deployers is the list of deployers that should + be installed. + items: + type: string + type: array + type: object + type: object + x-kubernetes-validations: + - message: apiServer is required once set + rule: '!has(oldSelf.apiServer)|| has(self.apiServer)' + desiredRegion: + description: DesiredRegion allows customers to specify a desired region + proximity. + properties: + direction: + description: Direction is the direction within the region. + enum: + - north + - east + - south + - west + - central + type: string + name: + description: Name is the name of the region. + enum: + - northamerica + - southamerica + - europe + - asia + - africa + - australia + type: string + type: object + x-kubernetes-validations: + - message: RegionSpecification is immutable + rule: self == oldSelf + disabledComponents: + description: |- + DisabledComponents contains a list of component types. + The resources for these components will still be generated, but they will get the ignore operation annotation, so they should not be processed by their respective controllers. + items: + type: string + type: array + required: + - components + type: object + x-kubernetes-validations: + - message: desiredRegion is required once set + rule: '!has(oldSelf.desiredRegion)|| has(self.desiredRegion)' + status: + description: ManagedControlPlaneStatus defines the observed state of ManagedControlPlane. + properties: + components: + description: ManagedControlPlaneComponentsStatus contains the status + of the components of a ManagedControlPlane. + properties: + apiServer: + description: |- + ExternalAPIServerStatus contains the status of the API server / ManagedControlPlane cluster. The Kuberenetes can act as an OIDC + compatible provider in a sense that they serve OIDC issuer endpoint URL so that other system can validate tokens that have been + issued by the external party. + properties: + endpoint: + description: Endpoint represents the Kubernetes API server + endpoint + type: string + serviceAccountIssuer: + description: ServiceAccountIssuer represents the OpenIDConnect + issuer URL that can be used to verify service account tokens. + type: string + type: object + authentication: + description: ExternalAuthenticationStatus contains the status + of the authentication component. + properties: + access: + description: |- + UserAccess reference the secret containing the kubeconfig + for the APIServer which is to be used by the customer. + properties: + key: + description: Key is the key inside the secret. + type: string + name: + description: Name is the object's name. + type: string + namespace: + description: Namespace is the object's namespace. + type: string + required: + - key + - name + - namespace + type: object + type: object + authorization: + description: ExternalAuthorizationStatus contains the status of + the external authorization component + type: object + cloudOrchestrator: + description: ExternalCloudOrchestratorStatus contains the status + of the CloudOrchestrator component. + type: object + landscaper: + description: ExternalLandscaperStatus contains the status of a + LaaS instance. + type: object + type: object + conditions: + description: Conditions collects the conditions of all components. + items: + properties: + lastTransitionTime: + description: LastTransitionTime specifies the time when this + condition's status last changed. + format: date-time + type: string + managedBy: + description: ManagedBy contains the information which component + manages this condition. + type: string + message: + description: |- + Message contains further details regarding the condition. + It is meant for human users, Reason should be used for programmatic evaluation instead. + It is optional, but should be filled at least when Status is not "True". + type: string + reason: + description: |- + Reason is expected to contain a CamelCased string that provides further information regarding the condition. + It should have a fixed value set (like an enum) to be machine-readable. The value set depends on the condition type. + It is optional, but should be filled at least when Status is not "True". + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: |- + Type is the type of the condition. + This is a unique identifier and each type of condition is expected to be managed by exactly one component controller. + type: string + required: + - managedBy + - status + - type + type: object + type: array + message: + description: Message contains an optional message. + type: string + observedGeneration: + description: ObservedGeneration is the last generation of this resource + that has successfully been reconciled. + format: int64 + type: integer + status: + description: |- + Status is the current status of the ManagedControlPlane. + It is "Deleting" if the ManagedControlPlane is being deleted. + It is "Ready" if all conditions are true, and "Not Ready" otherwise. + type: string + required: + - observedGeneration + - status + type: object + type: object + x-kubernetes-validations: + - message: name must not be longer than 36 characters + rule: size(self.metadata.name) <= 36 + served: true + storage: true + subresources: + status: {} diff --git a/config/default/manager_webhook_patch.yaml b/config/default/manager_webhook_patch.yaml new file mode 100644 index 0000000..4181f63 --- /dev/null +++ b/config/default/manager_webhook_patch.yaml @@ -0,0 +1,26 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system + labels: + app.kubernetes.io/name: mcp-operator + app.kubernetes.io/managed-by: kustomize +spec: + template: + spec: + containers: + - name: manager + ports: + - containerPort: 9443 + name: webhook-server + protocol: TCP + volumeMounts: + - mountPath: /tmp/k8s-webhook-server/serving-certs + name: cert + readOnly: true + volumes: + - name: cert + secret: + defaultMode: 420 + secretName: webhook-server-cert diff --git a/config/default/webhookcainjection_patch.yaml b/config/default/webhookcainjection_patch.yaml new file mode 100644 index 0000000..6275169 --- /dev/null +++ b/config/default/webhookcainjection_patch.yaml @@ -0,0 +1,25 @@ +# This patch add annotation to admission webhook config and +# CERTIFICATE_NAMESPACE and CERTIFICATE_NAME will be substituted by kustomize +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: mcp-operator + app.kubernetes.io/managed-by: kustomize + name: mutating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + labels: + app.kubernetes.io/name: validatingwebhookconfiguration + app.kubernetes.io/instance: validating-webhook-configuration + app.kubernetes.io/component: webhook + app.kubernetes.io/created-by: mcp-operator + app.kubernetes.io/part-of: mcp-operator + app.kubernetes.io/managed-by: kustomize + name: validating-webhook-configuration + annotations: + cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME diff --git a/config/network-policy/allow-webhook-traffic.yaml b/config/network-policy/allow-webhook-traffic.yaml new file mode 100644 index 0000000..6b8ca75 --- /dev/null +++ b/config/network-policy/allow-webhook-traffic.yaml @@ -0,0 +1,26 @@ +# This NetworkPolicy allows ingress traffic to your webhook server running +# as part of the controller-manager from specific namespaces and pods. CR(s) which uses webhooks +# will only work when applied in namespaces labeled with 'webhook: enabled' +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + labels: + app.kubernetes.io/name: mcp-operator + app.kubernetes.io/managed-by: kustomize + name: allow-webhook-traffic + namespace: system +spec: + podSelector: + matchLabels: + control-plane: controller-manager + policyTypes: + - Ingress + ingress: + # This allows ingress traffic from any namespace with the label webhook: enabled + - from: + - namespaceSelector: + matchLabels: + webhook: enabled # Only from namespaces with this label + ports: + - port: 443 + protocol: TCP diff --git a/config/rbac/core_managedcomponent_admin_role.yaml b/config/rbac/core_managedcomponent_admin_role.yaml new file mode 100644 index 0000000..51c6e5a --- /dev/null +++ b/config/rbac/core_managedcomponent_admin_role.yaml @@ -0,0 +1,27 @@ +# This rule is not used by the project mcp-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants full permissions ('*') over core.openmcp.cloud. +# This role is intended for users authorized to modify roles and bindings within the cluster, +# enabling them to delegate specific permissions to other users or groups as needed. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: mcp-operator + app.kubernetes.io/managed-by: kustomize + name: core-managedcomponent-admin-role +rules: +- apiGroups: + - core.openmcp.cloud + resources: + - managedcomponents + verbs: + - '*' +- apiGroups: + - core.openmcp.cloud + resources: + - managedcomponents/status + verbs: + - get diff --git a/config/rbac/core_managedcomponent_editor_role.yaml b/config/rbac/core_managedcomponent_editor_role.yaml new file mode 100644 index 0000000..91e8698 --- /dev/null +++ b/config/rbac/core_managedcomponent_editor_role.yaml @@ -0,0 +1,33 @@ +# This rule is not used by the project mcp-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants permissions to create, update, and delete resources within the core.openmcp.cloud. +# This role is intended for users who need to manage these resources +# but should not control RBAC or manage permissions for others. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: mcp-operator + app.kubernetes.io/managed-by: kustomize + name: core-managedcomponent-editor-role +rules: +- apiGroups: + - core.openmcp.cloud + resources: + - managedcomponents + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - core.openmcp.cloud + resources: + - managedcomponents/status + verbs: + - get diff --git a/config/rbac/core_managedcomponent_viewer_role.yaml b/config/rbac/core_managedcomponent_viewer_role.yaml new file mode 100644 index 0000000..cdcb761 --- /dev/null +++ b/config/rbac/core_managedcomponent_viewer_role.yaml @@ -0,0 +1,29 @@ +# This rule is not used by the project mcp-operator itself. +# It is provided to allow the cluster admin to help manage permissions for users. +# +# Grants read-only access to core.openmcp.cloud resources. +# This role is intended for users who need visibility into these resources +# without permissions to modify them. It is ideal for monitoring purposes and limited-access viewing. + +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: mcp-operator + app.kubernetes.io/managed-by: kustomize + name: core-managedcomponent-viewer-role +rules: +- apiGroups: + - core.openmcp.cloud + resources: + - managedcomponents + verbs: + - get + - list + - watch +- apiGroups: + - core.openmcp.cloud + resources: + - managedcomponents/status + verbs: + - get diff --git a/config/sample/sample_mcp.yaml b/config/sample/sample_mcp.yaml new file mode 100644 index 0000000..416f21d --- /dev/null +++ b/config/sample/sample_mcp.yaml @@ -0,0 +1,15 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ManagedControlPlane +metadata: + name: mcp-sample + namespace: project-sample--ws-sample +spec: + desiredRegion: + direction: central + name: europe + components: + apiServer: + type: GardenerDedicated + landscaper: {} + crossplane: + version: 1.17.0 \ No newline at end of file diff --git a/config/webhook/kustomization.yaml b/config/webhook/kustomization.yaml new file mode 100644 index 0000000..9cf2613 --- /dev/null +++ b/config/webhook/kustomization.yaml @@ -0,0 +1,6 @@ +resources: +- manifests.yaml +- service.yaml + +configurations: +- kustomizeconfig.yaml diff --git a/config/webhook/kustomizeconfig.yaml b/config/webhook/kustomizeconfig.yaml new file mode 100644 index 0000000..206316e --- /dev/null +++ b/config/webhook/kustomizeconfig.yaml @@ -0,0 +1,22 @@ +# the following config is for teaching kustomize where to look at when substituting nameReference. +# It requires kustomize v2.1.0 or newer to work properly. +nameReference: +- kind: Service + version: v1 + fieldSpecs: + - kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + - kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/name + +namespace: +- kind: MutatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true +- kind: ValidatingWebhookConfiguration + group: admissionregistration.k8s.io + path: webhooks/clientConfig/service/namespace + create: true diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml new file mode 100644 index 0000000..818e6e6 --- /dev/null +++ b/config/webhook/manifests.yaml @@ -0,0 +1,25 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-core-openmcp-cloud-v1alpha1-managedcontrolplane + failurePolicy: Fail + name: vmanagedcontrolplane.kb.io + rules: + - apiGroups: + - core.openmcp.cloud + apiVersions: + - v1alpha1 + operations: + - DELETE + resources: + - managedcontrolplanes + sideEffects: None diff --git a/config/webhook/service.yaml b/config/webhook/service.yaml new file mode 100644 index 0000000..a58b4e3 --- /dev/null +++ b/config/webhook/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + labels: + app.kubernetes.io/name: mcp-operator + app.kubernetes.io/managed-by: kustomize + name: webhook-service + namespace: system +spec: + ports: + - port: 443 + protocol: TCP + targetPort: 9443 + selector: + control-plane: controller-manager diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..f3d8e5a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,7 @@ + +# Documentation Index + +## Controllers + +- [APIServer Controller](controllers/apiserver.md) + diff --git a/docs/controllers/.docnames b/docs/controllers/.docnames new file mode 100644 index 0000000..46a2551 --- /dev/null +++ b/docs/controllers/.docnames @@ -0,0 +1,3 @@ +{ + "header": "Controllers" +} \ No newline at end of file diff --git a/docs/controllers/apiserver.md b/docs/controllers/apiserver.md new file mode 100644 index 0000000..939a27a --- /dev/null +++ b/docs/controllers/apiserver.md @@ -0,0 +1,381 @@ +# APIServer Controller + +The apiserver controller reconciles `APIServer` and is responsible for providing the k8s cluster environment that is used by the services that are configured for the MCP. + +## Configuration + +Unlike most of the other controllers that are part of the MCP operator, the apiserver controller requires a configuration file to work properly. This file defines the specifics of where and how the k8s cluster environments are created by the controller. + +The config file contains one top-level key per configured handler for cluster provisioning. + +### Gardener + +The configuration for using Gardener as cluster provisioner is stored under the `gardener` key. It can be specified in two variants: a simple 'single' mode, or a more complex 'multi' mode. + +#### Single Mode +```yaml +gardener: + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: single + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf +``` + +- `cloudprofile` _string_ - The name of the Gardener `CloudProfile` to use for shoot creation. +- `regions` _array_ - A list of regions. Only regions which are specified here _and_ in the cloudprofile will be available. + - `name` _string_ - The name of the region. +- `defaultRegion` _string_ - The default region to use, unless specified otherwise. +- `shootTemplate` _object_ - A template to use for the shoot creation. There are a few things to note here: + - For the valid values and effects of each field in here, please check the [Gardener documentation](https://github.com/gardener/gardener/blob/master/docs/README.md). + - The shoot template is mainly used for the creation of shoots with workers (`APIServer`s with the `GardenerDedicated` type). For workerless shoots (`Gardener` type), only `metadata.annotations` and `metadata.labels` are taken into account. + - Most of the fields under `spec.provider` are specific to the chosen cloud provider and have to fit to each other and the chosen cloudprofile. + - See below for examples for AWS and GCP. + - Some of the fields in here might be adapted before the actual shoot is created. For example, the worker count is set to `3`, if high-availability is configured. +- `project` _string_ - Name of the Gardener `Project` to create the shoot clusters in. +- `kubeconfig` _string_ - A kubeconfig for the Garden cluster of the Gardener landscape. + +##### infrastructureConfig & controlplaneConfig + +###### AWS +```yaml +controlPlaneConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig +infrastructureConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + vpc: + cidr: 10.180.0.0/16 + zones: # optional + - name: eu-central-1a + workers: 10.180.0.0/19 + public: 10.180.32.0/20 + internal: 10.180.48.0/20 +``` + +AWS shoots require subnet CIDR ranges for each zone that workers are put into. These zones can either be specified in the shoot template, or the apiserver controller tries to default them based on the VPC CIDR. It tries to default CIDR ranges similar to the ones shown above for all zones available in the chosen region. Note that, with a `/16` subnet CIDR for the VPC, only four zones (with one `/19` and two `/20` CIDRs) fit into the network range. + +###### GCP + +```yaml +controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" # injected by controller +infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 +``` + +The controlplane zone is injected by the apiserver controller. + +#### Multi Mode + +As one might have noticed, the above configuration causes all MCP shoots to be created on the same Gardener landscape, in the same project, with the same cloud provider. For more complex use-cases, multiple configurations can be passed in. + +```yaml +gardener: + defaultConfig: default/gcp + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: gcp + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/gcp + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: aws + cloudProfile: aws + regions: + - name: eu-central-1 + - name: eu-west-1 + - name: us-east-1 + - name: ap-southeast-1 + defaultRegion: eu-central-1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/aws + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: aws + infrastructureConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + vpc: + cidr: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: m5.large + image: + name: gardenlinux + version: 1592.1.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: gp3 + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo +``` + +While syntax and semantic of the config fields explained above remain the same, the structure changes slightly and a few new fields are introduced: +- `defaultConfig` _string_ - The configuration to use by default. + - This has to follow the format `/`. +- `landscapes` _array_ - A list of Gardener landscapes. Each landscape has its own kubeconfig for the Garden cluster and can have multiple configurations. + - `name` _string_ - The name of the landscape. Used for referencing this section of the configuration. + - `kubeconfig` _string_ - The kubeconfig for the Garden cluster, as explained above. + - `configs` _array_ - A list of configurations. + - `name` _string_ - The name of the config. Used for referencing this section of the configuration. + - Basically, each item is a complete single-mode config as explained above, without the `kubeconfig` (which has moved one level up). + +##### Converting a Single Config to a Multi Config + +If this is your single configuration +```yaml +gardener: + cloudProfile: + regions: + defaultRegion: + shootTemplate: + project: + kubeconfig: +``` +it can easily be converted into an equivalent multi config: +```yaml +gardener: + defaultConfig: foo/bar + landscapes: + - name: foo + kubeconfig: + configs: + - name: bar + cloudProfile: + regions: + defaultRegion: + shootTemplate: + project: +``` + +> Internally, sinlge configs are always converted into multi configs, using `default` as landscape and config name. + +##### Working with Multi Configs + +To use a different config than the default one, it has to be specified under `spec.internal.gardener.landscapeConfiguration` in the `APIServer` resource: +```yaml +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +<...> +spec: + internal: + gardener: + landscapeConfiguration: foo/bar +``` + +To create such an `APIServer` resource via a `ManagedControlPlane`, one has to create a corresponding `InternalConfiguration` resource: +```yaml +apiVersion: core.openmcp.cloud/v1alpha1 +kind: InternalConfiguration +metadata: + name: my-mcp + namespace: my-project +spec: + components: + apiServer: + gardener: + landscapeConfiguration: default/aws +``` +Putting this `InternalConfiguration` next to a `ManagedControlPlane` named `my-mcp` in namespace `my-project` would result in the MCP controller rendering the landscape configuration into the `APIServer` resource. + +###### Caveats + +There are a few things that should be noted when working with multiple configurations: +- `InternalConfigurations` can only be seen and modified by landscape operators, not by end-users. This means that there is currently no way to expose multiple configurations to customer (because this feature was implemented for development and testing purposes). +- The apiserver controller one of the first controllers to react on a new MCP resource, as most others depend on the cluster. This means that, if you want to create a non-default cluster, you have to make sure the `InternalConfiguration` that overwrites the default has to exist already before the corresponding `ManagedControlPlane` is read by the MCP controller for the first time. Otherwise, the apiserver controller would likely start to create a shoot from the wrong configuration. +- As the used config determines not only some shoot specifics, but also cloud provider as well as Gardener project and landscape, you should _never_ change the used config while the corresponding shoot exists. So, don't create or delete the `InternalConfiguration` if the shoot already exists and don't change the value of `spec.internal.gardener.landscapeConfiguration` (validation should prevent the latter one). In the best case, this would lead to an orphaned shoot, but it might also mess up the MCP in other undesirable ways. + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f9731be --- /dev/null +++ b/go.mod @@ -0,0 +1,107 @@ +module github.com/openmcp-project/mcp-operator + +go 1.23.5 + +toolchain go1.24.0 + +replace github.com/openmcp-project/mcp-operator/api => ./api + +replace github.com/imdario/mergo v1.0.0 => github.com/imdario/mergo v0.3.16 + +require ( + github.com/Masterminds/semver/v3 v3.3.1 + github.com/alitto/pond/v2 v2.2.0 + github.com/apparentlymart/go-cidr v1.1.0 + github.com/gardener/landscaper-service v0.122.0 + github.com/onsi/ginkgo/v2 v2.22.2 + github.com/onsi/gomega v1.36.2 + github.com/openmcp-project/control-plane-operator v0.1.4 + github.com/openmcp-project/controller-utils v0.4.2 + github.com/openmcp-project/mcp-operator/api v0.26.0 + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 + github.com/stretchr/testify v1.10.0 + gopkg.in/yaml.v3 v3.0.1 + k8s.io/api v0.32.2 + k8s.io/apiextensions-apiserver v0.32.2 + k8s.io/apimachinery v0.32.2 + k8s.io/client-go v0.32.2 + k8s.io/utils v0.0.0-20241210054802-24370beab758 + sigs.k8s.io/controller-runtime v0.20.2 + sigs.k8s.io/yaml v1.4.0 +) + +require ( + dario.cat/mergo v1.0.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/crossplane/crossplane v1.17.2 // indirect + github.com/crossplane/crossplane-runtime v1.17.0 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/gardener/component-spec/bindings-go v0.0.98 // indirect + github.com/gardener/landscaper/apis v0.127.0 // indirect + github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/zapr v1.3.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gobuffalo/flect v1.0.2 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.11 // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect + github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect + github.com/xeipuuv/gojsonschema v1.2.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac // indirect + golang.org/x/mod v0.23.0 // indirect + golang.org/x/net v0.35.0 // indirect + golang.org/x/oauth2 v0.26.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/term v0.29.0 // indirect + golang.org/x/text v0.22.0 // indirect + golang.org/x/time v0.10.0 // indirect + golang.org/x/tools v0.30.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/protobuf v1.36.5 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 // indirect + sigs.k8s.io/controller-tools v0.14.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.5.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..be06799 --- /dev/null +++ b/go.sum @@ -0,0 +1,252 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4= +github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/alitto/pond/v2 v2.2.0 h1:hX3B1Lu4b5PjSHR+IWNRDKD0Jfw2ew8V25J7Vu5j7RM= +github.com/alitto/pond/v2 v2.2.0/go.mod h1:xkjYEgQ05RSpWdfSd1nM3OVv7TBhLdy7rMp3+2Nq+yE= +github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU= +github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/crossplane/crossplane v1.17.2 h1:iJyqNCI0ub5W+L/2eUvWnKXHBHhqmRynVDhFuvZanvw= +github.com/crossplane/crossplane v1.17.2/go.mod h1:KAoOkSa6i9EW+Q9yseuf6GLKSko2HWztkNrFTMeh1bc= +github.com/crossplane/crossplane-runtime v1.17.0 h1:y+GvxPT1M9s8BKt2AeZJdd2d6pg2xZeCO6LiR+VxEF8= +github.com/crossplane/crossplane-runtime v1.17.0/go.mod h1:vtglCrnnbq2HurAk9yLHa4qS0bbnCxaKL7C21cQcB/0= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.9.0+incompatible h1:fBXyNpNMuTTDdquAq/uisOr2lShz4oaXpDTX2bLe7ls= +github.com/evanphx/json-patch v5.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/gardener/component-spec/bindings-go v0.0.98 h1:noougWGxAQHr3ipqA2vbZ3qwodZ7wWX24QTD+zcZf7M= +github.com/gardener/component-spec/bindings-go v0.0.98/go.mod h1:qr7kADDXbXB0huul+ih/B43YkwyiMFYQepp/tqJ331c= +github.com/gardener/landscaper-service v0.122.0 h1:PPodYmXwTSPl98mFUpNuOTbUq4yN1rvcdwRKSED+VP4= +github.com/gardener/landscaper-service v0.122.0/go.mod h1:JDnZp9p0tcSAOnH1jfQiasCp/V81y7lvaZZkiR+dhPk= +github.com/gardener/landscaper/apis v0.127.0 h1:/FzkmA72BJjVXypSLVJhUIcgEwVZTT2qq+hRhAGOnYk= +github.com/gardener/landscaper/apis v0.127.0/go.mod h1:CBgDseuwtSNXSmYBWG0G3e6hYuPGzuVhFNZ7q+TfbHY= +github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= +github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= +github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= +github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= +github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +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.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nxadm/tail v1.4.11 h1:8feyoE3OzPrcshW5/MJ4sGESc5cqmGkGCWlco4l0bqY= +github.com/nxadm/tail v1.4.11/go.mod h1:OTaG3NK980DZzxbRq6lEuzgU+mug70nY11sMd4JXXHc= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= +github.com/openmcp-project/control-plane-operator v0.1.4 h1:kwSfFYkH6VeqGU0MJi6a5b8cXocbzQSgWXSGR6Y0D5Q= +github.com/openmcp-project/control-plane-operator v0.1.4/go.mod h1:F68trPQsXm54c1xrQor1yQEXqacNexEsTYomGjHFRD4= +github.com/openmcp-project/controller-utils v0.4.2 h1:8viXmZZdBLanN1ummLr05ceCd7XaEBToflm1hU17ecQ= +github.com/openmcp-project/controller-utils v0.4.2/go.mod h1:6BeRpSZK/FkJbelqGA4pv10FEtNerT2RvWb3Eg1Z9j0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac h1:l5+whBCLH3iH2ZNHYLbAe58bo7yrN4mVcnkHDYz5vvs= +golang.org/x/exp v0.0.0-20250210185358-939b2ce775ac/go.mod h1:hH+7mtFmImwwcMvScyxUhjuVHR3HGaDPMn9rMSUUbxo= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= +golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= +golang.org/x/oauth2 v0.26.0 h1:afQXWNNaeC4nvZ0Ed9XvCCzXM6UHJG7iCg0W4fPqSBE= +golang.org/x/oauth2 v0.26.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= +golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4= +golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY= +golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= +google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.32.2 h1:bZrMLEkgizC24G9eViHGOPbW+aRo9duEISRIJKfdJuw= +k8s.io/api v0.32.2/go.mod h1:hKlhk4x1sJyYnHENsrdCWw31FEmCijNGPJO5WzHiJ6Y= +k8s.io/apiextensions-apiserver v0.32.2 h1:2YMk285jWMk2188V2AERy5yDwBYrjgWYggscghPCvV4= +k8s.io/apiextensions-apiserver v0.32.2/go.mod h1:GPwf8sph7YlJT3H6aKUWtd0E+oyShk/YHWQHf/OOgCA= +k8s.io/apimachinery v0.32.2 h1:yoQBR9ZGkA6Rgmhbp/yuT9/g+4lxtsGYwW6dR6BDPLQ= +k8s.io/apimachinery v0.32.2/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/client-go v0.32.2 h1:4dYCD4Nz+9RApM2b/3BtVvBHw54QjMFUl1OLcJG5yOA= +k8s.io/client-go v0.32.2/go.mod h1:fpZ4oJXclZ3r2nDOv+Ux3XcJutfrwjKTCHz2H3sww94= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7 h1:hcha5B1kVACrLujCKLbr8XWMxCxzQx42DY8QKYJrDLg= +k8s.io/kube-openapi v0.0.0-20241212222426-2c72e554b1e7/go.mod h1:GewRfANuJ70iYzvn+i4lezLDAFzvjxZYK1gn1lWcfas= +k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= +k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +sigs.k8s.io/controller-runtime v0.20.2 h1:/439OZVxoEc02psi1h4QO3bHzTgu49bb347Xp4gW1pc= +sigs.k8s.io/controller-runtime v0.20.2/go.mod h1:xg2XB0K5ShQzAgsoujxuKN4LNXR2LfwwHsPj7Iaw+XY= +sigs.k8s.io/controller-tools v0.14.0 h1:rnNoCC5wSXlrNoBKKzL70LNJKIQKEzT6lloG6/LF73A= +sigs.k8s.io/controller-tools v0.14.0/go.mod h1:TV7uOtNNnnR72SpzhStvPkoS/U5ir0nMudrkrC4M9Sc= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0 h1:nbCitCK2hfnhyiKo6uf2HxUPTCodY6Qaf85SbDIaMBk= +sigs.k8s.io/structured-merge-diff/v4 v4.5.0/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt new file mode 100644 index 0000000..e69de29 diff --git a/hack/common/Dockerfile b/hack/common/Dockerfile new file mode 100644 index 0000000..c6783d8 --- /dev/null +++ b/hack/common/Dockerfile @@ -0,0 +1,12 @@ +# Use distroless as minimal base image to package the component binary +# Refer to https://github.com/GoogleContainerTools/distroless for more details +FROM gcr.io/distroless/static:nonroot +ARG TARGETOS +ARG TARGETARCH +ARG COMPONENT +WORKDIR / +COPY bin/$COMPONENT-$TARGETOS.$TARGETARCH / +USER 65532:65532 + +# docker doesn't substitue args in ENTRYPOINT, so we replace this during the build script +ENTRYPOINT ["/"] diff --git a/hack/common/Makefile b/hack/common/Makefile new file mode 100644 index 0000000..9df0d3c --- /dev/null +++ b/hack/common/Makefile @@ -0,0 +1,273 @@ +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +############################################################################################################ +# This Makefile is meant to be included in the Makefile of the repository including this one as submodule. # +# Currently, this repository has to be included at 'hack/common' path in the parent repository. # +# Also, the parent Makefile has to set the following variables: # +# - REPO_ROOT: The root directory of the repository. # +# - COMPONENTS: The list of components to build (usually just one, e.g. 'mcp-operator'). # +############################################################################################################ + +##@ Common - General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk commands is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php + +ifndef HELP_TARGET +HELP_TARGET := true +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) +endif + +##@ Common - Development + +ifndef TIDY_TARGET +TIDY_TARGET := true +.PHONY: tidy +tidy: ## Runs 'go mod tidy' for all modules in this repo. + @$(REPO_ROOT)/hack/common/tidy.sh +endif + +ifndef GENERATE_DOCS_TARGET +GENERATE_DOCS_TARGET := true +.PHONY: generate-docs +generate-docs: jq ## Generates the documentation index. + @JQ=$(JQ) $(REPO_ROOT)/hack/common/generate-docs-index.sh +endif + +ifndef VERIFY_DOCS_TARGET +VERIFY_DOCS_TARGET := true +.PHONY: verify-docs +verify-docs: jq ## Verifies that the documentation index is up-to-date. + @test "$(SKIP_DOCS_INDEX_CHECK)" = "true" || \ + ( echo "> Verify documentation index ..." && \ + JQ=$(JQ) $(REPO_ROOT)/hack/common/verify-docs-index.sh ) +endif + +##@ Common - Release + +ifndef PREPARE_RELEASE_TARGET +PREPARE_RELEASE_TARGET := true +.PHONY: prepare-release +prepare-release: tidy generate verify test +endif + +ifndef RELEASE_MAJOR_TARGET +RELEASE_MAJOR_TARGET := true +.PHONY: release-major +release-major: prepare-release ## Creates a major release commit. + @COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/release.sh major +endif + +ifndef RELEASE_MINOR_TARGET +RELEASE_MINOR_TARGET := true +.PHONY: release-minor +release-minor: prepare-release ## Creates a minor release commit. + @COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/release.sh minor +endif + +ifndef RELEASE_PATCH_TARGET +RELEASE_PATCH_TARGET := true +.PHONY: release-patch +release-patch: prepare-release ## Creates a patch release commit. + @COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/release.sh patch +endif + +##@ Common - Build + +PLATFORMS ?= linux/arm64,linux/amd64 + +ifndef ALL_TARGET +ALL_TARGET := true +.PHONY: all +all: build image chart component ## Complete build and push for all components/platforms specified in COMPONENTS/PLATFORMS. Alias for 'make build image chart component'. +endif + +ifndef BUILD_TARGET +BUILD_TARGET := true +.PHONY: build +build: generate ## Builds binaries for all components specified in COMPONENTS and all platforms specified in PLATFORMS. + @PLATFORMS=$(PLATFORMS) COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/build-binary.sh +endif + +ifndef IMAGE_TARGET +IMAGE_TARGET := true +.PHONY: image +image: build image-build image-push ## Builds and pushes docker images for all components specified in COMPONENTS and all platforms specified in PLATFORMS. +endif + +ifndef IMAGE_BUILD_TARGET +IMAGE_BUILD_TARGET := true +.PHONY: image-build +image-build: ## Builds the docker images for all components specified in COMPONENTS and all platforms specified in PLATFORMS. Requires 'make build' to have run before. + @PLATFORMS=$(PLATFORMS) COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/build-image.sh +endif + +ifndef IMAGE_BUILD_LOCAL_TARGET +IMAGE_BUILD_LOCAL_TARGET := true +.PHONY: image-build-local +image-build-local: ## Builds the docker images for all components specified in COMPONENTS and all platforms specified in PLATFORMS. Requires 'make build' to have run before. + @PLATFORMS=$(PLATFORMS) COMPONENTS=$(COMPONENTS) BASE_REGISTRY=local $(REPO_ROOT)/hack/common/build-image.sh +endif + +ifndef IMAGE_PUSH_TARGET +IMAGE_PUSH_TARGET := true +.PHONY: image-push +image-push: ## Pushes the docker images for all components specified in COMPONENTS and all platforms specified in PLATFORMS. Requires 'make image-build' to have run before. + @PLATFORMS=$(PLATFORMS) COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/push-image.sh +endif + +ifndef CHART_TARGET +CHART_TARGET := true +.PHONY: chart +chart: chart-build chart-push ## Packs and pushes the helm charts for all components specified in COMPONENTS into the OCI registry. +endif + +ifndef CHART_BUILD_TARGET +CHART_BUILD_TARGET := true +.PHONY: chart-build +chart-build: helm ## Packs the helm charts for all components specified in COMPONENTS to prepare them for upload into the OCI registry. + @HELM=$(HELM) COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/build-chart.sh +endif + +ifndef CHART_PUSH_TARGET +CHART_PUSH_TARGET := true +.PHONY: chart-push +chart-push: helm jq yaml2json ## Pushes helm charts for all components specified in COMPONENTS into the OCI registry. Requires 'make chart-build' to have run before. + @HELM=$(HELM) COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/push-chart.sh +endif + +ifndef COMPONENT_TARGET +COMPONENT_TARGET := true +.PHONY: component +component: component-build component-push ## Builds and pushes the component descriptor into the registry. Requires charts and images to be already pushed into the registry. +endif + +ifndef COMPONENT_BUILD_TARGET +COMPONENT_BUILD_TARGET := true +.PHONY: component-build +component-build: ocm ## Builds the component descriptor for the mcp-operator. Several env variables can be used to control the result, see components/components.yaml for details. Requires charts and images to be already pushed into the registry. + @OCM=$(OCM) COMPONENTS=$(COMPONENTS) $(REPO_ROOT)/hack/common/build-component.sh +endif + +ifndef COMPONENT_PUSH_TARGET +COMPONENT_PUSH_TARGET := true +.PHONY: component-push +component-push: ocm ## Pushes the component descriptor into the registry. Requires 'make component-build' to have run before. + @OCM=$(OCM) $(REPO_ROOT)/hack/common/push-component.sh +endif + +##@ Common - Build Dependencies + +## Location to install dependencies to +LOCALBIN ?= $(REPO_ROOT)/bin + +## Tool Binaries +KUBECTL ?= kubectl +KIND ?= kind +CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen +FORMATTER ?= $(LOCALBIN)/goimports +LINTER ?= $(LOCALBIN)/golangci-lint +OCM ?= $(LOCALBIN)/ocm +HELM ?= $(LOCALBIN)/helm +JQ ?= $(LOCALBIN)/jq +YAML2JSON ?= $(LOCALBIN)/yaml2json + +## Tool Versions +# renovate: datasource=github-releases depName=kubernetes-sigs/controller-tools +CONTROLLER_TOOLS_VERSION ?= v0.16.4 +# renovate: datasource=github-tags depName=golang/tools +FORMATTER_VERSION ?= v0.26.0 +# renovate: datasource=github-releases depName=golangci/golangci-lint +LINTER_VERSION ?= v1.64.4 +# renovate: datasource=github-releases depName=jqlang/jq +JQ_VERSION ?= 1.7.1 +# renovate: datasource=github-releases depName=open-component-model/ocm +OCM_VERSION ?= 0.16.2 +HELM_VERSION ?= v3.13.2 +# renovate: datasource=github-releases depName=bronze1man/yaml2json +YAML2JSON_VERSION ?= v1.3.3 + +ifndef LOCALBIN_TARGET +LOCALBIN_TARGET := true +.PHONY: localbin +localbin: + @test -d $(LOCALBIN) || mkdir -p $(LOCALBIN) +endif + +ifndef CONTROLLER_GEN_TARGET +CONTROLLER_GEN_TARGET := true +.PHONY: controller-gen +controller-gen: localbin ## Download controller-gen locally if necessary. If wrong version is installed, it will be overwritten. + @test -s $(CONTROLLER_GEN) && $(CONTROLLER_GEN) --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ + ( echo "Installing controller-gen $(CONTROLLER_TOOLS_VERSION) ..."; \ + GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) ) +endif + +ifndef GOIMPORTS_TARGET +GOIMPORTS_TARGET := true +.PHONY: goimports +goimports: localbin ## Download goimports locally if necessary. If wrong version is installed, it will be overwritten. + @test -s $(FORMATTER) && test -s $(LOCALBIN)/goimports_version && cat $(LOCALBIN)/goimports_version | grep -q $(FORMATTER_VERSION) || \ + ( echo "Installing goimports $(FORMATTER_VERSION) ..."; \ + GOBIN=$(LOCALBIN) go install golang.org/x/tools/cmd/goimports@$(FORMATTER_VERSION) && \ + echo $(FORMATTER_VERSION) > $(LOCALBIN)/goimports_version ) +endif + +ifndef GOLANGCI_LINT_TARGET +GOLANGCI_LINT_TARGET := true +.PHONY: golangci-lint +golangci-lint: localbin ## Download golangci-lint locally if necessary. If wrong version is installed, it will be overwritten. + @test -s $(LINTER) && $(LINTER) --version | grep -q $(subst v,,$(LINTER_VERSION)) || \ + ( echo "Installing golangci-lint $(LINTER_VERSION) ..."; \ + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(LOCALBIN) $(LINTER_VERSION) ) +endif + +ifndef OCM_TARGET +OCM_TARGET := true +.PHONY: ocm +ocm: localbin ## Install OCM CLI if necessary. + @test -s $(OCM) && $(OCM) --version | grep -q $(OCM_VERSION) || \ + ( echo "Installing ocm $(OCM_VERSION) ..."; \ + curl -sSfL https://ocm.software/install.sh | OCM_VERSION=$(OCM_VERSION) bash -s $(LOCALBIN) ) +endif + +ifndef HELM_TARGET +HELM_TARGET := true +.PHONY: helm +helm: localbin ## Download helm locally if necessary. + @test -s $(HELM) && $(HELM) version --short | grep -q $(HELM_VERSION) || \ + ( echo "Installing helm $(HELM_VERSION) ..."; \ + HELM=$(HELM) LOCALBIN=$(LOCALBIN) $(REPO_ROOT)/hack/common/install/helm.sh $(HELM_VERSION) ) +endif + +ifndef JQ_TARGET +JQ_TARGET := true +.PHONY: jq +jq: localbin ## Download jq locally if necessary. If wrong version is installed, it will be overwritten. + @test -s $(JQ) && $(JQ) --version | grep -q $(subst v,,$(JQ_VERSION)) || \ + ( echo "Installing jq $(JQ_VERSION) ..."; \ + JQ=$(JQ) LOCALBIN=$(LOCALBIN) $(REPO_ROOT)/hack/common/install/jq.sh $(JQ_VERSION) ) +endif + +ifndef YAML2JSON_TARGET +YAML2JSON_TARGET := true +.PHONY: yaml2json +# yaml2json 1.3.3 incorrectly reports its version as 1.3.2, thus the workaround below +yaml2json: localbin ## Download yaml2json locally if necessary. If wrong version is installed, it will be overwritten. + @test -s $(YAML2JSON) && ($(YAML2JSON) --version || true) | grep -q $(subst 1.3.3,1.3.2,$(subst v,,$(YAML2JSON_VERSION))) || \ + ( echo "Installing yaml2json $(YAML2JSON_VERSION) ..."; \ + YAML2JSON=$(YAML2JSON) LOCALBIN=$(LOCALBIN) $(REPO_ROOT)/hack/common/install/yaml2json.sh $(YAML2JSON_VERSION) ) +endif diff --git a/hack/common/build-binary.sh b/hack/common/build-binary.sh new file mode 100755 index 0000000..e0e1249 --- /dev/null +++ b/hack/common/build-binary.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +echo +echo "> Building binaries ..." +( + cd "$PROJECT_ROOT" + for comp in ${COMPONENTS//,/ }; do + for pf in ${PLATFORMS//,/ }; do + echo "> Building binary for component '$comp' ($pf) ..." | indent 1 + os=${pf%/*} + arch=${pf#*/} + CGO_ENABLED=0 GOOS=$os GOARCH=$arch go build -a -o bin/${comp}-${os}.${arch} cmd/${comp}/main.go | indent 2 + done + done +) diff --git a/hack/common/build-chart.sh b/hack/common/build-chart.sh new file mode 100755 index 0000000..b136bb2 --- /dev/null +++ b/hack/common/build-chart.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +VERSION=$("$COMMON_SCRIPT_DIR/get-version.sh") + +echo +echo "> Packaging helm charts to prepare for upload ..." +tmpdir="$("$COMMON_SCRIPT_DIR/get-tmp-dir.sh")" +for comp in ${COMPONENTS//,/ }; do + echo "> Packaging helm chart for component '$comp' ..." | indent 1 + "$HELM" package "$PROJECT_ROOT/charts/$comp" -d "$tmpdir" --version "$VERSION" | indent 2 # file name is -.tgz (derived from Chart.yaml) +done diff --git a/hack/common/build-component.sh b/hack/common/build-component.sh new file mode 100755 index 0000000..e0234e2 --- /dev/null +++ b/hack/common/build-component.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +VERSION=$("$COMMON_SCRIPT_DIR/get-version.sh") +CHART_REGISTRY=$("$COMMON_SCRIPT_DIR/get-registry.sh" --helm) +IMG_REGISTRY=$("$COMMON_SCRIPT_DIR/get-registry.sh" --image) + +pushd ${PROJECT_ROOT} > /dev/null 2>&1 +COMMIT="$(git rev-parse HEAD)" +popd > /dev/null 2>&1 + +echo +echo "> Building component in version ${CD_VERSION:-$VERSION} ..." +compdir="$("$COMMON_SCRIPT_DIR/get-tmp-dir.sh")/component" +$OCM add componentversions --file "$compdir" --version "$VERSION" --create --force --templater spiff "$COMPONENT_DEFINITION_FILE" -- \ + VERSION="$VERSION" \ + CHART_REGISTRY="$CHART_REGISTRY" \ + IMG_REGISTRY="$IMG_REGISTRY" \ + COMMIT="$COMMIT" \ + COMPONENTS="$COMPONENTS" \ + ${CD_VERSION:+CD_VERSION=}${CD_VERSION:-} \ + ${CHART_VERSION:+CHART_VERSION=}${CHART_VERSION:-} \ + ${IMG_VERSION:+IMG_VERSION=}${IMG_VERSION:-} \ + ${BP_COMPONENTS:+BP_COMPONENTS=}${BP_COMPONENTS:-} \ + ${CHART_COMPONENTS:+CHART_COMPONENTS=}${CHART_COMPONENTS:-} \ + ${IMG_COMPONENTS:+IMG_COMPONENTS=}${IMG_COMPONENTS:-} \ + | indent 1 + +echo "Use '$(realpath --relative-base=$(pwd) $OCM) get cv $compdir -o yaml' to view the generated component descriptor." \ No newline at end of file diff --git a/hack/common/build-image.sh b/hack/common/build-image.sh new file mode 100755 index 0000000..c1595c5 --- /dev/null +++ b/hack/common/build-image.sh @@ -0,0 +1,33 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +if [[ -z ${IMAGE_REGISTRY:-} ]]; then + IMAGE_REGISTRY=$("$COMMON_SCRIPT_DIR/get-registry.sh" -i) +fi + +VERSION=$("$COMMON_SCRIPT_DIR/get-version.sh") + +DOCKER_BUILDER_NAME="mcp-multiarch-builder" +if ! docker buildx ls | grep "$DOCKER_BUILDER_NAME" >/dev/null; then + docker buildx create --name "$DOCKER_BUILDER_NAME" +fi + +# remove temporary Dockerfile on exit +trap "rm -f \"${PROJECT_ROOT}/Dockerfile.tmp\"" EXIT + +echo +echo "> Building images ..." +for comp in ${COMPONENTS//,/ }; do + for pf in ${PLATFORMS//,/ }; do + os=${pf%/*} + arch=${pf#*/} + img="${IMAGE_REGISTRY}/${comp}:${VERSION}-${os}-${arch}" + echo "> Building image for component '$comp' ($pf): $img ..." | indent 1 + cat "${COMMON_SCRIPT_DIR}/Dockerfile" | sed "s//$comp/g" > "${PROJECT_ROOT}/Dockerfile.tmp" + docker buildx build --builder ${DOCKER_BUILDER_NAME} --load --build-arg COMPONENT=${comp} --platform ${pf} -t $img -f Dockerfile.tmp "${PROJECT_ROOT}" | indent 2 + done +done + +docker buildx rm "$DOCKER_BUILDER_NAME" \ No newline at end of file diff --git a/hack/common/environment.sh b/hack/common/environment.sh new file mode 100755 index 0000000..2d03251 --- /dev/null +++ b/hack/common/environment.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +export COMMON_SCRIPT_DIR="$(realpath "$(dirname ${BASH_SOURCE[0]})")" +source "$COMMON_SCRIPT_DIR/lib.sh" +export PROJECT_ROOT="${PROJECT_ROOT:-$(realpath "$COMMON_SCRIPT_DIR/../..")}" +export COMPONENT_DEFINITION_FILE="${COMPONENT_DEFINITION_FILE:-"$PROJECT_ROOT/components/components.yaml"}" + +export LOCALBIN="${LOCALBIN:-"$PROJECT_ROOT/bin"}" +export HELM="${HELM:-"$LOCALBIN/helm"}" +export JQ="${JQ:-"$LOCALBIN/jq"}" +export FORMATTER=${FORMATTER:-"$LOCALBIN/goimports"} +export OCM="${OCM:-"$LOCALBIN/ocm"}" +export YAML2JSON="${YAML2JSON:-"$LOCALBIN/yaml2json"}" + +if [[ -f "$COMMON_SCRIPT_DIR/../environment.sh" ]]; then + source "$COMMON_SCRIPT_DIR/../environment.sh" +fi diff --git a/hack/common/format.sh b/hack/common/format.sh new file mode 100755 index 0000000..06c6b33 --- /dev/null +++ b/hack/common/format.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +write_mode="-w" +if [[ ${1:-} == "--verify" ]]; then + write_mode="" + shift +fi + +# MODULE_NAME must be set to the name of the local go module. +tmp=$("${FORMATTER}" -l $write_mode -local=$MODULE_NAME $("$COMMON_SCRIPT_DIR/unfold.sh" --clean --no-unfold "$@")) + +if [[ -z ${write_mode} ]] && [[ ${tmp} ]]; then + echo "unformatted files detected, please run 'make format'" 1>&2 + echo "$tmp" 1>&2 + exit 1 +fi + +if [[ ${tmp} ]]; then + echo "> Formatting imports ..." + echo "$tmp" +fi diff --git a/hack/common/generate-docs-index.sh b/hack/common/generate-docs-index.sh new file mode 100755 index 0000000..c0f03a3 --- /dev/null +++ b/hack/common/generate-docs-index.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +DOCS_FOLDER="${DOCS_FOLDER:-${PROJECT_ROOT}/docs}" +METAFILE_NAME=".docnames" + +doc_index_file=${1:-"$DOCS_FOLDER/README.md"} + +# prints to the new doc index +function println() { + echo "$@" >> "$newindex" +} + +# expects a path to a folder as argument +# returns how this folder should be named in an index +function getDocFolderName() { + local metafile="$1/$METAFILE_NAME" + if [[ -f "$metafile" ]]; then + cat "$metafile" | $JQ -r '.header' + fi +} + +# expects two arguments: +# - path to a doc folder +# - name of the file in there +# the file is expected to contain its header in the first line +# or there should be an overwrite present in /$METAFILE_NAME +function getDocName() { + local metafile="$1/$METAFILE_NAME" + local filename="$2" + if [[ -f "$metafile" ]]; then + local overwrite="$(cat "$metafile" | $JQ -r '.overwrites[$name]' --arg name "$2")" + if [[ "$overwrite" != "null" ]]; then + echo "$overwrite" + return + fi + fi + if [[ -f "$filename" ]]; then + local firstheader=$(grep -m1 "#" "$filename") + echo "${firstheader#'# '}" + fi +} + +echo "> Generating Documentation Index" + +newindex=$(mktemp) + +println '' +println "# Documentation Index" +println + +( + cd "$DOCS_FOLDER" + for f in *; do + if [[ -d "$f" ]]; then + foldername="$(getDocFolderName "$f")" + if [[ -z "$foldername" ]]; then + echo "Ignoring folder '$f' due to missing '$METAFILE_NAME' file." + continue + fi + + println "## $foldername" + println + + ( + cd "$f" + for f2 in *.md; do + docname="$(getDocName "../$f" "$f2")" + if [[ -z "$docname" ]]; then + echo "Ignoring file '$f/$f2' because the header could not be determined." + # There are two possible reasons for this: + # 1. The file doesn't start with a '# ' in the first line and no overwrite is defined in the folder's metafile. + # 2. The overwrite in the folder's metafile explicitly sets the name to an empty string, meaning this file should be ignored. + continue + fi + println "- [$docname]($f/$f2)" + done + ) + + println + fi + done +) + +cp "$newindex" "$doc_index_file" \ No newline at end of file diff --git a/hack/common/get-registry.sh b/hack/common/get-registry.sh new file mode 100755 index 0000000..fe299fd --- /dev/null +++ b/hack/common/get-registry.sh @@ -0,0 +1,40 @@ +#!/bin/bash -eu + +set -euo pipefail + +if [[ -z ${BASE_REGISTRY:-} ]]; then + BASE_REGISTRY=ghcr.io/openmcp-project +fi + +if [[ -z ${IMAGE_REGISTRY:-} ]]; then + IMAGE_REGISTRY=$BASE_REGISTRY +fi +if [[ -z ${CHART_REGISTRY:-} ]]; then + CHART_REGISTRY=$BASE_REGISTRY/charts +fi +if [[ -z ${COMPONENT_REGISTRY:-} ]]; then + COMPONENT_REGISTRY=$BASE_REGISTRY/components +fi + +mode="BASE_" + +while [[ "$#" -gt 0 ]]; do + case ${1:-} in + "-i"|"--image") + mode="IMAGE_" + ;; + "-h"|"--helm") + mode="CHART_" + ;; + "-c"|"--component") + mode="COMPONENT_" + ;; + *) + echo "invalid argument: $1" 1>&2 + exit 1 + ;; + esac + shift +done + +eval echo "\$${mode}REGISTRY" diff --git a/hack/common/get-tmp-dir.sh b/hack/common/get-tmp-dir.sh new file mode 100755 index 0000000..4f844eb --- /dev/null +++ b/hack/common/get-tmp-dir.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +# Creates a directory within the temporary folder (either TMPDIR or /tmp) and returns its path. + +tmpdir="${TMPDIR:-"/tmp"}" +tmpdir="${tmpdir%/}/mcp/$(basename "$PROJECT_ROOT")" +mkdir -p "$tmpdir" +echo "$tmpdir" \ No newline at end of file diff --git a/hack/common/get-version.sh b/hack/common/get-version.sh new file mode 100755 index 0000000..afc4cc9 --- /dev/null +++ b/hack/common/get-version.sh @@ -0,0 +1,22 @@ +#!/bin/bash -eu + +if [[ -n ${EFFECTIVE_VERSION:-} ]] ; then + # running in the pipeline use the provided EFFECTIVE_VERSION + echo "$EFFECTIVE_VERSION" + exit 0 +fi + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +VERSION="$(cat "${PROJECT_ROOT}/VERSION")" + +( + cd "$PROJECT_ROOT" + + if [[ "$VERSION" = *-dev ]] ; then + VERSION="$VERSION-$(git rev-parse HEAD)" + fi + + echo "$VERSION" +) diff --git a/hack/common/install/helm.sh b/hack/common/install/helm.sh new file mode 100755 index 0000000..5ac6fda --- /dev/null +++ b/hack/common/install/helm.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/../environment.sh")" + +HELM_VERSION="$1" + +arch=$(uname -m) +if [[ "$arch" == "x86_64" ]]; then + arch="amd64" +fi +os=$(uname | tr '[:upper:]' '[:lower:]') +curl -sfL "https://get.helm.sh/helm-${HELM_VERSION}-${os}-${arch}.tar.gz" --output "$LOCALBIN/helm.tar.gz" +mkdir -p "$LOCALBIN/helm-unpacked" +tar -xzf "$LOCALBIN/helm.tar.gz" --directory "$LOCALBIN/helm-unpacked" +mv "$LOCALBIN/helm-unpacked/${os}-${arch}/helm" "$LOCALBIN/helm" +chmod +x "$LOCALBIN/helm" +rm -rf "$LOCALBIN/helm.tar.gz" "$LOCALBIN/helm-unpacked" diff --git a/hack/common/install/jq.sh b/hack/common/install/jq.sh new file mode 100755 index 0000000..b1baeac --- /dev/null +++ b/hack/common/install/jq.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/../environment.sh")" + +JQ_VERSION="$1" +os="linux64" +if [[ $(uname -o) == "Darwin" ]]; then + os="osx-amd64" +fi +curl -sfL "https://github.com/stedolan/jq/releases/download/jq-${JQ_VERSION}/jq-${os}" --output "${LOCALBIN}/jq" +chmod +x "${LOCALBIN}/jq" \ No newline at end of file diff --git a/hack/common/install/yaml2json.sh b/hack/common/install/yaml2json.sh new file mode 100755 index 0000000..52f3cf9 --- /dev/null +++ b/hack/common/install/yaml2json.sh @@ -0,0 +1,14 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/../environment.sh")" + +YAML2JSON_VERSION="$1" + +arch=$(uname -m) +if [[ "$arch" == "x86_64" ]]; then + arch="amd64" +fi +os=$(uname | tr '[:upper:]' '[:lower:]') +curl -sfL "https://github.com/bronze1man/yaml2json/releases/download/${YAML2JSON_VERSION}/yaml2json_${os}_${arch}" --output "${LOCALBIN}/yaml2json" +chmod +x "${LOCALBIN}/yaml2json" \ No newline at end of file diff --git a/hack/common/lib.sh b/hack/common/lib.sh new file mode 100755 index 0000000..fbbf560 --- /dev/null +++ b/hack/common/lib.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# pipe some text into 'indent X' to indent each line by X levels (one 'level' being two spaces) +function indent() { + local level=${1:-""} + if [[ -z "$level" ]]; then + level=1 + fi + local spaces=$(($level * 2)) + local iv=$(printf %${spaces}s) + sed "s/^/$iv/" +} \ No newline at end of file diff --git a/hack/common/push-chart.sh b/hack/common/push-chart.sh new file mode 100755 index 0000000..b9be000 --- /dev/null +++ b/hack/common/push-chart.sh @@ -0,0 +1,16 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +VERSION=$("$COMMON_SCRIPT_DIR/get-version.sh") +HELM_REGISTRY=$("$COMMON_SCRIPT_DIR/get-registry.sh" --helm) + +echo +echo "> Uploading helm charts to $HELM_REGISTRY ..." +tmpdir="$("$COMMON_SCRIPT_DIR/get-tmp-dir.sh")" +for comp in ${COMPONENTS//,/ }; do + chname="$(cat "$PROJECT_ROOT/charts/$comp/Chart.yaml" | $YAML2JSON | $JQ -r .name)" + echo "> Pushing helm chart for component '$comp' ..." | indent 1 + "$HELM" push "$tmpdir/$chname-$VERSION.tgz" "oci://$HELM_REGISTRY" | indent 2 +done diff --git a/hack/common/push-component.sh b/hack/common/push-component.sh new file mode 100755 index 0000000..2012650 --- /dev/null +++ b/hack/common/push-component.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +VERSION=$("$COMMON_SCRIPT_DIR/get-version.sh") +COMPONENT_REGISTRY="$($COMMON_SCRIPT_DIR/get-registry.sh --component)" + +overwrite="" +if [[ -n ${OVERWRITE_COMPONENTS:-} ]] && [[ ${OVERWRITE_COMPONENTS} != "false" ]]; then + overwrite="--overwrite" +fi + +echo +echo "> Uploading Component Descriptors to $COMPONENT_REGISTRY ..." +compdir="$("$COMMON_SCRIPT_DIR/get-tmp-dir.sh")/component" +$OCM transfer componentversions "$compdir" "$COMPONENT_REGISTRY" $overwrite | indent 1 diff --git a/hack/common/push-image.sh b/hack/common/push-image.sh new file mode 100755 index 0000000..35814b3 --- /dev/null +++ b/hack/common/push-image.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +if [[ -z ${IMAGE_REGISTRY:-} ]]; then + IMAGE_REGISTRY=$("$COMMON_SCRIPT_DIR/get-registry.sh" -i) +fi + +VERSION=$("$COMMON_SCRIPT_DIR/get-version.sh") + +( + cd "$PROJECT_ROOT" + + echo + echo "> Pushing images to registry $IMAGE_REGISTRY ..." + for comp in ${COMPONENTS//,/ }; do + echo "Pushing image for component '$comp' ..." | indent 1 + img="${IMAGE_REGISTRY}/${comp}:${VERSION}" + for pf in ${PLATFORMS//,/ }; do + os=${pf%/*} + arch=${pf#*/} + pfimg="${img}-${os}-${arch}" + + echo "> Pushing platform-specific image for $pf ..." | indent 2 + docker push "$pfimg" | indent 3 + + echo "> Adding image to multi-platform manifest ..." | indent 2 + docker manifest create $img --amend $pfimg | indent 3 + done + + echo "> Pushing multi-platform manifest ..." + docker manifest push "$img" | indent 2 + + if [[ ${ADDITIONAL_TAG:-} ]]; then + echo "> Adding additional tag '$ADDITIONAL_TAG' ..." + docker buildx imagetools create "$img" --tag "${img%:*}:$ADDITIONAL_TAG" | indent 2 + fi + done +) \ No newline at end of file diff --git a/hack/common/release.sh b/hack/common/release.sh new file mode 100755 index 0000000..35e7e7d --- /dev/null +++ b/hack/common/release.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +VERSION=$("$COMMON_SCRIPT_DIR/get-version.sh") + +( + cd "$PROJECT_ROOT" + + echo "> Checking for uncommitted changes" + if [[ -n "$(git status --porcelain=v1)" ]]; then + echo "There are uncommitted changes in the working directory." + git status --short + echo + echo "These changes will be included in the release commit, unless you stash, commit, or remove them otherwise before." + echo "Do you want to continue, including these changes in the release commit? Please confirm with 'yes' or 'y':" + read confirm + if [[ "$confirm" != "yes" ]] && [[ "$confirm" != "y" ]]; then + echo "Release aborted." + exit 0 + fi + else + echo "No uncommitted changes found." + fi + echo + + echo "> Finding latest release" + major=${VERSION%%.*} + major=${major#v} + minor=${VERSION#*.} + minor=${minor%%.*} + patch=${VERSION##*.} + patch=${patch%%-*} + echo "v${major}.${minor}.${patch}" + echo + + semver=${1:-"minor"} + + case "$semver" in + ("major") + major=$((major + 1)) + minor=0 + patch=0 + ;; + ("minor") + minor=$((minor + 1)) + patch=0 + ;; + ("patch") + patch=$((patch + 1)) + ;; + (*) + echo "invalid argument: $semver" + exit 1 + ;; + esac + + release_version="v$major.$minor.$patch" + + echo "The release version will be $release_version. Please confirm with 'yes' or 'y':" + read confirm + + if [[ "$confirm" != "yes" ]] && [[ "$confirm" != "y" ]]; then + echo "Release not confirmed." + exit 0 + fi + echo + + echo "> Updating version to release version" + "$COMMON_SCRIPT_DIR/set-version.sh" $release_version + echo + + git add --all + git commit -m "release $release_version" + echo + + echo "> Successfully finished" +) \ No newline at end of file diff --git a/hack/common/renovate.json b/hack/common/renovate.json new file mode 100644 index 0000000..2ce1e84 --- /dev/null +++ b/hack/common/renovate.json @@ -0,0 +1,22 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:best-practices", + "security:openssf-scorecard", + ":dependencyDashboard" + ], + "customManagers": [ + { + "description": "Match in Makefile", + "customType": "regex", + "fileMatch": [ + "(^|/|\\.)([Dd]ocker|[Cc]ontainer)file$", + "(^|/)([Dd]ocker|[Cc]ontainer)file[^/]*$", + "(^|/)Makefile$" + ], + "matchStrings": [ + "# renovate: datasource=(?[a-z-.]+?) depName=(?[^\\s]+?)(?: (lookupName|packageName)=(?[^\\s]+?))?(?: versioning=(?[^\\s]+?))?(?: extractVersion=(?[^\\s]+?))?(?: registryUrl=(?[^\\s]+?))?\\s(?:ENV |ARG )?.+?_VERSION ?(?:\\?=|=)\"? ?(?.+?)\"?\\s" + ] + } + ] +} diff --git a/hack/common/set-version.sh b/hack/common/set-version.sh new file mode 100755 index 0000000..5a3a752 --- /dev/null +++ b/hack/common/set-version.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +VERSION=$1 + +GO_MOD_FILE="${PROJECT_ROOT}/go.mod" + +# update VERSION file +echo "$VERSION" > "$PROJECT_ROOT/VERSION" + +for comp in ${COMPONENTS//,/ }; do + CHART_FILE="${PROJECT_ROOT}/charts/${comp}/Chart.yaml" + CHART_VALUES_FILE="${PROJECT_ROOT}/charts/${comp}/values.yaml" + + # update version and image tag in helm charts + sed -E -i -e "s@version: v?[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+.*@version: $VERSION@1" "${CHART_FILE}" + sed -E -i -e "s@appVersion: v?[[:digit:]]+\.[[:digit:]]+\.[[:digit:]]+.*@appVersion: $VERSION@1" "${CHART_FILE}" + sed -i -e "s@ tag: .*@ tag: ${VERSION}@" "${CHART_VALUES_FILE}" + + # remove backup files (created by sed on MacOS) + for file in "${CHART_FILE}" "${CHART_VALUES_FILE}"; do + rm -f "${file}-e" + done +done + +# MODULE_NAME must be set to the local go module name. +# NESTED_MODULES must be set to the list of nested go modules, e.g. 'api,nested2,nested3' +for nm in ${NESTED_MODULES//,/ }; do + # update go.mod + sed -i -e "s@ $MODULE_NAME/$nm .*@ $MODULE_NAME/$nm ${VERSION}@" "${GO_MOD_FILE}" + # remove backup file (created by sed on MacOS) + rm -f "${GO_MOD_FILE}-e" +done diff --git a/hack/common/tidy.sh b/hack/common/tidy.sh new file mode 100755 index 0000000..8a6d46d --- /dev/null +++ b/hack/common/tidy.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +function tidy() { + go mod tidy -e +} + +# NESTED_MODULES must be set to the list of nested go modules, e.g. 'api,nested2,nested3' +for nm in ${NESTED_MODULES//,/ }; do + echo "Tidy $nm module ..." + ( + cd "$PROJECT_ROOT/$nm" + tidy + ) +done + +echo "Tidy root module ..." +( + cd "$PROJECT_ROOT" + tidy +) diff --git a/hack/common/unfold.sh b/hack/common/unfold.sh new file mode 100755 index 0000000..c2620c2 --- /dev/null +++ b/hack/common/unfold.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +set -euo pipefail + +# This is a small helper script that takes a list of paths and unfolds them: +# If the path ends with '/...', the path itself (without '/...') and all of its subfolders are printed. +# Otherwise, only the path is printed. +# +# Paths that don't exist will cause an error. +# +# Options: +# +# --absolute +# If active, converts all paths to absolute paths. Overrides --clean. +# +# --clean +# If active, all paths are printed relative to the working directory, with './' and '../' resolved where possible. +# +# --no-unfold +# If active, does simply remove '/...' suffixes instead of unfolding the corresponding paths. +# +# Note that each option's flag +# - toggles that option between active and inactive (with inactive being the default when no flag for that option is specified) +# - can be used multiple times, toggling the option on and off as described above +# - affects only the paths that are specified after it in the command + +# 'toggle X' flips $X between 'true' and 'false'. +function toggle() { + if eval \$$1; then + eval "$1=false" + else + eval "$1=true" + fi +} + +absolute=false +clean=false +no_unfold=false +for f in "$@"; do + case "$f" in + "--absolute") + toggle absolute + ;; + "--clean") + toggle clean + ;; + "--no-unfold") + toggle no_unfold + ;; + *) + depth_mod="" + if [[ "$f" == */... ]]; then + f="${f%/...}" # cut off '/...' + if $no_unfold; then + depth_mod="-maxdepth 0" + fi + else + depth_mod="-maxdepth 0" + fi + if $absolute; then + f="$(realpath "$f")" + elif $clean; then + f="$(realpath --relative-base="$PWD" "$f")" + fi + if tmp=$(find "$f" $depth_mod -type d 2>&1); then + echo "$tmp" + else + echo "error unfolding path '$f': $tmp" >&2 + exit 1 + fi + ;; + esac +done \ No newline at end of file diff --git a/hack/common/verify-docs-index.sh b/hack/common/verify-docs-index.sh new file mode 100755 index 0000000..4edc398 --- /dev/null +++ b/hack/common/verify-docs-index.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +set -euo pipefail +source "$(realpath "$(dirname $0)/environment.sh")" + +echo "> Checking if documentation index needs changes" +doc_index_file="$PROJECT_ROOT/docs/README.md" +tmp_compare_file=$(mktemp) +"$COMMON_SCRIPT_DIR/generate-docs-index.sh" "$tmp_compare_file" >/dev/null +if ! cmp -s "$doc_index_file" "$tmp_compare_file"; then + echo "The documentation index requires changes." + echo "Please run 'make generate-docs' to update it." + exit 1 +fi +echo "Documentation index is up-to-date." \ No newline at end of file diff --git a/hack/environment.sh b/hack/environment.sh new file mode 100755 index 0000000..3ad42e5 --- /dev/null +++ b/hack/environment.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# this file is sourced automatically by the environment.sh script in the 'common' scripts submodule + +export MODULE_NAME="github.com/openmcp-project/mcp-operator" +export NESTED_MODULES="api" diff --git a/hack/external-apis/apis.yaml b/hack/external-apis/apis.yaml new file mode 100644 index 0000000..1526f8f --- /dev/null +++ b/hack/external-apis/apis.yaml @@ -0,0 +1,86 @@ +apis: + gardener: + # renovate: datasource=github-releases + base: https://raw.githubusercontent.com/gardener/gardener/v1.112.2 + vendor: github.com/gardener/gardener + patches: + - replace: "github.com/gardener/gardener/pkg/apis/core" + with: "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core" + - replace: "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" + with: "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1/constants" + - replace: "github.com/gardener/gardener/pkg/apis/extensions" + with: "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/extensions" + - replace: "github.com/gardener/gardener/pkg/apis/core/v1beta1" + with: "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" + files: + - name: pkg/apis/core/types.go + - name: pkg/apis/core/v1beta1/register.go + patches: + - replace: ", addDefaultingFuncs, addConversionFuncs" + with: "" + - replace: "return RegisterDefaults(scheme)" + with: "return nil" + - name: pkg/apis/authentication/v1alpha1/register.go + patches: + - replace: ", addDefaultingFuncs" + with: "" + - name: pkg/apis/core/v1beta1/constants/types_constants.go + - name: pkg/apis/core/v1beta1/types_backupbucket.go + - name: pkg/apis/core/v1beta1/types_backupentry.go + - name: pkg/apis/core/v1beta1/types_cloudprofile.go + - name: pkg/apis/core/v1beta1/types_common.go + - name: pkg/apis/core/v1beta1/types_controllerdeployment.go + - name: pkg/apis/core/v1beta1/types_controllerinstallation.go + - name: pkg/apis/core/v1beta1/types_controllerregistration.go + - name: pkg/apis/core/v1beta1/types_exposureclass.go + - name: pkg/apis/core/v1beta1/types_internalsecret.go + - name: pkg/apis/core/v1beta1/types_namespacedcloudprofile.go + - name: pkg/apis/core/v1beta1/types_project.go + - name: pkg/apis/core/v1beta1/types_quota.go + - name: pkg/apis/core/v1beta1/types_secretbinding.go + - name: pkg/apis/core/v1beta1/types_seed.go + - name: pkg/apis/core/v1beta1/types_shoot.go + - name: pkg/apis/core/v1beta1/types_shootstate.go + - name: pkg/apis/core/v1beta1/types_utils.go + - name: pkg/apis/core/v1beta1/types.go + - name: pkg/apis/core/v1beta1/zz_generated.deepcopy.go + - name: pkg/apis/authentication/v1alpha1/types_adminkubeconfigrequest.go + - name: pkg/apis/authentication/v1alpha1/types_viewerkubeconfigrequest.go + - name: pkg/apis/authentication/v1alpha1/zz_generated.deepcopy.go + - name: pkg/apis/extensions/register.go + - name: pkg/apis/extensions/v1alpha1/register.go + - name: pkg/apis/extensions/v1alpha1/types.go + - name: pkg/apis/extensions/v1alpha1/types_backupbucket.go + - name: pkg/apis/extensions/v1alpha1/types_backupentry.go + - name: pkg/apis/extensions/v1alpha1/types_bastion.go + - name: pkg/apis/extensions/v1alpha1/types_cluster.go + - name: pkg/apis/extensions/v1alpha1/types_containerruntime.go + - name: pkg/apis/extensions/v1alpha1/types_controlplane.go + - name: pkg/apis/extensions/v1alpha1/types_defaults.go + - name: pkg/apis/extensions/v1alpha1/types_dnsrecord.go + - name: pkg/apis/extensions/v1alpha1/types_extension.go + - name: pkg/apis/extensions/v1alpha1/types_infrastructure.go + - name: pkg/apis/extensions/v1alpha1/types_network.go + - name: pkg/apis/extensions/v1alpha1/types_operatingsystemconfig.go + - name: pkg/apis/extensions/v1alpha1/types_worker.go + - name: pkg/apis/extensions/v1alpha1/zz_generated.deepcopy.go + gardener-extension-provider-aws: + # renovate: datasource=github-releases + base: https://raw.githubusercontent.com/gardener/gardener-extension-provider-aws/v1.60.0 + vendor: github.com/gardener/gardener-extension-provider-aws + patches: + - replace: "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" + with: "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/extensions/v1alpha1" + files: + - name: pkg/apis/aws/v1alpha1/register.go + patches: + - replace: "addDefaultingFuncs, " + with: "" + - replace: "return RegisterDefaults(scheme)" + with: "return nil" + - name: pkg/apis/aws/v1alpha1/types_cloudprofile.go + - name: pkg/apis/aws/v1alpha1/types_controlplane.go + - name: pkg/apis/aws/v1alpha1/types_infrastructure.go + - name: pkg/apis/aws/v1alpha1/types_worker.go + - name: pkg/apis/aws/v1alpha1/zz_generated.deepcopy.go + \ No newline at end of file diff --git a/hack/external-apis/main.go b/hack/external-apis/main.go new file mode 100644 index 0000000..01c5f04 --- /dev/null +++ b/hack/external-apis/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "path" + "path/filepath" + "runtime" + "strings" + + "gopkg.in/yaml.v3" +) + +type Config struct { + APIs map[string]API `yaml:"apis"` +} + +type API struct { + Base string `yaml:"base"` + Patches []Patch `yaml:"patches"` // these patches are applied to all files + Files []File `yaml:"files"` +} + +type File struct { + Name string `yaml:"name"` + Patches []Patch `yaml:"patches"` +} + +type Patch struct { + Replace string `yaml:"replace"` + With string `yaml:"with"` +} + +func main() { + _, mainfilepath, _, _ := runtime.Caller(0) + curPath := path.Dir(mainfilepath) + yamlBytes, err := os.ReadFile(filepath.Join(curPath, "apis.yaml")) + if err != nil { + log.Fatal(err) + } + + externalapisDir := filepath.Join(curPath, "..", "..", "api", "external") + if err := os.RemoveAll(externalapisDir); err != nil { + log.Fatal(err) + } + + config := &Config{} + if err := yaml.Unmarshal(yamlBytes, config); err != nil { + log.Fatal(err) + } + + for name, api := range config.APIs { + for _, file := range api.Files { + destination := path.Join(externalapisDir, name, file.Name) + url := fmt.Sprintf("%s/%s", api.Base, file.Name) + + if err := downloadFile(url, destination); err != nil { + log.Fatal(err) + } + + for _, patch := range file.Patches { + if err := applyPatch(destination, patch); err != nil { + log.Fatal(err) + } + } + for _, patch := range api.Patches { + if err := applyPatch(destination, patch); err != nil { + log.Fatal(err) + } + } + } + } +} + +func downloadFile(url, destination string) error { + destinationDir := filepath.Dir(destination) + if err := os.MkdirAll(destinationDir, 0o755); err != nil { + return err + } + + resp, err := http.Get(url) + if resp != nil { + defer resp.Body.Close() + } + if err != nil { + return err + } + + file, err := os.Create(destination) + if file != nil { + defer file.Close() + } + if err != nil { + return err + } + + _, err = io.Copy(file, resp.Body) + return err +} + +func applyPatch(file string, patch Patch) error { + fileBytes, err := os.ReadFile(file) + if err != nil { + return err + } + + replaced := strings.ReplaceAll(string(fileBytes), patch.Replace, patch.With) + return os.WriteFile(file, []byte(replaced), 0o644) +} diff --git a/internal/components/apiserver.go b/internal/components/apiserver.go new file mode 100644 index 0000000..5c80e22 --- /dev/null +++ b/internal/components/apiserver.go @@ -0,0 +1,59 @@ +package components + +import ( + "fmt" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +// This file contains methods for defaulting, validating, and converting APIServerConfiguration (into APIServerSpec) structs. + +type APIServerConverter struct{} + +var _ Component = &openmcpv1alpha1.APIServer{} +var _ ComponentConverter = &APIServerConverter{} + +// ConvertToResourceSpec implements ComponentConverter. +func (*APIServerConverter) ConvertToResourceSpec(mcp *openmcpv1alpha1.ManagedControlPlane, icfg *openmcpv1alpha1.InternalConfiguration) (any, error) { + apiServerConfig := mcp.Spec.Components.APIServer + if apiServerConfig == nil { + return nil, fmt.Errorf("APIServer configuration is missing") + } + + res := &openmcpv1alpha1.APIServerSpec{ + APIServerConfiguration: *apiServerConfig.DeepCopy(), + } + if icfg != nil { + if icfg.Spec.Components.APIServer != nil { + res.Internal = icfg.Spec.Components.APIServer.DeepCopy() + } + } + + cc, _ := GetCommonConfig(mcp, icfg) + if cc != nil { + res.DesiredRegion = cc.DesiredRegion + } + + res.Default() + if err := res.Validate("spec", "apiserver"); err != nil { + return nil, fmt.Errorf("invalid APIServer configuration: %w", err) + } + + return res, nil +} + +// InjectStatus implements ComponentConverter. +func (*APIServerConverter) InjectStatus(raw any, mcpStatus *openmcpv1alpha1.ManagedControlPlaneStatus) error { + status, ok := raw.(*openmcpv1alpha1.ExternalAPIServerStatus) + if !ok { + return openmcperrors.ErrWrongComponentStatusType + } + mcpStatus.Components.APIServer = status.DeepCopy() + return nil +} + +// IsConfigured implements ComponentConverter. +func (*APIServerConverter) IsConfigured(mcp *openmcpv1alpha1.ManagedControlPlane) bool { + return mcp != nil && mcp.Spec.Components.APIServer != nil +} diff --git a/internal/components/apiserver_test.go b/internal/components/apiserver_test.go new file mode 100644 index 0000000..3d460c1 --- /dev/null +++ b/internal/components/apiserver_test.go @@ -0,0 +1,166 @@ +package components_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openmcp-project/mcp-operator/internal/components" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("APIServerConverter", func() { + Context("ConvertToResourceSpec", func() { + It("should convert the spec", func() { + conv := &components.APIServerConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + APIServer: &openmcpv1alpha1.APIServerConfiguration{ + Type: openmcpv1alpha1.Gardener, + GardenerConfig: &openmcpv1alpha1.GardenerConfiguration{ + Region: "europe", + }, + }, + }, + }, + } + + apiServerSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(apiServerSpec).ToNot(BeNil()) + Expect(apiServerSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.APIServerSpec{})) + + apiServerSpecT := apiServerSpec.(*openmcpv1alpha1.APIServerSpec) + Expect(apiServerSpecT.Type).To(Equal(mcp.Spec.Components.APIServer.Type)) + Expect(apiServerSpecT.GardenerConfig).To(Equal(mcp.Spec.Components.APIServer.GardenerConfig)) + }) + + It("should convert the spec with an internal configuration and common configuration", func() { + conv := &components.APIServerConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + APIServer: &openmcpv1alpha1.APIServerConfiguration{ + Type: openmcpv1alpha1.Gardener, + GardenerConfig: &openmcpv1alpha1.GardenerConfiguration{ + Region: "europe", + }, + }, + }, + CommonConfig: &openmcpv1alpha1.CommonConfig{ + DesiredRegion: &openmcpv1alpha1.RegionSpecification{ + Name: "europe", + Direction: "west", + }, + }, + }, + } + + icfg := &openmcpv1alpha1.InternalConfiguration{ + Spec: openmcpv1alpha1.InternalConfigurationSpec{ + InternalCommonConfig: &openmcpv1alpha1.InternalCommonConfig{}, + Components: openmcpv1alpha1.InternalConfigurationComponents{ + APIServer: &openmcpv1alpha1.APIServerInternalConfiguration{ + GardenerConfig: &openmcpv1alpha1.GardenerInternalConfiguration{ + ShootOverwrite: &openmcpv1alpha1.NamespacedObjectReference{ + Name: "test", + Namespace: "test", + }, + }, + }, + }, + }, + } + + apiServerSpec, err := conv.ConvertToResourceSpec(mcp, icfg) + Expect(err).ToNot(HaveOccurred()) + Expect(apiServerSpec).ToNot(BeNil()) + Expect(apiServerSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.APIServerSpec{})) + + apiServerSpecT := apiServerSpec.(*openmcpv1alpha1.APIServerSpec) + Expect(apiServerSpecT.Type).To(Equal(mcp.Spec.Components.APIServer.Type)) + Expect(apiServerSpecT.GardenerConfig).To(Equal(mcp.Spec.Components.APIServer.GardenerConfig)) + Expect(apiServerSpecT.Internal.GardenerConfig.ShootOverwrite).To(Equal(icfg.Spec.Components.APIServer.GardenerConfig.ShootOverwrite)) + Expect(apiServerSpecT.DesiredRegion).To(Equal(mcp.Spec.CommonConfig.DesiredRegion)) + }) + + It("should return an error if the spec is not configured", func() { + conv := &components.APIServerConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + + apiServerSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).To(HaveOccurred()) + Expect(apiServerSpec).To(BeNil()) + }) + }) + + Context("InjectStatus", func() { + It("should inject the status", func() { + conv := &components.APIServerConverter{} + status := &openmcpv1alpha1.ExternalAPIServerStatus{} + + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + + err := conv.InjectStatus(status, mcpStatus) + Expect(err).ToNot(HaveOccurred()) + Expect(mcpStatus.Components.APIServer).ToNot(BeNil()) + Expect(mcpStatus.Components.APIServer).To(Equal(status)) + }) + + It("should not inject an incompatible status", func() { + conv := &components.APIServerConverter{} + unknownStatus := struct { + Foo string + }{ + Foo: "bar", + } + + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + err := conv.InjectStatus(unknownStatus, mcpStatus) + Expect(err).To(HaveOccurred()) + Expect(mcpStatus.Components.APIServer).To(BeNil()) + }) + }) + + Context("IsConfigured", func() { + It("should return true if the spec is configured", func() { + conv := &components.APIServerConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + APIServer: &openmcpv1alpha1.APIServerConfiguration{ + Type: openmcpv1alpha1.Gardener, + GardenerConfig: &openmcpv1alpha1.GardenerConfiguration{ + Region: "europe", + }, + }, + }, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return false if the spec is not configured", func() { + conv := &components.APIServerConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{} + + Expect(conv.IsConfigured(mcp)).To(BeFalse()) + }) + }) +}) diff --git a/internal/components/authentication.go b/internal/components/authentication.go new file mode 100644 index 0000000..c28c751 --- /dev/null +++ b/internal/components/authentication.go @@ -0,0 +1,47 @@ +package components + +import ( + "fmt" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +type AuthenticationConverter struct{} + +var _ Component = &openmcpv1alpha1.Authentication{} +var _ ComponentConverter = &AuthenticationConverter{} + +// ConvertToResourceSpec implements ComponentConverter. +func (ac *AuthenticationConverter) ConvertToResourceSpec(mcp *openmcpv1alpha1.ManagedControlPlane, _ *openmcpv1alpha1.InternalConfiguration) (any, error) { + acConfig := mcp.Spec.Authentication + if acConfig == nil { + acConfig = &openmcpv1alpha1.AuthenticationConfiguration{} + } + + res := &openmcpv1alpha1.AuthenticationSpec{ + AuthenticationConfiguration: *acConfig.DeepCopy(), + } + + res.Default() + if err := res.Validate("spec", "authentication"); err != nil { + return nil, fmt.Errorf("invalid Authentication configuration: %w", err) + } + + return res, nil +} + +// InjectStatus implements ComponentConverter. +func (ac *AuthenticationConverter) InjectStatus(raw any, mcpStatus *openmcpv1alpha1.ManagedControlPlaneStatus) error { + status, ok := raw.(*openmcpv1alpha1.ExternalAuthenticationStatus) + if !ok { + return openmcperrors.ErrWrongComponentStatusType + } + mcpStatus.Components.Authentication = status.DeepCopy() + return nil +} + +// IsConfigured implements ComponentConverter. +func (ac *AuthenticationConverter) IsConfigured(mcp *openmcpv1alpha1.ManagedControlPlane) bool { + return mcp != nil && (mcp.Spec.Authentication != nil || mcp.Spec.Components.APIServer != nil) +} diff --git a/internal/components/authentication_test.go b/internal/components/authentication_test.go new file mode 100644 index 0000000..1a055dc --- /dev/null +++ b/internal/components/authentication_test.go @@ -0,0 +1,160 @@ +package components_test + +import ( + "reflect" + + "github.com/openmcp-project/mcp-operator/internal/components" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("AuthenticationConverter", func() { + Context("ConvertToResourceSpec", func() { + It("should convert the spec", func() { + conv := &components.AuthenticationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Authentication: &openmcpv1alpha1.AuthenticationConfiguration{ + EnableSystemIdentityProvider: ptr.To(false), + IdentityProviders: []openmcpv1alpha1.IdentityProvider{ + { + Name: "test", + IssuerURL: "https://test", + ClientID: "aaa-bbb-ccc", + GroupsClaim: "grps1", + UsernameClaim: "usr1", + }, + }, + }, + }, + } + + authSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(authSpec).ToNot(BeNil()) + Expect(authSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.AuthenticationSpec{})) + + authSpecT := authSpec.(*openmcpv1alpha1.AuthenticationSpec) + Expect(reflect.DeepEqual(authSpecT.AuthenticationConfiguration, *mcp.Spec.Authentication)).To(BeTrue()) + }) + + It("should convert the spec with default values", func() { + conv := &components.AuthenticationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + + authSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(authSpec).ToNot(BeNil()) + Expect(authSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.AuthenticationSpec{})) + + authSpecT := authSpec.(*openmcpv1alpha1.AuthenticationSpec) + Expect(authSpecT.AuthenticationConfiguration.EnableSystemIdentityProvider).To(Equal(ptr.To(true))) + Expect(authSpecT.AuthenticationConfiguration.IdentityProviders).To(BeEmpty()) + }) + + It("should not covert an invalid spec", func() { + conv := &components.AuthenticationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Authentication: &openmcpv1alpha1.AuthenticationConfiguration{ + EnableSystemIdentityProvider: ptr.To(false), + IdentityProviders: []openmcpv1alpha1.IdentityProvider{ + { + Name: "test", + IssuerURL: "https://test", + ClientID: "aaa-bbb-ccc", + GroupsClaim: "grps1", + UsernameClaim: "usr1", + }, + { + Name: "test", + IssuerURL: "https://test1", + ClientID: "aaa-bbb-ccc", + GroupsClaim: "grps2", + UsernameClaim: "usr2", + }, + }, + }, + }, + } + + authSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).To(HaveOccurred()) + Expect(authSpec).To(BeNil()) + }) + }) + + Context("InjectStatus", func() { + It("should inject the status", func() { + conv := &components.AuthenticationConverter{} + status := &openmcpv1alpha1.ExternalAuthenticationStatus{ + UserAccess: &openmcpv1alpha1.SecretReference{ + NamespacedObjectReference: openmcpv1alpha1.NamespacedObjectReference{ + Name: "test", + Namespace: "test", + }, + Key: "access", + }, + } + + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + + err := conv.InjectStatus(status, mcpStatus) + Expect(err).ToNot(HaveOccurred()) + Expect(mcpStatus.Components.Authentication).ToNot(BeNil()) + Expect(mcpStatus.Components.Authentication).To(Equal(status)) + }) + + It("should not inject an incompatible status", func() { + conv := &components.AuthenticationConverter{} + unknownStatus := struct { + Foo string + }{ + Foo: "bar", + } + + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + err := conv.InjectStatus(unknownStatus, mcpStatus) + Expect(err).To(HaveOccurred()) + Expect(mcpStatus.Components.Authentication).To(BeNil()) + }) + }) + + Context("IsConfigured", func() { + It("should return true if the spec is configured", func() { + conv := &components.AuthenticationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Authentication: &openmcpv1alpha1.AuthenticationConfiguration{}, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return false if the spec is not configured", func() { + conv := &components.AuthenticationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{} + + Expect(conv.IsConfigured(mcp)).To(BeFalse()) + }) + }) +}) diff --git a/internal/components/authorization.go b/internal/components/authorization.go new file mode 100644 index 0000000..ba90664 --- /dev/null +++ b/internal/components/authorization.go @@ -0,0 +1,47 @@ +package components + +import ( + "fmt" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +type AuthorizationConverter struct{} + +var _ Component = &openmcpv1alpha1.Authorization{} +var _ ComponentConverter = &AuthorizationConverter{} + +// ConvertToResourceSpec implements ComponentConverter. +func (ac *AuthorizationConverter) ConvertToResourceSpec(mcp *openmcpv1alpha1.ManagedControlPlane, _ *openmcpv1alpha1.InternalConfiguration) (any, error) { + acConfig := mcp.Spec.Authorization + if acConfig == nil { + return nil, fmt.Errorf("authorization configuration is missing") + } + + res := &openmcpv1alpha1.AuthorizationSpec{ + AuthorizationConfiguration: *acConfig.DeepCopy(), + } + + res.Default() + if err := res.Validate("spec", "authorization"); err != nil { + return nil, fmt.Errorf("invalid Authorization configuration: %w", err) + } + + return res, nil +} + +// InjectStatus implements ComponentConverter. +func (ac *AuthorizationConverter) InjectStatus(raw any, mcpStatus *openmcpv1alpha1.ManagedControlPlaneStatus) error { + status, ok := raw.(*openmcpv1alpha1.ExternalAuthorizationStatus) + if !ok { + return openmcperrors.ErrWrongComponentStatusType + } + mcpStatus.Components.Authorization = status.DeepCopy() + return nil +} + +// IsConfigured implements ComponentConverter. +func (ac *AuthorizationConverter) IsConfigured(mcp *openmcpv1alpha1.ManagedControlPlane) bool { + return mcp != nil && mcp.Spec.Authorization != nil +} diff --git a/internal/components/authorization_test.go b/internal/components/authorization_test.go new file mode 100644 index 0000000..9e0c7f6 --- /dev/null +++ b/internal/components/authorization_test.go @@ -0,0 +1,190 @@ +package components_test + +import ( + "reflect" + + "github.com/openmcp-project/mcp-operator/internal/components" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("AuthorizationConverter", func() { + Context("ConvertToResourceSpec", func() { + It("should convert the spec", func() { + conv := &components.AuthorizationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Authorization: &openmcpv1alpha1.AuthorizationConfiguration{ + RoleBindings: []openmcpv1alpha1.RoleBinding{ + { + + Role: "admin", + Subjects: []openmcpv1alpha1.Subject{ + { + APIGroup: rbacv1.GroupName, + Kind: "User", + Name: "admin", + }, + }, + }, + }, + }, + }, + } + + authSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(authSpec).ToNot(BeNil()) + Expect(authSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.AuthorizationSpec{})) + + authSpecT := authSpec.(*openmcpv1alpha1.AuthorizationSpec) + Expect(reflect.DeepEqual(authSpecT.AuthorizationConfiguration, *mcp.Spec.Authorization)).To(BeTrue()) + }) + + It("should return an error if the spec is not configured", func() { + conv := &components.AuthorizationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + + authSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).To(HaveOccurred()) + Expect(authSpec).To(BeNil()) + }) + + It("should convert the spec with default values", func() { + conv := &components.AuthorizationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Authorization: &openmcpv1alpha1.AuthorizationConfiguration{ + RoleBindings: []openmcpv1alpha1.RoleBinding{ + { + Role: "admin", + Subjects: []openmcpv1alpha1.Subject{ + { + Kind: "User", + Name: "test", + }, + { + Kind: "Group", + Name: "admin", + }, + }, + }, + { + Role: "view", + Subjects: []openmcpv1alpha1.Subject{ + { + Kind: "ServiceAccount", + Name: "automate", + Namespace: "default", + }, + }, + }, + }, + }, + }, + } + + authSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(authSpec).ToNot(BeNil()) + Expect(authSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.AuthorizationSpec{})) + + authSpecT := authSpec.(*openmcpv1alpha1.AuthorizationSpec) + Expect(authSpecT.RoleBindings[0].Subjects[0].APIGroup).To(Equal(rbacv1.GroupName)) + Expect(authSpecT.RoleBindings[0].Subjects[1].APIGroup).To(Equal(rbacv1.GroupName)) + + }) + + It("should not covert an invalid spec", func() { + conv := &components.AuthorizationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Authorization: &openmcpv1alpha1.AuthorizationConfiguration{ + RoleBindings: []openmcpv1alpha1.RoleBinding{ + { + Role: "invalid", + Subjects: []openmcpv1alpha1.Subject{ + { + Kind: "invalid", + }, + }, + }, + }, + }, + }, + } + + authSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).To(HaveOccurred()) + Expect(authSpec).To(BeNil()) + }) + }) + + Context("InjectStatus", func() { + It("should inject the status", func() { + conv := &components.AuthorizationConverter{} + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + status := &openmcpv1alpha1.ExternalAuthorizationStatus{} + + err := conv.InjectStatus(status, mcpStatus) + Expect(err).ToNot(HaveOccurred()) + Expect(mcpStatus.Components.Authorization).To(Equal(status)) + }) + + It("should fail to inject the status", func() { + conv := &components.AuthorizationConverter{} + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + unknownStatus := struct { + Foo string + }{ + Foo: "bar", + } + + err := conv.InjectStatus(unknownStatus, mcpStatus) + Expect(err).To(HaveOccurred()) + Expect(mcpStatus.Components.Authorization).To(BeNil()) + }) + }) + + Context("IsConfigured", func() { + It("should return true if the spec is configured", func() { + conv := &components.AuthorizationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Authorization: &openmcpv1alpha1.AuthorizationConfiguration{}, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return false if the spec is not configured", func() { + conv := &components.AuthorizationConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{} + + Expect(conv.IsConfigured(mcp)).To(BeFalse()) + }) + }) +}) diff --git a/internal/components/cloudorchestrator.go b/internal/components/cloudorchestrator.go new file mode 100644 index 0000000..7f01431 --- /dev/null +++ b/internal/components/cloudorchestrator.go @@ -0,0 +1,46 @@ +package components + +import ( + "fmt" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +// This file contains methods for defaulting, validating, and converting CloudOrchestratorConfiguration (into CloudOrchestratorSpec) structs. + +var _ Component = &openmcpv1alpha1.CloudOrchestrator{} +var _ ComponentConverter = &CloudOrchestratorConverter{} + +// +kubebuilder:object:generate=false +type CloudOrchestratorConverter struct{} + +// ConvertToResourceSpec implements ComponentConverter. +func (*CloudOrchestratorConverter) ConvertToResourceSpec(mcp *openmcpv1alpha1.ManagedControlPlane, _ *openmcpv1alpha1.InternalConfiguration) (any, error) { + coCfg := mcp.Spec.Components.CloudOrchestratorConfiguration + res := &openmcpv1alpha1.CloudOrchestratorSpec{ + CloudOrchestratorConfiguration: *coCfg.DeepCopy(), + } + + res.Default() + if err := res.Validate("spec", "cloudorchestrator"); err != nil { + return nil, fmt.Errorf("invalid CloudOrchestrator configuration: %w", err) + } + + return res, nil +} + +// InjectStatus implements ComponentConverter. +func (*CloudOrchestratorConverter) InjectStatus(raw any, mcpStatus *openmcpv1alpha1.ManagedControlPlaneStatus) error { + status, ok := raw.(*openmcpv1alpha1.ExternalCloudOrchestratorStatus) + if !ok { + return openmcperrors.ErrWrongComponentStatusType + } + mcpStatus.Components.CloudOrchestrator = status.DeepCopy() + return nil +} + +// IsConfigured implements ComponentConverter. +func (*CloudOrchestratorConverter) IsConfigured(mcp *openmcpv1alpha1.ManagedControlPlane) bool { + return mcp != nil && (mcp.Spec.Components.Crossplane != nil || mcp.Spec.Components.BTPServiceOperator != nil || mcp.Spec.Components.ExternalSecretsOperator != nil || mcp.Spec.Components.Kyverno != nil || mcp.Spec.Components.Flux != nil) +} diff --git a/internal/components/cloudorchestrator_test.go b/internal/components/cloudorchestrator_test.go new file mode 100644 index 0000000..323e253 --- /dev/null +++ b/internal/components/cloudorchestrator_test.go @@ -0,0 +1,190 @@ +package components_test + +import ( + "reflect" + + "github.com/openmcp-project/mcp-operator/internal/components" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("CloudOrchestratorConverter", func() { + Context("ConvertToResourceSpec", func() { + It("should convert the spec", func() { + conv := &components.CloudOrchestratorConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{ + Crossplane: &openmcpv1alpha1.CrossplaneConfig{ + Version: "v1", + Providers: []*openmcpv1alpha1.CrossplaneProviderConfig{ + { + Name: "foo", + Version: "v2.1.0", + }, + }, + }, + BTPServiceOperator: nil, + ExternalSecretsOperator: nil, + Kyverno: nil, + Flux: nil, + }, + }, + }, + } + + authSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(authSpec).ToNot(BeNil()) + Expect(authSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.CloudOrchestratorSpec{})) + + coSpecT := authSpec.(*openmcpv1alpha1.CloudOrchestratorSpec) + Expect(reflect.DeepEqual(coSpecT.Crossplane, mcp.Spec.Components.Crossplane)).To(BeTrue()) + }) + + It("should convert the spec with default values", func() { + conv := &components.CloudOrchestratorConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + + coSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(coSpec).ToNot(BeNil()) + + coSpecT := coSpec.(*openmcpv1alpha1.CloudOrchestratorSpec) + Expect(coSpecT.Crossplane).To(BeNil()) + Expect(coSpecT.BTPServiceOperator).To(BeNil()) + Expect(coSpecT.ExternalSecretsOperator).To(BeNil()) + Expect(coSpecT.Kyverno).To(BeNil()) + Expect(coSpecT.Flux).To(BeNil()) + }) + }) + + Context("InjectStatus", func() { + It("should inject the status", func() { + conv := &components.CloudOrchestratorConverter{} + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + status := &openmcpv1alpha1.ExternalCloudOrchestratorStatus{} + + err := conv.InjectStatus(status, mcpStatus) + Expect(err).ToNot(HaveOccurred()) + Expect(mcpStatus.Components.CloudOrchestrator).To(Equal(status)) + }) + + It("should fail to inject the status", func() { + conv := &components.CloudOrchestratorConverter{} + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + unknownStatus := struct { + Foo string + }{ + Foo: "bar", + } + + err := conv.InjectStatus(unknownStatus, mcpStatus) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("IsConfigured", func() { + It("should return true if the crossplane configured", func() { + conv := &components.CloudOrchestratorConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{ + Crossplane: &openmcpv1alpha1.CrossplaneConfig{}, + }, + }, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return true if the BTPServiceOperator configured", func() { + conv := &components.CloudOrchestratorConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{ + BTPServiceOperator: &openmcpv1alpha1.BTPServiceOperatorConfig{}, + }, + }, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return true if the ExternalSecretsOperator configured", func() { + conv := &components.CloudOrchestratorConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{ + ExternalSecretsOperator: &openmcpv1alpha1.ExternalSecretsOperatorConfig{}, + }, + }, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return true if the Kyverno configured", func() { + conv := &components.CloudOrchestratorConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{ + Kyverno: &openmcpv1alpha1.KyvernoConfig{}, + }, + }, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return true if the Flux configured", func() { + conv := &components.CloudOrchestratorConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{ + Flux: &openmcpv1alpha1.FluxConfig{}, + }, + }, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return false no component is configured", func() { + conv := &components.CloudOrchestratorConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{}, + }, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeFalse()) + }) + }) +}) diff --git a/internal/components/component.go b/internal/components/component.go new file mode 100644 index 0000000..2cb7ea3 --- /dev/null +++ b/internal/components/component.go @@ -0,0 +1,56 @@ +package components + +import ( + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// Component is a helper interface which must be implemented by all component-specific in-cluster resources. +// It inherits client.Object, so it can be used instead of client.Object for the component-specific resources. +type Component interface { + client.Object + + // Type returns the type of this component. + Type() openmcpv1alpha1.ComponentType + + // GetSpec returns a pointer to the spec of the component. + GetSpec() any + + // SetSpec is used by the ManagedControlPlane controller to pass the configuration from the ManagedControlPlane's spec into the spec of the component resource. + // Returns ErrWrongComponentConfigType if cfg cannot be cast to the correct type to put into the component resource's spec. + // This function expects cfg to be a pointer to the component's spec. + SetSpec(cfg any) error + + // GetCommonStatus returns the part of the component's status that all components have in common. + GetCommonStatus() openmcpv1alpha1.CommonComponentStatus + + // SetCommonStatus is used to update the common status of the component. + SetCommonStatus(status openmcpv1alpha1.CommonComponentStatus) + + // GetExternalStatus returns a pointer to the component's external status. + // This is used by the ManagedControlPlane controller to fetch the component's status and put it into the ManagedControlPlane's status. + // The returned value will be fed into the corresponding ComponentConverter's InjectStatus method. + GetExternalStatus() any + + // GetRequiredConditions returns a set of types of conditions that are expected to be present in the component's status. + // This set can be static, but it can also depend on the component's spec. + // All condition types that are returned by this method but don't have a matching condition in the component's status will be added with status 'Unknown' (leading to an unhealthy MCP). + // Additional conditions in the component's status that are not in this list will still be propagated to the MCP's status (think of this as a set of minimal required conditions). + GetRequiredConditions() sets.Set[string] +} + +// GetCommonConfig takes the same arguments as the ConvertToResourceSpec function and returns the common configuration for ManagedControlPlane and InternalConfiguration. +// Both return values may be nil if no common configuration exists. +func GetCommonConfig(mcp *openmcpv1alpha1.ManagedControlPlane, icfg *openmcpv1alpha1.InternalConfiguration) (*openmcpv1alpha1.CommonConfig, *openmcpv1alpha1.InternalCommonConfig) { + var cc *openmcpv1alpha1.CommonConfig + if mcp != nil { + cc = mcp.Spec.CommonConfig + } + var icc *openmcpv1alpha1.InternalCommonConfig + if icfg != nil { + icc = icfg.Spec.InternalCommonConfig + } + return cc, icc +} diff --git a/internal/components/component_test.go b/internal/components/component_test.go new file mode 100644 index 0000000..5c46c45 --- /dev/null +++ b/internal/components/component_test.go @@ -0,0 +1,45 @@ +package components_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/openmcp-project/mcp-operator/internal/components" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("Components", func() { + Context("GetCommonConfig", func() { + It("should convert the spec", func() { + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + CommonConfig: &openmcpv1alpha1.CommonConfig{ + DesiredRegion: &openmcpv1alpha1.RegionSpecification{ + Name: "europe", + Direction: "west", + }, + }, + }, + } + icfg := &openmcpv1alpha1.InternalConfiguration{ + Spec: openmcpv1alpha1.InternalConfigurationSpec{ + InternalCommonConfig: &openmcpv1alpha1.InternalCommonConfig{}, + }, + } + commonCfg, iCommonConfig := components.GetCommonConfig(mcp, icfg) + Expect(commonCfg).ToNot(BeNil()) + Expect(iCommonConfig).ToNot(BeNil()) + Expect(commonCfg.DesiredRegion).To(Equal(mcp.Spec.CommonConfig.DesiredRegion)) + Expect(iCommonConfig).To(Equal(icfg.Spec.InternalCommonConfig)) + }) + + It("should return nil if the spec is not configured", func() { + mcp := &openmcpv1alpha1.ManagedControlPlane{} + icfg := &openmcpv1alpha1.InternalConfiguration{} + commonCfg, iCommonConfig := components.GetCommonConfig(mcp, icfg) + Expect(commonCfg).To(BeNil()) + Expect(iCommonConfig).To(BeNil()) + }) + }) +}) diff --git a/internal/components/converter.go b/internal/components/converter.go new file mode 100644 index 0000000..64fc11d --- /dev/null +++ b/internal/components/converter.go @@ -0,0 +1,20 @@ +package components + +import ( + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// ComponentConverter contains functions which require knowledge about how the component is configured in the ManagedControlPlane. +type ComponentConverter interface { + // ConvertToResourceSpec converts the ManagedControlPlane spec and a potential InternalConfiguration into the component resource's spec. + // The result of this function will be fed into the SetSpec function by the ManagedControlPlane controller. + ConvertToResourceSpec(mcp *openmcpv1alpha1.ManagedControlPlane, ic *openmcpv1alpha1.InternalConfiguration) (any, error) + + // IsConfigured returns true if the given ManagedControlPlane contains configuration for this component. + IsConfigured(mcp *openmcpv1alpha1.ManagedControlPlane) bool + + // InjectStatus injects the external status of this component into the ManagedControlPlane's status. + // It must only modify the fields that are specific to this component, excluding conditions and observed generations. + // Should throw an ErrWrongComponentStatusType error if the given object cannot be converted into the type specified in the ManagedControlPlane's status. + InjectStatus(comp any, mcpStatus *openmcpv1alpha1.ManagedControlPlaneStatus) error +} diff --git a/internal/components/landscaper.go b/internal/components/landscaper.go new file mode 100644 index 0000000..9dfaa2d --- /dev/null +++ b/internal/components/landscaper.go @@ -0,0 +1,49 @@ +package components + +import ( + "fmt" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +// This file contains methods for defaulting, validating, and converting LandscaperConfiguration (into LandscaperSpec) structs. + +type LandscaperConverter struct{} + +var _ Component = &openmcpv1alpha1.Landscaper{} +var _ ComponentConverter = &LandscaperConverter{} + +// ConvertToResourceSpec implements ComponentConverter. +func (*LandscaperConverter) ConvertToResourceSpec(mcp *openmcpv1alpha1.ManagedControlPlane, _ *openmcpv1alpha1.InternalConfiguration) (any, error) { + lcConfig := mcp.Spec.Components.Landscaper + if lcConfig == nil { + return nil, fmt.Errorf("landscaper configuration is missing") + } + + res := &openmcpv1alpha1.LandscaperSpec{ + LandscaperConfiguration: *lcConfig.DeepCopy(), + } + + res.Default() + if err := res.Validate("spec", "landscaper"); err != nil { + return nil, fmt.Errorf("invalid Landscaper configuration: %w", err) + } + + return res, nil +} + +// InjectStatus implements ComponentConverter. +func (*LandscaperConverter) InjectStatus(raw any, mcpStatus *openmcpv1alpha1.ManagedControlPlaneStatus) error { + status, ok := raw.(*openmcpv1alpha1.ExternalLandscaperStatus) + if !ok { + return openmcperrors.ErrWrongComponentStatusType + } + mcpStatus.Components.Landscaper = status.DeepCopy() + return nil +} + +// IsConfigured implements ComponentConverter. +func (*LandscaperConverter) IsConfigured(mcp *openmcpv1alpha1.ManagedControlPlane) bool { + return mcp != nil && mcp.Spec.Components.Landscaper != nil +} diff --git a/internal/components/landscaper_test.go b/internal/components/landscaper_test.go new file mode 100644 index 0000000..b130be3 --- /dev/null +++ b/internal/components/landscaper_test.go @@ -0,0 +1,127 @@ +package components_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openmcp-project/mcp-operator/internal/components" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("LandscaperConverter", func() { + Context("ConvertToResourceSpec", func() { + It("should convert the spec", func() { + conv := &components.LandscaperConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + Landscaper: &openmcpv1alpha1.LandscaperConfiguration{ + Deployers: []string{ + "manifest", + "helm", + }, + }, + }, + }, + } + + lsSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(lsSpec).ToNot(BeNil()) + Expect(lsSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.LandscaperSpec{})) + + lsSpecT := lsSpec.(*openmcpv1alpha1.LandscaperSpec) + Expect(lsSpecT.Deployers).To(Equal(mcp.Spec.Components.Landscaper.Deployers)) + }) + + It("should convert the spec with default values", func() { + conv := &components.LandscaperConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + Landscaper: &openmcpv1alpha1.LandscaperConfiguration{}, + }, + }, + } + + lsSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(lsSpec).ToNot(BeNil()) + Expect(lsSpec).To(BeAssignableToTypeOf(&openmcpv1alpha1.LandscaperSpec{})) + + lsSpecT := lsSpec.(*openmcpv1alpha1.LandscaperSpec) + Expect(lsSpecT.Deployers).To(ConsistOf("helm", "manifest", "container")) + }) + + It("should return an error if the spec is not configured", func() { + conv := &components.LandscaperConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + + lsSpec, err := conv.ConvertToResourceSpec(mcp, nil) + Expect(err).To(HaveOccurred()) + Expect(lsSpec).To(BeNil()) + }) + }) + + Context("InjectStatus", func() { + It("should inject the status", func() { + conv := &components.LandscaperConverter{} + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + status := &openmcpv1alpha1.ExternalLandscaperStatus{} + + err := conv.InjectStatus(status, mcpStatus) + Expect(err).ToNot(HaveOccurred()) + Expect(mcpStatus.Components.Landscaper).To(Equal(status)) + }) + + It("should not inject an incompatible status", func() { + conv := &components.LandscaperConverter{} + unknownStatus := struct { + Foo string + }{ + Foo: "bar", + } + + mcpStatus := &openmcpv1alpha1.ManagedControlPlaneStatus{} + err := conv.InjectStatus(unknownStatus, mcpStatus) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("IsConfigured", func() { + It("should return true if the spec is configured", func() { + conv := &components.LandscaperConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{ + Spec: openmcpv1alpha1.ManagedControlPlaneSpec{ + Components: openmcpv1alpha1.ManagedControlPlaneComponents{ + Landscaper: &openmcpv1alpha1.LandscaperConfiguration{}, + }, + }, + } + + Expect(conv.IsConfigured(mcp)).To(BeTrue()) + }) + + It("should return false if the spec is not configured", func() { + conv := &components.LandscaperConverter{} + mcp := &openmcpv1alpha1.ManagedControlPlane{} + + Expect(conv.IsConfigured(mcp)).To(BeFalse()) + }) + }) +}) diff --git a/internal/components/registered_components.go b/internal/components/registered_components.go new file mode 100644 index 0000000..e8119e5 --- /dev/null +++ b/internal/components/registered_components.go @@ -0,0 +1,194 @@ +package components + +import ( + apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcpinstall "github.com/openmcp-project/mcp-operator/api/install" +) + +const ( + LandscaperNamespaceScopedAdminMatchLabel = "rbac.landscaper.gardener.cloud/aggregate-to-admin" + LandscaperNamespaceScopedViewMatchLabel = "rbac.landscaper.gardener.cloud/aggregate-to-view" + CrossPlaneClusterScopedAdminMatchLabel = "rbac.crossplane.io/aggregate-to-admin" + CrossPlaneClusterScopedViewMatchLabel = "rbac.crossplane.io/aggregate-to-view" + CloudOrchestratorClusterScopedAdminMatchLabel = "core.orchestrate.cloud.sap/aggregate-to-admin" + CloudOrchestratorClusterScopedViewMatchLabel = "core.orchestrate.cloud.sap/aggregate-to-view" + MatchLabelValue = "true" +) + +var Registry *registry + +func init() { + colaScheme := runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(colaScheme)) + utilruntime.Must(apiextv1.AddToScheme(colaScheme)) + openmcpinstall.Install(colaScheme) + + Registry = newRegistry(colaScheme) + Registry.Register(openmcpv1alpha1.APIServerComponent, func() *ComponentHandler { + return NewComponentHandler(&openmcpv1alpha1.APIServer{}, &APIServerConverter{}, nil) + }) + Registry.Register(openmcpv1alpha1.LandscaperComponent, func() *ComponentHandler { + return NewComponentHandler(&openmcpv1alpha1.Landscaper{}, &LandscaperConverter{}, func(roleName string) []metav1.LabelSelector { + if openmcpv1alpha1.IsClusterScopedRole(roleName) { + // Landscaper uses only namespace-scoped resources + return nil + } + if openmcpv1alpha1.IsAdminRole(roleName) { + return []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + LandscaperNamespaceScopedAdminMatchLabel: MatchLabelValue, + }, + }, + } + } + return []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + LandscaperNamespaceScopedViewMatchLabel: MatchLabelValue, + }, + }, + } + }) + }) + Registry.Register(openmcpv1alpha1.CloudOrchestratorComponent, func() *ComponentHandler { + return NewComponentHandler(&openmcpv1alpha1.CloudOrchestrator{}, &CloudOrchestratorConverter{}, func(roleName string) []metav1.LabelSelector { + if openmcpv1alpha1.IsClusterScopedRole(roleName) { + if openmcpv1alpha1.IsAdminRole(roleName) { + return []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + CrossPlaneClusterScopedAdminMatchLabel: MatchLabelValue, // Crossplane admin role + }, + }, + { + MatchLabels: map[string]string{ + CloudOrchestratorClusterScopedAdminMatchLabel: MatchLabelValue, // CO Components admin role + }, + }, + } + } + return []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + CrossPlaneClusterScopedViewMatchLabel: MatchLabelValue, // Crossplane view role + }, + }, + { + MatchLabels: map[string]string{ + CloudOrchestratorClusterScopedViewMatchLabel: MatchLabelValue, // CO Components view role + }, + }, + } + } + return nil + }) + }) + Registry.Register(openmcpv1alpha1.AuthenticationComponent, func() *ComponentHandler { + return NewComponentHandler(&openmcpv1alpha1.Authentication{}, &AuthenticationConverter{}, nil) + }) + Registry.Register(openmcpv1alpha1.AuthorizationComponent, func() *ComponentHandler { + return NewComponentHandler(&openmcpv1alpha1.Authorization{}, &AuthorizationConverter{}, nil) + }) + + // add new components here + + // Note that the function argument must be an anonymous function and not be wrapped within a NewComponentHandlerFn function or similar, + // otherwise repeated calls to Registry.GetKnownComponents() will return pointers to the same instance of the resource struct, which breaks the ManagedControlPlane controller's logic! + // The whole idea behind registering a function instead of just a fixed ComponentHandler is that the registry will always return new ComponentHandlers, never the same one as returned before. +} + +var _ ComponentRegistry[*ComponentHandler] = ®istry{} +var _ ManagedComponent = &ComponentHandler{} + +// Arguments: +// - obj is an empty version of the in-cluster resource for this component +// - conv is the components ComponentConverter +// - aggregationLabelSelectorFunc is a function that gets a name of a (Cluster)Role and returns the LabelSelectors that should be added to that role's aggregation rules, if any. +// This is used by the Authorization component to grant end-users permissions for the component's resources on the API Server. +// If a component has custom resources on the API Server which the end-user has to interact with, the component itself should deploy corresponding (Cluster)Rules with aggregation labels +// and return the fitting selectors via this function here. +func NewComponentHandler(obj Component, conv ComponentConverter, aggregationLabelSelectorFunc func(string) []metav1.LabelSelector) *ComponentHandler { + return &ComponentHandler{ + resource: obj, + converter: conv, + aggregationLabelSelectorFunc: aggregationLabelSelectorFunc, + } +} + +type ComponentHandler struct { + resource Component + converter ComponentConverter + aggregationLabelSelectorFunc func(string) []metav1.LabelSelector +} + +// Resource implements v1alpha1.ComponentResourceGetter. +func (ch *ComponentHandler) Resource() Component { + return ch.resource +} + +func (ch *ComponentHandler) Converter() ComponentConverter { + return ch.converter +} + +func (ch *ComponentHandler) LabelSelectorsForRole(roleName string) []metav1.LabelSelector { + if ch.aggregationLabelSelectorFunc == nil { + return nil + } + return ch.aggregationLabelSelectorFunc(roleName) +} + +type registry struct { + reg map[openmcpv1alpha1.ComponentType]func() *ComponentHandler + sc *runtime.Scheme +} + +func newRegistry(baseScheme *runtime.Scheme) *registry { + return ®istry{ + reg: map[openmcpv1alpha1.ComponentType]func() *ComponentHandler{}, + sc: baseScheme, + } +} + +// GetComponent implements ComponentRegistry. +func (r *registry) GetComponent(ct openmcpv1alpha1.ComponentType) *ComponentHandler { + if chp, ok := r.reg[ct]; ok { + return chp() + } + return nil +} + +// GetKnownComponents implements ComponentRegistry. +func (r *registry) GetKnownComponents() map[openmcpv1alpha1.ComponentType]*ComponentHandler { + res := make(map[openmcpv1alpha1.ComponentType]*ComponentHandler, len(r.reg)) + for ct, chp := range r.reg { + res[ct] = chp() + } + return res +} + +// Has implements ComponentRegistry. +func (r *registry) Has(ct openmcpv1alpha1.ComponentType) bool { + _, ok := r.reg[ct] + return ok +} + +// Register implements ComponentRegistry. +func (r *registry) Register(ct openmcpv1alpha1.ComponentType, provideCh func() *ComponentHandler) { + if provideCh == nil { + delete(r.reg, ct) + return + } + r.reg[ct] = provideCh +} + +// Scheme returns the scheme of the Registry. +func (r *registry) Scheme() *runtime.Scheme { + return r.sc +} diff --git a/internal/components/registered_components_test.go b/internal/components/registered_components_test.go new file mode 100644 index 0000000..9d04409 --- /dev/null +++ b/internal/components/registered_components_test.go @@ -0,0 +1,286 @@ +package components_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + components "github.com/openmcp-project/mcp-operator/internal/components" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("ComponentHandler", func() { + Context("Registry", func() { + It("should return the known components", func() { + components := components.Registry.GetKnownComponents() + Expect(components).ToNot(BeNil()) + Expect(components).To(HaveLen(5)) + Expect(components).To(HaveKey(openmcpv1alpha1.AuthenticationComponent)) + Expect(components).To(HaveKey(openmcpv1alpha1.AuthorizationComponent)) + Expect(components).To(HaveKey(openmcpv1alpha1.APIServerComponent)) + Expect(components).To(HaveKey(openmcpv1alpha1.CloudOrchestratorComponent)) + Expect(components).To(HaveKey(openmcpv1alpha1.LandscaperComponent)) + }) + + It("should return true for known components", func() { + Expect(components.Registry.Has(openmcpv1alpha1.AuthenticationComponent)).To(BeTrue()) + Expect(components.Registry.Has(openmcpv1alpha1.AuthorizationComponent)).To(BeTrue()) + Expect(components.Registry.Has(openmcpv1alpha1.APIServerComponent)).To(BeTrue()) + Expect(components.Registry.Has(openmcpv1alpha1.CloudOrchestratorComponent)).To(BeTrue()) + Expect(components.Registry.Has(openmcpv1alpha1.LandscaperComponent)).To(BeTrue()) + }) + + It("should return false for unknown components", func() { + Expect(components.Registry.Has("unknown")).To(BeFalse()) + }) + + It("should return the scheme", func() { + scheme := components.Registry.Scheme() + Expect(scheme).ToNot(BeNil()) + }) + }) + + Context("Authentication", func() { + var ( + converter components.ComponentConverter + handler *components.ComponentHandler + ) + + BeforeEach(func() { + handler = components.Registry.GetComponent(openmcpv1alpha1.AuthenticationComponent) + Expect(handler).ToNot(BeNil()) + converter = handler.Converter() + Expect(converter).ToNot(BeNil()) + + }) + + It("should return the resource", func() { + comp := handler.Resource() + Expect(comp).ToNot(BeNil()) + Expect(comp).To(BeAssignableToTypeOf(&openmcpv1alpha1.Authentication{})) + }) + + It("should return empty label selectors for the namespace scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminNamespaceScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the namespace scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewNamespaceScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the cluster scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminClusterScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the cluster scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewClusterScopeRole) + Expect(ls).To(BeNil()) + }) + }) + + Context("Authorization", func() { + var ( + converter components.ComponentConverter + handler *components.ComponentHandler + ) + + BeforeEach(func() { + handler = components.Registry.GetComponent(openmcpv1alpha1.AuthorizationComponent) + Expect(handler).ToNot(BeNil()) + converter = handler.Converter() + Expect(converter).ToNot(BeNil()) + + }) + + It("should return the resource", func() { + comp := handler.Resource() + Expect(comp).ToNot(BeNil()) + Expect(comp).To(BeAssignableToTypeOf(&openmcpv1alpha1.Authorization{})) + }) + + It("should return empty label selectors for the namespace scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminNamespaceScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the namespace scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewNamespaceScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the cluster scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminClusterScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the cluster scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewClusterScopeRole) + Expect(ls).To(BeNil()) + }) + }) + + Context("APIServer", func() { + var ( + converter components.ComponentConverter + handler *components.ComponentHandler + ) + + BeforeEach(func() { + handler = components.Registry.GetComponent(openmcpv1alpha1.APIServerComponent) + Expect(handler).ToNot(BeNil()) + converter = handler.Converter() + Expect(converter).ToNot(BeNil()) + + }) + + It("should return the resource", func() { + comp := handler.Resource() + Expect(comp).ToNot(BeNil()) + Expect(comp).To(BeAssignableToTypeOf(&openmcpv1alpha1.APIServer{})) + }) + + It("should return empty label selectors for the namespace scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminNamespaceScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the namespace scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewNamespaceScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the cluster scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminClusterScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the cluster scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewClusterScopeRole) + Expect(ls).To(BeNil()) + }) + }) + + Context("CloudOrchestrator", func() { + var ( + converter components.ComponentConverter + handler *components.ComponentHandler + ) + + BeforeEach(func() { + handler = components.Registry.GetComponent(openmcpv1alpha1.CloudOrchestratorComponent) + Expect(handler).ToNot(BeNil()) + converter = handler.Converter() + Expect(converter).ToNot(BeNil()) + + }) + + It("should return the resource", func() { + comp := handler.Resource() + Expect(comp).ToNot(BeNil()) + Expect(comp).To(BeAssignableToTypeOf(&openmcpv1alpha1.CloudOrchestrator{})) + }) + + It("should return empty label selectors for the namespace scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminNamespaceScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the namespace scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewNamespaceScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return two label selectors for the cluster scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminClusterScopeRole) + Expect(ls).ToNot(BeNil()) + Expect(ls).To(HaveLen(2)) + Expect(ls).To(ConsistOf( + metav1.LabelSelector{ + MatchLabels: map[string]string{ + components.CrossPlaneClusterScopedAdminMatchLabel: components.MatchLabelValue, + }, + }, + metav1.LabelSelector{ + MatchLabels: map[string]string{ + components.CloudOrchestratorClusterScopedAdminMatchLabel: components.MatchLabelValue, + }, + })) + }) + + It("should return two label selectors for the cluster scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewClusterScopeRole) + Expect(ls).ToNot(BeNil()) + Expect(ls).To(HaveLen(2)) + Expect(ls).To(ConsistOf( + metav1.LabelSelector{ + MatchLabels: map[string]string{ + components.CrossPlaneClusterScopedViewMatchLabel: components.MatchLabelValue, + }, + }, + metav1.LabelSelector{ + MatchLabels: map[string]string{ + components.CloudOrchestratorClusterScopedViewMatchLabel: components.MatchLabelValue, + }, + })) + }) + }) + + Context("Landscaper", func() { + var ( + converter components.ComponentConverter + handler *components.ComponentHandler + ) + + BeforeEach(func() { + handler = components.Registry.GetComponent(openmcpv1alpha1.LandscaperComponent) + Expect(handler).ToNot(BeNil()) + converter = handler.Converter() + Expect(converter).ToNot(BeNil()) + + }) + + It("should return the resource", func() { + comp := handler.Resource() + Expect(comp).ToNot(BeNil()) + Expect(comp).To(BeAssignableToTypeOf(&openmcpv1alpha1.Landscaper{})) + }) + + It("should return one label selector for the namespace scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminNamespaceScopeRole) + Expect(ls).ToNot(BeNil()) + Expect(ls).To(HaveLen(1)) + Expect(ls).To(ConsistOf( + metav1.LabelSelector{ + MatchLabels: map[string]string{ + components.LandscaperNamespaceScopedAdminMatchLabel: components.MatchLabelValue, + }, + })) + }) + + It("should return one label selector for the namespace scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewNamespaceScopeRole) + Expect(ls).ToNot(BeNil()) + Expect(ls).To(HaveLen(1)) + Expect(ls).To(ConsistOf( + metav1.LabelSelector{ + MatchLabels: map[string]string{ + components.LandscaperNamespaceScopedViewMatchLabel: components.MatchLabelValue, + }, + })) + }) + + It("should return empty label selectors for the cluster scoped admin role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.AdminClusterScopeRole) + Expect(ls).To(BeNil()) + }) + + It("should return empty label selectors for the cluster scoped view role", func() { + ls := handler.LabelSelectorsForRole(openmcpv1alpha1.ViewClusterScopeRole) + Expect(ls).To(BeNil()) + }) + }) +}) diff --git a/internal/components/registry.go b/internal/components/registry.go new file mode 100644 index 0000000..0fea944 --- /dev/null +++ b/internal/components/registry.go @@ -0,0 +1,34 @@ +package components + +import ( + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// ComponentRegistry can be used to fetch the known components. +type ComponentRegistry[T ManagedComponent] interface { + // GetKnownComponents returns a mapping from all registered component types to their respective in-cluster resources. + // The resources are provided via the ManagedComponent interface to allow a registry to return more than just the in-cluster resource. + // Any modification of the returned map or any of its contents must not influence the return value of future calls (= this function has to return a deep copy of its internal representation). + GetKnownComponents() map[openmcpv1alpha1.ComponentType]T + + // GetComponent is a shorthand for 'GetKnownComponents()[comp]'. + // It returns nil if no component is registered for the given type. + // Any modification of the returned object must not influence the return value of future calls (= this function has to return a deep copy of its internal representation). + GetComponent(openmcpv1alpha1.ComponentType) T + + // Register registers a new component. + // The given function is supposed to return a 'fresh' ManagedComponent, so that each call to 'GetComponent' or 'GetKnownComponents' returns a new object. + // The type is used as key, calling this function multiple times with the same type argument will cause the last call to overwrite anything registered with the previous ones. + // Calling Register with a nil function is expected to unregister the given component type. + Register(openmcpv1alpha1.ComponentType, func() T) + + // Has returns true if the given component type is registered in this registry. + Has(openmcpv1alpha1.ComponentType) bool +} + +// ManagedComponent is a helper interface that wraps the ability to return the in-cluster representation of a component and install the component's scheme. +type ManagedComponent interface { + // Resource returns the in-cluster resource for the given component. + // This is expected to return a pointer to the resource object, so it can be modified. + Resource() Component +} diff --git a/internal/components/suite_test.go b/internal/components/suite_test.go new file mode 100644 index 0000000..adcd681 --- /dev/null +++ b/internal/components/suite_test.go @@ -0,0 +1,13 @@ +package components_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Components Test Suite") +} diff --git a/internal/controller/core/apiserver/config/config.go b/internal/controller/core/apiserver/config/config.go new file mode 100644 index 0000000..9d0f97c --- /dev/null +++ b/internal/controller/core/apiserver/config/config.go @@ -0,0 +1,108 @@ +package config + +import ( + "context" + "errors" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// APIServerProviderConfiguration contains the configuration for the APIServer provider. +// This is everything which must be configured, should neither be a command-line argument +// nor be configurable in the ManagedControlPlane resource. +// For example: Garden cluster kubeconfig and project name for creating Gardener shoots. +type APIServerProviderConfiguration struct { + CommonConfig `json:",inline"` + + // GardenerConfig contains configuration for creating shoot clusters on a Gardener landscape. + GardenerConfig *MultiGardenerConfiguration `json:"gardener,omitempty"` +} + +type CommonConfig struct { + // ServiceAccountNamespace is the namespace on the API Server to be used for serviceaccounts. + // Defaults to the cola system namespace (usually 'openmcp-system'). + ServiceAccountNamespace *string `json:"serviceAccountNamespace,omitempty"` + // AdminServiceAccountName is the name of the serviceaccount used for the admin access. + // Defaults to 'admin'. + AdminServiceAccountName *string `json:"adminServiceAccountName,omitempty"` +} + +type CompletedCommonConfig struct { + ServiceAccountNamespace string + AdminServiceAccountName string +} + +// CompletedAPIServerProviderConfiguration is a helper struct. It contains the original configuration, +// enriched with defaults and processed values (e.g. a client constructed from a plaintext kubeconfig). +type CompletedAPIServerProviderConfiguration struct { + *CompletedCommonConfig + + // ConfiguredTypes contains all types for which configuration was given in the global config. + // The APIServerProvider will return an error for an APIServerConfiguration with a type that is not in this set. + ConfiguredTypes sets.Set[openmcpv1alpha1.APIServerType] + + GardenerConfig *CompletedMultiGardenerConfiguration +} + +// Complete transforms the APIServerProviderConfiguration into a CompletedAPIServerProviderConfiguration. +func (cfg *APIServerProviderConfiguration) Complete(ctx context.Context) (*CompletedAPIServerProviderConfiguration, error) { + if cfg == nil { + return nil, nil + } + res := &CompletedAPIServerProviderConfiguration{ + ConfiguredTypes: sets.New[openmcpv1alpha1.APIServerType](), + } + errs := []error{} + var err error + + res.GardenerConfig, err = cfg.GardenerConfig.complete(ctx) + if err == nil && res.GardenerConfig != nil { + res.ConfiguredTypes.Insert(openmcpv1alpha1.Gardener) + } + errs = append(errs, err) + + res.CompletedCommonConfig, err = cfg.CommonConfig.complete() + errs = append(errs, err) + + return res, errors.Join(errs...) +} + +func Validate(cfg *APIServerProviderConfiguration) error { + allErrs := field.ErrorList{} + + if cfg == nil { + allErrs = append(allErrs, field.Required(field.NewPath(""), "APIServer provider configuration must not be empty")) + return allErrs.ToAggregate() + } + + if cfg.GardenerConfig != nil { + allErrs = append(allErrs, validateGardenerConfig(cfg.GardenerConfig, field.NewPath("gardener"))...) + } + + allErrs = append(allErrs, cfg.CommonConfig.validate()...) + + return allErrs.ToAggregate() +} + +func (cc *CommonConfig) validate() field.ErrorList { + // currently nothing to validate, might change in the future + return nil +} + +func (cc *CommonConfig) complete() (*CompletedCommonConfig, error) { + res := &CompletedCommonConfig{} + + res.ServiceAccountNamespace = openmcpv1alpha1.SystemNamespace + if cc.ServiceAccountNamespace != nil { + res.ServiceAccountNamespace = *cc.ServiceAccountNamespace + } + res.AdminServiceAccountName = "admin" + if cc.AdminServiceAccountName != nil { + res.AdminServiceAccountName = *cc.AdminServiceAccountName + } + + return res, nil +} diff --git a/internal/controller/core/apiserver/config/config_gardener.go b/internal/controller/core/apiserver/config/config_gardener.go new file mode 100644 index 0000000..e47066c --- /dev/null +++ b/internal/controller/core/apiserver/config/config_gardener.go @@ -0,0 +1,534 @@ +package config + +import ( + "context" + "fmt" + "regexp" + "strings" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/schemes" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" +) + +const ( + // defaultGardenerLandscapeName is used to default the landscape name if a single config (old format) is provided. + defaultGardenerLandscapeName = "default" + // defaultGardenerConfigName is used to default the config name if a single config (old format) is provided. + defaultGardenerConfigName = "default" +) + +var ( + defaultLandscapeAndConfigurationRegex = regexp.MustCompile(`^([a-z0-9-]+/[a-z0-9-]+)$`) +) + +// MultiGardenerConfiguration contains configuration for multiple Gardener landscapes. +type MultiGardenerConfiguration struct { + // GardenerConfiguration accepts a single Gardener configuration. + // It is mainly used for backward compatibility. + *GardenerConfiguration `json:",inline"` + + // GardenerLandscapeWithoutConfig takes the fields which have been removed from GardenerConfiguration to preserve backward compatibility. + *GardenerLandscapeWithoutConfig `json:",inline"` + + // DefaultLandscapeAndConfiguration is the name of the default Gardener configuration. + // It is expected to follow the format '/'. + // Only required in multi-config mode. + DefaultLandscapeAndConfiguration string `json:"defaultConfig,omitempty"` + + // Landscapes is a list of supported Gardener landscapes. + // Only required in multi-config mode. + Landscapes []GardenerLandscape `json:"landscapes,omitempty"` +} + +// GardenerLandscape represents a Gardener landscape. +type GardenerLandscape struct { + GardenerLandscapeWithoutConfig `json:",inline"` + + // Name is the name of this Gardener landscape. + Name string `json:"name,omitempty"` + + // Configurations is a list of Gardener configurations. + Configurations []GardenerConfiguration `json:"configs,omitempty"` +} + +// This type exists only for backward compatibility. +// It can be merged with GardenerLandscape, if we at one point decide to get rid of the single configuration option. +type GardenerLandscapeWithoutConfig struct { + // Kubeconfig contains an inline kubeconfig. + Kubeconfig string `json:"kubeconfig,omitempty"` + + // If not nil, this client is used instead of creating a new one from the given kubeconfig during the 'complete' method. + // This is meant as a way to inject a fake client for testing purposes. + gardenClusterClient client.Client +} + +// GardenerConfiguration contains configuration for a Gardener. +type GardenerConfiguration struct { + // Name is the name of this Gardener configuration. + // Only required in multi-config mode. + Name string `json:"name,omitempty"` + + // Project is the Gardener project which should be used to create shoot clusters in it. + // The provided kubeconfig must have priviliges for this project. + Project string `json:"project,omitempty"` + + // CloudProfile is the name of the Gardener CloudProfile that should be used for this shoot. + CloudProfile string `json:"cloudProfile,omitempty"` + + // ShootTemplate contains the configuration for shoot clusters with worker nodes. + // It is relevant for APIServers for which spec.gardener.enableWorkers is true. + ShootTemplate *gardenv1beta1.ShootTemplate `json:"shootTemplate,omitempty"` + + // Regions contains the supported regions and their zones. + Regions []gardenv1beta1.Region `json:"regions,omitempty"` + + // DefaultRegion is the default region for the workerless shoots. + // If not specified, a region must be chosen in the APIServer spec. + // If specified, this region will be used if there is none in the APIServer spec. + DefaultRegion string `json:"defaultRegion,omitempty"` +} + +// InjectGardenClusterClient can be used to inject a fake client for testing purposes. +// It has to be called before calling the 'complete' method. +// For single config mode, the landscape parameter must be an empty string. +func (cfg *MultiGardenerConfiguration) InjectGardenClusterClient(landscape string, fakeClient client.Client) { + if landscape == "" { + if cfg.GardenerLandscapeWithoutConfig == nil { + cfg.GardenerLandscapeWithoutConfig = &GardenerLandscapeWithoutConfig{} + } + cfg.gardenClusterClient = fakeClient + } else { + for i := range cfg.Landscapes { + ls := &cfg.Landscapes[i] + if ls.Name == landscape { + ls.gardenClusterClient = fakeClient + return + } + } + } +} + +type CompletedMultiGardenerConfiguration struct { + // DefaultConfiguration is the name of the default Gardener configuration. + DefaultConfiguration string + + // DefaultLandscape is the name of the default Gardener landscape. + DefaultLandscape string + + // Landscapes is a map of Gardener landscapes. + Landscapes map[string]CompletedGardenerLandscape +} + +// LandscapeConfiguration returns the Gardener configuration with the given name, or an error if the configuration is unknown. +// This function can either be called with two separate arguments, or a single combined argument in the format '/'. +// +// In the two-argument case, the first argument is the landscape name and the second argument is the configuration name. +// An empty landscape or configuration string is interpreted as the respective default value. +// Note that the default config belongs to the default landscape, so a non-empty landscape string in combination with an empty config string is invalid. +// +// In the single-argument case, the argument is a combined string in the format '/'. +// A single empty string will be interpreted as default landscape and configuration. +// Apart from that, neither landscape nor configuration may be empty in this case. +// +// Will default landscape and configuration if called without any arguments. +// +// Any other amount of arguments will result in an error. +func (ccfg *CompletedMultiGardenerConfiguration) LandscapeConfiguration(data ...string) (*CompletedGardenerLandscape, *CompletedGardenerConfiguration, error) { + switch len(data) { + case 0: + return ccfg.configurationFromLandscapeAndConfigFields(ccfg.DefaultLandscape, ccfg.DefaultConfiguration) + case 1: + return ccfg.configurationFromCombinedField(data[0]) + case 2: + return ccfg.configurationFromLandscapeAndConfigFields(data[0], data[1]) + default: + return nil, nil, fmt.Errorf("invalid arguments: expected either one combined argument or two separate arguments, got %d", len(data)) + } +} + +func (ccfg *CompletedMultiGardenerConfiguration) configurationFromCombinedField(lc string) (*CompletedGardenerLandscape, *CompletedGardenerConfiguration, error) { + if lc == "" { + return ccfg.configurationFromLandscapeAndConfigFields(ccfg.DefaultLandscape, ccfg.DefaultConfiguration) + } + fields := strings.Split(lc, "/") + if len(fields) != 2 { + return nil, nil, fmt.Errorf("expected format is '/', but got '%s'", lc) + } + if fields[0] == "" || fields[1] == "" { + return nil, nil, fmt.Errorf("invalid arguments: landscape and config must not be empty in combined format") + } + return ccfg.configurationFromLandscapeAndConfigFields(fields[0], fields[1]) +} + +func (ccfg *CompletedMultiGardenerConfiguration) configurationFromLandscapeAndConfigFields(landscape, config string) (*CompletedGardenerLandscape, *CompletedGardenerConfiguration, error) { + if landscape == "" { + landscape = ccfg.DefaultLandscape + } else if landscape != ccfg.DefaultLandscape && config == "" { + return nil, nil, fmt.Errorf("invalid arguments: config can only be defaulted if landscape is also default (default landscape '%s', given landscape '%s')", ccfg.DefaultLandscape, landscape) + } + ls, ok := ccfg.Landscapes[landscape] + if !ok { + return nil, nil, fmt.Errorf("unable to get Gardener landscape: unknown landscape '%s'", landscape) + } + if config == "" { + config = ccfg.DefaultConfiguration + } + cfg, ok := ls.Configurations[config] + if !ok { + return nil, nil, fmt.Errorf("unable to get Gardener configuration: unknown configuration '%s'", config) + } + return &ls, &cfg, nil +} + +type CompletedGardenerLandscape struct { + // Client for accessing the Gardener landscape. + Client client.Client + + // Kubeconfig contains the kubeconfig the client was constructed from (unless injected for test scenarios). + Kubeconfig string + + // Configurations is a map of Gardener configurations. + Configurations map[string]CompletedGardenerConfiguration +} + +type CompletedGardenerConfiguration struct { + GardenerConfiguration + + // ProjectNamespace is the namespace belonging to the configured project. + ProjectNamespace string + + // ProviderType is the provider type. It is extracted from the given CloudProfile. + ProviderType string + + // ValidRegions is the set of valid regions. It contains the regions from the given CloudProfile + // whose names are listed in the APIServer config. + ValidRegions map[string]gardenv1beta1.Region + + // ValidK8SVersions is the set of valid k8s versions. It is extracted from the given CloudProfile. + ValidK8SVersions sets.Set[string] +} + +// Worker is the base definition of a worker group. +type Worker struct { + Name string + // Machine contains information about the machine type and image. + Machine Machine + // Maximum is the maximum number of machines to create. + // This value is divided by the number of configured zones for a fair distribution. + Maximum int32 + // Minimum is the minimum number of machines to create. + // This value is divided by the number of configured zones for a fair distribution. + Minimum int32 +} + +// Machine contains information about the machine type and image. +type Machine struct { + // Type is the machine type of the worker group. + Type string + // Image holds information about the machine image to use for all nodes of this pool. It will default to the + // latest version of the first image stated in the referenced CloudProfile if no value has been provided. + // +optional + Image *ShootMachineImage + // Architecture is CPU architecture of machines in this worker pool. + // +optional + Architecture *string +} + +// ShootMachineImage defines the name and the version of the shoot's machine image in any environment. Has to be +// defined in the respective CloudProfile. +type ShootMachineImage struct { + // Name is the name of the image. + Name string + // ProviderConfig is the shoot's individual configuration passed to an extension resource. + // +optional + ProviderConfig *runtime.RawExtension + // Version is the version of the shoot's image. + // If version is not provided, it will be defaulted to the latest version from the CloudProfile. + // +optional + Version *string +} + +func (cfg *MultiGardenerConfiguration) complete(ctx context.Context) (*CompletedMultiGardenerConfiguration, error) { + if cfg == nil { + return nil, nil + } + + res := &CompletedMultiGardenerConfiguration{} + + if cfg.DefaultLandscapeAndConfiguration == "" && len(cfg.Landscapes) == 0 { + // old single config mode + // transform to multi config mode + cfg.DefaultLandscapeAndConfiguration = fmt.Sprintf("%s/%s", defaultGardenerLandscapeName, defaultGardenerConfigName) + cfg.Landscapes = []GardenerLandscape{ + { + Name: defaultGardenerLandscapeName, + GardenerLandscapeWithoutConfig: *cfg.GardenerLandscapeWithoutConfig, + Configurations: []GardenerConfiguration{ + { + Name: defaultGardenerConfigName, + CloudProfile: cfg.CloudProfile, + DefaultRegion: cfg.DefaultRegion, + Project: cfg.Project, + Regions: cfg.Regions, + ShootTemplate: cfg.ShootTemplate, + }, + }, + }, + } + } + + // split default landscape and configuration into separate fields + defaults := strings.Split(cfg.DefaultLandscapeAndConfiguration, "/") + if len(defaults) != 2 { + return nil, fmt.Errorf("default landscape and configuration '%s' does not follow the expected format '/'", cfg.DefaultLandscapeAndConfiguration) + } + res.DefaultLandscape = defaults[0] + res.DefaultConfiguration = defaults[1] + + // complete landscapes + res.Landscapes = map[string]CompletedGardenerLandscape{} + for _, ls := range cfg.Landscapes { + cls := CompletedGardenerLandscape{} + + // use injected client or build from given kubeconfig + cls.Kubeconfig = ls.Kubeconfig + if ls.gardenClusterClient != nil { + // use fake client for testing + cls.Client = ls.gardenClusterClient + } else { + // build client from kubeconfig + restCfg, err := clientcmd.RESTConfigFromKubeConfig([]byte(cls.Kubeconfig)) + if err != nil { + return nil, fmt.Errorf("error building rest config from kubeconfig for landscape '%s': %w", ls.Name, err) + } + cls.Client, err = client.New(restCfg, client.Options{ + Scheme: schemes.GardenerScheme, + }) + if err != nil { + return nil, fmt.Errorf("error creating client for landscape '%s': %w", ls.Name, err) + } + } + + cls.Configurations = map[string]CompletedGardenerConfiguration{} + for _, lscfg := range ls.Configurations { + clscfg := CompletedGardenerConfiguration{ + GardenerConfiguration: lscfg, + } + + // fetch project to get project namespace + pr := &gardenv1beta1.Project{} + pr.SetName(lscfg.Project) + if err := cls.Client.Get(ctx, client.ObjectKeyFromObject(pr), pr); err != nil { + return nil, fmt.Errorf("[%s/%s] error fetching Project '%s': %w", ls.Name, lscfg.Name, lscfg.Project, err) + } + if pr.Spec.Namespace == nil { + return nil, fmt.Errorf("[%s/%s] project namespace is not set", ls.Name, lscfg.Name) + } + clscfg.ProjectNamespace = *pr.Spec.Namespace + + // fetch cloudprofile + cp := &gardenv1beta1.CloudProfile{} + cp.SetName(lscfg.CloudProfile) + if err := cls.Client.Get(ctx, client.ObjectKeyFromObject(cp), cp); err != nil { + return nil, fmt.Errorf("[%s/%s] error fetching CloudProfile '%s': %w", ls.Name, lscfg.Name, lscfg.CloudProfile, err) + } + + // set provider type + clscfg.ProviderType = cp.Spec.Type + + // set valid regions: select all regions from the cloud profile whose name is contained in the configured regions + clscfg.ValidRegions = map[string]gardenv1beta1.Region{} + for _, cpRegion := range cp.Spec.Regions { + for _, cfgRegion := range lscfg.Regions { + if cpRegion.Name == cfgRegion.Name { + clscfg.ValidRegions[cpRegion.Name] = cpRegion + break + } + } + } + + // set valid k8s versions + clscfg.ValidK8SVersions = sets.New[string]() + for _, version := range cp.Spec.Kubernetes.Versions { + semver := strings.Split(version.Version, ".") + minorVersion := fmt.Sprintf("%s.%s", semver[0], semver[1]) + clscfg.ValidK8SVersions.Insert(version.Version, minorVersion) + } + + // if a default region is given, check that it is valid + if lscfg.DefaultRegion != "" { + _, ok := clscfg.ValidRegions[lscfg.DefaultRegion] + if !ok { + return nil, fmt.Errorf("[%s/%s] the specified default region '%s' is not valid for the chosen cloudprofile '%s'", ls.Name, lscfg.Name, lscfg.DefaultRegion, lscfg.CloudProfile) + } + } + + cls.Configurations[lscfg.Name] = clscfg + } + + res.Landscapes[ls.Name] = cls + } + + return res, nil +} + +func validateGardenerConfig(cfg *MultiGardenerConfiguration, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if cfg == nil { + allErrs = append(allErrs, field.Required(fldPath, "Gardener config must not be empty")) + return allErrs + } + + if cfg.DefaultLandscapeAndConfiguration == "" && len(cfg.Landscapes) == 0 { + // single config mode (old format) + if cfg.GardenerConfiguration == nil || cfg.Project == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("project"), "project name must not be empty")) + } + if cfg.GardenerConfiguration == nil || cfg.CloudProfile == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("cloudProfile"), "cloudprofile name must not be empty")) + } + if cfg.GardenerLandscapeWithoutConfig == nil || len(cfg.Kubeconfig) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("kubeconfig"), "kubeconfig must not be empty")) + } + if cfg.GardenerConfiguration == nil || len(cfg.Regions) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("regions"), "regions must not be empty")) + } + if cfg.GardenerConfiguration == nil || cfg.ShootTemplate == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("shootTemplate"), "shootTemplate must not be empty")) + } else { + allErrs = append(allErrs, validateShootTemplate(cfg.ShootTemplate, fldPath.Child("shootTemplate"))...) + } + } else { + // multi config mode + if cfg.DefaultLandscapeAndConfiguration == "" { + allErrs = append(allErrs, field.Required(fldPath.Child("defaultConfig"), "default landscape/configuration name must not be empty")) + } else if !defaultLandscapeAndConfigurationRegex.MatchString(cfg.DefaultLandscapeAndConfiguration) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("defaultConfig"), cfg.DefaultLandscapeAndConfiguration, "default configuration name must follow the format '/'")) + } + if len(cfg.Landscapes) == 0 { + allErrs = append(allErrs, field.Required(fldPath.Child("landscapes"), "landscapes must not be empty")) + } + + knownLandscapeNames := sets.New[string]() + for i, ls := range cfg.Landscapes { + landscapePath := fldPath.Child("landscapes").Index(i) + if ls.Name == "" { + allErrs = append(allErrs, field.Required(landscapePath.Child("name"), "landscape name must not be empty")) + } + if knownLandscapeNames.Has(ls.Name) { + allErrs = append(allErrs, field.Duplicate(landscapePath.Child("name"), ls.Name)) + } + if len(ls.Configurations) == 0 { + allErrs = append(allErrs, field.Required(landscapePath.Child("configs"), "configurations must not be empty")) + } + if len(ls.Kubeconfig) == 0 { + allErrs = append(allErrs, field.Required(landscapePath.Child("kubeconfig"), "kubeconfig must not be empty")) + } + + knownLandscapeNames.Insert(ls.Name) + knownConfigNames := sets.New[string]() + + for j, lscfg := range ls.Configurations { + configPath := landscapePath.Child("configs").Index(j) + if lscfg.Name == "" { + allErrs = append(allErrs, field.Required(configPath.Child("name"), "configuration name must not be empty")) + } + if knownConfigNames.Has(lscfg.Name) { + allErrs = append(allErrs, field.Duplicate(configPath.Child("name"), lscfg.Name)) + } + + knownConfigNames.Insert(lscfg.Name) + + if lscfg.Project == "" { + allErrs = append(allErrs, field.Required(configPath.Child("project"), "project name must not be empty")) + } + if lscfg.CloudProfile == "" { + allErrs = append(allErrs, field.Required(configPath.Child("cloudProfile"), "cloudprofile name must not be empty")) + } + if len(lscfg.Regions) == 0 { + allErrs = append(allErrs, field.Required(configPath.Child("regions"), "regions must not be empty")) + } + + allErrs = append(allErrs, validateShootTemplate(lscfg.ShootTemplate, configPath.Child("shootTemplate"))...) + } + } + } + + return allErrs +} + +func validateShootTemplate(st *gardenv1beta1.ShootTemplate, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if st == nil { + allErrs = append(allErrs, field.Required(fldPath, "shootTemplate must not be empty")) + return allErrs + } + + specPath := fldPath.Child("spec") + + if st.Spec.Networking == nil { + allErrs = append(allErrs, field.Required(specPath.Child("networking"), "networking must not be empty")) + } else { + networkingPath := specPath.Child("networking") + if st.Spec.Networking.Type == nil { + allErrs = append(allErrs, field.Required(networkingPath.Child("type"), "networking type must not be empty")) + } + if st.Spec.Networking.Nodes == nil { + allErrs = append(allErrs, field.Required(networkingPath.Child("nodes"), "networking nodes must not be empty")) + } + } + + if len(st.Spec.Provider.Type) == 0 { + allErrs = append(allErrs, field.Required(specPath.Child("provider", "type"), "provider type must not be empty")) + } + + if st.Spec.Provider.InfrastructureConfig == nil { + allErrs = append(allErrs, field.Required(specPath.Child("provider", "infrastructureConfig"), "infrastructureConfig must not be empty")) + } + + if len(st.Spec.Provider.Workers) == 0 { + allErrs = append(allErrs, field.Required(specPath.Child("provider", "workers"), "workers must not be empty")) + } + + for i, worker := range st.Spec.Provider.Workers { + workerPath := specPath.Child("provider", "workers").Index(i) + if worker.Machine.Architecture == nil { + allErrs = append(allErrs, field.Required(workerPath.Child("machine", "architecture"), "architecture must not be empty")) + } + if worker.Machine.Image == nil { + allErrs = append(allErrs, field.Required(workerPath.Child("machine", "image"), "machine image must not be empty")) + } else { + if worker.Machine.Image.Version == nil { + allErrs = append(allErrs, field.Required(workerPath.Child("machine", "image", "version"), "machine image version must not be empty")) + } + } + if len(worker.Machine.Type) == 0 { + allErrs = append(allErrs, field.Required(workerPath.Child("machine", "type"), "machine type must not be empty")) + } + if worker.Volume == nil { + allErrs = append(allErrs, field.Required(workerPath.Child("volume"), "volume must not be empty")) + } else { + if worker.Volume.Type == nil { + allErrs = append(allErrs, field.Required(workerPath.Child("volume", "type"), "volume type must not be empty")) + } + if len(worker.Volume.VolumeSize) == 0 { + allErrs = append(allErrs, field.Required(workerPath.Child("volume", "size"), "volume size must not be empty")) + } + } + } + + if st.Spec.SecretBindingName == nil { + allErrs = append(allErrs, field.Required(specPath.Child("secretBindingName"), "secretBindingName must not be empty")) + } + + return allErrs +} diff --git a/internal/controller/core/apiserver/config/config_test.go b/internal/controller/core/apiserver/config/config_test.go new file mode 100644 index 0000000..edd2a1a --- /dev/null +++ b/internal/controller/core/apiserver/config/config_test.go @@ -0,0 +1,218 @@ +package config_test + +import ( + "context" + "fmt" + "path" + "testing" + + apiserverconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + openmcptesting "github.com/openmcp-project/controller-utils/pkg/testing" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +const ( + gardenCluster = "garden" + gardenCluster2 = "garden2" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "APIServer Config Test Suite") +} + +var _ = Describe("APIServer Config", func() { + + Context("Config Loading", func() { + + It("should load and complete a valid config", func() { + cfgFile := path.Join("testdata", "config_valid.yaml") + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + env := openmcptesting.NewEnvironmentBuilder().WithFakeClient(testutils.Scheme).WithInitObjectPath("testdata", "garden_cluster").Build() + cfg.GardenerConfig.InjectGardenClusterClient("", env.Client()) + + cc, err := cfg.Complete(context.TODO()) + Expect(err).ToNot(HaveOccurred()) + Expect(cc).ToNot(BeNil()) + Expect(cc.ServiceAccountNamespace).To(Equal(openmcpv1alpha1.SystemNamespace)) + Expect(cc.AdminServiceAccountName).To(Equal("admin")) + }) + + It("should fail to load an incorrect config", func() { + cfgFile := path.Join("testdata", "config_invalid-1.yaml") + _, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).To(HaveOccurred()) + }) + + It("should fail to load a non-existing config", func() { + cfgFile := path.Join("testdata", "config_non_existing.yaml") + _, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).To(HaveOccurred()) + }) + + Context("Gardener Config Loading", func() { + + for _, configMode := range []string{"single", "multi"} { + var affix string + var injectKubeconfigs func(cfg *apiserverconfig.MultiGardenerConfiguration, env *openmcptesting.ComplexEnvironment) + switch configMode { + case "multi": + affix = "multi_" + injectKubeconfigs = func(cfg *apiserverconfig.MultiGardenerConfiguration, env *openmcptesting.ComplexEnvironment) { + cfg.InjectGardenClusterClient("default", env.Client(gardenCluster)) + cfg.InjectGardenClusterClient("extra", env.Client(gardenCluster2)) + } + default: + affix = "" + injectKubeconfigs = func(cfg *apiserverconfig.MultiGardenerConfiguration, env *openmcptesting.ComplexEnvironment) { + cfg.InjectGardenClusterClient("", env.Client(gardenCluster)) + } + } + + Context(fmt.Sprintf("Config Mode: %s", configMode), func() { + + It("should fail to complete a config if the default region is not in the configured regions", func() { + cfgFile := path.Join("testdata", fmt.Sprintf("config_%sinvalid_region-1.yaml", affix)) + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + env := openmcptesting.NewComplexEnvironmentBuilder().WithFakeClient(gardenCluster, testutils.Scheme).WithInitObjectPath(gardenCluster, "testdata", "garden_cluster").WithFakeClient(gardenCluster2, testutils.Scheme).WithInitObjectPath(gardenCluster2, "testdata", "garden_cluster_2").Build() + injectKubeconfigs(cfg.GardenerConfig, env) + + _, err = cfg.Complete(context.TODO()) + Expect(err).To(MatchError(ContainSubstring("default region"))) + if configMode == "multi" { + Expect(err).To(MatchError(ContainSubstring("extra/foo"))) + } + }) + + It("should fail to complete a config if the default region is in the configured regions but not in the cloudprofile", func() { + cfgFile := path.Join("testdata", fmt.Sprintf("config_%sinvalid_region-2.yaml", affix)) + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + env := openmcptesting.NewComplexEnvironmentBuilder().WithFakeClient(gardenCluster, testutils.Scheme).WithInitObjectPath(gardenCluster, "testdata", "garden_cluster").WithFakeClient(gardenCluster2, testutils.Scheme).WithInitObjectPath(gardenCluster2, "testdata", "garden_cluster_2").Build() + injectKubeconfigs(cfg.GardenerConfig, env) + + _, err = cfg.Complete(context.TODO()) + Expect(err).To(MatchError(ContainSubstring("default region"))) + if configMode == "multi" { + Expect(err).To(MatchError(ContainSubstring("extra/foo"))) + } + }) + + }) + } + + }) + + }) + + Context("Config Validation", func() { + + It("should correctly validate a valid config", func() { + cfgFile := path.Join("testdata", "config_valid.yaml") + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + Expect(apiserverconfig.Validate(cfg)).To(Succeed()) + }) + + It("should correctly validate a valid config (Gardener multi)", func() { + cfgFile := path.Join("testdata", "config_multi_valid.yaml") + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + Expect(apiserverconfig.Validate(cfg)).To(Succeed()) + }) + + Context("Gardener Config Validation", func() { + + It("should detect if the default landscape/configuration is missing for multi mode", func() { + cfgFile := path.Join("testdata", "config_multi_invalid_missing_default.yaml") + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + err = apiserverconfig.Validate(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("default")) + }) + + for _, configMode := range []string{"single", "multi"} { + affix := "" + switch configMode { + case "multi": + affix = "multi_" + } + + Context(fmt.Sprintf("Config Mode: %s", configMode), func() { + + It("should detect if the regions are missing", func() { + cfgFile := path.Join("testdata", fmt.Sprintf("config_%sinvalid-2.yaml", affix)) + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + err = apiserverconfig.Validate(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("regions")) + }) + + It("should detect if the cloudprofile is missing", func() { + cfgFile := path.Join("testdata", fmt.Sprintf("config_%sinvalid-3.yaml", affix)) + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + err = apiserverconfig.Validate(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("cloudProfile")) + }) + + It("should detect if the shoot template is missing", func() { + cfgFile := path.Join("testdata", fmt.Sprintf("config_%sinvalid-4.yaml", affix)) + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + err = apiserverconfig.Validate(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("shootTemplate")) + }) + + It("should detect if the project is missing", func() { + cfgFile := path.Join("testdata", fmt.Sprintf("config_%sinvalid-5.yaml", affix)) + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + err = apiserverconfig.Validate(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("project")) + }) + + It("should detect if the kubeconfig is missing", func() { + cfgFile := path.Join("testdata", fmt.Sprintf("config_%sinvalid-6.yaml", affix)) + cfg, err := apiserverconfig.LoadConfig(cfgFile) + Expect(err).ToNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + err = apiserverconfig.Validate(cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("kubeconfig")) + }) + + }) + } + + }) + + }) + +}) diff --git a/internal/controller/core/apiserver/config/testdata/config_invalid-1.yaml b/internal/controller/core/apiserver/config/testdata/config_invalid-1.yaml new file mode 100644 index 0000000..7e9668e --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_invalid-1.yaml @@ -0,0 +1 @@ +"foo" \ No newline at end of file diff --git a/internal/controller/core/apiserver/config/testdata/config_invalid-2.yaml b/internal/controller/core/apiserver/config/testdata/config_invalid-2.yaml new file mode 100644 index 0000000..a560ddc --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_invalid-2.yaml @@ -0,0 +1,57 @@ +gardener: + cloudProfile: gcp + # regions: + # - name: europe-west1 + # - name: europe-west3 + # - name: us-central1 + # - name: asia-south1 + defaultRegion: europe-west3 + shootTemplate: + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf diff --git a/internal/controller/core/apiserver/config/testdata/config_invalid-3.yaml b/internal/controller/core/apiserver/config/testdata/config_invalid-3.yaml new file mode 100644 index 0000000..0f83ab4 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_invalid-3.yaml @@ -0,0 +1,57 @@ +gardener: + # cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: europe-west3 + shootTemplate: + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf diff --git a/internal/controller/core/apiserver/config/testdata/config_invalid-4.yaml b/internal/controller/core/apiserver/config/testdata/config_invalid-4.yaml new file mode 100644 index 0000000..80b63ba --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_invalid-4.yaml @@ -0,0 +1,57 @@ +gardener: + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: europe-west3 + # shootTemplate: + # spec: + # networking: + # type: "calico" + # nodes: "10.180.0.0/16" + # provider: + # type: gcp + # infrastructureConfig: + # apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + # kind: InfrastructureConfig + # networks: + # workers: 10.180.0.0/16 + # controlPlaneConfig: + # apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + # kind: ControlPlaneConfig + # zone: "" + # workers: + # - name: worker-0 + # machine: + # type: n1-standard-2 + # image: + # name: gardenlinux + # version: 1312.3.0 + # architecture: amd64 + # maximum: 2 + # minimum: 1 + # volume: + # type: pd-balanced + # size: 50Gi + # secretBindingName: test + project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf diff --git a/internal/controller/core/apiserver/config/testdata/config_invalid-5.yaml b/internal/controller/core/apiserver/config/testdata/config_invalid-5.yaml new file mode 100644 index 0000000..290af5c --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_invalid-5.yaml @@ -0,0 +1,57 @@ +gardener: + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: europe-west3 + shootTemplate: + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + # project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf diff --git a/internal/controller/core/apiserver/config/testdata/config_invalid-6.yaml b/internal/controller/core/apiserver/config/testdata/config_invalid-6.yaml new file mode 100644 index 0000000..77444b4 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_invalid-6.yaml @@ -0,0 +1,57 @@ +gardener: + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: europe-west3 + shootTemplate: + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + # kubeconfig: | + # apiVersion: v1 + # kind: Config + # clusters: + # - cluster: + # certificate-authority-data: ZHVtbXkK + # server: https://127.0.0.1:55761 + # name: dummy + # contexts: + # - context: + # cluster: dummy + # user: dummy + # name: dummy + # current-context: dummy + # users: + # - name: dummy + # user: + # token: asdf diff --git a/internal/controller/core/apiserver/config/testdata/config_invalid_region-1.yaml b/internal/controller/core/apiserver/config/testdata/config_invalid_region-1.yaml new file mode 100644 index 0000000..3512dae --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_invalid_region-1.yaml @@ -0,0 +1,57 @@ +gardener: + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: europe-north1 + shootTemplate: + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf diff --git a/internal/controller/core/apiserver/config/testdata/config_invalid_region-2.yaml b/internal/controller/core/apiserver/config/testdata/config_invalid_region-2.yaml new file mode 100644 index 0000000..d526cff --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_invalid_region-2.yaml @@ -0,0 +1,58 @@ +gardener: + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + - name: europe-north32 + defaultRegion: europe-north32 + shootTemplate: + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_invalid-2.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-2.yaml new file mode 100644 index 0000000..ca5582f --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-2.yaml @@ -0,0 +1,205 @@ +gardener: + defaultConfig: default/default + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + # regions: + # - name: europe-west1 + # - name: europe-west3 + # - name: us-central1 + # - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_invalid-3.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-3.yaml new file mode 100644 index 0000000..900a30d --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-3.yaml @@ -0,0 +1,205 @@ +gardener: + defaultConfig: default/default + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + # cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_invalid-4.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-4.yaml new file mode 100644 index 0000000..df1611a --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-4.yaml @@ -0,0 +1,205 @@ +gardener: + defaultConfig: default/default + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + # shootTemplate: + # metadata: + # annotations: + # test.openmcp.cloud/config: multi/extra/foo + # spec: + # networking: + # type: "calico" + # nodes: "10.180.0.0/16" + # provider: + # type: gcp + # infrastructureConfig: + # apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + # kind: InfrastructureConfig + # networks: + # workers: 10.180.0.0/16 + # controlPlaneConfig: + # apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + # kind: ControlPlaneConfig + # zone: "" + # workers: + # - name: worker-0 + # machine: + # type: n1-standard-2 + # image: + # name: gardenlinux + # version: 1312.3.0 + # architecture: amd64 + # maximum: 2 + # minimum: 1 + # volume: + # type: pd-balanced + # size: 50Gi + # secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_invalid-5.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-5.yaml new file mode 100644 index 0000000..4ca7a4f --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-5.yaml @@ -0,0 +1,205 @@ +gardener: + defaultConfig: default/default + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + # project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_invalid-6.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-6.yaml new file mode 100644 index 0000000..17c1ca8 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_invalid-6.yaml @@ -0,0 +1,205 @@ +gardener: + defaultConfig: default/default + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + # kubeconfig: | + # apiVersion: v1 + # kind: Config + # clusters: + # - cluster: + # certificate-authority-data: ZHVtbXkK + # server: https://127.0.0.1:55761 + # name: dummy + # contexts: + # - context: + # cluster: dummy + # user: dummy + # name: dummy + # current-context: dummy + # users: + # - name: dummy + # user: + # token: asdf + configs: + - name: foo + cloudProfile: gcp + # regions: + # - name: europe-west1 + # - name: europe-west3 + # - name: us-central1 + # - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_invalid_missing_default.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_invalid_missing_default.yaml new file mode 100644 index 0000000..4f866fc --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_invalid_missing_default.yaml @@ -0,0 +1,204 @@ +gardener: + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_invalid_region-1.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_invalid_region-1.yaml new file mode 100644 index 0000000..ec8d6c9 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_invalid_region-1.yaml @@ -0,0 +1,205 @@ +gardener: + defaultConfig: default/default + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: europe-north1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_invalid_region-2.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_invalid_region-2.yaml new file mode 100644 index 0000000..9ec45f4 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_invalid_region-2.yaml @@ -0,0 +1,206 @@ +gardener: + defaultConfig: default/default + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + - name: europe-north32 + defaultRegion: europe-north32 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_multi_valid.yaml b/internal/controller/core/apiserver/config/testdata/config_multi_valid.yaml new file mode 100644 index 0000000..2e8c420 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_multi_valid.yaml @@ -0,0 +1,205 @@ +gardener: + defaultConfig: default/default + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: default + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/default + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: extra + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/extra + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/config/testdata/config_valid.yaml b/internal/controller/core/apiserver/config/testdata/config_valid.yaml new file mode 100644 index 0000000..7d1a142 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/config_valid.yaml @@ -0,0 +1,57 @@ +gardener: + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: europe-west3 + shootTemplate: + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf diff --git a/internal/controller/core/apiserver/config/testdata/garden_cluster/cloudprofile-gcp.yaml b/internal/controller/core/apiserver/config/testdata/garden_cluster/cloudprofile-gcp.yaml new file mode 100644 index 0000000..c26881f --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/garden_cluster/cloudprofile-gcp.yaml @@ -0,0 +1,2089 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: CloudProfile +metadata: + name: gcp +spec: + kubernetes: + versions: + - classification: preview + version: 1.29.4 + - classification: supported + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.3 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.2 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.1 + - classification: preview + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.9 + - classification: supported + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.8 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.7 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.6 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.4 + - classification: preview + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.13 + - classification: supported + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.12 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.11 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.10 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.9 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.8 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.7 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.6 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.5 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.15 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.14 + - classification: deprecated + expirationDate: "2024-06-30T23:59:59Z" + version: 1.26.13 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.11 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.10 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.26.9 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.8 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.7 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.6 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.5 + - classification: deprecated + expirationDate: "2024-07-31T23:59:59Z" + version: 1.25.16 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.25.15 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.25.14 + machineImages: + - name: gardenlinux + updateStrategy: minor + versions: + - architectures: + - amd64 + - arm64 + classification: preview + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.5.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-06-23T23:59:59Z" + version: 1443.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.0.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1312.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 1312.0.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-07-15T23:59:59Z" + version: 934.11.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-05-15T23:59:59Z" + version: 934.10.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 934.9.0 + machineTypes: + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 7Gi + name: n1-standard-2 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 85Gi + name: a2-highgpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 170Gi + name: a2-highgpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 340Gi + name: a2-highgpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 680Gi + name: a2-highgpu-8g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-megagpu-16g + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 170Gi + name: a2-ultragpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 340Gi + name: a2-ultragpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 680Gi + name: a2-ultragpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-ultragpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-highgpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-megagpu-8g + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2-highcpu-56 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2-standard-16 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c2-standard-30 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2-standard-56 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c2-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2-standard-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 224Gi + name: c2d-highcpu-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c2d-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2d-highcpu-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 896Gi + name: c2d-highmem-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: c2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: c2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 448Gi + name: c2d-highmem-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 448Gi + name: c2d-standard-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: c2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: c2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2d-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2d-standard-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 352Gi + name: c3-highcpu-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 44Gi + name: c3-highcpu-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3-highcpu-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 88Gi + name: c3-highcpu-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3-highcpu-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 176Gi + name: c3-highcpu-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: c3-highmem-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 176Gi + name: c3-highmem-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3-highmem-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 352Gi + name: c3-highmem-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3-highmem-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: c3-highmem-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176-lssd + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4-lssd + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c3d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 354Gi + name: c3d-highcpu-180 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 59Gi + name: c3d-highcpu-30 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 708Gi + name: c3d-highcpu-360 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 118Gi + name: c3d-highcpu-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 177Gi + name: c3d-highcpu-90 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3d-highmem-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3d-standard-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90-lssd + usable: true + - architecture: amd64 + cpu: "240" + gpu: "0" + memory: 407Gi + name: ct4p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5l-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5l-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5l-hightpu-8t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5lp-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5lp-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5lp-hightpu-8t + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 448Gi + name: ct5p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: e2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: e2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: e2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: e2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: e2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: e2-highmem-2 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: e2-highmem-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: e2-highmem-8 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: e2-medium + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: e2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: e2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: e2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: e2-standard-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: e2-standard-8 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 48Gi + name: g2-standard-12 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: g2-standard-16 + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 96Gi + name: g2-standard-24 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: g2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: g2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: g2-standard-48 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: g2-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: g2-standard-96 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: h3-standard-88 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1433Gi + name: m1-megamem-96 + usable: true + - architecture: amd64 + cpu: "160" + gpu: "0" + memory: 3844Gi + name: m1-ultramem-160 + usable: true + - architecture: amd64 + cpu: "40" + gpu: "0" + memory: 961Gi + name: m1-ultramem-40 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 1922Gi + name: m1-ultramem-80 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 8832Gi + name: m2-hypermem-416 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 5888Gi + name: m2-megamem-416 + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 5888Gi + name: m2-ultramem-208 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 11776Gi + name: m2-ultramem-416 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1250Gi + name: m2-ultramem2x-96 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 600Gi + name: m2-ultramemx-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1952Gi + name: m3-megamem-128 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 976Gi + name: m3-megamem-64 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 3904Gi + name: m3-ultramem-128 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 976Gi + name: m3-ultramem-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 1952Gi + name: m3-ultramem-64 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 14Gi + name: n1-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 28Gi + name: n1-highcpu-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 57Gi + name: n1-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 7Gi + name: n1-highcpu-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 86Gi + name: n1-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 104Gi + name: n1-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 13Gi + name: n1-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 208Gi + name: n1-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 26Gi + name: n1-highmem-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 416Gi + name: n1-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 52Gi + name: n1-highmem-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 624Gi + name: n1-highmem-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 60Gi + name: n1-standard-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 120Gi + name: n1-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 15Gi + name: n1-standard-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 240Gi + name: n1-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 30Gi + name: n1-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 360Gi + name: n1-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2-highcpu-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 864Gi + name: n2-highmem-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2-standard-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 128Gi + name: n2d-highcpu-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 224Gi + name: n2d-highcpu-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2d-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2d-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2d-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2d-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2d-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2d-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2d-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2d-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2d-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2d-standard-2 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 896Gi + name: n2d-standard-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2d-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2d-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2d-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2d-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2d-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: n4-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: n4-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: n4-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: n4-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: n4-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: n4-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: n4-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 160Gi + name: n4-highcpu-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n4-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n4-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n4-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n4-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n4-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n4-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n4-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n4-highmem-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n4-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n4-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n4-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n4-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n4-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n4-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n4-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n4-standard-80 + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2a-standard-16 + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2a-standard-2 + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2a-standard-32 + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2a-standard-4 + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2a-standard-48 + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2a-standard-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2d-standard-48 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: t2d-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: z3-highmem-176 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: z3-highmem-88 + usable: true + providerConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: CloudProfileConfig + machineImages: + - name: gardenlinux + versions: + - architecture: amd64 + image: images/gardenlinux-amd64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-3-c261f887 + version: 1443.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-3-c261f887 + version: 1443.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-2-77c8471d + version: 1312.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-2-77c8471d + version: 1312.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-11-75132b8 + version: 934.11.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-10-f057c9b + version: 934.10.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-9-54c63e5 + version: 934.9.0 + regions: + - name: africa-south1 + zones: + - name: africa-south1-a + - name: africa-south1-b + - name: africa-south1-c + - name: asia-east1 + zones: + - name: asia-east1-a + - name: asia-east1-b + - name: asia-east1-c + - name: asia-east2 + zones: + - name: asia-east2-a + - name: asia-east2-b + - name: asia-east2-c + - name: asia-northeast1 + zones: + - name: asia-northeast1-a + - name: asia-northeast1-b + - name: asia-northeast1-c + - name: asia-northeast2 + zones: + - name: asia-northeast2-a + - name: asia-northeast2-b + - name: asia-northeast2-c + - name: asia-northeast3 + zones: + - name: asia-northeast3-a + - name: asia-northeast3-b + - name: asia-northeast3-c + - name: asia-south1 + zones: + - name: asia-south1-a + - name: asia-south1-b + - name: asia-south1-c + - name: asia-south2 + zones: + - name: asia-south2-a + - name: asia-south2-b + - name: asia-south2-c + - name: asia-southeast1 + zones: + - name: asia-southeast1-a + - name: asia-southeast1-b + - name: asia-southeast1-c + - name: asia-southeast2 + zones: + - name: asia-southeast2-a + - name: asia-southeast2-b + - name: asia-southeast2-c + - name: australia-southeast1 + zones: + - name: australia-southeast1-a + - name: australia-southeast1-b + - name: australia-southeast1-c + - name: australia-southeast2 + zones: + - name: australia-southeast2-a + - name: australia-southeast2-b + - name: australia-southeast2-c + - name: europe-central2 + zones: + - name: europe-central2-a + - name: europe-central2-b + - name: europe-central2-c + - name: europe-north1 + zones: + - name: europe-north1-a + - name: europe-north1-b + - name: europe-north1-c + - name: europe-southwest1 + zones: + - name: europe-southwest1-a + - name: europe-southwest1-b + - name: europe-southwest1-c + - name: europe-west1 + zones: + - name: europe-west1-b + - name: europe-west1-c + - name: europe-west1-d + - name: europe-west10 + zones: + - name: europe-west10-a + - name: europe-west10-b + - name: europe-west10-c + - name: europe-west12 + zones: + - name: europe-west12-a + - name: europe-west12-b + - name: europe-west12-c + - name: europe-west2 + zones: + - name: europe-west2-a + - name: europe-west2-b + - name: europe-west2-c + - name: europe-west3 + zones: + - name: europe-west3-a + - name: europe-west3-b + - name: europe-west3-c + - name: europe-west4 + zones: + - name: europe-west4-a + - name: europe-west4-b + - name: europe-west4-c + - name: europe-west5 + zones: + - name: europe-west5-a + - name: europe-west5-b + - name: europe-west5-c + - name: europe-west6 + zones: + - name: europe-west6-a + - name: europe-west6-b + - name: europe-west6-c + - name: europe-west8 + zones: + - name: europe-west8-a + - name: europe-west8-b + - name: europe-west8-c + - name: europe-west9 + zones: + - name: europe-west9-a + - name: europe-west9-b + - name: europe-west9-c + - name: me-central1 + zones: + - name: me-central1-a + - name: me-central1-b + - name: me-central1-c + - name: me-central2 + zones: + - name: me-central2-a + - name: me-central2-b + - name: me-central2-c + - name: me-west1 + zones: + - name: me-west1-a + - name: me-west1-b + - name: me-west1-c + - name: northamerica-northeast1 + zones: + - name: northamerica-northeast1-a + - name: northamerica-northeast1-b + - name: northamerica-northeast1-c + - name: northamerica-northeast2 + zones: + - name: northamerica-northeast2-a + - name: northamerica-northeast2-b + - name: northamerica-northeast2-c + - name: southamerica-east1 + zones: + - name: southamerica-east1-a + - name: southamerica-east1-b + - name: southamerica-east1-c + - name: southamerica-west1 + zones: + - name: southamerica-west1-a + - name: southamerica-west1-b + - name: southamerica-west1-c + - name: us-central1 + zones: + - name: us-central1-a + - name: us-central1-b + - name: us-central1-c + - name: us-central1-f + - name: us-east1 + zones: + - name: us-east1-b + - name: us-east1-c + - name: us-east1-d + - name: us-east4 + zones: + - name: us-east4-a + - name: us-east4-b + - name: us-east4-c + - name: us-east5 + zones: + - name: us-east5-a + - name: us-east5-b + - name: us-east5-c + - name: us-south1 + zones: + - name: us-south1-a + - name: us-south1-b + - name: us-south1-c + - name: us-west1 + zones: + - name: us-west1-a + - name: us-west1-b + - name: us-west1-c + - name: us-west2 + zones: + - name: us-west2-a + - name: us-west2-b + - name: us-west2-c + - name: us-west3 + zones: + - name: us-west3-a + - name: us-west3-b + - name: us-west3-c + - name: us-west4 + zones: + - name: us-west4-a + - name: us-west4-b + - name: us-west4-c + type: gcp + volumeTypes: + - class: premium + minSize: 20Gi + name: pd-balanced + usable: true + - class: standard + minSize: 20Gi + name: pd-standard + usable: true + - class: premium + minSize: 20Gi + name: pd-ssd + usable: true diff --git a/internal/controller/core/apiserver/config/testdata/garden_cluster/project-test.yaml b/internal/controller/core/apiserver/config/testdata/garden_cluster/project-test.yaml new file mode 100644 index 0000000..6a319b0 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/garden_cluster/project-test.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: test +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-test + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/config/testdata/garden_cluster/project-test2.yaml b/internal/controller/core/apiserver/config/testdata/garden_cluster/project-test2.yaml new file mode 100644 index 0000000..4dbb636 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/garden_cluster/project-test2.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: test2 +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-test2 + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/config/testdata/garden_cluster_2/cloudprofile-gcp.yaml b/internal/controller/core/apiserver/config/testdata/garden_cluster_2/cloudprofile-gcp.yaml new file mode 100644 index 0000000..c26881f --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/garden_cluster_2/cloudprofile-gcp.yaml @@ -0,0 +1,2089 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: CloudProfile +metadata: + name: gcp +spec: + kubernetes: + versions: + - classification: preview + version: 1.29.4 + - classification: supported + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.3 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.2 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.1 + - classification: preview + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.9 + - classification: supported + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.8 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.7 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.6 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.4 + - classification: preview + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.13 + - classification: supported + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.12 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.11 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.10 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.9 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.8 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.7 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.6 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.5 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.15 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.14 + - classification: deprecated + expirationDate: "2024-06-30T23:59:59Z" + version: 1.26.13 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.11 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.10 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.26.9 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.8 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.7 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.6 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.5 + - classification: deprecated + expirationDate: "2024-07-31T23:59:59Z" + version: 1.25.16 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.25.15 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.25.14 + machineImages: + - name: gardenlinux + updateStrategy: minor + versions: + - architectures: + - amd64 + - arm64 + classification: preview + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.5.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-06-23T23:59:59Z" + version: 1443.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.0.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1312.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 1312.0.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-07-15T23:59:59Z" + version: 934.11.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-05-15T23:59:59Z" + version: 934.10.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 934.9.0 + machineTypes: + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 7Gi + name: n1-standard-2 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 85Gi + name: a2-highgpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 170Gi + name: a2-highgpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 340Gi + name: a2-highgpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 680Gi + name: a2-highgpu-8g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-megagpu-16g + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 170Gi + name: a2-ultragpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 340Gi + name: a2-ultragpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 680Gi + name: a2-ultragpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-ultragpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-highgpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-megagpu-8g + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2-highcpu-56 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2-standard-16 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c2-standard-30 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2-standard-56 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c2-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2-standard-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 224Gi + name: c2d-highcpu-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c2d-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2d-highcpu-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 896Gi + name: c2d-highmem-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: c2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: c2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 448Gi + name: c2d-highmem-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 448Gi + name: c2d-standard-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: c2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: c2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2d-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2d-standard-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 352Gi + name: c3-highcpu-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 44Gi + name: c3-highcpu-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3-highcpu-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 88Gi + name: c3-highcpu-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3-highcpu-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 176Gi + name: c3-highcpu-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: c3-highmem-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 176Gi + name: c3-highmem-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3-highmem-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 352Gi + name: c3-highmem-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3-highmem-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: c3-highmem-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176-lssd + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4-lssd + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c3d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 354Gi + name: c3d-highcpu-180 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 59Gi + name: c3d-highcpu-30 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 708Gi + name: c3d-highcpu-360 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 118Gi + name: c3d-highcpu-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 177Gi + name: c3d-highcpu-90 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3d-highmem-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3d-standard-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90-lssd + usable: true + - architecture: amd64 + cpu: "240" + gpu: "0" + memory: 407Gi + name: ct4p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5l-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5l-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5l-hightpu-8t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5lp-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5lp-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5lp-hightpu-8t + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 448Gi + name: ct5p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: e2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: e2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: e2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: e2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: e2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: e2-highmem-2 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: e2-highmem-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: e2-highmem-8 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: e2-medium + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: e2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: e2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: e2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: e2-standard-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: e2-standard-8 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 48Gi + name: g2-standard-12 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: g2-standard-16 + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 96Gi + name: g2-standard-24 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: g2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: g2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: g2-standard-48 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: g2-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: g2-standard-96 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: h3-standard-88 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1433Gi + name: m1-megamem-96 + usable: true + - architecture: amd64 + cpu: "160" + gpu: "0" + memory: 3844Gi + name: m1-ultramem-160 + usable: true + - architecture: amd64 + cpu: "40" + gpu: "0" + memory: 961Gi + name: m1-ultramem-40 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 1922Gi + name: m1-ultramem-80 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 8832Gi + name: m2-hypermem-416 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 5888Gi + name: m2-megamem-416 + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 5888Gi + name: m2-ultramem-208 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 11776Gi + name: m2-ultramem-416 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1250Gi + name: m2-ultramem2x-96 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 600Gi + name: m2-ultramemx-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1952Gi + name: m3-megamem-128 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 976Gi + name: m3-megamem-64 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 3904Gi + name: m3-ultramem-128 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 976Gi + name: m3-ultramem-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 1952Gi + name: m3-ultramem-64 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 14Gi + name: n1-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 28Gi + name: n1-highcpu-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 57Gi + name: n1-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 7Gi + name: n1-highcpu-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 86Gi + name: n1-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 104Gi + name: n1-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 13Gi + name: n1-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 208Gi + name: n1-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 26Gi + name: n1-highmem-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 416Gi + name: n1-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 52Gi + name: n1-highmem-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 624Gi + name: n1-highmem-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 60Gi + name: n1-standard-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 120Gi + name: n1-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 15Gi + name: n1-standard-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 240Gi + name: n1-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 30Gi + name: n1-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 360Gi + name: n1-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2-highcpu-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 864Gi + name: n2-highmem-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2-standard-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 128Gi + name: n2d-highcpu-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 224Gi + name: n2d-highcpu-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2d-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2d-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2d-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2d-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2d-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2d-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2d-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2d-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2d-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2d-standard-2 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 896Gi + name: n2d-standard-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2d-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2d-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2d-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2d-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2d-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: n4-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: n4-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: n4-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: n4-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: n4-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: n4-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: n4-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 160Gi + name: n4-highcpu-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n4-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n4-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n4-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n4-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n4-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n4-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n4-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n4-highmem-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n4-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n4-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n4-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n4-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n4-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n4-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n4-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n4-standard-80 + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2a-standard-16 + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2a-standard-2 + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2a-standard-32 + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2a-standard-4 + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2a-standard-48 + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2a-standard-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2d-standard-48 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: t2d-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: z3-highmem-176 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: z3-highmem-88 + usable: true + providerConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: CloudProfileConfig + machineImages: + - name: gardenlinux + versions: + - architecture: amd64 + image: images/gardenlinux-amd64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-3-c261f887 + version: 1443.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-3-c261f887 + version: 1443.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-2-77c8471d + version: 1312.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-2-77c8471d + version: 1312.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-11-75132b8 + version: 934.11.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-10-f057c9b + version: 934.10.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-9-54c63e5 + version: 934.9.0 + regions: + - name: africa-south1 + zones: + - name: africa-south1-a + - name: africa-south1-b + - name: africa-south1-c + - name: asia-east1 + zones: + - name: asia-east1-a + - name: asia-east1-b + - name: asia-east1-c + - name: asia-east2 + zones: + - name: asia-east2-a + - name: asia-east2-b + - name: asia-east2-c + - name: asia-northeast1 + zones: + - name: asia-northeast1-a + - name: asia-northeast1-b + - name: asia-northeast1-c + - name: asia-northeast2 + zones: + - name: asia-northeast2-a + - name: asia-northeast2-b + - name: asia-northeast2-c + - name: asia-northeast3 + zones: + - name: asia-northeast3-a + - name: asia-northeast3-b + - name: asia-northeast3-c + - name: asia-south1 + zones: + - name: asia-south1-a + - name: asia-south1-b + - name: asia-south1-c + - name: asia-south2 + zones: + - name: asia-south2-a + - name: asia-south2-b + - name: asia-south2-c + - name: asia-southeast1 + zones: + - name: asia-southeast1-a + - name: asia-southeast1-b + - name: asia-southeast1-c + - name: asia-southeast2 + zones: + - name: asia-southeast2-a + - name: asia-southeast2-b + - name: asia-southeast2-c + - name: australia-southeast1 + zones: + - name: australia-southeast1-a + - name: australia-southeast1-b + - name: australia-southeast1-c + - name: australia-southeast2 + zones: + - name: australia-southeast2-a + - name: australia-southeast2-b + - name: australia-southeast2-c + - name: europe-central2 + zones: + - name: europe-central2-a + - name: europe-central2-b + - name: europe-central2-c + - name: europe-north1 + zones: + - name: europe-north1-a + - name: europe-north1-b + - name: europe-north1-c + - name: europe-southwest1 + zones: + - name: europe-southwest1-a + - name: europe-southwest1-b + - name: europe-southwest1-c + - name: europe-west1 + zones: + - name: europe-west1-b + - name: europe-west1-c + - name: europe-west1-d + - name: europe-west10 + zones: + - name: europe-west10-a + - name: europe-west10-b + - name: europe-west10-c + - name: europe-west12 + zones: + - name: europe-west12-a + - name: europe-west12-b + - name: europe-west12-c + - name: europe-west2 + zones: + - name: europe-west2-a + - name: europe-west2-b + - name: europe-west2-c + - name: europe-west3 + zones: + - name: europe-west3-a + - name: europe-west3-b + - name: europe-west3-c + - name: europe-west4 + zones: + - name: europe-west4-a + - name: europe-west4-b + - name: europe-west4-c + - name: europe-west5 + zones: + - name: europe-west5-a + - name: europe-west5-b + - name: europe-west5-c + - name: europe-west6 + zones: + - name: europe-west6-a + - name: europe-west6-b + - name: europe-west6-c + - name: europe-west8 + zones: + - name: europe-west8-a + - name: europe-west8-b + - name: europe-west8-c + - name: europe-west9 + zones: + - name: europe-west9-a + - name: europe-west9-b + - name: europe-west9-c + - name: me-central1 + zones: + - name: me-central1-a + - name: me-central1-b + - name: me-central1-c + - name: me-central2 + zones: + - name: me-central2-a + - name: me-central2-b + - name: me-central2-c + - name: me-west1 + zones: + - name: me-west1-a + - name: me-west1-b + - name: me-west1-c + - name: northamerica-northeast1 + zones: + - name: northamerica-northeast1-a + - name: northamerica-northeast1-b + - name: northamerica-northeast1-c + - name: northamerica-northeast2 + zones: + - name: northamerica-northeast2-a + - name: northamerica-northeast2-b + - name: northamerica-northeast2-c + - name: southamerica-east1 + zones: + - name: southamerica-east1-a + - name: southamerica-east1-b + - name: southamerica-east1-c + - name: southamerica-west1 + zones: + - name: southamerica-west1-a + - name: southamerica-west1-b + - name: southamerica-west1-c + - name: us-central1 + zones: + - name: us-central1-a + - name: us-central1-b + - name: us-central1-c + - name: us-central1-f + - name: us-east1 + zones: + - name: us-east1-b + - name: us-east1-c + - name: us-east1-d + - name: us-east4 + zones: + - name: us-east4-a + - name: us-east4-b + - name: us-east4-c + - name: us-east5 + zones: + - name: us-east5-a + - name: us-east5-b + - name: us-east5-c + - name: us-south1 + zones: + - name: us-south1-a + - name: us-south1-b + - name: us-south1-c + - name: us-west1 + zones: + - name: us-west1-a + - name: us-west1-b + - name: us-west1-c + - name: us-west2 + zones: + - name: us-west2-a + - name: us-west2-b + - name: us-west2-c + - name: us-west3 + zones: + - name: us-west3-a + - name: us-west3-b + - name: us-west3-c + - name: us-west4 + zones: + - name: us-west4-a + - name: us-west4-b + - name: us-west4-c + type: gcp + volumeTypes: + - class: premium + minSize: 20Gi + name: pd-balanced + usable: true + - class: standard + minSize: 20Gi + name: pd-standard + usable: true + - class: premium + minSize: 20Gi + name: pd-ssd + usable: true diff --git a/internal/controller/core/apiserver/config/testdata/garden_cluster_2/project-bar.yaml b/internal/controller/core/apiserver/config/testdata/garden_cluster_2/project-bar.yaml new file mode 100644 index 0000000..798a6f8 --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/garden_cluster_2/project-bar.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: bar +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-bar + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/config/testdata/garden_cluster_2/project-foo.yaml b/internal/controller/core/apiserver/config/testdata/garden_cluster_2/project-foo.yaml new file mode 100644 index 0000000..bf43eec --- /dev/null +++ b/internal/controller/core/apiserver/config/testdata/garden_cluster_2/project-foo.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: foo +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-foo + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/config/utils.go b/internal/controller/core/apiserver/config/utils.go new file mode 100644 index 0000000..b0a35f0 --- /dev/null +++ b/internal/controller/core/apiserver/config/utils.go @@ -0,0 +1,26 @@ +package config + +import ( + "fmt" + "os" + + "sigs.k8s.io/yaml" +) + +// LoadConfig reads the configuration file from a given path and parses it into an APIServerProviderConfiguration object. +func LoadConfig(path string) (*APIServerProviderConfiguration, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading config file: %w", err) + } + return LoadConfigFromBytes(data) +} + +func LoadConfigFromBytes(data []byte) (*APIServerProviderConfiguration, error) { + cfg := &APIServerProviderConfiguration{} + err := yaml.Unmarshal(data, cfg) + if err != nil { + return nil, fmt.Errorf("error parsing config file: %w", err) + } + return cfg, nil +} diff --git a/internal/controller/core/apiserver/controller.go b/internal/controller/core/apiserver/controller.go new file mode 100644 index 0000000..a6c370b --- /dev/null +++ b/internal/controller/core/apiserver/controller.go @@ -0,0 +1,200 @@ +package apiserver + +import ( + "cmp" + "context" + "fmt" + "strings" + "time" + + "github.com/openmcp-project/mcp-operator/internal/utils" + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + apiserverconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/config" + apiserverhandler "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/handler" + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/handler/gardener" + + "github.com/openmcp-project/controller-utils/pkg/logging" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/sets" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +const ControllerName = "APIServer" + +func (r *APIServerProvider) GetAPIServerHandlerForType(ctx context.Context, t openmcpv1alpha1.APIServerType, cfg apiserverconfig.CompletedAPIServerProviderConfiguration) (apiserverhandler.APIServerHandler, error) { + log := logging.FromContextOrPanic(ctx) + switch t { + case openmcpv1alpha1.Gardener, openmcpv1alpha1.GardenerDedicated: + log.Debug(fmt.Sprintf("APIServer has type %s, loading corresponding connector", string(t))) + return gardener.NewGardenerConnector(cfg.CompletedCommonConfig, cfg.GardenerConfig, t) + case "Fake": + if r.FakeHandler != nil { + return r.FakeHandler, nil + } + } + return nil, fmt.Errorf("unknown API server type '%s'", string(t)) +} + +func NewAPIServerProvider(ctx context.Context, client client.Client, cfg *apiserverconfig.APIServerProviderConfiguration) (*APIServerProvider, error) { + log, ctx := utils.InitializeControllerLogger(ctx, ControllerName) + ccfg, err := cfg.Complete(ctx) + if err != nil { + return nil, fmt.Errorf("error completing config: %w", err) + } + + if ccfg.GardenerConfig != nil { + log.Info("APIServer handler for type 'Gardener' configured") + } + + return &APIServerProvider{ + CompletedAPIServerProviderConfiguration: *ccfg, + Client: client, + }, nil +} + +// APIServerProvider reconciles a ManagedControlPlane object +type APIServerProvider struct { + apiserverconfig.CompletedAPIServerProviderConfiguration + + // Client is the registration cluster client. + Client client.Client + + // FakeHandler is a fake APIServerHandler for testing purposes. + // It should only be non-nil in tests. + FakeHandler apiserverhandler.APIServerHandler +} + +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=apiservers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=apiservers/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=apiservers/finalizers,verbs=update + +func (r *APIServerProvider) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log, ctx := utils.InitializeControllerLogger(ctx, ControllerName) + log.Debug(cconst.MsgStartReconcile) + + rr := r.reconcile(ctx, req) + rr.LogRequeue(log, logging.DEBUG) + if rr.Component == nil { + return rr.Result, rr.ReconcileError + } + return componentutils.UpdateStatus(ctx, r.Client, rr) +} + +func (r *APIServerProvider) reconcile(ctx context.Context, req ctrl.Request) componentutils.ReconcileResult[*openmcpv1alpha1.APIServer] { + log := logging.FromContextOrPanic(ctx) + + // get internal APIServer resource + as := &openmcpv1alpha1.APIServer{} + if err := r.Client.Get(ctx, req.NamespacedName, as); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("Resource not found") + return componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{} + } + return componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("unable to get resource '%s' from cluster: %w", req.NamespacedName.String(), err), cconst.ReasonCrateClusterInteractionProblem)} + } + + // handle operation annotation + if as.GetAnnotations() != nil { + op, ok := as.GetAnnotations()[openmcpv1alpha1.OperationAnnotation] + if ok { + switch op { + case openmcpv1alpha1.OperationAnnotationValueIgnore: + log.Info("Ignoring resource due to ignore operation annotation") + return componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{} + case openmcpv1alpha1.OperationAnnotationValueReconcile: + log.Debug("Removing reconcile operation annotation from resource") + if err := componentutils.PatchAnnotation(ctx, r.Client, as, openmcpv1alpha1.OperationAnnotation, "", componentutils.ANNOTATION_DELETE); err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing operation annotation: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + } + } + + deleteAPIServer := false + deletionWaitingForDependenciesMsg := "" + if !as.DeletionTimestamp.IsZero() { + log.Info("Deleting APIServer") + if componentutils.HasAnyDependencyFinalizer(as) { + depString := strings.Join(sets.List(componentutils.GetDependents(as)), ", ") + log.Info("APIServer cannot be deleted, because it still contains dependency finalizers", "dependingComponents", depString) + deletionWaitingForDependenciesMsg = fmt.Sprintf("Deletion is waiting for the following dependencies to be removed: [%s]", depString) + } else { + deleteAPIServer = true + } + } else { + log.Info("Triggering creation/update of APIServer") + + old := as.DeepCopy() + if controllerutil.AddFinalizer(as, openmcpv1alpha1.APIServerComponent.Finalizer()) { + if err := r.Client.Patch(ctx, as, client.MergeFrom(old)); err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{Component: as, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error patching finalizer on APIServer: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + } + + apiServerHandler, err := r.GetAPIServerHandlerForType(ctx, as.Spec.Type, r.CompletedAPIServerProviderConfiguration) + if err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{Component: as, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error getting APIServer handler: %w", err), cconst.ReasonConfigurationProblem)} + } + ctx = logging.NewContext(ctx, log.WithValues("apiServerType", string(as.Spec.Type))) + + old := as.DeepCopy() + var res ctrl.Result + var usf apiserverhandler.UpdateStatusFunc + var cons []openmcpv1alpha1.ComponentCondition + var errr openmcperrors.ReasonableError + if !deleteAPIServer { + res, usf, cons, errr = apiServerHandler.HandleCreateOrUpdate(ctx, as, r.Client) + } else { + res, usf, cons, errr = apiServerHandler.HandleDelete(ctx, as, r.Client) + } + errs := openmcperrors.NewReasonableErrorList(errr) + + if usf != nil { + errs.Append(usf(&as.Status)) + } + + if deletionWaitingForDependenciesMsg != "" { + // we are waiting for one or more dependencies to be deleted + return componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{Component: as, OldComponent: old, Result: ctrl.Result{Requeue: res.Requeue, RequeueAfter: minExceptZero(res.RequeueAfter, 60*time.Second)}, ReconcileError: errs.Aggregate(), Reason: cconst.ReasonDeletionWaitingForDependingComponents, Message: deletionWaitingForDependenciesMsg, Conditions: cons} + } + + if deleteAPIServer && componentutils.AllConditionsTrue(cons...) { + old := as.DeepCopy() + changed := controllerutil.RemoveFinalizer(as, openmcpv1alpha1.APIServerComponent.Finalizer()) + if changed { + if err := r.Client.Patch(ctx, as, client.MergeFrom(old)); err != nil { + errs.Append(fmt.Errorf("error removing finalizer from APIServer: %w", err)) + } + } + } + + return componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{Component: as, OldComponent: old, Result: res, ReconcileError: errs.Aggregate(), Conditions: cons} +} + +// SetupWithManager sets up the controller with the Manager. +func (r *APIServerProvider) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&openmcpv1alpha1.APIServer{}). + WithEventFilter(componentutils.DefaultComponentControllerPredicates()). + Complete(r) +} + +// minExceptZero works like the builtin 'min' function, but will only return the zero value if all arguments are zero. +func minExceptZero[T cmp.Ordered](x T, y ...T) T { + var zero T + min := x + for _, v := range y { + if v != zero && (min == zero || v < min) { + min = v + } + } + return min +} diff --git a/internal/controller/core/apiserver/controller_test.go b/internal/controller/core/apiserver/controller_test.go new file mode 100644 index 0000000..5201b51 --- /dev/null +++ b/internal/controller/core/apiserver/controller_test.go @@ -0,0 +1,317 @@ +package apiserver_test + +import ( + "context" + "fmt" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver" + apiserverhandler "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/handler" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +func getReconciler(c ...client.Client) reconcile.Reconciler { + r, err := apiserver.NewAPIServerProvider(constructorContext, c[0], defaultConfig) + Expect(err).NotTo(HaveOccurred()) + r.FakeHandler = fakeHandler + return r +} + +const ( + apiServerReconciler = "apiserver" + mockReadyConditionType = "mockReady" +) + +func testEnvSetup(testDirPathSegments ...string) *testing.ComplexEnvironment { + return testutils.DefaultTestSetupBuilder(testDirPathSegments...).WithFakeClient(testutils.APIServerCluster, testutils.Scheme).WithReconcilerConstructor(apiServerReconciler, getReconciler, testutils.CrateCluster).Build() +} + +func mockReadyConditions(ready bool) []openmcpv1alpha1.ComponentCondition { + return []openmcpv1alpha1.ComponentCondition{ + { + Type: mockReadyConditionType, + Status: openmcpv1alpha1.ComponentConditionStatusFromBool(ready), + LastTransitionTime: metav1.Now(), + }, + } +} + +var _ = Describe("CO-1153 APIServer Controller", func() { + It("should do nothing if the reconciled resource is not found", func() { + env := testEnvSetup() + + env.ShouldReconcile(apiServerReconciler, reconcile.Request{NamespacedName: types.NamespacedName{Name: "test", Namespace: "test"}}) + }) + + It("should handle the ignore annotation", func() { + env := testEnvSetup("testdata", "test-01") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + old := as.DeepCopy() + + req := testing.RequestFromObject(as) + _ = env.ShouldReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Status.Conditions).To(HaveLen(0)) + Expect(old).To(Equal(as)) + }) + + It("should handle the reconcile annotation", func() { + env := testEnvSetup("testdata", "test-02") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + Expect(as.Annotations).To(HaveKeyWithValue(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile)) + + fakeHandler.MockHandleCreateOrUpdateCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, nil, mockReadyConditions(true), nil + }) + req := testing.RequestFromObject(as) + _ = env.ShouldReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Annotations).ToNot(HaveKeyWithValue(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile)) + }) + + It("should not be deleted when it has a dependency finalizer", func() { + env := testEnvSetup("testdata", "test-03") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, as) + Expect(err).ToNot(HaveOccurred()) + + fakeHandler.MockHandleCreateOrUpdateCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, nil, mockReadyConditions(true), nil + }) + req := testing.RequestFromObject(as) + _ = env.ShouldReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Finalizers).To(ContainElement(openmcpv1alpha1.LandscaperComponent.DependencyFinalizer())) + }) + + It("should fail for unknown APIServer types", func() { + env := testEnvSetup("testdata", "test-04") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(as) + _ = env.ShouldNotReconcile(apiServerReconciler, req) + }) + + It("should add the APIServer finalizer if it does not yet exist", func() { + env := testEnvSetup("testdata", "test-05") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + fakeHandler.MockHandleCreateOrUpdateCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, nil, mockReadyConditions(true), nil + }) + req := testing.RequestFromObject(as) + _ = env.ShouldReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Finalizers).To(ContainElement(openmcpv1alpha1.APIServerComponent.Finalizer())) + }) + + It("should propagate the error from HandleCreateOrUpdate to the APIServer status", func() { + env := testEnvSetup("testdata", "test-05") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + fakeHandler.MockHandleCreateOrUpdateCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, nil, mockReadyConditions(false), openmcperrors.WithReason(fmt.Errorf("test error"), "test reason") + }) + req := testing.RequestFromObject(as) + _ = env.ShouldNotReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + Message: cconst.MessageReconciliationError, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: "test reason", + Message: "test error", + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: mockReadyConditionType, + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + )) + }) + + It("should not remove the finalizer if the deletion did not finish", func() { + env := testEnvSetup("testdata", "test-06") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + fakeHandler.MockHandleDeleteCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, nil, mockReadyConditions(false), nil + }) + req := testing.RequestFromObject(as) + _ = env.ShouldReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Finalizers).To(ContainElement(openmcpv1alpha1.APIServerComponent.Finalizer())) + }) + + It("should remove the finalizer if the deletion finished", func() { + env := testEnvSetup("testdata", "test-06") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + fakeHandler.MockHandleDeleteCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, nil, mockReadyConditions(true), nil + }) + req := testing.RequestFromObject(as) + _ = env.ShouldReconcile(apiServerReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + + It("should propagate the error from HandleDelete to the APIServer status", func() { + env := testEnvSetup("testdata", "test-06") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + fakeHandler.MockHandleDeleteCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, nil, mockReadyConditions(false), openmcperrors.WithReason(fmt.Errorf("test error"), "test reason") + }) + req := testing.RequestFromObject(as) + _ = env.ShouldNotReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + Message: cconst.MessageReconciliationError, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: "test reason", + Message: "test error", + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: mockReadyConditionType, + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + )) + }) + + It("should call the UpdateStatusFunc if returned by HandleCreateOrUpdate", func() { + env := testEnvSetup("testdata", "test-05") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + fakeHandler.MockHandleCreateOrUpdateCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, func(dps *openmcpv1alpha1.APIServerStatus) error { + dps.GardenerStatus = &openmcpv1alpha1.GardenerStatus{ + Shoot: &runtime.RawExtension{ + Raw: []byte(`{"apiVersion":"garden.sapcloud.io/v1beta1","kind":"Shoot","metadata":{"name":"foo","namespace":"bar"}}`), + }, + } + dps.ExternalAPIServerStatus = &openmcpv1alpha1.ExternalAPIServerStatus{ + Endpoint: "https://k8s-external.ondemand.com", + ServiceAccountIssuer: "https://k8s-sa.ondemand.com", + } + return nil + }, mockReadyConditions(true), nil + }) + req := testing.RequestFromObject(as) + _ = env.ShouldReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Status.GardenerStatus).ToNot(BeNil()) + Expect(as.Status.GardenerStatus.Shoot).ToNot(BeNil()) + uShoot, err := as.Status.GardenerStatus.GetShoot() + Expect(err).NotTo(HaveOccurred()) + Expect(uShoot.GetName()).To(Equal("foo")) + Expect(uShoot.GetNamespace()).To(Equal("bar")) + Expect(as.Status.ExternalAPIServerStatus.Endpoint).To(Equal("https://k8s-external.ondemand.com")) + Expect(as.Status.ExternalAPIServerStatus.ServiceAccountIssuer).To(Equal("https://k8s-sa.ondemand.com")) + }) + + It("should call the UpdateStatusFunc if returned by HandleDelete", func() { + env := testEnvSetup("testdata", "test-06") + + as := &openmcpv1alpha1.APIServer{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + fakeHandler.MockHandleDeleteCall(func(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + return reconcile.Result{}, func(dps *openmcpv1alpha1.APIServerStatus) error { + dps.GardenerStatus = &openmcpv1alpha1.GardenerStatus{ + Shoot: &runtime.RawExtension{ + Raw: []byte(`{"apiVersion":"garden.sapcloud.io/v1beta1","kind":"Shoot","metadata":{"name":"foo","namespace":"bar"}}`), + }, + } + dps.ExternalAPIServerStatus = &openmcpv1alpha1.ExternalAPIServerStatus{ + Endpoint: "https://k8s-external.ondemand.com", + ServiceAccountIssuer: "https://k8s-sa.ondemand.com", + } + return nil + }, mockReadyConditions(false), nil + }) + req := testing.RequestFromObject(as) + _ = env.ShouldReconcile(apiServerReconciler, req) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Status.GardenerStatus).ToNot(BeNil()) + Expect(as.Status.GardenerStatus.Shoot).ToNot(BeNil()) + uShoot, err := as.Status.GardenerStatus.GetShoot() + Expect(err).NotTo(HaveOccurred()) + Expect(uShoot.GetName()).To(Equal("foo")) + Expect(uShoot.GetNamespace()).To(Equal("bar")) + Expect(as.Status.ExternalAPIServerStatus.Endpoint).To(Equal("https://k8s-external.ondemand.com")) + Expect(as.Status.ExternalAPIServerStatus.ServiceAccountIssuer).To(Equal("https://k8s-sa.ondemand.com")) + }) + +}) diff --git a/internal/controller/core/apiserver/handler/gardener/connector.go b/internal/controller/core/apiserver/handler/gardener/connector.go new file mode 100644 index 0000000..411f6b2 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/connector.go @@ -0,0 +1,486 @@ +package gardener + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/openmcp-project/mcp-operator/internal/utils" + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + apiserverconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/config" + apiserverhandler "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/handler" + + "github.com/openmcp-project/controller-utils/pkg/logging" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" + "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1/constants" +) + +const ( + GardenerDeletionConfirmationAnnotation = "confirmation.gardener.cloud/deletion" +) + +var _ apiserverhandler.APIServerHandler = &GardenerConnector{} + +type GardenerConnector struct { + apiserverconfig.CompletedMultiGardenerConfiguration + Common *apiserverconfig.CompletedCommonConfig + APIServerType openmcpv1alpha1.APIServerType +} + +func NewGardenerConnector(cc *apiserverconfig.CompletedCommonConfig, cfg *apiserverconfig.CompletedMultiGardenerConfiguration, apiServerType openmcpv1alpha1.APIServerType) (*GardenerConnector, openmcperrors.ReasonableError) { + if cfg == nil { + return nil, openmcperrors.WithReason(fmt.Errorf("APIServer handler for type 'Gardener' is not configured"), cconst.ReasonConfigurationProblem) + } + return &GardenerConnector{ + CompletedMultiGardenerConfiguration: *cfg, + Common: cc, + APIServerType: apiServerType, + }, nil +} + +// GetShoot tries to fetch the corresponding shoot cluster. +// If there is a shoot reference in the InternalControlPlane's status, but the shoot is not found, an error is returned unless inDeletion is true. +// If there is no shoot reference, the function searches for a shoot with a fitting back-reference and returns that, if found. +// Otherwise, nil is returned. +func (gc *GardenerConnector) GetShoot(ctx context.Context, as *openmcpv1alpha1.APIServer, inDeletion bool) (*gardenv1beta1.Shoot, openmcperrors.ReasonableError) { + log := logging.FromContextOrPanic(ctx) + var sh *gardenv1beta1.Shoot + + lc := "" + if as.Spec.Internal != nil && as.Spec.Internal.GardenerConfig != nil { + lc = as.Spec.Internal.GardenerConfig.LandscapeConfiguration + } + gls, gcfg, err := gc.LandscapeConfiguration(lc) + if err != nil { + return nil, openmcperrors.WithReason(err, cconst.ReasonConfigurationProblem) + } + + uShoot, err := as.Status.GardenerStatus.GetShoot() + if err != nil { + return nil, openmcperrors.WithReason(err, cconst.ReasonShootIdentificationNotPossible) + } + if uShoot != nil { + sh = &gardenv1beta1.Shoot{} + sh.SetName(uShoot.GetName()) + sh.SetNamespace(uShoot.GetNamespace()) + log.Debug("Found shoot reference", "shoot", client.ObjectKeyFromObject(sh).String()) + if err := gls.Client.Get(ctx, client.ObjectKeyFromObject(sh), sh); err != nil { + if inDeletion && apierrors.IsNotFound(err) { + return nil, nil + } + return nil, openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + } else { + // Check if the status has somehow been lost, but there is a Shoot referencing this ManagedControlPlane + log.Debug("No reference to shoot found, searching for shoot with matching back-reference") + shoots := &gardenv1beta1.ShootList{} + if err := gls.Client.List(ctx, shoots, client.MatchingLabels{ + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName: as.Name, + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace: as.Namespace, + }, client.InNamespace(gcfg.ProjectNamespace)); err != nil { + return nil, openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + + if len(shoots.Items) > 0 { + if len(shoots.Items) > 1 { + return nil, openmcperrors.WithReason(fmt.Errorf("found %d Shoots referencing ManagedControlPlane '%s', there should never be more than one", len(shoots.Items), client.ObjectKeyFromObject(as).String()), cconst.ReasonShootIdentificationNotPossible) + } + sh = &shoots.Items[0] + log.Debug("Found a Shoot with a matching back-reference", "shoot", client.ObjectKeyFromObject(sh).String()) + } + } + + return sh, nil +} + +func (gc *GardenerConnector) HandleCreateOrUpdate(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client) (ctrl.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + log := logging.FromContextOrPanic(ctx).WithName("GardenerConnector") + ctx = logging.NewContext(ctx, log) + + lc := "" + if as.Spec.Internal != nil && as.Spec.Internal.GardenerConfig != nil { + lc = as.Spec.Internal.GardenerConfig.LandscapeConfiguration + } + gls, gcfg, err := gc.LandscapeConfiguration(lc) + if err != nil { + return ctrl.Result{}, nil, gardenerConditions(false, cconst.ReasonConfigurationProblem, err.Error()), openmcperrors.WithReason(err, cconst.ReasonConfigurationProblem) + } + + // check if shoot already exists + sh, errr := gc.GetShoot(ctx, as, false) + if errr != nil { + log.Error(errr, "error checking for corresponding shoot") + } + + auditLogShootAnnotations, auditLogErr := gc.reconcileAuditLogResources(ctx, as, gc.GetShootName(sh, as, gcfg), crateClient, gls, gcfg) + if auditLogErr != nil { + return ctrl.Result{}, nil, gardenerConditions(false, cconst.ReasonAuditLogProblem, auditLogErr.Error()), auditLogErr + } + + shootReady := false + shootNotReadyMessage := "" + var updateShootManifestInStatusFunc func(status *openmcpv1alpha1.APIServerStatus) error + if sh == nil { + log.Debug("No existing shoot found, creating a new one") + sh = &gardenv1beta1.Shoot{} + if err := gc.Shoot_v1beta1_from_APIServer_v1alpha1(ctx, as, sh); err != nil { + return ctrl.Result{}, nil, gardenerConditions(false, cconst.ReasonConfigurationProblem, err.Error()), openmcperrors.WithReason(err, cconst.ReasonConfigurationProblem) + } + updateShootManifestInStatusFunc = func(status *openmcpv1alpha1.APIServerStatus) error { + status.GardenerStatus = &openmcpv1alpha1.GardenerStatus{} + return InjectShootManifestInGardenerStatus(status.GardenerStatus, sh) + } + if err := gls.Client.Create(ctx, sh); err != nil { + return ctrl.Result{}, updateShootManifestInStatusFunc, gardenerConditions(false, cconst.ReasonGardenClusterInteractionProblem, err.Error()), openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + } else { + log.Debug("Updating existing shoot", "shoot", client.ObjectKeyFromObject(sh).String()) + if err := gc.Shoot_v1beta1_from_APIServer_v1alpha1(ctx, as, sh); err != nil { + return ctrl.Result{}, nil, gardenerConditions(false, cconst.ReasonConfigurationProblem, err.Error()), openmcperrors.WithReason(err, cconst.ReasonConfigurationProblem) + } + + if sh.Annotations == nil { + sh.Annotations = make(map[string]string) + } + + for k, v := range auditLogShootAnnotations { + sh.Annotations[k] = v + } + + updateShootManifestInStatusFunc = func(status *openmcpv1alpha1.APIServerStatus) error { + status.GardenerStatus = &openmcpv1alpha1.GardenerStatus{} + return InjectShootManifestInGardenerStatus(status.GardenerStatus, sh) + } + if err := gls.Client.Update(ctx, sh); err != nil { + if apierrors.IsConflict(err) { + log.Error(err, "Conflict updating shoot") + return ctrl.Result{Requeue: true}, updateShootManifestInStatusFunc, gardenerConditions(false, cconst.ReasonGardenClusterInteractionProblem, err.Error()), openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + + log.Error(err, "Error updating shoot") + return ctrl.Result{}, updateShootManifestInStatusFunc, gardenerConditions(false, cconst.ReasonGardenClusterInteractionProblem, err.Error()), openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + shootReady, shootNotReadyMessage = isShootReady(sh) + } + log = log.WithValues("shoot", client.ObjectKeyFromObject(sh).String()) + + var adminAccess *openmcpv1alpha1.APIServerAccess + res := ctrl.Result{} + if shootReady { + log.Debug("Shoot is ready") + adminAccess, res.RequeueAfter, err = apiserverhandler.GetClusterAccess(ctx, gc.Common.ServiceAccountNamespace, gc.Common.AdminServiceAccountName, as.Status.AdminAccess, &gardenerClusterAccessEnabler{ + gardenClient: gls.Client, + shoot: sh, + }) + if err != nil { + err = fmt.Errorf("error creating kubeconfigs for shoot cluster: %w", err) + return ctrl.Result{}, updateShootManifestInStatusFunc, gardenerConditions(false, cconst.ReasonAPIServerAccessProvisioningNotPossible, err.Error()), openmcperrors.WithReason(err, cconst.ReasonAPIServerAccessProvisioningNotPossible) + } + } else { + log.Debug("Shoot is not ready yet, requeueing APIServer") + res.RequeueAfter = 60 * time.Second + } + + usf := func(status *openmcpv1alpha1.APIServerStatus) error { + if updateShootManifestInStatusFunc != nil { + if err := updateShootManifestInStatusFunc(status); err != nil { + return err + } + } + + if status.ExternalAPIServerStatus == nil { + status.ExternalAPIServerStatus = &openmcpv1alpha1.ExternalAPIServerStatus{} + } + for _, endpoint := range sh.Status.AdvertisedAddresses { + switch endpoint.Name { + case constants.AdvertisedAddressExternal: + status.ExternalAPIServerStatus.Endpoint = endpoint.URL + case constants.AdvertisedAddressInternal: + case constants.AdvertisedAddressServiceAccountIssuer: + status.ExternalAPIServerStatus.ServiceAccountIssuer = endpoint.URL + default: + log.Error(nil, "unexpected endpoint name in shoot's advertised addresses", "endpoint", endpoint.Name) + } + } + + if adminAccess != nil { + status.AdminAccess = adminAccess + } + + return nil + } + + conRsn := "" + conMsg := strings.Builder{} + if sh.Status.LastOperation != nil { + conMsg.WriteString(fmt.Sprintf("[%s: %s] %s", sh.Status.LastOperation.Type, sh.Status.LastOperation.State, sh.Status.LastOperation.Description)) + } + if !shootReady { + conRsn = cconst.ReasonWaitingForGardenerShoot + if conMsg.Len() > 0 { + conMsg.WriteString("\n") + } + conMsg.WriteString(shootNotReadyMessage) + } + + return res, usf, gardenerConditions(shootReady, conRsn, conMsg.String()), nil +} + +func (gc *GardenerConnector) HandleDelete(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client) (ctrl.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) { + log := logging.FromContextOrPanic(ctx).WithName("GardenerConnector") + ctx = logging.NewContext(ctx, log) + // check if shoot already exists + sh, err := gc.GetShoot(ctx, as, true) + if err != nil { + err = openmcperrors.Errorf("error fetching corresponding shoot: %w", err, err) + return ctrl.Result{}, nil, gardenerConditions(false, cconst.ReasonGardenClusterInteractionProblem, err.Error()), err + } + + if sh == nil { + // shoot is gone, cleanup is finished + log.Debug("Shoot has been deleted") + return ctrl.Result{}, func(status *openmcpv1alpha1.APIServerStatus) error { + status.AdminAccess = nil + if status.GardenerStatus != nil { + status.GardenerStatus.Shoot = nil + } + return nil + }, gardenerConditions(true, "", "Shoot has been deleted."), nil + } + + lc := "" + if as.Spec.Internal != nil && as.Spec.Internal.GardenerConfig != nil { + lc = as.Spec.Internal.GardenerConfig.LandscapeConfiguration + } + gls, gcfg, errr := gc.LandscapeConfiguration(lc) + if errr != nil { + return ctrl.Result{}, nil, gardenerConditions(false, cconst.ReasonConfigurationProblem, errr.Error()), openmcperrors.WithReason(errr, cconst.ReasonConfigurationProblem) + } + + if as.Spec.GardenerConfig != nil { + as.Spec.GardenerConfig.AuditLog = nil + } + // delete the Audit Log resources + if _, err := gc.reconcileAuditLogResources(ctx, as, sh.Name, crateClient, gls, gcfg); err != nil { + return ctrl.Result{}, nil, gardenerConditions(false, cconst.ReasonAuditLogProblem, err.Error()), err + + } + + if sh.DeletionTimestamp.IsZero() { + // delete the shoot cluster + log.Debug("Deleting shoot", "shoot", client.ObjectKeyFromObject(sh).String()) + if err := componentutils.PatchAnnotation(ctx, gls.Client, sh, GardenerDeletionConfirmationAnnotation, "true", componentutils.ANNOTATION_OVERWRITE); err != nil { + errr := openmcperrors.WithReason(fmt.Errorf("error patching deletion confirmation annotation onto shoot: %w", err), cconst.ReasonGardenClusterInteractionProblem) + return ctrl.Result{}, nil, gardenerConditions(false, errr.Reason(), errr.Error()), errr + } + if err := gls.Client.Delete(ctx, sh); err != nil { + errr := openmcperrors.WithReason(fmt.Errorf("error deleting shoot: %w", err), cconst.ReasonGardenClusterInteractionProblem) + return ctrl.Result{}, nil, gardenerConditions(false, errr.Reason(), errr.Error()), errr + } + } + + conMsg := "Waiting for shoot cluster to be deleted." + if sh.Status.LastOperation != nil { + conMsg = fmt.Sprintf("[%s: %s] %s", sh.Status.LastOperation.Type, sh.Status.LastOperation.State, sh.Status.LastOperation.Description) + } + + return ctrl.Result{RequeueAfter: 60 * time.Second}, nil, gardenerConditions(false, cconst.ReasonWaitingForGardenerShoot, conMsg), nil +} + +func isShootReady(sh *gardenv1beta1.Shoot) (bool, string) { + if sh.Status.ObservedGeneration != sh.Generation { + return false, "Shoot's observed generation does not match its generation, indicating that it has not yet been reconciled after the last changes have been applied." + } + if len(sh.Status.Conditions) == 0 { + return false, "Shoot is missing conditions." + } + unhealthyConditions := []string{} + for _, con := range sh.Status.Conditions { + if con.Status != gardenv1beta1.ConditionTrue { + unhealthyConditions = append(unhealthyConditions, string(con.Type)) + } + } + if len(unhealthyConditions) > 0 { + return false, fmt.Sprintf("The following shoot conditions are not satisfied: %s", strings.Join(unhealthyConditions, ", ")) + } + return true, "" +} + +func isAuditLogEnabled(as *openmcpv1alpha1.APIServer) bool { + return as.Spec.GardenerConfig != nil && as.Spec.GardenerConfig.AuditLog != nil +} + +type AuditLogAnnotations map[string]string + +// reconcileAuditLogResources reconciles the audit log resources for the given shoot in the Garden cluster. +// If the content of the audit log resources in the Crate cluster has changed, this function will return the annotations that are needed to be set on the shoot. +// Otherwise, the annotations will be nil. +func (gc *GardenerConnector) reconcileAuditLogResources(ctx context.Context, as *openmcpv1alpha1.APIServer, shootName string, crateClient client.Client, gls *apiserverconfig.CompletedGardenerLandscape, gcfg *apiserverconfig.CompletedGardenerConfiguration) (AuditLogAnnotations, openmcperrors.ReasonableError) { + if isAuditLogEnabled(as) { + resultPolicy, err := gc.createOrUpdateAuditLogPolicy(ctx, as, shootName, crateClient, gls, gcfg) + if err != nil { + return nil, openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + resultCreds, err := gc.createOrUpdateAuditLogCredentials(ctx, as, shootName, crateClient, gls, gcfg) + if err != nil { + return nil, openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + if resultPolicy != controllerutil.OperationResultNone || resultCreds != controllerutil.OperationResultNone { + return AuditLogAnnotations{ + constants.GardenerOperation: constants.GardenerOperationReconcile, + }, nil + } + } else { + if err := gc.deleteAuditLogPolicy(ctx, shootName, gls, gcfg); err != nil { + return nil, openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + if err := gc.deleteAuditLogCredentials(ctx, shootName, gls, gcfg); err != nil { + return nil, openmcperrors.WithReason(err, cconst.ReasonGardenClusterInteractionProblem) + } + } + return nil, nil +} + +// createOrUpdateAuditLogPolicy creates or updates the audit log policy ConfigMap for the given shoot. +func (gc *GardenerConnector) createOrUpdateAuditLogPolicy(ctx context.Context, as *openmcpv1alpha1.APIServer, shootName string, crateClient client.Client, gls *apiserverconfig.CompletedGardenerLandscape, gcfg *apiserverconfig.CompletedGardenerConfiguration) (controllerutil.OperationResult, error) { + cmCrate := &corev1.ConfigMap{} + err := crateClient.Get(ctx, types.NamespacedName{Name: as.Spec.GardenerConfig.AuditLog.PolicyRef.Name, Namespace: as.Namespace}, cmCrate) + if err != nil { + return "", err + } + + cmGarden := &corev1.ConfigMap{} + cmGarden.SetName(utils.PrefixWithNamespace(shootName, "auditlog-policy")) + cmGarden.SetNamespace(gcfg.ProjectNamespace) + result, err := controllerutil.CreateOrUpdate(ctx, gls.Client, cmGarden, func() error { + cmGarden.Data = cmCrate.Data + return nil + + }) + return result, err + +} + +// createOrUpdateAuditLogCredentials creates or updates the audit log credentials Secret for the given shoot. +func (gc *GardenerConnector) createOrUpdateAuditLogCredentials(ctx context.Context, as *openmcpv1alpha1.APIServer, shootName string, crateClient client.Client, gls *apiserverconfig.CompletedGardenerLandscape, gcfg *apiserverconfig.CompletedGardenerConfiguration) (controllerutil.OperationResult, error) { + secretCrate := &corev1.Secret{} + err := crateClient.Get(ctx, types.NamespacedName{Name: as.Spec.GardenerConfig.AuditLog.SecretRef.Name, Namespace: as.Namespace}, secretCrate) + if err != nil { + return "", err + } + + secretGarden := &corev1.Secret{} + secretGarden.SetName(utils.PrefixWithNamespace(shootName, "auditlog-credentials")) + secretGarden.SetNamespace(gcfg.ProjectNamespace) + result, err := controllerutil.CreateOrUpdate(ctx, gls.Client, secretGarden, func() error { + secretGarden.Data = secretCrate.Data + secretGarden.Type = secretCrate.Type + return nil + + }) + return result, err +} + +// deleteAuditLogPolicy deletes the audit log policy ConfigMap for the given shoot. +func (gc *GardenerConnector) deleteAuditLogPolicy(ctx context.Context, shootName string, gls *apiserverconfig.CompletedGardenerLandscape, gcfg *apiserverconfig.CompletedGardenerConfiguration) error { + cmGarden := &corev1.ConfigMap{} + cmGarden.SetName(utils.PrefixWithNamespace(shootName, "auditlog-policy")) + cmGarden.SetNamespace(gcfg.ProjectNamespace) + err := gls.Client.Delete(ctx, cmGarden) + if apierrors.IsNotFound(err) { + return nil + } + return err +} + +// deleteAuditLogCredentials deletes the audit log credentials Secret for the given shoot. +func (gc *GardenerConnector) deleteAuditLogCredentials(ctx context.Context, shootName string, gls *apiserverconfig.CompletedGardenerLandscape, gcfg *apiserverconfig.CompletedGardenerConfiguration) error { + secretGarden := &corev1.Secret{} + secretGarden.SetName(utils.PrefixWithNamespace(shootName, "auditlog-credentials")) + secretGarden.SetNamespace(gcfg.ProjectNamespace) + err := gls.Client.Delete(ctx, secretGarden) + if apierrors.IsNotFound(err) { + return nil + } + return err +} + +var _ apiserverhandler.ClusterAccessEnabler = &gardenerClusterAccessEnabler{} + +type gardenerClusterAccessEnabler struct { + gardenClient client.Client + shoot *gardenv1beta1.Shoot + + client client.Client + restCfg *rest.Config + isInitialized bool +} + +func (g *gardenerClusterAccessEnabler) Init(ctx context.Context) error { + if g.isInitialized { + return nil + } + var err error + g.client, g.restCfg, err = getTemporaryClientForShoot(ctx, g.gardenClient, g.shoot) + return err +} + +func (g *gardenerClusterAccessEnabler) Client() client.Client { + return g.client +} + +func (g *gardenerClusterAccessEnabler) RESTConfig() *rest.Config { + return g.restCfg +} + +func gardenerConditions(shootReady bool, reason, message string) []openmcpv1alpha1.ComponentCondition { + conditions := []openmcpv1alpha1.ComponentCondition{ + componentutils.NewCondition(openmcpv1alpha1.APIServerComponent.HealthyCondition(), openmcpv1alpha1.ComponentConditionStatusFromBool(shootReady), reason, message), + } + return conditions +} + +// InjectShootManifestInGardenerStatus takes a GardenerStatus pointer and a shoot object and injects the shoot manifest into the GardenerStatus. +// It removes some metadata fields as well as the shoot's status. +func InjectShootManifestInGardenerStatus(status *openmcpv1alpha1.GardenerStatus, shoot *gardenv1beta1.Shoot) error { + uShoot := &unstructured.Unstructured{} + data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(shoot) + if err != nil { + return fmt.Errorf("unable to convert shoot to unstructured object: %w", err) + } + uShoot.SetUnstructuredContent(data) + // ensure type information is set + uShoot.SetAPIVersion(gardenv1beta1.SchemeGroupVersion.String()) + uShoot.SetKind("Shoot") + // delete fields that should not be part of the shoot manifest in the status + uShoot.SetFinalizers(nil) + uShoot.SetResourceVersion("") + uShoot.SetCreationTimestamp(metav1.Time{}) + uShoot.SetGenerateName("") + uShoot.SetGeneration(0) + uShoot.SetManagedFields(nil) + uShoot.SetDeletionGracePeriodSeconds(nil) + uShoot.SetDeletionTimestamp(nil) + uShoot.SetOwnerReferences(nil) + // remove shoot status + unstructured.RemoveNestedField(uShoot.Object, "status") + + status.Shoot = &runtime.RawExtension{Object: uShoot} + return nil +} diff --git a/internal/controller/core/apiserver/handler/gardener/connector_test.go b/internal/controller/core/apiserver/handler/gardener/connector_test.go new file mode 100644 index 0000000..d7c8f6b --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/connector_test.go @@ -0,0 +1,333 @@ +package gardener_test + +import ( + "fmt" + "time" + + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" + "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1/constants" + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/handler/gardener" + apiserverutils "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/utils" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +var ( + defaultAPIServerType = openmcpv1alpha1.Gardener +) + +var _ = Describe("APIServer Gardener Conversion", func() { + + for cfgType, initGardenerHandlerTest := range map[string]func(openmcpv1alpha1.APIServerType, string, ...string) (*gardener.GardenerConnector, *openmcpv1alpha1.APIServer){ + "single": initGardenerHandlerTestSingle, + "multi": initGardenerHandlerTestMulti, + } { + + Context(fmt.Sprintf("Config Type: %s", cfgType), func() { + + Context("GetShoot", func() { + + It("should fetch the shoot from its reference in the APIServer status", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-01.yaml") + sh1 := &gardenv1beta1.Shoot{} + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh1)).To(Succeed()) + sh2 := &gardenv1beta1.Shoot{} + sh2.SetName("test2") + sh2.SetNamespace(sh1.Namespace) + sh2.SetLabels(sh1.GetLabels()) + sh2.SetAnnotations(sh1.GetAnnotations()) + sh2.Spec = *sh1.Spec.DeepCopy() + sh2.Status = *sh1.Status.DeepCopy() + Expect(env.Client(gardenCluster).Create(env.Ctx, sh2)).To(Succeed()) + shoot, rerr := gc.GetShoot(env.Ctx, as, false) + Expect(rerr).ToNot(HaveOccurred()) + Expect(shoot).To(Equal(sh1)) + }) + + It("should fetch the shoot based on its labels if the APIServer status is lost", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-02.yaml") + compare := &gardenv1beta1.Shoot{} + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, compare)).To(Succeed()) + shoot, rerr := gc.GetShoot(env.Ctx, as, false) + Expect(rerr).ToNot(HaveOccurred()) + Expect(shoot).To(Equal(compare)) + }) + + It("should return nil if the shoot did never exist", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-03.yaml") + shoot, rerr := gc.GetShoot(env.Ctx, as, false) + Expect(rerr).ToNot(HaveOccurred()) + Expect(shoot).To(BeNil()) + }) + + It("should return an error if more than one shoot matches the labels", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-02.yaml") + sh1 := &gardenv1beta1.Shoot{} + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh1)).To(Succeed()) + sh2 := &gardenv1beta1.Shoot{} + sh2.SetName("test2") + sh2.SetNamespace(sh1.Namespace) + sh2.SetLabels(sh1.GetLabels()) + sh2.SetAnnotations(sh1.GetAnnotations()) + sh2.Spec = *sh1.Spec.DeepCopy() + sh2.Status = *sh1.Status.DeepCopy() + Expect(env.Client(gardenCluster).Create(env.Ctx, sh2)).To(Succeed()) + _, rerr := gc.GetShoot(env.Ctx, as, false) + Expect(rerr).To(MatchError(ContainSubstring("more than one"))) + }) + + }) + + Context("HandleCreateOrUpdate", func() { + + It("should add admin access if the shoot is ready", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-04.yaml") + res, usf, cons, err := gc.HandleCreateOrUpdate(env.Ctx, as, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + Expect(res.RequeueAfter).To(BeNumerically(">=", apiserverutils.DefaultAdminAccessValidityTime/2)) + Expect(res.RequeueAfter).To(BeNumerically("<=", apiserverutils.DefaultAdminAccessValidityTime)) + Expect(usf).ToNot(BeNil()) + Expect(as.Status.AdminAccess).To(BeNil()) + Expect(usf(&as.Status)).To(Succeed()) + Expect(as.Status.AdminAccess).ToNot(BeNil()) + }) + + It("should not add admin access if the shoot is not ready", func() { + sh := &gardenv1beta1.Shoot{} + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh)).To(Succeed()) + old := sh.DeepCopy() + sh.Status.Conditions[0].Status = gardenv1beta1.ConditionFalse + Expect(env.Client(gardenCluster).Status().Patch(env.Ctx, sh, client.MergeFrom(old))).To(Succeed()) + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-04.yaml") + res, usf, cons, err := gc.HandleCreateOrUpdate(env.Ctx, as, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + )) + Expect(res.RequeueAfter).To(BeNumerically(">", 0)) + Expect(res.RequeueAfter).To(BeNumerically("<=", 5*time.Minute)) + Expect(usf).ToNot(BeNil()) + Expect(as.Status.AdminAccess).To(BeNil()) + Expect(usf(&as.Status)).To(Succeed()) + Expect(as.Status.AdminAccess).To(BeNil()) + }) + + It("should expose endpoint and serviceaccount issuer if exposed in shoot status", func() { + sh := &gardenv1beta1.Shoot{} + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh)).To(Succeed()) + expectedEndpoint := "" + expectedServiceAccountIssuer := "" + for _, aa := range sh.Status.AdvertisedAddresses { + if aa.Name == constants.AdvertisedAddressExternal { + expectedEndpoint = aa.URL + continue + } + if aa.Name == constants.AdvertisedAddressServiceAccountIssuer { + expectedServiceAccountIssuer = aa.URL + continue + } + } + Expect(expectedEndpoint).ToNot(BeEmpty(), "test prerequisite not fulfilled: shoot status should advertise an 'external' address") + Expect(expectedServiceAccountIssuer).ToNot(BeEmpty(), "test prerequisite not fulfilled: shoot status should advertise a 'serviceAccountIssuer' address") + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-04.yaml") + _, usf, _, err := gc.HandleCreateOrUpdate(env.Ctx, as, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(usf).ToNot(BeNil()) + if as.Status.ExternalAPIServerStatus != nil { + Expect(as.Status.ExternalAPIServerStatus.Endpoint).To(BeEmpty()) + Expect(as.Status.ExternalAPIServerStatus.ServiceAccountIssuer).To(BeEmpty()) + } + Expect(usf(&as.Status)).To(Succeed()) + Expect(as.Status.ExternalAPIServerStatus.Endpoint).To(Equal(expectedEndpoint)) + Expect(as.Status.ExternalAPIServerStatus.ServiceAccountIssuer).To(Equal(expectedServiceAccountIssuer)) + }) + + It("should create a new shoot if none exists", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-05.yaml") + res, usf, cons, err := gc.HandleCreateOrUpdate(env.Ctx, as, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + )) + Expect(res.RequeueAfter).To(BeNumerically(">", 0)) + Expect(res.RequeueAfter).To(BeNumerically("<=", 5*time.Minute)) + Expect(usf).ToNot(BeNil()) + Expect(as.Status.AdminAccess).To(BeNil()) + Expect(as.Status.GardenerStatus).To(BeNil()) + Expect(usf(&as.Status)).To(Succeed()) + Expect(as.Status.AdminAccess).To(BeNil()) + Expect(as.Status.GardenerStatus).ToNot(BeNil()) + sh1 := &gardenv1beta1.Shoot{} + uShoot, err2 := as.Status.GardenerStatus.GetShoot() + Expect(err2).ToNot(HaveOccurred()) + Expect(uShoot).ToNot(BeNil()) + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: uShoot.GetName(), Namespace: uShoot.GetNamespace()}, sh1)).To(Succeed()) + }) + + It("should fail if the to-be-created shoot already exists, but is lacking the required labels", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-05.yaml") + sh := &gardenv1beta1.Shoot{} + sh.SetName(gardener.ComputeShootName(&as.ObjectMeta, "test")) + sh.SetNamespace("garden-test") + Expect(env.Client(gardenCluster).Create(env.Ctx, sh)).To(Succeed()) + _, _, _, err := gc.HandleCreateOrUpdate(env.Ctx, as, nil) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("already exists"))) + }) + + It("should copy audit log resources to the Garden namespace", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-06.yaml") + _, _, cons, errr := gc.HandleCreateOrUpdate(env.Ctx, as, env.Client(testutils.CrateCluster)) + Expect(errr).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + cmGarden := &corev1.ConfigMap{} + err := env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test--auditlog-policy", Namespace: "garden-test"}, cmGarden) + Expect(err).ToNot(HaveOccurred()) + secretGarden := &corev1.Secret{} + err = env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test--auditlog-credentials", Namespace: "garden-test"}, secretGarden) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should update the audit log resources in the Garden namespace when the shoot already exists with the correct auditlog configuration", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-07.yaml") + _, _, cons, errr := gc.HandleCreateOrUpdate(env.Ctx, as, env.Client(testutils.CrateCluster)) + Expect(errr).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + cmGarden := &corev1.ConfigMap{} + err := env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test-auditlog--auditlog-policy", Namespace: "garden-test"}, cmGarden) + Expect(err).ToNot(HaveOccurred()) + secretGarden := &corev1.Secret{} + err = env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test-auditlog--auditlog-credentials", Namespace: "garden-test"}, secretGarden) + Expect(err).ToNot(HaveOccurred()) + + sh := &gardenv1beta1.Shoot{} + err = env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test-auditlog", Namespace: "garden-test"}, sh) + Expect(err).ToNot(HaveOccurred()) + Expect(sh.GetAnnotations()).To(HaveKeyWithValue(constants.GardenerOperation, constants.GardenerOperationReconcile)) + }) + + }) + + Context("HandleDelete", func() { + + It("should delete the shoot if it exists", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-04.yaml") + sh := &gardenv1beta1.Shoot{} + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh)).To(Succeed()) + res, _, cons, err := gc.HandleDelete(env.Ctx, as, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + )) + Expect(res.RequeueAfter).To(BeNumerically(">", 0)) + Expect(res.RequeueAfter).To(BeNumerically("<=", 5*time.Minute)) + err2 := env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh) + Expect(err2).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err2)).To(BeTrue()) + }) + + It("should return ready and remove the shoot reference if the shoot does not exist", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-04.yaml") + gls, _, err := gc.LandscapeConfiguration() + Expect(err).ToNot(HaveOccurred()) + sh := &gardenv1beta1.Shoot{} + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh)).To(Succeed()) + Expect(componentutils.PatchAnnotation(env.Ctx, gls.Client, sh, gardener.GardenerDeletionConfirmationAnnotation, "true", componentutils.ANNOTATION_OVERWRITE)).To(Succeed()) + Expect(env.Client(gardenCluster).Delete(env.Ctx, sh)).To(Succeed()) + err2 := env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh) + Expect(err2).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err2)).To(BeTrue()) + res, usf, cons, err := gc.HandleDelete(env.Ctx, as, nil) + Expect(err).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + Expect(res).To(Equal(ctrl.Result{})) + Expect(usf).ToNot(BeNil()) + Expect(as.Status.GardenerStatus).ToNot(BeNil()) + Expect(as.Status.GardenerStatus.Shoot).ToNot(BeNil()) + Expect(usf(&as.Status)).To(Succeed()) + Expect(as.Status.GardenerStatus.Shoot).To(BeNil()) + }) + + It("should create and then delete the audit log resources along with the shoot", func() { + gc, as := initGardenerHandlerTest(defaultAPIServerType, "", "testdata", "connector", "apiserver-06.yaml") + _, _, cons, errr := gc.HandleCreateOrUpdate(env.Ctx, as, env.Client(testutils.CrateCluster)) + Expect(errr).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + _, _, cons, errr = gc.HandleDelete(env.Ctx, as, env.Client(testutils.CrateCluster)) + Expect(errr).ToNot(HaveOccurred()) + Expect(cons).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + )) + + cmGarden := &corev1.ConfigMap{} + err := env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test--auditlog-policy", Namespace: "garden-test"}, cmGarden) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + secretGarden := &corev1.Secret{} + err = env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test--auditlog-credentials", Namespace: "garden-test"}, secretGarden) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + sh := &gardenv1beta1.Shoot{} + err = env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "garden-test"}, sh) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + + }) + + }) + + } + +}) diff --git a/internal/controller/core/apiserver/handler/gardener/conversion.go b/internal/controller/core/apiserver/handler/gardener/conversion.go new file mode 100644 index 0000000..e047802 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/conversion.go @@ -0,0 +1,510 @@ +package gardener + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "hash/fnv" + "time" + + "github.com/Masterminds/semver/v3" + "github.com/openmcp-project/controller-utils/pkg/collections/maps" + "github.com/openmcp-project/controller-utils/pkg/logging" + authenticationv1 "k8s.io/api/authentication/v1" + autoscalingv1 "k8s.io/api/autoscaling/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + authenticationv1alpha1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/authentication/v1alpha1" + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" + gardenconstants "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1/constants" + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/config" + "github.com/openmcp-project/mcp-operator/internal/utils" + "github.com/openmcp-project/mcp-operator/internal/utils/region" +) + +const ( + auditlogExtensionServiceName = "shoot-auditlog-service" + auditlogCredentialName = "auditlog-credentials" +) + +// Shoot_v1beta1_from_APIServer_v1alpha1 updates a v1beta1.Shoot based on a v1alpha1.APIServer. +// Since most fields are immutable, the values of an existing shoot will be preserved in the most cases. +// For the k8s version, there is a special logic in place which prevents downgrades and allows upgrades. +func (gc *GardenerConnector) Shoot_v1beta1_from_APIServer_v1alpha1(ctx context.Context, as *openmcpv1alpha1.APIServer, sh *gardenv1beta1.Shoot) error { + log := logging.FromContextOrPanic(ctx).WithName("ShootConversion") + + lc := "" + if as.Spec.Internal != nil && as.Spec.Internal.GardenerConfig != nil { + lc = as.Spec.Internal.GardenerConfig.LandscapeConfiguration + } + _, gcfg, err := gc.LandscapeConfiguration(lc) + if err != nil { + return fmt.Errorf("error resolving landscape and configuration: %w", err) + } + + sh.SetName(gc.GetShootName(sh, as, gcfg)) + + if sh.Namespace == "" { + if as.Spec.Internal != nil && as.Spec.Internal.GardenerConfig != nil && as.Spec.Internal.GardenerConfig.ShootOverwrite != nil { + sh.SetNamespace(as.Spec.Internal.GardenerConfig.ShootOverwrite.Namespace) + } else { + sh.SetNamespace(gcfg.ProjectNamespace) + } + } + log = log.WithValues("shoot", client.ObjectKeyFromObject(sh).String()) + + enforcedAnnotations := maps.Merge(gcfg.ShootTemplate.Annotations, map[string]string{ + "shoot.gardener.cloud/cleanup-extended-apis-finalize-grace-period-seconds": "30", + gardenconstants.AnnotationAuthenticationIssuer: gardenconstants.AnnotationAuthenticationIssuerManaged, + }) + existingAnnotations := sh.GetAnnotations() + if existingAnnotations == nil { + sh.SetAnnotations(enforcedAnnotations) + } else { + for k, v := range enforcedAnnotations { + val, exists := existingAnnotations[k] + if !exists || val != v { + sh.SetAnnotations(maps.Merge(existingAnnotations, enforcedAnnotations)) + break + } + } + } + + enforcedLabels := maps.Merge(gcfg.ShootTemplate.Labels, map[string]string{ + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName: as.Name, + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace: as.Namespace, + }) + if project, ok := as.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject]; ok { + enforcedLabels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject] = project + } + if workspace, ok := as.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace]; ok { + enforcedLabels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace] = workspace + } + existingLabels := sh.GetLabels() + if existingLabels == nil { + sh.SetLabels(enforcedLabels) + } else { + for k, v := range enforcedLabels { + val, exists := existingLabels[k] + if !exists || val != v { + sh.SetLabels(maps.Merge(existingLabels, enforcedLabels)) + break + } + } + } + + if sh.Spec.Purpose == nil { + log.Debug("Setting shoot.Spec.Purpose", "value", string(gardenv1beta1.ShootPurposeProduction)) + sh.Spec.Purpose = ptr.To(gardenv1beta1.ShootPurposeProduction) + } + if sh.Spec.CloudProfile == nil { + log.Debug("Setting shoot.Spec.CloudProfile", "value_kind", gardenconstants.CloudProfileReferenceKindCloudProfile, "value_name", gcfg.CloudProfile) + sh.Spec.CloudProfile = &gardenv1beta1.CloudProfileReference{ + Kind: gardenconstants.CloudProfileReferenceKindCloudProfile, + Name: gcfg.CloudProfile, + } + } + if sh.Spec.Provider.Type == "" { + log.Debug("Setting shoot.Spec.Provider.Type", "value", gcfg.ProviderType) + sh.Spec.Provider.Type = gcfg.ProviderType + } + if sh.Spec.Hibernation == nil { + sh.Spec.Hibernation = &gardenv1beta1.Hibernation{} + } + if sh.Spec.Hibernation.Enabled == nil || *sh.Spec.Hibernation.Enabled { + log.Debug("Setting shoot.Spec.Hibernation.Enabled", "value", false) + sh.Spec.Hibernation.Enabled = ptr.To(false) + } + h := HashAsNumber(as.Name, as.Namespace) + if sh.Spec.Region == "" { + log.Debug("Setting shoot.Spec.Region") + if as.Spec.GardenerConfig != nil && as.Spec.GardenerConfig.Region != "" { + sh.Spec.Region = as.Spec.GardenerConfig.Region + log.Debug("Using shoot region specified in APIServer's Gardener config", "region", sh.Spec.Region) + } else { + // try to derive region from the common configuration + if as.Spec.DesiredRegion != nil && as.Spec.DesiredRegion.Name != "" { + dr := as.Spec.DesiredRegion.DeepCopy() + if dr.Direction == "" { + dr.Direction = openmcpv1alpha1.CENTRAL + } + mapper := region.GetPredefinedMapperByCloudprovider(gcfg.ProviderType) + if mapper != nil { + regions, err := region.GetClosestRegions(*dr, mapper, sets.KeySet(gcfg.ValidRegions).UnsortedList(), true) + if err != nil { + // log, but don't break + log.Error(err, "error finding closest regions", "region", dr.Name, "direction", dr.Direction) + } else if len(regions) > 0 { + if len(regions) == 1 { + sh.Spec.Region = regions[0] + } else { + sh.Spec.Region = regions[h%len(regions)] + } + log.Debug("Resolved shoot region from APIServer's desiredRegion field", "region", sh.Spec.Region) + } + } + } + // fallback to specified default region + if sh.Spec.Region == "" { + sh.Spec.Region = gcfg.DefaultRegion + log.Debug("Neither desired region nor explicit Gardener region specified in APIServer, using fallback from global configuration", "region", sh.Spec.Region) + } + } + } + configuredVersion := "" + if as.Spec.Internal != nil && as.Spec.Internal.GardenerConfig != nil && as.Spec.Internal.GardenerConfig.K8SVersionOverwrite != "" { + log.Debug("Found internal k8s version overwrite", "version", as.Spec.Internal.GardenerConfig.K8SVersionOverwrite) + configuredVersion = as.Spec.Internal.GardenerConfig.K8SVersionOverwrite + } + newK8sVersion := computeK8sVersion(configuredVersion, sh.Spec.Kubernetes.Version) + if sh.Spec.Kubernetes.Version != newK8sVersion { + log.Debug("Setting shoot.Spec.Kubernetes.Version", "value", newK8sVersion) + sh.Spec.Kubernetes.Version = newK8sVersion + } + if sh.Spec.Kubernetes.KubeAPIServer == nil { + sh.Spec.Kubernetes.KubeAPIServer = &gardenv1beta1.KubeAPIServerConfig{} + } + if sh.Spec.Kubernetes.KubeAPIServer.RuntimeConfig == nil { + sh.Spec.Kubernetes.KubeAPIServer.RuntimeConfig = map[string]bool{} + } + if val, ok := sh.Spec.Kubernetes.KubeAPIServer.RuntimeConfig["apps/v1"]; !ok || !val { + log.Debug("Setting shoot.Spec.Kubernetes.KubeAPIServer.RuntimeConfig[apps/v1]", "value", true) + sh.Spec.Kubernetes.KubeAPIServer.RuntimeConfig["apps/v1"] = true + } + if val, ok := sh.Spec.Kubernetes.KubeAPIServer.RuntimeConfig["batch/v1"]; !ok || !val { + log.Debug("Setting shoot.Spec.Kubernetes.KubeAPIServer.RuntimeConfig[batch/v1]", "value", true) + sh.Spec.Kubernetes.KubeAPIServer.RuntimeConfig["batch/v1"] = true + } + + addOIDCExtension(log, sh) + + // add all audit log configurations + if as.Spec.GardenerConfig != nil && as.Spec.GardenerConfig.AuditLog != nil { + log.Debug("Adding audit log configuration to shoot") + sh.Spec.Kubernetes.KubeAPIServer.AuditConfig = &gardenv1beta1.AuditConfig{ + AuditPolicy: &gardenv1beta1.AuditPolicy{ + ConfigMapRef: &corev1.ObjectReference{ + Name: utils.PrefixWithNamespace(sh.Name, "auditlog-policy"), + }, + }, + } + + err := addAuditLogServiceExtension(sh, as) + if err != nil { + return err + } + addAuditLogCredentialResource(sh) + } + + // remove all audit log configurations when nothing is configured in the ManagedControlPlane + if as.Spec.GardenerConfig == nil || as.Spec.GardenerConfig.AuditLog == nil { + sh.Spec.Kubernetes.KubeAPIServer.AuditConfig = nil + removeAuditLogServiceExtension(sh) + removeAuditLogCredentialResource(sh) + } + + if gc.APIServerType == openmcpv1alpha1.GardenerDedicated { + log.Debug("APIServer type is GardenerDedicated, configuring shoot workers") + region := gcfg.ValidRegions[sh.Spec.Region] + var highAvailabilityConfig *openmcpv1alpha1.HighAvailabilityConfig + if as.Spec.GardenerConfig != nil { + highAvailabilityConfig = as.Spec.GardenerConfig.HighAvailabilityConfig + } + builder, err := getShootBuilderByCloudProvider(log, sh, h, gcfg.ProviderType, gcfg.ShootTemplate, ®ion, highAvailabilityConfig) + if err != nil { + return fmt.Errorf("error constructing shoot builder: %w", err) + } + + if sh.Spec.Networking == nil { + sh.Spec.Networking = &gardenv1beta1.Networking{} + } + if sh.Spec.Networking.Type == nil { + log.Debug("Setting shoot.Spec.Networking.Type", "value", gcfg.ShootTemplate.Spec.Networking.Type) + sh.Spec.Networking.Type = gcfg.ShootTemplate.Spec.Networking.Type + } + if sh.Spec.Networking.Nodes == nil { + log.Debug("Setting shoot.Spec.Networking.Nodes", "value", gcfg.ShootTemplate.Spec.Networking.Nodes) + sh.Spec.Networking.Nodes = gcfg.ShootTemplate.Spec.Networking.Nodes + } + + if sh.Spec.Provider.InfrastructureConfig == nil { + log.Debug("Setting shoot.Spec.Provider.InfrastructureConfig") + sh.Spec.Provider.InfrastructureConfig, err = builder.newInfrastructureConfig(log) + if err != nil { + return err + } + } + + if sh.Spec.Provider.ControlPlaneConfig == nil { + log.Debug("Setting shoot.Spec.Provider.ControlPlaneConfig") + controlPlaneConfig, err := builder.newControlPlaneConfig(log) + if err != nil { + return err + } + sh.Spec.Provider.ControlPlaneConfig = controlPlaneConfig + } + + builder.adjustWorkers(log, &sh.Spec.Provider) + + if sh.Spec.SecretBindingName == nil { + log.Debug("Setting shoot.Spec.SecretBindingName", "value", gcfg.ShootTemplate.Spec.SecretBindingName) + sh.Spec.SecretBindingName = gcfg.ShootTemplate.Spec.SecretBindingName + } + } + + if as.Spec.GardenerConfig != nil && as.Spec.GardenerConfig.HighAvailabilityConfig != nil { + if sh.Spec.ControlPlane == nil { + sh.Spec.ControlPlane = &gardenv1beta1.ControlPlane{} + } + + log.Debug("Setting shoot.Spec.ControlPlane.HighAvailability.FailureTolerance.Type", "value", as.Spec.GardenerConfig.HighAvailabilityConfig.FailureToleranceType) + sh.Spec.ControlPlane.HighAvailability = &gardenv1beta1.HighAvailability{ + FailureTolerance: gardenv1beta1.FailureTolerance{ + Type: gardenv1beta1.FailureToleranceType(as.Spec.GardenerConfig.HighAvailabilityConfig.FailureToleranceType), + }, + } + } + + if as.Spec.GardenerConfig != nil { + log.Debug("Setting shoot.Spec.Kubernetes.KubeAPIServer.EncryptionConfig") + if as.Spec.GardenerConfig.EncryptionConfig == nil { + sh.Spec.Kubernetes.KubeAPIServer.EncryptionConfig = nil + } else { + sh.Spec.Kubernetes.KubeAPIServer.EncryptionConfig = &gardenv1beta1.EncryptionConfig{ + Resources: as.Spec.GardenerConfig.EncryptionConfig.Resources, + } + } + } + + return nil +} + +// addOIDCExtension adds the OIDC extension to the shoot spec if it is not already present. +func addOIDCExtension(log logging.Logger, sh *gardenv1beta1.Shoot) { + // Update configuration + for _, extension := range sh.Spec.Extensions { + if extension.Type == "shoot-oidc-service" { + return + } + } + + // Add configuration + log.Debug("Adding 'shoot-oidc-service' extension to shoot") + sh.Spec.Extensions = append(sh.Spec.Extensions, gardenv1beta1.Extension{ + Type: "shoot-oidc-service", + }) + +} + +// addAuditLogServiceExtension adds the audit log service extension to the shoot spec if it is not already present. +func addAuditLogServiceExtension(sh *gardenv1beta1.Shoot, as *openmcpv1alpha1.APIServer) error { + m := map[string]string{ + "apiVersion": "service.auditlog.extensions.gardener.cloud/v1alpha1", + "kind": "AuditlogConfig", + "type": as.Spec.GardenerConfig.AuditLog.Type, + "tenantID": as.Spec.GardenerConfig.AuditLog.TenantID, + "serviceURL": as.Spec.GardenerConfig.AuditLog.ServiceURL, + "secretReferenceName": "auditlog-credentials", + } + raw, err := json.Marshal(m) + if err != nil { + return err + } + + // Update configuration + for i := range sh.Spec.Extensions { + extension := &sh.Spec.Extensions[i] + if extension.Type == auditlogExtensionServiceName { + extension.ProviderConfig = &runtime.RawExtension{ + Raw: raw, + } + return nil + } + } + + // Add configuration + sh.Spec.Extensions = append(sh.Spec.Extensions, gardenv1beta1.Extension{ + Type: auditlogExtensionServiceName, + ProviderConfig: &runtime.RawExtension{Raw: raw}, + }) + + return nil +} + +// removeAuditLogServiceExtension removes the audit log service extension from the shoot spec if it is present. +func removeAuditLogServiceExtension(sh *gardenv1beta1.Shoot) { + var extensions []gardenv1beta1.Extension + for _, extension := range sh.Spec.Extensions { + if extension.Type != auditlogExtensionServiceName { + extensions = append(extensions, extension) + } + } + sh.Spec.Extensions = extensions +} + +// addAuditLogCredentialResource adds the audit log credential resource to the shoot spec if it is not already present. +func addAuditLogCredentialResource(sh *gardenv1beta1.Shoot) { + resourceRef := autoscalingv1.CrossVersionObjectReference{ + APIVersion: "v1", + Kind: "Secret", + Name: utils.PrefixWithNamespace(sh.Name, "auditlog-credentials"), + } + + // add audit log credentials + for i := range sh.Spec.Resources { + resource := &sh.Spec.Resources[i] + if resource.Name == auditlogCredentialName { + resource.ResourceRef = resourceRef + return + } + } + + sh.Spec.Resources = append(sh.Spec.Resources, gardenv1beta1.NamedResourceReference{ + Name: auditlogCredentialName, + ResourceRef: resourceRef, + }) +} + +// removeAuditLogCredentialResource removes the audit log credential resource from the shoot spec if it is present. +func removeAuditLogCredentialResource(sh *gardenv1beta1.Shoot) { + var resources []gardenv1beta1.NamedResourceReference + for _, resource := range sh.Spec.Resources { + if resource.Name != auditlogCredentialName { + resources = append(resources, resource) + } + } + sh.Spec.Resources = resources +} + +// computeK8sVersion computes which k8s version should be rendered into the generated shoot manifest. +// It takes a k8s version which is configured and one which comes from an already existing shoot. +// The logic is as follows: +// - If both are empty, the result is empty. +// - If only one is empty, the result is the non-empty one. +// - If neither is empty, the higher one is returned. To not cause any unplanned shoot updates, a configured version without a patch number is considered to be less than an existing version with a patch number (that is otherwise identical). +func computeK8sVersion(configured, existing string) string { + if existing == "" && configured != "" { + return configured + } else if existing != "" && configured != "" { + configuredK8sVersion := semver.MustParse(configured) + existingK8sVersion := semver.MustParse(existing) + + if configuredK8sVersion.GreaterThan(existingK8sVersion) { + return configuredK8sVersion.Original() + } + + } + + return existing +} + +// getAdminKubeconfigForShoot uses the AdminKubeconfigRequest subresource of a shoot to get a admin kubeconfig for the given shoot. +func getAdminKubeconfigForShoot(ctx context.Context, c client.Client, shoot *gardenv1beta1.Shoot, desiredValidity time.Duration) ([]byte, error) { + expirationSeconds := int64(desiredValidity.Seconds()) + adminKubeconfigRequest := &authenticationv1alpha1.AdminKubeconfigRequest{ + Spec: authenticationv1alpha1.AdminKubeconfigRequestSpec{ + ExpirationSeconds: &expirationSeconds, + }, + } + err := c.SubResource("adminkubeconfig").Create(ctx, shoot, adminKubeconfigRequest) + if err != nil { + return nil, err + } + return adminKubeconfigRequest.Status.Kubeconfig, nil +} + +// getTemporaryClientForShoot creates a client.Client for accessing the shoot cluster. +// Also returns the rest.Config used to create the client. +// The token used by the client has a validity of one hour. +func getTemporaryClientForShoot(ctx context.Context, c client.Client, shoot *gardenv1beta1.Shoot) (client.Client, *rest.Config, error) { + kcfg, err := getAdminKubeconfigForShoot(ctx, c, shoot, time.Hour) + if err != nil { + return nil, nil, err + } + if bytes.Equal(kcfg, []byte("fake")) { + // inject fake client for tests + return fake.NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{ + SubResourceCreate: func(ctx context.Context, c client.Client, subResourceName string, obj, subResource client.Object, opts ...client.SubResourceCreateOption) error { + switch subResourceName { + case "token": + tr, ok := subResource.(*authenticationv1.TokenRequest) + if !ok { + return fmt.Errorf("unexpected object type %T", subResource) + } + tr.Status.Token = "fake" + tr.Status.ExpirationTimestamp = metav1.Time{Time: time.Now().Add(time.Duration(*tr.Spec.ExpirationSeconds * int64(time.Second)))} + return nil + } + // use default logic + return c.SubResource(subResourceName).Create(ctx, obj, subResource, opts...) + }, + }).Build(), &rest.Config{}, nil + } + cfg, err := clientcmd.RESTConfigFromKubeConfig(kcfg) + if err != nil { + return nil, nil, err + } + shootClient, err := client.New(cfg, client.Options{}) + if err != nil { + return nil, nil, err + } + return shootClient, cfg, nil +} + +// ComputeShootName computes the name of a shoot based on the given APIServer metadata and the project name. +func ComputeShootName(apiServerMeta *metav1.ObjectMeta, project string) string { + // Gardener enforces a length limit on shoot names which is at max 21 characters for project name + shoot name. + shootMaxLength := 21 - len(project) + shootName := utils.ScopeToControlPlane(apiServerMeta) + return shootName[:shootMaxLength] +} + +// HashAsNumber takes any number of strings and returns a hash value as an integer. +// Note that this function is not cyptographically secure. +func HashAsNumber(data ...string) int { + h := fnv.New32a() + for _, s := range data { + h.Write([]byte(s)) + } + return int(h.Sum32()) +} + +func (gc *GardenerConnector) GetShootNameNoConfig(sh *gardenv1beta1.Shoot, as *openmcpv1alpha1.APIServer) (string, error) { + lc := "" + if as.Spec.Internal != nil && as.Spec.Internal.GardenerConfig != nil { + lc = as.Spec.Internal.GardenerConfig.LandscapeConfiguration + } + _, gcfg, err := gc.LandscapeConfiguration(lc) + if err != nil { + return "", fmt.Errorf("error resolving landscape and configuration: %w", err) + } + + return gc.GetShootName(sh, as, gcfg), nil +} + +func (gc *GardenerConnector) GetShootName(sh *gardenv1beta1.Shoot, as *openmcpv1alpha1.APIServer, gcfg *config.CompletedGardenerConfiguration) string { + shootName := "" + + if sh == nil || sh.Name == "" { + if as.Spec.Internal != nil && as.Spec.Internal.GardenerConfig != nil && as.Spec.Internal.GardenerConfig.ShootOverwrite != nil { + shootName = as.Spec.Internal.GardenerConfig.ShootOverwrite.Name + } else { + shootName = ComputeShootName(&as.ObjectMeta, gcfg.Project) + } + } else { + shootName = sh.Name + } + + return shootName +} diff --git a/internal/controller/core/apiserver/handler/gardener/conversion_test.go b/internal/controller/core/apiserver/handler/gardener/conversion_test.go new file mode 100644 index 0000000..9fc7a4f --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/conversion_test.go @@ -0,0 +1,422 @@ +package gardener_test + +import ( + "fmt" + "net" + "strings" + + "github.com/apparentlymart/go-cidr/cidr" + "sigs.k8s.io/yaml" + + "github.com/openmcp-project/mcp-operator/internal/utils" + "github.com/openmcp-project/mcp-operator/internal/utils/region" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/handler/gardener" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + gardenawsv1alpha1 "github.com/openmcp-project/mcp-operator/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1" + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" +) + +var _ = Describe("APIServer Gardener Conversion", func() { + + for cfgType, initGardenerHandlerTest := range map[string]func(openmcpv1alpha1.APIServerType, string, ...string) (*gardener.GardenerConnector, *openmcpv1alpha1.APIServer){ + "single": initGardenerHandlerTestSingle, + "multi": initGardenerHandlerTestMulti, + } { + + Context(fmt.Sprintf("Config Type: %s", cfgType), func() { + + var flavors []string + if cfgType == "single" { + flavors = []string{"default/default"} + } else { + flavors = []string{"default/gcp", "default/aws"} + } + + for _, flavor := range flavors { + + Context(fmt.Sprintf("Flavor: %s", flavor), func() { + + for _, apiServerType := range []openmcpv1alpha1.APIServerType{openmcpv1alpha1.Gardener, openmcpv1alpha1.GardenerDedicated} { + + Context(fmt.Sprintf("Type: %s", string(apiServerType)), func() { + + It("should convert a APIServer to a Shoot (generic region)", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-01.yaml") + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + + // spec.region + mapper := region.GetPredefinedMapperByCloudprovider(gcfg.ProviderType) + Expect(mapper).ToNot(BeNil()) + regions, err := region.GetClosestRegions(*apiServer.Spec.DesiredRegion, mapper, sets.KeySet(gcfg.ValidRegions).UnsortedList(), true) + Expect(err).ToNot(HaveOccurred()) + Expect(regions).ToNot(BeEmpty()) + Expect(regions).To(ContainElement(shoot.Spec.Region), "shoot region is not in the list of closest regions") + + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + It("should convert a APIServer to a Shoot (region overwrite)", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-02.yaml") + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + + // spec.region + Expect(shoot.Spec.Region).To(Equal(apiServer.Spec.GardenerConfig.Region)) + + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + It("should convert a APIServer to a Shoot (region fallback)", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-03.yaml") + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + + // spec.region + Expect(shoot.Spec.Region).To(Equal(gcfg.DefaultRegion)) + + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + It("should not overwrite immutable fields of the shoot", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-01.yaml") + gc.APIServerType = apiServerType + apiServer.Spec.Type = apiServerType + shoot := &gardenv1beta1.Shoot{} + Expect(env.Client(gardenCluster).Get(env.Ctx, types.NamespacedName{Name: "modified", Namespace: "garden-test"}, shoot)).To(Succeed()) + compare := shoot.DeepCopy() + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Spec.Region).To(Equal(compare.Spec.Region)) + Expect(shoot.Spec.CloudProfileName).To(Equal(compare.Spec.CloudProfileName)) + Expect(shoot.Spec.SecretBindingName).To(Equal(compare.Spec.SecretBindingName)) + Expect(shoot.Spec.Provider.Type).To(Equal(compare.Spec.Provider.Type)) + Expect(shoot.Spec.Networking.Type).To(Equal(compare.Spec.Networking.Type)) + }) + + It("should convert the AuditLog configuration to the Shoot spec", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-04.yaml") + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + + Expect(shoot.Spec.Resources[0].Name).To(Equal("auditlog-credentials")) + Expect(shoot.Spec.Resources[0].ResourceRef.Kind).To(Equal("Secret")) + Expect(shoot.Spec.Resources[0].ResourceRef.Name).To(Equal(utils.PrefixWithNamespace(shoot.Name, "auditlog-credentials"))) + + Expect(shoot.Spec.Extensions).To(HaveLen(2)) + Expect(shoot.Spec.Extensions[1].ProviderConfig.Raw).ToNot(BeEmpty()) + + Expect(shoot.Spec.Kubernetes.KubeAPIServer.AuditConfig.AuditPolicy.ConfigMapRef.Name).To(Equal(utils.PrefixWithNamespace(shoot.Name, "auditlog-policy"))) + + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + It("should remove the AuditLog configuration from the Shoot spec", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-05.yaml") + + shoot := &gardenv1beta1.Shoot{} + Expect(testing.LoadObject(shoot, "testdata", "conversion", "shoot-05.yaml")).To(Succeed()) + oldNs := shoot.Namespace + + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(oldNs)) + + Expect(shoot.Spec.Resources).To(BeEmpty()) + Expect(shoot.Spec.Extensions).To(HaveLen(2)) + Expect(shoot.Spec.Kubernetes.KubeAPIServer.AuditConfig).To(BeNil()) + }) + + It("should set the high availability configuration to node", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-07.yaml") + if strings.HasSuffix(flavor, "aws") { + // aws doesn't have the 'europe-west3' region that is hardcoded in the apiserver config + apiServer.Spec.GardenerConfig.Region = "eu-west-1" + } + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + + Expect(shoot.Spec.ControlPlane).ToNot(BeNil()) + Expect(shoot.Spec.ControlPlane.HighAvailability).ToNot(BeNil()) + Expect(shoot.Spec.ControlPlane.HighAvailability.FailureTolerance.Type).To( + Equal(gardenv1beta1.FailureToleranceType(openmcpv1alpha1.HighAvailabilityFailureToleranceNode))) + + if apiServerType == openmcpv1alpha1.GardenerDedicated { + Expect(shoot.Spec.Provider.Workers).To(HaveLen(1)) + Expect(shoot.Spec.Provider.Workers[0].Zones).To(HaveLen(1)) + Expect(shoot.Spec.Provider.Workers[0].Minimum).To(Equal(int32(3))) + Expect(shoot.Spec.Provider.Workers[0].Maximum).To(Equal(int32(3))) + } + + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + It("should set the high availability configuration to zone", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-08.yaml") + if strings.HasSuffix(flavor, "aws") { + // aws doesn't have the 'europe-west3' region that is hardcoded in the apiserver config + apiServer.Spec.GardenerConfig.Region = "eu-west-1" + } + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + + Expect(shoot.Spec.ControlPlane).ToNot(BeNil()) + Expect(shoot.Spec.ControlPlane.HighAvailability).ToNot(BeNil()) + Expect(shoot.Spec.ControlPlane.HighAvailability.FailureTolerance.Type).To( + Equal(gardenv1beta1.FailureToleranceType(openmcpv1alpha1.HighAvailabilityFailureToleranceZone))) + + if apiServerType == openmcpv1alpha1.GardenerDedicated { + Expect(shoot.Spec.Provider.Workers).To(HaveLen(1)) + Expect(shoot.Spec.Provider.Workers[0].Zones).To(HaveLen(3)) + Expect(shoot.Spec.Provider.Workers[0].Minimum).To(Equal(int32(3))) + Expect(shoot.Spec.Provider.Workers[0].Maximum).To(Equal(int32(3))) + } + + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + It("should convert the EncryptionConfig configuration to the Shoot spec", func() { + gc, apiServer := initGardenerHandlerTest(apiServerType, flavor, "testdata", "conversion", "apiserver-06.yaml") + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + Expect(shoot.Spec.Kubernetes.KubeAPIServer.EncryptionConfig.Resources).To(ConsistOf("configmaps", "statefulsets.apps", "flunders.example.com")) + + commonShootValidation(gc, apiServer, shoot, flavor) + + // removing the EncryptionConfig from the APIServer should remove it from the Shoot + apiServer.Spec.GardenerConfig.EncryptionConfig = nil + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + Expect(shoot.Spec.Kubernetes.KubeAPIServer.EncryptionConfig).To(BeNil()) + + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + }) + + } + + Context(fmt.Sprintf("Tests specific to the '%s' APIServerType", openmcpv1alpha1.GardenerDedicated), func() { + + It("controlplane zone must not be overwritten and always part of any worker's zones", func() { + gc, apiServer := initGardenerHandlerTest(openmcpv1alpha1.GardenerDedicated, flavor, "testdata", "conversion", "apiserver-01.yaml") + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + existingZone := "asia-south1-a" + cpc := map[string]interface{}{} + Expect(shoot.Spec.Provider.ControlPlaneConfig).ToNot(BeNil()) + Expect(yaml.Unmarshal(shoot.Spec.Provider.ControlPlaneConfig.Raw, &cpc)).To(Succeed()) + _, ok := cpc["zone"] + if ok { + // exchange zone to simulate an existing shoot with a different controlplane zone + cpc["zone"] = existingZone + cpcRaw, err := yaml.Marshal(cpc) + Expect(err).ToNot(HaveOccurred()) + shoot.Spec.Provider.ControlPlaneConfig.Raw = cpcRaw + // also set the worker's zones accordingly + Expect(shoot.Spec.Provider.Workers).ToNot(BeEmpty()) + Expect(shoot.Spec.Provider.Workers[0].Zones).ToNot(BeEmpty()) + shoot.Spec.Provider.Workers[0].Zones[0] = existingZone + + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + Expect(shoot.Spec.Provider.ControlPlaneConfig).ToNot(BeNil()) + Expect(yaml.Unmarshal(shoot.Spec.Provider.ControlPlaneConfig.Raw, &cpc)).To(Succeed()) + Expect(cpc).To(HaveKeyWithValue("zone", existingZone), "controlplane zone must not be changed") + Expect(shoot.Spec.Provider.Workers).ToNot(BeEmpty()) + Expect(shoot.Spec.Provider.Workers[0].Zones).ToNot(BeEmpty()) + Expect(shoot.Spec.Provider.Workers[0].Zones).To(ContainElement(existingZone), "controlplane zone must be part of the worker's zones") + } + // nothing to do if the controlplane zone is not set + }) + + }) + + }) + + } + + }) + + } + + Context("Multi-Config-Specific Tests", func() { + + for _, apiServerType := range []openmcpv1alpha1.APIServerType{openmcpv1alpha1.Gardener, openmcpv1alpha1.GardenerDedicated} { + + Context(fmt.Sprintf("Type: %s", string(apiServerType)), func() { + + It("should use a non-default configuration (default landscape), if set in shoot", func() { + flavor := "default/aws" + gc, apiServer := initGardenerHandlerTestMulti(apiServerType, flavor, "testdata", "conversion", "apiserver-03.yaml") + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + Expect(shoot.Spec.Region).To(Equal(gcfg.DefaultRegion)) + Expect(shoot.Annotations).To(HaveKeyWithValue("test.openmcp.cloud/config", fmt.Sprintf("multi/%s", flavor))) + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + It("should use a non-default configuration (different landscape), if set in shoot", func() { + flavor := "extra/foo" + gc, apiServer := initGardenerHandlerTestMulti(apiServerType, flavor, "testdata", "conversion", "apiserver-03.yaml") + _, gcfg, err := gc.LandscapeConfiguration(flavor) + Expect(err).ToNot(HaveOccurred()) + shoot := &gardenv1beta1.Shoot{} + Expect(gc.Shoot_v1beta1_from_APIServer_v1alpha1(env.Ctx, apiServer, shoot)).To(Succeed()) + + Expect(shoot.Namespace).To(Equal(gcfg.ProjectNamespace)) + Expect(shoot.Spec.Region).To(Equal(gcfg.DefaultRegion)) + Expect(shoot.Annotations).To(HaveKeyWithValue("test.openmcp.cloud/config", fmt.Sprintf("multi/%s", flavor))) + commonShootValidation(gc, apiServer, shoot, flavor) + }) + + }) + + } + + }) + +}) + +// commonShootValidation is a helper function to test the parts of the shoot spec which are independent of the configuration. +// This is meant to avoid code duplication in the tests. +func commonShootValidation(gc *gardener.GardenerConnector, as *openmcpv1alpha1.APIServer, shoot *gardenv1beta1.Shoot, lc string) { + _, gcfg, err := gc.LandscapeConfiguration(lc) + Expect(err).ToNot(HaveOccurred()) + + // metadata + Expect(shoot.Annotations).To(HaveKey("shoot.gardener.cloud/cleanup-extended-apis-finalize-grace-period-seconds")) + for k, v := range gcfg.ShootTemplate.Annotations { + Expect(shoot.Annotations).To(HaveKeyWithValue(k, v)) + } + Expect(shoot.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName, as.Name)) + Expect(shoot.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace, as.Namespace)) + if project, ok := as.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject]; ok { + Expect(shoot.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject, project)) + } + if workspace, ok := as.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace]; ok { + Expect(shoot.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace, workspace)) + } + for k, v := range gcfg.ShootTemplate.Labels { + Expect(shoot.Labels).To(HaveKeyWithValue(k, v)) + } + + // spec.hibernation + Expect(shoot.Spec.Hibernation.Enabled).To(PointTo(BeFalse())) + + // spec.cloudprofilename + Expect(shoot.Spec.CloudProfile.Name).To(Equal(gcfg.CloudProfile)) + + // spec.extensions + Expect(shoot.Spec.Extensions).To(ContainElement(MatchFields(IgnoreExtras, Fields{ + "Type": Equal("shoot-oidc-service"), + }))) + + // spec.provider + Expect(shoot.Spec.Provider.Type).To(Equal(gcfg.ProviderType)) + switch as.Spec.Type { + case openmcpv1alpha1.Gardener: + Expect(shoot.Spec.Provider.Workers).To(BeEmpty()) + case openmcpv1alpha1.GardenerDedicated: + Expect(shoot.Spec.Networking.Type).To(Equal(gcfg.ShootTemplate.Spec.Networking.Type)) + Expect(shoot.Spec.Networking.Nodes).To(Equal(gcfg.ShootTemplate.Spec.Networking.Nodes)) + Expect(shoot.Spec.SecretBindingName).To(Equal(gcfg.ShootTemplate.Spec.SecretBindingName)) + Expect(shoot.Spec.Provider.Workers).ToNot(BeEmpty()) + + switch shoot.Spec.Provider.Type { + case "gcp": + cpc := map[string]interface{}{} + Expect(yaml.Unmarshal(shoot.Spec.Provider.ControlPlaneConfig.Raw, &cpc)).To(Succeed()) + Expect(cpc).To(HaveKeyWithValue("apiVersion", "gcp.provider.extensions.gardener.cloud/v1alpha1")) + Expect(cpc).To(HaveKey("zone")) + cpZone, ok := cpc["zone"].(string) + Expect(ok).To(BeTrue(), "spec.provider.controlPlaneConfig.zone is not a string") + Expect(gcfg.ValidRegions[shoot.Spec.Region].Zones).To(ContainElement(MatchFields(IgnoreExtras, Fields{ + "Name": Equal(cpZone), + })), "spec.provider.controlPlaneConfig.zone is not a valid zone") + foundCPC := false + if len(shoot.Spec.Provider.Workers) > 0 { + for _, w := range shoot.Spec.Provider.Workers { + for _, z := range w.Zones { + if z == cpZone { + foundCPC = true + break + } + } + if foundCPC { + break + } + } + Expect(foundCPC).To(BeTrue(), "control plane zone is not contained in any worker's zones") + } + case "aws": + cpc := map[string]interface{}{} + Expect(yaml.Unmarshal(shoot.Spec.Provider.ControlPlaneConfig.Raw, &cpc)).To(Succeed()) + Expect(cpc).To(HaveKeyWithValue("apiVersion", "aws.provider.extensions.gardener.cloud/v1alpha1")) + awsInfraCfg := &gardenawsv1alpha1.InfrastructureConfig{} + Expect(yaml.Unmarshal(shoot.Spec.Provider.InfrastructureConfig.Raw, awsInfraCfg)).To(Succeed()) + Expect(awsInfraCfg.Networks.Zones).To(HaveLen(len(gcfg.ValidRegions[shoot.Spec.Region].Zones))) + Expect(awsInfraCfg.Networks.VPC.CIDR).ToNot(BeNil()) + _, vpc, err := net.ParseCIDR(*awsInfraCfg.Networks.VPC.CIDR) + Expect(err).ToNot(HaveOccurred()) + // verify that all zone CIDRs are within the VPC CIDR, but completely disjunct from each other + cidrs := make([]*net.IPNet, 0, len(awsInfraCfg.Networks.Zones)*3) + for _, z := range awsInfraCfg.Networks.Zones { + _, workers, err := net.ParseCIDR(z.Workers) + Expect(err).ToNot(HaveOccurred()) + _, public, err := net.ParseCIDR(z.Public) + Expect(err).ToNot(HaveOccurred()) + _, internal, err := net.ParseCIDR(z.Internal) + Expect(err).ToNot(HaveOccurred()) + cidrs = append(cidrs, workers, public, internal) + } + Expect(cidr.VerifyNoOverlap(cidrs, vpc)).To(Succeed()) + } + } + + // spec.kubernetes + Expect(shoot.Spec.Kubernetes.KubeAPIServer.RuntimeConfig).To(HaveKeyWithValue("apps/v1", true)) + Expect(shoot.Spec.Kubernetes.KubeAPIServer.RuntimeConfig).To(HaveKeyWithValue("batch/v1", true)) +} diff --git a/internal/controller/core/apiserver/handler/gardener/shootbuilder.go b/internal/controller/core/apiserver/handler/gardener/shootbuilder.go new file mode 100644 index 0000000..557ed43 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/shootbuilder.go @@ -0,0 +1,249 @@ +package gardener + +import ( + "fmt" + "strings" + + "math/rand" + + "github.com/openmcp-project/controller-utils/pkg/logging" + "sigs.k8s.io/yaml" + + "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/sets" + + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" +) + +type shootBuilder interface { + newControlPlaneConfig(log logging.Logger) (*runtime.RawExtension, error) + newInfrastructureConfig(log logging.Logger) (*runtime.RawExtension, error) + adjustWorkers(log logging.Logger, provider *gardenv1beta1.Provider) +} + +func getShootBuilderByCloudProvider( + log logging.Logger, + existingShoot *gardenv1beta1.Shoot, + nameNamespaceHash int, + provider string, + shootTemplate *gardenv1beta1.ShootTemplate, + region *gardenv1beta1.Region, + haConfig *v1alpha1.HighAvailabilityConfig, +) (shootBuilder, error) { + // build a numeric hash from shoot name and namespace + // this allows to choose a random element from a slice (e.g. the region) in a deterministic way + bsb := baseShootBuilder{ + shootTemplate: shootTemplate, + region: region, + workerZones: region.Zones, + haConfig: haConfig, + } + // check if a control plane zone is already set in the shoot + // it's immutable and tied to the worker zones, so we shouldn't change it + cpc := existingShoot.Spec.Provider.ControlPlaneConfig + if cpc != nil && cpc.Raw != nil { + cpcData := map[string]any{} + if err := yaml.Unmarshal(cpc.Raw, &cpcData); err != nil { + return nil, fmt.Errorf("failed to unmarshal control plane config: %w", err) + } + rawCpcZone, ok := cpcData["zone"] + if ok { + cpcZone, ok := rawCpcZone.(string) + if ok { + log.Debug("Using control plane zone from existing shoot", "zone", cpcZone) + bsb.controlPlaneZone = cpcZone + } + } + } + if bsb.controlPlaneZone == "" && len(region.Zones) > 0 { + bsb.controlPlaneZone = region.Zones[nameNamespaceHash%len(region.Zones)].Name + } + + switch strings.ToLower(provider) { + case "gcp": + return &shootBuilderGCP{baseShootBuilder: bsb}, nil + case "aws": + return &shootBuilderAWS{baseShootBuilder: bsb}, nil + } + return nil, fmt.Errorf("unsupported cloud provider: %s", provider) +} + +// baseShootBuilder provides methods which don't depend on the cloud provider +type baseShootBuilder struct { + shootTemplate *gardenv1beta1.ShootTemplate + region *gardenv1beta1.Region + // randomly selected controlPlaneZone of the region, used for the controlplane config + controlPlaneZone string + workerZones []gardenv1beta1.AvailabilityZone + haConfig *v1alpha1.HighAvailabilityConfig +} + +func (b *baseShootBuilder) newInfrastructureConfig(log logging.Logger) (*runtime.RawExtension, error) { + return b.shootTemplate.Spec.Provider.InfrastructureConfig, nil +} + +// adjustWorkers adjusts the shoot's workers based on the shoot template and the HA configuration. +// The reason why this is so complex is that some parts of a worker spec, e.g. the zones, are immutable. +// Instead of changing it, one has to create a new worker spec (with a new name) and remove the old one. +func (b *baseShootBuilder) adjustWorkers(log logging.Logger, provider *gardenv1beta1.Provider) { + // generate workers based on shoot template + desiredWorkers := b.shootTemplate.Spec.Provider.DeepCopy().Workers + for i := 0; i < len(desiredWorkers); i++ { + worker := &desiredWorkers[i] + worker.Name = fmt.Sprintf("worker-%s", randString(5)) + + if b.haConfig != nil { + // Consensus-based software components depend on maintaining a quorum of (n/2)+1. + // Therefore, at least 3 zones are needed to tolerate the outage of 1 zone. + // For all non-consensus-based software components, 2 nodes are sufficient to tolerate the outage of 1 node. + // To be safe, use 3 zones as the minimum number of zones. + // If the region has less than 3 zones, all available zones are used. + worker.Minimum = int32(min(3, len(b.workerZones))) + // The maximum number of workers should be at least the minimum number of workers. + // If the maximum is configured higher, use the configured maximum. + worker.Maximum = int32(max(int(worker.Minimum), int(worker.Maximum))) + + if b.haConfig.FailureToleranceType == v1alpha1.HighAvailabilityFailureToleranceZone { + // place workers in all available zones, up to the minimum number of workers + worker.Zones = make([]string, 0, worker.Minimum) + for i := 0; i < int(worker.Minimum); i++ { + worker.Zones = append(worker.Zones, b.workerZones[i].Name) + } + } + if b.haConfig.FailureToleranceType == v1alpha1.HighAvailabilityFailureToleranceNode { + // place all workers in one zone + worker.Zones = []string{b.controlPlaneZone} + } + } else { + // If the control plane is not HA or only node-tolerant, all workers are placed in the control plane zone. + worker.Zones = []string{b.controlPlaneZone} + } + } + + keepFromOld := sets.New[string]() + takeFromNew := sets.New[string]() + for idx, dw := range desiredWorkers { + found := false + for _, w := range provider.Workers { + if keepFromOld.Has(w.Name) { + // this worker spec is already kept because it matches another desired spec + continue + } + diffs := workerEquals(&dw, &w) + if len(diffs) == 0 { + // there exists a worker spec that matches one of our desired specs, so let's keep it + log.Debug("Keeping existing worker group, as its specs match desired worker group", "workerGroup", w.Name, "desiredWorkerGroupIndex", idx) + keepFromOld.Insert(w.Name) + found = true + break + } else { + log.Debug("Existing worker group does not match desired worker group", "workerGroup", w.Name, "desiredWorkerGroupIndex", idx, "differences", diffs.String()) + } + } + if !found { + // no worker spec matches the current desired one, so we need to create a new one + takeFromNew.Insert(dw.Name) + } + } + + newWorkers := make([]gardenv1beta1.Worker, 0, keepFromOld.Len()+takeFromNew.Len()) + for _, w := range provider.Workers { + if keepFromOld.Has(w.Name) { + newWorkers = append(newWorkers, w) + } else { + log.Debug("Discarding existing worker group", "workerGroup", w.Name) + } + } + for _, dw := range desiredWorkers { + if takeFromNew.Has(dw.Name) { + log.Debug("Adding new worker group", "workerGroup", dw.Name) + newWorkers = append(newWorkers, dw) + } + } + provider.Workers = newWorkers +} + +const randStringChars = "abcdefghijklmnopqrstuvwxyz0123456789" + +func randString(n int) string { + b := make([]byte, n) + for i := range b { + b[i] = randStringChars[rand.Intn(len(randStringChars))] + } + return string(b) +} + +// workerEquals compares two worker specs for equality, ignoring the name. +// Note that only selected fields are compared, since a DeepEqual causes continuous reconciliation loops +// (probably due to Gardener injecting some information into the worker spec). +// The return value is a slice of fields that are different. If it is nil or empty, the worker specs are equal. +func workerEquals(a, b *gardenv1beta1.Worker) diffList { + if a == nil && b == nil { + return nil + } + if a == nil || b == nil { + aName := "nil" + bName := "nil" + if a != nil { + aName = a.Name + } else if b != nil { + bName = b.Name + } + return []diff{newDiff("nil", aName, bName)} + } + res := []diff{} + if a.Machine.Image.Name != b.Machine.Image.Name { + res = append(res, newDiff("Machine.Image.Name", a.Machine.Image.Name, b.Machine.Image.Name)) + } + if a.Machine.Image.Version != nil && b.Machine.Image.Version != nil && *a.Machine.Image.Version != *b.Machine.Image.Version { + res = append(res, newDiff("Machine.Image.Version", a.Machine.Image.Version, b.Machine.Image.Version)) + } + if a.Machine.Type != b.Machine.Type { + res = append(res, newDiff("Machine.Type", a.Machine.Type, b.Machine.Type)) + } + if a.Machine.Architecture != nil && b.Machine.Architecture != nil && *a.Machine.Architecture != *b.Machine.Architecture { + res = append(res, newDiff("Machine.Architecture", a.Machine.Architecture, b.Machine.Architecture)) + } + if a.Minimum != b.Minimum { + res = append(res, newDiff("Minimum", a.Minimum, b.Minimum)) + } + if a.Maximum != b.Maximum { + res = append(res, newDiff("Maximum", a.Maximum, b.Maximum)) + } + if a.MaxSurge != nil && b.MaxSurge != nil && *a.MaxSurge != *b.MaxSurge { + res = append(res, newDiff("MaxSurge", a.MaxSurge, b.MaxSurge)) + } + if a.MaxUnavailable != nil && b.MaxUnavailable != nil && *a.MaxUnavailable != *b.MaxUnavailable { + res = append(res, newDiff("MaxUnavailable", a.MaxUnavailable, b.MaxUnavailable)) + } + if !sets.New(a.Zones...).Equal(sets.New(b.Zones...)) { + res = append(res, newDiff("Zones", fmt.Sprintf("[%s]", strings.Join(a.Zones, ", ")), fmt.Sprintf("[%s]", strings.Join(b.Zones, ", ")))) + } + return res +} + +func newDiff(id string, a, b any) diff { + return diff{id: id, a: fmt.Sprint(a), b: fmt.Sprint(b)} +} + +type diff struct { + id string + a string + b string +} + +func (d diff) String() string { + return fmt.Sprintf("%s: %s != %s", d.id, d.a, d.b) +} + +type diffList []diff + +func (d diffList) String() string { + res := make([]string, len(d)) + for i, diff := range d { + res[i] = diff.String() + } + return fmt.Sprintf("[%s]", strings.Join(res, ", ")) +} diff --git a/internal/controller/core/apiserver/handler/gardener/shootbuilder_aws.go b/internal/controller/core/apiserver/handler/gardener/shootbuilder_aws.go new file mode 100644 index 0000000..ef85710 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/shootbuilder_aws.go @@ -0,0 +1,91 @@ +package gardener + +import ( + "encoding/json" + "fmt" + "net" + + "sigs.k8s.io/yaml" + + "github.com/apparentlymart/go-cidr/cidr" + + "k8s.io/apimachinery/pkg/runtime" + + "github.com/openmcp-project/controller-utils/pkg/logging" + + gardenawsv1alpha1 "github.com/openmcp-project/mcp-operator/api/external/gardener-extension-provider-aws/pkg/apis/aws/v1alpha1" +) + +var _ shootBuilder = &shootBuilderAWS{} + +type shootBuilderAWS struct { + baseShootBuilder +} + +func (b *shootBuilderAWS) newControlPlaneConfig(log logging.Logger) (*runtime.RawExtension, error) { + controlPlaneConfig := map[string]any{ + "apiVersion": "aws.provider.extensions.gardener.cloud/v1alpha1", + "kind": "ControlPlaneConfig", + } + controlPlaneConfigRaw, err := json.Marshal(controlPlaneConfig) + if err != nil { + return nil, err + } + return &runtime.RawExtension{ + Raw: controlPlaneConfigRaw, + }, nil +} + +// The AWS infrastructure config expects a list of zones with CIDR ranges for workers, public, and internal subnets. +// If this information is not provided in the shoot template, it is added based on the VPC CIDR range. +func (b *shootBuilderAWS) newInfrastructureConfig(log logging.Logger) (*runtime.RawExtension, error) { + // get defined zone networks from shoot template + awsInfraCfg := &gardenawsv1alpha1.InfrastructureConfig{} + err := yaml.Unmarshal(b.shootTemplate.Spec.Provider.InfrastructureConfig.Raw, awsInfraCfg) + if err != nil { + return nil, err + } + if len(awsInfraCfg.Networks.Zones) == 0 { + // add zones to infrastructure config if they are not defined + // Note that this CIDR computation logic is designed for a /16 VPC CIDR range and might not work well with netmask sizes containing fewer IP addresses (= larger number behind the '/'). + vpcRaw := awsInfraCfg.Networks.VPC.CIDR + if vpcRaw == nil { + return nil, fmt.Errorf("networks.vpc.cidr is not defined in the AWS infrastructure config") + } + _, vpc, err := net.ParseCIDR(*vpcRaw) + if err != nil { + return nil, fmt.Errorf("networks.vpc.cidr '%s' is not a valid CIDR: %w", *vpcRaw, err) + } + internal, _ := cidr.PreviousSubnet(vpc, 32) // dummy to simplify the loop + for _, zone := range b.workerZones { + workers, overflow := cidr.NextSubnet(internal, 19) + _, lastIP := cidr.AddressRange(workers) + if overflow || !vpc.Contains(lastIP) { + return nil, fmt.Errorf("unable to calculate 'workers' subnet for zone '%s': vpc CIDR range '%s' does not fully contain computed subnet '%s'", zone.Name, vpc.String(), workers.String()) + } + public, overflow := cidr.NextSubnet(workers, 20) + _, lastIP = cidr.AddressRange(public) + if overflow || !vpc.Contains(lastIP) { + return nil, fmt.Errorf("unable to calculate 'public' subnet for zone '%s': vpc CIDR range '%s' does not fully contain computed subnet '%s'", zone.Name, vpc.String(), public.String()) + } + internal, overflow = cidr.NextSubnet(public, 20) + _, lastIP = cidr.AddressRange(internal) + if overflow || !vpc.Contains(lastIP) { + return nil, fmt.Errorf("unable to calculate 'internal' subnet for zone '%s': vpc CIDR range '%s' does not fully contain computed subnet '%s'", zone.Name, vpc.String(), internal.String()) + } + + awsInfraCfg.Networks.Zones = append(awsInfraCfg.Networks.Zones, gardenawsv1alpha1.Zone{ + Name: zone.Name, + Workers: workers.String(), + Public: public.String(), + Internal: internal.String(), + }) + } + } + + awsInfraCfgRaw, err := json.Marshal(awsInfraCfg) + if err != nil { + return nil, err + } + return &runtime.RawExtension{Raw: awsInfraCfgRaw}, nil +} diff --git a/internal/controller/core/apiserver/handler/gardener/shootbuilder_gcp.go b/internal/controller/core/apiserver/handler/gardener/shootbuilder_gcp.go new file mode 100644 index 0000000..f429721 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/shootbuilder_gcp.go @@ -0,0 +1,30 @@ +package gardener + +import ( + "encoding/json" + + "github.com/openmcp-project/controller-utils/pkg/logging" + "k8s.io/apimachinery/pkg/runtime" +) + +var _ shootBuilder = &shootBuilderGCP{} + +type shootBuilderGCP struct { + baseShootBuilder +} + +func (b *shootBuilderGCP) newControlPlaneConfig(log logging.Logger) (*runtime.RawExtension, error) { + log.Debug("Setting shoot.Spec.Provider.ControlPlaneConfig.Zone", "value", b.controlPlaneZone) + controlPlaneConfig := map[string]any{ + "apiVersion": "gcp.provider.extensions.gardener.cloud/v1alpha1", + "kind": "ControlPlaneConfig", + "zone": b.controlPlaneZone, + } + controlPlaneConfigRaw, err := json.Marshal(controlPlaneConfig) + if err != nil { + return nil, err + } + return &runtime.RawExtension{ + Raw: controlPlaneConfigRaw, + }, nil +} diff --git a/internal/controller/core/apiserver/handler/gardener/suite_test.go b/internal/controller/core/apiserver/handler/gardener/suite_test.go new file mode 100644 index 0000000..e62da20 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/suite_test.go @@ -0,0 +1,178 @@ +package gardener_test + +import ( + "context" + "fmt" + "os" + "path" + "testing" + "time" + + apiserverconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/config" + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/handler/gardener" + "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/schemes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + colactrlutil "github.com/openmcp-project/controller-utils/pkg/controller" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/interceptor" + + openmcptesting "github.com/openmcp-project/controller-utils/pkg/testing" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + authenticationv1alpha1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/authentication/v1alpha1" + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +const ( + gardenCluster = "garden" + gardenCluster2 = "garden2" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Gardener Handler Test Suite") +} + +var ( + defaultConfigSingleBytes []byte + defaultConfigMultiBytes []byte + defaultConfigSingle *apiserverconfig.APIServerProviderConfiguration + defaultConfigMulti *apiserverconfig.APIServerProviderConfiguration + completedDefaultConfigSingle *apiserverconfig.CompletedAPIServerProviderConfiguration + completedDefaultConfigMulti *apiserverconfig.CompletedAPIServerProviderConfiguration + testGardenObjs []client.Object + testGardenObjs2 []client.Object + testCrateObjs []client.Object + env *openmcptesting.ComplexEnvironment + + gardenClusterInterceptorFuncs = interceptor.Funcs{Delete: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.DeleteOption) error { + switch obj.(type) { + case *gardenv1beta1.Shoot: + // throw an error in case of missing deletion confirmation annotation to mimic Gardener webhook behavior + if !colactrlutil.HasAnnotationWithValue(obj, gardener.GardenerDeletionConfirmationAnnotation, "true") { + return fmt.Errorf("missing deletion confirmation annotation") + } + } + // use default logic + return c.Delete(ctx, obj, opts...) + }, + SubResourceCreate: func(ctx context.Context, c client.Client, subResourceName string, obj, subResource client.Object, opts ...client.SubResourceCreateOption) error { + switch subResourceName { + case "adminkubeconfig": + adminKubeconfigRequest, ok := subResource.(*authenticationv1alpha1.AdminKubeconfigRequest) + if !ok { + return fmt.Errorf("unexpected object type %T", subResource) + } + adminKubeconfigRequest.Status.Kubeconfig = []byte("fake") + adminKubeconfigRequest.Status.ExpirationTimestamp = metav1.Time{Time: time.Now().Add(time.Hour)} + return nil + } + // use default logic + return c.SubResource(subResourceName).Create(ctx, obj, subResource, opts...) + }, + } +) + +var _ = BeforeSuite(func() { + var err error + + // load test objects for the Garden cluster + testGardenObjs, err = openmcptesting.LoadObjects(path.Join("testdata", "garden_cluster"), testutils.Scheme) + Expect(err).ToNot(HaveOccurred()) + + // load test objects for the 2nd Garden cluster + testGardenObjs2, err = openmcptesting.LoadObjects(path.Join("testdata", "garden_cluster_2"), testutils.Scheme) + Expect(err).ToNot(HaveOccurred()) + + // load test objects for the Crate cluster + testCrateObjs, err = openmcptesting.LoadObjects(path.Join("testdata", "crate_cluster"), testutils.Scheme) + Expect(err).ToNot(HaveOccurred()) + + // load base config for single Gardener configuration + defaultConfigSingleBytes, err = os.ReadFile(path.Join("testdata", "default_config_single.yaml")) + Expect(err).NotTo(HaveOccurred()) + + // load base config for multi Gardener configuration + defaultConfigMultiBytes, err = os.ReadFile(path.Join("testdata", "default_config_multi.yaml")) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = BeforeEach(func() { + var err error + + // generate default config for single Gardener configuration + defaultConfigSingle, err = apiserverconfig.LoadConfigFromBytes(defaultConfigSingleBytes) + Expect(err).NotTo(HaveOccurred()) + + // generate default config for multi Gardener configuration + defaultConfigMulti, err = apiserverconfig.LoadConfigFromBytes(defaultConfigMultiBytes) + Expect(err).NotTo(HaveOccurred()) + + env = openmcptesting.NewComplexEnvironmentBuilder(). + WithFakeClient(testutils.CrateCluster, testutils.Scheme). + WithInitObjects(testutils.CrateCluster, testCrateObjs...). + WithFakeClient(gardenCluster, schemes.GardenerScheme). + WithInitObjects(gardenCluster, testGardenObjs...). + WithDynamicObjectsWithStatus(gardenCluster, testGardenObjs...). + WithFakeClientBuilderCall(gardenCluster, "WithInterceptorFuncs", gardenClusterInterceptorFuncs). + WithFakeClient(gardenCluster2, schemes.GardenerScheme). + WithInitObjects(gardenCluster2, testGardenObjs2...). + WithDynamicObjectsWithStatus(gardenCluster2, testGardenObjs2...). + WithFakeClientBuilderCall(gardenCluster2, "WithInterceptorFuncs", gardenClusterInterceptorFuncs). + Build() + + // complete the single config + defaultConfigSingle.GardenerConfig.InjectGardenClusterClient("", env.Client(gardenCluster)) + completedDefaultConfigSingle, err = defaultConfigSingle.Complete(env.Ctx) + Expect(err).NotTo(HaveOccurred()) + + // complete the multi config + defaultConfigMulti.GardenerConfig.InjectGardenClusterClient("default", env.Client(gardenCluster)) + defaultConfigMulti.GardenerConfig.InjectGardenClusterClient("extra", env.Client(gardenCluster2)) + completedDefaultConfigMulti, err = defaultConfigMulti.Complete(env.Ctx) + Expect(err).NotTo(HaveOccurred()) +}) + +func initGardenerHandlerTestSingle(apiServerType openmcpv1alpha1.APIServerType, configFlavor string, paths ...string) (*gardener.GardenerConnector, *openmcpv1alpha1.APIServer) { + // load APIServer from file + as := &openmcpv1alpha1.APIServer{} + Expect(openmcptesting.LoadObject(as, paths...)).To(Succeed()) + + // initialize handler + con, err := gardener.NewGardenerConnector(completedDefaultConfigSingle.CompletedCommonConfig, completedDefaultConfigSingle.GardenerConfig, as.Spec.Type) + Expect(err).NotTo(HaveOccurred()) + + con.APIServerType = apiServerType + as.Spec.Type = apiServerType + as.Spec.Internal = &openmcpv1alpha1.APIServerInternalConfiguration{ + GardenerConfig: &openmcpv1alpha1.GardenerInternalConfiguration{ + LandscapeConfiguration: configFlavor, + }, + } + + return con, as +} + +func initGardenerHandlerTestMulti(apiServerType openmcpv1alpha1.APIServerType, configFlavor string, paths ...string) (*gardener.GardenerConnector, *openmcpv1alpha1.APIServer) { + // load APIServer from file + as := &openmcpv1alpha1.APIServer{} + Expect(openmcptesting.LoadObject(as, paths...)).To(Succeed()) + + // initialize handler + con, err := gardener.NewGardenerConnector(completedDefaultConfigMulti.CompletedCommonConfig, completedDefaultConfigMulti.GardenerConfig, as.Spec.Type) + Expect(err).NotTo(HaveOccurred()) + + con.APIServerType = apiServerType + as.Spec.Type = apiServerType + as.Spec.Internal = &openmcpv1alpha1.APIServerInternalConfiguration{ + GardenerConfig: &openmcpv1alpha1.GardenerInternalConfiguration{ + LandscapeConfiguration: configFlavor, + }, + } + + return con, as +} diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-01.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-01.yaml new file mode 100644 index 0000000..3f9645a --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-01.yaml @@ -0,0 +1,53 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Gardener +status: + adminAccess: + creationTimestamp: "2024-05-16T07:29:26Z" + expirationTimestamp: "2024-11-12T07:29:26Z" + kubeconfig: | + apiVersion: v1 + clusters: + - cluster: + certificate-authority-data: redacted + server: redacted + name: cluster + contexts: + - context: + cluster: cluster + user: admin + name: cluster + current-context: cluster + kind: Config + preferences: {} + users: + - name: admin + user: + token: redacted + conditions: + - lastTransitionTime: "2024-06-03T09:07:26Z" + status: "True" + type: apiServerHealthy + gardener: + shoot: + apiVersion: core.gardener.cloud/v1beta1 + kind: Shoot + metadata: + name: test + namespace: garden-test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 3 + resource: 1 diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-02.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-02.yaml new file mode 100644 index 0000000..97ddbeb --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-02.yaml @@ -0,0 +1,15 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Gardener diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-03.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-03.yaml new file mode 100644 index 0000000..f945d52 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-03.yaml @@ -0,0 +1,15 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test2 + openmcp.cloud/mcp-namespace: test2 + name: test2 + namespace: test2 +spec: + desiredRegion: + direction: central + name: europe + type: Gardener diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-04.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-04.yaml new file mode 100644 index 0000000..f4e562b --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-04.yaml @@ -0,0 +1,19 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + gardener: {} + type: Gardener +status: + gardener: + shoot: + apiVersion: core.gardener.cloud/v1beta1 + kind: Shoot + metadata: + name: test + namespace: garden-test diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-05.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-05.yaml new file mode 100644 index 0000000..f885606 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-05.yaml @@ -0,0 +1,11 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test2 + namespace: test2 +spec: + desiredRegion: + direction: central + name: europe + gardener: {} + type: Gardener \ No newline at end of file diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-06.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-06.yaml new file mode 100644 index 0000000..7ec12c5 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-06.yaml @@ -0,0 +1,27 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + gardener: + auditLog: + policyRef: + name: my-policy + secretRef: + name: my-credentials + serviceURL: https://auditlog.example.com:8081 + tenantID: 83b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b + type: standard + type: Gardener +status: + gardener: + shoot: + apiVersion: core.gardener.cloud/v1beta1 + kind: Shoot + metadata: + name: test + namespace: garden-test diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-07.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-07.yaml new file mode 100644 index 0000000..2be99ad --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/connector/apiserver-07.yaml @@ -0,0 +1,27 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + gardener: + auditLog: + policyRef: + name: my-policy + secretRef: + name: my-credentials + serviceURL: https://auditlog.example.com:8081 + tenantID: 83b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b + type: standard + type: Gardener +status: + gardener: + shoot: + apiVersion: core.gardener.cloud/v1beta1 + kind: Shoot + metadata: + name: test-auditlog + namespace: garden-test diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-01.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-01.yaml new file mode 100644 index 0000000..61d6bf3 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-01.yaml @@ -0,0 +1,17 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + openmcp.cloud/mcp-project: test-project + openmcp.cloud/mcp-workspace: test-workspace + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-02.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-02.yaml new file mode 100644 index 0000000..d192b90 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-02.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + openmcp.cloud/mcp-project: test-project + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Gardener + gardener: + region: asia-south1 diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-03.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-03.yaml new file mode 100644 index 0000000..339a66f --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-03.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test +spec: + type: Gardener + diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-04.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-04.yaml new file mode 100644 index 0000000..b5aad98 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-04.yaml @@ -0,0 +1,28 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + openmcp.cloud/mcp-project: test-project + openmcp.cloud/mcp-workspace: test-workspace + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated + gardener: + region: asia-south1 + auditLog: + policyRef: + name: my-test-policy + secretRef: + name: my-test-credentials + serviceURL: https://my-test-auditlog.com:8081 + tenantID: bf123-4567cdef-1234567-89ab-890ab + type: standard + diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-05.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-05.yaml new file mode 100644 index 0000000..d1247c8 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-05.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + openmcp.cloud/mcp-project: test-project + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated + gardener: + region: asia-south1 diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-06.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-06.yaml new file mode 100644 index 0000000..04c9559 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-06.yaml @@ -0,0 +1,22 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated + gardener: + region: asia-south1 + encryptionConfig: + resources: + - configmaps + - statefulsets.apps + - flunders.example.com diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-07.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-07.yaml new file mode 100644 index 0000000..4356985 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-07.yaml @@ -0,0 +1,19 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Gardener + gardener: + region: europe-west3 + highAvailability: + failureToleranceType: node diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-08.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-08.yaml new file mode 100644 index 0000000..e23d907 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/apiserver-08.yaml @@ -0,0 +1,19 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Gardener + gardener: + region: europe-west3 + highAvailability: + failureToleranceType: zone diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/conversion/shoot-05.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/shoot-05.yaml new file mode 100644 index 0000000..7d62b76 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/conversion/shoot-05.yaml @@ -0,0 +1,170 @@ +kind: Shoot +apiVersion: core.gardener.cloud/v1beta1 +metadata: + name: test + namespace: garden-test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + provider.extensions.gardener.cloud/gcp: 'true' + shoot.gardener.cloud/status: healthy + annotations: + shoot.gardener.cloud/cleanup-extended-apis-finalize-grace-period-seconds: '30' +spec: + resources: + - name: auditlog-credentials + resourceRef: + apiVersion: v1 + kind: Secret + name: my-test-credentials + addons: + kubernetesDashboard: + enabled: false + authenticationMode: token + cloudProfileName: gcp + dns: + domain: test.example.org + extensions: + - type: shoot-oidc-service + - type: shoot-dns-service + providerConfig: + apiVersion: service.dns.extensions.gardener.cloud/v1alpha1 + kind: DNSConfig + syncProvidersFromShootSpecDNS: true + - type: shoot-auditlog-service + providerConfig: + apiVersion: service.auditlog.extensions.gardener.cloud/v1alpha1 + kind: AuditlogConfig + type: standard + tenantID: bf123-4567cdef-1234567-89ab-890ab + serviceURL: https://my-test-auditlog.com:8081 + secretReferenceName: auditlog-credentials + hibernation: + enabled: false + kubernetes: + kubeAPIServer: + auditConfig: + auditPolicy: + configMapRef: + name: my-test-policy + runtimeConfig: + apps/v1: true + batch/v1: true + requests: + maxNonMutatingInflight: 400 + maxMutatingInflight: 200 + enableAnonymousAuthentication: false + eventTTL: 1h0m0s + logging: + verbosity: 2 + defaultNotReadyTolerationSeconds: 300 + defaultUnreachableTolerationSeconds: 300 + kubeControllerManager: + nodeCIDRMaskSize: 24 + nodeMonitorGracePeriod: 40s + kubeScheduler: + profile: balanced + kubeProxy: + mode: IPTables + enabled: true + kubelet: + failSwapOn: true + kubeReserved: + cpu: 80m + memory: 1Gi + pid: 20k + imageGCHighThresholdPercent: 50 + imageGCLowThresholdPercent: 40 + serializeImagePulls: true + version: 1.29.3 + verticalPodAutoscaler: + enabled: true + evictAfterOOMThreshold: 10m0s + evictionRateBurst: 1 + evictionRateLimit: -1 + evictionTolerance: 0.5 + recommendationMarginFraction: 0.15 + updaterInterval: 1m0s + recommenderInterval: 1m0s + targetCPUPercentile: 0.9 + enableStaticTokenKubeconfig: false + maintenance: + autoUpdate: + kubernetesVersion: true + machineImageVersion: true + timeWindow: + begin: 000000+0000 + end: 010000+0000 + provider: + type: gcp + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: europe-west1-b + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + purpose: production + region: europe-west1 + secretBindingName: laasds + systemComponents: + coreDNS: + autoscaling: + mode: horizontal + nodeLocalDNS: + enabled: true + schedulerName: default-scheduler +status: + conditions: + - type: APIServerAvailable + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: HealthzRequestSucceeded + message: API server /healthz endpoint responded with success status code. + - type: ControlPlaneHealthy + status: 'True' + lastTransitionTime: '2024-06-11T02:13:15Z' + lastUpdateTime: '2024-06-11T02:13:15Z' + reason: ControlPlaneRunning + message: All control plane components are healthy. + - type: ObservabilityComponentsHealthy + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: ObservabilityComponentsRunning + message: All observability components are healthy. + - type: EveryNodeReady + status: 'True' + lastTransitionTime: '2024-06-11T02:11:15Z' + lastUpdateTime: '2024-06-11T02:11:15Z' + reason: EveryNodeReady + message: All nodes are ready. + - type: SystemComponentsHealthy + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: SystemComponentsRunning + message: All system components are healthy. + constraints: + - type: HibernationPossible + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-07T01:56:39Z' + reason: NoProblematicWebhooks + message: All webhooks are properly configured. + - type: MaintenancePreconditionsSatisfied + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-07T01:56:39Z' + reason: NoProblematicWebhooks + message: All webhooks are properly configured. + hibernated: false + lastOperation: + description: Shoot cluster has been successfully reconciled. + lastUpdateTime: '2024-06-11T00:38:43Z' + progress: 100 + state: Succeeded + type: Reconcile diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/crate_cluster/cm_policy.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/crate_cluster/cm_policy.yaml new file mode 100644 index 0000000..0504407 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/crate_cluster/cm_policy.yaml @@ -0,0 +1,79 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: my-policy + namespace: test +data: + policy: | + apiVersion: audit.k8s.io/v1 + kind: Policy + omitStages: + - RequestReceived + rules: + - level: None + users: + - "gardener" + - "kubelet" + - "etcd-client" + - "vpn-seed" + - "aws-lb-readvertiser" + - "cloud-config-downloader" + - "system:kube-apiserver:kubelet" + - "system:kube-controller-manager" + - "system:kube-aggregator" + - "system:kube-scheduler" + - "system:kube-addon-manager" + - "system:kube-aggregator" + - "system:kube-proxy" + - "system:cluster-autoscaler" + - "system:machine-controller-manager" + - "system:cloud-controller-manager" + - "system:apiserver" + - "garden.sapcloud.io:system:cert-broker" + - "gardener.cloud:system:cert-management" + - "gardener.cloud:system:gardener-resource-manager" + - level: None + userGroups: + - "system:nodes" + - "system:bootstrappers" + - "system:serviceaccounts:kube-system" + - "garden.sapcloud.io:monitoring" + - level: Metadata + # technical users that interact with these resources can have their serviceaccounts excluded from audit logs + # as an example see the upper section where all serviceaccounts from the kube-system namespaces are ignored + resources: + - group: "" + resources: [ "secrets", "configmaps", "serviceaccounts/token" ] + - group: authentication.k8s.io + resources: [ "tokenreviews" ] + - level: None + resources: + - group: "" # core + resources: [ "events" ] + - level: None + verbs: [ "watch", "get", "list" ] + - level: None + nonResourceURLs: + - /* + - level: Metadata + resources: + - group: "" # core + - group: "admissionregistration.k8s.io" + - group: "apiextensions.k8s.io" + - group: "apiregistration.k8s.io" + - group: "apps" + - group: "authentication.k8s.io" + - group: "authorization.k8s.io" + - group: "autoscaling" + - group: "batch" + - group: "certificates.k8s.io" + - group: "coordination.k8s.io" + - group: "extensions" + - group: "metrics.k8s.io" + - group: "networking.k8s.io" + - group: "policy" + - group: "rbac.authorization.k8s.io" + - group: "scheduling.k8s.io" + - group: "settings.k8s.io" + - group: "storage.k8s.io" + - group: "node.k8s.io" \ No newline at end of file diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/crate_cluster/secret-policy-credentials.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/crate_cluster/secret-policy-credentials.yaml new file mode 100644 index 0000000..25ba514 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/crate_cluster/secret-policy-credentials.yaml @@ -0,0 +1,14 @@ +# The credentials stored in this secret are no real credentials! They are used for testing purposes only! +# They can't be used to access any real system or service. +kind: Secret +apiVersion: v1 +metadata: + name: my-credentials + namespace: test +type: Opaque +stringData: + credentials: | + { + "username": "noRealUser", + "password": "noRealPassword", + } \ No newline at end of file diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/default_config_multi.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/default_config_multi.yaml new file mode 100644 index 0000000..3413de9 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/default_config_multi.yaml @@ -0,0 +1,209 @@ +gardener: + defaultConfig: default/gcp + landscapes: + - name: default + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: gcp + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/gcp + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + - name: aws + cloudProfile: aws + regions: + - name: eu-central-1 + - name: eu-west-1 + - name: us-east-1 + - name: ap-southeast-1 + defaultRegion: eu-central-1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/default/aws + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: aws + infrastructureConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + vpc: + cidr: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: m5.large + image: + name: gardenlinux + version: 1592.1.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: gp3 + size: 50Gi + secretBindingName: test + project: test2 + - name: extra + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf + configs: + - name: foo + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/foo + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: foo + - name: bar + cloudProfile: gcp + regions: + - name: europe-west1 + defaultRegion: europe-west1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: multi/extra/bar + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: bar diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/default_config_single.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/default_config_single.yaml new file mode 100644 index 0000000..1161421 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/default_config_single.yaml @@ -0,0 +1,60 @@ +gardener: + cloudProfile: gcp + regions: + - name: europe-west1 + - name: europe-west3 + - name: us-central1 + - name: asia-south1 + defaultRegion: us-central1 + shootTemplate: + metadata: + annotations: + test.openmcp.cloud/config: single + spec: + networking: + type: "calico" + nodes: "10.180.0.0/16" + provider: + type: gcp + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: "" + workers: + - name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + volume: + type: pd-balanced + size: 50Gi + secretBindingName: test + project: test + kubeconfig: | + apiVersion: v1 + kind: Config + clusters: + - cluster: + certificate-authority-data: ZHVtbXkK + server: https://127.0.0.1:55761 + name: dummy + contexts: + - context: + cluster: dummy + user: dummy + name: dummy + current-context: dummy + users: + - name: dummy + user: + token: asdf diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/cloudprofile-aws.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/cloudprofile-aws.yaml new file mode 100644 index 0000000..1e70da9 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/cloudprofile-aws.yaml @@ -0,0 +1,6906 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: CloudProfile +metadata: + name: aws +spec: + kubernetes: + versions: + - classification: preview + version: 1.30.5 + - classification: supported + expirationDate: "2026-06-30T23:59:59Z" + version: 1.30.4 + - classification: deprecated + expirationDate: "2026-06-30T23:59:59Z" + version: 1.30.3 + - classification: deprecated + expirationDate: "2026-06-30T23:59:59Z" + version: 1.30.2 + - classification: deprecated + expirationDate: "2026-06-30T23:59:59Z" + version: 1.30.1 + - classification: preview + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.9 + - classification: supported + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.8 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.7 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.6 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.5 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.4 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.3 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.2 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.1 + - classification: preview + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.14 + - classification: supported + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.13 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.12 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.11 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.10 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.9 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.8 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.7 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.6 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.4 + - classification: deprecated + expirationDate: "2024-10-31T23:59:59Z" + version: 1.27.16 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.15 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.14 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.13 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.12 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.11 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.26.15 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.14 + machineImages: + - name: gardenlinux + updateStrategy: minor + versions: + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1592.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-12-01T23:59:59Z" + version: 1592.0.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.10.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2025-03-01T23:59:59Z" + version: 1443.9.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-10-15T23:59:59Z" + version: 1443.8.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-10-15T23:59:59Z" + version: 1443.7.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-10-15T23:59:59Z" + version: 1443.5.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2025-01-15T23:59:59Z" + version: 1443.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2025-02-28T23:59:59Z" + version: 1312.7.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-15T23:59:59Z" + version: 1312.5.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2025-01-15T23:59:59Z" + version: 1312.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-08-13T23:59:59Z" + version: 934.11.0 + machineTypes: + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m5.large + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: a1.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: a1.4xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: a1.large + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: a1.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: a1.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 7Gi + name: c1.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 15Gi + name: c3.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 30Gi + name: c3.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 60Gi + name: c3.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 7Gi + name: c3.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 15Gi + name: c4.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 30Gi + name: c4.4xlarge + usable: true + - architecture: amd64 + cpu: "36" + gpu: "0" + memory: 60Gi + name: c4.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 7Gi + name: c4.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c5.12xlarge + usable: true + - architecture: amd64 + cpu: "72" + gpu: "0" + memory: 144Gi + name: c5.18xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c5.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c5.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c5.4xlarge + usable: true + - architecture: amd64 + cpu: "36" + gpu: "0" + memory: 72Gi + name: c5.9xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c5.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c5.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c5.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c5a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c5a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c5a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c5a.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c5a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c5a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c5a.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c5a.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c5ad.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c5ad.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c5ad.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c5ad.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c5ad.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c5ad.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c5ad.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c5ad.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c5d.12xlarge + usable: true + - architecture: amd64 + cpu: "72" + gpu: "0" + memory: 144Gi + name: c5d.18xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c5d.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c5d.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c5d.4xlarge + usable: true + - architecture: amd64 + cpu: "36" + gpu: "0" + memory: 72Gi + name: c5d.9xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c5d.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c5d.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c5d.xlarge + usable: true + - architecture: amd64 + cpu: "72" + gpu: "0" + memory: 192Gi + name: c5n.18xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 21Gi + name: c5n.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 42Gi + name: c5n.4xlarge + usable: true + - architecture: amd64 + cpu: "36" + gpu: "0" + memory: 96Gi + name: c5n.9xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 5Gi + name: c5n.large + usable: true + - architecture: amd64 + cpu: "72" + gpu: "0" + memory: 192Gi + name: c5n.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 10Gi + name: c5n.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c6a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c6a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c6a.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 256Gi + name: c6a.32xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 384Gi + name: c6a.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c6a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c6a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c6a.large + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 384Gi + name: c6a.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c6a.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c6g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6g.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c6g.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c6g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c6g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c6g.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6g.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c6g.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c6gd.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6gd.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c6gd.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c6gd.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c6gd.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c6gd.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6gd.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c6gd.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c6gn.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6gn.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c6gn.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c6gn.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c6gn.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c6gn.large + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c6gn.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c6i.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6i.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c6i.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c6i.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 256Gi + name: c6i.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c6i.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c6i.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c6i.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 256Gi + name: c6i.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c6i.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c6id.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6id.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c6id.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c6id.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 256Gi + name: c6id.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c6id.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c6id.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c6id.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 256Gi + name: c6id.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c6id.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c6in.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c6in.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c6in.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c6in.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 256Gi + name: c6in.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c6in.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c6in.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c6in.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 256Gi + name: c6in.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c6in.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c7a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c7a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c7a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c7a.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 256Gi + name: c7a.32xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 384Gi + name: c7a.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c7a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c7a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c7a.large + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 384Gi + name: c7a.metal-48xl + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c7a.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c7g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c7g.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c7g.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c7g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c7g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c7g.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c7g.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c7g.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c7gd.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c7gd.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c7gd.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c7gd.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c7gd.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c7gd.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c7gd.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c7gd.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c7gn.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c7gn.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c7gn.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c7gn.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c7gn.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c7gn.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c7gn.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c7gn.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c7i-flex.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c7i-flex.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c7i-flex.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c7i-flex.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c7i-flex.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c7i.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c7i.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c7i.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c7i.2xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 384Gi + name: c7i.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c7i.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c7i.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c7i.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c7i.metal-24xl + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 384Gi + name: c7i.metal-48xl + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c7i.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: c8g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: c8g.16xlarge + usable: true + - architecture: arm64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c8g.24xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c8g.2xlarge + usable: true + - architecture: arm64 + cpu: "192" + gpu: "0" + memory: 384Gi + name: c8g.48xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c8g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c8g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c8g.large + usable: true + - architecture: arm64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: c8g.metal-24xl + usable: true + - architecture: arm64 + cpu: "192" + gpu: "0" + memory: 384Gi + name: c8g.metal-48xl + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c8g.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 61Gi + name: d2.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 122Gi + name: d2.4xlarge + usable: true + - architecture: amd64 + cpu: "36" + gpu: "0" + memory: 244Gi + name: d2.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 30Gi + name: d2.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: d3.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: d3.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: d3.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: d3.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: d3en.12xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: d3en.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: d3en.4xlarge + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 96Gi + name: d3en.6xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: d3en.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: d3en.xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "8" + memory: 768Gi + name: dl1.24xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: dl2q.24xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 976Gi + name: f1.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 122Gi + name: f1.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 244Gi + name: f1.4xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "4" + memory: 488Gi + name: g3.16xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "1" + memory: 122Gi + name: g3.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "2" + memory: 244Gi + name: g3.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "1" + memory: 30Gi + name: g3s.xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "4" + memory: 256Gi + name: g4ad.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "1" + memory: 32Gi + name: g4ad.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "1" + memory: 64Gi + name: g4ad.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "2" + memory: 128Gi + name: g4ad.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "1" + memory: 16Gi + name: g4ad.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "4" + memory: 192Gi + name: g4dn.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "1" + memory: 256Gi + name: g4dn.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "1" + memory: 32Gi + name: g4dn.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "1" + memory: 64Gi + name: g4dn.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "1" + memory: 128Gi + name: g4dn.8xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "8" + memory: 384Gi + name: g4dn.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "1" + memory: 16Gi + name: g4dn.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "4" + memory: 192Gi + name: g5.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "1" + memory: 256Gi + name: g5.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "4" + memory: 384Gi + name: g5.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "1" + memory: 32Gi + name: g5.2xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "8" + memory: 768Gi + name: g5.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "1" + memory: 64Gi + name: g5.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "1" + memory: 128Gi + name: g5.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "1" + memory: 16Gi + name: g5.xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "2" + memory: 128Gi + name: g5g.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "1" + memory: 16Gi + name: g5g.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "1" + memory: 32Gi + name: g5g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "1" + memory: 64Gi + name: g5g.8xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "2" + memory: 128Gi + name: g5g.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "1" + memory: 8Gi + name: g5g.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "4" + memory: 192Gi + name: g6.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "1" + memory: 256Gi + name: g6.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "4" + memory: 384Gi + name: g6.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "1" + memory: 32Gi + name: g6.2xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "8" + memory: 768Gi + name: g6.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "1" + memory: 64Gi + name: g6.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "1" + memory: 128Gi + name: g6.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "1" + memory: 16Gi + name: g6.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "4" + memory: 384Gi + name: g6e.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "1" + memory: 512Gi + name: g6e.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "4" + memory: 768Gi + name: g6e.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "1" + memory: 64Gi + name: g6e.2xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "8" + memory: 1536Gi + name: g6e.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "1" + memory: 128Gi + name: g6e.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "1" + memory: 256Gi + name: g6e.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "1" + memory: 32Gi + name: g6e.xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "1" + memory: 128Gi + name: gr6.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "1" + memory: 256Gi + name: gr6.8xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: h1.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: h1.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: h1.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: h1.8xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: hpc6a.48xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 1Ti + name: hpc6id.32xlarge + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 768Gi + name: hpc7a.12xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 768Gi + name: hpc7a.24xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: hpc7a.48xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: hpc7a.96xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: hpc7g.16xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: hpc7g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: hpc7g.8xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 61Gi + name: i2.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 122Gi + name: i2.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 244Gi + name: i2.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 30Gi + name: i2.xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 488Gi + name: i3.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 61Gi + name: i3.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 122Gi + name: i3.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 244Gi + name: i3.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 15Gi + name: i3.large + usable: true + - architecture: amd64 + cpu: "72" + gpu: "0" + memory: 512Gi + name: i3.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 30Gi + name: i3.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: i3en.12xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: i3en.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: i3en.2xlarge + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 96Gi + name: i3en.3xlarge + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 192Gi + name: i3en.6xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: i3en.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: i3en.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: i3en.xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: i4g.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: i4g.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: i4g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: i4g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: i4g.large + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: i4g.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: i4i.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: i4i.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: i4i.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: i4i.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: i4i.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: i4i.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: i4i.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: i4i.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: i4i.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: i4i.xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: im4gn.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: im4gn.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: im4gn.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: im4gn.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: im4gn.large + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: im4gn.xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: inf1.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: inf1.2xlarge + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: inf1.6xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: inf1.xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: inf2.24xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: inf2.48xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: inf2.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: inf2.xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 48Gi + name: is4gen.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 96Gi + name: is4gen.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 192Gi + name: is4gen.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 12Gi + name: is4gen.large + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 24Gi + name: is4gen.xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 7Gi + name: m1.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 15Gi + name: m1.xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 34Gi + name: m2.2xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 68Gi + name: m2.4xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 17Gi + name: m2.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 30Gi + name: m3.2xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 7Gi + name: m3.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 15Gi + name: m3.xlarge + usable: true + - architecture: amd64 + cpu: "40" + gpu: "0" + memory: 160Gi + name: m4.10xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m4.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m4.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m4.4xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m4.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m4.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m5.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m5.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m5.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m5.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m5.8xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m5.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m5a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m5a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m5a.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m5a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m5a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m5a.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m5a.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m5ad.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m5ad.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5ad.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m5ad.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m5ad.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m5ad.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m5ad.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m5ad.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m5d.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m5d.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5d.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m5d.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m5d.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m5d.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m5d.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5d.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m5d.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m5dn.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m5dn.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5dn.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m5dn.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m5dn.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m5dn.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m5dn.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5dn.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m5dn.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m5n.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m5n.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5n.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m5n.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m5n.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m5n.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m5n.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m5n.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m5n.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m5zn.12xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m5zn.2xlarge + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 48Gi + name: m5zn.3xlarge + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 96Gi + name: m5zn.6xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m5zn.large + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m5zn.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m5zn.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m6a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m6a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m6a.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6a.32xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: m6a.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m6a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m6a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m6a.large + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: m6a.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m6a.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m6g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6g.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m6g.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m6g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m6g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m6g.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6g.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m6g.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m6gd.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6gd.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m6gd.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m6gd.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m6gd.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m6gd.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6gd.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m6gd.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m6i.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6i.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m6i.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m6i.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6i.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m6i.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m6i.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m6i.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6i.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m6i.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m6id.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6id.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m6id.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m6id.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6id.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m6id.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m6id.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m6id.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6id.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m6id.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m6idn.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6idn.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m6idn.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m6idn.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6idn.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m6idn.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m6idn.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m6idn.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6idn.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m6idn.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m6in.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m6in.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m6in.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m6in.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6in.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m6in.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m6in.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m6in.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m6in.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m6in.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m7a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m7a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m7a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m7a.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: m7a.32xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: m7a.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m7a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m7a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m7a.large + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: m7a.metal-48xl + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m7a.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m7g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m7g.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m7g.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m7g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m7g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m7g.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m7g.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m7g.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m7gd.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m7gd.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m7gd.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m7gd.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m7gd.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m7gd.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m7gd.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m7gd.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m7i-flex.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m7i-flex.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m7i-flex.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m7i-flex.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m7i-flex.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m7i.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m7i.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m7i.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m7i.2xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: m7i.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m7i.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m7i.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m7i.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m7i.metal-24xl + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: m7i.metal-48xl + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m7i.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: m8g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: m8g.16xlarge + usable: true + - architecture: arm64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m8g.24xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: m8g.2xlarge + usable: true + - architecture: arm64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: m8g.48xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: m8g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: m8g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: m8g.large + usable: true + - architecture: arm64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: m8g.metal-24xl + usable: true + - architecture: arm64 + cpu: "192" + gpu: "0" + memory: 768Gi + name: m8g.metal-48xl + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: m8g.xlarge + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 32Gi + name: mac1.metal + usable: true + - architecture: amd64 + cpu: "20" + gpu: "0" + memory: 128Gi + name: mac2-m1ultra.metal + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 24Gi + name: mac2-m2.metal + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 32Gi + name: mac2-m2pro.metal + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: mac2.metal + usable: true + - architecture: amd64 + cpu: "64" + gpu: "16" + memory: 732Gi + name: p2.16xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "8" + memory: 488Gi + name: p2.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "1" + memory: 61Gi + name: p2.xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "8" + memory: 488Gi + name: p3.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "1" + memory: 61Gi + name: p3.2xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "4" + memory: 244Gi + name: p3.8xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "8" + memory: 768Gi + name: p3dn.24xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "8" + memory: 1152Gi + name: p4d.24xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "8" + memory: 1152Gi + name: p4de.24xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "8" + memory: 2Ti + name: p5.48xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "8" + memory: 2Ti + name: p5e.48xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 61Gi + name: r3.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 122Gi + name: r3.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 244Gi + name: r3.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 15Gi + name: r3.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 30Gi + name: r3.xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 488Gi + name: r4.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 61Gi + name: r4.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 122Gi + name: r4.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 244Gi + name: r4.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 15Gi + name: r4.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 30Gi + name: r4.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r5.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r5.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r5.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r5.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r5.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r5.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r5.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r5a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r5a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r5a.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r5a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r5a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r5a.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r5a.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r5ad.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r5ad.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5ad.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r5ad.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r5ad.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r5ad.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r5ad.large + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r5ad.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r5b.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r5b.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5b.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r5b.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r5b.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r5b.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r5b.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5b.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r5b.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r5d.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r5d.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5d.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r5d.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r5d.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r5d.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r5d.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5d.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r5d.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r5dn.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r5dn.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5dn.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r5dn.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r5dn.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r5dn.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r5dn.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5dn.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r5dn.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r5n.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r5n.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5n.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r5n.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r5n.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r5n.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r5n.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r5n.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r5n.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r6a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r6a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r6a.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6a.32xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 1536Gi + name: r6a.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r6a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r6a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r6a.large + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 1536Gi + name: r6a.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r6a.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r6g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6g.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r6g.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r6g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r6g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r6g.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6g.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r6g.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r6gd.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6gd.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r6gd.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r6gd.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r6gd.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r6gd.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6gd.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r6gd.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r6i.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6i.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r6i.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r6i.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6i.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r6i.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r6i.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r6i.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6i.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r6i.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r6id.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6id.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r6id.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r6id.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6id.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r6id.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r6id.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r6id.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6id.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r6id.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r6idn.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6idn.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r6idn.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r6idn.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6idn.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r6idn.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r6idn.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r6idn.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6idn.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r6idn.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r6in.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r6in.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r6in.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r6in.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6in.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r6in.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r6in.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r6in.large + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r6in.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r6in.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r7a.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r7a.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r7a.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r7a.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r7a.32xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 1536Gi + name: r7a.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r7a.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r7a.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r7a.large + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 1536Gi + name: r7a.metal-48xl + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r7a.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r7g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r7g.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r7g.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r7g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r7g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r7g.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r7g.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r7g.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r7gd.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r7gd.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r7gd.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r7gd.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r7gd.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r7gd.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r7gd.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r7gd.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r7i.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r7i.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r7i.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r7i.2xlarge + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 1536Gi + name: r7i.48xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r7i.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r7i.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r7i.large + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r7i.metal-24xl + usable: true + - architecture: amd64 + cpu: "192" + gpu: "0" + memory: 1536Gi + name: r7i.metal-48xl + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r7i.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r7iz.12xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r7iz.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r7iz.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r7iz.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r7iz.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r7iz.8xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r7iz.large + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r7iz.metal-16xl + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1Ti + name: r7iz.metal-32xl + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r7iz.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: r8g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: r8g.16xlarge + usable: true + - architecture: arm64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r8g.24xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: r8g.2xlarge + usable: true + - architecture: arm64 + cpu: "192" + gpu: "0" + memory: 1536Gi + name: r8g.48xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: r8g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: r8g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: r8g.large + usable: true + - architecture: arm64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: r8g.metal-24xl + usable: true + - architecture: arm64 + cpu: "192" + gpu: "0" + memory: 1536Gi + name: r8g.metal-48xl + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: r8g.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2.2xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2.large + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: t2.medium + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t3.2xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t3.large + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: t3.medium + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t3.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t3a.2xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t3a.large + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: t3a.medium + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t3a.xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t4g.2xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t4g.large + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: t4g.medium + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t4g.xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: trn1.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: trn1.32xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: trn1n.32xlarge + usable: true + - architecture: amd64 + cpu: "448" + gpu: "0" + memory: 12Ti + name: u-12tb1.112xlarge + usable: true + - architecture: amd64 + cpu: "448" + gpu: "0" + memory: 12Ti + name: u-12tb1.metal + usable: true + - architecture: amd64 + cpu: "448" + gpu: "0" + memory: 18Ti + name: u-18tb1.112xlarge + usable: true + - architecture: amd64 + cpu: "448" + gpu: "0" + memory: 24Ti + name: u-24tb1.112xlarge + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 3Ti + name: u-3tb1.56xlarge + usable: true + - architecture: amd64 + cpu: "448" + gpu: "0" + memory: 6Ti + name: u-6tb1.112xlarge + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 6Ti + name: u-6tb1.56xlarge + usable: true + - architecture: amd64 + cpu: "448" + gpu: "0" + memory: 6Ti + name: u-6tb1.metal + usable: true + - architecture: amd64 + cpu: "448" + gpu: "0" + memory: 9Ti + name: u-9tb1.112xlarge + usable: true + - architecture: amd64 + cpu: "448" + gpu: "0" + memory: 9Ti + name: u-9tb1.metal + usable: true + - architecture: amd64 + cpu: "896" + gpu: "0" + memory: 12Ti + name: u7i-12tb.224xlarge + usable: true + - architecture: amd64 + cpu: "896" + gpu: "0" + memory: 16Ti + name: u7in-16tb.224xlarge + usable: true + - architecture: amd64 + cpu: "896" + gpu: "0" + memory: 24Ti + name: u7in-24tb.224xlarge + usable: true + - architecture: amd64 + cpu: "896" + gpu: "0" + memory: 32Ti + name: u7in-32tb.224xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 192Gi + name: vt1.24xlarge + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 24Gi + name: vt1.3xlarge + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: vt1.6xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 976Gi + name: x1.16xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1952Gi + name: x1.32xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 1952Gi + name: x1e.16xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 244Gi + name: x1e.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 3904Gi + name: x1e.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 488Gi + name: x1e.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 976Gi + name: x1e.8xlarge + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 122Gi + name: x1e.xlarge + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 768Gi + name: x2gd.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 1Ti + name: x2gd.16xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 128Gi + name: x2gd.2xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 256Gi + name: x2gd.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 512Gi + name: x2gd.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 32Gi + name: x2gd.large + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 1Ti + name: x2gd.metal + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 64Gi + name: x2gd.xlarge + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 1Ti + name: x2idn.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1536Gi + name: x2idn.24xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 2Ti + name: x2idn.32xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 2Ti + name: x2idn.metal + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 2Ti + name: x2iedn.16xlarge + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 3Ti + name: x2iedn.24xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 256Gi + name: x2iedn.2xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 4Ti + name: x2iedn.32xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 512Gi + name: x2iedn.4xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 1Ti + name: x2iedn.8xlarge + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 4Ti + name: x2iedn.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 128Gi + name: x2iedn.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 1536Gi + name: x2iezn.12xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 256Gi + name: x2iezn.2xlarge + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 512Gi + name: x2iezn.4xlarge + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 768Gi + name: x2iezn.6xlarge + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 1Ti + name: x2iezn.8xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 1536Gi + name: x2iezn.metal + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 768Gi + name: x8g.12xlarge + usable: true + - architecture: arm64 + cpu: "64" + gpu: "0" + memory: 1Ti + name: x8g.16xlarge + usable: true + - architecture: arm64 + cpu: "96" + gpu: "0" + memory: 1536Gi + name: x8g.24xlarge + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 128Gi + name: x8g.2xlarge + usable: true + - architecture: arm64 + cpu: "192" + gpu: "0" + memory: 3Ti + name: x8g.48xlarge + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 256Gi + name: x8g.4xlarge + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 512Gi + name: x8g.8xlarge + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 32Gi + name: x8g.large + usable: true + - architecture: arm64 + cpu: "96" + gpu: "0" + memory: 1536Gi + name: x8g.metal-24xl + usable: true + - architecture: arm64 + cpu: "192" + gpu: "0" + memory: 3Ti + name: x8g.metal-48xl + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 64Gi + name: x8g.xlarge + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: z1d.12xlarge + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: z1d.2xlarge + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 96Gi + name: z1d.3xlarge + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 192Gi + name: z1d.6xlarge + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: z1d.large + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: z1d.metal + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: z1d.xlarge + usable: true + providerConfig: + apiVersion: aws.provider.extensions.gardener.cloud/v1alpha1 + kind: CloudProfileConfig + machineImages: + - name: gardenlinux + versions: + - regions: + - ami: ami-01676edc93091f9b1 + architecture: amd64 + name: ap-south-1 + - ami: ami-0c250ef63862b7795 + architecture: amd64 + name: eu-north-1 + - ami: ami-090e6ac1aa9d2bd3e + architecture: amd64 + name: eu-west-3 + - ami: ami-0148232d0981c655c + architecture: amd64 + name: eu-west-2 + - ami: ami-07ec6a0b4df7e943b + architecture: amd64 + name: eu-west-1 + - ami: ami-0911f85d8817a3bc7 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-00760e4febfda3afc + architecture: amd64 + name: ap-northeast-2 + - ami: ami-0b76d073c998e7ef2 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-05ac1e88454239da9 + architecture: amd64 + name: me-central-1 + - ami: ami-06f848aa8edaeda77 + architecture: amd64 + name: ca-central-1 + - ami: ami-005e07fc35e592a71 + architecture: amd64 + name: sa-east-1 + - ami: ami-0a2fa6133a6411a22 + architecture: amd64 + name: ap-southeast-1 + - ami: ami-0cb7b9a40617555f0 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0b7e922f40c46d5bd + architecture: amd64 + name: us-east-1 + - ami: ami-074659cc3930c866e + architecture: amd64 + name: us-east-2 + - ami: ami-06a6b4d68b1c5848e + architecture: amd64 + name: us-west-1 + - ami: ami-03692fdf1ce3b6d63 + architecture: amd64 + name: us-west-2 + - ami: ami-0a439213d42b8ae8c + architecture: amd64 + name: eu-central-1 + - ami: ami-0c8c87d96ac42c1f8 + architecture: amd64 + name: cn-north-1 + - ami: ami-08f5c2ade7fa8ab47 + architecture: amd64 + name: cn-northwest-1 + - ami: ami-03011405dcc93dd01 + architecture: arm64 + name: ap-south-1 + - ami: ami-08c946a20aa997245 + architecture: arm64 + name: eu-north-1 + - ami: ami-002fd642bf354b0e6 + architecture: arm64 + name: eu-west-3 + - ami: ami-0250a4ba5a3f868de + architecture: arm64 + name: eu-west-2 + - ami: ami-057afd1dd77eb7dba + architecture: arm64 + name: eu-west-1 + - ami: ami-086786f2469834f6c + architecture: arm64 + name: ap-northeast-3 + - ami: ami-08e9def3ab97ca1f9 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-07b6f78575c60651a + architecture: arm64 + name: ap-northeast-1 + - ami: ami-0a72ca5e771fd18eb + architecture: arm64 + name: me-central-1 + - ami: ami-0bf4769fc3b774a59 + architecture: arm64 + name: ca-central-1 + - ami: ami-011fa9fce065489a2 + architecture: arm64 + name: sa-east-1 + - ami: ami-05806ebbce21eff1f + architecture: arm64 + name: ap-southeast-1 + - ami: ami-04220df75ed3ec85f + architecture: arm64 + name: ap-southeast-2 + - ami: ami-01114d33a8f81a4cc + architecture: arm64 + name: us-east-1 + - ami: ami-0aea287f21404e1e8 + architecture: arm64 + name: us-east-2 + - ami: ami-0a8cbcff8d9efbb98 + architecture: arm64 + name: us-west-1 + - ami: ami-0049f9bf6dc4d9c0d + architecture: arm64 + name: us-west-2 + - ami: ami-0e7e9c5729cdcf387 + architecture: arm64 + name: eu-central-1 + - ami: ami-0a475f0c85c986995 + architecture: arm64 + name: cn-north-1 + - ami: ami-01055603da87e89ff + architecture: arm64 + name: cn-northwest-1 + version: 1592.1.0 + - regions: + - ami: ami-0ae59bd1c5f04401c + architecture: arm64 + name: ap-south-1 + - ami: ami-05f9c6cefd001be5e + architecture: arm64 + name: eu-north-1 + - ami: ami-0f11b55e12078f5eb + architecture: arm64 + name: eu-west-3 + - ami: ami-0e912a62b51a2c1cd + architecture: arm64 + name: eu-west-2 + - ami: ami-09a86a93361bb72f9 + architecture: arm64 + name: eu-west-1 + - ami: ami-0037de560a3b4efb7 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-09cf7e440ec36db57 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-002efa311a0687a36 + architecture: arm64 + name: ap-northeast-1 + - ami: ami-0a1c59313a5594db4 + architecture: arm64 + name: me-central-1 + - ami: ami-0af8c06528e12a233 + architecture: arm64 + name: ca-central-1 + - ami: ami-0fa7ab489241343d9 + architecture: arm64 + name: sa-east-1 + - ami: ami-058220f2696313b3d + architecture: arm64 + name: ap-southeast-1 + - ami: ami-0b2796e907e62433a + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0b383d24206ed2963 + architecture: arm64 + name: us-east-1 + - ami: ami-0b607a22c020c2e7d + architecture: arm64 + name: us-east-2 + - ami: ami-01e8d12f05449faec + architecture: arm64 + name: us-west-1 + - ami: ami-03b11b7a3aaf6ef24 + architecture: arm64 + name: us-west-2 + - ami: ami-0b031eed480886797 + architecture: arm64 + name: eu-central-1 + - ami: ami-0d7b3160c38338f0c + architecture: arm64 + name: cn-north-1 + - ami: ami-02e3ffddae475b596 + architecture: arm64 + name: cn-northwest-1 + - ami: ami-051b43acee2bec292 + architecture: amd64 + name: ap-south-1 + - ami: ami-0e0c7704328c2d08c + architecture: amd64 + name: eu-north-1 + - ami: ami-08ef7db927702f8f6 + architecture: amd64 + name: eu-west-3 + - ami: ami-030df78edc884d316 + architecture: amd64 + name: eu-west-2 + - ami: ami-01570197ba3344ceb + architecture: amd64 + name: eu-west-1 + - ami: ami-0985bb5267ee3c714 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-048784b29c1cc91df + architecture: amd64 + name: ap-northeast-2 + - ami: ami-03691ec608b0da82a + architecture: amd64 + name: ap-northeast-1 + - ami: ami-0e1c3b4e0c6df3a56 + architecture: amd64 + name: me-central-1 + - ami: ami-0afb7c9e8b5f1fd7e + architecture: amd64 + name: ca-central-1 + - ami: ami-07cc8ec46f2e7fe0f + architecture: amd64 + name: sa-east-1 + - ami: ami-0aa2f4c55b4befe36 + architecture: amd64 + name: ap-southeast-1 + - ami: ami-01558da4f9b68b0a8 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0b1351c507c03a93f + architecture: amd64 + name: us-east-1 + - ami: ami-043dcf3b7b11a3d9f + architecture: amd64 + name: us-east-2 + - ami: ami-09e84fd54917a9dff + architecture: amd64 + name: us-west-1 + - ami: ami-0a22155a8f4076bd6 + architecture: amd64 + name: us-west-2 + - ami: ami-0cf3d562f47cad166 + architecture: amd64 + name: eu-central-1 + - ami: ami-072ccae10ad28812b + architecture: amd64 + name: cn-north-1 + - ami: ami-09b0fb632d39b2304 + architecture: amd64 + name: cn-northwest-1 + version: 1592.0.0 + - regions: + - ami: ami-0b9519e680eebae1f + architecture: arm64 + name: ap-south-1 + - ami: ami-027934b8b0ab9e757 + architecture: arm64 + name: eu-north-1 + - ami: ami-0a25909ef0ee75ccc + architecture: arm64 + name: eu-west-3 + - ami: ami-0638f8efd343d720c + architecture: arm64 + name: eu-west-2 + - ami: ami-01e59a96180e1768d + architecture: arm64 + name: eu-west-1 + - ami: ami-0baf178c6046d3176 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-0768059761cf09bd9 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-03f28b1775a6ca1b6 + architecture: arm64 + name: ap-northeast-1 + - ami: ami-0fafb2984f09143bc + architecture: arm64 + name: me-central-1 + - ami: ami-03a0c3f9b3ff4752e + architecture: arm64 + name: ca-central-1 + - ami: ami-0d252bf259fd46355 + architecture: arm64 + name: sa-east-1 + - ami: ami-0e313553b2e616d42 + architecture: arm64 + name: ap-southeast-1 + - ami: ami-018706bf211faee33 + architecture: arm64 + name: ap-southeast-2 + - ami: ami-04422c50bf71e1321 + architecture: arm64 + name: us-east-1 + - ami: ami-061a8560e2af99441 + architecture: arm64 + name: us-east-2 + - ami: ami-0d4ef167948b78142 + architecture: arm64 + name: us-west-1 + - ami: ami-0d5ee92bb2e2b2971 + architecture: arm64 + name: us-west-2 + - ami: ami-0bdd371db6603e621 + architecture: arm64 + name: eu-central-1 + - ami: ami-04ae0e68481657e0b + architecture: arm64 + name: cn-north-1 + - ami: ami-0c2206a5ab2536feb + architecture: arm64 + name: cn-northwest-1 + - ami: ami-03be8516d87341c81 + architecture: amd64 + name: ap-south-1 + - ami: ami-0f3fdd663938f3252 + architecture: amd64 + name: eu-north-1 + - ami: ami-09c30e411a4b5fee8 + architecture: amd64 + name: eu-west-3 + - ami: ami-08984a13ac1c87db2 + architecture: amd64 + name: eu-west-2 + - ami: ami-0741a5403f7b3fc03 + architecture: amd64 + name: eu-west-1 + - ami: ami-06bbc482546025d0c + architecture: amd64 + name: ap-northeast-3 + - ami: ami-07003b86c24454888 + architecture: amd64 + name: ap-northeast-2 + - ami: ami-0aa30be6cfb0813a2 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-038d3cc1909156f54 + architecture: amd64 + name: me-central-1 + - ami: ami-011be797272f4c06d + architecture: amd64 + name: ca-central-1 + - ami: ami-09510cc14711d1493 + architecture: amd64 + name: sa-east-1 + - ami: ami-0fb152a5d8b4679a5 + architecture: amd64 + name: ap-southeast-1 + - ami: ami-0c6aa750435931a80 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-05fe3f8ff9b793947 + architecture: amd64 + name: us-east-1 + - ami: ami-0948f9d87e4f588a2 + architecture: amd64 + name: us-east-2 + - ami: ami-0dfc8952d45f3a6e3 + architecture: amd64 + name: us-west-1 + - ami: ami-085805e3ea1dc5123 + architecture: amd64 + name: us-west-2 + - ami: ami-0e8ea85b0fe76a6a4 + architecture: amd64 + name: eu-central-1 + - ami: ami-0915053bf45587347 + architecture: amd64 + name: cn-north-1 + - ami: ami-01a366356931b588d + architecture: amd64 + name: cn-northwest-1 + version: 1443.10.0 + - regions: + - ami: ami-03d2c06996df0522f + architecture: amd64 + name: ap-south-1 + - ami: ami-036dc40d40f6c16da + architecture: amd64 + name: eu-north-1 + - ami: ami-0a5709527de87a201 + architecture: amd64 + name: eu-west-3 + - ami: ami-0c9a53f49a9d42d20 + architecture: amd64 + name: eu-west-2 + - ami: ami-0c6ae41704347aeb5 + architecture: amd64 + name: eu-west-1 + - ami: ami-01b46199d248df22d + architecture: amd64 + name: ap-northeast-3 + - ami: ami-0bc04965f9599f983 + architecture: amd64 + name: ap-northeast-2 + - ami: ami-08ea47af96debc058 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-0e899e6e0e153291e + architecture: amd64 + name: me-central-1 + - ami: ami-07f84b5ff4b512015 + architecture: amd64 + name: ca-central-1 + - ami: ami-0fbfb6ee1f9d010b4 + architecture: amd64 + name: sa-east-1 + - ami: ami-063334b106a8dcfe6 + architecture: amd64 + name: ap-southeast-1 + - ami: ami-032934fed09d15d27 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0f9452a89fbd699bf + architecture: amd64 + name: us-east-1 + - ami: ami-0dfe8e5e4db2ff342 + architecture: amd64 + name: us-east-2 + - ami: ami-0c835c7f7cb1e3d9c + architecture: amd64 + name: us-west-1 + - ami: ami-0d18df2b543698b03 + architecture: amd64 + name: us-west-2 + - ami: ami-060bea803241116fe + architecture: amd64 + name: eu-central-1 + - ami: ami-038d4ba5a709c18c8 + architecture: arm64 + name: ap-south-1 + - ami: ami-0d60cd06a9145cf0c + architecture: arm64 + name: eu-north-1 + - ami: ami-0b96a1517bb154f42 + architecture: arm64 + name: eu-west-3 + - ami: ami-0031d5fa719797d9c + architecture: arm64 + name: eu-west-2 + - ami: ami-0ff05c8771a8046c5 + architecture: arm64 + name: eu-west-1 + - ami: ami-0ee56de71955b96bd + architecture: arm64 + name: ap-northeast-3 + - ami: ami-06efa05b670a4f669 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-06d15786192d341cc + architecture: arm64 + name: ap-northeast-1 + - ami: ami-077f23c9df68e906a + architecture: arm64 + name: me-central-1 + - ami: ami-02fd193ff0bd192ec + architecture: arm64 + name: ca-central-1 + - ami: ami-0314c12dd9d299e5a + architecture: arm64 + name: sa-east-1 + - ami: ami-006b265c35f3326ce + architecture: arm64 + name: ap-southeast-1 + - ami: ami-0fc5719d60d6b0828 + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0fc6a9e753344a5e9 + architecture: arm64 + name: us-east-1 + - ami: ami-0005d3c507a0c2734 + architecture: arm64 + name: us-east-2 + - ami: ami-0e39fb8c6bf56d3cd + architecture: arm64 + name: us-west-1 + - ami: ami-0323ddadeb43c8a73 + architecture: arm64 + name: us-west-2 + - ami: ami-05f2f720aab1998d8 + architecture: arm64 + name: eu-central-1 + version: 1443.9.0 + - regions: + - ami: ami-079fb418d03a64d3f + architecture: arm64 + name: ap-south-1 + - ami: ami-0de8f2eaa3035ce88 + architecture: arm64 + name: eu-north-1 + - ami: ami-02c949aaccf9d0d65 + architecture: arm64 + name: eu-west-3 + - ami: ami-07d0649df8dcaf555 + architecture: arm64 + name: eu-west-2 + - ami: ami-0112292aa321f6354 + architecture: arm64 + name: eu-west-1 + - ami: ami-0bccabc7e52a10518 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-0741998a6a6939553 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-058270775dd8a2bfb + architecture: arm64 + name: ap-northeast-1 + - ami: ami-00682d3eba6b33009 + architecture: arm64 + name: me-central-1 + - ami: ami-07a862572b9a179c1 + architecture: arm64 + name: ca-central-1 + - ami: ami-02889ccaf1b2549e9 + architecture: arm64 + name: sa-east-1 + - ami: ami-0b70110d6b7e0a7cd + architecture: arm64 + name: ap-southeast-1 + - ami: ami-0584c54fa8c487a0e + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0fa98dad398c3440d + architecture: arm64 + name: us-east-1 + - ami: ami-023da08d18bad38a2 + architecture: arm64 + name: us-east-2 + - ami: ami-06f47aa06ee429deb + architecture: arm64 + name: us-west-1 + - ami: ami-0565b0e28382ed5f2 + architecture: arm64 + name: us-west-2 + - ami: ami-0ee47e2df72132aea + architecture: arm64 + name: eu-central-1 + - ami: ami-0c6a6d1c79d92f674 + architecture: arm64 + name: cn-north-1 + - ami: ami-0e549c491e9ccd21b + architecture: arm64 + name: cn-northwest-1 + - ami: ami-0b21cd12adf7ad720 + architecture: amd64 + name: ap-south-1 + - ami: ami-0181aa4821dd11f15 + architecture: amd64 + name: eu-north-1 + - ami: ami-05eb914f570e864f5 + architecture: amd64 + name: eu-west-3 + - ami: ami-06b9d057c5d984a18 + architecture: amd64 + name: eu-west-2 + - ami: ami-08ec1ca4dae16cb1f + architecture: amd64 + name: eu-west-1 + - ami: ami-02489646ef93d9aa4 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-013a26a8fbc3b68bb + architecture: amd64 + name: ap-northeast-2 + - ami: ami-0e83d8ce261b85deb + architecture: amd64 + name: ap-northeast-1 + - ami: ami-022d671be6b3763a1 + architecture: amd64 + name: me-central-1 + - ami: ami-0f1f43de5f0c3388b + architecture: amd64 + name: ca-central-1 + - ami: ami-04120f2df5dfc283e + architecture: amd64 + name: sa-east-1 + - ami: ami-0c033523d3d33e72e + architecture: amd64 + name: ap-southeast-1 + - ami: ami-03478bf7220d90ddb + architecture: amd64 + name: ap-southeast-2 + - ami: ami-013d104b2ee06c6d5 + architecture: amd64 + name: us-east-1 + - ami: ami-039ead6f4a5371ca6 + architecture: amd64 + name: us-east-2 + - ami: ami-0116340e3f057a927 + architecture: amd64 + name: us-west-1 + - ami: ami-0880f2fcbb19ffc60 + architecture: amd64 + name: us-west-2 + - ami: ami-0073decb44280a611 + architecture: amd64 + name: eu-central-1 + - ami: ami-07700f773ac9a446c + architecture: amd64 + name: cn-north-1 + - ami: ami-048bbadd63bf39c1d + architecture: amd64 + name: cn-northwest-1 + version: 1443.8.0 + - regions: + - ami: ami-09f6e3d5be5108986 + architecture: arm64 + name: ap-south-1 + - ami: ami-07240a4e807afc40f + architecture: arm64 + name: eu-north-1 + - ami: ami-03463aef957943918 + architecture: arm64 + name: eu-west-3 + - ami: ami-006a8725fba2a8137 + architecture: arm64 + name: eu-west-2 + - ami: ami-05ba985411c77716d + architecture: arm64 + name: eu-west-1 + - ami: ami-07dfcf742c9db11cf + architecture: arm64 + name: ap-northeast-3 + - ami: ami-0a1eba01f2969fc63 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-01182aa1c6de874e5 + architecture: arm64 + name: ap-northeast-1 + - ami: ami-060a37bfd382e3d99 + architecture: arm64 + name: me-central-1 + - ami: ami-04bc7b56d37930f6d + architecture: arm64 + name: ca-central-1 + - ami: ami-03cf84043b73dcb3c + architecture: arm64 + name: sa-east-1 + - ami: ami-0eadaa615113f8ead + architecture: arm64 + name: ap-southeast-1 + - ami: ami-0a2b2d96e064f9cb6 + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0345d40523c122a2e + architecture: arm64 + name: us-east-1 + - ami: ami-08176773fdfaf2c02 + architecture: arm64 + name: us-east-2 + - ami: ami-0437b447598db6a00 + architecture: arm64 + name: us-west-1 + - ami: ami-094fa7ac8ae8aa529 + architecture: arm64 + name: us-west-2 + - ami: ami-0db784186de71dae2 + architecture: arm64 + name: eu-central-1 + - ami: ami-052d50e7466f3d43b + architecture: arm64 + name: cn-north-1 + - ami: ami-0f55689f1da01e23e + architecture: arm64 + name: cn-northwest-1 + - ami: ami-05cd1ca45a928e2b2 + architecture: amd64 + name: ap-south-1 + - ami: ami-096e105b537026237 + architecture: amd64 + name: eu-north-1 + - ami: ami-0cd2d69a7ff5eb1b3 + architecture: amd64 + name: eu-west-3 + - ami: ami-0bdc910cfe5e90a73 + architecture: amd64 + name: eu-west-2 + - ami: ami-0f94e06093cf02371 + architecture: amd64 + name: eu-west-1 + - ami: ami-0e5d60372243f077b + architecture: amd64 + name: ap-northeast-3 + - ami: ami-066a44073e9aecf5f + architecture: amd64 + name: ap-northeast-2 + - ami: ami-07760dde064d09041 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-07bed4a073508bc22 + architecture: amd64 + name: me-central-1 + - ami: ami-0b3f27960eaea4b9c + architecture: amd64 + name: ca-central-1 + - ami: ami-09f15d30ca9c0b6ef + architecture: amd64 + name: sa-east-1 + - ami: ami-0af06ac11cdb58b67 + architecture: amd64 + name: ap-southeast-1 + - ami: ami-061e6293c7f250c19 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0dbefd80ffe95a6a1 + architecture: amd64 + name: us-east-1 + - ami: ami-0fa5d9e92e8391548 + architecture: amd64 + name: us-east-2 + - ami: ami-055135f91ee0afcc6 + architecture: amd64 + name: us-west-1 + - ami: ami-00fc19c1280c6244f + architecture: amd64 + name: us-west-2 + - ami: ami-04b08612dc97b5514 + architecture: amd64 + name: eu-central-1 + - ami: ami-072806cd9283164e8 + architecture: amd64 + name: cn-north-1 + - ami: ami-04bc7fe6bae603f45 + architecture: amd64 + name: cn-northwest-1 + version: 1443.7.0 + - regions: + - ami: ami-0f413f29fee6d88a7 + architecture: amd64 + name: ap-south-1 + - ami: ami-0b4410d4c62ca7f52 + architecture: amd64 + name: eu-north-1 + - ami: ami-0176110e49eec2507 + architecture: amd64 + name: eu-west-3 + - ami: ami-02cf9b428a3f33ee4 + architecture: amd64 + name: eu-west-2 + - ami: ami-07fe0e0180e449aab + architecture: amd64 + name: eu-west-1 + - ami: ami-02d3f9d1c4c385cd2 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-044bd75b588204f4c + architecture: amd64 + name: ap-northeast-2 + - ami: ami-0de2908b9f8bf7383 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-0e18dbc08161594d7 + architecture: amd64 + name: me-central-1 + - ami: ami-076381cc46d43a1a1 + architecture: amd64 + name: ca-central-1 + - ami: ami-0e13940867f563c94 + architecture: amd64 + name: sa-east-1 + - ami: ami-00d2d79117803381d + architecture: amd64 + name: ap-southeast-1 + - ami: ami-03a545526f57b75fc + architecture: amd64 + name: ap-southeast-2 + - ami: ami-061a97581e0e9ef5b + architecture: amd64 + name: us-east-1 + - ami: ami-0297cb7cab5875929 + architecture: amd64 + name: us-east-2 + - ami: ami-070f82e475c2395c7 + architecture: amd64 + name: us-west-1 + - ami: ami-04b8a6c110a7da9fa + architecture: amd64 + name: us-west-2 + - ami: ami-02e2809582e6b9f33 + architecture: amd64 + name: eu-central-1 + - ami: ami-07e7cefafa52378c7 + architecture: amd64 + name: cn-north-1 + - ami: ami-0fe07f3936c6c8745 + architecture: amd64 + name: cn-northwest-1 + - ami: ami-0b3ad42bf3a67c7f4 + architecture: arm64 + name: ap-south-1 + - ami: ami-02a9e58f91b7671d2 + architecture: arm64 + name: eu-north-1 + - ami: ami-05a3d112fd68b3837 + architecture: arm64 + name: eu-west-3 + - ami: ami-0bc08cf557e5ad137 + architecture: arm64 + name: eu-west-2 + - ami: ami-07f6c625f8cdabc7e + architecture: arm64 + name: eu-west-1 + - ami: ami-04f34681126c903b8 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-0faadc027d099ca76 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-0eb8d05cee27a5137 + architecture: arm64 + name: ap-northeast-1 + - ami: ami-069d9aedd77234e74 + architecture: arm64 + name: me-central-1 + - ami: ami-0fa0653a1dd5d7fe6 + architecture: arm64 + name: ca-central-1 + - ami: ami-08bc44324a2426e05 + architecture: arm64 + name: sa-east-1 + - ami: ami-01c05c3df8a32d53a + architecture: arm64 + name: ap-southeast-1 + - ami: ami-0a66044207667d6af + architecture: arm64 + name: ap-southeast-2 + - ami: ami-049297bbb5fef414f + architecture: arm64 + name: us-east-1 + - ami: ami-07f5f6023270de3e1 + architecture: arm64 + name: us-east-2 + - ami: ami-091935ae387dfdd08 + architecture: arm64 + name: us-west-1 + - ami: ami-01b1378d253d89b01 + architecture: arm64 + name: us-west-2 + - ami: ami-05abfe8a8e526ce6a + architecture: arm64 + name: eu-central-1 + - ami: ami-02db40deab0a94453 + architecture: arm64 + name: cn-north-1 + - ami: ami-042f9ececc2c436f5 + architecture: arm64 + name: cn-northwest-1 + version: 1443.5.0 + - regions: + - ami: ami-01153e3bffd66e9d0 + architecture: amd64 + name: ap-south-1 + - ami: ami-025f32b2cae0193be + architecture: amd64 + name: eu-north-1 + - ami: ami-0da97e8c6ce348089 + architecture: amd64 + name: eu-west-3 + - ami: ami-0161091419cc49e2a + architecture: amd64 + name: eu-west-2 + - ami: ami-0b50a84eb1902087e + architecture: amd64 + name: eu-west-1 + - ami: ami-07886cffb5b69fed1 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-04d073ce94f15715b + architecture: amd64 + name: ap-northeast-2 + - ami: ami-07bedff70711c074e + architecture: amd64 + name: ap-northeast-1 + - ami: ami-02e71c499bedd91ab + architecture: amd64 + name: ca-central-1 + - ami: ami-09d4e9c957a3b46ab + architecture: amd64 + name: sa-east-1 + - ami: ami-0fd5417802a3c2cee + architecture: amd64 + name: ap-southeast-1 + - ami: ami-09ae980fe95e8f1f0 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0cb4d393dbe973322 + architecture: amd64 + name: us-east-1 + - ami: ami-0fd87701d33f55569 + architecture: amd64 + name: us-east-2 + - ami: ami-0c9a4840d94d2165f + architecture: amd64 + name: us-west-1 + - ami: ami-0ee14c68f42310830 + architecture: amd64 + name: us-west-2 + - ami: ami-081e9a5c2e368841d + architecture: amd64 + name: eu-central-1 + - ami: ami-06eb9f99c8efbac4d + architecture: amd64 + name: cn-north-1 + - ami: ami-0d92043550d9576ad + architecture: amd64 + name: cn-northwest-1 + - ami: ami-078e5e8535f790a56 + architecture: arm64 + name: ap-south-1 + - ami: ami-0287004bafd100a1a + architecture: arm64 + name: eu-north-1 + - ami: ami-09ceefea0d1380c9f + architecture: arm64 + name: eu-west-3 + - ami: ami-0a69171c5a5b7e63d + architecture: arm64 + name: eu-west-2 + - ami: ami-0b63a6a8554244589 + architecture: arm64 + name: eu-west-1 + - ami: ami-06a291bf052f8c279 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-043132c3c173cdd5c + architecture: arm64 + name: ap-northeast-2 + - ami: ami-0a6763cee96e51392 + architecture: arm64 + name: ap-northeast-1 + - ami: ami-06d45978d995526bb + architecture: arm64 + name: ca-central-1 + - ami: ami-00684d42811e8065f + architecture: arm64 + name: sa-east-1 + - ami: ami-05b4f62a16ce457cf + architecture: arm64 + name: ap-southeast-1 + - ami: ami-07b687e184ca202ca + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0abf1d918c86a042f + architecture: arm64 + name: us-east-1 + - ami: ami-0195b5ea5c4a00d98 + architecture: arm64 + name: us-east-2 + - ami: ami-0ea778307edd05327 + architecture: arm64 + name: us-west-1 + - ami: ami-051f45c9476c3e1ba + architecture: arm64 + name: us-west-2 + - ami: ami-026106028b341418e + architecture: arm64 + name: eu-central-1 + - ami: ami-038c42ee307b92895 + architecture: arm64 + name: cn-north-1 + - ami: ami-0152f541c858d28c7 + architecture: arm64 + name: cn-northwest-1 + - ami: ami-02df3b441309a845b + architecture: amd64 + name: me-central-1 + - ami: ami-005f5ca86fc0ba494 + architecture: arm64 + name: me-central-1 + version: 1443.3.0 + - regions: + - ami: ami-02cc57146b0ab3d7c + architecture: amd64 + name: ap-south-1 + - ami: ami-06318c9d432a6fc15 + architecture: amd64 + name: eu-north-1 + - ami: ami-0ef69e0d5c02689e0 + architecture: amd64 + name: eu-west-3 + - ami: ami-059a66e9c1e57827b + architecture: amd64 + name: eu-west-2 + - ami: ami-06cea2f563b229b90 + architecture: amd64 + name: eu-west-1 + - ami: ami-0ff751899d88355c5 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-06ec722fc110bf7c4 + architecture: amd64 + name: ap-northeast-2 + - ami: ami-01f1d1ba4673e5890 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-0d7451293e8ac4ed2 + architecture: amd64 + name: me-central-1 + - ami: ami-0a24448f3a12ec356 + architecture: amd64 + name: ca-central-1 + - ami: ami-0dffb49f20fc15e0b + architecture: amd64 + name: sa-east-1 + - ami: ami-02e9e8534f7ec2f85 + architecture: amd64 + name: ap-southeast-1 + - ami: ami-0e8990ccc4402450b + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0b0819201e8d000c9 + architecture: amd64 + name: us-east-1 + - ami: ami-023e774a5aa81534e + architecture: amd64 + name: us-east-2 + - ami: ami-093a465791a6b677b + architecture: amd64 + name: us-west-1 + - ami: ami-0145e504d99ff5f4b + architecture: amd64 + name: us-west-2 + - ami: ami-0b9fd1a36a2ac77d0 + architecture: amd64 + name: eu-central-1 + - ami: ami-02ebcc670aac0cafd + architecture: arm64 + name: ap-south-1 + - ami: ami-07d4262ff4a969b3a + architecture: arm64 + name: eu-north-1 + - ami: ami-06f295c2be431ab68 + architecture: arm64 + name: eu-west-3 + - ami: ami-0eb6c81f3d77e94b4 + architecture: arm64 + name: eu-west-2 + - ami: ami-0c9acf25672f82692 + architecture: arm64 + name: eu-west-1 + - ami: ami-0f23608b42552c0c8 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-01d810aa8c96eb897 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-04a9e590fbb3805ce + architecture: arm64 + name: ap-northeast-1 + - ami: ami-08ff5012199163963 + architecture: arm64 + name: me-central-1 + - ami: ami-0f9985ac5dd77a4fe + architecture: arm64 + name: ca-central-1 + - ami: ami-0e2950550140aff8c + architecture: arm64 + name: sa-east-1 + - ami: ami-0b2fcb00e8f2a5db8 + architecture: arm64 + name: ap-southeast-1 + - ami: ami-01b4192fa442ec939 + architecture: arm64 + name: ap-southeast-2 + - ami: ami-017f262d92b0b53c5 + architecture: arm64 + name: us-east-1 + - ami: ami-04d8fa1ae38e6c58f + architecture: arm64 + name: us-east-2 + - ami: ami-015df355345269cd5 + architecture: arm64 + name: us-west-1 + - ami: ami-019ee3b862cdf1726 + architecture: arm64 + name: us-west-2 + - ami: ami-0a80e036a0a092839 + architecture: arm64 + name: eu-central-1 + version: 1312.7.0 + - regions: + - ami: ami-0d84910700d522bc7 + architecture: amd64 + name: ap-south-1 + - ami: ami-049c6be95d0227bac + architecture: amd64 + name: eu-north-1 + - ami: ami-08a4ff8ca6c4f238c + architecture: amd64 + name: eu-west-3 + - ami: ami-01efee03acaff0207 + architecture: amd64 + name: eu-west-2 + - ami: ami-087f241336af13827 + architecture: amd64 + name: eu-west-1 + - ami: ami-00d5fbac9d75bb624 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-06d0a1e9eda88623f + architecture: amd64 + name: ap-northeast-2 + - ami: ami-01bff7d1ab36db25d + architecture: amd64 + name: ap-northeast-1 + - ami: ami-0550e982d7773af02 + architecture: amd64 + name: me-central-1 + - ami: ami-0d2d2f2de28888dc9 + architecture: amd64 + name: ca-central-1 + - ami: ami-014e1e826d4f5af62 + architecture: amd64 + name: sa-east-1 + - ami: ami-0861d6fea89fb011d + architecture: amd64 + name: ap-southeast-1 + - ami: ami-0c1ebd77a6efce658 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0253797156ecb2661 + architecture: amd64 + name: us-east-1 + - ami: ami-048a859f72bd09b2f + architecture: amd64 + name: us-east-2 + - ami: ami-0e98294d69a61c348 + architecture: amd64 + name: us-west-1 + - ami: ami-06ccbb9706cc24903 + architecture: amd64 + name: us-west-2 + - ami: ami-0e0b05c9fc5c02b34 + architecture: amd64 + name: eu-central-1 + - ami: ami-0bbc5c7c1b84353db + architecture: amd64 + name: cn-north-1 + - ami: ami-037f85f63ad4830b9 + architecture: amd64 + name: cn-northwest-1 + - ami: ami-0a95e32c115de3ab9 + architecture: arm64 + name: ap-south-1 + - ami: ami-094ce78b519e20127 + architecture: arm64 + name: eu-north-1 + - ami: ami-008ea4fbd1e43936b + architecture: arm64 + name: eu-west-3 + - ami: ami-07377b52da14c4f59 + architecture: arm64 + name: eu-west-2 + - ami: ami-07d6c578ca28ba554 + architecture: arm64 + name: eu-west-1 + - ami: ami-09e6c9e8356cab511 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-04f1ade99182fd834 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-0993b5fd0834d7b35 + architecture: arm64 + name: ap-northeast-1 + - ami: ami-0f0945fa6b3839d5d + architecture: arm64 + name: me-central-1 + - ami: ami-08dd8907d59f63a09 + architecture: arm64 + name: ca-central-1 + - ami: ami-0adf27db23e42dbb1 + architecture: arm64 + name: sa-east-1 + - ami: ami-04db65c208a48007a + architecture: arm64 + name: ap-southeast-1 + - ami: ami-09943a12ecd221cf0 + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0204a8d494478f1ac + architecture: arm64 + name: us-east-1 + - ami: ami-024399431edaa0d33 + architecture: arm64 + name: us-east-2 + - ami: ami-0c94d021f4a1b43c7 + architecture: arm64 + name: us-west-1 + - ami: ami-0d4f0c2301aec8dbd + architecture: arm64 + name: us-west-2 + - ami: ami-0a891239d1ff0a785 + architecture: arm64 + name: eu-central-1 + - ami: ami-0aa518865b62907f2 + architecture: arm64 + name: cn-north-1 + - ami: ami-033c7343a5245a5d2 + architecture: arm64 + name: cn-northwest-1 + version: 1312.5.0 + - regions: + - ami: ami-0e05abbcaa95e3a05 + architecture: amd64 + name: ap-south-1 + - ami: ami-0f1c6f84ab5897f1e + architecture: amd64 + name: eu-north-1 + - ami: ami-09b6fd4f7a915ba85 + architecture: amd64 + name: eu-west-3 + - ami: ami-0c5702bea9fe78a5f + architecture: amd64 + name: eu-west-2 + - ami: ami-07ba253a606c58177 + architecture: amd64 + name: eu-west-1 + - ami: ami-05c9a95af4d3301c7 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-067ed8ddfd2b8e1af + architecture: amd64 + name: ap-northeast-2 + - ami: ami-07238c3286226c757 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-0aa25b4c3c96c02a3 + architecture: amd64 + name: ca-central-1 + - ami: ami-0155a3af49403d419 + architecture: amd64 + name: sa-east-1 + - ami: ami-0f8955ae6213edd49 + architecture: amd64 + name: ap-southeast-1 + - ami: ami-053d2a861ac19b3a7 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0381a6d4bd01c1fad + architecture: amd64 + name: us-east-1 + - ami: ami-0925827603090625c + architecture: amd64 + name: us-east-2 + - ami: ami-04cf65320d07748e9 + architecture: amd64 + name: us-west-1 + - ami: ami-0de3fa4c932bf743d + architecture: amd64 + name: us-west-2 + - ami: ami-0637a1b3efeb57ff6 + architecture: amd64 + name: eu-central-1 + - ami: ami-0a1c9ef53179d0a84 + architecture: amd64 + name: cn-north-1 + - ami: ami-0e7e094d9384bd812 + architecture: amd64 + name: cn-northwest-1 + - ami: ami-03369d00d235ca23e + architecture: arm64 + name: ap-south-1 + - ami: ami-0137771a1477e8a0a + architecture: arm64 + name: eu-north-1 + - ami: ami-0c26712af20c22eec + architecture: arm64 + name: eu-west-3 + - ami: ami-008d167c302fb609e + architecture: arm64 + name: eu-west-2 + - ami: ami-0c3b30cc11e70193f + architecture: arm64 + name: eu-west-1 + - ami: ami-05be59a425d030fbe + architecture: arm64 + name: ap-northeast-3 + - ami: ami-072c4a0b991f6acb9 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-00c4f0350458cf98d + architecture: arm64 + name: ap-northeast-1 + - ami: ami-0d1ba989a8ebf76ba + architecture: arm64 + name: ca-central-1 + - ami: ami-0258cb1ad7e6e722d + architecture: arm64 + name: sa-east-1 + - ami: ami-0910ebf9b6ced167b + architecture: arm64 + name: ap-southeast-1 + - ami: ami-05d22ae9704e9712b + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0e4d3c6fc94645f4e + architecture: arm64 + name: us-east-1 + - ami: ami-06176d5c81fb161bd + architecture: arm64 + name: us-east-2 + - ami: ami-002831b77c0e83478 + architecture: arm64 + name: us-west-1 + - ami: ami-0ac6802b91df1f1fb + architecture: arm64 + name: us-west-2 + - ami: ami-095da14b5464da4ed + architecture: arm64 + name: eu-central-1 + - ami: ami-0fc5d77597c71851f + architecture: arm64 + name: cn-north-1 + - ami: ami-0bb08dfa668597c48 + architecture: arm64 + name: cn-northwest-1 + - ami: ami-016fd4f837bb122e9 + architecture: amd64 + name: me-central-1 + - ami: ami-07ca2c448dc4350a3 + architecture: arm64 + name: me-central-1 + version: 1312.3.0 + - regions: + - ami: ami-025103f156beec76b + architecture: amd64 + name: ap-south-1 + - ami: ami-061b7d013062fd177 + architecture: amd64 + name: eu-north-1 + - ami: ami-08f5df7b39379d802 + architecture: amd64 + name: eu-west-3 + - ami: ami-0cec10fe6ae9d0007 + architecture: amd64 + name: eu-west-2 + - ami: ami-048f4cc0927df1708 + architecture: amd64 + name: eu-west-1 + - ami: ami-0eccc9dea316baa80 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-09ec42f658f8c39ff + architecture: amd64 + name: ap-northeast-2 + - ami: ami-042075c59a2c62639 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-0a3cfc30ec5559f4a + architecture: amd64 + name: ca-central-1 + - ami: ami-058dc21fa9ffd3d7c + architecture: amd64 + name: sa-east-1 + - ami: ami-08e79fefc23cf203f + architecture: amd64 + name: ap-southeast-1 + - ami: ami-0997e2e7431b5934a + architecture: amd64 + name: ap-southeast-2 + - ami: ami-08b1dbb9114b59422 + architecture: amd64 + name: us-east-1 + - ami: ami-0542e23f75934885c + architecture: amd64 + name: us-east-2 + - ami: ami-0daf131a6b04ff197 + architecture: amd64 + name: us-west-1 + - ami: ami-01977b0a8ae01a4fd + architecture: amd64 + name: us-west-2 + - ami: ami-08aacf410be7dbb21 + architecture: amd64 + name: eu-central-1 + - ami: ami-071c096e6d1626959 + architecture: amd64 + name: cn-north-1 + - ami: ami-0e06bbd82ed97992d + architecture: amd64 + name: cn-northwest-1 + - ami: ami-0066c24e272fad32f + architecture: arm64 + name: ap-south-1 + - ami: ami-0afd5bf280165e4f2 + architecture: arm64 + name: eu-north-1 + - ami: ami-08167c10ca61cf13a + architecture: arm64 + name: eu-west-3 + - ami: ami-0c85b124a386b47f9 + architecture: arm64 + name: eu-west-2 + - ami: ami-0b4ec856dc3a9d9c5 + architecture: arm64 + name: eu-west-1 + - ami: ami-0fabf5519b372aab1 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-0c909c7eb6271b345 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-009331ec14c04cb8a + architecture: arm64 + name: ap-northeast-1 + - ami: ami-01682c08631cdfa46 + architecture: arm64 + name: ca-central-1 + - ami: ami-055675edbeab9f57b + architecture: arm64 + name: sa-east-1 + - ami: ami-091be22728a6cb95f + architecture: arm64 + name: ap-southeast-1 + - ami: ami-06c7585d0c180a182 + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0da999ce923d66245 + architecture: arm64 + name: us-east-1 + - ami: ami-0f29303dd11e989c2 + architecture: arm64 + name: us-east-2 + - ami: ami-00d813293fa968226 + architecture: arm64 + name: us-west-1 + - ami: ami-00f9d5bed25a9c311 + architecture: arm64 + name: us-west-2 + - ami: ami-08f3f6465922ee8fb + architecture: arm64 + name: eu-central-1 + - ami: ami-0ae50890db8713c9e + architecture: arm64 + name: cn-north-1 + - ami: ami-0472645cdb98e490e + architecture: arm64 + name: cn-northwest-1 + - ami: ami-0a5ba831c184a715a + architecture: amd64 + name: me-central-1 + - ami: ami-0cf7bf9c42dcf9702 + architecture: arm64 + name: me-central-1 + version: 1312.2.0 + - regions: + - ami: ami-0682714281cb8309d + architecture: arm64 + name: ap-south-1 + - ami: ami-0ef5f8c976d341c4b + architecture: arm64 + name: eu-north-1 + - ami: ami-0753378c904aa0ce4 + architecture: arm64 + name: eu-west-3 + - ami: ami-0d4d7a409a49c46ff + architecture: arm64 + name: eu-west-2 + - ami: ami-03c563bc1a614b79b + architecture: arm64 + name: eu-west-1 + - ami: ami-0ae1c26351ddb4317 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-0c4379ba6b639e023 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-043e5f70df3d42f3c + architecture: arm64 + name: ap-northeast-1 + - ami: ami-0163ea3c2210f3d5a + architecture: arm64 + name: ca-central-1 + - ami: ami-034c7504493a8b7b0 + architecture: arm64 + name: sa-east-1 + - ami: ami-08baf11fcbc6ff91c + architecture: arm64 + name: ap-southeast-1 + - ami: ami-083af2626d25824c4 + architecture: arm64 + name: ap-southeast-2 + - ami: ami-0b967cafdb6a2b256 + architecture: arm64 + name: us-east-1 + - ami: ami-05e2b2ba5fd36a3cc + architecture: arm64 + name: us-east-2 + - ami: ami-046b4db1f2161e17d + architecture: arm64 + name: us-west-1 + - ami: ami-05b16e3d5078730f3 + architecture: arm64 + name: us-west-2 + - ami: ami-0bf9802b317c61df8 + architecture: arm64 + name: eu-central-1 + - ami: ami-079a00239a7407910 + architecture: arm64 + name: cn-north-1 + - ami: ami-0de517225a37117bc + architecture: arm64 + name: cn-northwest-1 + - ami: ami-0f8a7501ddd3d8a90 + architecture: amd64 + name: ap-south-1 + - ami: ami-0c3c783a06fcfab3c + architecture: amd64 + name: eu-north-1 + - ami: ami-0aa897feec4b27650 + architecture: amd64 + name: eu-west-3 + - ami: ami-02de08fca6414cc6b + architecture: amd64 + name: eu-west-2 + - ami: ami-0bbc44cdb29600162 + architecture: amd64 + name: eu-west-1 + - ami: ami-0bfc623e3a3fe9cd9 + architecture: amd64 + name: ap-northeast-3 + - ami: ami-099d239279b6a04b0 + architecture: amd64 + name: ap-northeast-2 + - ami: ami-0436fbde664e27a36 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-043feefa0a543cf68 + architecture: amd64 + name: ca-central-1 + - ami: ami-002dd8d9f55a83721 + architecture: amd64 + name: sa-east-1 + - ami: ami-07ecc722894229732 + architecture: amd64 + name: ap-southeast-1 + - ami: ami-044e3d7660a60b59e + architecture: amd64 + name: ap-southeast-2 + - ami: ami-072109c7c561a30c3 + architecture: amd64 + name: us-east-1 + - ami: ami-0430ed64106f4f39d + architecture: amd64 + name: us-east-2 + - ami: ami-019592438972f9902 + architecture: amd64 + name: us-west-1 + - ami: ami-012dfeb9cd27c7a2d + architecture: amd64 + name: us-west-2 + - ami: ami-0cf51dc2ec24ae503 + architecture: amd64 + name: eu-central-1 + - ami: ami-0e49e6cce2e9b35a0 + architecture: amd64 + name: cn-north-1 + - ami: ami-01d105423ddf6b06f + architecture: amd64 + name: cn-northwest-1 + - ami: ami-0bae3d4bdb25574fc + architecture: arm64 + name: me-central-1 + - ami: ami-02a6e41a605d4226b + architecture: amd64 + name: me-central-1 + version: 1312.1.0 + - regions: + - ami: ami-02d89e0b1d6af53a3 + architecture: amd64 + name: ap-northeast-1 + - ami: ami-0066c855c1200e176 + architecture: amd64 + name: ap-northeast-2 + - ami: ami-0bac394fa5b39096d + architecture: amd64 + name: ap-northeast-3 + - ami: ami-04b53592717be484b + architecture: amd64 + name: ap-south-1 + - ami: ami-0a58d99db803d002d + architecture: amd64 + name: ap-southeast-1 + - ami: ami-0a0bc106816633fd9 + architecture: amd64 + name: ap-southeast-2 + - ami: ami-0b9961130d6b06134 + architecture: amd64 + name: ca-central-1 + - ami: ami-0e00027ca263f14ab + architecture: amd64 + name: eu-central-1 + - ami: ami-0f7fdf06c850344c7 + architecture: amd64 + name: eu-north-1 + - ami: ami-0957e660d70702caa + architecture: amd64 + name: eu-west-1 + - ami: ami-091958cfdab022e65 + architecture: amd64 + name: eu-west-2 + - ami: ami-0818aa990e3c1779d + architecture: amd64 + name: eu-west-3 + - ami: ami-0e924aeb9e47c00c0 + architecture: amd64 + name: sa-east-1 + - ami: ami-00af50de5dae1aaad + architecture: amd64 + name: us-east-1 + - ami: ami-065c94da42c497318 + architecture: amd64 + name: us-east-2 + - ami: ami-014fbc66d37cae795 + architecture: amd64 + name: us-west-1 + - ami: ami-01c15c5b57e4dc329 + architecture: amd64 + name: us-west-2 + - ami: ami-03d83aeece1aaf1f1 + architecture: amd64 + name: cn-north-1 + - ami: ami-01bbd7c985c0b23b6 + architecture: amd64 + name: cn-northwest-1 + - ami: ami-036314567405bfe5b + architecture: arm64 + name: ap-northeast-1 + - ami: ami-016919c0f1765e930 + architecture: arm64 + name: ap-northeast-2 + - ami: ami-00a4fae3a08ff6ec8 + architecture: arm64 + name: ap-northeast-3 + - ami: ami-02be479874c12f432 + architecture: arm64 + name: ap-south-1 + - ami: ami-00fc604c3a37f282c + architecture: arm64 + name: ap-southeast-2 + - ami: ami-04c42ff35e63dec56 + architecture: arm64 + name: ca-central-1 + - ami: ami-0bbbd67f038acc147 + architecture: arm64 + name: eu-central-1 + - ami: ami-0166037fcc6938a8d + architecture: arm64 + name: eu-north-1 + - ami: ami-0266bc70a2d973e13 + architecture: arm64 + name: eu-west-1 + - ami: ami-05d38a9a993a4d2b6 + architecture: arm64 + name: eu-west-2 + - ami: ami-0ac5c4703e9ec6e0f + architecture: arm64 + name: eu-west-3 + - ami: ami-010ff8fcd822288f9 + architecture: arm64 + name: sa-east-1 + - ami: ami-0c3bcc42feecbddd9 + architecture: arm64 + name: us-east-1 + - ami: ami-02c1508fa2e72493c + architecture: arm64 + name: us-east-2 + - ami: ami-03dbe8ed6585ca135 + architecture: arm64 + name: us-west-1 + - ami: ami-0a55a0a1b8c557e5b + architecture: arm64 + name: us-west-2 + - ami: ami-07efac1af90e595c9 + architecture: arm64 + name: cn-north-1 + - ami: ami-00ccc16c810fc80bb + architecture: arm64 + name: cn-northwest-1 + - ami: ami-0dffc09650d530172 + architecture: amd64 + name: me-central-1 + - ami: ami-06b53e65fe7cbd10c + architecture: arm64 + name: me-central-1 + version: 934.11.0 + regions: + - name: ap-northeast-1 + zones: + - name: ap-northeast-1a + - name: ap-northeast-1c + - name: ap-northeast-1d + - name: ap-northeast-2 + zones: + - name: ap-northeast-2a + - name: ap-northeast-2b + - name: ap-northeast-2c + - name: ap-northeast-2d + - name: ap-northeast-3 + zones: + - name: ap-northeast-3a + - name: ap-northeast-3b + - name: ap-northeast-3c + - name: ap-south-1 + zones: + - name: ap-south-1a + - name: ap-south-1b + - name: ap-south-1c + - name: ap-southeast-1 + zones: + - name: ap-southeast-1a + - name: ap-southeast-1b + - name: ap-southeast-1c + - name: ap-southeast-2 + zones: + - name: ap-southeast-2a + - name: ap-southeast-2b + - name: ap-southeast-2c + - name: ca-central-1 + zones: + - name: ca-central-1a + - name: ca-central-1b + - name: ca-central-1d + - name: eu-central-1 + zones: + - name: eu-central-1a + - name: eu-central-1b + - name: eu-central-1c + - name: eu-north-1 + zones: + - name: eu-north-1a + - name: eu-north-1b + - name: eu-north-1c + - name: eu-west-1 + zones: + - name: eu-west-1a + - name: eu-west-1b + - name: eu-west-1c + - name: eu-west-2 + zones: + - name: eu-west-2a + - name: eu-west-2b + - name: eu-west-2c + - name: eu-west-3 + zones: + - name: eu-west-3a + - name: eu-west-3b + - name: eu-west-3c + - name: me-central-1 + zones: + - name: me-central-1a + - name: me-central-1b + - name: me-central-1c + - name: sa-east-1 + zones: + - name: sa-east-1a + - name: sa-east-1b + - name: sa-east-1c + - name: us-east-1 + zones: + - name: us-east-1a + - name: us-east-1b + - name: us-east-1c + - name: us-east-1d + - name: us-east-1e + - name: us-east-1f + - name: us-east-2 + zones: + - name: us-east-2a + - name: us-east-2b + - name: us-east-2c + - name: us-west-1 + zones: + - name: us-west-1a + - name: us-west-1b + - name: us-west-1c + - name: us-west-2 + zones: + - name: us-west-2a + - name: us-west-2b + - name: us-west-2c + - name: us-west-2d + type: aws + volumeTypes: + - class: standard + name: gp3 + usable: true + - class: standard + name: gp2 + usable: true + - class: standard + minSize: 4Gi + name: io1 + usable: true diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/cloudprofile-gcp.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/cloudprofile-gcp.yaml new file mode 100644 index 0000000..c26881f --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/cloudprofile-gcp.yaml @@ -0,0 +1,2089 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: CloudProfile +metadata: + name: gcp +spec: + kubernetes: + versions: + - classification: preview + version: 1.29.4 + - classification: supported + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.3 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.2 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.1 + - classification: preview + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.9 + - classification: supported + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.8 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.7 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.6 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.4 + - classification: preview + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.13 + - classification: supported + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.12 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.11 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.10 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.9 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.8 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.7 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.6 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.5 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.15 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.14 + - classification: deprecated + expirationDate: "2024-06-30T23:59:59Z" + version: 1.26.13 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.11 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.10 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.26.9 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.8 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.7 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.6 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.5 + - classification: deprecated + expirationDate: "2024-07-31T23:59:59Z" + version: 1.25.16 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.25.15 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.25.14 + machineImages: + - name: gardenlinux + updateStrategy: minor + versions: + - architectures: + - amd64 + - arm64 + classification: preview + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.5.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-06-23T23:59:59Z" + version: 1443.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.0.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1312.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 1312.0.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-07-15T23:59:59Z" + version: 934.11.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-05-15T23:59:59Z" + version: 934.10.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 934.9.0 + machineTypes: + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 7Gi + name: n1-standard-2 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 85Gi + name: a2-highgpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 170Gi + name: a2-highgpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 340Gi + name: a2-highgpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 680Gi + name: a2-highgpu-8g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-megagpu-16g + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 170Gi + name: a2-ultragpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 340Gi + name: a2-ultragpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 680Gi + name: a2-ultragpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-ultragpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-highgpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-megagpu-8g + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2-highcpu-56 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2-standard-16 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c2-standard-30 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2-standard-56 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c2-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2-standard-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 224Gi + name: c2d-highcpu-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c2d-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2d-highcpu-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 896Gi + name: c2d-highmem-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: c2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: c2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 448Gi + name: c2d-highmem-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 448Gi + name: c2d-standard-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: c2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: c2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2d-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2d-standard-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 352Gi + name: c3-highcpu-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 44Gi + name: c3-highcpu-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3-highcpu-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 88Gi + name: c3-highcpu-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3-highcpu-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 176Gi + name: c3-highcpu-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: c3-highmem-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 176Gi + name: c3-highmem-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3-highmem-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 352Gi + name: c3-highmem-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3-highmem-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: c3-highmem-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176-lssd + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4-lssd + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c3d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 354Gi + name: c3d-highcpu-180 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 59Gi + name: c3d-highcpu-30 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 708Gi + name: c3d-highcpu-360 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 118Gi + name: c3d-highcpu-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 177Gi + name: c3d-highcpu-90 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3d-highmem-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3d-standard-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90-lssd + usable: true + - architecture: amd64 + cpu: "240" + gpu: "0" + memory: 407Gi + name: ct4p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5l-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5l-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5l-hightpu-8t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5lp-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5lp-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5lp-hightpu-8t + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 448Gi + name: ct5p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: e2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: e2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: e2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: e2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: e2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: e2-highmem-2 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: e2-highmem-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: e2-highmem-8 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: e2-medium + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: e2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: e2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: e2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: e2-standard-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: e2-standard-8 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 48Gi + name: g2-standard-12 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: g2-standard-16 + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 96Gi + name: g2-standard-24 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: g2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: g2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: g2-standard-48 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: g2-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: g2-standard-96 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: h3-standard-88 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1433Gi + name: m1-megamem-96 + usable: true + - architecture: amd64 + cpu: "160" + gpu: "0" + memory: 3844Gi + name: m1-ultramem-160 + usable: true + - architecture: amd64 + cpu: "40" + gpu: "0" + memory: 961Gi + name: m1-ultramem-40 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 1922Gi + name: m1-ultramem-80 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 8832Gi + name: m2-hypermem-416 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 5888Gi + name: m2-megamem-416 + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 5888Gi + name: m2-ultramem-208 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 11776Gi + name: m2-ultramem-416 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1250Gi + name: m2-ultramem2x-96 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 600Gi + name: m2-ultramemx-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1952Gi + name: m3-megamem-128 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 976Gi + name: m3-megamem-64 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 3904Gi + name: m3-ultramem-128 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 976Gi + name: m3-ultramem-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 1952Gi + name: m3-ultramem-64 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 14Gi + name: n1-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 28Gi + name: n1-highcpu-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 57Gi + name: n1-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 7Gi + name: n1-highcpu-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 86Gi + name: n1-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 104Gi + name: n1-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 13Gi + name: n1-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 208Gi + name: n1-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 26Gi + name: n1-highmem-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 416Gi + name: n1-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 52Gi + name: n1-highmem-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 624Gi + name: n1-highmem-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 60Gi + name: n1-standard-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 120Gi + name: n1-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 15Gi + name: n1-standard-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 240Gi + name: n1-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 30Gi + name: n1-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 360Gi + name: n1-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2-highcpu-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 864Gi + name: n2-highmem-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2-standard-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 128Gi + name: n2d-highcpu-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 224Gi + name: n2d-highcpu-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2d-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2d-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2d-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2d-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2d-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2d-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2d-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2d-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2d-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2d-standard-2 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 896Gi + name: n2d-standard-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2d-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2d-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2d-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2d-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2d-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: n4-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: n4-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: n4-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: n4-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: n4-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: n4-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: n4-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 160Gi + name: n4-highcpu-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n4-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n4-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n4-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n4-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n4-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n4-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n4-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n4-highmem-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n4-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n4-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n4-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n4-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n4-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n4-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n4-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n4-standard-80 + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2a-standard-16 + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2a-standard-2 + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2a-standard-32 + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2a-standard-4 + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2a-standard-48 + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2a-standard-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2d-standard-48 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: t2d-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: z3-highmem-176 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: z3-highmem-88 + usable: true + providerConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: CloudProfileConfig + machineImages: + - name: gardenlinux + versions: + - architecture: amd64 + image: images/gardenlinux-amd64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-3-c261f887 + version: 1443.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-3-c261f887 + version: 1443.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-2-77c8471d + version: 1312.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-2-77c8471d + version: 1312.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-11-75132b8 + version: 934.11.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-10-f057c9b + version: 934.10.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-9-54c63e5 + version: 934.9.0 + regions: + - name: africa-south1 + zones: + - name: africa-south1-a + - name: africa-south1-b + - name: africa-south1-c + - name: asia-east1 + zones: + - name: asia-east1-a + - name: asia-east1-b + - name: asia-east1-c + - name: asia-east2 + zones: + - name: asia-east2-a + - name: asia-east2-b + - name: asia-east2-c + - name: asia-northeast1 + zones: + - name: asia-northeast1-a + - name: asia-northeast1-b + - name: asia-northeast1-c + - name: asia-northeast2 + zones: + - name: asia-northeast2-a + - name: asia-northeast2-b + - name: asia-northeast2-c + - name: asia-northeast3 + zones: + - name: asia-northeast3-a + - name: asia-northeast3-b + - name: asia-northeast3-c + - name: asia-south1 + zones: + - name: asia-south1-a + - name: asia-south1-b + - name: asia-south1-c + - name: asia-south2 + zones: + - name: asia-south2-a + - name: asia-south2-b + - name: asia-south2-c + - name: asia-southeast1 + zones: + - name: asia-southeast1-a + - name: asia-southeast1-b + - name: asia-southeast1-c + - name: asia-southeast2 + zones: + - name: asia-southeast2-a + - name: asia-southeast2-b + - name: asia-southeast2-c + - name: australia-southeast1 + zones: + - name: australia-southeast1-a + - name: australia-southeast1-b + - name: australia-southeast1-c + - name: australia-southeast2 + zones: + - name: australia-southeast2-a + - name: australia-southeast2-b + - name: australia-southeast2-c + - name: europe-central2 + zones: + - name: europe-central2-a + - name: europe-central2-b + - name: europe-central2-c + - name: europe-north1 + zones: + - name: europe-north1-a + - name: europe-north1-b + - name: europe-north1-c + - name: europe-southwest1 + zones: + - name: europe-southwest1-a + - name: europe-southwest1-b + - name: europe-southwest1-c + - name: europe-west1 + zones: + - name: europe-west1-b + - name: europe-west1-c + - name: europe-west1-d + - name: europe-west10 + zones: + - name: europe-west10-a + - name: europe-west10-b + - name: europe-west10-c + - name: europe-west12 + zones: + - name: europe-west12-a + - name: europe-west12-b + - name: europe-west12-c + - name: europe-west2 + zones: + - name: europe-west2-a + - name: europe-west2-b + - name: europe-west2-c + - name: europe-west3 + zones: + - name: europe-west3-a + - name: europe-west3-b + - name: europe-west3-c + - name: europe-west4 + zones: + - name: europe-west4-a + - name: europe-west4-b + - name: europe-west4-c + - name: europe-west5 + zones: + - name: europe-west5-a + - name: europe-west5-b + - name: europe-west5-c + - name: europe-west6 + zones: + - name: europe-west6-a + - name: europe-west6-b + - name: europe-west6-c + - name: europe-west8 + zones: + - name: europe-west8-a + - name: europe-west8-b + - name: europe-west8-c + - name: europe-west9 + zones: + - name: europe-west9-a + - name: europe-west9-b + - name: europe-west9-c + - name: me-central1 + zones: + - name: me-central1-a + - name: me-central1-b + - name: me-central1-c + - name: me-central2 + zones: + - name: me-central2-a + - name: me-central2-b + - name: me-central2-c + - name: me-west1 + zones: + - name: me-west1-a + - name: me-west1-b + - name: me-west1-c + - name: northamerica-northeast1 + zones: + - name: northamerica-northeast1-a + - name: northamerica-northeast1-b + - name: northamerica-northeast1-c + - name: northamerica-northeast2 + zones: + - name: northamerica-northeast2-a + - name: northamerica-northeast2-b + - name: northamerica-northeast2-c + - name: southamerica-east1 + zones: + - name: southamerica-east1-a + - name: southamerica-east1-b + - name: southamerica-east1-c + - name: southamerica-west1 + zones: + - name: southamerica-west1-a + - name: southamerica-west1-b + - name: southamerica-west1-c + - name: us-central1 + zones: + - name: us-central1-a + - name: us-central1-b + - name: us-central1-c + - name: us-central1-f + - name: us-east1 + zones: + - name: us-east1-b + - name: us-east1-c + - name: us-east1-d + - name: us-east4 + zones: + - name: us-east4-a + - name: us-east4-b + - name: us-east4-c + - name: us-east5 + zones: + - name: us-east5-a + - name: us-east5-b + - name: us-east5-c + - name: us-south1 + zones: + - name: us-south1-a + - name: us-south1-b + - name: us-south1-c + - name: us-west1 + zones: + - name: us-west1-a + - name: us-west1-b + - name: us-west1-c + - name: us-west2 + zones: + - name: us-west2-a + - name: us-west2-b + - name: us-west2-c + - name: us-west3 + zones: + - name: us-west3-a + - name: us-west3-b + - name: us-west3-c + - name: us-west4 + zones: + - name: us-west4-a + - name: us-west4-b + - name: us-west4-c + type: gcp + volumeTypes: + - class: premium + minSize: 20Gi + name: pd-balanced + usable: true + - class: standard + minSize: 20Gi + name: pd-standard + usable: true + - class: premium + minSize: 20Gi + name: pd-ssd + usable: true diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/ns-garden-test.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/ns-garden-test.yaml new file mode 100644 index 0000000..e48bb72 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/ns-garden-test.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: garden-test diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/ns-garden-test2.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/ns-garden-test2.yaml new file mode 100644 index 0000000..daf8361 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/ns-garden-test2.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: garden-test2 diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/project-test.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/project-test.yaml new file mode 100644 index 0000000..6a319b0 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/project-test.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: test +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-test + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/project-test2.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/project-test2.yaml new file mode 100644 index 0000000..4dbb636 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/project-test2.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: test2 +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-test2 + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-modified.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-modified.yaml new file mode 100644 index 0000000..b0321e4 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-modified.yaml @@ -0,0 +1,135 @@ +kind: Shoot +apiVersion: core.gardener.cloud/v1beta1 +metadata: + name: modified + namespace: garden-test + labels: + openmcp.cloud/mcp-name: modified + openmcp.cloud/mcp-namespace: modified + provider.extensions.gardener.cloud/gcp: 'true' + shoot.gardener.cloud/status: healthy + annotations: + shoot.gardener.cloud/cleanup-extended-apis-finalize-grace-period-seconds: '30' +spec: + addons: + kubernetesDashboard: + enabled: false + authenticationMode: token + cloudProfileName: modified + dns: + domain: modified.example.org + extensions: + - type: shoot-oidc-service + - type: shoot-dns-service + providerConfig: + apiVersion: service.dns.extensions.gardener.cloud/v1alpha1 + kind: DNSConfig + syncProvidersFromShootSpecDNS: true + hibernation: + enabled: false + kubernetes: + kubeAPIServer: + runtimeConfig: + apps/v1: true + batch/v1: true + requests: + maxNonMutatingInflight: 400 + maxMutatingInflight: 200 + enableAnonymousAuthentication: false + eventTTL: 1h0m0s + logging: + verbosity: 2 + defaultNotReadyTolerationSeconds: 300 + defaultUnreachableTolerationSeconds: 300 + kubeControllerManager: + nodeCIDRMaskSize: 24 + nodeMonitorGracePeriod: 40s + kubeScheduler: + profile: balanced + kubeProxy: + mode: IPTables + enabled: true + kubelet: + failSwapOn: true + kubeReserved: + cpu: 80m + memory: 1Gi + pid: 20k + imageGCHighThresholdPercent: 50 + imageGCLowThresholdPercent: 40 + serializeImagePulls: true + version: 1.29.3 + verticalPodAutoscaler: + enabled: true + evictAfterOOMThreshold: 10m0s + evictionRateBurst: 1 + evictionRateLimit: -1 + evictionTolerance: 0.5 + recommendationMarginFraction: 0.15 + updaterInterval: 1m0s + recommenderInterval: 1m0s + targetCPUPercentile: 0.9 + enableStaticTokenKubeconfig: false + networking: + type: modified + providerConfig: + overlay: + enabled: false + pods: 100.64.0.0/12 + nodes: 10.180.0.0/16 + services: 100.104.0.0/13 + ipFamilies: + - IPv4 + maintenance: + autoUpdate: + kubernetesVersion: true + machineImageVersion: true + timeWindow: + begin: 000000+0000 + end: 010000+0000 + provider: + type: modified + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: europe-west1-b + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + workers: + - cri: + name: containerd + name: worker-0 + machine: + type: n1-standard-2 + image: + name: gardenlinux + version: 1312.3.0 + architecture: amd64 + maximum: 2 + minimum: 1 + maxSurge: 1 + maxUnavailable: 0 + volume: + type: pd-balanced + size: 50Gi + zones: + - europe-west1-b + systemComponents: + allow: true + workersSettings: + sshAccess: + enabled: true + purpose: production + region: europe-west1 + secretBindingName: modified + seedName: modified + systemComponents: + coreDNS: + autoscaling: + mode: horizontal + nodeLocalDNS: + enabled: true + schedulerName: default-scheduler diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-test-auditlog.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-test-auditlog.yaml new file mode 100644 index 0000000..69935a8 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-test-auditlog.yaml @@ -0,0 +1,170 @@ +kind: Shoot +apiVersion: core.gardener.cloud/v1beta1 +metadata: + name: test-auditlog + namespace: garden-test + labels: + openmcp.cloud/mcp-name: test-auditlog + openmcp.cloud/mcp-namespace: test + provider.extensions.gardener.cloud/gcp: 'true' + shoot.gardener.cloud/status: healthy + annotations: + shoot.gardener.cloud/cleanup-extended-apis-finalize-grace-period-seconds: '30' +spec: + addons: + kubernetesDashboard: + enabled: false + authenticationMode: token + cloudProfileName: gcp + dns: + domain: test.example.org + extensions: + - type: shoot-oidc-service + - type: shoot-dns-service + providerConfig: + apiVersion: service.dns.extensions.gardener.cloud/v1alpha1 + kind: DNSConfig + syncProvidersFromShootSpecDNS: true + - type: shoot-auditlog-service + providerConfig: + apiVersion: service.auditlog.extensions.gardener.cloud/v1alpha1 + kind: AuditlogConfig + secretReferenceName: autitlog-credentials + serviceURL: https://auditlog.example.com:8081 + tenantID: 83b3b3b3-3b3b-3b3b-3b3b-3b3b3b3b3b3b + type: standard + hibernation: + enabled: false + kubernetes: + kubeAPIServer: + auditConfig: + auditPolicy: + configMapRef: + name: test-auditlog--auditlog-policy + runtimeConfig: + apps/v1: true + batch/v1: true + requests: + maxNonMutatingInflight: 400 + maxMutatingInflight: 200 + enableAnonymousAuthentication: false + eventTTL: 1h0m0s + logging: + verbosity: 2 + defaultNotReadyTolerationSeconds: 300 + defaultUnreachableTolerationSeconds: 300 + kubeControllerManager: + nodeCIDRMaskSize: 24 + nodeMonitorGracePeriod: 40s + kubeScheduler: + profile: balanced + kubeProxy: + mode: IPTables + enabled: true + kubelet: + failSwapOn: true + kubeReserved: + cpu: 80m + memory: 1Gi + pid: 20k + imageGCHighThresholdPercent: 50 + imageGCLowThresholdPercent: 40 + serializeImagePulls: true + version: 1.29.3 + verticalPodAutoscaler: + enabled: true + evictAfterOOMThreshold: 10m0s + evictionRateBurst: 1 + evictionRateLimit: -1 + evictionTolerance: 0.5 + recommendationMarginFraction: 0.15 + updaterInterval: 1m0s + recommenderInterval: 1m0s + targetCPUPercentile: 0.9 + enableStaticTokenKubeconfig: false + maintenance: + autoUpdate: + kubernetesVersion: true + machineImageVersion: true + timeWindow: + begin: 000000+0000 + end: 010000+0000 + provider: + type: gcp + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: europe-west1-b + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + purpose: production + region: europe-west1 + secretBindingName: laasds + systemComponents: + coreDNS: + autoscaling: + mode: horizontal + nodeLocalDNS: + enabled: true + schedulerName: default-scheduler + resources: + - name: auditlog-credentials + resourceRef: + kind: Secret + name: test-auditlog--auditlog-credentials + apiVersion: v1 +status: + conditions: + - type: APIServerAvailable + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: HealthzRequestSucceeded + message: API server /healthz endpoint responded with success status code. + - type: ControlPlaneHealthy + status: 'True' + lastTransitionTime: '2024-06-11T02:13:15Z' + lastUpdateTime: '2024-06-11T02:13:15Z' + reason: ControlPlaneRunning + message: All control plane components are healthy. + - type: ObservabilityComponentsHealthy + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: ObservabilityComponentsRunning + message: All observability components are healthy. + - type: EveryNodeReady + status: 'True' + lastTransitionTime: '2024-06-11T02:11:15Z' + lastUpdateTime: '2024-06-11T02:11:15Z' + reason: EveryNodeReady + message: All nodes are ready. + - type: SystemComponentsHealthy + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: SystemComponentsRunning + message: All system components are healthy. + constraints: + - type: HibernationPossible + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-07T01:56:39Z' + reason: NoProblematicWebhooks + message: All webhooks are properly configured. + - type: MaintenancePreconditionsSatisfied + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-07T01:56:39Z' + reason: NoProblematicWebhooks + message: All webhooks are properly configured. + hibernated: false + lastOperation: + description: Shoot cluster has been successfully reconciled. + lastUpdateTime: '2024-06-11T00:38:43Z' + progress: 100 + state: Succeeded + type: Reconcile diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-test.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-test.yaml new file mode 100644 index 0000000..efa0b2f --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster/shoot-test.yaml @@ -0,0 +1,160 @@ +kind: Shoot +apiVersion: core.gardener.cloud/v1beta1 +metadata: + name: test + namespace: garden-test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + provider.extensions.gardener.cloud/gcp: 'true' + shoot.gardener.cloud/status: healthy + annotations: + shoot.gardener.cloud/cleanup-extended-apis-finalize-grace-period-seconds: '30' +spec: + addons: + kubernetesDashboard: + enabled: false + authenticationMode: token + cloudProfileName: gcp + dns: + domain: test.example.org + extensions: + - type: shoot-oidc-service + - type: shoot-dns-service + providerConfig: + apiVersion: service.dns.extensions.gardener.cloud/v1alpha1 + kind: DNSConfig + syncProvidersFromShootSpecDNS: true + hibernation: + enabled: false + kubernetes: + kubeAPIServer: + runtimeConfig: + apps/v1: true + batch/v1: true + requests: + maxNonMutatingInflight: 400 + maxMutatingInflight: 200 + enableAnonymousAuthentication: false + eventTTL: 1h0m0s + logging: + verbosity: 2 + defaultNotReadyTolerationSeconds: 300 + defaultUnreachableTolerationSeconds: 300 + kubeControllerManager: + nodeCIDRMaskSize: 24 + nodeMonitorGracePeriod: 40s + kubeScheduler: + profile: balanced + kubeProxy: + mode: IPTables + enabled: true + kubelet: + failSwapOn: true + kubeReserved: + cpu: 80m + memory: 1Gi + pid: 20k + imageGCHighThresholdPercent: 50 + imageGCLowThresholdPercent: 40 + serializeImagePulls: true + version: 1.29.3 + verticalPodAutoscaler: + enabled: true + evictAfterOOMThreshold: 10m0s + evictionRateBurst: 1 + evictionRateLimit: -1 + evictionTolerance: 0.5 + recommendationMarginFraction: 0.15 + updaterInterval: 1m0s + recommenderInterval: 1m0s + targetCPUPercentile: 0.9 + enableStaticTokenKubeconfig: false + maintenance: + autoUpdate: + kubernetesVersion: true + machineImageVersion: true + timeWindow: + begin: 000000+0000 + end: 010000+0000 + provider: + type: gcp + controlPlaneConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: ControlPlaneConfig + zone: europe-west1-b + infrastructureConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: InfrastructureConfig + networks: + workers: 10.180.0.0/16 + purpose: production + region: europe-west1 + secretBindingName: laasds + systemComponents: + coreDNS: + autoscaling: + mode: horizontal + nodeLocalDNS: + enabled: true + schedulerName: default-scheduler +status: + conditions: + - type: APIServerAvailable + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: HealthzRequestSucceeded + message: API server /healthz endpoint responded with success status code. + - type: ControlPlaneHealthy + status: 'True' + lastTransitionTime: '2024-06-11T02:13:15Z' + lastUpdateTime: '2024-06-11T02:13:15Z' + reason: ControlPlaneRunning + message: All control plane components are healthy. + - type: ObservabilityComponentsHealthy + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: ObservabilityComponentsRunning + message: All observability components are healthy. + - type: EveryNodeReady + status: 'True' + lastTransitionTime: '2024-06-11T02:11:15Z' + lastUpdateTime: '2024-06-11T02:11:15Z' + reason: EveryNodeReady + message: All nodes are ready. + - type: SystemComponentsHealthy + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-11T00:38:43Z' + reason: SystemComponentsRunning + message: All system components are healthy. + constraints: + - type: HibernationPossible + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-07T01:56:39Z' + reason: NoProblematicWebhooks + message: All webhooks are properly configured. + - type: MaintenancePreconditionsSatisfied + status: 'True' + lastTransitionTime: '2024-06-11T00:38:43Z' + lastUpdateTime: '2024-06-07T01:56:39Z' + reason: NoProblematicWebhooks + message: All webhooks are properly configured. + hibernated: false + advertisedAddresses: + - name: external + url: https://api.example.org + - name: internal + url: https://api.internal.example.org + - name: service-account-issuer + url: >- + https://discovery.example.org/projects/test/shoots/test/issuer + lastOperation: + description: Shoot cluster has been successfully reconciled. + lastUpdateTime: '2024-06-11T00:38:43Z' + progress: 100 + state: Succeeded + type: Reconcile diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/cloudprofile-gcp.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/cloudprofile-gcp.yaml new file mode 100644 index 0000000..c26881f --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/cloudprofile-gcp.yaml @@ -0,0 +1,2089 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: CloudProfile +metadata: + name: gcp +spec: + kubernetes: + versions: + - classification: preview + version: 1.29.4 + - classification: supported + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.3 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.2 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.1 + - classification: preview + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.9 + - classification: supported + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.8 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.7 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.6 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.4 + - classification: preview + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.13 + - classification: supported + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.12 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.11 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.10 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.9 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.8 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.7 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.6 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.5 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.15 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.14 + - classification: deprecated + expirationDate: "2024-06-30T23:59:59Z" + version: 1.26.13 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.11 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.10 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.26.9 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.8 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.7 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.6 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.5 + - classification: deprecated + expirationDate: "2024-07-31T23:59:59Z" + version: 1.25.16 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.25.15 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.25.14 + machineImages: + - name: gardenlinux + updateStrategy: minor + versions: + - architectures: + - amd64 + - arm64 + classification: preview + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.5.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-06-23T23:59:59Z" + version: 1443.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.0.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1312.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 1312.0.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-07-15T23:59:59Z" + version: 934.11.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-05-15T23:59:59Z" + version: 934.10.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 934.9.0 + machineTypes: + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 7Gi + name: n1-standard-2 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 85Gi + name: a2-highgpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 170Gi + name: a2-highgpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 340Gi + name: a2-highgpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 680Gi + name: a2-highgpu-8g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-megagpu-16g + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 170Gi + name: a2-ultragpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 340Gi + name: a2-ultragpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 680Gi + name: a2-ultragpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-ultragpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-highgpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-megagpu-8g + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2-highcpu-56 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2-standard-16 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c2-standard-30 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2-standard-56 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c2-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2-standard-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 224Gi + name: c2d-highcpu-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c2d-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2d-highcpu-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 896Gi + name: c2d-highmem-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: c2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: c2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 448Gi + name: c2d-highmem-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 448Gi + name: c2d-standard-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: c2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: c2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2d-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2d-standard-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 352Gi + name: c3-highcpu-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 44Gi + name: c3-highcpu-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3-highcpu-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 88Gi + name: c3-highcpu-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3-highcpu-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 176Gi + name: c3-highcpu-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: c3-highmem-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 176Gi + name: c3-highmem-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3-highmem-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 352Gi + name: c3-highmem-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3-highmem-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: c3-highmem-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176-lssd + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4-lssd + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c3d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 354Gi + name: c3d-highcpu-180 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 59Gi + name: c3d-highcpu-30 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 708Gi + name: c3d-highcpu-360 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 118Gi + name: c3d-highcpu-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 177Gi + name: c3d-highcpu-90 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3d-highmem-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3d-standard-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90-lssd + usable: true + - architecture: amd64 + cpu: "240" + gpu: "0" + memory: 407Gi + name: ct4p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5l-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5l-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5l-hightpu-8t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5lp-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5lp-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5lp-hightpu-8t + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 448Gi + name: ct5p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: e2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: e2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: e2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: e2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: e2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: e2-highmem-2 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: e2-highmem-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: e2-highmem-8 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: e2-medium + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: e2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: e2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: e2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: e2-standard-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: e2-standard-8 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 48Gi + name: g2-standard-12 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: g2-standard-16 + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 96Gi + name: g2-standard-24 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: g2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: g2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: g2-standard-48 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: g2-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: g2-standard-96 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: h3-standard-88 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1433Gi + name: m1-megamem-96 + usable: true + - architecture: amd64 + cpu: "160" + gpu: "0" + memory: 3844Gi + name: m1-ultramem-160 + usable: true + - architecture: amd64 + cpu: "40" + gpu: "0" + memory: 961Gi + name: m1-ultramem-40 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 1922Gi + name: m1-ultramem-80 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 8832Gi + name: m2-hypermem-416 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 5888Gi + name: m2-megamem-416 + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 5888Gi + name: m2-ultramem-208 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 11776Gi + name: m2-ultramem-416 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1250Gi + name: m2-ultramem2x-96 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 600Gi + name: m2-ultramemx-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1952Gi + name: m3-megamem-128 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 976Gi + name: m3-megamem-64 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 3904Gi + name: m3-ultramem-128 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 976Gi + name: m3-ultramem-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 1952Gi + name: m3-ultramem-64 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 14Gi + name: n1-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 28Gi + name: n1-highcpu-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 57Gi + name: n1-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 7Gi + name: n1-highcpu-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 86Gi + name: n1-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 104Gi + name: n1-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 13Gi + name: n1-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 208Gi + name: n1-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 26Gi + name: n1-highmem-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 416Gi + name: n1-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 52Gi + name: n1-highmem-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 624Gi + name: n1-highmem-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 60Gi + name: n1-standard-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 120Gi + name: n1-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 15Gi + name: n1-standard-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 240Gi + name: n1-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 30Gi + name: n1-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 360Gi + name: n1-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2-highcpu-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 864Gi + name: n2-highmem-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2-standard-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 128Gi + name: n2d-highcpu-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 224Gi + name: n2d-highcpu-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2d-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2d-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2d-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2d-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2d-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2d-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2d-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2d-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2d-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2d-standard-2 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 896Gi + name: n2d-standard-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2d-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2d-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2d-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2d-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2d-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: n4-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: n4-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: n4-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: n4-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: n4-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: n4-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: n4-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 160Gi + name: n4-highcpu-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n4-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n4-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n4-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n4-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n4-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n4-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n4-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n4-highmem-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n4-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n4-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n4-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n4-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n4-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n4-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n4-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n4-standard-80 + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2a-standard-16 + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2a-standard-2 + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2a-standard-32 + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2a-standard-4 + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2a-standard-48 + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2a-standard-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2d-standard-48 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: t2d-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: z3-highmem-176 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: z3-highmem-88 + usable: true + providerConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: CloudProfileConfig + machineImages: + - name: gardenlinux + versions: + - architecture: amd64 + image: images/gardenlinux-amd64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-3-c261f887 + version: 1443.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-3-c261f887 + version: 1443.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-2-77c8471d + version: 1312.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-2-77c8471d + version: 1312.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-11-75132b8 + version: 934.11.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-10-f057c9b + version: 934.10.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-9-54c63e5 + version: 934.9.0 + regions: + - name: africa-south1 + zones: + - name: africa-south1-a + - name: africa-south1-b + - name: africa-south1-c + - name: asia-east1 + zones: + - name: asia-east1-a + - name: asia-east1-b + - name: asia-east1-c + - name: asia-east2 + zones: + - name: asia-east2-a + - name: asia-east2-b + - name: asia-east2-c + - name: asia-northeast1 + zones: + - name: asia-northeast1-a + - name: asia-northeast1-b + - name: asia-northeast1-c + - name: asia-northeast2 + zones: + - name: asia-northeast2-a + - name: asia-northeast2-b + - name: asia-northeast2-c + - name: asia-northeast3 + zones: + - name: asia-northeast3-a + - name: asia-northeast3-b + - name: asia-northeast3-c + - name: asia-south1 + zones: + - name: asia-south1-a + - name: asia-south1-b + - name: asia-south1-c + - name: asia-south2 + zones: + - name: asia-south2-a + - name: asia-south2-b + - name: asia-south2-c + - name: asia-southeast1 + zones: + - name: asia-southeast1-a + - name: asia-southeast1-b + - name: asia-southeast1-c + - name: asia-southeast2 + zones: + - name: asia-southeast2-a + - name: asia-southeast2-b + - name: asia-southeast2-c + - name: australia-southeast1 + zones: + - name: australia-southeast1-a + - name: australia-southeast1-b + - name: australia-southeast1-c + - name: australia-southeast2 + zones: + - name: australia-southeast2-a + - name: australia-southeast2-b + - name: australia-southeast2-c + - name: europe-central2 + zones: + - name: europe-central2-a + - name: europe-central2-b + - name: europe-central2-c + - name: europe-north1 + zones: + - name: europe-north1-a + - name: europe-north1-b + - name: europe-north1-c + - name: europe-southwest1 + zones: + - name: europe-southwest1-a + - name: europe-southwest1-b + - name: europe-southwest1-c + - name: europe-west1 + zones: + - name: europe-west1-b + - name: europe-west1-c + - name: europe-west1-d + - name: europe-west10 + zones: + - name: europe-west10-a + - name: europe-west10-b + - name: europe-west10-c + - name: europe-west12 + zones: + - name: europe-west12-a + - name: europe-west12-b + - name: europe-west12-c + - name: europe-west2 + zones: + - name: europe-west2-a + - name: europe-west2-b + - name: europe-west2-c + - name: europe-west3 + zones: + - name: europe-west3-a + - name: europe-west3-b + - name: europe-west3-c + - name: europe-west4 + zones: + - name: europe-west4-a + - name: europe-west4-b + - name: europe-west4-c + - name: europe-west5 + zones: + - name: europe-west5-a + - name: europe-west5-b + - name: europe-west5-c + - name: europe-west6 + zones: + - name: europe-west6-a + - name: europe-west6-b + - name: europe-west6-c + - name: europe-west8 + zones: + - name: europe-west8-a + - name: europe-west8-b + - name: europe-west8-c + - name: europe-west9 + zones: + - name: europe-west9-a + - name: europe-west9-b + - name: europe-west9-c + - name: me-central1 + zones: + - name: me-central1-a + - name: me-central1-b + - name: me-central1-c + - name: me-central2 + zones: + - name: me-central2-a + - name: me-central2-b + - name: me-central2-c + - name: me-west1 + zones: + - name: me-west1-a + - name: me-west1-b + - name: me-west1-c + - name: northamerica-northeast1 + zones: + - name: northamerica-northeast1-a + - name: northamerica-northeast1-b + - name: northamerica-northeast1-c + - name: northamerica-northeast2 + zones: + - name: northamerica-northeast2-a + - name: northamerica-northeast2-b + - name: northamerica-northeast2-c + - name: southamerica-east1 + zones: + - name: southamerica-east1-a + - name: southamerica-east1-b + - name: southamerica-east1-c + - name: southamerica-west1 + zones: + - name: southamerica-west1-a + - name: southamerica-west1-b + - name: southamerica-west1-c + - name: us-central1 + zones: + - name: us-central1-a + - name: us-central1-b + - name: us-central1-c + - name: us-central1-f + - name: us-east1 + zones: + - name: us-east1-b + - name: us-east1-c + - name: us-east1-d + - name: us-east4 + zones: + - name: us-east4-a + - name: us-east4-b + - name: us-east4-c + - name: us-east5 + zones: + - name: us-east5-a + - name: us-east5-b + - name: us-east5-c + - name: us-south1 + zones: + - name: us-south1-a + - name: us-south1-b + - name: us-south1-c + - name: us-west1 + zones: + - name: us-west1-a + - name: us-west1-b + - name: us-west1-c + - name: us-west2 + zones: + - name: us-west2-a + - name: us-west2-b + - name: us-west2-c + - name: us-west3 + zones: + - name: us-west3-a + - name: us-west3-b + - name: us-west3-c + - name: us-west4 + zones: + - name: us-west4-a + - name: us-west4-b + - name: us-west4-c + type: gcp + volumeTypes: + - class: premium + minSize: 20Gi + name: pd-balanced + usable: true + - class: standard + minSize: 20Gi + name: pd-standard + usable: true + - class: premium + minSize: 20Gi + name: pd-ssd + usable: true diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/ns-garden-bar.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/ns-garden-bar.yaml new file mode 100644 index 0000000..3da0e7d --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/ns-garden-bar.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: garden-bar diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/ns-garden-foo.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/ns-garden-foo.yaml new file mode 100644 index 0000000..9a1a19d --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/ns-garden-foo.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: garden-foo diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/project-bar.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/project-bar.yaml new file mode 100644 index 0000000..798a6f8 --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/project-bar.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: bar +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-bar + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/project-foo.yaml b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/project-foo.yaml new file mode 100644 index 0000000..bf43eec --- /dev/null +++ b/internal/controller/core/apiserver/handler/gardener/testdata/garden_cluster_2/project-foo.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: foo +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-foo + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/handler/handler.go b/internal/controller/core/apiserver/handler/handler.go new file mode 100644 index 0000000..d8eccaa --- /dev/null +++ b/internal/controller/core/apiserver/handler/handler.go @@ -0,0 +1,99 @@ +package controller + +import ( + "context" + "fmt" + "time" + + apiserverutils "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/utils" + + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +// UpdateStatusFunc is expected to update all component-specific fields in the status. +type UpdateStatusFunc func(*openmcpv1alpha1.APIServerStatus) error + +// APIServerHandler is an interface for the handlers for the different APIServer types. +type APIServerHandler interface { + // HandleCreateOrUpdate handles creation/update of the APIServer. + // It returns a reconcile result, an update function to update the status with, conditions that determine health/readiness of the cluster, and potentially an error that occurred. + // The status' condition will be overwritten based on the other returned values. + HandleCreateOrUpdate(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (ctrl.Result, UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) + + // HandleDelete handles the deletion of the APIServer. + // It returns a reconcile result, an update function to update the status with, conditions that determine health/readiness of the cluster (deletion), and potentially an error that occurred. + HandleDelete(ctx context.Context, dp *openmcpv1alpha1.APIServer, crateClient client.Client) (ctrl.Result, UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, openmcperrors.ReasonableError) +} + +// ClusterAccessEnabler is a helper interface. +// It is used to initially access the cluster in order to create serviceaccounts and generate kubeconfigs for them. +type ClusterAccessEnabler interface { + // Init is called before Client and RESTConfig. It is called only if access to the cluster is actually required. + // This can be used to put expensive operations into which should not be executed always - which would happen if they were in the constructor - but only when actually needed. + Init(ctx context.Context) error + // Client returns a client for accessing the cluster. + Client() client.Client + // RESTConfig returns the rest config for the cluster. The information from here is used for kubeconfig construction. + RESTConfig() *rest.Config +} + +// GetClusterAccess is a helper function to get admin and user kubeconfigs for an APIServer. +// It takes a possible existing admin and user access (or nil), as well as a ClusterAccessEnabler which provides initial access to the cluster. +// It returns an admin access, a user access, and the computed duration after which the APIServer should be reconciled to renew the kubeconfigs, if required. +func GetClusterAccess(ctx context.Context, serviceAccountNamespace, adminServiceAccount string, adminAccess *openmcpv1alpha1.APIServerAccess, cae ClusterAccessEnabler) (*openmcpv1alpha1.APIServerAccess, time.Duration, error) { + // generate kubeconfig/check token validity + // check if admin kubeconfig already exists + adminAccessExists := false + var adminRenewalAt time.Time + if adminAccess != nil { + adminAccessExists = true + adminRenewalAt = computeTokenRenewalTime(adminAccess) + } + renewAdminAccess := !adminAccessExists || (!adminRenewalAt.IsZero() && adminRenewalAt.Before(time.Now())) + + var requeueAfter time.Duration + if renewAdminAccess { + err := cae.Init(ctx) + if err != nil { + return nil, 0, fmt.Errorf("error initializing ClusterAccessEnabler: %w", err) + } + c := cae.Client() + rc := cae.RESTConfig() + + ns, err := apiserverutils.EnsureNamespace(ctx, c, serviceAccountNamespace) + if err != nil { + return nil, 0, err + } + + if renewAdminAccess { + adminAccess, err = apiserverutils.GetAdminAccess(ctx, c, rc, adminServiceAccount, ns.Name) + if err != nil { + return nil, 0, fmt.Errorf("error creating/renewing admin access for APIServer shoot cluster: %w", err) + } + requeueAfter = time.Until(computeTokenRenewalTime(adminAccess)) + } + } + + return adminAccess, requeueAfter, nil +} + +// computeTokenRenewalTime computes the time at which the given access should be renewed. +// Can only be computed if CreationTimestamp and ExpirationTimestamp are non-nil, otherwise the zero time is returned. +// The returned time is when 80% of the validity duration are reached. +func computeTokenRenewalTime(acc *openmcpv1alpha1.APIServerAccess) time.Time { + if acc == nil || acc.CreationTimestamp == nil || acc.ExpirationTimestamp == nil { + return time.Time{} + } + // validity is how long the token was valid in the first place + validity := acc.ExpirationTimestamp.Time.Sub(acc.CreationTimestamp.Time) + // renewalAfter is 80% of the validity + renewalAfter := time.Duration(float64(validity) * 0.8) + // renewalAt is the point in time at which the token should be renewed + renewalAt := acc.CreationTimestamp.Time.Add(renewalAfter) + return renewalAt +} diff --git a/internal/controller/core/apiserver/schemes/scheme.go b/internal/controller/core/apiserver/schemes/scheme.go new file mode 100644 index 0000000..d75ada1 --- /dev/null +++ b/internal/controller/core/apiserver/schemes/scheme.go @@ -0,0 +1,21 @@ +package schemes + +import ( + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + gardenauthenticationv1alpha1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/authentication/v1alpha1" + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" +) + +var ( + GardenerScheme *runtime.Scheme +) + +func init() { + GardenerScheme = runtime.NewScheme() + utilruntime.Must(clientgoscheme.AddToScheme(GardenerScheme)) + utilruntime.Must(gardenv1beta1.AddToScheme(GardenerScheme)) + utilruntime.Must(gardenauthenticationv1alpha1.AddToScheme(GardenerScheme)) +} diff --git a/internal/controller/core/apiserver/suite_test.go b/internal/controller/core/apiserver/suite_test.go new file mode 100644 index 0000000..9f1800a --- /dev/null +++ b/internal/controller/core/apiserver/suite_test.go @@ -0,0 +1,116 @@ +package apiserver_test + +import ( + "context" + "path" + "testing" + + apiserverconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/config" + apiserverhandler "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/handler" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/openmcp-project/controller-utils/pkg/collections" + "github.com/openmcp-project/controller-utils/pkg/logging" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + openmcptesting "github.com/openmcp-project/controller-utils/pkg/testing" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + "github.com/openmcp-project/mcp-operator/api/errors" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "APIServer Controller Test Suite") +} + +var defaultConfig *apiserverconfig.APIServerProviderConfiguration +var completedDefaultConfig *apiserverconfig.CompletedAPIServerProviderConfiguration +var constructorContext context.Context +var testObjs []client.Object +var fakeHandler *FakeHandler + +var _ = BeforeSuite(func() { + var err error + + // create a context with logger + log, err := logging.GetLogger() + Expect(err).NotTo(HaveOccurred()) + constructorContext = logging.NewContext(context.Background(), log) + + // load test objects + testObjs, err = openmcptesting.LoadObjects(path.Join("testdata", "garden_cluster"), testutils.Scheme) + Expect(err).ToNot(HaveOccurred()) + + // generate default config + defaultConfig, err = apiserverconfig.LoadConfig(path.Join("testdata", "default_config.yaml")) + Expect(err).NotTo(HaveOccurred()) +}) + +var _ = BeforeEach(func() { + // complete the config + var err error + completedDefaultConfig, err = defaultConfig.Complete(constructorContext) + Expect(err).NotTo(HaveOccurred()) + fakeHandler = NewFakeHandler() +}) + +var _ = AfterEach(func() { + cou, d := fakeHandler.ExpectedCalls() + Expect(cou).To(BeZero(), "expected %d more calls to HandleCreateOrUpdate", cou) + Expect(d).To(BeZero(), "expected %d more calls to HandleDelete", d) +}) + +// FakeHandler is a fake implementation of the APIServerHandler interface. +type FakeHandler struct { + MockedHandleCreateOrUpdateCalls collections.Queue[func(context.Context, *openmcpv1alpha1.APIServer, client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, errors.ReasonableError)] + MockedHandleDeleteCalls collections.Queue[func(context.Context, *openmcpv1alpha1.APIServer, client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, errors.ReasonableError)] +} + +// NewFakeHandler creates a new FakeHandler. +// Use this fake handler's Mock<...>Call methods to instruct it which actions to perform. +func NewFakeHandler() *FakeHandler { + return &FakeHandler{ + MockedHandleCreateOrUpdateCalls: collections.NewLinkedList[func(context.Context, *openmcpv1alpha1.APIServer, client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, errors.ReasonableError)](), + MockedHandleDeleteCalls: collections.NewLinkedList[func(context.Context, *openmcpv1alpha1.APIServer, client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, errors.ReasonableError)](), + } +} + +// MockHandleCreateOrUpdateCall adds a mocked HandleCreateOrUpdate call to the queue. +// Each time this handler's HandleCreateOrUpdate method is called, the next call in the queue will be executed. +func (f *FakeHandler) MockHandleCreateOrUpdateCall(call func(context.Context, *openmcpv1alpha1.APIServer, client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, errors.ReasonableError)) { + Expect(f.MockedHandleCreateOrUpdateCalls.Push(call)).To(Succeed()) +} + +// MockHandleDeleteCall adds a mocked HandleDelete call to the queue. +// Each time this handler's HandleDelete method is called, the next call in the queue will be executed. +func (f *FakeHandler) MockHandleDeleteCall(call func(context.Context, *openmcpv1alpha1.APIServer, client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, errors.ReasonableError)) { + Expect(f.MockedHandleDeleteCalls.Push(call)).To(Succeed()) +} + +func (f *FakeHandler) ExpectedCalls() (int, int) { + return f.MockedHandleCreateOrUpdateCalls.Size(), f.MockedHandleDeleteCalls.Size() +} + +var _ apiserverhandler.APIServerHandler = &FakeHandler{} + +// HandleCreateOrUpdate implements controller.APIServerHandler. +func (f *FakeHandler) HandleCreateOrUpdate(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, errors.ReasonableError) { + call := f.MockedHandleCreateOrUpdateCalls.Poll() + if call == nil { + panic("unexpected call to HandleCreateOrUpdate") + } + return call(ctx, as, crateClient) +} + +// HandleDelete implements controller.APIServerHandler. +func (f *FakeHandler) HandleDelete(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client) (reconcile.Result, apiserverhandler.UpdateStatusFunc, []openmcpv1alpha1.ComponentCondition, errors.ReasonableError) { + call := f.MockedHandleDeleteCalls.Poll() + if call == nil { + panic("unexpected call to HandleDelete") + } + return call(ctx, as, crateClient) +} diff --git a/internal/controller/core/apiserver/testdata/default_config.yaml b/internal/controller/core/apiserver/testdata/default_config.yaml new file mode 100644 index 0000000..e69de29 diff --git a/internal/controller/core/apiserver/testdata/garden_cluster/cloudprofile-gcp.yaml b/internal/controller/core/apiserver/testdata/garden_cluster/cloudprofile-gcp.yaml new file mode 100644 index 0000000..c26881f --- /dev/null +++ b/internal/controller/core/apiserver/testdata/garden_cluster/cloudprofile-gcp.yaml @@ -0,0 +1,2089 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: CloudProfile +metadata: + name: gcp +spec: + kubernetes: + versions: + - classification: preview + version: 1.29.4 + - classification: supported + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.3 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.2 + - classification: deprecated + expirationDate: "2025-04-15T23:59:59Z" + version: 1.29.1 + - classification: preview + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.9 + - classification: supported + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.8 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.7 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.6 + - classification: deprecated + expirationDate: "2024-12-15T23:59:59Z" + version: 1.28.4 + - classification: preview + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.13 + - classification: supported + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.12 + - classification: deprecated + expirationDate: "2024-09-30T23:59:59Z" + version: 1.27.11 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.10 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.9 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.8 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.7 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.6 + - classification: deprecated + expirationDate: "2024-08-11T23:59:59Z" + version: 1.27.5 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.15 + - classification: deprecated + expirationDate: "2024-08-31T23:59:59Z" + version: 1.26.14 + - classification: deprecated + expirationDate: "2024-06-30T23:59:59Z" + version: 1.26.13 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.11 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.26.10 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.26.9 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.8 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.7 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.6 + - classification: deprecated + expirationDate: "2024-04-09T23:59:59Z" + version: 1.26.5 + - classification: deprecated + expirationDate: "2024-07-31T23:59:59Z" + version: 1.25.16 + - classification: deprecated + expirationDate: "2024-05-31T23:59:59Z" + version: 1.25.15 + - classification: deprecated + expirationDate: "2024-05-20T23:59:59Z" + version: 1.25.14 + machineImages: + - name: gardenlinux + updateStrategy: minor + versions: + - architectures: + - amd64 + - arm64 + classification: preview + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.5.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1443.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-06-23T23:59:59Z" + version: 1443.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-04-17T23:59:59Z" + version: 1443.0.0 + - architectures: + - amd64 + - arm64 + classification: supported + cri: + - containerRuntimes: + - type: gvisor + name: containerd + version: 1312.3.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.2.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-09-01T23:59:59Z" + version: 1312.1.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 1312.0.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-07-15T23:59:59Z" + version: 934.11.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-05-15T23:59:59Z" + version: 934.10.0 + - architectures: + - amd64 + - arm64 + classification: deprecated + cri: + - containerRuntimes: + - type: gvisor + name: containerd + expirationDate: "2024-02-29T23:59:59Z" + version: 934.9.0 + machineTypes: + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 7Gi + name: n1-standard-2 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 85Gi + name: a2-highgpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 170Gi + name: a2-highgpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 340Gi + name: a2-highgpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 680Gi + name: a2-highgpu-8g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-megagpu-16g + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 170Gi + name: a2-ultragpu-1g + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 340Gi + name: a2-ultragpu-2g + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 680Gi + name: a2-ultragpu-4g + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1360Gi + name: a2-ultragpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-highgpu-8g + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 1872Gi + name: a3-megagpu-8g + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2-highcpu-56 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2-standard-16 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c2-standard-30 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2-standard-56 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c2-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2-standard-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 224Gi + name: c2d-highcpu-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: c2d-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: c2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 112Gi + name: c2d-highcpu-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 896Gi + name: c2d-highmem-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: c2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: c2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 448Gi + name: c2d-highmem-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 448Gi + name: c2d-standard-112 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: c2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: c2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c2d-standard-4 + usable: true + - architecture: amd64 + cpu: "56" + gpu: "0" + memory: 224Gi + name: c2d-standard-56 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 352Gi + name: c3-highcpu-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 44Gi + name: c3-highcpu-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3-highcpu-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 88Gi + name: c3-highcpu-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3-highcpu-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 176Gi + name: c3-highcpu-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: c3-highmem-176 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 176Gi + name: c3-highmem-22 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3-highmem-4 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 352Gi + name: c3-highmem-44 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3-highmem-8 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: c3-highmem-88 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 704Gi + name: c3-standard-176-lssd + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22 + usable: true + - architecture: amd64 + cpu: "22" + gpu: "0" + memory: 88Gi + name: c3-standard-22-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3-standard-4-lssd + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44 + usable: true + - architecture: amd64 + cpu: "44" + gpu: "0" + memory: 176Gi + name: c3-standard-44-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: c3-standard-88-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: c3d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 354Gi + name: c3d-highcpu-180 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 59Gi + name: c3d-highcpu-30 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 708Gi + name: c3d-highcpu-360 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: c3d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 118Gi + name: c3d-highcpu-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: c3d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 177Gi + name: c3d-highcpu-90 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: c3d-highmem-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 1440Gi + name: c3d-highmem-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 240Gi + name: c3d-highmem-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 2880Gi + name: c3d-highmem-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: c3d-highmem-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 480Gi + name: c3d-highmem-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: c3d-highmem-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 720Gi + name: c3d-highmem-90-lssd + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: c3d-standard-16-lssd + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180 + usable: true + - architecture: amd64 + cpu: "180" + gpu: "0" + memory: 720Gi + name: c3d-standard-180-lssd + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30 + usable: true + - architecture: amd64 + cpu: "30" + gpu: "0" + memory: 120Gi + name: c3d-standard-30-lssd + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360 + usable: true + - architecture: amd64 + cpu: "360" + gpu: "0" + memory: 1440Gi + name: c3d-standard-360-lssd + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: c3d-standard-4 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: c3d-standard-60-lssd + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: c3d-standard-8-lssd + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90 + usable: true + - architecture: amd64 + cpu: "90" + gpu: "0" + memory: 360Gi + name: c3d-standard-90-lssd + usable: true + - architecture: amd64 + cpu: "240" + gpu: "0" + memory: 407Gi + name: ct4p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5l-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5l-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5l-hightpu-8t + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 48Gi + name: ct5lp-hightpu-1t + usable: true + - architecture: amd64 + cpu: "112" + gpu: "0" + memory: 192Gi + name: ct5lp-hightpu-4t + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 384Gi + name: ct5lp-hightpu-8t + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 448Gi + name: ct5p-hightpu-4t + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: e2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: e2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: e2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: e2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: e2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: e2-highmem-2 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: e2-highmem-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: e2-highmem-8 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: e2-medium + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: e2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: e2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: e2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: e2-standard-4 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: e2-standard-8 + usable: true + - architecture: amd64 + cpu: "12" + gpu: "0" + memory: 48Gi + name: g2-standard-12 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: g2-standard-16 + usable: true + - architecture: amd64 + cpu: "24" + gpu: "0" + memory: 96Gi + name: g2-standard-24 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: g2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: g2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: g2-standard-48 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: g2-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: g2-standard-96 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 352Gi + name: h3-standard-88 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1433Gi + name: m1-megamem-96 + usable: true + - architecture: amd64 + cpu: "160" + gpu: "0" + memory: 3844Gi + name: m1-ultramem-160 + usable: true + - architecture: amd64 + cpu: "40" + gpu: "0" + memory: 961Gi + name: m1-ultramem-40 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 1922Gi + name: m1-ultramem-80 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 8832Gi + name: m2-hypermem-416 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 5888Gi + name: m2-megamem-416 + usable: true + - architecture: amd64 + cpu: "208" + gpu: "0" + memory: 5888Gi + name: m2-ultramem-208 + usable: true + - architecture: amd64 + cpu: "416" + gpu: "0" + memory: 11776Gi + name: m2-ultramem-416 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 1250Gi + name: m2-ultramem2x-96 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 600Gi + name: m2-ultramemx-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 1952Gi + name: m3-megamem-128 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 976Gi + name: m3-megamem-64 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 3904Gi + name: m3-ultramem-128 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 976Gi + name: m3-ultramem-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 1952Gi + name: m3-ultramem-64 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 14Gi + name: n1-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 28Gi + name: n1-highcpu-32 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 57Gi + name: n1-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 7Gi + name: n1-highcpu-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 86Gi + name: n1-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 104Gi + name: n1-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 13Gi + name: n1-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 208Gi + name: n1-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 26Gi + name: n1-highmem-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 416Gi + name: n1-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 52Gi + name: n1-highmem-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 624Gi + name: n1-highmem-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 60Gi + name: n1-standard-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 120Gi + name: n1-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 15Gi + name: n1-standard-4 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 240Gi + name: n1-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 30Gi + name: n1-standard-8 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 360Gi + name: n1-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2-highcpu-16 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2-highcpu-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 864Gi + name: n2-highmem-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2-standard-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 128Gi + name: n2d-highcpu-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 16Gi + name: n2d-highcpu-16 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 224Gi + name: n2d-highcpu-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 32Gi + name: n2d-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 4Gi + name: n2d-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 48Gi + name: n2d-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 64Gi + name: n2d-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 8Gi + name: n2d-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 80Gi + name: n2d-highcpu-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 96Gi + name: n2d-highcpu-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n2d-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n2d-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n2d-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n2d-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n2d-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n2d-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n2d-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n2d-highmem-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 768Gi + name: n2d-highmem-96 + usable: true + - architecture: amd64 + cpu: "128" + gpu: "0" + memory: 512Gi + name: n2d-standard-128 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n2d-standard-2 + usable: true + - architecture: amd64 + cpu: "224" + gpu: "0" + memory: 896Gi + name: n2d-standard-224 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n2d-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n2d-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n2d-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n2d-standard-80 + usable: true + - architecture: amd64 + cpu: "96" + gpu: "0" + memory: 384Gi + name: n2d-standard-96 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 32Gi + name: n4-highcpu-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 4Gi + name: n4-highcpu-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 64Gi + name: n4-highcpu-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 8Gi + name: n4-highcpu-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 96Gi + name: n4-highcpu-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 128Gi + name: n4-highcpu-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 16Gi + name: n4-highcpu-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 160Gi + name: n4-highcpu-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 128Gi + name: n4-highmem-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 16Gi + name: n4-highmem-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 256Gi + name: n4-highmem-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 32Gi + name: n4-highmem-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 384Gi + name: n4-highmem-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 512Gi + name: n4-highmem-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 64Gi + name: n4-highmem-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 640Gi + name: n4-highmem-80 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: n4-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: n4-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: n4-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: n4-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: n4-standard-48 + usable: true + - architecture: amd64 + cpu: "64" + gpu: "0" + memory: 256Gi + name: n4-standard-64 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: n4-standard-8 + usable: true + - architecture: amd64 + cpu: "80" + gpu: "0" + memory: 320Gi + name: n4-standard-80 + usable: true + - architecture: arm64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2a-standard-16 + usable: true + - architecture: arm64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2a-standard-2 + usable: true + - architecture: arm64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2a-standard-32 + usable: true + - architecture: arm64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2a-standard-4 + usable: true + - architecture: arm64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2a-standard-48 + usable: true + - architecture: arm64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2a-standard-8 + usable: true + - architecture: amd64 + cpu: "16" + gpu: "0" + memory: 64Gi + name: t2d-standard-16 + usable: true + - architecture: amd64 + cpu: "2" + gpu: "0" + memory: 8Gi + name: t2d-standard-2 + usable: true + - architecture: amd64 + cpu: "32" + gpu: "0" + memory: 128Gi + name: t2d-standard-32 + usable: true + - architecture: amd64 + cpu: "4" + gpu: "0" + memory: 16Gi + name: t2d-standard-4 + usable: true + - architecture: amd64 + cpu: "48" + gpu: "0" + memory: 192Gi + name: t2d-standard-48 + usable: true + - architecture: amd64 + cpu: "60" + gpu: "0" + memory: 240Gi + name: t2d-standard-60 + usable: true + - architecture: amd64 + cpu: "8" + gpu: "0" + memory: 32Gi + name: t2d-standard-8 + usable: true + - architecture: amd64 + cpu: "176" + gpu: "0" + memory: 1408Gi + name: z3-highmem-176 + usable: true + - architecture: amd64 + cpu: "88" + gpu: "0" + memory: 704Gi + name: z3-highmem-88 + usable: true + providerConfig: + apiVersion: gcp.provider.extensions.gardener.cloud/v1alpha1 + kind: CloudProfileConfig + machineImages: + - name: gardenlinux + versions: + - architecture: amd64 + image: images/gardenlinux-amd64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-5-bfb687a7 + version: 1443.5.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-3-c261f887 + version: 1443.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-3-c261f887 + version: 1443.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-2-7c14ae22 + version: 1443.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-1-36c740a4 + version: 1443.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1443-0-ea851d67 + version: 1443.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-3-3dc9d0b5 + version: 1312.3.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-2-77c8471d + version: 1312.2.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-2-77c8471d + version: 1312.2.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-1-c6ebc74e + version: 1312.1.0 + - architecture: amd64 + image: images/gardenlinux-amd64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: arm64 + image: images/gardenlinux-arm64-1312-0-40b9db2c + version: 1312.0.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-11-75132b8 + version: 934.11.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-10-f057c9b + version: 934.10.0 + - architecture: amd64 + image: images/gardenlinux-amd64-934-9-54c63e5 + version: 934.9.0 + regions: + - name: africa-south1 + zones: + - name: africa-south1-a + - name: africa-south1-b + - name: africa-south1-c + - name: asia-east1 + zones: + - name: asia-east1-a + - name: asia-east1-b + - name: asia-east1-c + - name: asia-east2 + zones: + - name: asia-east2-a + - name: asia-east2-b + - name: asia-east2-c + - name: asia-northeast1 + zones: + - name: asia-northeast1-a + - name: asia-northeast1-b + - name: asia-northeast1-c + - name: asia-northeast2 + zones: + - name: asia-northeast2-a + - name: asia-northeast2-b + - name: asia-northeast2-c + - name: asia-northeast3 + zones: + - name: asia-northeast3-a + - name: asia-northeast3-b + - name: asia-northeast3-c + - name: asia-south1 + zones: + - name: asia-south1-a + - name: asia-south1-b + - name: asia-south1-c + - name: asia-south2 + zones: + - name: asia-south2-a + - name: asia-south2-b + - name: asia-south2-c + - name: asia-southeast1 + zones: + - name: asia-southeast1-a + - name: asia-southeast1-b + - name: asia-southeast1-c + - name: asia-southeast2 + zones: + - name: asia-southeast2-a + - name: asia-southeast2-b + - name: asia-southeast2-c + - name: australia-southeast1 + zones: + - name: australia-southeast1-a + - name: australia-southeast1-b + - name: australia-southeast1-c + - name: australia-southeast2 + zones: + - name: australia-southeast2-a + - name: australia-southeast2-b + - name: australia-southeast2-c + - name: europe-central2 + zones: + - name: europe-central2-a + - name: europe-central2-b + - name: europe-central2-c + - name: europe-north1 + zones: + - name: europe-north1-a + - name: europe-north1-b + - name: europe-north1-c + - name: europe-southwest1 + zones: + - name: europe-southwest1-a + - name: europe-southwest1-b + - name: europe-southwest1-c + - name: europe-west1 + zones: + - name: europe-west1-b + - name: europe-west1-c + - name: europe-west1-d + - name: europe-west10 + zones: + - name: europe-west10-a + - name: europe-west10-b + - name: europe-west10-c + - name: europe-west12 + zones: + - name: europe-west12-a + - name: europe-west12-b + - name: europe-west12-c + - name: europe-west2 + zones: + - name: europe-west2-a + - name: europe-west2-b + - name: europe-west2-c + - name: europe-west3 + zones: + - name: europe-west3-a + - name: europe-west3-b + - name: europe-west3-c + - name: europe-west4 + zones: + - name: europe-west4-a + - name: europe-west4-b + - name: europe-west4-c + - name: europe-west5 + zones: + - name: europe-west5-a + - name: europe-west5-b + - name: europe-west5-c + - name: europe-west6 + zones: + - name: europe-west6-a + - name: europe-west6-b + - name: europe-west6-c + - name: europe-west8 + zones: + - name: europe-west8-a + - name: europe-west8-b + - name: europe-west8-c + - name: europe-west9 + zones: + - name: europe-west9-a + - name: europe-west9-b + - name: europe-west9-c + - name: me-central1 + zones: + - name: me-central1-a + - name: me-central1-b + - name: me-central1-c + - name: me-central2 + zones: + - name: me-central2-a + - name: me-central2-b + - name: me-central2-c + - name: me-west1 + zones: + - name: me-west1-a + - name: me-west1-b + - name: me-west1-c + - name: northamerica-northeast1 + zones: + - name: northamerica-northeast1-a + - name: northamerica-northeast1-b + - name: northamerica-northeast1-c + - name: northamerica-northeast2 + zones: + - name: northamerica-northeast2-a + - name: northamerica-northeast2-b + - name: northamerica-northeast2-c + - name: southamerica-east1 + zones: + - name: southamerica-east1-a + - name: southamerica-east1-b + - name: southamerica-east1-c + - name: southamerica-west1 + zones: + - name: southamerica-west1-a + - name: southamerica-west1-b + - name: southamerica-west1-c + - name: us-central1 + zones: + - name: us-central1-a + - name: us-central1-b + - name: us-central1-c + - name: us-central1-f + - name: us-east1 + zones: + - name: us-east1-b + - name: us-east1-c + - name: us-east1-d + - name: us-east4 + zones: + - name: us-east4-a + - name: us-east4-b + - name: us-east4-c + - name: us-east5 + zones: + - name: us-east5-a + - name: us-east5-b + - name: us-east5-c + - name: us-south1 + zones: + - name: us-south1-a + - name: us-south1-b + - name: us-south1-c + - name: us-west1 + zones: + - name: us-west1-a + - name: us-west1-b + - name: us-west1-c + - name: us-west2 + zones: + - name: us-west2-a + - name: us-west2-b + - name: us-west2-c + - name: us-west3 + zones: + - name: us-west3-a + - name: us-west3-b + - name: us-west3-c + - name: us-west4 + zones: + - name: us-west4-a + - name: us-west4-b + - name: us-west4-c + type: gcp + volumeTypes: + - class: premium + minSize: 20Gi + name: pd-balanced + usable: true + - class: standard + minSize: 20Gi + name: pd-standard + usable: true + - class: premium + minSize: 20Gi + name: pd-ssd + usable: true diff --git a/internal/controller/core/apiserver/testdata/garden_cluster/ns-garden-test.yaml b/internal/controller/core/apiserver/testdata/garden_cluster/ns-garden-test.yaml new file mode 100644 index 0000000..e48bb72 --- /dev/null +++ b/internal/controller/core/apiserver/testdata/garden_cluster/ns-garden-test.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: garden-test diff --git a/internal/controller/core/apiserver/testdata/garden_cluster/project-test.yaml b/internal/controller/core/apiserver/testdata/garden_cluster/project-test.yaml new file mode 100644 index 0000000..6a319b0 --- /dev/null +++ b/internal/controller/core/apiserver/testdata/garden_cluster/project-test.yaml @@ -0,0 +1,29 @@ +apiVersion: core.gardener.cloud/v1beta1 +kind: Project +metadata: + generation: 27 + name: test +spec: + createdBy: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + description: Test Project + members: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + role: admin + roles: + - uam + - serviceaccountmanager + namespace: garden-test + owner: + apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.org + purpose: Test Purpose +status: + lastActivityTimestamp: "2024-06-06T14:56:01Z" + observedGeneration: 27 + phase: Ready \ No newline at end of file diff --git a/internal/controller/core/apiserver/testdata/test-01/apiserver.yaml b/internal/controller/core/apiserver/testdata/test-01/apiserver.yaml new file mode 100644 index 0000000..fb79d47 --- /dev/null +++ b/internal/controller/core/apiserver/testdata/test-01/apiserver.yaml @@ -0,0 +1,17 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + annotations: + openmcp.cloud/operation: ignore + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: "test" + openmcp.cloud/mcp-namespace: "test" + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Fake diff --git a/internal/controller/core/apiserver/testdata/test-02/apiserver.yaml b/internal/controller/core/apiserver/testdata/test-02/apiserver.yaml new file mode 100644 index 0000000..f45d488 --- /dev/null +++ b/internal/controller/core/apiserver/testdata/test-02/apiserver.yaml @@ -0,0 +1,17 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + annotations: + openmcp.cloud/operation: reconcile + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: "test" + openmcp.cloud/mcp-namespace: "test" + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Fake diff --git a/internal/controller/core/apiserver/testdata/test-03/apiserver.yaml b/internal/controller/core/apiserver/testdata/test-03/apiserver.yaml new file mode 100644 index 0000000..6295770 --- /dev/null +++ b/internal/controller/core/apiserver/testdata/test-03/apiserver.yaml @@ -0,0 +1,17 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + finalizers: + - dependency.openmcp.cloud/landscaper + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: "test" + openmcp.cloud/mcp-namespace: "test" + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Fake diff --git a/internal/controller/core/apiserver/testdata/test-04/apiserver.yaml b/internal/controller/core/apiserver/testdata/test-04/apiserver.yaml new file mode 100644 index 0000000..8a53178 --- /dev/null +++ b/internal/controller/core/apiserver/testdata/test-04/apiserver.yaml @@ -0,0 +1,15 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: "test" + openmcp.cloud/mcp-namespace: "test" + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Unknown diff --git a/internal/controller/core/apiserver/testdata/test-05/apiserver.yaml b/internal/controller/core/apiserver/testdata/test-05/apiserver.yaml new file mode 100644 index 0000000..e5c63e4 --- /dev/null +++ b/internal/controller/core/apiserver/testdata/test-05/apiserver.yaml @@ -0,0 +1,15 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: "test" + openmcp.cloud/mcp-namespace: "test" + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Fake diff --git a/internal/controller/core/apiserver/testdata/test-06/APIServer.yaml b/internal/controller/core/apiserver/testdata/test-06/APIServer.yaml new file mode 100644 index 0000000..ef65635 --- /dev/null +++ b/internal/controller/core/apiserver/testdata/test-06/APIServer.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + openmcp.cloud/mcp-name: "test" + openmcp.cloud/mcp-namespace: "test" + deletionTimestamp: "2024-06-17T13:54:12Z" + finalizers: + - apiserver.openmcp.cloud + name: test + namespace: test +spec: + desiredRegion: + direction: central + name: europe + type: Fake diff --git a/internal/controller/core/apiserver/utils/access.go b/internal/controller/core/apiserver/utils/access.go new file mode 100644 index 0000000..1baff00 --- /dev/null +++ b/internal/controller/core/apiserver/utils/access.go @@ -0,0 +1,375 @@ +package utils + +import ( + "context" + "fmt" + "reflect" + "strings" + "time" + + colactrlutil "github.com/openmcp-project/controller-utils/pkg/controller" + authenticationv1 "k8s.io/api/authentication/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmdapi "k8s.io/client-go/tools/clientcmd/api" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +const DefaultAdminAccessValidityTime = 180 * 24 * time.Hour + +// GetAdminAccess creates a ServiceAccount (if it does not exist), binds it to the cluster-admin role and returns a kubeconfig for it. +func GetAdminAccess(ctx context.Context, c client.Client, cfg *rest.Config, saName, saNamespace string) (*openmcpv1alpha1.APIServerAccess, error) { + sa, err := EnsureServiceAccount(ctx, c, saName, saNamespace) + if err != nil { + return nil, err + } + + _, err = BindToClusterRole(ctx, c, "cluster-admin", rbacv1.Subject{ + Kind: "ServiceAccount", + Name: sa.Name, + Namespace: sa.Namespace, + }) + if err != nil { + return nil, err + } + + tr, err := CreateTokenForServiceAccount(ctx, c, sa, ptr.To(DefaultAdminAccessValidityTime)) + if err != nil { + return nil, err + } + + kcfgBytes, err := CreateTokenKubeconfig(saName, cfg.Host, cfg.CAData, tr.Status.Token) + if err != nil { + return nil, err + } + + return &openmcpv1alpha1.APIServerAccess{ + Kubeconfig: string(kcfgBytes), + CreationTimestamp: ptr.To(metav1.Now()), + ExpirationTimestamp: &tr.Status.ExpirationTimestamp, + }, nil +} + +// BindToClusterRole creates/updates a ClusterRoleBinding that binds the given subject to the given ClusterRole. +// It returns the created/updated ClusterRoleBinding. +func BindToClusterRole(ctx context.Context, c client.Client, clusterRoleName string, subject rbacv1.Subject) (*rbacv1.ClusterRoleBinding, error) { + crb := &rbacv1.ClusterRoleBinding{} + crb.SetName(fmt.Sprintf("%s--%s", subject.Name, clusterRoleName)) + if err := FailIfNotManaged(ctx, c, crb); err != nil { + return nil, fmt.Errorf("error updating ClusterRoleBinding '%s': %w", crb.Name, err) + } + _, err := controllerutil.CreateOrUpdate(ctx, c, crb, func() error { + // set managed-by label + labels := crb.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[openmcpv1alpha1.ManagedByAPIServerLabel] = "true" + crb.SetLabels(labels) + + // set role ref + crb.RoleRef = rbacv1.RoleRef{ + APIGroup: rbacv1.SchemeGroupVersion.Group, + Kind: "ClusterRole", + Name: clusterRoleName, + } + + // set subject + if crb.Subjects == nil { + crb.Subjects = []rbacv1.Subject{} + } + + for _, sub := range crb.Subjects { + if reflect.DeepEqual(sub, subject) { + // ClusterRoleBinding is up-to-date, nothing to do + return nil + } + } + crb.Subjects = append(crb.Subjects, subject) + + return nil + }) + if err != nil { + return nil, fmt.Errorf("error creating/updating ClusterRoleBinding '%s': %w", crb.Name, err) + } + + return crb, nil +} + +// EnsureServiceAccount creates a ServiceAccount, if required. +// It returns the ServiceAccount. +func EnsureServiceAccount(ctx context.Context, c client.Client, saName, saNamespace string) (*corev1.ServiceAccount, error) { + sa := &corev1.ServiceAccount{} + sa.SetName(saName) + sa.SetNamespace(saNamespace) + if err := FailIfNotManaged(ctx, c, sa); err != nil { + return nil, fmt.Errorf("error updating ServiceAccount '%s/%s': %w", sa.Namespace, sa.Name, err) + } + _, err := controllerutil.CreateOrUpdate(ctx, c, sa, func() error { + // set managed-by label + labels := sa.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[openmcpv1alpha1.ManagedByAPIServerLabel] = "true" + sa.SetLabels(labels) + + return nil + }) + if err != nil { + return nil, fmt.Errorf("error creating/updating ServiceAccount '%s/%s': %w", sa.Namespace, sa.Name, err) + } + + return sa, nil +} + +// EnsureNamespace creates a Namespace, if required. +// It returns the Namespace. +func EnsureNamespace(ctx context.Context, c client.Client, nsName string) (*corev1.Namespace, error) { + ns := &corev1.Namespace{} + ns.SetName(nsName) + if err := FailIfNotManaged(ctx, c, ns); err != nil { + return nil, fmt.Errorf("error updating Namespace '%s': %w", ns.Name, err) + } + _, err := controllerutil.CreateOrUpdate(ctx, c, ns, func() error { + // set managed-by label + labels := ns.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[openmcpv1alpha1.ManagedByAPIServerLabel] = "true" + ns.SetLabels(labels) + + return nil + }) + if err != nil { + return nil, fmt.Errorf("error creating/updating Namespace '%s': %w", ns.Name, err) + } + + return ns, nil +} + +// EnsureUserClusterRole creates/updates a ClusterRole that has permissions for namespaces, secrets, and configmaps. +func EnsureUserClusterRole(ctx context.Context, c client.Client, crName string) (*rbacv1.ClusterRole, error) { + cr := &rbacv1.ClusterRole{} + cr.SetName(crName) + if err := FailIfNotManaged(ctx, c, cr); err != nil { + return nil, fmt.Errorf("error updating ClusterRole '%s': %w", cr.Name, err) + } + _, err := controllerutil.CreateOrUpdate(ctx, c, cr, func() error { + // set managed-by label + labels := cr.GetLabels() + if labels == nil { + labels = map[string]string{} + } + labels[openmcpv1alpha1.ManagedByAPIServerLabel] = "true" + cr.SetLabels(labels) + + cr.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{corev1.GroupName}, + Resources: []string{ + "namespaces", + "secrets", + "configmaps", + }, + Verbs: []string{rbacv1.VerbAll}, + }, + } + return nil + }) + if err != nil { + return nil, fmt.Errorf("error creating/updating ClusterRole '%s': %w", cr.Name, err) + } + + return cr, nil +} + +// CreateTokenForServiceAccount generates a token for the given ServiceAccount. +// Returns a TokenRequest object whose status contains the token and its expiration timestamp. +func CreateTokenForServiceAccount(ctx context.Context, c client.Client, sa *corev1.ServiceAccount, desiredDuration *time.Duration) (*authenticationv1.TokenRequest, error) { + tr := &authenticationv1.TokenRequest{} + if desiredDuration != nil { + tr.Spec.ExpirationSeconds = ptr.To((int64)(desiredDuration.Seconds())) + } + + if err := c.SubResource("token").Create(ctx, sa, tr); err != nil { + return nil, fmt.Errorf("error creating token for ServiceAccount '%s/%s': %w", sa.Namespace, sa.Name, err) + } + + return tr, nil +} + +// FailIfNotManaged fetches the given object from the cluster and returns an error if it does not contain the managed-by label set to 'true'. +// Also returns an error if fetching the object doesn't work, unless the reason is that it doesn't exist, then nil is returned. +func FailIfNotManaged(ctx context.Context, c client.Client, obj client.Object) error { + if err := c.Get(ctx, client.ObjectKeyFromObject(obj), obj); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + if !colactrlutil.HasLabelWithValue(obj, openmcpv1alpha1.ManagedByAPIServerLabel, "true") { + return fmt.Errorf("resource already exists, but does not have label '%s' with value 'true'", openmcpv1alpha1.ManagedByAPIServerLabel) + } + return nil +} + +// PatchManagedByLabel adds the managed-by label to the given resource via a patch. +func PatchManagedByLabel(ctx context.Context, c client.Client, obj client.Object) error { + return c.Patch(ctx, obj, client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf(`{"metadata":{"labels":{"%s":"true"}}}`, openmcpv1alpha1.ManagedByAPIServerLabel)))) +} + +// CreateTokenKubeconfig generates a kubeconfig based on the given values. +// The 'user' arg is used as key for the auth configuration and can be chosen freely. +func CreateTokenKubeconfig(user, host string, caData []byte, token string) ([]byte, error) { + id := "cluster" + kcfg := clientcmdapi.Config{ + APIVersion: "v1", + Kind: "Config", + Clusters: map[string]*clientcmdapi.Cluster{ + id: { + Server: host, + CertificateAuthorityData: caData, + }, + }, + Contexts: map[string]*clientcmdapi.Context{ + id: { + Cluster: id, + AuthInfo: user, + }, + }, + CurrentContext: id, + AuthInfos: map[string]*clientcmdapi.AuthInfo{ + user: { + Token: token, + }, + }, + } + + kcfgBytes, err := clientcmd.Write(kcfg) + if err != nil { + return nil, fmt.Errorf("error converting converting generated kubeconfig into yaml: %w", err) + } + return kcfgBytes, nil +} + +// CreateOIDCKubeconfig generates a kubeconfig for a cluster that uses OIDC for authentication. +// For each identity provider, a user is created that uses the 'oidc-login' plugin to get a token. +// The cluster name is prefixed with 'mcp--' and the context name is clusterName--idpName. +func CreateOIDCKubeconfig(ctx context.Context, crateClient client.Client, clusterName, namespace, host, defaultIdp string, caData []byte, identityProviders []openmcpv1alpha1.IdentityProvider) ([]byte, error) { + contextName := func(clusterName, idpName string) string { + return clusterName + "--" + idpName + } + createParameter := func(key, value string) string { + if value == "" { + return "--" + key + } + return "--" + key + "=" + value + } + + clusterName = "mcp-" + namespace + "-" + clusterName + + users := make(map[string]*clientcmdapi.AuthInfo) + contexts := make(map[string]*clientcmdapi.Context) + + flags := map[string]openmcpv1alpha1.SingleOrMultiStringValue{ + openmcpv1alpha1.OIDCParameterUsePKCE: {}, + openmcpv1alpha1.OIDCParameterGrantType: {Value: openmcpv1alpha1.OIDCDefaultGrantType}, + openmcpv1alpha1.OIDCParameterExtraScope: {Values: strings.Split(openmcpv1alpha1.OIDCDefaultExtraScopes, ",")}, + } + + for _, idp := range identityProviders { + if idp.ClientConfig.ExtraConfig != nil { + for key, value := range idp.ClientConfig.ExtraConfig { + flags[key] = value + } + } + + user := &clientcmdapi.AuthInfo{ + Exec: &clientcmdapi.ExecConfig{ + APIVersion: "client.authentication.k8s.io/v1beta1", + Command: "kubectl", + Env: nil, + ProvideClusterInfo: false, + Args: []string{ + "oidc-login", + "get-token", + createParameter(openmcpv1alpha1.OIDCParameterIssuerURL, idp.IssuerURL), + createParameter(openmcpv1alpha1.OIDCParameterClientID, idp.ClientID), + }, + }, + } + + for key, value := range flags { + if len(value.Values) > 0 { + // repeatable parameter + for _, v := range value.Values { + user.Exec.Args = append(user.Exec.Args, createParameter(key, v)) + } + continue + } + if len(value.Value) > 0 { + // single parameter + user.Exec.Args = append(user.Exec.Args, createParameter(key, value.Value)) + + continue + } + // flag without value + user.Exec.Args = append(user.Exec.Args, createParameter(key, "")) + } + + if idp.ClientConfig.ClientSecret != nil { + ref := idp.ClientConfig.ClientSecret + secret := &corev1.Secret{} + err := crateClient.Get(ctx, types.NamespacedName{Name: ref.Name, Namespace: namespace}, secret) + if err != nil { + return nil, fmt.Errorf("error getting idp clientSecret secret '%s': %w", ref.Name, err) + } + + clientSecret, ok := secret.Data[ref.Key] + if !ok { + return nil, fmt.Errorf("clientSecret key '%s' not found in secret '%s'", ref.Key, ref.Name) + } + user.Exec.Args = append(user.Exec.Args, createParameter("oidc-client-secret", string(clientSecret))) + } + + context := &clientcmdapi.Context{ + Cluster: clusterName, + AuthInfo: idp.Name, + Namespace: "default", + } + + users[idp.Name] = user + contexts[contextName(clusterName, idp.Name)] = context + } + + kcfg := clientcmdapi.Config{ + APIVersion: "v1", + Kind: "Config", + Clusters: map[string]*clientcmdapi.Cluster{ + clusterName: { + Server: host, + CertificateAuthorityData: caData, + }, + }, + Contexts: contexts, + CurrentContext: contextName(clusterName, defaultIdp), + AuthInfos: users, + } + + kcfgBytes, err := clientcmd.Write(kcfg) + if err != nil { + return nil, fmt.Errorf("error converting converting generated kubeconfig into yaml: %w", err) + } + return kcfgBytes, nil +} diff --git a/internal/controller/core/authentication/auth_suite_test.go b/internal/controller/core/authentication/auth_suite_test.go new file mode 100644 index 0000000..f9f3a65 --- /dev/null +++ b/internal/controller/core/authentication/auth_suite_test.go @@ -0,0 +1,13 @@ +package authentication_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Authentication Controller Test Suite") +} diff --git a/internal/controller/core/authentication/config/config.go b/internal/controller/core/authentication/config/config.go new file mode 100644 index 0000000..ccdb853 --- /dev/null +++ b/internal/controller/core/authentication/config/config.go @@ -0,0 +1,68 @@ +package config + +import ( + "k8s.io/apimachinery/pkg/util/validation/field" + + "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +const ( + DefaultSystemIdPName = "openmcp" + DefaultSystemUsernameClaim = "email" + DefaultSystemGroupsClaim = "groups" + + DefaultCratedIdPName = "crate" + DefaultCrateClientID = "mcp" + DefaultCrateUsernameClaim = "sub" +) + +// AuthenticationConfig contains the configuration for the authentication controller. +type AuthenticationConfig struct { + // SystemIdentityProvider contains the configuration for the system identity provider. + SystemIdentityProvider v1alpha1.IdentityProvider `json:"systemIdentityProvider,omitempty"` + // CrateIdentityProvider contains the configuration for the Crate token issuer. + // This can be used to validate tokens issued by the crate cluster. + // +optional + CrateIdentityProvider *v1alpha1.IdentityProvider `json:"crateIdentityProvider,omitempty"` +} + +// SetDefaults sets the default values for the authentication configuration when not set. +func (ac *AuthenticationConfig) SetDefaults() { + // SystemIdentityProvider + if ac.SystemIdentityProvider.Name == "" { + ac.SystemIdentityProvider.Name = DefaultSystemIdPName + } + + if ac.SystemIdentityProvider.UsernameClaim == "" { + ac.SystemIdentityProvider.UsernameClaim = DefaultSystemUsernameClaim + } + + if ac.SystemIdentityProvider.GroupsClaim == "" { + ac.SystemIdentityProvider.GroupsClaim = DefaultSystemGroupsClaim + } + + // CrateIdentityProvider + if ac.CrateIdentityProvider != nil { + if ac.CrateIdentityProvider.Name == "" { + ac.CrateIdentityProvider.Name = DefaultCratedIdPName + } + + if ac.CrateIdentityProvider.ClientID == "" { + ac.CrateIdentityProvider.ClientID = DefaultCrateClientID + } + + if ac.CrateIdentityProvider.UsernameClaim == "" { + ac.CrateIdentityProvider.UsernameClaim = DefaultCrateUsernameClaim + } + } +} + +// Validate validates the authentication configuration. +func Validate(ac *AuthenticationConfig) error { + errs := field.ErrorList{} + errs = append(errs, v1alpha1.ValidateIdp(ac.SystemIdentityProvider, field.NewPath("systemIdentityProvider"))...) + if ac.CrateIdentityProvider != nil { + errs = append(errs, v1alpha1.ValidateIdp(*ac.CrateIdentityProvider, field.NewPath("crateIdentityProvider"))...) + } + return errs.ToAggregate() +} diff --git a/internal/controller/core/authentication/config/config_suite_test.go b/internal/controller/core/authentication/config/config_suite_test.go new file mode 100644 index 0000000..662250b --- /dev/null +++ b/internal/controller/core/authentication/config/config_suite_test.go @@ -0,0 +1,13 @@ +package config_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Authentication Config Test Suite") +} diff --git a/internal/controller/core/authentication/config/config_test.go b/internal/controller/core/authentication/config/config_test.go new file mode 100644 index 0000000..6bb7013 --- /dev/null +++ b/internal/controller/core/authentication/config/config_test.go @@ -0,0 +1,90 @@ +package config_test + +import ( + "errors" + + "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + + authconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + k8serrors "k8s.io/apimachinery/pkg/util/errors" +) + +var _ = Describe("Auth Config", func() { + It("should set defaults", func() { + config := &authconfig.AuthenticationConfig{} + config.CrateIdentityProvider = &v1alpha1.IdentityProvider{} + + config.SetDefaults() + + Expect(config.SystemIdentityProvider.Name).To(Equal(authconfig.DefaultSystemIdPName)) + Expect(config.SystemIdentityProvider.UsernameClaim).To(Equal(authconfig.DefaultSystemUsernameClaim)) + Expect(config.SystemIdentityProvider.GroupsClaim).To(Equal(authconfig.DefaultSystemGroupsClaim)) + + Expect(config.CrateIdentityProvider.Name).To(Equal(authconfig.DefaultCratedIdPName)) + Expect(config.CrateIdentityProvider.ClientID).To(Equal(authconfig.DefaultCrateClientID)) + Expect(config.CrateIdentityProvider.UsernameClaim).To(Equal(authconfig.DefaultCrateUsernameClaim)) + }) + + It("should validate", func() { + config := &authconfig.AuthenticationConfig{} + config.SetDefaults() + + config.SystemIdentityProvider.IssuerURL = "https://openmcp.local" + config.SystemIdentityProvider.ClientID = "aaa-bbb-ccc" + + err := authconfig.Validate(config) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should not validate", func() { + config := &authconfig.AuthenticationConfig{} + + err := authconfig.Validate(config) + Expect(err).To(HaveOccurred()) + + var aggErr k8serrors.Aggregate + Expect(errors.As(err, &aggErr)).To(BeTrue()) + + Expect(aggErr.Errors()).To(HaveLen(2)) + Expect(aggErr.Errors()[0].Error()).To(ContainSubstring("issuerURL")) + Expect(aggErr.Errors()[1].Error()).To(ContainSubstring("clientID")) + }) + + It("should validate crate identity provider", func() { + config := &authconfig.AuthenticationConfig{} + config.SetDefaults() + + config.SystemIdentityProvider.IssuerURL = "https://openmcp.local" + config.SystemIdentityProvider.ClientID = "aaa-bbb-ccc" + + config.CrateIdentityProvider = &v1alpha1.IdentityProvider{} + config.CrateIdentityProvider.IssuerURL = "https://crate.local" + config.CrateIdentityProvider.ClientID = "aaa-bbb-ccc" + + err := authconfig.Validate(config) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should not validate an invalid crate identity provider", func() { + config := &authconfig.AuthenticationConfig{} + config.SetDefaults() + + config.SystemIdentityProvider.IssuerURL = "https://openmcp.local" + config.SystemIdentityProvider.ClientID = "aaa-bbb-ccc" + + config.CrateIdentityProvider = &v1alpha1.IdentityProvider{} + + err := authconfig.Validate(config) + Expect(err).To(HaveOccurred()) + + var aggErr k8serrors.Aggregate + Expect(errors.As(err, &aggErr)).To(BeTrue()) + + Expect(aggErr.Errors()).To(HaveLen(2)) + Expect(aggErr.Errors()[0].Error()).To(ContainSubstring("issuerURL")) + Expect(aggErr.Errors()[1].Error()).To(ContainSubstring("clientID")) + }) +}) diff --git a/internal/controller/core/authentication/config/testdata/config_invalid.yaml b/internal/controller/core/authentication/config/testdata/config_invalid.yaml new file mode 100644 index 0000000..50a269a --- /dev/null +++ b/internal/controller/core/authentication/config/testdata/config_invalid.yaml @@ -0,0 +1 @@ +"invalid" diff --git a/internal/controller/core/authentication/config/testdata/config_valid.yaml b/internal/controller/core/authentication/config/testdata/config_valid.yaml new file mode 100644 index 0000000..4eb4503 --- /dev/null +++ b/internal/controller/core/authentication/config/testdata/config_valid.yaml @@ -0,0 +1,6 @@ +systemIdentityProvider: + name: "system" + issuerURL: "https://system.local" + clientID: "xxx-yyy-zzz" + usernameClaim: "email" + groupsClaim: "groups" diff --git a/internal/controller/core/authentication/config/utils.go b/internal/controller/core/authentication/config/utils.go new file mode 100644 index 0000000..a889b3c --- /dev/null +++ b/internal/controller/core/authentication/config/utils.go @@ -0,0 +1,22 @@ +package config + +import ( + "fmt" + "os" + + "sigs.k8s.io/yaml" +) + +// LoadConfig reads the configuration file from a given path and parses it into an AuthenticationConfig object. +func LoadConfig(path string) (*AuthenticationConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading config file: %w", err) + } + cfg := &AuthenticationConfig{} + err = yaml.Unmarshal(data, cfg) + if err != nil { + return nil, fmt.Errorf("error parsing config file: %w", err) + } + return cfg, nil +} diff --git a/internal/controller/core/authentication/config/utils_test.go b/internal/controller/core/authentication/config/utils_test.go new file mode 100644 index 0000000..d77b2a1 --- /dev/null +++ b/internal/controller/core/authentication/config/utils_test.go @@ -0,0 +1,34 @@ +package config_test + +import ( + "path" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Auth Config Utils", func() { + It("should load the config from file", func() { + authConfig, err := config.LoadConfig(path.Join("testdata", "config_valid.yaml")) + Expect(err).ToNot(HaveOccurred()) + Expect(authConfig).ToNot(BeNil()) + + Expect(authConfig.SystemIdentityProvider.Name).To(Equal("system")) + Expect(authConfig.SystemIdentityProvider.IssuerURL).To(Equal("https://system.local")) + Expect(authConfig.SystemIdentityProvider.ClientID).To(Equal("xxx-yyy-zzz")) + Expect(authConfig.SystemIdentityProvider.UsernameClaim).To(Equal("email")) + Expect(authConfig.SystemIdentityProvider.GroupsClaim).To(Equal("groups")) + }) + + It("should fail to load the config from file", func() { + authConfig, err := config.LoadConfig(path.Join("testdata", "config_invalid.yaml")) + Expect(err).To(HaveOccurred()) + Expect(authConfig).To(BeNil()) + + authConfig, err = config.LoadConfig(path.Join("testdata", "config_missing.yaml")) + Expect(err).To(HaveOccurred()) + Expect(authConfig).To(BeNil()) + }) +}) diff --git a/internal/controller/core/authentication/controller.go b/internal/controller/core/authentication/controller.go new file mode 100644 index 0000000..9744647 --- /dev/null +++ b/internal/controller/core/authentication/controller.go @@ -0,0 +1,535 @@ +package authentication + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/openmcp-project/mcp-operator/internal/utils" + "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" + "github.com/openmcp-project/mcp-operator/internal/utils/components" + + apiserverutils "github.com/openmcp-project/mcp-operator/internal/controller/core/apiserver/utils" + "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication/config" + + "github.com/openmcp-project/controller-utils/pkg/logging" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/client-go/tools/clientcmd" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +// ControllerName is the name of the controller +const ControllerName = "Authentication" + +// openIdConnectGVK is the GroupVersionKind for Gardener OpenIDConnect resources +var openIdConnectGVK = schema.GroupVersionKind{ + Group: "authentication.gardener.cloud", + Kind: "OpenIDConnect", + Version: "v1alpha1", +} + +// The AuthenticationReconciler reconciles Authentication resources +type AuthenticationReconciler struct { + Client client.Client + Config *config.AuthenticationConfig + APIServerAccess apiserver.APIServerAccess +} + +// NewAuthenticationReconciler creates a new AuthenticationReconciler +func NewAuthenticationReconciler(c client.Client, config *config.AuthenticationConfig) *AuthenticationReconciler { + config.SetDefaults() + return &AuthenticationReconciler{ + Client: c, + Config: config, + APIServerAccess: &apiserver.APIServerAccessImpl{ + NewClient: client.New, + }, + } +} + +// SetAPIServerAccess sets the APIServerAccess implementation. +// Used for testing. +func (ar *AuthenticationReconciler) SetAPIServerAccess(apiServerAccess apiserver.APIServerAccess) { + ar.APIServerAccess = apiServerAccess +} + +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=authentications,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=authentications/status,verbs=get;update;patch + +// Reconcile reconciles authentications and updates Gardener OpenIDConnect resources +func (ar *AuthenticationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log, ctx := utils.InitializeControllerLogger(ctx, ControllerName) + log.Debug(cconst.MsgStartReconcile) + + rr := ar.reconcile(ctx, req) + rr.LogRequeue(log, logging.DEBUG) + if rr.Component == nil { + return rr.Result, rr.ReconcileError + } + if rr.ReconcileError != nil && len(rr.Conditions) == 0 { // shortcut so we don't have to add the same ready condition to each return statement (won't work anymore if we have multiple conditions) + rr.Conditions = authenticationConditions(false, cconst.ReasonReconciliationError, cconst.MessageReconciliationError) + } + return components.UpdateStatus(ctx, ar.Client, rr) +} + +func (ar *AuthenticationReconciler) reconcile(ctx context.Context, req ctrl.Request) components.ReconcileResult[*openmcpv1alpha1.Authentication] { + // get the logger + log := logging.FromContextOrPanic(ctx) + + auth := &openmcpv1alpha1.Authentication{} + if err := ar.Client.Get(ctx, req.NamespacedName, auth); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("Resource not found") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{} + } + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("unable to get resource '%s' from cluster: %w", req.NamespacedName.String(), err), cconst.ReasonCrateClusterInteractionProblem)} + } + + // handle operation annotation + if auth.GetAnnotations() != nil { + op, ok := auth.GetAnnotations()[openmcpv1alpha1.OperationAnnotation] + if ok { + switch op { + case openmcpv1alpha1.OperationAnnotationValueIgnore: + log.Info("Ignoring resource due to ignore operation annotation") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{} + case openmcpv1alpha1.OperationAnnotationValueReconcile: + log.Debug("Removing reconcile operation annotation from resource") + if err := components.PatchAnnotation(ctx, ar.Client, auth, openmcpv1alpha1.OperationAnnotation, "", components.ANNOTATION_DELETE); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing operation annotation: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + } + } + + // checking for APIServer component + log.Debug("Checking for APIServer dependency") + ownCPGeneration, ownICGeneration, _ := components.GetCreatedFromGeneration(auth) + as := &openmcpv1alpha1.APIServer{} + as.SetName(auth.Name) + as.SetNamespace(auth.Namespace) + + if err := ar.Client.Get(ctx, client.ObjectKeyFromObject(as), as); err != nil { + if !apierrors.IsNotFound(err) { + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error fetching APIServer resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + // APIServer not found + as = nil + } + if as == nil || !components.IsDependencyReady(as, ownCPGeneration, ownICGeneration) { + log.Info("APIServer not found or it isn't ready") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, Conditions: authenticationConditions(false, cconst.ReasonWaitingForDependencies, "Waiting for APIServer dependency to be ready."), Result: ctrl.Result{RequeueAfter: 60 * time.Second}} + } + + log.Debug("APIServer dependency is ready") + + if as.Spec.Type != openmcpv1alpha1.Gardener && as.Spec.Type != openmcpv1alpha1.GardenerDedicated { + log.Info("APIServer is not of type Gardener/GardenerDedicated") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("APIServer dependency is ready, but the APIServer type is not supported"), cconst.ReasonInvalidAPIServerType)} + } + + if as.Status.AdminAccess == nil || as.Status.AdminAccess.Kubeconfig == "" { + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("APIServer dependency is ready, but no kubeconfig could be found in its status"), cconst.ReasonDependencyStatusInvalid)} + } + + apiServerClient, err := ar.APIServerAccess.GetAdminAccessClient(as, client.Options{}) + if err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error creating client from APIServer kubeconfig: %w", err), cconst.ReasonDependencyStatusInvalid)} + } + + deleteAuthentication := false + if !auth.DeletionTimestamp.IsZero() { + log.Info("Deleting Authentication") + if components.HasAnyDependencyFinalizer(auth) { + depString := strings.Join(sets.List(components.GetDependents(auth)), ", ") + log.Info("Authentication cannot be deleted, because it still contains dependency finalizers", "dependingComponents", depString) + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, Conditions: authenticationConditions(true, cconst.ReasonDeletionWaitingForDependingComponents, fmt.Sprintf("Deletion is waiting for the following dependencies to be removed: [%s]", depString)), Result: ctrl.Result{RequeueAfter: 60 * time.Second}} + } + deleteAuthentication = true + } else { + log.Info("Triggering creation/update of Authentication") + + old := auth.DeepCopy() + if controllerutil.AddFinalizer(auth, openmcpv1alpha1.AuthenticationComponent.Finalizer()) { + log.Debug("Adding finalizer to Authentication resource") + if err := ar.Client.Patch(ctx, auth, client.MergeFrom(old)); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error patching finalizer on Authentication: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + + log.Debug("Ensuring dependency finalizer on APIServer resource") + err := components.EnsureDependencyFinalizer(ctx, ar.Client, as, auth, true) + if err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error setting dependency finalizer on APIServer component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + + old := auth.DeepCopy() + if deleteAuthentication { + // delete all OpenIDConnect resources if the authentication resource is being deleted + if err = ar.deleteOpenIDConnectResources(ctx, apiServerClient, []openmcpv1alpha1.IdentityProvider{}); err != nil { + log.Error(err, "failed to delete all OpenIDConnect resources") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error deleting OpenIDConnect resources: %w", err), cconst.ReasonManagingOpenIDConnect)} + } + + // delete the access secret if the authentication resource is being deleted + if err = ar.ensureAccessSecret(ctx, false, []openmcpv1alpha1.IdentityProvider{}, auth, as); err != nil { + log.Error(err, "failed to delete access secret") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error deleting access secret: %w", err), cconst.ReasonManagingOpenIDConnect)} + } + + // remove the auth dependency finalizer from the APIServer resource if the auth resource is being deleted + err = components.EnsureDependencyFinalizer(ctx, ar.Client, as, auth, false) + if err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing dependency finalizer from APIServer component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + + // remove finalizer from auth resource + old := auth.DeepCopy() + changed := controllerutil.RemoveFinalizer(auth, openmcpv1alpha1.AuthenticationComponent.Finalizer()) + if changed { + if err := ar.Client.Patch(ctx, auth, client.MergeFrom(old)); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing finalizer from Authentication: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + + } else { + // build the list of currently enabled identity providers + var enabledIdentityProviders []openmcpv1alpha1.IdentityProvider + var accessSecretIdentityProviders []openmcpv1alpha1.IdentityProvider + + // add system identity provider if enabled + if auth.IsSystemIdentityProviderEnabled() { + enabledIdentityProviders = append(enabledIdentityProviders, ar.Config.SystemIdentityProvider) + accessSecretIdentityProviders = append(accessSecretIdentityProviders, ar.Config.SystemIdentityProvider) + } + + if ar.Config.CrateIdentityProvider != nil { + // add crate identity provider if enabled + // the crate identity provider is not added to the access secret + enabledIdentityProviders = append(enabledIdentityProviders, *ar.Config.CrateIdentityProvider) + } + + // add all other identity providers enabled in the control plane + enabledIdentityProviders = append(enabledIdentityProviders, auth.Spec.IdentityProviders...) + accessSecretIdentityProviders = append(accessSecretIdentityProviders, auth.Spec.IdentityProviders...) + + // create/update all OpenIDConnect resources that are in the list of enabled identity providers + if len(enabledIdentityProviders) > 0 { + if err = ar.createOrOpenIDConnectResources(ctx, apiServerClient, enabledIdentityProviders); err != nil { + log.Error(err, "failed to create or update OpenIDConnect resources") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error creating or updating OpenIDConnect resources: %w", err), cconst.ReasonManagingOpenIDConnect)} + } + } + + // delete all OpenIDConnect resources that are not in the list of enabled identity providers + if err = ar.deleteOpenIDConnectResources(ctx, apiServerClient, enabledIdentityProviders); err != nil { + log.Error(err, "failed to delete OpenIDConnect resources") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error deleting OpenIDConnect resources: %w", err), cconst.ReasonManagingOpenIDConnect)} + } + + // create or update the access secret + if err = ar.ensureAccessSecret(ctx, true, accessSecretIdentityProviders, auth, as); err != nil { + log.Error(err, "failed to ensure access secret") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error ensuring access secret: %w", err), cconst.ReasonManagingOpenIDConnect)} + } + + // update the external status of the Authentication resource + if err = ar.updateExternalStatus(ctx, auth); err != nil { + log.Error(err, "failed to update external status") + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{OldComponent: old, Component: auth, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error updating external status: %w", err), cconst.ReasonManagingOpenIDConnect)} + } + } + + return components.ReconcileResult[*openmcpv1alpha1.Authentication]{OldComponent: old, Component: auth, Conditions: authenticationConditions(true, "", "")} +} + +// create or updates a list of Gardener OpenIDConnect resources for the given control plane +func (ar *AuthenticationReconciler) createOrOpenIDConnectResources(ctx context.Context, apiServerClient client.Client, enabledIdentityProviders []openmcpv1alpha1.IdentityProvider) error { + log, ctx := logging.FromContextOrNew(ctx, []interface{}{cconst.KeyMethod, "createOrOpenIDConnectResources"}) + + initializeOpenIDConnect := func(oidc *unstructured.Unstructured, name string) { + oidc.SetGroupVersionKind(openIdConnectGVK) + oidc.SetName(name) + } + + // check if the enabled identity providers contain duplicate names + identityProviderNames := make(map[string]struct{}) + + for _, idp := range enabledIdentityProviders { + if _, ok := identityProviderNames[idp.Name]; ok { + return fmt.Errorf("duplicate identity provider name: %s", idp.Name) + } + + identityProviderNames[idp.Name] = struct{}{} + + oidc := &unstructured.Unstructured{} + initializeOpenIDConnect(oidc, idp.Name) + + operationResult, err := controllerutil.CreateOrUpdate(ctx, apiServerClient, oidc, func() error { + isSystemIdentityProvider := idp.Name == ar.Config.SystemIdentityProvider.Name + isCrateIdentityProvider := ar.Config.CrateIdentityProvider != nil && idp.Name == ar.Config.CrateIdentityProvider.Name + + var idpTypeLabel string + + if isSystemIdentityProvider { + idpTypeLabel = "system" + } else if isCrateIdentityProvider { + idpTypeLabel = "crate" + } else { + idpTypeLabel = "user" + } + + labels := oidc.GetLabels() + + if labels == nil { + labels = map[string]string{} + } + + labels[openmcpv1alpha1.ManagedByLabel] = ControllerName + labels[openmcpv1alpha1.BaseDomain+"/idp-type"] = idpTypeLabel + + oidc.SetLabels(labels) + + oidc.Object["spec"] = map[string]interface{}{ + "issuerURL": idp.IssuerURL, + "clientID": idp.ClientID, + "usernameClaim": idp.UsernameClaim, + "groupsClaim": idp.GroupsClaim, + "usernamePrefix": idp.Name + ":", + "groupsPrefix": idp.Name + ":", + } + + spec := oidc.Object["spec"].(map[string]interface{}) + + if len(idp.CABundle) > 0 { + spec["caBundle"] = idp.CABundle + } + + if len(idp.SigningAlgs) > 0 { + spec["signingAlgs"] = make([]interface{}, len(idp.SigningAlgs)) + + for i, alg := range idp.SigningAlgs { + spec["signingAlgs"].([]interface{})[i] = alg + } + } + + if len(idp.RequiredClaims) > 0 { + spec["requiredClaims"] = make(map[string]interface{}) + + for key, value := range idp.RequiredClaims { + spec["requiredClaims"].(map[string]interface{})[key] = value + } + } + + return nil + }) + + if err != nil { + return fmt.Errorf("failed to create or update Gardener OpenIDConnect resource: %w", err) + } + + log.Debug("OpenIDConnect resource created or updated", cconst.KeyResource, idp.Name, "operationResult", operationResult) + } + + return nil +} + +// deleteOpenIDConnectResources deletes all Gardener OpenIDConnect resources that are not in the list of enabled identity providers +func (ar *AuthenticationReconciler) deleteOpenIDConnectResources(ctx context.Context, apiServerClient client.Client, enabledIdentityProviders []openmcpv1alpha1.IdentityProvider) error { + log, ctx := logging.FromContextOrNew(ctx, []interface{}{cconst.KeyMethod, "deleteOpenIDConnectResources"}) + + // list all openIDConnect resources with label managed by this controller + oidcList := &unstructured.UnstructuredList{} + oidcList.SetGroupVersionKind(openIdConnectGVK) + + err := apiServerClient.List(ctx, oidcList, client.MatchingLabels{openmcpv1alpha1.ManagedByLabel: ControllerName}) + if err != nil { + return fmt.Errorf("failed to list Gardener OpenIDConnect resources: %w", err) + } + + containsIdentityProvider := func(idps []openmcpv1alpha1.IdentityProvider, name string) bool { + for _, idp := range idps { + if idp.Name == name { + return true + } + } + return false + } + + // Compare the oidc list with the enabled identity providers. + // If an oidc resource is not in the enabled identity providers, delete it. + for _, oidc := range oidcList.Items { + if !containsIdentityProvider(enabledIdentityProviders, oidc.GetName()) { + if err = apiServerClient.Delete(ctx, &oidc); err != nil { + return fmt.Errorf("failed to delete Gardener OpenIDConnect resource: %w", err) + } + + log.Debug("OpenIDConnect resource deleted", cconst.KeyResource, oidc.GetName()) + } + } + + return nil +} + +// ensureAccessSecret ensures that the access secret exists or does not exist (based on argument 'expected') +// and that the finalizer is removed from the secret if it exists. +// If the access secret is created or updated, it is populated with the kubeconfig containing the OIDC configuration for the user access to the APIServer cluster. +func (ar *AuthenticationReconciler) ensureAccessSecret(ctx context.Context, expected bool, enabledIdentityProviders []openmcpv1alpha1.IdentityProvider, auth *openmcpv1alpha1.Authentication, as *openmcpv1alpha1.APIServer) error { + log, ctx := logging.FromContextOrNew(ctx, []interface{}{cconst.KeyMethod, "ensureAccessSecret"}) + + accessSecret := getSecretAccessor(auth) + + if !expected { + // get the secret if it exists + if err := ar.Client.Get(ctx, client.ObjectKeyFromObject(accessSecret), accessSecret); err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("error getting access secret: %w", err) + } + } + + // remove the finalizer from the secret + if components.HasAnyDependencyFinalizer(accessSecret) { + accessSecret.Finalizers = sets.NewString(accessSecret.Finalizers...).Delete(openmcpv1alpha1.AuthenticationComponent.DependencyFinalizer()).List() + + if err := ar.Client.Update(ctx, accessSecret); err != nil { + return fmt.Errorf("error removing finalizer from access secret: %w", err) + } + } + + log.Debug("deleting auth access secret", cconst.KeyResource, client.ObjectKeyFromObject(accessSecret).String()) + + // delete the secret if it exists + if err := ar.Client.Delete(ctx, accessSecret); err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("error deleting access secret: %w", err) + } + } + + return nil + } + + // check if the access secret exists + if as.Status.AdminAccess == nil { + return fmt.Errorf("admin access is not available in APIServer status") + } + + // create or update the secret + defaultIDP := "" + + if len(auth.Spec.IdentityProviders) > 0 { + defaultIDP = auth.Spec.IdentityProviders[0].Name + } else if auth.IsSystemIdentityProviderEnabled() { + defaultIDP = ar.Config.SystemIdentityProvider.Name + } + + var oidcKubeconfig []byte + + if len(defaultIDP) >= 0 { + restConfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(as.Status.AdminAccess.Kubeconfig)) + if err != nil { + return fmt.Errorf("error creating REST config from kubeconfig: %w", err) + } + + oidcKubeconfig, err = apiserverutils.CreateOIDCKubeconfig(ctx, ar.Client, as.Name, as.Namespace, restConfig.Host, defaultIDP, restConfig.CAData, enabledIdentityProviders) + if err != nil { + return fmt.Errorf("error creating OIDC kubeconfig: %w", err) + } + } + + result, err := controllerutil.CreateOrUpdate(ctx, ar.Client, accessSecret, func() error { + if len(accessSecret.Finalizers) == 0 { + accessSecret.Finalizers = make([]string, 0) + } + + accessSecret.Finalizers = sets.NewString(accessSecret.Finalizers...).Insert(openmcpv1alpha1.AuthenticationComponent.DependencyFinalizer()).List() + accessSecret.Data = map[string][]byte{} + + if len(oidcKubeconfig) > 0 { + accessSecret.StringData = map[string]string{ + kubeconfigSecretValueKey: string(oidcKubeconfig), + } + } + return nil + }) + + if err != nil { + return fmt.Errorf("error creating or updating secret: %w", err) + } + + log.Debug("access secret created or updated", cconst.KeyResource, client.ObjectKeyFromObject(accessSecret).String(), "result", result) + + return nil +} + +// updateExternalStatus updates the external status of the Authentication resource +// by creating a kubeconfig for the user access to the APIServer +func (ar *AuthenticationReconciler) updateExternalStatus(ctx context.Context, auth *openmcpv1alpha1.Authentication) error { + if auth.Status.ExternalAuthenticationStatus == nil { + auth.Status.ExternalAuthenticationStatus = &openmcpv1alpha1.ExternalAuthenticationStatus{} + } + + // try to get access secret + accessSecret := getSecretAccessor(auth) + + secretExists := true + if err := ar.Client.Get(ctx, client.ObjectKeyFromObject(accessSecret), accessSecret); err != nil { + if !apierrors.IsNotFound(err) { + return fmt.Errorf("error getting access secret: %w", err) + } + secretExists = false + } + + if secretExists { + auth.Status.UserAccess = &openmcpv1alpha1.SecretReference{ + NamespacedObjectReference: openmcpv1alpha1.NamespacedObjectReference{ + Name: accessSecret.Name, + Namespace: accessSecret.Namespace, + }, + Key: kubeconfigSecretValueKey, + } + } else { + auth.Status.UserAccess = nil + } + + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (ar *AuthenticationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&openmcpv1alpha1.Authentication{}, builder.WithPredicates(components.DefaultComponentControllerPredicates())). + Watches(&openmcpv1alpha1.APIServer{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(components.StatusChangedPredicate{})). + Complete(ar) +} + +const kubeconfigSecretValueKey = "kubeconfig" + +func getSecretAccessor(auth *openmcpv1alpha1.Authentication) *corev1.Secret { + return &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s.%s", auth.Name, kubeconfigSecretValueKey), + Namespace: auth.Namespace, + }, + } +} + +func authenticationConditions(ready bool, reason, message string) []openmcpv1alpha1.ComponentCondition { + return []openmcpv1alpha1.ComponentCondition{ + components.NewCondition(openmcpv1alpha1.AuthenticationComponent.HealthyCondition(), openmcpv1alpha1.ComponentConditionStatusFromBool(ready), reason, message), + } +} diff --git a/internal/controller/core/authentication/controller_test.go b/internal/controller/core/authentication/controller_test.go new file mode 100644 index 0000000..88aac05 --- /dev/null +++ b/internal/controller/core/authentication/controller_test.go @@ -0,0 +1,663 @@ +package authentication_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + "github.com/openmcp-project/mcp-operator/internal/components" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication" + "github.com/openmcp-project/mcp-operator/internal/controller/core/authentication/config" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +var ( + systemIdentityProvider = openmcpv1alpha1.IdentityProvider{ + Name: "openmcp", + IssuerURL: "https://issuer.local", + ClientID: "aaa-bbb-ccc", + UsernameClaim: "email", + GroupsClaim: "groups", + } + + crateIdentityProvider = openmcpv1alpha1.IdentityProvider{ + Name: "crate", + IssuerURL: "https://crate.local", + ClientID: "mcp", + UsernameClaim: "sub", + } +) + +func getReconciler(c ...client.Client) reconcile.Reconciler { + return authentication.NewAuthenticationReconciler(c[0], &config.AuthenticationConfig{ + SystemIdentityProvider: systemIdentityProvider, + }) +} + +func getReconcilerWithCrateIdentityProvider(c ...client.Client) reconcile.Reconciler { + return authentication.NewAuthenticationReconciler(c[0], &config.AuthenticationConfig{ + SystemIdentityProvider: systemIdentityProvider, + CrateIdentityProvider: &crateIdentityProvider, + }) +} + +func getOpenIDConnect() *unstructured.Unstructured { + openIdConnect := &unstructured.Unstructured{} + openIdConnect.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "authentication.gardener.cloud", + Version: "v1alpha1", + Kind: "OpenIDConnect", + }) + return openIdConnect +} + +func getOpenIDConnectList() *unstructured.UnstructuredList { + openIdConnectList := &unstructured.UnstructuredList{} + openIdConnectList.SetGroupVersionKind(schema.GroupVersionKind{ + Group: "authentication.gardener.cloud", + Version: "v1alpha1", + Kind: "OpenIDConnectList", + }) + return openIdConnectList +} + +const ( + authReconciler = "auth" +) + +func testEnvWithAPIServerAccess(testDataPathSegments ...string) *testing.ComplexEnvironment { + env := testutils.DefaultTestSetupBuilder(testDataPathSegments...).WithFakeClient(testutils.APIServerCluster, testutils.Scheme).WithReconcilerConstructor(authReconciler, getReconciler, testutils.CrateCluster).Build() + env.Reconcilers[authReconciler].(*authentication.AuthenticationReconciler).SetAPIServerAccess(&testutils.TestAPIServerAccess{Client: env.Client(testutils.APIServerCluster)}) + return env +} + +func testEnvWithAPIServerAccessWithCrateIdentityProvider(testDataPathSegments ...string) *testing.ComplexEnvironment { + env := testutils.DefaultTestSetupBuilder(testDataPathSegments...).WithFakeClient(testutils.APIServerCluster, testutils.Scheme).WithReconcilerConstructor(authReconciler, getReconcilerWithCrateIdentityProvider, testutils.CrateCluster).Build() + env.Reconcilers[authReconciler].(*authentication.AuthenticationReconciler).SetAPIServerAccess(&testutils.TestAPIServerAccess{Client: env.Client(testutils.APIServerCluster)}) + return env +} + +var _ = Describe("CO-1153 Authentication Controller", func() { + It("should set the ready condition to false when there is no APIServer available", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-01") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + res := env.ShouldReconcile(authReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + + Expect(auth.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForDependencies, + }), + )) + }) + + It("should set the status condition to false when APIServer is not ready", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-02") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + res := env.ShouldReconcile(authReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + + Expect(auth.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForDependencies, + }), + )) + }) + + It("should fail to reconcile and set the status condition to false when APIServer has an unsupported type", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-03") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldNotReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + + Expect(auth.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + }), + )) + }) + + It("should fail to reconcile and set the status condition to false when APIServer status has no access kubeconfig", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-04") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldNotReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + + Expect(auth.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + }), + )) + }) + + It("should create an OpenIDConnect resource on the APIServer and an access secret", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-05") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + Expect(auth.Finalizers).To(ContainElements(openmcpv1alpha1.AuthenticationComponent.Finalizer())) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + authComp := components.Component(auth) + Expect(as.Finalizers).To(ContainElements(authComp.Type().DependencyFinalizer())) + + Expect(auth.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + Expect(auth.Status.ExternalAuthenticationStatus).NotTo(BeNil()) + Expect(auth.Status.ExternalAuthenticationStatus.UserAccess).ToNot(BeNil()) + Expect(auth.Status.ExternalAuthenticationStatus.UserAccess.Key).To(Equal("kubeconfig")) + Expect(auth.Status.ExternalAuthenticationStatus.UserAccess.Name).To(Equal("test.kubeconfig")) + Expect(auth.Status.ExternalAuthenticationStatus.UserAccess.Namespace).To(Equal("test")) + + accessSecret := &corev1.Secret{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test.kubeconfig", Namespace: "test"}, accessSecret) + Expect(err).NotTo(HaveOccurred()) + + Expect(accessSecret.StringData).To(HaveKey("kubeconfig")) + config, err := clientcmd.NewClientConfigFromBytes([]byte(accessSecret.StringData["kubeconfig"])) + Expect(err).NotTo(HaveOccurred()) + + rawConfig, err := config.RawConfig() + Expect(err).NotTo(HaveOccurred()) + + Expect(rawConfig.AuthInfos).To(HaveKey(systemIdentityProvider.Name)) + + systemIdP := rawConfig.AuthInfos[systemIdentityProvider.Name] + Expect(systemIdP.Exec).ToNot(BeNil()) + Expect(systemIdP.Exec.Command).To(Equal("kubectl")) + Expect(systemIdP.Exec.Args).To(ContainElements("oidc-login")) + Expect(systemIdP.Exec.Args).To(ContainElements("get-token")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-issuer-url=" + systemIdentityProvider.IssuerURL)) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-client-id=" + systemIdentityProvider.ClientID)) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-extra-scope=email")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-extra-scope=profile")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-extra-scope=offline_access")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-use-pkce")) + Expect(systemIdP.Exec.Args).To(ContainElements("--grant-type=auto")) + + openIdConnect := getOpenIDConnect() + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: systemIdentityProvider.Name}, openIdConnect) + Expect(err).NotTo(HaveOccurred()) + + Expect(openIdConnect.Object).To(HaveKey("spec")) + spec := openIdConnect.Object["spec"] + Expect(spec).To(HaveKeyWithValue("issuerURL", systemIdentityProvider.IssuerURL)) + Expect(spec).To(HaveKeyWithValue("clientID", systemIdentityProvider.ClientID)) + Expect(spec).To(HaveKeyWithValue("usernameClaim", systemIdentityProvider.UsernameClaim)) + Expect(spec).To(HaveKeyWithValue("groupsClaim", systemIdentityProvider.GroupsClaim)) + Expect(spec).To(HaveKeyWithValue("usernamePrefix", systemIdentityProvider.Name+":")) + Expect(spec).To(HaveKeyWithValue("groupsPrefix", systemIdentityProvider.Name+":")) + + Expect(rawConfig.AuthInfos).To(HaveKey("customer")) + customerIdP := rawConfig.AuthInfos["customer"] + Expect(customerIdP.Exec.Args).To(ContainElements("--oidc-issuer-url=https://customer.local")) + Expect(customerIdP.Exec.Args).To(ContainElements("--oidc-client-id=xxx-yyy-zzz")) + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: "customer"}, openIdConnect) + Expect(err).NotTo(HaveOccurred()) + + Expect(openIdConnect.Object).To(HaveKey("spec")) + spec = openIdConnect.Object["spec"] + Expect(spec).To(HaveKeyWithValue("issuerURL", "https://customer.local")) + Expect(spec).To(HaveKeyWithValue("clientID", "xxx-yyy-zzz")) + Expect(spec).To(HaveKeyWithValue("usernameClaim", "u_name")) + Expect(spec).To(HaveKeyWithValue("groupsClaim", "grp")) + Expect(spec).To(HaveKeyWithValue("usernamePrefix", "customer:")) + Expect(spec).To(HaveKeyWithValue("groupsPrefix", "customer:")) + }) + + It("should update/delete OpenIDConnect resource for spec updates", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-06") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + + openIDConnectList := getOpenIDConnectList() + err = env.Client(testutils.APIServerCluster).List(env.Ctx, openIDConnectList) + Expect(err).NotTo(HaveOccurred()) + + Expect(openIDConnectList.Items).To(HaveLen(3)) + + // remove last element from auth spec identity providers + auth.Spec.IdentityProviders = auth.Spec.IdentityProviders[:len(auth.Spec.IdentityProviders)-1] + + err = env.Client(testutils.CrateCluster).Update(env.Ctx, auth) + Expect(err).NotTo(HaveOccurred()) + + _ = env.ShouldReconcile(authReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth)).To(Succeed()) + + err = env.Client(testutils.APIServerCluster).List(env.Ctx, openIDConnectList) + Expect(err).NotTo(HaveOccurred()) + Expect(openIDConnectList.Items).To(HaveLen(2)) + + auth.Spec.EnableSystemIdentityProvider = ptr.To(false) + err = env.Client(testutils.CrateCluster).Update(env.Ctx, auth) + Expect(err).NotTo(HaveOccurred()) + + _ = env.ShouldReconcile(authReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth)).To(Succeed()) + + err = env.Client(testutils.APIServerCluster).List(env.Ctx, openIDConnectList) + Expect(err).NotTo(HaveOccurred()) + Expect(openIDConnectList.Items).To(HaveLen(1)) + + auth.Spec.IdentityProviders = append(auth.Spec.IdentityProviders, openmcpv1alpha1.IdentityProvider{ + Name: "new", + IssuerURL: "https://new.local", + ClientID: "new-client-id", + UsernameClaim: "new-username", + GroupsClaim: "new-groups", + }) + + err = env.Client(testutils.CrateCluster).Update(env.Ctx, auth) + Expect(err).NotTo(HaveOccurred()) + + _ = env.ShouldReconcile(authReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth)).To(Succeed()) + + err = env.Client(testutils.APIServerCluster).List(env.Ctx, openIDConnectList) + Expect(err).NotTo(HaveOccurred()) + Expect(openIDConnectList.Items).To(HaveLen(2)) + + accessSecret := &corev1.Secret{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test.kubeconfig", Namespace: "test"}, accessSecret) + Expect(err).NotTo(HaveOccurred()) + + Expect(accessSecret.StringData).To(HaveKey("kubeconfig")) + config, err := clientcmd.NewClientConfigFromBytes([]byte(accessSecret.StringData["kubeconfig"])) + Expect(err).NotTo(HaveOccurred()) + + rawConfig, err := config.RawConfig() + Expect(err).NotTo(HaveOccurred()) + + Expect(rawConfig.AuthInfos).To(HaveLen(2)) + Expect(rawConfig.AuthInfos).To(HaveKey("new")) + Expect(rawConfig.AuthInfos).To(HaveKey("customer")) + }) + + It("should delete authentication", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-07") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + authComp := components.Component(auth) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + + openIDConnectList := getOpenIDConnectList() + err = env.Client(testutils.APIServerCluster).List(env.Ctx, openIDConnectList) + Expect(err).NotTo(HaveOccurred()) + Expect(openIDConnectList.Items).To(HaveLen(2)) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, auth) + Expect(err).NotTo(HaveOccurred()) + + req = testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.APIServerCluster).List(env.Ctx, openIDConnectList) + Expect(err).NotTo(HaveOccurred()) + Expect(openIDConnectList.Items).To(HaveLen(0)) + + accessSecret := &corev1.Secret{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test.kubeconfig", Namespace: "test"}, accessSecret) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + Expect(as.Finalizers).ToNot(ContainElement(authComp.Type().DependencyFinalizer())) + }) + + It("reconcile should handle when authentication is not found", func() { + env := testEnvWithAPIServerAccess() + + env.ShouldReconcile(authReconciler, testing.RequestFromObject(&openmcpv1alpha1.Authentication{ + ObjectMeta: v1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + })) + }) + + It("should handle the ignore annotation", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-08") + apiServerAccess := &testutils.TestAPIServerAccess{Client: env.Client(testutils.APIServerCluster)} + env.Reconcilers[authReconciler].(*authentication.AuthenticationReconciler).SetAPIServerAccess(apiServerAccess) + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + Expect(auth.Status.Conditions).To(HaveLen(0)) + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: systemIdentityProvider.Name}, getOpenIDConnect()) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) + + It("should handle the reconcile annotation", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-09") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + Expect(auth.Annotations).ToNot(HaveKeyWithValue(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile)) + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: systemIdentityProvider.Name}, getOpenIDConnect()) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should not be deleted when it has a dependency finalizer", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-10") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, auth) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + Expect(auth.Finalizers).To(ContainElement(openmcpv1alpha1.AuthenticationComponent.Finalizer())) + Expect(auth.Finalizers).To(ContainElement("dependency." + openmcpv1alpha1.BaseDomain + "/other_comp")) + }) + + It("should create a client kubeconfig with client secret and own extra scopes", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-11") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + Expect(auth.Finalizers).To(ContainElements(openmcpv1alpha1.AuthenticationComponent.Finalizer())) + + Expect(auth.Status.ExternalAuthenticationStatus).NotTo(BeNil()) + Expect(auth.Status.ExternalAuthenticationStatus.UserAccess).ToNot(BeNil()) + Expect(auth.Status.ExternalAuthenticationStatus.UserAccess.Key).To(Equal("kubeconfig")) + Expect(auth.Status.ExternalAuthenticationStatus.UserAccess.Name).To(Equal("test.kubeconfig")) + Expect(auth.Status.ExternalAuthenticationStatus.UserAccess.Namespace).To(Equal("test")) + + accessSecret := &corev1.Secret{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test.kubeconfig", Namespace: "test"}, accessSecret) + Expect(err).NotTo(HaveOccurred()) + + Expect(accessSecret.StringData).To(HaveKey("kubeconfig")) + config, err := clientcmd.NewClientConfigFromBytes([]byte(accessSecret.StringData["kubeconfig"])) + Expect(err).NotTo(HaveOccurred()) + + rawConfig, err := config.RawConfig() + Expect(err).NotTo(HaveOccurred()) + + Expect(rawConfig.AuthInfos).To(HaveKey("customer")) + + systemIdP := rawConfig.AuthInfos["customer"] + Expect(systemIdP.Exec).ToNot(BeNil()) + Expect(systemIdP.Exec.Command).To(Equal("kubectl")) + Expect(systemIdP.Exec.Args).To(ContainElements("oidc-login")) + Expect(systemIdP.Exec.Args).To(ContainElements("get-token")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-issuer-url=https://customer.local")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-client-id=xxx-yyy-zzz")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-client-secret=myclientsecret")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-extra-scope=scope1")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-extra-scope=scope2")) + Expect(systemIdP.Exec.Args).To(ContainElements("--oidc-use-pkce")) + Expect(systemIdP.Exec.Args).To(ContainElements("--grant-type=auto")) + Expect(systemIdP.Exec.Args).To(ContainElements("--extra-param=foo")) + Expect(systemIdP.Exec.Args).To(ContainElements("--extra-repeatable=bar1")) + Expect(systemIdP.Exec.Args).To(ContainElements("--extra-repeatable=bar2")) + Expect(systemIdP.Exec.Args).To(ContainElements("--no-value-param")) + + openIdConnect := getOpenIDConnect() + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: "customer"}, openIdConnect) + Expect(err).NotTo(HaveOccurred()) + + Expect(openIdConnect.Object).To(HaveKey("spec")) + spec := openIdConnect.Object["spec"] + Expect(spec).To(HaveKeyWithValue("issuerURL", "https://customer.local")) + Expect(spec).To(HaveKeyWithValue("clientID", "xxx-yyy-zzz")) + Expect(spec).To(HaveKeyWithValue("usernameClaim", "u_name")) + Expect(spec).To(HaveKeyWithValue("groupsClaim", "grp")) + Expect(spec).To(HaveKeyWithValue("usernamePrefix", "customer:")) + Expect(spec).To(HaveKeyWithValue("groupsPrefix", "customer:")) + Expect(spec).To(HaveKeyWithValue("caBundle", "-----BEGIN CERTIFICATE-----\nLi4u\n-----END CERTIFICATE-----\n")) + Expect(spec).To(HaveKeyWithValue("signingAlgs", []interface{}{"RS256", "RS384", "RS512"})) + Expect(spec).To(HaveKeyWithValue("requiredClaims", map[string]interface{}{"myClaimKey": "myClaimValue"})) + }) + + It("should create the crate openid connect resource with the correct values", func() { + var err error + + env := testEnvWithAPIServerAccessWithCrateIdentityProvider("testdata", "test-05") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + Expect(auth.Finalizers).To(ContainElements(openmcpv1alpha1.AuthenticationComponent.Finalizer())) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + authComp := components.Component(auth) + Expect(as.Finalizers).To(ContainElements(authComp.Type().DependencyFinalizer())) + + Expect(auth.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + openIdConnect := getOpenIDConnect() + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: crateIdentityProvider.Name}, openIdConnect) + Expect(err).NotTo(HaveOccurred()) + + Expect(openIdConnect.Object).To(HaveKey("spec")) + spec := openIdConnect.Object["spec"] + Expect(spec).To(HaveKeyWithValue("issuerURL", crateIdentityProvider.IssuerURL)) + Expect(spec).To(HaveKeyWithValue("clientID", crateIdentityProvider.ClientID)) + Expect(spec).To(HaveKeyWithValue("usernameClaim", crateIdentityProvider.UsernameClaim)) + Expect(spec).To(HaveKeyWithValue("usernamePrefix", crateIdentityProvider.Name+":")) + + // the crate identity provider should not be contained in the user access secret + Expect(auth.Status.UserAccess).ToNot(BeNil()) + + accessSecret := &corev1.Secret{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: auth.Status.UserAccess.Name, Namespace: auth.Status.UserAccess.Namespace}, accessSecret) + Expect(err).NotTo(HaveOccurred()) + + Expect(accessSecret.StringData).To(HaveKey("kubeconfig")) + config, err := clientcmd.NewClientConfigFromBytes([]byte(accessSecret.StringData["kubeconfig"])) + Expect(err).NotTo(HaveOccurred()) + + rawConfig, err := config.RawConfig() + Expect(err).NotTo(HaveOccurred()) + + Expect(rawConfig.AuthInfos).ToNot(HaveKey(crateIdentityProvider.Name)) + }) + + It("should not accept duplicate identity provider names", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-12") + + auth := &openmcpv1alpha1.Authentication{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldNotReconcile(authReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + + Expect(auth.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + }), + )) + }) +}) diff --git a/internal/controller/core/authentication/testdata/test-01/authentication.yaml b/internal/controller/core/authentication/testdata/test-01/authentication.yaml new file mode 100644 index 0000000..de55547 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-01/authentication.yaml @@ -0,0 +1,9 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/authentication/testdata/test-02/apiserver.yaml b/internal/controller/core/authentication/testdata/test-02/apiserver.yaml new file mode 100644 index 0000000..39df31a --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-02/apiserver.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "False" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/authentication/testdata/test-02/authentication.yaml b/internal/controller/core/authentication/testdata/test-02/authentication.yaml new file mode 100644 index 0000000..de55547 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-02/authentication.yaml @@ -0,0 +1,9 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/authentication/testdata/test-03/apiserver.yaml b/internal/controller/core/authentication/testdata/test-03/apiserver.yaml new file mode 100644 index 0000000..b750d4b --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-03/apiserver.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: Invalid +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/authentication/testdata/test-03/authentication.yaml b/internal/controller/core/authentication/testdata/test-03/authentication.yaml new file mode 100644 index 0000000..de55547 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-03/authentication.yaml @@ -0,0 +1,9 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/authentication/testdata/test-04/apiserver.yaml b/internal/controller/core/authentication/testdata/test-04/apiserver.yaml new file mode 100644 index 0000000..aa619eb --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-04/apiserver.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/authentication/testdata/test-04/authentication.yaml b/internal/controller/core/authentication/testdata/test-04/authentication.yaml new file mode 100644 index 0000000..de55547 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-04/authentication.yaml @@ -0,0 +1,9 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/authentication/testdata/test-05/apiserver.yaml b/internal/controller/core/authentication/testdata/test-05/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-05/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/authentication/testdata/test-05/authentication.yaml b/internal/controller/core/authentication/testdata/test-05/authentication.yaml new file mode 100644 index 0000000..3c56f03 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-05/authentication.yaml @@ -0,0 +1,16 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + enableSystemIdentityProvider: true + + identityProviders: + - name: customer + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + groupsClaim: grp diff --git a/internal/controller/core/authentication/testdata/test-06/apiserver.yaml b/internal/controller/core/authentication/testdata/test-06/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-06/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/authentication/testdata/test-06/authentication.yaml b/internal/controller/core/authentication/testdata/test-06/authentication.yaml new file mode 100644 index 0000000..af4d534 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-06/authentication.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + enableSystemIdentityProvider: true + + identityProviders: + - name: customer + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + groupsClaim: grp + - name: extra + issuerURL: https://extra.local + clientID: jjj-kkk-lll + usernameClaim: u_name + groupsClaim: grp diff --git a/internal/controller/core/authentication/testdata/test-07/access_secret.yaml b/internal/controller/core/authentication/testdata/test-07/access_secret.yaml new file mode 100644 index 0000000..f323ed6 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-07/access_secret.yaml @@ -0,0 +1,10 @@ +# a Kubernetes secret +apiVersion: v1 +kind: Secret +metadata: + name: test.kubeconfig + namespace: test + labels: + "openmcp.cloud/managed-by": authorization +stringData: + kubeconfig: "dummy" diff --git a/internal/controller/core/authentication/testdata/test-07/apiserver.yaml b/internal/controller/core/authentication/testdata/test-07/apiserver.yaml new file mode 100644 index 0000000..bd5e7a7 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-07/apiserver.yaml @@ -0,0 +1,44 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - dependency.openmcp.cloud/authentication +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/authentication/testdata/test-07/apiserver/openIDConnect.yaml b/internal/controller/core/authentication/testdata/test-07/apiserver/openIDConnect.yaml new file mode 100644 index 0000000..9445c46 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-07/apiserver/openIDConnect.yaml @@ -0,0 +1,28 @@ +--- +apiVersion: authentication.gardener.cloud/v1alpha1 +kind: OpenIDConnect +metadata: + name: openmcp + labels: + openmcp.cloud/managed-by: authentication +spec: + issuerURL: https://openmcp.local + clientID: aaa-bbb-ccc + usernameClaim: email + usernamePrefix: "openmcp:" + groupsClaim: groups + groupsPrefix: "openmcp:" +--- +apiVersion: authentication.gardener.cloud/v1alpha1 +kind: OpenIDConnect +metadata: + name: customer + labels: + openmcp.cloud/managed-by: authentication +spec: + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + usernamePrefix: "customer:" + groupsClaim: grp + groupsPrefix: "customer:" diff --git a/internal/controller/core/authentication/testdata/test-07/authentication.yaml b/internal/controller/core/authentication/testdata/test-07/authentication.yaml new file mode 100644 index 0000000..b12053b --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-07/authentication.yaml @@ -0,0 +1,32 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - authentication.openmcp.cloud +spec: + enableSystemIdentityProvider: true + + identityProviders: + - name: customer + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + groupsClaim: grp + +status: + access: + key: kubeconfig + name: test.kubeconfig + namespace: test + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: authenticationHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 diff --git a/internal/controller/core/authentication/testdata/test-08/apiserver.yaml b/internal/controller/core/authentication/testdata/test-08/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-08/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/authentication/testdata/test-08/authentication.yaml b/internal/controller/core/authentication/testdata/test-08/authentication.yaml new file mode 100644 index 0000000..bed27f8 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-08/authentication.yaml @@ -0,0 +1,23 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + annotations: + "openmcp.cloud/operation": "ignore" +spec: + enableSystemIdentityProvider: true + + identityProviders: + - name: customer + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + groupsClaim: grp + - name: extra + issuerURL: https://extra.local + clientID: jjj-kkk-lll + usernameClaim: u_name + groupsClaim: grp diff --git a/internal/controller/core/authentication/testdata/test-09/apiserver.yaml b/internal/controller/core/authentication/testdata/test-09/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-09/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/authentication/testdata/test-09/authentication.yaml b/internal/controller/core/authentication/testdata/test-09/authentication.yaml new file mode 100644 index 0000000..4748a5b --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-09/authentication.yaml @@ -0,0 +1,23 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + annotations: + "openmcp.cloud/operation": "reconcile" +spec: + enableSystemIdentityProvider: true + + identityProviders: + - name: customer + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + groupsClaim: grp + - name: extra + issuerURL: https://extra.local + clientID: jjj-kkk-lll + usernameClaim: u_name + groupsClaim: grp diff --git a/internal/controller/core/authentication/testdata/test-10/apiserver.yaml b/internal/controller/core/authentication/testdata/test-10/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-10/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/authentication/testdata/test-10/authentication.yaml b/internal/controller/core/authentication/testdata/test-10/authentication.yaml new file mode 100644 index 0000000..075bf07 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-10/authentication.yaml @@ -0,0 +1,26 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + annotations: + "openmcp.cloud/operation": "reconcile" + finalizers: + - authentication.openmcp.cloud + - dependency.openmcp.cloud/other_comp +spec: + enableSystemIdentityProvider: true + + identityProviders: + - name: customer + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + groupsClaim: grp + - name: extra + issuerURL: https://extra.local + clientID: jjj-kkk-lll + usernameClaim: u_name + groupsClaim: grp diff --git a/internal/controller/core/authentication/testdata/test-11/apiserver.yaml b/internal/controller/core/authentication/testdata/test-11/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-11/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/authentication/testdata/test-11/authentication.yaml b/internal/controller/core/authentication/testdata/test-11/authentication.yaml new file mode 100644 index 0000000..29f4878 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-11/authentication.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + enableSystemIdentityProvider: true + + identityProviders: + - name: customer + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + groupsClaim: grp + caBundle: | + -----BEGIN CERTIFICATE----- + Li4u + -----END CERTIFICATE----- + signingAlgs: + - RS256 + - RS384 + - RS512 + requiredClaims: + myClaimKey: myClaimValue + clientConfig: + clientSecret: + name: client-secret + key: secret + extraConfig: + oidc-extra-scope: + values: + - scope1 + - scope2 + extra-param: + value: foo + extra-repeatable: + values: + - bar1 + - bar2 + no-value-param: diff --git a/internal/controller/core/authentication/testdata/test-11/secret.yaml b/internal/controller/core/authentication/testdata/test-11/secret.yaml new file mode 100644 index 0000000..18263ad --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-11/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: client-secret + namespace: test +data: + secret: bXljbGllbnRzZWNyZXQ= diff --git a/internal/controller/core/authentication/testdata/test-12/apiserver.yaml b/internal/controller/core/authentication/testdata/test-12/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-12/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/authentication/testdata/test-12/authentication.yaml b/internal/controller/core/authentication/testdata/test-12/authentication.yaml new file mode 100644 index 0000000..a08533b --- /dev/null +++ b/internal/controller/core/authentication/testdata/test-12/authentication.yaml @@ -0,0 +1,16 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + enableSystemIdentityProvider: true + + identityProviders: + - name: openmcp + issuerURL: https://customer.local + clientID: xxx-yyy-zzz + usernameClaim: u_name + groupsClaim: grp diff --git a/internal/controller/core/authorization/authz_suite_test.go b/internal/controller/core/authorization/authz_suite_test.go new file mode 100644 index 0000000..28f0766 --- /dev/null +++ b/internal/controller/core/authorization/authz_suite_test.go @@ -0,0 +1,13 @@ +package authorization_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Authorization Controller Test Suite") +} diff --git a/internal/controller/core/authorization/clusteradmin/clusteradmin_suite_test.go b/internal/controller/core/authorization/clusteradmin/clusteradmin_suite_test.go new file mode 100644 index 0000000..92bcd5b --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/clusteradmin_suite_test.go @@ -0,0 +1,13 @@ +package clusteradmin_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ClusterAdmin Config Test Suite") +} diff --git a/internal/controller/core/authorization/clusteradmin/controller.go b/internal/controller/core/authorization/clusteradmin/controller.go new file mode 100644 index 0000000..20e0660 --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/controller.go @@ -0,0 +1,304 @@ +package clusteradmin + +import ( + "context" + "fmt" + "time" + + "github.com/openmcp-project/controller-utils/pkg/logging" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/tools/record" + "k8s.io/utils/ptr" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + authzconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/config" + apiserverutils "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" +) + +const ( + ControllerName = "ClusterAdmin" +) + +// ClusterAdminReconciler reconciles a ClusterAdmin object +type ClusterAdminReconciler struct { + Client client.Client + Config *authzconfig.ClusterAdmin + APIServerAccess apiserverutils.APIServerAccess + EventBroadcaster record.EventBroadcaster + EventRecorder record.EventRecorder +} + +// NewClusterAdminReconciler creates a new ClusterAdminReconciler +func NewClusterAdminReconciler(c client.Client, config *authzconfig.AuthorizationConfig) *ClusterAdminReconciler { + return &ClusterAdminReconciler{ + Client: c, + Config: &config.ClusterAdmin, + APIServerAccess: &apiserverutils.APIServerAccessImpl{ + NewClient: client.New, + }, + EventBroadcaster: record.NewBroadcaster(), + } +} + +func (car *ClusterAdminReconciler) SetAPIServerAccess(apiServerAccess apiserverutils.APIServerAccess) *ClusterAdminReconciler { + car.APIServerAccess = apiServerAccess + return car +} + +// SetupWithManager sets up the controller with the controller-runtime manager +func (car *ClusterAdminReconciler) SetupWithManager(mgr ctrl.Manager) error { + cs, err := kubernetes.NewForConfig(mgr.GetConfig()) + if err != nil { + return err + } + + car.EventBroadcaster.StartStructuredLogging(4) + car.EventBroadcaster.StartRecordingToSink(&v1.EventSinkImpl{ + Interface: cs.CoreV1().Events(""), + }) + car.EventRecorder = car.EventBroadcaster.NewRecorder(mgr.GetScheme(), corev1.EventSource{}) + + return ctrl.NewControllerManagedBy(mgr). + For(&openmcpv1alpha1.ClusterAdmin{}). + Complete(car) +} + +// Reconcile reconciles the ClusterAdmin object +func (car *ClusterAdminReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + var err error + + // get the logger + log := logging.FromContextOrPanic(ctx) + + ca := &openmcpv1alpha1.ClusterAdmin{} + if err = car.Client.Get(ctx, req.NamespacedName, ca); err != nil { + log.Error(err, "unable to fetch ClusterAdmin") + return ctrl.Result{}, client.IgnoreNotFound(err) + } + + authz := &openmcpv1alpha1.Authorization{} + err = car.Client.Get(ctx, client.ObjectKey{Name: ca.Name, Namespace: ca.Namespace}, authz) + if err != nil { + log.Error(err, "unable to fetch Authorization for ClusterAdmin") + return ctrl.Result{}, err + } + + apiServer := &openmcpv1alpha1.APIServer{} + err = car.Client.Get(ctx, client.ObjectKey{Name: ca.Name, Namespace: ca.Namespace}, apiServer) + if err != nil { + log.Error(err, "unable to fetch APIServer for ClusterAdmin") + return ctrl.Result{}, err + } + + if apiServer.Status.AdminAccess == nil || apiServer.Status.AdminAccess.Kubeconfig == "" { + log.Debug("APIServer admin access not ready yet") + return ctrl.Result{ + RequeueAfter: 10 * time.Second, + }, nil + } + + apiServerClient, err := car.APIServerAccess.GetAdminAccessClient(apiServer, client.Options{}) + if err != nil { + log.Error(err, "unable to get APIServer admin access client") + return ctrl.Result{}, err + } + + if !ca.DeletionTimestamp.IsZero() { + log.Debug("ClusterAdmin is being deleted") + // deletion + return ctrl.Result{}, car.handleDelete(ctx, ca, apiServerClient) + } else { + log.Debug("ClusterAdmin is being created/updated") + // creation/update + return car.handleCreateUpdate(ctx, ca, apiServerClient) + } +} + +// emitActivatedEvent emits a K8S event when the cluster admin is activated +func (car *ClusterAdminReconciler) emitActivatedEvent(ctx context.Context, ca *openmcpv1alpha1.ClusterAdmin) { + if car.EventRecorder == nil { + return + } + + log := logging.FromContextOrPanic(ctx) + log.Debug("Cluster admin activated", "validUntil", ca.Status.Expiration, "subjects", ca.Spec.Subjects) + + mcp := &openmcpv1alpha1.ManagedControlPlane{} + err := car.Client.Get(ctx, client.ObjectKey{Name: ca.Name, Namespace: ca.Namespace}, mcp) + if err == nil { + car.EventRecorder.Eventf(mcp, corev1.EventTypeWarning, "ClusterAdminActivated", "Cluster admin activated (valid until %s) with subjects: %v", ca.Status.Expiration.Format(time.RFC3339), ca.Spec.Subjects) + } +} + +// emitDeactivatedEvent emits a K8S event when the cluster admin is deactivated +func (car *ClusterAdminReconciler) emitDeactivatedEvent(ctx context.Context, ca *openmcpv1alpha1.ClusterAdmin) { + if car.EventRecorder == nil { + return + } + + log := logging.FromContextOrPanic(ctx) + log.Debug("Cluster admin deactivated") + + mcp := &openmcpv1alpha1.ManagedControlPlane{} + err := car.Client.Get(ctx, client.ObjectKey{Name: ca.Name, Namespace: ca.Namespace}, mcp) + if err == nil { + car.EventRecorder.Eventf(mcp, corev1.EventTypeWarning, "ClusterAdminDeactivated", "Cluster admin deactivated") + } +} + +// handleCreateUpdate handles the creation and update of the ClusterAdmin object +func (car *ClusterAdminReconciler) handleCreateUpdate(ctx context.Context, ca *openmcpv1alpha1.ClusterAdmin, apiServerClient client.Client) (reconcile.Result, error) { + var err error + + mutateClusterRoleBinding := func(clusterRoleBinding *rbacv1.ClusterRoleBinding, subjects []openmcpv1alpha1.Subject) { + clusterRoleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: openmcpv1alpha1.ClusterAdminRole, + } + clusterRoleBinding.Labels = map[string]string{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + } + clusterRoleBinding.Subjects = make([]rbacv1.Subject, 0, len(subjects)) + for _, subject := range subjects { + clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ + Kind: subject.Kind, + Name: subject.Name, + Namespace: subject.Namespace, + APIGroup: subject.APIGroup, + }) + } + } + + if controllerutil.AddFinalizer(ca, openmcpv1alpha1.AuthorizationComponent.Finalizer()) { + if err := car.Client.Update(ctx, ca); err != nil { + return reconcile.Result{}, err + } + } + + if ca.Status.Active { + // was activated before + if ca.Status.Activated == nil || ca.Status.Expiration == nil { + return reconcile.Result{}, fmt.Errorf("cluster admin status is active, but activated or expiration time is missing") + } + // get now + now := metav1.Now() + // if is activated for more than the desired duration, then deactivate + if now.Sub(ca.Status.Activated.Time) >= car.Config.ActiveDuration.Duration { + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ClusterAdminRoleBinding, + }, + } + + err = apiServerClient.Delete(ctx, clusterRoleBinding) + if err != nil { + if !apierrors.IsNotFound(err) { + return reconcile.Result{}, err + } + } + + ca.Status.Active = false + + if err = car.Client.Status().Update(ctx, ca); err != nil { + return reconcile.Result{}, err + } + + car.emitDeactivatedEvent(ctx, ca) + + } else { + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ClusterAdminRoleBinding, + }, + } + _, err = controllerutil.CreateOrUpdate(ctx, apiServerClient, clusterRoleBinding, func() error { + mutateClusterRoleBinding(clusterRoleBinding, ca.Spec.Subjects) + return nil + }) + + if err != nil { + return reconcile.Result{}, err + } + + // reconcile before expiration + return reconcile.Result{ + RequeueAfter: ca.Status.Expiration.Sub(now.Time), + }, nil + } + } + + if ca.Status.Activated == nil { + // was not activated before + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ClusterAdminRoleBinding, + }, + } + _, err = controllerutil.CreateOrUpdate(ctx, apiServerClient, clusterRoleBinding, func() error { + mutateClusterRoleBinding(clusterRoleBinding, ca.Spec.Subjects) + return nil + }) + + if err != nil { + return reconcile.Result{}, err + } + + ca.Status.Active = true + ca.Status.Activated = ptr.To(metav1.Now()) + ca.Status.Expiration = ptr.To(metav1.Time{Time: ca.Status.Activated.Add(car.Config.ActiveDuration.Duration)}) + + if err = car.Client.Status().Update(ctx, ca); err != nil { + return reconcile.Result{}, err + } + + car.emitActivatedEvent(ctx, ca) + + return reconcile.Result{ + RequeueAfter: car.Config.ActiveDuration.Duration, + }, nil + } + + return reconcile.Result{}, nil +} + +// handleDelete handles the deletion of the ClusterAdmin object +func (car *ClusterAdminReconciler) handleDelete(ctx context.Context, ca *openmcpv1alpha1.ClusterAdmin, apiServerClient client.Client) error { + var err error + + clusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ClusterAdminRoleBinding, + }, + } + + err = apiServerClient.Delete(ctx, clusterRoleBinding) + if err != nil { + if !apierrors.IsNotFound(err) { + return err + } + } + + if controllerutil.RemoveFinalizer(ca, openmcpv1alpha1.AuthorizationComponent.Finalizer()) { + err = car.Client.Update(ctx, ca) + if err != nil { + return err + } + } + + if ca.Status.Active { + car.emitDeactivatedEvent(ctx, ca) + } + + return nil +} diff --git a/internal/controller/core/authorization/clusteradmin/controller_test.go b/internal/controller/core/authorization/clusteradmin/controller_test.go new file mode 100644 index 0000000..6da516f --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/controller_test.go @@ -0,0 +1,200 @@ +package clusteradmin_test + +import ( + "time" + + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/clusteradmin" + "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/config" + testutils "github.com/openmcp-project/mcp-operator/test/utils" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +const ( + clusterAdminReconciler = "ClusterAdmin" +) + +func getReconciler(c ...client.Client) reconcile.Reconciler { + return clusteradmin.NewClusterAdminReconciler(c[0], &config.AuthorizationConfig{ + ClusterAdmin: config.ClusterAdmin{ + ActiveDuration: metav1.Duration{Duration: 1 * time.Second}, + }, + }) +} + +func testEnvWithAPIServerAccess(testDataPathSegments ...string) *testing.ComplexEnvironment { + env := testutils.DefaultTestSetupBuilder(testDataPathSegments...). + WithFakeClient(testutils.APIServerCluster, testutils.Scheme). + WithReconcilerConstructor(clusterAdminReconciler, getReconciler, testutils.CrateCluster). + WithDynamicObjectsWithStatus(testutils.CrateCluster). + Build() + env.Reconcilers[clusterAdminReconciler].(*clusteradmin.ClusterAdminReconciler). + SetAPIServerAccess(&testutils.TestAPIServerAccess{Client: env.Client(testutils.APIServerCluster)}) + + return env +} + +var _ = Describe("CO-1153 ClusterAdmin Controller", func() { + It("should create a cluster role binding for the cluster admin", func() { + env := testEnvWithAPIServerAccess("testdata", "test-01") + + authz := &openmcpv1alpha1.Authorization{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + ca := &openmcpv1alpha1.ClusterAdmin{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ca) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(ca) + res := env.ShouldReconcile(clusterAdminReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ca), ca) + Expect(err).ToNot(HaveOccurred()) + + Expect(ca.Status.Active).To(BeTrue()) + Expect(ca.Status.Activated).ToNot(BeNil()) + Expect(ca.Status.Expiration).ToNot(BeNil()) + + // Check if the cluster role binding was created + crb := &rbacv1.ClusterRoleBinding{} + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: openmcpv1alpha1.ClusterAdminRoleBinding}, crb) + Expect(err).ToNot(HaveOccurred()) + + // Check if the cluster role binding was created with the correct subjects + Expect(crb.Subjects).To(HaveLen(1)) + Expect(crb.Subjects[0].Kind).To(Equal("User")) + Expect(crb.Subjects[0].Name).To(Equal("admin")) + + // Check if the cluster role binding was created with the correct role ref + Expect(crb.RoleRef.APIGroup).To(Equal("rbac.authorization.k8s.io")) + Expect(crb.RoleRef.Kind).To(Equal("ClusterRole")) + Expect(crb.RoleRef.Name).To(Equal(openmcpv1alpha1.ClusterAdminRole)) + + Eventually(func() bool { + res := env.ShouldReconcile(clusterAdminReconciler, req) + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: openmcpv1alpha1.ClusterAdminRoleBinding}, crb) + return errors.IsNotFound(err) && res.Requeue == false && res.RequeueAfter == 0 + }, 2*time.Second, 100*time.Millisecond).Should(BeTrue()) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ca), ca) + Expect(err).ToNot(HaveOccurred()) + + Expect(ca.Status.Active).To(BeFalse()) + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: openmcpv1alpha1.ClusterAdminRoleBinding}, crb) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + // it should not activate again + res = env.ShouldReconcile(clusterAdminReconciler, req) + testing.ExpectNoRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ca), ca) + Expect(err).ToNot(HaveOccurred()) + + Expect(ca.Status.Active).To(BeFalse()) + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: openmcpv1alpha1.ClusterAdminRoleBinding}, crb) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) + + It("should deactivate the cluster admin if the clusteradmin object is deleted while being active", func() { + env := testEnvWithAPIServerAccess("testdata", "test-01") + + authz := &openmcpv1alpha1.Authorization{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + ca := &openmcpv1alpha1.ClusterAdmin{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ca) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(ca) + res := env.ShouldReconcile(clusterAdminReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ca), ca) + Expect(err).ToNot(HaveOccurred()) + + Expect(ca.Status.Active).To(BeTrue()) + Expect(ca.Status.Activated).ToNot(BeNil()) + Expect(ca.Status.Expiration).ToNot(BeNil()) + + crb := &rbacv1.ClusterRoleBinding{} + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: openmcpv1alpha1.ClusterAdminRoleBinding}, crb) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, ca) + Expect(err).ToNot(HaveOccurred()) + + res = env.ShouldReconcile(clusterAdminReconciler, req) + testing.ExpectNoRequeue(res) + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, types.NamespacedName{Name: openmcpv1alpha1.ClusterAdminRoleBinding}, crb) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) + + It("should fail if there is no authorization resource", func() { + env := testEnvWithAPIServerAccess("testdata", "test-02") + + ca := &openmcpv1alpha1.ClusterAdmin{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ca) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(ca) + env.ShouldNotReconcile(clusterAdminReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ca), ca) + Expect(err).ToNot(HaveOccurred()) + + Expect(ca.Status.Active).To(BeFalse()) + }) + + It("should fail if there is no apiserver resource", func() { + env := testEnvWithAPIServerAccess("testdata", "test-03") + + ca := &openmcpv1alpha1.ClusterAdmin{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ca) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(ca) + env.ShouldNotReconcile(clusterAdminReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ca), ca) + Expect(err).ToNot(HaveOccurred()) + + Expect(ca.Status.Active).To(BeFalse()) + }) + + It("should requeue when apiserver has no admin access", func() { + env := testEnvWithAPIServerAccess("testdata", "test-04") + + ca := &openmcpv1alpha1.ClusterAdmin{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ca) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(ca) + res := env.ShouldReconcile(clusterAdminReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ca), ca) + Expect(err).ToNot(HaveOccurred()) + + Expect(ca.Status.Active).To(BeFalse()) + }) +}) diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-01/apiserver.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-01/apiserver.yaml new file mode 100644 index 0000000..bab5063 --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-01/apiserver.yaml @@ -0,0 +1,44 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - dependency.openmcp.cloud/authorization +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-01/authorization.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-01/authorization.yaml new file mode 100644 index 0000000..34ec805 --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-01/authorization.yaml @@ -0,0 +1,31 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - authorization.openmcp.cloud +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin + - kind: ServiceAccount + name: pipeline + namespace: automate + - role: view + subjects: + - kind: Group + name: auditors +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: authorizationHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-01/clusteradmin.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-01/clusteradmin.yaml new file mode 100644 index 0000000..9f1564e --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-01/clusteradmin.yaml @@ -0,0 +1,9 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ClusterAdmin +metadata: + name: test + namespace: test +spec: + subjects: + - kind: User + name: admin diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-01/mcp.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-01/mcp.yaml new file mode 100644 index 0000000..488bb14 --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-01/mcp.yaml @@ -0,0 +1,7 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ManagedControlPlane +metadata: + name: test + namespace: test +spec: + components: {} \ No newline at end of file diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-02/clusteradmin.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-02/clusteradmin.yaml new file mode 100644 index 0000000..9f1564e --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-02/clusteradmin.yaml @@ -0,0 +1,9 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ClusterAdmin +metadata: + name: test + namespace: test +spec: + subjects: + - kind: User + name: admin diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-03/authorization.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-03/authorization.yaml new file mode 100644 index 0000000..34ec805 --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-03/authorization.yaml @@ -0,0 +1,31 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - authorization.openmcp.cloud +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin + - kind: ServiceAccount + name: pipeline + namespace: automate + - role: view + subjects: + - kind: Group + name: auditors +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: authorizationHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-03/clusteradmin.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-03/clusteradmin.yaml new file mode 100644 index 0000000..9f1564e --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-03/clusteradmin.yaml @@ -0,0 +1,9 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ClusterAdmin +metadata: + name: test + namespace: test +spec: + subjects: + - kind: User + name: admin diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-04/apiserver.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-04/apiserver.yaml new file mode 100644 index 0000000..486737e --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-04/apiserver.yaml @@ -0,0 +1,23 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - dependency.openmcp.cloud/authorization +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-04/authorization.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-04/authorization.yaml new file mode 100644 index 0000000..34ec805 --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-04/authorization.yaml @@ -0,0 +1,31 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - authorization.openmcp.cloud +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin + - kind: ServiceAccount + name: pipeline + namespace: automate + - role: view + subjects: + - kind: Group + name: auditors +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: authorizationHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 diff --git a/internal/controller/core/authorization/clusteradmin/testdata/test-04/clusteradmin.yaml b/internal/controller/core/authorization/clusteradmin/testdata/test-04/clusteradmin.yaml new file mode 100644 index 0000000..9f1564e --- /dev/null +++ b/internal/controller/core/authorization/clusteradmin/testdata/test-04/clusteradmin.yaml @@ -0,0 +1,9 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ClusterAdmin +metadata: + name: test + namespace: test +spec: + subjects: + - kind: User + name: admin diff --git a/internal/controller/core/authorization/config/config.go b/internal/controller/core/authorization/config/config.go new file mode 100644 index 0000000..de934a5 --- /dev/null +++ b/internal/controller/core/authorization/config/config.go @@ -0,0 +1,307 @@ +package config + +import ( + "regexp" + "strings" + "time" + + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/validation/field" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// AuthorizationConfig contains the configuration for the authorization controller. +type AuthorizationConfig struct { + // Admin contains the configuration for the admin role. + Admin RoleConfig `json:"admin,omitempty"` + // View contains the configuration for the view role. + View RoleConfig `json:"view,omitempty"` + + // ProtectedNamespaces contains the list of namespaces that are protected from being modified by the user. + ProtectedNamespaces []ProtectedNamespace `json:"protectedNamespaces,omitempty"` + + // ClusterAdmin contains the configuration for the cluster admin role. + ClusterAdmin ClusterAdmin `json:"clusterAdmin,omitempty"` +} + +// RoleConfig contains the configuration for a role. +type RoleConfig struct { + // AdditionalSubjects contains the additional subjects for the role. + // They are added to a MCP alongside the subjects specified by the user. + AdditionalSubjects []rbacv1.Subject `json:"additionalSubjects,omitempty"` + // NamespaceScoped contains the configuration for the namespace scoped rules of the role. + NamespaceScoped RulesConfig `json:"namespaceScoped,omitempty"` + // ClusterScoped contains the configuration for the cluster scoped rules of the role. + ClusterScoped RulesConfig `json:"clusterScoped,omitempty"` +} + +// RulesConfig contains the configuration for the rules of a role. +type RulesConfig struct { + // Labels are added to the `ClusterRole` that defines the common rules for a user. + Labels map[string]string `json:"labels,omitempty"` + // ClusterRoleSelectors define label selector which aggregate specific `Cluster` to the common `ClusterRole`. + ClusterRoleSelectors []metav1.LabelSelector `json:"clusterRoleSelectors,omitempty"` + // Rules specifies the rules for the role. + Rules []rbacv1.PolicyRule `json:"rules,omitempty"` +} + +// ProtectedNamespace contains the configuration for a protected namespace. +// If any of the non-empty fields is matched, the namespace is considered protected. +// The ordering of the matching is as follows: +// 1. Exact +// 2. Prefix +// 3. Postfix +// 4. Pattern +type ProtectedNamespace struct { + // Exact is the exact namespace name. + Exact string `json:"exact,omitempty"` + // Prefix is the prefix of the namespace name. + Prefix string `json:"prefix,omitempty"` + // Postfix is the postfix of the namespace name. + Postfix string `json:"postfix,omitempty"` + // Pattern is the pattern of the namespace name. + Pattern string `json:"pattern,omitempty"` + + // CompiledPattern is the compiled pattern of the namespace name. + // Not serialized. + CompiledPattern *regexp.Regexp `json:"-"` +} + +// ClusterAdmin contains the configuration for the cluster admin role. +type ClusterAdmin struct { + // ActiveDuration is the duration for which the cluster admin role is active. + ActiveDuration metav1.Duration `json:"activeDuration,omitempty"` +} + +// SetDefaults sets the default values for the authorization configuration when not set. +func (ac *AuthorizationConfig) SetDefaults() { + // Admin Section + admin := &ac.Admin + adminNamespaceScoped := &admin.NamespaceScoped + adminClusterScoped := &admin.ClusterScoped + + if adminNamespaceScoped.ClusterRoleSelectors == nil { + adminNamespaceScoped.ClusterRoleSelectors = make([]metav1.LabelSelector, 0) + } + + adminNamespaceScoped.ClusterRoleSelectors = append(adminNamespaceScoped.ClusterRoleSelectors, metav1.LabelSelector{ + MatchLabels: map[string]string{ + openmcpv1alpha1.AdminNamespaceScopeMatchLabel: "true", + }, + }) + + if adminNamespaceScoped.Labels == nil { + adminNamespaceScoped.Labels = make(map[string]string) + } + + adminNamespaceScoped.Labels[openmcpv1alpha1.AdminNamespaceScopeMatchLabel] = "true" + + if adminClusterScoped.ClusterRoleSelectors == nil { + adminClusterScoped.ClusterRoleSelectors = make([]metav1.LabelSelector, 0) + } + + adminClusterScoped.ClusterRoleSelectors = append(adminClusterScoped.ClusterRoleSelectors, metav1.LabelSelector{ + MatchLabels: map[string]string{ + openmcpv1alpha1.AdminClusterScopeMatchLabel: "true", + }, + }) + + if adminClusterScoped.Labels == nil { + adminClusterScoped.Labels = make(map[string]string) + } + + adminClusterScoped.Labels[openmcpv1alpha1.AdminClusterScopeMatchLabel] = "true" + + // View Section + view := &ac.View + viewNamespaceScoped := &view.NamespaceScoped + viewClusterScoped := &view.ClusterScoped + + if viewNamespaceScoped.ClusterRoleSelectors == nil { + viewNamespaceScoped.ClusterRoleSelectors = make([]metav1.LabelSelector, 0) + } + + viewNamespaceScoped.ClusterRoleSelectors = append(viewNamespaceScoped.ClusterRoleSelectors, metav1.LabelSelector{ + MatchLabels: map[string]string{ + openmcpv1alpha1.ViewNamespaceScopeMatchLabel: "true", + }, + }) + + if viewNamespaceScoped.Labels == nil { + viewNamespaceScoped.Labels = make(map[string]string) + } + + viewNamespaceScoped.Labels[openmcpv1alpha1.ViewNamespaceScopeMatchLabel] = "true" + viewNamespaceScoped.Labels[openmcpv1alpha1.AdminNamespaceScopeMatchLabel] = "true" + + if viewClusterScoped.ClusterRoleSelectors == nil { + viewClusterScoped.ClusterRoleSelectors = make([]metav1.LabelSelector, 0) + } + + viewClusterScoped.ClusterRoleSelectors = append(viewClusterScoped.ClusterRoleSelectors, metav1.LabelSelector{ + MatchLabels: map[string]string{ + openmcpv1alpha1.ViewClusterScopeMatchLabel: "true", + }, + }) + + if viewClusterScoped.Labels == nil { + viewClusterScoped.Labels = make(map[string]string) + } + + viewClusterScoped.Labels[openmcpv1alpha1.ViewClusterScopeMatchLabel] = "true" + viewClusterScoped.Labels[openmcpv1alpha1.AdminClusterScopeMatchLabel] = "true" + + if len(ac.ProtectedNamespaces) == 0 { + ac.ProtectedNamespaces = []ProtectedNamespace{ + { + Prefix: "kube-", + }, + { + Postfix: "-system", + }, + } + } + + // Set the compiled pattern for each protected namespace. + for i := range ac.ProtectedNamespaces { + pn := &ac.ProtectedNamespaces[i] + + if pn.Pattern != "" { + pn.CompiledPattern = regexp.MustCompile(pn.Pattern) + } + } + + // Cluster Admin Section + if ac.ClusterAdmin.ActiveDuration.Duration == 0 { + ac.ClusterAdmin.ActiveDuration.Duration = 24 * time.Hour + } +} + +// GetRulesConfig returns the rules configuration for the given cluster role name. +func (ac *AuthorizationConfig) GetRulesConfig(clusterRoleName string) *RulesConfig { + var rulesConfig *RulesConfig + + if openmcpv1alpha1.IsAdminRole(clusterRoleName) && openmcpv1alpha1.IsClusterScopedRole(clusterRoleName) { + rulesConfig = &ac.Admin.ClusterScoped + } else if openmcpv1alpha1.IsAdminRole(clusterRoleName) && !openmcpv1alpha1.IsClusterScopedRole(clusterRoleName) { + rulesConfig = &ac.Admin.NamespaceScoped + } else if !openmcpv1alpha1.IsAdminRole(clusterRoleName) && openmcpv1alpha1.IsClusterScopedRole(clusterRoleName) { + rulesConfig = &ac.View.ClusterScoped + } else if !openmcpv1alpha1.IsAdminRole(clusterRoleName) && !openmcpv1alpha1.IsClusterScopedRole(clusterRoleName) { + rulesConfig = &ac.View.NamespaceScoped + } + + return rulesConfig +} + +// Validate validates the authorization configuration. +func Validate(config *AuthorizationConfig) error { + allErrs := field.ErrorList{} + + path := field.NewPath("admin").Child("namespaceScoped").Child("rules") + for i, rule := range config.Admin.NamespaceScoped.Rules { + allErrs = append(allErrs, validateRule(rule, path.Index(i))...) + } + + path = field.NewPath("admin").Child("clusterScoped").Child("rules") + for i, rule := range config.Admin.ClusterScoped.Rules { + allErrs = append(allErrs, validateRule(rule, path.Index(i))...) + } + + path = field.NewPath("view").Child("namespaceScoped").Child("rules") + for i, rule := range config.View.NamespaceScoped.Rules { + allErrs = append(allErrs, validateRule(rule, path.Index(i))...) + } + + path = field.NewPath("view").Child("clusterScoped").Child("rules") + for i, rule := range config.View.ClusterScoped.Rules { + allErrs = append(allErrs, validateRule(rule, path.Index(i))...) + } + + path = field.NewPath("admin").Child("subjects") + for i, subject := range config.Admin.AdditionalSubjects { + allErrs = append(allErrs, validateSubject(&subject, path.Index(i))...) + } + + path = field.NewPath("view").Child("subjects") + for i, subject := range config.View.AdditionalSubjects { + allErrs = append(allErrs, validateSubject(&subject, path.Index(i))...) + } + + path = field.NewPath("protectedNamespaces") + for i, pn := range config.ProtectedNamespaces { + if pn.Pattern != "" { + _, err := regexp.Compile(pn.Pattern) + if err != nil { + allErrs = append(allErrs, field.Invalid(path.Index(i).Child("pattern"), pn.Pattern, "pattern is invalid")) + } + } + } + + return allErrs.ToAggregate() +} + +// validateRule validates the given rule. +func validateRule(rule rbacv1.PolicyRule, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if len(rule.APIGroups) == 0 { + allErrs = append(allErrs, field.Required(fieldPath.Child("apiGroups"), "apiGroups must be set")) + } + + if len(rule.Resources) == 0 { + allErrs = append(allErrs, field.Required(fieldPath.Child("resources"), "resources must be set")) + } + + if len(rule.Verbs) == 0 { + allErrs = append(allErrs, field.Required(fieldPath.Child("verbs"), "verbs must be set")) + } + + return allErrs +} + +func validateSubject(subject *rbacv1.Subject, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if subject.Kind != rbacv1.UserKind && subject.Kind != rbacv1.GroupKind && subject.Kind != rbacv1.ServiceAccountKind { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("kind"), subject.Kind, "kind must be either User, Group or ServiceAccount")) + } + + if (subject.Kind == rbacv1.UserKind || subject.Kind == rbacv1.GroupKind) && subject.APIGroup != rbacv1.GroupName { + allErrs = append(allErrs, field.Invalid(fieldPath.Child("apiGroup"), subject.APIGroup, "apiGroup must be set to "+rbacv1.GroupName)) + } + + if subject.Kind == rbacv1.ServiceAccountKind && subject.Namespace == "" { + allErrs = append(allErrs, field.Required(fieldPath.Child("namespace"), "namespace must be set")) + } + + if subject.Name == "" { + allErrs = append(allErrs, field.Required(fieldPath.Child("name"), "name must be set")) + } + + return allErrs +} + +// IsAllowedNamespaceName returns true if the given namespace name is allowed to be modified by the user. +func (ac *AuthorizationConfig) IsAllowedNamespaceName(name string) bool { + for _, pn := range ac.ProtectedNamespaces { + if pn.Exact != "" && pn.Exact == name { + return false + } + + if pn.Prefix != "" && strings.HasPrefix(name, pn.Prefix) { + return false + } + + if pn.Postfix != "" && strings.HasSuffix(name, pn.Postfix) { + return false + } + + if pn.CompiledPattern != nil && pn.CompiledPattern.MatchString(name) { + return false + } + } + return true +} diff --git a/internal/controller/core/authorization/config/config_suite_test.go b/internal/controller/core/authorization/config/config_suite_test.go new file mode 100644 index 0000000..59145b0 --- /dev/null +++ b/internal/controller/core/authorization/config/config_suite_test.go @@ -0,0 +1,13 @@ +package config_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Authorization Config Test Suite") +} diff --git a/internal/controller/core/authorization/config/config_test.go b/internal/controller/core/authorization/config/config_test.go new file mode 100644 index 0000000..74371d1 --- /dev/null +++ b/internal/controller/core/authorization/config/config_test.go @@ -0,0 +1,202 @@ +package config_test + +import ( + "errors" + + authzconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + rbacv1 "k8s.io/api/rbac/v1" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("Authorization Config", func() { + It("should set defaults", func() { + config := &authzconfig.AuthorizationConfig{} + config.SetDefaults() + + Expect(config.Admin.NamespaceScoped.Labels).To(HaveKeyWithValue(openmcpv1alpha1.AdminNamespaceScopeMatchLabel, "true")) + Expect(config.Admin.NamespaceScoped.ClusterRoleSelectors).To(HaveLen(1)) + Expect(config.Admin.NamespaceScoped.ClusterRoleSelectors[0].MatchLabels).To(HaveKeyWithValue(openmcpv1alpha1.AdminNamespaceScopeMatchLabel, "true")) + + Expect(config.Admin.ClusterScoped.Labels).To(HaveKeyWithValue(openmcpv1alpha1.AdminClusterScopeMatchLabel, "true")) + Expect(config.Admin.ClusterScoped.ClusterRoleSelectors).To(HaveLen(1)) + Expect(config.Admin.ClusterScoped.ClusterRoleSelectors[0].MatchLabels).To(HaveKeyWithValue(openmcpv1alpha1.AdminClusterScopeMatchLabel, "true")) + + Expect(config.View.NamespaceScoped.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ViewNamespaceScopeMatchLabel, "true")) + Expect(config.View.NamespaceScoped.ClusterRoleSelectors).To(HaveLen(1)) + Expect(config.View.NamespaceScoped.ClusterRoleSelectors[0].MatchLabels).To(HaveKeyWithValue(openmcpv1alpha1.ViewNamespaceScopeMatchLabel, "true")) + + Expect(config.View.ClusterScoped.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ViewClusterScopeMatchLabel, "true")) + Expect(config.View.ClusterScoped.ClusterRoleSelectors).To(HaveLen(1)) + Expect(config.View.ClusterScoped.ClusterRoleSelectors[0].MatchLabels).To(HaveKeyWithValue(openmcpv1alpha1.ViewClusterScopeMatchLabel, "true")) + }) + + It("should not validate", func() { + config := &authzconfig.AuthorizationConfig{} + + config.Admin.NamespaceScoped.Rules = []rbacv1.PolicyRule{ + {}, // 3 errors + } + config.Admin.ClusterScoped.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, // 2 errors + }, + } + config.View.NamespaceScoped.Rules = []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{"pods"}, // 1 error + }, + } + config.View.ClusterScoped.Rules = []rbacv1.PolicyRule{ + { + Verbs: []string{"get"}, // 2 errors + }, + } + + config.Admin.AdditionalSubjects = []rbacv1.Subject{ + { + Kind: "User", // 2 errors + }, + { + Kind: "Group", // 1 errors + APIGroup: "invalid", + Name: "foo", + }, + { + Kind: "Unknown", // 1 error + Name: "foo", + }, + } + + config.View.AdditionalSubjects = []rbacv1.Subject{ + { + Kind: "ServiceAccount", // 2 errors + }, + } + + err := authzconfig.Validate(config) + Expect(err).To(HaveOccurred()) + var errorList utilerrors.Aggregate + ok := errors.As(err, &errorList) + Expect(ok).To(BeTrue()) + Expect(errorList.Errors()).To(HaveLen(14)) + + }) + + Context("RulesConfig", func() { + It("returns Admin NamespaceScoped RulesConfig for admin namespace scoped role", func() { + config := &authzconfig.AuthorizationConfig{} + config.Admin.NamespaceScoped = authzconfig.RulesConfig{} + rulesConfig := config.GetRulesConfig(openmcpv1alpha1.AdminNamespaceScopeRole) + Expect(rulesConfig).To(Equal(&config.Admin.NamespaceScoped)) + }) + + It("returns Admin ClusterScoped RulesConfig for admin cluster scoped role", func() { + config := &authzconfig.AuthorizationConfig{} + config.Admin.ClusterScoped = authzconfig.RulesConfig{} + rulesConfig := config.GetRulesConfig(openmcpv1alpha1.AdminClusterScopeRole) + Expect(rulesConfig).To(Equal(&config.Admin.ClusterScoped)) + }) + + It("returns View NamespaceScoped RulesConfig for view namespace scoped role", func() { + config := &authzconfig.AuthorizationConfig{} + config.View.NamespaceScoped = authzconfig.RulesConfig{} + rulesConfig := config.GetRulesConfig(openmcpv1alpha1.ViewNamespaceScopeRole) + Expect(rulesConfig).To(Equal(&config.View.NamespaceScoped)) + }) + + It("returns View ClusterScoped RulesConfig for view cluster scoped role", func() { + config := &authzconfig.AuthorizationConfig{} + config.View.ClusterScoped = authzconfig.RulesConfig{} + rulesConfig := config.GetRulesConfig(openmcpv1alpha1.ViewClusterScopeRole) + Expect(rulesConfig).To(Equal(&config.View.ClusterScoped)) + }) + }) +}) + +var _ = Describe("IsAllowedNamespaceName", func() { + Context("defaults", func() { + It("returns true for valid namespace names", func() { + config := &authzconfig.AuthorizationConfig{} + config.SetDefaults() + Expect(config.IsAllowedNamespaceName("valid-namespace")).To(BeTrue()) + Expect(config.IsAllowedNamespaceName("another-valid-namespace")).To(BeTrue()) + }) + + It("returns false for namespace names starting with 'kube-'", func() { + config := &authzconfig.AuthorizationConfig{} + config.SetDefaults() + Expect(config.IsAllowedNamespaceName("kube-system")).To(BeFalse()) + Expect(config.IsAllowedNamespaceName("kube-public")).To(BeFalse()) + }) + + It("returns false for namespace names ending with '-system'", func() { + config := &authzconfig.AuthorizationConfig{} + config.SetDefaults() + Expect(config.IsAllowedNamespaceName("default-system")).To(BeFalse()) + Expect(config.IsAllowedNamespaceName("my-system")).To(BeFalse()) + Expect(config.IsAllowedNamespaceName("my-system")).To(BeFalse()) + }) + }) + + Context("custom", func() { + var ( + config *authzconfig.AuthorizationConfig + ) + + BeforeEach(func() { + config = &authzconfig.AuthorizationConfig{ + ProtectedNamespaces: []authzconfig.ProtectedNamespace{ + { + Exact: "foobar", + }, + { + Prefix: "protected-", + Postfix: "-protected", + }, + { + Pattern: "custom-.*-pattern", + }, + }, + } + config.SetDefaults() + Expect(authzconfig.Validate(config)).To(Succeed()) + }) + + It("returns true for valid namespace names", func() { + Expect(config.IsAllowedNamespaceName("valid-namespace")).To(BeTrue()) + Expect(config.IsAllowedNamespaceName("another-valid-namespace")).To(BeTrue()) + }) + + It("returns false for exact match namespace names", func() { + Expect(config.IsAllowedNamespaceName("foobar")).To(BeFalse()) + }) + + It("returns false for namespace names starting with 'protected-'", func() { + Expect(config.IsAllowedNamespaceName("protected-system")).To(BeFalse()) + Expect(config.IsAllowedNamespaceName("protected-public")).To(BeFalse()) + }) + + It("returns false for namespace names ending with '-protected'", func() { + Expect(config.IsAllowedNamespaceName("default-protected")).To(BeFalse()) + Expect(config.IsAllowedNamespaceName("my-protected")).To(BeFalse()) + }) + + It("returns false for namespace names matching 'custom-.*-pattern'", func() { + Expect(config.IsAllowedNamespaceName("custom-foo-pattern")).To(BeFalse()) + Expect(config.IsAllowedNamespaceName("custom-bar-pattern")).To(BeFalse()) + }) + + It("should not validate an invalid pattern", func() { + config.ProtectedNamespaces = append(config.ProtectedNamespaces, authzconfig.ProtectedNamespace{ + Pattern: "[", + }) + Expect(authzconfig.Validate(config)).To(HaveOccurred()) + }) + }) +}) diff --git a/internal/controller/core/authorization/config/testdata/config_invalid.yaml b/internal/controller/core/authorization/config/testdata/config_invalid.yaml new file mode 100644 index 0000000..50a269a --- /dev/null +++ b/internal/controller/core/authorization/config/testdata/config_invalid.yaml @@ -0,0 +1 @@ +"invalid" diff --git a/internal/controller/core/authorization/config/testdata/config_valid.yaml b/internal/controller/core/authorization/config/testdata/config_valid.yaml new file mode 100644 index 0000000..ed806ca --- /dev/null +++ b/internal/controller/core/authorization/config/testdata/config_valid.yaml @@ -0,0 +1,65 @@ +admin: + additionalSubjects: + - kind: User + name: system-admin + apiGroup: rbac.authorization.k8s.io + - kind: Group + name: system:admins + apiGroup: rbac.authorization.k8s.io + namespaceScoped: + labels: + openmcp.cloud/aggregate-to-admin: "true" + clusterRoleSelectors: + - matchLabels: + openmcp.cloud/aggregate-to-admin: "true" + rules: + - apiGroups: + - "" + resources: + - pods + verbs: + - create + - update + - patch + - delete + clusterScoped: + rules: + - apiGroups: + - "" + resources: + - namespaces + verbs: + - create + - update + - patch + - delete +view: + additionalSubjects: + - kind: ServiceAccount + name: manager + namespace: openmcp-system + namespaceScoped: + labels: + openmcp.cloud/aggregate-to-view: "true" + clusterRoleSelectors: + - matchLabels: + openmcp.cloud/aggregate-to-view: "true" + rules: + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - watch + clusterScoped: + rules: + - apiGroups: + - "" + resources: + - namespaces + verbs: + - get + - list + - watch \ No newline at end of file diff --git a/internal/controller/core/authorization/config/utils.go b/internal/controller/core/authorization/config/utils.go new file mode 100644 index 0000000..4e7ff91 --- /dev/null +++ b/internal/controller/core/authorization/config/utils.go @@ -0,0 +1,22 @@ +package config + +import ( + "fmt" + "os" + + "sigs.k8s.io/yaml" +) + +// LoadConfig reads the configuration file from a given path and parses it into an AuthorizationConfig object. +func LoadConfig(path string) (*AuthorizationConfig, error) { + data, err := os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("error reading config file: %w", err) + } + cfg := &AuthorizationConfig{} + err = yaml.Unmarshal(data, cfg) + if err != nil { + return nil, fmt.Errorf("error parsing config file: %w", err) + } + return cfg, nil +} diff --git a/internal/controller/core/authorization/config/utils_test.go b/internal/controller/core/authorization/config/utils_test.go new file mode 100644 index 0000000..e533ca8 --- /dev/null +++ b/internal/controller/core/authorization/config/utils_test.go @@ -0,0 +1,91 @@ +package config_test + +import ( + "path" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + rbacv1 "k8s.io/api/rbac/v1" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("Auth Config Utils", func() { + It("should load the config from file", func() { + authzConfig, err := config.LoadConfig(path.Join("testdata", "config_valid.yaml")) + Expect(err).ToNot(HaveOccurred()) + Expect(authzConfig).ToNot(BeNil()) + + admin := authzConfig.Admin + adminNamespaceScoped := admin.NamespaceScoped + adminClusterScoped := admin.ClusterScoped + + Expect(adminNamespaceScoped.ClusterRoleSelectors).To(HaveLen(1)) + Expect(adminNamespaceScoped.ClusterRoleSelectors[0].MatchLabels).To(HaveKeyWithValue(openmcpv1alpha1.AdminNamespaceScopeMatchLabel, "true")) + Expect(adminNamespaceScoped.Labels).To(HaveKeyWithValue(openmcpv1alpha1.AdminNamespaceScopeMatchLabel, "true")) + + Expect(adminNamespaceScoped.Rules).To(HaveLen(1)) + Expect(adminNamespaceScoped.Rules).To(ConsistOf(rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"create", "update", "patch", "delete"}, + })) + + Expect(adminClusterScoped.ClusterRoleSelectors).To(HaveLen(0)) + Expect(adminClusterScoped.Labels).To(HaveLen(0)) + + Expect(adminClusterScoped.Rules).To(HaveLen(1)) + Expect(adminClusterScoped.Rules).To(ConsistOf(rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"create", "update", "patch", "delete"}, + })) + + Expect(authzConfig.Admin.AdditionalSubjects).To(HaveLen(2)) + Expect(authzConfig.Admin.AdditionalSubjects).To(ConsistOf( + rbacv1.Subject{Kind: "User", Name: "system-admin", APIGroup: rbacv1.GroupName}, + rbacv1.Subject{Kind: "Group", Name: "system:admins", APIGroup: rbacv1.GroupName}, + )) + + view := authzConfig.View + viewNamespaceScoped := view.NamespaceScoped + viewClusterScoped := view.ClusterScoped + + Expect(viewNamespaceScoped.ClusterRoleSelectors).To(HaveLen(1)) + Expect(viewNamespaceScoped.ClusterRoleSelectors[0].MatchLabels).To(HaveKeyWithValue(openmcpv1alpha1.ViewNamespaceScopeMatchLabel, "true")) + Expect(viewNamespaceScoped.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ViewNamespaceScopeMatchLabel, "true")) + + Expect(viewNamespaceScoped.Rules).To(HaveLen(1)) + Expect(viewNamespaceScoped.Rules).To(ConsistOf(rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "list", "watch"}, + })) + + Expect(viewClusterScoped.ClusterRoleSelectors).To(HaveLen(0)) + Expect(viewClusterScoped.Labels).To(HaveLen(0)) + + Expect(viewClusterScoped.Rules).To(HaveLen(1)) + Expect(viewClusterScoped.Rules).To(ConsistOf(rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"get", "list", "watch"}, + })) + + Expect(authzConfig.View.AdditionalSubjects).To(ConsistOf( + rbacv1.Subject{Kind: "ServiceAccount", Name: "manager", Namespace: "openmcp-system", APIGroup: ""}, + )) + }) + + It("should fail to load the config from file", func() { + authzConfig, err := config.LoadConfig(path.Join("testdata", "config_invalid.yaml")) + Expect(err).To(HaveOccurred()) + Expect(authzConfig).To(BeNil()) + + authzConfig, err = config.LoadConfig(path.Join("testdata", "config_missing.yaml")) + Expect(err).To(HaveOccurred()) + Expect(authzConfig).To(BeNil()) + }) +}) diff --git a/internal/controller/core/authorization/controller.go b/internal/controller/core/authorization/controller.go new file mode 100644 index 0000000..a14f195 --- /dev/null +++ b/internal/controller/core/authorization/controller.go @@ -0,0 +1,600 @@ +package authorization + +import ( + "context" + "fmt" + "reflect" + "strings" + "time" + + "github.com/openmcp-project/controller-utils/pkg/logging" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" + "github.com/openmcp-project/mcp-operator/internal/components" + authzconfig "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/config" + "github.com/openmcp-project/mcp-operator/internal/utils" + apiserverutils "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +const ControllerName = "Authorization" + +type AuthorizationReconciler struct { + Client client.Client + Config *authzconfig.AuthorizationConfig + APIServerAccess apiserverutils.APIServerAccess +} + +func NewAuthorizationReconciler(c client.Client, config *authzconfig.AuthorizationConfig) *AuthorizationReconciler { + config.SetDefaults() + return &AuthorizationReconciler{ + Client: c, + Config: config, + APIServerAccess: &apiserverutils.APIServerAccessImpl{ + NewClient: client.New, + }, + } +} + +// SetAPIServerAccess sets the APIServerAccess implementation. +// Used for testing. +func (ar *AuthorizationReconciler) SetAPIServerAccess(apiServerAccess apiserverutils.APIServerAccess) { + ar.APIServerAccess = apiServerAccess +} + +// +kubebuilder:rbac:groups=authorization.k8s.io,resources=authorizations,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=authorization.k8s.io,resources=authorizations/status,verbs=get;update;patch + +func (ar *AuthorizationReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log, ctx := utils.InitializeControllerLogger(ctx, ControllerName) + log.Debug(cconst.MsgStartReconcile) + + rr := ar.reconcile(ctx, req) + rr.LogRequeue(log, logging.DEBUG) + if rr.Component == nil { + return rr.Result, rr.ReconcileError + } + if rr.ReconcileError != nil && len(rr.Conditions) == 0 { // shortcut so we don't have to add the same ready condition to each return statement (won't work anymore if we have multiple conditions) + rr.Conditions = authorizationConditions(false, cconst.ReasonReconciliationError, cconst.MessageReconciliationError) + } + return componentutils.UpdateStatus(ctx, ar.Client, rr) +} + +func (ar *AuthorizationReconciler) reconcile(ctx context.Context, req ctrl.Request) componentutils.ReconcileResult[*openmcpv1alpha1.Authorization] { + // get the logger + log := logging.FromContextOrPanic(ctx) + + authz := &openmcpv1alpha1.Authorization{} + if err := ar.Client.Get(ctx, req.NamespacedName, authz); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("Resource not found") + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{} + } + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("unable to get resource '%s' from cluster: %w", req.NamespacedName.String(), err), cconst.ReasonCrateClusterInteractionProblem)} + } + + // handle operation annotation + if authz.GetAnnotations() != nil { + op, ok := authz.GetAnnotations()[openmcpv1alpha1.OperationAnnotation] + if ok { + switch op { + case openmcpv1alpha1.OperationAnnotationValueIgnore: + log.Info("Ignoring resource due to ignore operation annotation") + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{} + case openmcpv1alpha1.OperationAnnotationValueReconcile: + log.Debug("Removing reconcile operation annotation from resource") + if err := componentutils.PatchAnnotation(ctx, ar.Client, authz, openmcpv1alpha1.OperationAnnotation, "", componentutils.ANNOTATION_DELETE); err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing operation annotation: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + } + } + + // checking for APIServer component + log.Debug("Checking for APIServer dependency") + ownCPGeneration, ownICGeneration, _ := componentutils.GetCreatedFromGeneration(authz) + as := &openmcpv1alpha1.APIServer{} + as.SetName(authz.Name) + as.SetNamespace(authz.Namespace) + + if err := ar.Client.Get(ctx, client.ObjectKeyFromObject(as), as); err != nil { + if !apierrors.IsNotFound(err) { + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{Component: authz, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error fetching APIServer resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + // APIServer not found + as = nil + } + if as == nil || !componentutils.IsDependencyReady(as, ownCPGeneration, ownICGeneration) { + log.Info("APIServer not found or it isn't ready") + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{Component: authz, Conditions: authorizationConditions(false, cconst.ReasonWaitingForDependencies, "Waiting for APIServer dependency to be ready"), Result: reconcile.Result{RequeueAfter: 60 * time.Second}} + } + + log.Debug("APIServer dependency is ready") + + if as.Status.AdminAccess == nil || as.Status.AdminAccess.Kubeconfig == "" { + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{Component: authz, ReconcileError: openmcperrors.WithReason(fmt.Errorf("APIServer dependency is ready, but no kubeconfig could be found in its status"), cconst.ReasonDependencyStatusInvalid)} + } + + apiServerClient, err := ar.APIServerAccess.GetAdminAccessClient(as, client.Options{}) + if err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{Component: authz, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error creating client from APIServer kubeconfig: %w", err), cconst.ReasonDependencyStatusInvalid)} + } + + old := authz.DeepCopy() + if !authz.DeletionTimestamp.IsZero() { + log.Info("Deleting Authorization") + if componentutils.HasAnyDependencyFinalizer(authz) { + depString := strings.Join(sets.List(componentutils.GetDependents(authz)), ", ") + log.Info("Authorization cannot be deleted, because it still contains dependency finalizers", "dependingComponents", depString) + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{Component: authz, Conditions: authorizationConditions(true, cconst.ReasonDeletionWaitingForDependingComponents, fmt.Sprintf("Deletion is waiting for the following dependencies to be removed: [%s]", depString)), Result: ctrl.Result{RequeueAfter: 60 * time.Second}} + } + + log.Info("Deleting Authorization") + if err = ar.deleteAuthorization(ctx, apiServerClient); err != nil { + log.Error(err, "error deleting authorization resources") + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{Component: authz, ReconcileError: openmcperrors.WithReason(err, cconst.ReasonManagingAuthorization)} + } + + // remove the auth dependency finalizer from the APIServer resource if the auth resource is being deleted + err = componentutils.EnsureDependencyFinalizer(ctx, ar.Client, as, authz, false) + if err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{Component: authz, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing dependency finalizer from APIServer component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + + // remove finalizer from authz resource + old := authz.DeepCopy() + changed := controllerutil.RemoveFinalizer(authz, openmcpv1alpha1.AuthorizationComponent.Finalizer()) + if changed { + if err := ar.Client.Patch(ctx, authz, client.MergeFrom(old)); err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing finalizer from Authorization: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + } else { + log.Info("Triggering creation/update of Authorization") + + if controllerutil.AddFinalizer(authz, openmcpv1alpha1.AuthorizationComponent.Finalizer()) { + log.Debug("Adding finalizer to Authorization resource") + if err := ar.Client.Patch(ctx, authz, client.MergeFrom(old)); err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error patching finalizer on Authorization: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + + log.Debug("Ensuring dependency finalizer on APIServer resource") + err = componentutils.EnsureDependencyFinalizer(ctx, ar.Client, as, authz, true) + if err != nil { + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{Component: authz, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error setting dependency finalizer on APIServer component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + + log.Info("Creating/Updating Authorization") + if err = ar.ensureClusterRoles(ctx, apiServerClient, authz); err != nil { + log.Error(err, "error creating/updating cluster roles") + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{OldComponent: old, Component: authz, ReconcileError: openmcperrors.WithReason(err, cconst.ReasonManagingAuthorization)} + } + + if err = ar.ensureClusterRoleBindings(ctx, apiServerClient, authz); err != nil { + log.Error(err, "error ensuring cluster role bindings") + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{OldComponent: old, Component: authz, ReconcileError: openmcperrors.WithReason(err, cconst.ReasonManagingAuthorization)} + } + + if err = ar.ensureRoleBindings(ctx, apiServerClient, authz); err != nil { + log.Error(err, "error ensuring role bindings") + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{OldComponent: old, Component: authz, ReconcileError: openmcperrors.WithReason(err, cconst.ReasonManagingAuthorization)} + } + } + + return componentutils.ReconcileResult[*openmcpv1alpha1.Authorization]{OldComponent: old, Component: authz, Conditions: authorizationConditions(true, "", "")} +} + +// ensureClusterRoles creates or updates the cluster roles as defined in the configuration +func (ar *AuthorizationReconciler) ensureClusterRoles(ctx context.Context, apiServerClient client.Client, authz *openmcpv1alpha1.Authorization) error { + log, ctx := logging.FromContextOrNew(ctx, []interface{}{}) + + createOrUpdateClusterRole := func(name string, cfg *authzconfig.RulesConfig, setLabels, setAggregation bool) error { + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + + result, err := controllerutil.CreateOrUpdate(ctx, apiServerClient, clusterRole, func() error { + clusterRole.Rules = make([]rbacv1.PolicyRule, len(cfg.Rules)) + copy(clusterRole.Rules, cfg.Rules) + + if clusterRole.Name == openmcpv1alpha1.AdminClusterScopeStandardRulesRole && len(authz.Status.UserNamespaces) > 0 { + nsRule := rbacv1.PolicyRule{ + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"update", "patch", "delete"}, + ResourceNames: make([]string, len(authz.Status.UserNamespaces)), + } + copy(nsRule.ResourceNames, authz.Status.UserNamespaces) + + clusterRole.Rules = append(clusterRole.Rules, nsRule) + } + + clusterRole.Labels = map[string]string{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + } + + if setLabels { + for key, val := range cfg.Labels { + clusterRole.Labels[key] = val + } + } + + if setAggregation { + clusterRole.AggregationRule = &rbacv1.AggregationRule{ + ClusterRoleSelectors: make([]metav1.LabelSelector, len(cfg.ClusterRoleSelectors)), + } + copy(clusterRole.AggregationRule.ClusterRoleSelectors, cfg.ClusterRoleSelectors) + for _, comp := range components.Registry.GetKnownComponents() { + ls := comp.LabelSelectorsForRole(name) + if ls != nil { + clusterRole.AggregationRule.ClusterRoleSelectors = append(clusterRole.AggregationRule.ClusterRoleSelectors, ls...) + } + } + } else { + clusterRole.AggregationRule = nil + } + return nil + }) + + if err != nil { + return err + } + + log.Debug("Cluster role created/updated", "result", result, cconst.KeyResource, clusterRole.Name) + return nil + } + + allErrs := field.ErrorList{} + clusterRoleNames := openmcpv1alpha1.GetClusterRoleNames() + + for _, name := range clusterRoleNames { + isAggregatedRole := openmcpv1alpha1.IsAggregatedRole(name) + rulesConfig := ar.Config.GetRulesConfig(name) + + if err := createOrUpdateClusterRole(name, rulesConfig, !isAggregatedRole, isAggregatedRole); err != nil { + allErrs = append(allErrs, field.InternalError(field.NewPath(name), err)) + } + } + + return allErrs.ToAggregate() +} + +// updateClusterRoleBindingSubjects updates the subjects of a cluster role binding +func updateClusterRoleBindingSubjects(clusterRoleBinding *rbacv1.ClusterRoleBinding, staticSubjects []rbacv1.Subject, dynamicSubjects []openmcpv1alpha1.Subject) { + numSubjects := len(staticSubjects) + len(dynamicSubjects) + + if numSubjects == 0 { + clusterRoleBinding.Subjects = nil + return + } + + clusterRoleBinding.Subjects = make([]rbacv1.Subject, 0, numSubjects) + clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, staticSubjects...) + + for _, subject := range dynamicSubjects { + clusterRoleBinding.Subjects = append(clusterRoleBinding.Subjects, rbacv1.Subject{ + Kind: subject.Kind, + Name: subject.Name, + Namespace: subject.Namespace, + APIGroup: subject.APIGroup, + }) + } +} + +// ensureClusterRoleBindings is a cyclic task that creates or updates the admin and view cluster role bindings +func (ar *AuthorizationReconciler) ensureClusterRoleBindings(ctx context.Context, apiServerClient client.Client, authz *openmcpv1alpha1.Authorization) error { + adminClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminClusterRoleBinding, + }, + } + + allErrs := field.ErrorList{} + + adminRole := authz.Spec.GetRoleForName(openmcpv1alpha1.RoleBindingRoleAdmin) + _, err := controllerutil.CreateOrUpdate(ctx, apiServerClient, adminClusterRoleBinding, func() error { + adminClusterRoleBinding.Labels = map[string]string{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + } + + adminClusterRoleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: openmcpv1alpha1.AdminClusterScopeRole, + } + + var dynamicSubjects []openmcpv1alpha1.Subject + if adminRole != nil { + dynamicSubjects = adminRole.Subjects + } + updateClusterRoleBindingSubjects(adminClusterRoleBinding, ar.Config.Admin.AdditionalSubjects, dynamicSubjects) + + return nil + }) + + if err != nil { + allErrs = append(allErrs, field.InternalError(field.NewPath("admin"), err)) + } + + viewClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ViewClusterRoleBinding, + }, + } + + viewRole := authz.Spec.GetRoleForName(openmcpv1alpha1.RoleBindingRoleView) + _, err = controllerutil.CreateOrUpdate(ctx, apiServerClient, viewClusterRoleBinding, func() error { + viewClusterRoleBinding.Labels = map[string]string{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + } + + viewClusterRoleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: openmcpv1alpha1.ViewClusterScopeRole, + } + + var dynamicSubjects []openmcpv1alpha1.Subject + if viewRole != nil { + dynamicSubjects = viewRole.Subjects + } + updateClusterRoleBindingSubjects(viewClusterRoleBinding, ar.Config.View.AdditionalSubjects, dynamicSubjects) + + return nil + }) + + if err != nil { + allErrs = append(allErrs, field.InternalError(field.NewPath("view"), err)) + } + + return allErrs.ToAggregate() +} + +// updateRoleBindingSubjects updates the subjects of a role binding +func updateRoleBindingSubjects(roleBinding *rbacv1.RoleBinding, staticSubjects []rbacv1.Subject, dynamicSubjects []openmcpv1alpha1.Subject) { + numSubjects := len(staticSubjects) + len(dynamicSubjects) + + if numSubjects == 0 { + roleBinding.Subjects = nil + return + } + + roleBinding.Subjects = make([]rbacv1.Subject, 0, numSubjects) + roleBinding.Subjects = append(roleBinding.Subjects, staticSubjects...) + + for _, subject := range dynamicSubjects { + roleBinding.Subjects = append(roleBinding.Subjects, rbacv1.Subject{ + Kind: subject.Kind, + Name: subject.Name, + Namespace: subject.Namespace, + APIGroup: subject.APIGroup, + }) + } +} + +// ensureRoleBindings creates or updates the admin and view role bindings +func (ar *AuthorizationReconciler) ensureRoleBindings(ctx context.Context, apiServerClient client.Client, authz *openmcpv1alpha1.Authorization) error { + allErrs := field.ErrorList{} + adminRole := authz.Spec.GetRoleForName(openmcpv1alpha1.RoleBindingRoleAdmin) + viewRole := authz.Spec.GetRoleForName(openmcpv1alpha1.RoleBindingRoleView) + + for _, ns := range authz.Status.UserNamespaces { + namespace := &corev1.Namespace{} + if err := apiServerClient.Get(ctx, client.ObjectKey{Name: ns}, namespace); err != nil { + allErrs = append(allErrs, field.InternalError(field.NewPath(ns), err)) + continue + } + + // ignore namespace with deletion timestamp + if namespace.DeletionTimestamp != nil { + continue + } + + adminRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminRoleBinding, + Namespace: ns, + }, + } + + _, err := controllerutil.CreateOrUpdate(ctx, apiServerClient, adminRoleBinding, func() error { + adminRoleBinding.Labels = map[string]string{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + } + + adminRoleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: openmcpv1alpha1.AdminNamespaceScopeRole, + } + + var dynamicSubjects []openmcpv1alpha1.Subject + if adminRole != nil { + dynamicSubjects = adminRole.Subjects + } + updateRoleBindingSubjects(adminRoleBinding, ar.Config.Admin.AdditionalSubjects, dynamicSubjects) + + return nil + }) + + if err != nil { + allErrs = append(allErrs, field.InternalError(field.NewPath(ns).Child("admin"), err)) + } + + viewRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ViewRoleBinding, + Namespace: ns, + }, + } + + _, err = controllerutil.CreateOrUpdate(ctx, apiServerClient, viewRoleBinding, func() error { + viewRoleBinding.Labels = map[string]string{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + } + + viewRoleBinding.RoleRef = rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "ClusterRole", + Name: openmcpv1alpha1.ViewNamespaceScopeRole, + } + + var dynamicSubjects []openmcpv1alpha1.Subject + if viewRole != nil { + dynamicSubjects = viewRole.Subjects + } + updateRoleBindingSubjects(viewRoleBinding, ar.Config.View.AdditionalSubjects, dynamicSubjects) + + return nil + }) + + if err != nil { + allErrs = append(allErrs, field.InternalError(field.NewPath(ns).Child("view"), err)) + } + + } + + return allErrs.ToAggregate() +} + +// deleteAuthorization deletes all cluster roles, cluster role bindings and role bindings +func (ar *AuthorizationReconciler) deleteAuthorization(ctx context.Context, apiServerClient client.Client) error { + allErrs := field.ErrorList{} + + path := field.NewPath("roleBindings") + roleBindings := rbacv1.RoleBindingList{} + if err := apiServerClient.List(ctx, &roleBindings, client.MatchingLabels{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + }); err != nil { + allErrs = append(allErrs, field.InternalError(path, err)) + } + + for _, roleBinding := range roleBindings.Items { + if err := apiServerClient.Delete(ctx, &roleBinding); err != nil { + allErrs = append(allErrs, field.InternalError(path.Child(roleBinding.Name), err)) + } + } + + path = field.NewPath("clusterRoleBindings") + clusterRoleBindings := rbacv1.ClusterRoleBindingList{} + if err := apiServerClient.List(ctx, &clusterRoleBindings, client.MatchingLabels{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + }); err != nil { + allErrs = append(allErrs, field.InternalError(path, err)) + } + + for _, clusterRoleBinding := range clusterRoleBindings.Items { + if err := apiServerClient.Delete(ctx, &clusterRoleBinding); err != nil { + allErrs = append(allErrs, field.InternalError(path.Child(clusterRoleBinding.Name), err)) + } + } + + path = field.NewPath("clusterRoles") + clusterRoles := rbacv1.ClusterRoleList{} + if err := apiServerClient.List(ctx, &clusterRoles, client.MatchingLabels{ + openmcpv1alpha1.ManagedByLabel: ControllerName, + }); err != nil { + allErrs = append(allErrs, field.InternalError(path, err)) + } + + for _, clusterRole := range clusterRoles.Items { + if err := apiServerClient.Delete(ctx, &clusterRole); err != nil { + allErrs = append(allErrs, field.InternalError(path.Child(clusterRole.Name), err)) + } + } + + return allErrs.ToAggregate() +} + +// namespacesTask is a cyclic task that updates the Authorization object with the list of user namespaces from the APIServer +func (ar *AuthorizationReconciler) namespacesTask(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient, apiServerClient client.Client) error { + // get the authorization object + authz := &openmcpv1alpha1.Authorization{} + if err := crateClient.Get(ctx, client.ObjectKeyFromObject(as), authz); err != nil { + // the APIServer has no corresponding Authorization object + return nil + } + + old := authz.DeepCopy() + + // get the namespaces from the APIServer + nsList := &corev1.NamespaceList{} + if err := apiServerClient.List(ctx, nsList); err != nil { + return fmt.Errorf("error fetching namespaces from APIServer: %w", err) + } + + userNamespaces := make([]string, 0, len(nsList.Items)) + + // filter out the namespaces that are not allowed + for _, ns := range nsList.Items { + if !ar.Config.IsAllowedNamespaceName(ns.Name) { + continue + } + + // ignore namespace with deletion timestamp + if ns.DeletionTimestamp != nil { + continue + } + + userNamespaces = append(userNamespaces, ns.Name) + } + + if len(userNamespaces) > 0 { + authz.Status.UserNamespaces = userNamespaces + } else { + authz.Status.UserNamespaces = nil + } + + if !reflect.DeepEqual(old.Status, authz.Status) { + if err := crateClient.Status().Patch(ctx, authz, client.MergeFrom(old)); err != nil { + return fmt.Errorf("error updating Authorization status: %w", err) + } + + if authz.GetAnnotations() == nil { + authz.SetAnnotations(make(map[string]string)) + } + authz.Annotations[openmcpv1alpha1.OperationAnnotation] = openmcpv1alpha1.OperationAnnotationValueReconcile + if err := crateClient.Update(ctx, authz); err != nil { + return fmt.Errorf("error updating Authorization object: %w", err) + } + } + + return nil +} + +func (ar *AuthorizationReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&openmcpv1alpha1.Authorization{}, builder.WithPredicates(componentutils.DefaultComponentControllerPredicates())). + Watches(&openmcpv1alpha1.APIServer{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(componentutils.StatusChangedPredicate{})). + Complete(ar) +} + +// RegisterTasks registers the cyclic tasks for the AuthorizationReconciler +func (ar *AuthorizationReconciler) RegisterTasks(worker apiserverutils.Worker) *AuthorizationReconciler { + worker.RegisterTask("authz_namespaces", ar.namespacesTask) + return ar +} + +func authorizationConditions(ready bool, reason, message string) []openmcpv1alpha1.ComponentCondition { + return []openmcpv1alpha1.ComponentCondition{ + componentutils.NewCondition(openmcpv1alpha1.AuthorizationComponent.HealthyCondition(), openmcpv1alpha1.ComponentConditionStatusFromBool(ready), reason, message), + } +} diff --git a/internal/controller/core/authorization/controller_test.go b/internal/controller/core/authorization/controller_test.go new file mode 100644 index 0000000..25c7a93 --- /dev/null +++ b/internal/controller/core/authorization/controller_test.go @@ -0,0 +1,1132 @@ +package authorization_test + +import ( + "strings" + + components "github.com/openmcp-project/mcp-operator/internal/components" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization" + "github.com/openmcp-project/mcp-operator/internal/controller/core/authorization/config" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +const ( + adminVerbs = "create,update,patch,delete" + viewVerbs = "get,list,watch" + + namespaceScopedResource = "secrets" + clusterScopedResource = "namespaces" + + aggregateToAdminLabel = "rbac.dummy.local/aggregate-to-admin" + aggregateToViewLabel = "rbac.dummy.local/aggregate-to-view" + + aggregateToAdminClusterScopedLabel = "rbac.dummy.local/aggregate-to-admin-clusterscoped" + aggregateToViewClusterScopedLabel = "rbac.dummy.local/aggregate-to-view-clusterscoped" + + authzReconciler = "authz" +) + +func getReconciler(c ...client.Client) reconcile.Reconciler { + return authorization.NewAuthorizationReconciler(c[0], &config.AuthorizationConfig{ + Admin: config.RoleConfig{ + AdditionalSubjects: []rbacv1.Subject{ + { + Kind: rbacv1.UserKind, + Name: "static-admin", + APIGroup: rbacv1.GroupName, + }, + { + Kind: rbacv1.ServiceAccountKind, + Name: "static-manager", + Namespace: "openmcp-system", + }, + }, + NamespaceScoped: config.RulesConfig{ + Labels: map[string]string{ + aggregateToAdminLabel: "true", + }, + ClusterRoleSelectors: []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + aggregateToAdminLabel: "true", + }, + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{namespaceScopedResource}, + Verbs: strings.Split(adminVerbs, ","), + }, + }, + }, + ClusterScoped: config.RulesConfig{ + Labels: map[string]string{ + aggregateToAdminClusterScopedLabel: "true", + }, + ClusterRoleSelectors: []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + aggregateToAdminClusterScopedLabel: "true", + }, + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{clusterScopedResource}, + Verbs: strings.Split(adminVerbs, ","), + }, + }, + }, + }, + View: config.RoleConfig{ + AdditionalSubjects: []rbacv1.Subject{ + { + Kind: rbacv1.GroupKind, + Name: "static-auditors", + APIGroup: rbacv1.GroupName, + }, + }, + NamespaceScoped: config.RulesConfig{ + Labels: map[string]string{ + aggregateToViewLabel: "true", + }, + ClusterRoleSelectors: []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + aggregateToViewLabel: "true", + }, + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{namespaceScopedResource}, + Verbs: strings.Split(viewVerbs, ","), + }, + }, + }, + ClusterScoped: config.RulesConfig{ + Labels: map[string]string{ + aggregateToViewClusterScopedLabel: "true", + }, + ClusterRoleSelectors: []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + aggregateToViewClusterScopedLabel: "true", + }, + }, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{clusterScopedResource}, + Verbs: strings.Split(viewVerbs, ","), + }, + }, + }, + }, + }) +} + +func testEnvWithAPIServerAccess(testDataPathSegments ...string) *testing.ComplexEnvironment { + env := testutils.DefaultTestSetupBuilder(testDataPathSegments...).WithFakeClient(testutils.APIServerCluster, testutils.Scheme).WithReconcilerConstructor(authzReconciler, getReconciler, testutils.CrateCluster).Build() + env.Reconcilers[authzReconciler].(*authorization.AuthorizationReconciler).SetAPIServerAccess(&testutils.TestAPIServerAccess{Client: env.Client(testutils.APIServerCluster)}) + return env +} + +var _ = Describe("CO-1153 Authorization Controller", func() { + It("should set the status condition to false when there is no APIServer available", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-01") + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + res := env.ShouldReconcile(authzReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + + Expect(authz.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForDependencies, + }), + )) + }) + + It("should set the ready condition to false when APIServer is not ready", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-02") + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + res := env.ShouldReconcile(authzReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + + Expect(authz.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForDependencies, + }), + )) + }) + + It("should fail to reconcile and set the status condition to false when APIServer status has no access kubeconfig", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-03") + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldNotReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + + Expect(authz.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + }), + )) + }) + + It("should set the finalizers", func() { + var err error + env := testEnvWithAPIServerAccess("testdata", "test-04") + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Finalizers).To(ContainElement(openmcpv1alpha1.AuthorizationComponent.Finalizer())) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).NotTo(HaveOccurred()) + + authzComp := components.Component(authz) + Expect(as.Finalizers).To(ContainElements(authzComp.Type().DependencyFinalizer())) + }) + + Context("admin cluster roles/bindings", func() { + var err error + var env *testing.ComplexEnvironment + + BeforeEach(func() { + env = testEnvWithAPIServerAccess("testdata", "test-04") + }) + + It("should create cluster roles", func() { + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + adminNamespaceScopeClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminNamespaceScopeRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminNamespaceScopeClusterRole), adminNamespaceScopeClusterRole) + Expect(err).ToNot(HaveOccurred()) + verifyAggregationClusterRole(adminNamespaceScopeClusterRole) + + adminClusterScopeClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminClusterScopeRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminClusterScopeClusterRole), adminClusterScopeClusterRole) + Expect(err).ToNot(HaveOccurred()) + verifyAggregationClusterRole(adminClusterScopeClusterRole) + + adminNamespaceScopeClusterRoleBinding := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminNamespaceScopeStandardRulesRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminNamespaceScopeClusterRoleBinding), adminNamespaceScopeClusterRoleBinding) + Expect(err).ToNot(HaveOccurred()) + verifyStandardClusterRole(adminNamespaceScopeClusterRoleBinding) + + adminClusterScopeClusterRoleBinding := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminClusterScopeStandardRulesRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminClusterScopeClusterRoleBinding), adminClusterScopeClusterRoleBinding) + Expect(err).ToNot(HaveOccurred()) + verifyStandardClusterRole(adminClusterScopeClusterRoleBinding) + }) + + It("should create cluster role bindings", func() { + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + // set the defaults for the API groups + authz.Spec.Default() + err = env.Client(testutils.CrateCluster).Update(env.Ctx, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + adminClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminClusterRoleBinding, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminClusterRoleBinding), adminClusterRoleBinding) + Expect(err).ToNot(HaveOccurred()) + Expect(adminClusterRoleBinding.RoleRef.Name).To(Equal(openmcpv1alpha1.AdminClusterScopeRole)) + Expect(adminClusterRoleBinding.Subjects).To(ConsistOf( + rbacv1.Subject{ + Kind: rbacv1.UserKind, + Name: "admin", + APIGroup: rbacv1.GroupName, + }, + rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: "pipeline", + Namespace: "automate", + }, + rbacv1.Subject{ + Kind: rbacv1.UserKind, + Name: "static-admin", + APIGroup: rbacv1.GroupName, + }, + rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: "static-manager", + Namespace: "openmcp-system", + })) + }) + }) + + Context("view cluster roles/bindings", func() { + var err error + var env *testing.ComplexEnvironment + + BeforeEach(func() { + env = testEnvWithAPIServerAccess("testdata", "test-04") + }) + + It("should create cluster roles", func() { + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + viewNamespaceScopeClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ViewNamespaceScopeRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(viewNamespaceScopeClusterRole), viewNamespaceScopeClusterRole) + Expect(err).ToNot(HaveOccurred()) + verifyAggregationClusterRole(viewNamespaceScopeClusterRole) + + viewClusterScopeClusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ViewClusterScopeRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(viewClusterScopeClusterRole), viewClusterScopeClusterRole) + Expect(err).ToNot(HaveOccurred()) + verifyAggregationClusterRole(viewClusterScopeClusterRole) + + viewNamespaceScopeClusterRoleBinding := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ViewNamespaceScopeStandardRulesRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(viewNamespaceScopeClusterRoleBinding), viewNamespaceScopeClusterRoleBinding) + Expect(err).ToNot(HaveOccurred()) + verifyStandardClusterRole(viewNamespaceScopeClusterRoleBinding) + + viewClusterScopeClusterRoleBinding := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ViewClusterScopeStandardClusterRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(viewClusterScopeClusterRoleBinding), viewClusterScopeClusterRoleBinding) + Expect(err).ToNot(HaveOccurred()) + verifyStandardClusterRole(viewClusterScopeClusterRoleBinding) + }) + + It("should create cluster role bindings", func() { + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + // set the defaults for the API groups + authz.Spec.Default() + err = env.Client(testutils.CrateCluster).Update(env.Ctx, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Finalizers).To(ContainElement(openmcpv1alpha1.AuthorizationComponent.Finalizer())) + Expect(authz.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + viewClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ViewClusterRoleBinding, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(viewClusterRoleBinding), viewClusterRoleBinding) + Expect(err).ToNot(HaveOccurred()) + Expect(viewClusterRoleBinding.RoleRef.Name).To(Equal(openmcpv1alpha1.ViewClusterScopeRole)) + Expect(viewClusterRoleBinding.Subjects).To(ConsistOf( + rbacv1.Subject{ + Kind: rbacv1.GroupKind, + Name: "auditors", + APIGroup: rbacv1.GroupName, + }, + rbacv1.Subject{ + Kind: rbacv1.GroupKind, + Name: "static-auditors", + APIGroup: rbacv1.GroupName, + })) + }) + }) + + It("should add namespace resource name in cluster role for user namespace", func() { + var err error + env := testEnvWithAPIServerAccess("testdata", "test-04") + + // create user namespace + namespace := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user", + }, + } + err = env.Client(testutils.APIServerCluster).Create(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + testWorker := testutils.NewTestWorker(env.Client(testutils.CrateCluster), env.Client(testutils.APIServerCluster)) + controller := env.Reconciler(authzReconciler).(*authorization.AuthorizationReconciler) + controller.RegisterTasks(testWorker) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).ToNot(HaveOccurred()) + + // run tasks to update the user namespaces in the authorization status + // it then should contain the user namespace name and the reconcile annotation should be set + err = testWorker.RunTasks(env.Ctx, as) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Annotations).To(HaveKeyWithValue(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile)) + Expect(authz.Status.UserNamespaces).To(ConsistOf(namespace.Name)) + + req = testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + // the user namespace should be added to the cluster role + adminClusterScopeRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminClusterScopeStandardRulesRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminClusterScopeRole), adminClusterScopeRole) + Expect(err).ToNot(HaveOccurred()) + Expect(adminClusterScopeRole.Rules).To(HaveLen(2)) + Expect(adminClusterScopeRole.Rules).To(ConsistOf([]rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{clusterScopedResource}, + Verbs: strings.Split(adminVerbs, ","), + }, + { + APIGroups: []string{""}, + Resources: []string{"namespaces"}, + Verbs: []string{"update", "patch", "delete"}, + ResourceNames: []string{ + namespace.Name, + }, + }, + })) + + // delete the user namespace + err = env.Client(testutils.APIServerCluster).Delete(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + // run tasks to update the user namespaces in the authorization status + // it then should not contain the user namespace name and the reconcile annotation should be set + err = testWorker.RunTasks(env.Ctx, as) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Annotations).To(HaveKeyWithValue(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile)) + Expect(authz.Status.UserNamespaces).To(BeEmpty()) + + req = testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + // the user namespace should be removed from the cluster role + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminClusterScopeRole), adminClusterScopeRole) + Expect(err).ToNot(HaveOccurred()) + Expect(adminClusterScopeRole.Rules).To(HaveLen(1)) + Expect(adminClusterScopeRole.Rules).To(ConsistOf([]rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{clusterScopedResource}, + Verbs: strings.Split(adminVerbs, ","), + }, + })) + }) + + It("should not add a namespace which is not allowed", func() { + var err error + env := testEnvWithAPIServerAccess("testdata", "test-04") + + // create user namespace + namespace := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-system", + }, + } + err = env.Client(testutils.APIServerCluster).Create(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + testWorker := testutils.NewTestWorker(env.Client(testutils.CrateCluster), env.Client(testutils.APIServerCluster)) + controller := env.Reconciler(authzReconciler).(*authorization.AuthorizationReconciler) + controller.RegisterTasks(testWorker) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).ToNot(HaveOccurred()) + + // run tasks to update the user namespaces in the authorization status + // it then should not contain the system namespace name and the reconcile annotation should not be set + err = testWorker.RunTasks(env.Ctx, as) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Annotations).ToNot(HaveKeyWithValue(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile)) + Expect(authz.Status.UserNamespaces).To(BeEmpty()) + + req = testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + // the user namespace should be added to the cluster role + adminClusterScopeRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminClusterScopeStandardRulesRole, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminClusterScopeRole), adminClusterScopeRole) + Expect(err).ToNot(HaveOccurred()) + Expect(adminClusterScopeRole.Rules).To(HaveLen(1)) + Expect(adminClusterScopeRole.Rules).To(ConsistOf([]rbacv1.PolicyRule{ + { + APIGroups: []string{""}, + Resources: []string{clusterScopedResource}, + Verbs: strings.Split(adminVerbs, ","), + }, + })) + }) + + It("should create namespaced role bindings", func() { + var err error + env := testEnvWithAPIServerAccess("testdata", "test-04") + + // create user namespace + namespace := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user", + }, + } + err = env.Client(testutils.APIServerCluster).Create(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + // set the defaults for the API groups + authz.Spec.Default() + err = env.Client(testutils.CrateCluster).Update(env.Ctx, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + testWorker := testutils.NewTestWorker(env.Client(testutils.CrateCluster), env.Client(testutils.APIServerCluster)) + controller := env.Reconciler(authzReconciler).(*authorization.AuthorizationReconciler) + controller.RegisterTasks(testWorker) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).ToNot(HaveOccurred()) + + // run tasks to update the user namespaces in the authorization status + // it then should not contain the system namespace name + err = testWorker.RunTasks(env.Ctx, as) + Expect(err).ToNot(HaveOccurred()) + + req = testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + adminRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminRoleBinding, + Namespace: namespace.Name, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminRoleBinding), adminRoleBinding) + Expect(err).ToNot(HaveOccurred()) + Expect(adminRoleBinding.RoleRef.Name).To(Equal(openmcpv1alpha1.AdminNamespaceScopeRole)) + Expect(adminRoleBinding.Subjects).To(HaveLen(4)) + Expect(adminRoleBinding.Subjects).To(ConsistOf([]rbacv1.Subject{ + { + Kind: rbacv1.UserKind, + Name: "admin", + APIGroup: rbacv1.GroupName, + }, + { + Kind: rbacv1.ServiceAccountKind, + Name: "pipeline", + Namespace: "automate", + }, + { + Kind: rbacv1.UserKind, + Name: "static-admin", + APIGroup: rbacv1.GroupName, + }, + { + Kind: rbacv1.ServiceAccountKind, + Name: "static-manager", + Namespace: "openmcp-system", + }, + }, + )) + + viewRoleBinding := &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.ViewRoleBinding, + Namespace: namespace.Name, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(viewRoleBinding), viewRoleBinding) + Expect(err).ToNot(HaveOccurred()) + Expect(viewRoleBinding.RoleRef.Name).To(Equal(openmcpv1alpha1.ViewNamespaceScopeRole)) + Expect(viewRoleBinding.Subjects).To(HaveLen(2)) + Expect(viewRoleBinding.Subjects).To(ConsistOf([]rbacv1.Subject{ + { + Kind: rbacv1.GroupKind, + Name: "auditors", + APIGroup: rbacv1.GroupName, + }, + { + Kind: rbacv1.GroupKind, + Name: "static-auditors", + APIGroup: rbacv1.GroupName, + }, + }, + )) + }) + + It("should not create namespaced role bindings for system namespaces", func() { + var err error + env := testEnvWithAPIServerAccess("testdata", "test-04") + + // create user namespace + namespace := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-system", + }, + } + err = env.Client(testutils.APIServerCluster).Create(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + testWorker := testutils.NewTestWorker(env.Client(testutils.CrateCluster), env.Client(testutils.APIServerCluster)) + controller := env.Reconciler(authzReconciler).(*authorization.AuthorizationReconciler) + controller.RegisterTasks(testWorker) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).ToNot(HaveOccurred()) + + // run tasks to update the user namespaces in the authorization status + // it then should not contain the system namespace name + err = testWorker.RunTasks(env.Ctx, as) + Expect(err).ToNot(HaveOccurred()) + + req = testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + roleBindings := &rbacv1.RoleBindingList{} + err = env.Client(testutils.APIServerCluster).List(env.Ctx, roleBindings, client.InNamespace(namespace.Name)) + Expect(err).ToNot(HaveOccurred()) + Expect(roleBindings.Items).To(BeEmpty()) + }) + + It("should handle delete of the authorization object", func() { + var err error + env := testEnvWithAPIServerAccess("testdata", "test-05") + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + clusterRoles := &rbacv1.ClusterRoleList{} + err = env.Client(testutils.APIServerCluster).List(env.Ctx, clusterRoles) + Expect(err).ToNot(HaveOccurred()) + Expect(clusterRoles.Items).To(BeEmpty()) + + clusterRoleBindings := &rbacv1.ClusterRoleBindingList{} + err = env.Client(testutils.APIServerCluster).List(env.Ctx, clusterRoleBindings) + Expect(err).ToNot(HaveOccurred()) + Expect(clusterRoleBindings.Items).To(BeEmpty()) + + roleBindings := &rbacv1.RoleBindingList{} + err = env.Client(testutils.APIServerCluster).List(env.Ctx, roleBindings) + Expect(err).ToNot(HaveOccurred()) + Expect(roleBindings.Items).To(BeEmpty()) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).ToNot(HaveOccurred()) + Expect(as.Finalizers).To(BeEmpty()) + }) + + It("reconcile should handle when authentication is not found", func() { + env := testutils.DefaultTestSetupBuilder().WithReconcilerConstructor(authzReconciler, getReconciler, testutils.CrateCluster).Build() + + env.ShouldReconcile(authzReconciler, testing.RequestFromObject(&openmcpv1alpha1.Authorization{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + })) + }) + + It("should handle the ignore annotation", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-06") + + auth := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + Expect(auth.Status.Conditions).To(HaveLen(0)) + + clusterRole := &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminNamespaceScopeRole, + }, + } + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(clusterRole), clusterRole) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) + + It("should not be deleted when it has a dependency finalizer", func() { + var err error + + env := testEnvWithAPIServerAccess("testdata", "test-07") + + auth := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth) + Expect(err).NotTo(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, auth) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(auth) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth) + Expect(err).NotTo(HaveOccurred()) + Expect(auth.Finalizers).To(ContainElement(openmcpv1alpha1.AuthorizationComponent.Finalizer())) + Expect(auth.Finalizers).To(ContainElement("dependency." + openmcpv1alpha1.BaseDomain + "/other_comp")) + }) + + It("should not handle namespaces with deletion timestamp", func() { + var err error + env := testEnvWithAPIServerAccess("testdata", "test-04") + + // 1. create user namespace + // 2. delete user namespace + // 3. run tasks to update the user namespaces in the authorization status + // 4. run reconcile + // 5. verify that the user namespace is not added to the authorization status + // 6. verify that the role bindings are not being created + namespace := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-deleting", + Finalizers: []string{"openmcp.cloud/testing"}, + }, + } + err = env.Client(testutils.APIServerCluster).Create(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.APIServerCluster).Delete(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(&namespace), &namespace) + Expect(err).ToNot(HaveOccurred()) + + authz := &openmcpv1alpha1.Authorization{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + testWorker := testutils.NewTestWorker(env.Client(testutils.CrateCluster), env.Client(testutils.APIServerCluster)) + controller := env.Reconciler(authzReconciler).(*authorization.AuthorizationReconciler) + controller.RegisterTasks(testWorker) + + as := &openmcpv1alpha1.APIServer{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as) + Expect(err).ToNot(HaveOccurred()) + + // run tasks to update the user namespaces in the authorization status + // it then should not contain the namespace being deleted + err = testWorker.RunTasks(env.Ctx, as) + Expect(err).ToNot(HaveOccurred()) + + req = testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Status.UserNamespaces).To(BeEmpty()) + + roleBindings := &rbacv1.RoleBindingList{} + err = env.Client(testutils.APIServerCluster).List(env.Ctx, roleBindings, client.InNamespace(namespace.Name)) + Expect(err).ToNot(HaveOccurred()) + Expect(roleBindings.Items).To(BeEmpty()) + + // 1. create user namespace + // 2. run tasks to update the user namespaces in the authorization status + // 3. delete user namespace + // 4. run reconcile + // 5. verify that the user namespace is added to the authorization status + // 6. verify that the role bindings are not being created + namespace = v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "user-deleting2", + Finalizers: []string{"openmcp.cloud/testing"}, + }, + } + err = env.Client(testutils.APIServerCluster).Create(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + // run tasks to update the user namespaces in the authorization status + // it then should contain the namespace + err = testWorker.RunTasks(env.Ctx, as) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.APIServerCluster).Delete(env.Ctx, &namespace) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(&namespace), &namespace) + Expect(err).ToNot(HaveOccurred()) + + req = testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Status.UserNamespaces).ToNot(BeEmpty()) + Expect(authz.Status.UserNamespaces).To(ConsistOf(namespace.Name)) + + roleBindings = &rbacv1.RoleBindingList{} + err = env.Client(testutils.APIServerCluster).List(env.Ctx, roleBindings, client.InNamespace(namespace.Name)) + Expect(err).ToNot(HaveOccurred()) + Expect(roleBindings.Items).To(BeEmpty()) + }) + + It("should merge subject lists from multiple roles with the same name", func() { + env := testEnvWithAPIServerAccess("testdata", "test-08") + authz := &openmcpv1alpha1.Authorization{} + err := env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz) + Expect(err).ToNot(HaveOccurred()) + + // set the defaults for the API groups + authz.Spec.Default() + err = env.Client(testutils.CrateCluster).Update(env.Ctx, authz) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(authz) + _ = env.ShouldReconcile(authzReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz) + Expect(err).ToNot(HaveOccurred()) + Expect(authz.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthorizationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + adminClusterRoleBinding := &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: openmcpv1alpha1.AdminClusterRoleBinding, + }, + } + err = env.Client(testutils.APIServerCluster).Get(env.Ctx, client.ObjectKeyFromObject(adminClusterRoleBinding), adminClusterRoleBinding) + Expect(err).ToNot(HaveOccurred()) + Expect(adminClusterRoleBinding.RoleRef.Name).To(Equal(openmcpv1alpha1.AdminClusterScopeRole)) + Expect(adminClusterRoleBinding.Subjects).To(ConsistOf( + rbacv1.Subject{ + Kind: rbacv1.UserKind, + Name: "admin", + APIGroup: rbacv1.GroupName, + }, + rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: "pipeline", + Namespace: "automate", + }, + rbacv1.Subject{ + Kind: rbacv1.UserKind, + Name: "static-admin", + APIGroup: rbacv1.GroupName, + }, + rbacv1.Subject{ + Kind: rbacv1.ServiceAccountKind, + Name: "static-manager", + Namespace: "openmcp-system", + })) + }) + +}) + +func verifyStandardClusterRole(role *rbacv1.ClusterRole) { + Expect(role.Rules).To(HaveLen(1)) + + switch role.Name { + case openmcpv1alpha1.AdminNamespaceScopeStandardRulesRole: + Expect(role.Labels).To(HaveKeyWithValue(aggregateToAdminLabel, "true")) + Expect(role.Labels).To(HaveKeyWithValue(openmcpv1alpha1.AdminNamespaceScopeMatchLabel, "true")) + Expect(role.Rules[0].Verbs).To(ConsistOf(strings.Split(adminVerbs, ","))) + Expect(role.Rules[0].Resources).To(ConsistOf(namespaceScopedResource)) + case openmcpv1alpha1.ViewNamespaceScopeStandardRulesRole: + Expect(role.Labels).To(HaveKeyWithValue(aggregateToViewLabel, "true")) + Expect(role.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ViewNamespaceScopeMatchLabel, "true")) + Expect(role.Rules[0].Verbs).To(ConsistOf(strings.Split(viewVerbs, ","))) + Expect(role.Rules[0].Resources).To(ConsistOf(namespaceScopedResource)) + case openmcpv1alpha1.AdminClusterScopeStandardRulesRole: + Expect(role.Labels).To(HaveKeyWithValue(aggregateToAdminClusterScopedLabel, "true")) + Expect(role.Labels).To(HaveKeyWithValue(openmcpv1alpha1.AdminClusterScopeMatchLabel, "true")) + Expect(role.Rules[0].Verbs).To(ConsistOf(strings.Split(adminVerbs, ","))) + Expect(role.Rules[0].Resources).To(ConsistOf(clusterScopedResource)) + case openmcpv1alpha1.ViewClusterScopeStandardClusterRole: + Expect(role.Labels).To(HaveKeyWithValue(aggregateToViewClusterScopedLabel, "true")) + Expect(role.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ViewClusterScopeMatchLabel, "true")) + Expect(role.Rules[0].Verbs).To(ConsistOf(strings.Split(viewVerbs, ","))) + Expect(role.Rules[0].Resources).To(ConsistOf(clusterScopedResource)) + } +} + +func verifyAggregationClusterRole(role *rbacv1.ClusterRole) { + var labelSelectors []metav1.LabelSelector + + switch role.Name { + case openmcpv1alpha1.AdminNamespaceScopeRole: + labelSelectors = []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + aggregateToAdminLabel: "true", + }, + }, + { + MatchLabels: map[string]string{ + openmcpv1alpha1.AdminNamespaceScopeMatchLabel: "true", + }, + }, + } + case openmcpv1alpha1.ViewNamespaceScopeRole: + labelSelectors = []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + aggregateToViewLabel: "true", + }, + }, + { + MatchLabels: map[string]string{ + openmcpv1alpha1.ViewNamespaceScopeMatchLabel: "true", + }, + }, + } + case openmcpv1alpha1.AdminClusterScopeRole: + labelSelectors = []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + aggregateToAdminClusterScopedLabel: "true", + }, + }, + { + MatchLabels: map[string]string{ + openmcpv1alpha1.AdminClusterScopeMatchLabel: "true", + }, + }, + } + case openmcpv1alpha1.ViewClusterScopeRole: + labelSelectors = []metav1.LabelSelector{ + { + MatchLabels: map[string]string{ + aggregateToViewClusterScopedLabel: "true", + }, + }, + { + MatchLabels: map[string]string{ + openmcpv1alpha1.ViewClusterScopeMatchLabel: "true", + }, + }, + } + } + + knownComponents := components.Registry.GetKnownComponents() + for _, comp := range knownComponents { + ls := comp.LabelSelectorsForRole(role.Name) + if ls != nil { + labelSelectors = append(labelSelectors, ls...) + } + } + + Expect(role.AggregationRule.ClusterRoleSelectors).To(HaveLen(len(labelSelectors))) + Expect(role.AggregationRule.ClusterRoleSelectors).To(ConsistOf(labelSelectors)) +} diff --git a/internal/controller/core/authorization/testdata/test-01/authorization.yaml b/internal/controller/core/authorization/testdata/test-01/authorization.yaml new file mode 100644 index 0000000..c13fa32 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-01/authorization.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin diff --git a/internal/controller/core/authorization/testdata/test-02/apiserver.yaml b/internal/controller/core/authorization/testdata/test-02/apiserver.yaml new file mode 100644 index 0000000..39df31a --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-02/apiserver.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "False" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/authorization/testdata/test-02/authorization.yaml b/internal/controller/core/authorization/testdata/test-02/authorization.yaml new file mode 100644 index 0000000..c13fa32 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-02/authorization.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin diff --git a/internal/controller/core/authorization/testdata/test-03/apiserver.yaml b/internal/controller/core/authorization/testdata/test-03/apiserver.yaml new file mode 100644 index 0000000..acb48dd --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-03/apiserver.yaml @@ -0,0 +1,22 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + diff --git a/internal/controller/core/authorization/testdata/test-03/authorization.yaml b/internal/controller/core/authorization/testdata/test-03/authorization.yaml new file mode 100644 index 0000000..c13fa32 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-03/authorization.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin diff --git a/internal/controller/core/authorization/testdata/test-04/apiserver.yaml b/internal/controller/core/authorization/testdata/test-04/apiserver.yaml new file mode 100644 index 0000000..4fd39a7 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-04/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/authorization/testdata/test-04/authorization.yaml b/internal/controller/core/authorization/testdata/test-04/authorization.yaml new file mode 100644 index 0000000..6025d2a --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-04/authorization.yaml @@ -0,0 +1,20 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin + - kind: ServiceAccount + name: pipeline + namespace: automate + - role: view + subjects: + - kind: Group + name: auditors diff --git a/internal/controller/core/authorization/testdata/test-05/apiserver.yaml b/internal/controller/core/authorization/testdata/test-05/apiserver.yaml new file mode 100644 index 0000000..bab5063 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-05/apiserver.yaml @@ -0,0 +1,44 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - dependency.openmcp.cloud/authorization +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/authorization/testdata/test-05/apiserver/clusterrolebindings.yaml b/internal/controller/core/authorization/testdata/test-05/apiserver/clusterrolebindings.yaml new file mode 100644 index 0000000..f52984f --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-05/apiserver/clusterrolebindings.yaml @@ -0,0 +1,21 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: openmcp:admin + labels: + "openmcp.cloud/managed-by": "Authorization" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: openmcp:admin:clusterscoped +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: openmcp:view + labels: + "openmcp.cloud/managed-by": "Authorization" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: openmcp:view:clusterscoped diff --git a/internal/controller/core/authorization/testdata/test-05/apiserver/clusterroles.yaml b/internal/controller/core/authorization/testdata/test-05/apiserver/clusterroles.yaml new file mode 100644 index 0000000..7ae64dc --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-05/apiserver/clusterroles.yaml @@ -0,0 +1,63 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openmcp:admin + labels: + "openmcp.cloud/managed-by": "Authorization" +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openmcp:view + labels: + "openmcp.cloud/managed-by": "Authorization" +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openmcp:aggregate-to-admin + labels: + "openmcp.cloud/managed-by": "Authorization" +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openmcp:aggregate-to-view + labels: + "openmcp.cloud/managed-by": "Authorization" +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openmcp:admin:clusterscoped + labels: + "openmcp.cloud/managed-by": "Authorization" +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openmcp:view:clusterscoped + labels: + "openmcp.cloud/managed-by": "Authorization" +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openmcp:clusterscoped:aggregate-to-admin + labels: + "openmcp.cloud/managed-by": "Authorization" +rules: [] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: openmcp:clusterscoped:aggregate-to-view + labels: + "openmcp.cloud/managed-by": "Authorization" +rules: [] diff --git a/internal/controller/core/authorization/testdata/test-05/apiserver/namespace.yaml b/internal/controller/core/authorization/testdata/test-05/apiserver/namespace.yaml new file mode 100644 index 0000000..7c265c0 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-05/apiserver/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: test diff --git a/internal/controller/core/authorization/testdata/test-05/apiserver/rolebindings.yaml b/internal/controller/core/authorization/testdata/test-05/apiserver/rolebindings.yaml new file mode 100644 index 0000000..80b10a4 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-05/apiserver/rolebindings.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: openmcp:admin + namespace: test + labels: + "openmcp.cloud/managed-by": "Authorization" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: CLusterRole + name: openmcp:admin +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: openmcp:view + namespace: test + labels: + "openmcp.cloud/managed-by": "Authorization" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: CLusterRole + name: openmcp:view \ No newline at end of file diff --git a/internal/controller/core/authorization/testdata/test-05/authorization.yaml b/internal/controller/core/authorization/testdata/test-05/authorization.yaml new file mode 100644 index 0000000..34ec805 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-05/authorization.yaml @@ -0,0 +1,31 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - authorization.openmcp.cloud +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin + - kind: ServiceAccount + name: pipeline + namespace: automate + - role: view + subjects: + - kind: Group + name: auditors +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: authorizationHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 diff --git a/internal/controller/core/authorization/testdata/test-06/apiserver.yaml b/internal/controller/core/authorization/testdata/test-06/apiserver.yaml new file mode 100644 index 0000000..4fd39a7 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-06/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/authorization/testdata/test-06/authorization.yaml b/internal/controller/core/authorization/testdata/test-06/authorization.yaml new file mode 100644 index 0000000..6ef8b40 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-06/authorization.yaml @@ -0,0 +1,22 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + annotations: + "openmcp.cloud/operation": "ignore" +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin + - kind: ServiceAccount + name: pipeline + namespace: automate + - role: view + subjects: + - kind: Group + name: auditors diff --git a/internal/controller/core/authorization/testdata/test-07/apiserver.yaml b/internal/controller/core/authorization/testdata/test-07/apiserver.yaml new file mode 100644 index 0000000..4fd39a7 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-07/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/authorization/testdata/test-07/authorization.yaml b/internal/controller/core/authorization/testdata/test-07/authorization.yaml new file mode 100644 index 0000000..2d291f1 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-07/authorization.yaml @@ -0,0 +1,23 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - authorization.openmcp.cloud + - dependency.openmcp.cloud/other_comp +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin + - kind: ServiceAccount + name: pipeline + namespace: automate + - role: view + subjects: + - kind: Group + name: auditors diff --git a/internal/controller/core/authorization/testdata/test-08/apiserver.yaml b/internal/controller/core/authorization/testdata/test-08/apiserver.yaml new file mode 100644 index 0000000..4fd39a7 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-08/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/authorization/testdata/test-08/authorization.yaml b/internal/controller/core/authorization/testdata/test-08/authorization.yaml new file mode 100644 index 0000000..3a7c7a3 --- /dev/null +++ b/internal/controller/core/authorization/testdata/test-08/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + roleBindings: + - role: admin + subjects: + - kind: User + name: admin + - role: admin + subjects: + - kind: ServiceAccount + name: pipeline + namespace: automate diff --git a/internal/controller/core/cloudorchestrator/co_suite_test.go b/internal/controller/core/cloudorchestrator/co_suite_test.go new file mode 100644 index 0000000..bd6a69d --- /dev/null +++ b/internal/controller/core/cloudorchestrator/co_suite_test.go @@ -0,0 +1,13 @@ +package cloudorchestrator_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CloudOrchestrator Controller Test Suite") +} diff --git a/internal/controller/core/cloudorchestrator/conditions.go b/internal/controller/core/cloudorchestrator/conditions.go new file mode 100644 index 0000000..b4c24b3 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/conditions.go @@ -0,0 +1,73 @@ +package cloudorchestrator + +import ( + "fmt" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + corev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" +) + +func mcpConditionStatusFromCOConditionStatus(coStatus metav1.ConditionStatus) openmcpv1alpha1.ComponentConditionStatus { + switch coStatus { + case metav1.ConditionTrue: + return openmcpv1alpha1.ComponentConditionStatusTrue + case metav1.ConditionFalse: + return openmcpv1alpha1.ComponentConditionStatusFalse + default: + return openmcpv1alpha1.ComponentConditionStatusUnknown + } +} + +// cloudOrchestratorConditions builds up the conditions for the CloudOrchestrator +// It copies the passed in conditions and adds the conditions from the ControlPlane resource (if not nil), +// as well as an aggregated 'CloudOrchestratorHealthy' condition. +func cloudOrchestratorConditions(ready bool, reason, message string, cocp *corev1beta1.ControlPlane, cons ...openmcpv1alpha1.ComponentCondition) []openmcpv1alpha1.ComponentCondition { + resLen := len(cons) + 1 + if cocp != nil { + resLen += len(cocp.Status.Conditions) + } + res := make([]openmcpv1alpha1.ComponentCondition, len(cons), resLen) + copy(res, cons) + + // iterate over all conditions from the ControlPlane resource and add them to the list + // additionally, they are all aggregated into the 'CloudOrchestratorHealthy' condition + // the reason is that some of them are lower-case conditions that are not propagated to the MCP status + healthy := ready + healthyMsg := strings.Builder{} + if cocp != nil { + for _, con := range cocp.Status.Conditions { + res = append(res, componentutils.NewCondition(con.Type, mcpConditionStatusFromCOConditionStatus(con.Status), con.Reason, con.Message)) + if con.Status != metav1.ConditionTrue { + healthy = false + if healthyMsg.Len() == 0 { + healthyMsg.WriteString("The following ControlPlane conditions are not 'True':\n") + } + healthyMsg.WriteString("\t") + healthyMsg.WriteString(con.Type) + healthyMsg.WriteString(": ") + healthyMsg.WriteString(con.Message) + healthyMsg.WriteString("\n") + } + } + } + if !healthy { + if reason == "" { + reason = "UnhealthyControlPlaneConditions" + } + if healthyMsg.Len() > 0 { + if message == "" { + message = healthyMsg.String() + } else { + message = fmt.Sprintf("%s\n%s", message, healthyMsg.String()) + } + } + } + res = append(res, componentutils.NewCondition(openmcpv1alpha1.CloudOrchestratorComponent.HealthyCondition(), openmcpv1alpha1.ComponentConditionStatusFromBool(healthy), reason, message)) + + return res +} diff --git a/internal/controller/core/cloudorchestrator/controller.go b/internal/controller/core/cloudorchestrator/controller.go new file mode 100644 index 0000000..541df52 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/controller.go @@ -0,0 +1,425 @@ +package cloudorchestrator + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strings" + "time" + + "github.com/openmcp-project/mcp-operator/internal/utils" + "github.com/openmcp-project/mcp-operator/internal/utils/components" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/cluster" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + "github.com/openmcp-project/controller-utils/pkg/api" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/yaml" + + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + corev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" + "github.com/openmcp-project/controller-utils/pkg/logging" + apierrors "k8s.io/apimachinery/pkg/api/errors" + condApi "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +const ( + defaultNamespace string = "openmcp-system" // TODO make this configurable + ControllerName string = "CloudOrchestrator" +) + +var ( + errFetchingCloudOrchestrator openmcperrors.ReasonableError = openmcperrors.WithReason(errors.New("unable to fetch CloudOrchestrator"), cconst.ReasonCrateClusterInteractionProblem) + errDeletingControlPlane openmcperrors.ReasonableError = openmcperrors.WithReason(errors.New("unable to delete the ControlPlane"), cconst.ReasonCOCoreClusterInteractionProblem) + errModifyingControlPlane openmcperrors.ReasonableError = openmcperrors.WithReason(errors.New("unable to create or update Cloud Orchestrator ControlPlane resource"), cconst.ReasonCOCoreClusterInteractionProblem) +) + +func NewCloudOrchestratorController(crateClient, coreClient client.Client, coreCluster cluster.Cluster) *CloudOrchestratorReconciler { + return &CloudOrchestratorReconciler{ + CoreCluster: coreCluster, + CoreClient: coreClient, + CrateClient: crateClient, + } +} + +// CloudOrchestratorReconciler reconciles a CloudOrchestrator object +type CloudOrchestratorReconciler struct { + CoreCluster cluster.Cluster + CoreClient client.Client + CrateClient client.Client +} + +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=cloudorchestrators,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=cloudorchestrators/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=cloudorchestrators/finalizers,verbs=update +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=apiservers/finalizers,verbs=update +//+kubebuilder:rbac:groups=*,resources=*,verbs=* +//+kubebuilder:rbac:urls=*,verbs=* + +func (r *CloudOrchestratorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log, ctx := utils.InitializeControllerLogger(ctx, ControllerName) + log.Debug(cconst.MsgStartReconcile) + + rr, cp, reason, message := r.reconcile(ctx, req) + rr.LogRequeue(log, logging.DEBUG) + if rr.Component == nil { + return rr.Result, rr.ReconcileError + } + if rr.ReconcileError != nil { + reason = cconst.ReasonReconciliationError + message = cconst.MessageReconciliationError + } + rr.Conditions = cloudOrchestratorConditions(reason == "" || reason == cconst.ReasonDeletionWaitingForDependingComponents, reason, message, cp, rr.Conditions...) + return components.UpdateStatus(ctx, r.CrateClient, rr) +} + +func (r *CloudOrchestratorReconciler) reconcile(ctx context.Context, req ctrl.Request) (components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator], *corev1beta1.ControlPlane, string, string) { + log := logging.FromContextOrPanic(ctx) + + // get CloudOrchestrator resource + co := &openmcpv1alpha1.CloudOrchestrator{} + if err := r.CrateClient.Get(ctx, req.NamespacedName, co); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("CloudOrchestrator not found") + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{}, nil, "", "" + } + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{ReconcileError: openmcperrors.Join(errFetchingCloudOrchestrator, err)}, nil, "", "" + } + + // handle operation annotation + if co.GetAnnotations() != nil { + op, ok := co.GetAnnotations()[openmcpv1alpha1.OperationAnnotation] + if ok { + switch op { + case openmcpv1alpha1.OperationAnnotationValueIgnore: + log.Info("Ignoring resource due to ignore operation annotation") + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{}, nil, "", "" + case openmcpv1alpha1.OperationAnnotationValueReconcile: + log.Debug("Removing reconcile operation annotation from resource") + if err := components.PatchAnnotation(ctx, r.CrateClient, co, openmcpv1alpha1.OperationAnnotation, "", components.ANNOTATION_DELETE); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing operation annotation: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, nil, "", "" + } + } + } + } + + // Get ControlPlane as it could exist already and contain conditions that should be exposed on the CloudOrchestrator resource + coreControlPlane := &corev1beta1.ControlPlane{} + err := r.CoreClient.Get(ctx, client.ObjectKey{Name: utils.PrefixWithNamespace(co.Namespace, co.Name)}, coreControlPlane) + if err != nil { + if !apierrors.IsNotFound(err) { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error fetching CloudOrchestrator ControlPlane resource: %w", err), cconst.ReasonCOCoreClusterInteractionProblem)}, nil, "", "" + } + // ControlPlane not found + coreControlPlane = nil + } + + // checking for APIServer component + log.Debug("Checking for APIServer dependency") + ownCPGeneration, ownICGeneration, _ := components.GetCreatedFromGeneration(co) + as := &openmcpv1alpha1.APIServer{} + if err := r.CrateClient.Get(ctx, req.NamespacedName, as); err != nil { + if !apierrors.IsNotFound(err) { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error fetching APIServer resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + // APIServer not found + as = nil + } + + if as == nil || !components.IsDependencyReady(as, ownCPGeneration, ownICGeneration) { + log.Info("APIServer not found or it isn't ready") + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, Result: ctrl.Result{RequeueAfter: 60 * time.Second}}, coreControlPlane, cconst.ReasonWaitingForDependencies, "Waiting for APIServer dependency to be ready." + } + log.Debug("APIServer dependency is ready") + auth := &openmcpv1alpha1.Authentication{} + auth.SetName(co.Name) + auth.SetNamespace(co.Namespace) + authz := &openmcpv1alpha1.Authorization{} + authz.SetName(co.Name) + authz.SetNamespace(co.Namespace) + + if as.Spec.Type != openmcpv1alpha1.Gardener && as.Spec.Type != openmcpv1alpha1.GardenerDedicated { + log.Info("APIServer is not of type Gardener/GardenerDedicated") + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("APIServer dependency is ready, but the APIServer type is not supported"), cconst.ReasonInvalidAPIServerType)}, coreControlPlane, "", "" + } + + if as.Status.AdminAccess == nil || as.Status.AdminAccess.Kubeconfig == "" { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("APIServer dependency is ready, but no kubeconfig could be found in its status"), cconst.ReasonDependencyStatusInvalid)}, coreControlPlane, "", "" + } + + if !co.DeletionTimestamp.IsZero() { + // handle deletion + log.Info("Deleting CloudOrchestrator") + if components.HasAnyDependencyFinalizer(co) { + depString := strings.Join(sets.List(components.GetDependents(co)), ", ") + log.Info("CloudOrchestrator cannot be deleted, because it still contains dependency finalizers", "dependingComponents", depString) + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, Result: ctrl.Result{RequeueAfter: 60 * time.Second}}, coreControlPlane, cconst.ReasonDeletionWaitingForDependingComponents, fmt.Sprintf("Deletion is waiting for the following dependencies to be removed: [%s]", depString) + } + + _, err := r.deleteControlPlane(ctx, co) + if err != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.Join(errDeletingControlPlane, err)}, coreControlPlane, "", "" + } + + if coreControlPlane != nil { + oldCO := co.DeepCopy() + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{OldComponent: oldCO, Component: co, Reason: cconst.ReasonComponentIsInDeletion, Result: ctrl.Result{RequeueAfter: 10 * time.Second}}, coreControlPlane, "", "" + } + + // remove dependency finalizer from APIServer resource + if err = components.EnsureDependencyFinalizer(ctx, r.CrateClient, as, co, false); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing dependency finalizer from APIServer component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + // remove dependency finalizer from Authentication resource + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, auth, co, false); client.IgnoreNotFound(err) != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing dependency finalizer from Authentication component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + // remove dependency finalizer from Authorization resource + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, authz, co, false); client.IgnoreNotFound(err) != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing dependency finalizer from Authorization component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + + // remove finalizer from CloudOrchestrator resource + old := co.DeepCopy() + changed := controllerutil.RemoveFinalizer(co, openmcpv1alpha1.CloudOrchestratorComponent.Finalizer()) + if changed { + if err := r.CrateClient.Patch(ctx, co, client.MergeFrom(old)); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing finalizer from CloudOrchestrator: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + } + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{}, nil, "", "" + } + + // handle creation/update + log.Info("Triggering creation/update of CloudOrchestrator") + + old := co.DeepCopy() + if controllerutil.AddFinalizer(co, openmcpv1alpha1.CloudOrchestratorComponent.Finalizer()) { + log.Debug("Adding finalizer to CloudOrchestrator resource") + if err := r.CrateClient.Patch(ctx, co, client.MergeFrom(old)); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error patching finalizer on CloudOrchestrator: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + } + + log.Debug("Ensuring dependency finalizer on APIServer resource") + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, as, co, true); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error setting dependency finalizer on APIServer component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + log.Debug("Ensuring dependency finalizer on Authentication resource") + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, auth, co, true); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error setting dependency finalizer on Authentication component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + log.Debug("Ensuring dependency finalizer on Authorization resource") + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, authz, co, true); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error setting dependency finalizer on Authorization component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)}, coreControlPlane, "", "" + } + + // ControlPlane from Core Cluster + coreControlPlane = &corev1beta1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.PrefixWithNamespace(co.Namespace, co.Name), + }, + } + + // create or update the CO ControlPlane with the configuration from the openmcpv1alpha1.CloudOrchestrator CR + _, err = controllerutil.CreateOrUpdate(ctx, r.CoreClient, coreControlPlane, func() error { + spec, err := convertToControlPlaneSpec(&co.Spec, &as.Status) + if err != nil { + return err + } + coreControlPlane.Spec = *spec + + // update labels + labels, err := r.copyLabels(ctx, co) + if err != nil { + return err + } + coreControlPlane.Labels = labels + return nil + }) + errs := openmcperrors.NewReasonableErrorList() + if err != nil { + errs = errs.Append(openmcperrors.Join(errModifyingControlPlane, err)) + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{Component: co, ReconcileError: errs.Aggregate()}, coreControlPlane, "", "" + } + + // find out if the CO ControlPlane resource is Ready + isReady := r.isCloudOrchestratorReady(coreControlPlane.Status) + + res := ctrl.Result{} + reason := "" + message := "" + if !isReady { + log.Debug("ControlPlane resource is not ready yet") + reason = cconst.ReasonWaitingForCloudOrchestrator + message = "The ControlPlane resource is not ready yet." + } + + // update CO status + old = co.DeepCopy() + updateCloudOrchestratorStatus(co, coreControlPlane) + return components.ReconcileResult[*openmcpv1alpha1.CloudOrchestrator]{OldComponent: old, Component: co, Result: res}, coreControlPlane, reason, message +} + +func updateCloudOrchestratorStatus(co *openmcpv1alpha1.CloudOrchestrator, coreControlPlane *corev1beta1.ControlPlane) { + co.Status.ComponentsEnabled = coreControlPlane.Status.ComponentsEnabled + co.Status.ComponentsHealthy = coreControlPlane.Status.ComponentsHealthy +} + +// SetupWithManager sets up the controller with the Manager. +func (r *CloudOrchestratorReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&openmcpv1alpha1.CloudOrchestrator{}). + WatchesRawSource(source.Kind(r.CoreCluster.GetCache(), &corev1beta1.ControlPlane{}, handler.TypedEnqueueRequestsFromMapFunc(func(ctx context.Context, t *corev1beta1.ControlPlane) []reconcile.Request { + mcpName := t.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName] + mcpNamespace := t.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace] + if mcpName == "" || mcpNamespace == "" { + return nil + } + return []reconcile.Request{ + {NamespacedName: types.NamespacedName{Name: mcpName, Namespace: mcpNamespace}}, + } + }))). + Complete(r) +} + +// convertToControlPlaneSpec will return a v1beta1.ControlPlaneSpec from a openmcpv1alpha1.CloudOrchestratorSpec and +// a openmcpv1alpha1.APIServerStatus. +func convertToControlPlaneSpec(coSpec *openmcpv1alpha1.CloudOrchestratorSpec, apiServerStatus *openmcpv1alpha1.APIServerStatus) (*corev1beta1.ControlPlaneSpec, error) { + m := map[string]any{ + "rbac": map[string]any{ + "roleRef": map[string]string{ + "name": openmcpv1alpha1.AdminClusterScopeRole, + }, + }, + } + fluxValues, err := json.Marshal(m) + if err != nil { + return nil, err + } + + jsonData, err := yaml.ToJSON([]byte(apiServerStatus.AdminAccess.Kubeconfig)) + if err != nil { + return nil, err + } + controlPlaneSpec := &corev1beta1.ControlPlaneSpec{ + Target: corev1beta1.Target{ + Target: api.Target{ + Kubeconfig: &apiextensionsv1.JSON{Raw: jsonData}, + }, + FluxServiceAccount: corev1beta1.ServiceAccountReference{ + Name: "co-flux-deployer", + Namespace: defaultNamespace, + }, + }, + ComponentsConfig: corev1beta1.ComponentsConfig{}, + } + + if coSpec.Crossplane != nil { + controlPlaneSpec.ComponentsConfig.Crossplane = &corev1beta1.CrossplaneConfig{ + Version: coSpec.Crossplane.Version, + Providers: convertCrossplaneProviders(coSpec.Crossplane.Providers), + } + } + + if coSpec.BTPServiceOperator != nil { + controlPlaneSpec.ComponentsConfig.BTPServiceOperator = &corev1beta1.BTPServiceOperatorConfig{ + Version: coSpec.BTPServiceOperator.Version, + } + controlPlaneSpec.ComponentsConfig.CertManager = &corev1beta1.CertManagerConfig{ + Version: "1.16.1", + } + } + + if coSpec.ExternalSecretsOperator != nil { + controlPlaneSpec.ComponentsConfig.ExternalSecretsOperator = &corev1beta1.ExternalSecretsOperatorConfig{ + Version: coSpec.ExternalSecretsOperator.Version, + } + } + + if coSpec.Kyverno != nil { + controlPlaneSpec.ComponentsConfig.Kyverno = &corev1beta1.KyvernoConfig{ + Version: coSpec.Kyverno.Version, + } + } + + if coSpec.Flux != nil { + controlPlaneSpec.ComponentsConfig.Flux = &corev1beta1.FluxConfig{ + Version: coSpec.Flux.Version, + Values: &apiextensionsv1.JSON{Raw: fluxValues}, + } + } + + return controlPlaneSpec, nil +} + +// deleteControlPlane will delete the ManagedControlPlane at the CO Core cluster. +// The returned bool will be true if the ControlPlane still exists after the deletion attempt. Otherwise, it will be false. +func (r *CloudOrchestratorReconciler) deleteControlPlane(ctx context.Context, co *openmcpv1alpha1.CloudOrchestrator) (bool, error) { + coreControlPlane := &corev1beta1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: utils.PrefixWithNamespace(co.Namespace, co.Name), + }, + } + + err := r.CoreClient.Delete(ctx, coreControlPlane) + if apierrors.IsNotFound(err) { + return false, nil + } + return true, err +} + +// isCloudOrchestratorReady will return true if the status of the CO ManagedControlPlane is Ready and the Components are healthy +func (r *CloudOrchestratorReconciler) isCloudOrchestratorReady(status corev1beta1.ControlPlaneStatus) bool { + return condApi.IsStatusConditionTrue(status.Conditions, "Ready") && status.ComponentsHealthy == status.ComponentsEnabled +} + +// convertCrossplaneProviders will convert a slice of openmcpv1alpha1.CrossplaneProviderConfig to a slice of corev1beta1.CrossplaneProviderConfig +func convertCrossplaneProviders(providers []*openmcpv1alpha1.CrossplaneProviderConfig) []*corev1beta1.CrossplaneProviderConfig { + if providers == nil { + return nil + } + + converted := make([]*corev1beta1.CrossplaneProviderConfig, len(providers)) + for i, p := range providers { + converted[i] = &corev1beta1.CrossplaneProviderConfig{ + Name: p.Name, + Version: p.Version, + } + } + return converted + +} + +// copyLabels will return a map of labels that should be added to the CO ManagedControlPlane +func (r *CloudOrchestratorReconciler) copyLabels(ctx context.Context, co *openmcpv1alpha1.CloudOrchestrator) (map[string]string, error) { + // copy project and workspace name over + ns := &v1.Namespace{} + if err := r.CrateClient.Get(ctx, client.ObjectKey{Name: co.Namespace}, ns); err != nil { + return nil, err + } + + labels := map[string]string{} + + copyMapEntries(labels, co.Labels, openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName) + copyMapEntries(labels, co.Labels, openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace) + copyMapEntries(labels, ns.Labels, "openmcp.cloud/project", "openmcp.cloud/workspace") + + return labels, nil +} diff --git a/internal/controller/core/cloudorchestrator/controller_test.go b/internal/controller/core/cloudorchestrator/controller_test.go new file mode 100644 index 0000000..c12e073 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/controller_test.go @@ -0,0 +1,431 @@ +package cloudorchestrator_test + +import ( + "path" + + "github.com/openmcp-project/mcp-operator/internal/components" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/cloudorchestrator" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +const ( + coReconciler = "cloudOrchestrator" +) + +func getReconciler(c ...client.Client) reconcile.Reconciler { + return cloudorchestrator.NewCloudOrchestratorController(c[0], c[1], nil) +} + +func testEnvSetup(crateObjectsPath, coObjectsPath string, coDynamicObjects ...client.Object) *testing.ComplexEnvironment { + builder := testutils.DefaultTestSetupBuilder(crateObjectsPath).WithFakeClient(testutils.COCoreCluster, testutils.Scheme).WithReconcilerConstructor(coReconciler, getReconciler, testutils.CrateCluster, testutils.COCoreCluster) + if coObjectsPath != "" { + builder.WithInitObjectPath(testutils.COCoreCluster, coObjectsPath) + } + if len(coDynamicObjects) > 0 { + builder.WithDynamicObjectsWithStatus(testutils.COCoreCluster, coDynamicObjects...) + } + return builder.Build() +} + +var _ = Describe("CO-1153 CloudOrchestrator Controller", func() { + It("should not find CloudOrchestrator resource", func() { + env := testEnvSetup("", "") + + co := &openmcpv1alpha1.CloudOrchestrator{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "test"}, + } + + req := testing.RequestFromObject(co) + res := env.ShouldReconcile(coReconciler, req) + Expect(res).To(Equal(reconcile.Result{})) + }) + + It("should ignore CloudOrchestrator resource due to annotation", func() { + var err error + env := testEnvSetup(path.Join("testdata", "test-01"), "") + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(co) + res := env.ShouldReconcile(coReconciler, req) + Expect(res).To(Equal(reconcile.Result{})) + }) + + It("should not find APIServer resource", func() { + var err error + env := testEnvSetup(path.Join("testdata", "test-02"), "") + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(co) + res := env.ShouldReconcile(coReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).NotTo(HaveOccurred()) + + Expect(co.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForDependencies, + Message: "Waiting for APIServer dependency to be ready.", + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + }) + + It("should fail to reconcile and set the status condition to false when APIServer has an unsupported type", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-03"), "") + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(co) + _ = env.ShouldNotReconcile(coReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).NotTo(HaveOccurred()) + + Expect(co.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + Message: cconst.MessageReconciliationError, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonInvalidAPIServerType, + }), + )) + }) + + It("should fail to reconcile and set the status condition to false when APIServer status has no access kubeconfig", func() { + var err error + env := testEnvSetup(path.Join("testdata", "test-04"), "") + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(co) + _ = env.ShouldNotReconcile(coReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).NotTo(HaveOccurred()) + + Expect(co.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + Message: cconst.MessageReconciliationError, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonDependencyStatusInvalid, + }), + )) + }) + + It("should create the ControlPlane resource", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-08"), "") + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(co) + _ = env.ShouldReconcile(coReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).NotTo(HaveOccurred()) + + Expect(co.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForCloudOrchestrator, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + cp := &corev1beta1.ControlPlane{} + err = env.Client(testutils.COCoreCluster).Get(env.Ctx, types.NamespacedName{ + Namespace: "", + Name: "test--test", + }, cp) + Expect(err).NotTo(HaveOccurred()) + Expect(cp.Spec.Target.Kubeconfig).NotTo(BeNil()) + Expect(cp.Spec.Crossplane.Version).To(Equal("1.17.0")) // configured + Expect(cp.Spec.Crossplane.Providers[0]).NotTo(BeNil()) + Expect(cp.Spec.Crossplane.Providers[0].Name).To(Equal("provider-kubernetes")) // configured + Expect(cp.Spec.Crossplane.Providers[0].Version).To(Equal("0.14.1")) // configured + Expect(cp.Spec.ExternalSecretsOperator.Version).To(Equal("0.10.0")) // configured + Expect(cp.Spec.BTPServiceOperator.Version).To(Equal("0.6.0")) // configured + Expect(cp.Spec.CertManager.Version).To(Equal("1.16.1")) // configured automatically + Expect(cp.Spec.Flux.Version).To(Equal("3.2.0")) // configured + Expect(cp.Spec.Kyverno.Version).To(Equal("3.2.7")) // configured + }) + + It("should delete the ControlPlane resource and then the CO Resource", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-06"), "") + // adding ControlPlane resource to Core cluster to simulate the deletion of the ControlPlane resource + cp := &corev1beta1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test--test", + Namespace: "", + Finalizers: []string{"COre.orchestrate.cloud.sap"}, + }, + Status: corev1beta1.ControlPlaneStatus{ComponentsEnabled: 1, ComponentsHealthy: 0}, + } + err = env.Client(testutils.COCoreCluster).Create(env.Ctx, cp) + Expect(err).NotTo(HaveOccurred()) + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(co) + _ = env.ShouldReconcile(coReconciler, req) + + // delete CloudOrchestrator resource + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, co) + Expect(err).ToNot(HaveOccurred()) + + // trigger new reconcile + req = testing.RequestFromObject(co) + res := env.ShouldReconcile(coReconciler, req) + Expect(res.RequeueAfter > 0).To(BeTrue()) // expecting requeue, since ControlPlane resource is not deleted yet + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).ToNot(HaveOccurred()) + Expect(co.Finalizers).To(ContainElement(openmcpv1alpha1.CloudOrchestratorComponent.Finalizer())) + + Expect(co.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + Reason: cconst.ReasonComponentIsInDeletion, + }), + )) + + cp = &corev1beta1.ControlPlane{} + err = env.Client(testutils.COCoreCluster).Get(env.Ctx, types.NamespacedName{ + Namespace: "", + Name: "test--test", + }, cp) + Expect(apierrors.IsNotFound(err)).To(BeFalse()) // ControlPlane resource is still there + + as := &openmcpv1alpha1.APIServer{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as)).To(Succeed()) + auth := &openmcpv1alpha1.Authentication{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth)).To(Succeed()) + authz := &openmcpv1alpha1.Authorization{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz)).To(Succeed()) + coComp := components.Component(co) + Expect(as.Finalizers).To(ContainElement(coComp.Type().DependencyFinalizer())) // Finalizer on APIServer resource is still there + Expect(auth.Finalizers).To(ContainElement(coComp.Type().DependencyFinalizer())) // Finalizer on Authentication resource is still there + Expect(authz.Finalizers).To(ContainElement(coComp.Type().DependencyFinalizer())) // Finalizer on Authorization resource is still there + + // --- SUB TEST: simulating a requeue for the CO resource to check if the finalizer at the APIServer resource is removed --- + cp = &corev1beta1.ControlPlane{} + err = env.Client(testutils.COCoreCluster).Get(env.Ctx, types.NamespacedName{ + Namespace: "", + Name: "test--test", + }, cp) + Expect(err).NotTo(HaveOccurred()) + controllerutil.RemoveFinalizer(cp, "COre.orchestrate.cloud.sap") // removing finalizer on ControlPlane resource to simulate successful deletion + err = env.Client(testutils.COCoreCluster).Update(env.Ctx, cp) + Expect(err).NotTo(HaveOccurred()) + + req = testing.RequestFromObject(co) + res = env.ShouldReconcile(coReconciler, req) + Expect(res.RequeueAfter == 0).To(BeTrue()) // expecting no requeue + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(as), as)).To(Succeed()) + Expect(as.Finalizers).ToNot(ContainElement(coComp.Type().DependencyFinalizer())) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(auth), auth)).To(Succeed()) + Expect(auth.Finalizers).ToNot(ContainElement(coComp.Type().DependencyFinalizer())) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(authz), authz)).To(Succeed()) + Expect(authz.Finalizers).ToNot(ContainElement(coComp.Type().DependencyFinalizer())) + }) + + It("should not be deleted when it has a dependency finalizer", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-07"), "") + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, co) + Expect(err).ToNot(HaveOccurred()) + + req := testing.RequestFromObject(co) + _ = env.ShouldReconcile(coReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).NotTo(HaveOccurred()) + Expect(co.Finalizers).To(ContainElement(openmcpv1alpha1.CloudOrchestratorComponent.Finalizer())) + Expect(co.Finalizers).To(ContainElement("dependency." + openmcpv1alpha1.BaseDomain + "/other_comp")) + }) + + It("should update the ControlPlane resource since the configuration got changed", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-05"), "") + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(co) + _ = env.ShouldReconcile(coReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).NotTo(HaveOccurred()) + + cp := &corev1beta1.ControlPlane{} + err = env.Client(testutils.COCoreCluster).Get(env.Ctx, types.NamespacedName{ + Namespace: "", + Name: "test--test", + }, cp) + Expect(err).NotTo(HaveOccurred()) + Expect(cp.Spec.Target.Kubeconfig).NotTo(BeNil()) + Expect(cp.Spec.Crossplane.Version).To(Equal("1.17.0")) // configured + Expect(cp.Spec.ExternalSecretsOperator).To(BeNil()) + + // Update CO Spec + co.Spec.BTPServiceOperator = &openmcpv1alpha1.BTPServiceOperatorConfig{ + Version: "0.6.0", + } + co.Spec.ExternalSecretsOperator = &openmcpv1alpha1.ExternalSecretsOperatorConfig{ + Version: "0.10.0", + } + err = env.Client(testutils.CrateCluster).Update(env.Ctx, co) + Expect(err).NotTo(HaveOccurred()) + + req2 := testing.RequestFromObject(co) + _ = env.ShouldReconcile(coReconciler, req2) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).NotTo(HaveOccurred()) + + Expect(co.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForCloudOrchestrator, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.CloudOrchestratorComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + + cp = &corev1beta1.ControlPlane{} + err = env.Client(testutils.COCoreCluster).Get(env.Ctx, types.NamespacedName{ + Namespace: "", + Name: "test--test", + }, cp) + Expect(err).NotTo(HaveOccurred()) + Expect(cp.Spec.Target.Kubeconfig).NotTo(BeNil()) + Expect(cp.Spec.Crossplane.Version).To(Equal("1.17.0")) // configured + Expect(cp.Spec.ExternalSecretsOperator).ToNot(BeNil()) // updated + Expect(cp.Spec.ExternalSecretsOperator.Version).To(Equal("0.10.0")) + Expect(cp.Spec.BTPServiceOperator).ToNot(BeNil()) // updated + Expect(cp.Spec.BTPServiceOperator.Version).To(Equal("0.6.0")) + Expect(cp.Spec.CertManager).ToNot(BeNil()) // updated automatically + Expect(cp.Spec.CertManager.Version).To(Equal("1.16.1")) + }) + + It("should remove Crossplane configuration from the ControlPlane resource", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-05"), "") + + co := &openmcpv1alpha1.CloudOrchestrator{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, co) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(co) + _ = env.ShouldReconcile(coReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co) + Expect(err).NotTo(HaveOccurred()) + + cp := &corev1beta1.ControlPlane{} + err = env.Client(testutils.COCoreCluster).Get(env.Ctx, types.NamespacedName{ + Namespace: "", + Name: "test--test", + }, cp) + Expect(err).NotTo(HaveOccurred()) + Expect(cp.Spec.Target.Kubeconfig).NotTo(BeNil()) + Expect(cp.Spec.Crossplane.Version).To(Equal("1.17.0")) // configured + Expect(cp.Spec.ExternalSecretsOperator).To(BeNil()) + + // Disable Crossplane + co.Spec.Crossplane = nil + err = env.Client(testutils.CrateCluster).Update(env.Ctx, co) + Expect(err).NotTo(HaveOccurred()) + + req2 := testing.RequestFromObject(co) + _ = env.ShouldReconcile(coReconciler, req2) + + cp = &corev1beta1.ControlPlane{} + err = env.Client(testutils.COCoreCluster).Get(env.Ctx, types.NamespacedName{ + Namespace: "", + Name: "test--test", + }, cp) + Expect(err).NotTo(HaveOccurred()) + Expect(cp.Spec.Target.Kubeconfig).NotTo(BeNil()) + Expect(cp.Spec.Crossplane).To(BeNil()) + }) +}) diff --git a/internal/controller/core/cloudorchestrator/conversion_convertToControlPlaneSpec_test.go b/internal/controller/core/cloudorchestrator/conversion_convertToControlPlaneSpec_test.go new file mode 100644 index 0000000..708a84e --- /dev/null +++ b/internal/controller/core/cloudorchestrator/conversion_convertToControlPlaneSpec_test.go @@ -0,0 +1,116 @@ +package cloudorchestrator + +import ( + "testing" + + corev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" + "github.com/stretchr/testify/assert" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +func Test_convertToControlPlaneSpec(t *testing.T) { + apiServerStatus := &openmcpv1alpha1.APIServerStatus{ + AdminAccess: &openmcpv1alpha1.APIServerAccess{ + Kubeconfig: testKubeConfig, + }, + } + + tests := []struct { + name string + input *openmcpv1alpha1.CloudOrchestratorSpec + validateFunc func(*corev1beta1.ControlPlaneSpec) error + expectedErr error + }{ + { + name: "Crossplane enabled through non nil pointer - everything else disabled", + input: &openmcpv1alpha1.CloudOrchestratorSpec{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{ + Crossplane: &openmcpv1alpha1.CrossplaneConfig{ + Version: "1.0.0", + }, + }, + }, + validateFunc: func(spec *corev1beta1.ControlPlaneSpec) error { + assert.NotNil(t, spec.Crossplane) + assert.Nil(t, spec.Crossplane.Providers) + assert.Nil(t, spec.ExternalSecretsOperator) + assert.Nil(t, spec.Flux) + assert.Nil(t, spec.BTPServiceOperator) + assert.Nil(t, spec.Kyverno) + assert.Nil(t, spec.CertManager) + return nil + }, + }, + { + name: "All Components are enabled through non nil pointer", + input: &openmcpv1alpha1.CloudOrchestratorSpec{ + CloudOrchestratorConfiguration: openmcpv1alpha1.CloudOrchestratorConfiguration{ + Crossplane: &openmcpv1alpha1.CrossplaneConfig{ + Version: "1.0.0", + Providers: []*openmcpv1alpha1.CrossplaneProviderConfig{ + { + Name: "provider1", + Version: "1.0.0", + }, + }, + }, + ExternalSecretsOperator: &openmcpv1alpha1.ExternalSecretsOperatorConfig{ + Version: "1.0.0", + }, + Flux: &openmcpv1alpha1.FluxConfig{ + Version: "1.0.0", + }, + BTPServiceOperator: &openmcpv1alpha1.BTPServiceOperatorConfig{ + Version: "1.0.0", + }, + Kyverno: &openmcpv1alpha1.KyvernoConfig{ + Version: "1.0.0", + }, + }, + }, + validateFunc: func(spec *corev1beta1.ControlPlaneSpec) error { + assert.NotNil(t, spec.Crossplane) + assert.NotNil(t, spec.Crossplane.Providers) + assert.Len(t, spec.Crossplane.Providers, 1) + assert.NotNil(t, spec.ExternalSecretsOperator) + assert.NotNil(t, spec.Flux) + assert.NotNil(t, spec.BTPServiceOperator) + assert.NotNil(t, spec.Kyverno) + assert.NotNil(t, spec.CertManager) + return nil + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + spec, err := convertToControlPlaneSpec(tt.input, apiServerStatus) + assert.ErrorIs(t, err, tt.expectedErr) + if err := tt.validateFunc(spec); err != nil { + t.Errorf("convertToControlPlaneSpec() = %v, want %v", err, "no error") + } + }) + + } +} + +const testKubeConfig = ` +apiVersion: v1 +clusters: +- name: apiserver +cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK +contexts: +- name: apiserver +context: + cluster: apiserver + user: apiserver +current-context: apiserver +users: +- name: apiserver +user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK +` diff --git a/internal/controller/core/cloudorchestrator/testdata/test-01/cloudorchestrator.yaml b/internal/controller/core/cloudorchestrator/testdata/test-01/cloudorchestrator.yaml new file mode 100644 index 0000000..f8c86d8 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-01/cloudorchestrator.yaml @@ -0,0 +1,12 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + annotations: + "openmcp.cloud/operation": "ignore" +spec: + crossplane: + version: 1.17.0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-02/cloudorchestrator.yaml b/internal/controller/core/cloudorchestrator/testdata/test-02/cloudorchestrator.yaml new file mode 100644 index 0000000..cae0dee --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-02/cloudorchestrator.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + crossplane: + version: 1.17.0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-03/apiserver.yaml b/internal/controller/core/cloudorchestrator/testdata/test-03/apiserver.yaml new file mode 100644 index 0000000..b750d4b --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-03/apiserver.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: Invalid +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-03/authentication.yaml b/internal/controller/core/cloudorchestrator/testdata/test-03/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-03/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/cloudorchestrator/testdata/test-03/authorization.yaml b/internal/controller/core/cloudorchestrator/testdata/test-03/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-03/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-03/cloudorchestrator.yaml b/internal/controller/core/cloudorchestrator/testdata/test-03/cloudorchestrator.yaml new file mode 100644 index 0000000..cae0dee --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-03/cloudorchestrator.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + crossplane: + version: 1.17.0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-04/apiserver.yaml b/internal/controller/core/cloudorchestrator/testdata/test-04/apiserver.yaml new file mode 100644 index 0000000..aa619eb --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-04/apiserver.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-04/authentication.yaml b/internal/controller/core/cloudorchestrator/testdata/test-04/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-04/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/cloudorchestrator/testdata/test-04/authorization.yaml b/internal/controller/core/cloudorchestrator/testdata/test-04/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-04/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-04/cloudorchestrator.yaml b/internal/controller/core/cloudorchestrator/testdata/test-04/cloudorchestrator.yaml new file mode 100644 index 0000000..cae0dee --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-04/cloudorchestrator.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + crossplane: + version: 1.17.0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-05/apiserver.yaml b/internal/controller/core/cloudorchestrator/testdata/test-05/apiserver.yaml new file mode 100644 index 0000000..4fd39a7 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-05/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-05/authentication.yaml b/internal/controller/core/cloudorchestrator/testdata/test-05/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-05/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/cloudorchestrator/testdata/test-05/authorization.yaml b/internal/controller/core/cloudorchestrator/testdata/test-05/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-05/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-05/cloudorchestrator.yaml b/internal/controller/core/cloudorchestrator/testdata/test-05/cloudorchestrator.yaml new file mode 100644 index 0000000..d8eba78 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-05/cloudorchestrator.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + crossplane: + version: 1.17.0 + providers: + - name: provider-kubernetes + version: 0.14.0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-05/test-namespace.yaml b/internal/controller/core/cloudorchestrator/testdata/test-05/test-namespace.yaml new file mode 100644 index 0000000..7c265c0 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-05/test-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: test diff --git a/internal/controller/core/cloudorchestrator/testdata/test-06/apiserver.yaml b/internal/controller/core/cloudorchestrator/testdata/test-06/apiserver.yaml new file mode 100644 index 0000000..4fd39a7 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-06/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-06/authentication.yaml b/internal/controller/core/cloudorchestrator/testdata/test-06/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-06/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/cloudorchestrator/testdata/test-06/authorization.yaml b/internal/controller/core/cloudorchestrator/testdata/test-06/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-06/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-06/cloudorchestrator.yaml b/internal/controller/core/cloudorchestrator/testdata/test-06/cloudorchestrator.yaml new file mode 100644 index 0000000..cae0dee --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-06/cloudorchestrator.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + crossplane: + version: 1.17.0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-06/test-namespace.yaml b/internal/controller/core/cloudorchestrator/testdata/test-06/test-namespace.yaml new file mode 100644 index 0000000..7c265c0 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-06/test-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: test diff --git a/internal/controller/core/cloudorchestrator/testdata/test-07/apiserver.yaml b/internal/controller/core/cloudorchestrator/testdata/test-07/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-07/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/cloudorchestrator/testdata/test-07/authentication.yaml b/internal/controller/core/cloudorchestrator/testdata/test-07/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-07/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/cloudorchestrator/testdata/test-07/authorization.yaml b/internal/controller/core/cloudorchestrator/testdata/test-07/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-07/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-07/cloudorchestrator.yaml b/internal/controller/core/cloudorchestrator/testdata/test-07/cloudorchestrator.yaml new file mode 100644 index 0000000..f4bc4d8 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-07/cloudorchestrator.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - cloudorchestrator.openmcp.cloud + - dependency.openmcp.cloud/other_comp +spec: + crossplane: + version: 1.17.0 diff --git a/internal/controller/core/cloudorchestrator/testdata/test-08/apiserver.yaml b/internal/controller/core/cloudorchestrator/testdata/test-08/apiserver.yaml new file mode 100644 index 0000000..4fd39a7 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-08/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-08/authentication.yaml b/internal/controller/core/cloudorchestrator/testdata/test-08/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-08/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/cloudorchestrator/testdata/test-08/authorization.yaml b/internal/controller/core/cloudorchestrator/testdata/test-08/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-08/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-08/cloudorchestrator.yaml b/internal/controller/core/cloudorchestrator/testdata/test-08/cloudorchestrator.yaml new file mode 100644 index 0000000..15f354f --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-08/cloudorchestrator.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + crossplane: + version: 1.17.0 + providers: + - name: provider-kubernetes + version: 0.14.1 + btpServiceOperator: + version: 0.6.0 + externalSecretsOperator: + version: 0.10.0 + kyverno: + version: 3.2.7 + flux: + version: 3.2.0 \ No newline at end of file diff --git a/internal/controller/core/cloudorchestrator/testdata/test-08/test-namespace.yaml b/internal/controller/core/cloudorchestrator/testdata/test-08/test-namespace.yaml new file mode 100644 index 0000000..7c265c0 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/testdata/test-08/test-namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: test diff --git a/internal/controller/core/cloudorchestrator/utils.go b/internal/controller/core/cloudorchestrator/utils.go new file mode 100644 index 0000000..f7e5df7 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/utils.go @@ -0,0 +1,12 @@ +package cloudorchestrator + +// copyMapEntries copies map entries from a source to a target map, both must have the the same type. +func copyMapEntries[K comparable, V any](target map[K]V, source map[K]V, keys ...K) { + if source == nil { + return + } + + for _, key := range keys { + target[key] = source[key] + } +} diff --git a/internal/controller/core/cloudorchestrator/utils_test.go b/internal/controller/core/cloudorchestrator/utils_test.go new file mode 100644 index 0000000..d100122 --- /dev/null +++ b/internal/controller/core/cloudorchestrator/utils_test.go @@ -0,0 +1,80 @@ +package cloudorchestrator + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCopyMapEntries(t *testing.T) { + tests := []struct { + description string + sourceMap map[string]string + targetMap map[string]string + copyKeys []string + expectedTargetMap map[string]string + }{ + { + description: "doesn't run into error if the source or target map is nil", + sourceMap: nil, + targetMap: nil, + copyKeys: []string{"a"}, + expectedTargetMap: nil, + }, + { + description: "doesn't modify target map if no keys are given", + sourceMap: map[string]string{ + "d": "d", + }, + targetMap: map[string]string{ + "a": "a", + "b": "b", + "c": "c", + }, + expectedTargetMap: map[string]string{ + "a": "a", + "b": "b", + "c": "c", + }, + }, + { + description: "copies map entries successfully into another map", + sourceMap: map[string]string{ + "d": "d", + }, + targetMap: map[string]string{ + "a": "a", + "b": "b", + "c": "c", + }, + copyKeys: []string{"d"}, + expectedTargetMap: map[string]string{ + "a": "a", + "b": "b", + "c": "c", + "d": "d", + }, + }, + { + description: "overwrites map entries if already available", + sourceMap: map[string]string{ + "d": "b", + }, + targetMap: map[string]string{ + "d": "d", + }, + copyKeys: []string{"d"}, + expectedTargetMap: map[string]string{ + "d": "b", + }, + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + copyMapEntries(test.targetMap, test.sourceMap, test.copyKeys...) + + assert.Equal(t, test.expectedTargetMap, test.targetMap) + }) + } +} diff --git a/internal/controller/core/landscaper/controller.go b/internal/controller/core/landscaper/controller.go new file mode 100644 index 0000000..71d8c4d --- /dev/null +++ b/internal/controller/core/landscaper/controller.go @@ -0,0 +1,344 @@ +package landscaper + +import ( + "context" + "fmt" + "reflect" + "strings" + "time" + + "github.com/openmcp-project/mcp-operator/internal/utils" + "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" + "github.com/openmcp-project/mcp-operator/internal/utils/components" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper/conversion" + lsutils "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper/utils" + + laasv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" + "github.com/openmcp-project/controller-utils/pkg/collections/maps" + "github.com/openmcp-project/controller-utils/pkg/logging" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/util/sets" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/handler" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +const ( + ControllerName = "Landscaper" + + LandscaperReadyPhase = "Succeeded" + LandscaperErrorPhase = "Failed" +) + +func NewLandscaperConnector(crateClient, laasClient client.Client) *LandscaperConnector { + return &LandscaperConnector{ + CrateClient: crateClient, + LaaSClient: laasClient, + ApiServerAccess: &apiserver.APIServerAccessImpl{}, + } +} + +// LandscaperConnector reconciles a ManagedControlPlane object +type LandscaperConnector struct { + CrateClient, LaaSClient client.Client + ApiServerAccess apiserver.APIServerAccess +} + +// SetAPIServerAccess sets the ApiServerAccess implementation. +// Used for testing. +func (r *LandscaperConnector) SetAPIServerAccess(apiServerAccess apiserver.APIServerAccess) { + r.ApiServerAccess = apiServerAccess +} + +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=managedcontrolplanes,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=managedcontrolplanes/status,verbs=get;update;patch + +func (r *LandscaperConnector) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log, ctx := utils.InitializeControllerLogger(ctx, ControllerName) + log.Debug(cconst.MsgStartReconcile) + + rr := r.reconcile(ctx, req) + rr.LogRequeue(log, logging.DEBUG) + if rr.Component == nil { + return rr.Result, rr.ReconcileError + } + if rr.ReconcileError != nil && len(rr.Conditions) == 0 { // shortcut so we don't have to add the same ready condition to each return statement (won't work anymore if we have multiple conditions) + rr.Conditions = landscaperConditions(false, cconst.ReasonReconciliationError, cconst.MessageReconciliationError) + } + return components.UpdateStatus(ctx, r.CrateClient, rr) +} + +func (r *LandscaperConnector) reconcile(ctx context.Context, req ctrl.Request) components.ReconcileResult[*openmcpv1alpha1.Landscaper] { + log := logging.FromContextOrPanic(ctx) + + // get Landscaper resource + ls := &openmcpv1alpha1.Landscaper{} + if err := r.CrateClient.Get(ctx, req.NamespacedName, ls); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("Resource not found") + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{} + } + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("unable to get resource '%s' from cluster: %w", req.NamespacedName.String(), err), cconst.ReasonCrateClusterInteractionProblem)} + } + + // handle operation annotation + if ls.GetAnnotations() != nil { + op, ok := ls.GetAnnotations()[openmcpv1alpha1.OperationAnnotation] + if ok { + switch op { + case openmcpv1alpha1.OperationAnnotationValueIgnore: + log.Info("Ignoring resource due to ignore operation annotation") + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{} + case openmcpv1alpha1.OperationAnnotationValueReconcile: + log.Debug("Removing reconcile operation annotation from resource") + if err := components.PatchAnnotation(ctx, r.CrateClient, ls, openmcpv1alpha1.OperationAnnotation, "", components.ANNOTATION_DELETE); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing operation annotation: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + } + } + + // checking for APIServer component + log.Debug("Checking for APIServer dependency") + ownCPGeneration, ownICGeneration, _ := components.GetCreatedFromGeneration(ls) + as := &openmcpv1alpha1.APIServer{} + as.SetName(ls.Name) + as.SetNamespace(ls.Namespace) + if err := r.CrateClient.Get(ctx, client.ObjectKeyFromObject(as), as); err != nil { + if !apierrors.IsNotFound(err) { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error fetching APIServer resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + // APIServer not found + as = nil + } + if as == nil || !components.IsDependencyReady(as, ownCPGeneration, ownICGeneration) { + log.Info("APIServer not found or it isn't ready") + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{Component: ls, Conditions: landscaperConditions(false, cconst.ReasonWaitingForDependencies, "Waiting for APIServer dependency to be ready."), Result: ctrl.Result{RequeueAfter: 60 * time.Second}} + } + log.Debug("APIServer dependency is ready") + if as.Status.AdminAccess == nil || as.Status.AdminAccess.Kubeconfig == "" { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("APIServer dependency is ready, but no kubeconfig could be found in its status"), cconst.ReasonDependencyStatusInvalid)} + } + auth := &openmcpv1alpha1.Authentication{} + auth.SetName(ls.Name) + auth.SetNamespace(ls.Namespace) + authz := &openmcpv1alpha1.Authorization{} + authz.SetName(ls.Name) + authz.SetNamespace(ls.Namespace) + + deleteLandscaper := false + if !ls.DeletionTimestamp.IsZero() { + log.Info("Deleting Landscaper") + if components.HasAnyDependencyFinalizer(ls) { + depString := strings.Join(sets.List(components.GetDependents(ls)), ", ") + log.Info("Landscaper cannot be deleted, because it still contains dependency finalizers", "dependingComponents", depString) + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{Component: ls, Conditions: landscaperConditions(true, cconst.ReasonDeletionWaitingForDependingComponents, fmt.Sprintf("Deletion is waiting for the following dependencies to be removed: [%s]", depString)), Result: ctrl.Result{RequeueAfter: 60 * time.Second}} + } + deleteLandscaper = true + } else { + log.Info("Triggering creation/update of Landscaper") + + old := ls.DeepCopy() + if controllerutil.AddFinalizer(ls, openmcpv1alpha1.LandscaperComponent.Finalizer()) { + log.Debug("Adding finalizer to Landscaper resource") + if err := r.CrateClient.Patch(ctx, ls, client.MergeFrom(old)); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{ReconcileError: openmcperrors.WithReason(fmt.Errorf("error patching finalizer on Landscaper: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + + log.Debug("Ensuring dependency finalizer on APIServer resource") + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, as, ls, true); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error setting dependency finalizer on APIServer component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + log.Debug("Ensuring dependency finalizer on Authentication resource") + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, auth, ls, true); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error setting dependency finalizer on Authentication component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + log.Debug("Ensuring dependency finalizer on Authorization resource") + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, authz, ls, true); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error setting dependency finalizer on Authorization component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + } + + ld, err := lsutils.GetCorrespondingLandscaperDeployment(ctx, r.LaaSClient, ls) + if err != nil { + if !apierrors.IsNotFound(err) { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error trying to fetch corresponding LandscaperDeployment: %w", err), cconst.ReasonLaaSCoreClusterInteractionProblem)} + } + // This means that the control plane has a reference to a LandscaperDeployment which doesn't exist. + // That shouldn't happen, but if we error out here, we prevent the system from recovering from a lost LandscaperDeployment. + log.Info("Referenced LandscaperDeployment does not exist") + } + + var res ctrl.Result + var ready bool + var reason string + var errr openmcperrors.ReasonableError + old := ls.DeepCopy() + if deleteLandscaper { + res, ready, reason, errr = r.handleDelete(ctx, ls, ld) + } else { + res, ready, reason, errr = r.handleCreateOrUpdate(ctx, ls, ld, as) + } + errs := openmcperrors.NewReasonableErrorList(errr) + + if deleteLandscaper && ready { + // remove dependency finalizer from APIServer resource + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, as, ls, false); err != nil { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{OldComponent: old, Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing dependency finalizer from APIServer component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + // remove dependency finalizer from Authentication resource + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, auth, ls, false); client.IgnoreNotFound(err) != nil { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{OldComponent: old, Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing dependency finalizer from Authentication component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + // remove dependency finalizer from Authorization resource + if err := components.EnsureDependencyFinalizer(ctx, r.CrateClient, authz, ls, false); client.IgnoreNotFound(err) != nil { + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{OldComponent: old, Component: ls, ReconcileError: openmcperrors.WithReason(fmt.Errorf("error removing dependency finalizer from Authorization component resource: %w", err), cconst.ReasonCrateClusterInteractionProblem)} + } + + // remove finalizer from Landscaper resource + old := ls.DeepCopy() + changed := controllerutil.RemoveFinalizer(ls, openmcpv1alpha1.LandscaperComponent.Finalizer()) + if changed { + if err := r.CrateClient.Patch(ctx, ls, client.MergeFrom(old)); err != nil { + errs.Append(fmt.Errorf("error removing finalizer from Landscaper: %w", err)) + } + } + } + + cons := landscaperConditions(ready, reason, "") + if ld != nil { + cons[0].Message = fmt.Sprintf("LandscaperDeployment phase: %s", ld.Status.Phase) + if !ready && errr == nil && ld.Status.LastError != nil { + cons[0].Message = fmt.Sprintf("[%s] %s - %s", ld.Status.LastError.Operation, ld.Status.LastError.Reason, ld.Status.LastError.Message) + } + } + return components.ReconcileResult[*openmcpv1alpha1.Landscaper]{OldComponent: old, Component: ls, Result: res, Reason: reason, ReconcileError: errs.Aggregate(), Conditions: cons} +} + +func (r *LandscaperConnector) handleCreateOrUpdate(ctx context.Context, ls *openmcpv1alpha1.Landscaper, ld *laasv1alpha1.LandscaperDeployment, as *openmcpv1alpha1.APIServer) (ctrl.Result, bool, string, openmcperrors.ReasonableError) { + log := logging.FromContextOrPanic(ctx) + + apiServerKubeconfig, err := r.ApiServerAccess.GetAdminAccessRaw(as) + if err != nil { + return ctrl.Result{}, false, "", openmcperrors.WithReason(err, cconst.ReasonLaaSCoreClusterInteractionProblem) + } + + generatedLD := conversion.LandscaperDeployment_v1alpha1_from_Landscaper_v1alpha1(ls, apiServerKubeconfig) + ldUpToDate := false + + if ld == nil { + // no existing LandscaperDeployment has been found + // let's create a new one + ld = generatedLD + log = log.WithValues("ldNamespace", ld.Namespace, "ldName", ld.Name) + + //check if namespace exists and create if necessary + targetNamespace := &corev1.Namespace{} + targetNamespace.SetName(ld.Namespace) + targetNamespace.SetLabels(map[string]string{ + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName: ls.Name, + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace: ls.Namespace, + }) + if err := r.LaaSClient.Get(ctx, client.ObjectKeyFromObject(targetNamespace), targetNamespace); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("Namespace for LandscaperDeployment does not exist, creating it") + if err := r.LaaSClient.Create(ctx, targetNamespace); err != nil { + return ctrl.Result{}, false, "", openmcperrors.WithReason(err, cconst.ReasonLaaSCoreClusterInteractionProblem) + } + } else { + return ctrl.Result{}, false, "", openmcperrors.WithReason(err, cconst.ReasonLaaSCoreClusterInteractionProblem) + } + } + + log.Info("Creating LandscaperDeployment") + if err := r.LaaSClient.Create(ctx, ld); err != nil { + return ctrl.Result{}, false, "", openmcperrors.WithReason(err, cconst.ReasonLaaSCoreClusterInteractionProblem) + } + } else { + // merge/overwrite values of existing LandscaperDeployment with the generated ones + log = log.WithValues("ldNamespace", ld.Namespace, "ldName", ld.Name) + changed := false + wrongLabels := false + for k, v := range generatedLD.GetLabels() { + val, exists := ld.GetLabels()[k] + if !exists || val != v { + wrongLabels = true + break + } + } + if wrongLabels { + ld.SetLabels(maps.Merge(ld.GetLabels(), generatedLD.GetLabels())) + changed = true + } + if !reflect.DeepEqual(ld.Spec, generatedLD.Spec) { + ld.Spec = generatedLD.Spec + changed = true + } + if changed { + log.Info("Updating existing LandscaperDeployment", "ldNamespace", ld.Namespace, "ldName", ld.Name) + if err := r.LaaSClient.Update(ctx, ld); err != nil { + return ctrl.Result{}, false, "", openmcperrors.WithReason(err, cconst.ReasonLaaSCoreClusterInteractionProblem) + } + } else { + ldUpToDate = ld.Status.ObservedGeneration == ld.Generation && ld.Status.Phase == LandscaperReadyPhase + if ldUpToDate { + log.Info("LandscaperDeployment is up-to-date") + } else { + log.Info("Waiting for LandscaperDeployment to become ready") + } + } + } + + ls.Status.LandscaperDeploymentInfo = &openmcpv1alpha1.LandscaperDeploymentInfo{ + Name: ld.GetName(), + Namespace: ld.GetNamespace(), + } + + var requeueAfter time.Duration + reason := "" + if !ldUpToDate { + requeueAfter = 30 * time.Second + reason = cconst.ReasonWaitingForLaaS + } + return ctrl.Result{RequeueAfter: requeueAfter}, ldUpToDate, reason, nil +} + +func (r *LandscaperConnector) handleDelete(ctx context.Context, ls *openmcpv1alpha1.Landscaper, ld *laasv1alpha1.LandscaperDeployment) (ctrl.Result, bool, string, openmcperrors.ReasonableError) { + log := logging.FromContextOrPanic(ctx) + if ld != nil { + log = log.WithValues("ldNamespace", ld.Namespace, "ldName", ld.Name) + log.Info("LandscaperDeployment still exists, deleting it") + // remove the LandscaperDeployment + if err := r.LaaSClient.Delete(ctx, ld); err != nil { + return ctrl.Result{}, false, "", openmcperrors.WithReason(err, cconst.ReasonLaaSCoreClusterInteractionProblem) + } + return ctrl.Result{RequeueAfter: 30 * time.Second}, false, cconst.ReasonWaitingForLaaS, nil + } + // LandscaperDeployment is gone + log.Debug("Corresponding LandscaperDeployment is deleted") + ls.Status.LandscaperDeploymentInfo = nil + return ctrl.Result{}, true, "", nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *LandscaperConnector) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&openmcpv1alpha1.Landscaper{}, builder.WithPredicates(components.DefaultComponentControllerPredicates())). + Watches(&openmcpv1alpha1.APIServer{}, &handler.EnqueueRequestForObject{}, builder.WithPredicates(components.StatusChangedPredicate{})). + Complete(r) +} + +func landscaperConditions(ready bool, reason, message string) []openmcpv1alpha1.ComponentCondition { + return []openmcpv1alpha1.ComponentCondition{ + components.NewCondition(openmcpv1alpha1.LandscaperComponent.HealthyCondition(), openmcpv1alpha1.ComponentConditionStatusFromBool(ready), reason, message), + } +} diff --git a/internal/controller/core/landscaper/controller_test.go b/internal/controller/core/landscaper/controller_test.go new file mode 100644 index 0000000..4fb1897 --- /dev/null +++ b/internal/controller/core/landscaper/controller_test.go @@ -0,0 +1,487 @@ +package landscaper_test + +import ( + "path" + + "github.com/openmcp-project/mcp-operator/internal/components" + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper" + + lssv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +const ( + lsReconciler = "landscaper" +) + +func getReconciler(c ...client.Client) reconcile.Reconciler { + return landscaper.NewLandscaperConnector(c[0], c[1]) +} + +func testEnvSetup(crateObjectsPath, laasObjectsPath string, laasDynamicObjects ...client.Object) *testing.ComplexEnvironment { + builder := testutils.DefaultTestSetupBuilder(crateObjectsPath).WithFakeClient(testutils.LaaSCoreCluster, testutils.Scheme).WithReconcilerConstructor(lsReconciler, getReconciler, testutils.CrateCluster, testutils.LaaSCoreCluster) + if laasObjectsPath != "" { + builder.WithInitObjectPath(testutils.LaaSCoreCluster, laasObjectsPath) + } + if len(laasDynamicObjects) > 0 { + builder.WithDynamicObjectsWithStatus(testutils.LaaSCoreCluster, laasDynamicObjects...) + } + return builder.Build() +} + +var _ = Describe("CO-1153 Landscaper Controller", func() { + It("should set the status condition to false when there is no APIServer available", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-01"), "") + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + res := env.ShouldReconcile(lsReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + Expect(ls.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForDependencies, + }), + )) + }) + + It("should set the status condition to false when APIServer is not ready", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-02"), "") + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + res := env.ShouldReconcile(lsReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + Expect(ls.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForDependencies, + }), + )) + }) + + It("should fail to reconcile and set the status condition to false when APIServer status has no access kubeconfig", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-03"), "") + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + _ = env.ShouldNotReconcile(lsReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + Expect(ls.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonDependencyStatusInvalid, + }), + )) + }) + + It("should create a LandscaperDeployment and wait for deployment ready when APIServer is ready", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-04"), "", &lssv1alpha1.LandscaperDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + }) + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + res := env.ShouldReconcile(lsReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + Expect(ls.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForLaaS, + }), + )) + + Expect(ls.Status.LandscaperDeploymentInfo).NotTo(BeNil()) + + lsDeployment := &lssv1alpha1.LandscaperDeployment{} + err = env.Client(testutils.LaaSCoreCluster).Get(env.Ctx, types.NamespacedName{Name: ls.Status.LandscaperDeploymentInfo.Name, Namespace: ls.Status.LandscaperDeploymentInfo.Name}, lsDeployment) + Expect(err).NotTo(HaveOccurred()) + + Expect(lsDeployment.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName, ls.Name)) + Expect(lsDeployment.Labels).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace, ls.Namespace)) + Expect(lsDeployment.Spec.DataPlane).NotTo(BeNil()) + Expect(lsDeployment.Spec.DataPlane.Kubeconfig).NotTo(BeEmpty()) + Expect(lsDeployment.Spec.LandscaperConfiguration).NotTo(BeNil()) + Expect(lsDeployment.Spec.LandscaperConfiguration.Deployers).To(HaveLen(2)) + Expect(lsDeployment.Spec.LandscaperConfiguration.Deployers).To(ContainElements("helm", "manifest")) + + lsDeployment.Status.Phase = landscaper.LandscaperReadyPhase + lsDeployment.Status.ObservedGeneration = lsDeployment.Generation + err = env.Client(testutils.LaaSCoreCluster).Status().Update(env.Ctx, lsDeployment) + Expect(err).NotTo(HaveOccurred()) + + req = testing.RequestFromObject(ls) + res = env.ShouldReconcile(lsReconciler, req) + testing.ExpectNoRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + Expect(ls.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + )) + }) + + It("should handle when the referenced LandscaperDeployment is not found", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-05"), "", &lssv1alpha1.LandscaperDeployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + }) + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + res := env.ShouldReconcile(lsReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + Expect(ls.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForLaaS, + }), + )) + + Expect(ls.Status.LandscaperDeploymentInfo).NotTo(BeNil()) + + lsDeployment := &lssv1alpha1.LandscaperDeployment{} + err = env.Client(testutils.LaaSCoreCluster).Get(env.Ctx, types.NamespacedName{Name: ls.Status.LandscaperDeploymentInfo.Name, Namespace: ls.Status.LandscaperDeploymentInfo.Name}, lsDeployment) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should handle when multiple Landscaper Deployments for the referenced are found", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-06"), path.Join("testdata", "test-06", "laas")) + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + _ = env.ShouldNotReconcile(lsReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + Expect(ls.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonLaaSCoreClusterInteractionProblem, + }), + )) + }) + + It("should handle when the LandscaperDeployment reference was lost", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-07"), path.Join("testdata", "test-07", "laas")) + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + res := env.ShouldReconcile(lsReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + Expect(ls.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForLaaS, + }), + )) + + Expect(ls.Status.LandscaperDeploymentInfo).NotTo(BeNil()) + Expect(ls.Status.LandscaperDeploymentInfo.Name).To(Equal("test1")) + Expect(ls.Status.LandscaperDeploymentInfo.Namespace).To(Equal("test")) + }) + + It("should handle when the LandscaperDeployment has issues", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-08"), path.Join("testdata", "test-08", "laas")) + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + res := env.ShouldReconcile(lsReconciler, req) + testing.ExpectRequeue(res) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).NotTo(HaveOccurred()) + + Expect(ls.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.LandscaperComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonWaitingForLaaS, + }), + )) + }) + + It("should handle delete", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-09"), path.Join("testdata", "test-09", "laas")) + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + lsComp := components.Component(ls) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + env.ShouldReconcile(lsReconciler, req) + + // ls should still exist, since we need a second reconcile to notice that the LandscaperDeployment is gone + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).ToNot(HaveOccurred()) + + env.ShouldReconcile(lsReconciler, req) + // now it should be gone + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + err = env.Client(testutils.LaaSCoreCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, &lssv1alpha1.LandscaperDeployment{}) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + as := &openmcpv1alpha1.APIServer{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as)).To(Succeed()) + auth := &openmcpv1alpha1.Authentication{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth)).To(Succeed()) + authz := &openmcpv1alpha1.Authorization{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz)).To(Succeed()) + + Expect(componentutils.HasDepedencyFinalizer(as, lsComp.Type())).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(auth, lsComp.Type())).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(authz, lsComp.Type())).To(BeFalse()) + }) + + It("should not delete when a component dependency finalizer is set", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-10"), path.Join("testdata", "test-10", "laas")) + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + lsComp := components.Component(ls) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + _ = env.ShouldReconcile(lsReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).ToNot(HaveOccurred()) + + err = env.Client(testutils.LaaSCoreCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, &lssv1alpha1.LandscaperDeployment{}) + Expect(err).ToNot(HaveOccurred()) + + as := &openmcpv1alpha1.APIServer{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as)).To(Succeed()) + auth := &openmcpv1alpha1.Authentication{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth)).To(Succeed()) + authz := &openmcpv1alpha1.Authorization{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz)).To(Succeed()) + + Expect(componentutils.HasDepedencyFinalizer(as, lsComp.Type())).To(BeTrue()) + Expect(componentutils.HasDepedencyFinalizer(auth, lsComp.Type())).To(BeTrue()) + Expect(componentutils.HasDepedencyFinalizer(authz, lsComp.Type())).To(BeTrue()) + }) + + It("should handle the reconcile annotation", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-11"), "") + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + _ = env.ShouldReconcile(lsReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).ToNot(HaveOccurred()) + Expect(ls.Annotations).ToNot(HaveKeyWithValue(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile)) + }) + + It("should handle the ignore annotation", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-12"), "") + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + _ = env.ShouldReconcile(lsReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).ToNot(HaveOccurred()) + Expect(ls.Annotations).To(HaveKeyWithValue(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueIgnore)) + + err = env.Client(testutils.LaaSCoreCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, &lssv1alpha1.LandscaperDeployment{}) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + }) + + It("should handle when landscaper is not found", func() { + env := testEnvSetup("", "") + + env.ShouldReconcile(lsReconciler, testing.RequestFromObject(&openmcpv1alpha1.Landscaper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + })) + }) + + It("should handle delete with no LandscaperDeployment", func() { + var err error + + env := testEnvSetup(path.Join("testdata", "test-13"), "") + + ls := &openmcpv1alpha1.Landscaper{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ls) + Expect(err).NotTo(HaveOccurred()) + + lsComp := components.Component(ls) + + err = env.Client(testutils.CrateCluster).Delete(env.Ctx, ls) + Expect(err).NotTo(HaveOccurred()) + + req := testing.RequestFromObject(ls) + _ = env.ShouldReconcile(lsReconciler, req) + + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls) + Expect(err).To(HaveOccurred()) + Expect(errors.IsNotFound(err)).To(BeTrue()) + + as := &openmcpv1alpha1.APIServer{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, as)).To(Succeed()) + auth := &openmcpv1alpha1.Authentication{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, auth)).To(Succeed()) + authz := &openmcpv1alpha1.Authorization{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, authz)).To(Succeed()) + + Expect(componentutils.HasDepedencyFinalizer(as, lsComp.Type())).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(auth, lsComp.Type())).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(authz, lsComp.Type())).To(BeFalse()) + }) + +}) diff --git a/internal/controller/core/landscaper/conversion/conversion_suite_test.go b/internal/controller/core/landscaper/conversion/conversion_suite_test.go new file mode 100644 index 0000000..94dcd6f --- /dev/null +++ b/internal/controller/core/landscaper/conversion/conversion_suite_test.go @@ -0,0 +1,13 @@ +package conversion_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Landscaper Conversion Test Suite") +} diff --git a/internal/controller/core/landscaper/conversion/conversion_test.go b/internal/controller/core/landscaper/conversion/conversion_test.go new file mode 100644 index 0000000..29c46a5 --- /dev/null +++ b/internal/controller/core/landscaper/conversion/conversion_test.go @@ -0,0 +1,69 @@ +package conversion_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper/conversion" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("LandscaperDeployment_v1alpha1_from_Landscaper_v1alpha1", func() { + It("returns nil when Landscaper is nil", func() { + ld := conversion.LandscaperDeployment_v1alpha1_from_Landscaper_v1alpha1(nil, "apiServerKubeconfig") + Expect(ld).To(BeNil()) + }) + + It("returns LandscaperDeployment with correct name and namespace", func() { + ls := &openmcpv1alpha1.Landscaper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Namespace: "test-namespace", + }, + } + ld := conversion.LandscaperDeployment_v1alpha1_from_Landscaper_v1alpha1(ls, "apiServerKubeconfig") + Expect(ld.GetName()).To(Equal("test-name")) + Expect(ld.GetNamespace()).To(Equal("test-namespace")) + }) + + It("returns LandscaperDeployment with correct labels", func() { + ls := &openmcpv1alpha1.Landscaper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Namespace: "test-namespace", + }, + } + ld := conversion.LandscaperDeployment_v1alpha1_from_Landscaper_v1alpha1(ls, "apiServerKubeconfig") + Expect(ld.GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName, "test-name")) + Expect(ld.GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace, "test-namespace")) + }) + + It("returns LandscaperDeployment with correct APIServer Kubeconfig", func() { + ls := &openmcpv1alpha1.Landscaper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-name", + Namespace: "test-namespace", + }, + } + ld := conversion.LandscaperDeployment_v1alpha1_from_Landscaper_v1alpha1(ls, "apiServerKubeconfig") + Expect(ld.Spec.DataPlane.Kubeconfig).To(Equal("apiServerKubeconfig")) + }) +}) + +var _ = Describe("LandscaperConfig_v1alpha1_from_lsConfig_v1alpha1", func() { + It("returns empty LandscaperConfiguration when source is empty", func() { + src := openmcpv1alpha1.LandscaperConfiguration{} + result := conversion.LandscaperConfig_v1alpha1_from_lsConfig_v1alpha1(src) + Expect(result.Deployers).To(BeEmpty()) + }) + + It("returns LandscaperConfiguration with same deployers as source", func() { + src := openmcpv1alpha1.LandscaperConfiguration{ + Deployers: []string{"deployer1", "deployer2"}, + } + result := conversion.LandscaperConfig_v1alpha1_from_lsConfig_v1alpha1(src) + Expect(result.Deployers).To(Equal(src.Deployers)) + }) +}) diff --git a/internal/controller/core/landscaper/conversion/v1alpha_to_v1alpha.go b/internal/controller/core/landscaper/conversion/v1alpha_to_v1alpha.go new file mode 100644 index 0000000..7f1eb43 --- /dev/null +++ b/internal/controller/core/landscaper/conversion/v1alpha_to_v1alpha.go @@ -0,0 +1,50 @@ +package conversion + +import ( + laasv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" + + "github.com/openmcp-project/mcp-operator/internal/utils" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// LandscaperDeployment_v1alpha1_from_Landscaper_v1alpha1 generates a LandscaperDeployment based on the given ManagedControlPlane resource. +func LandscaperDeployment_v1alpha1_from_Landscaper_v1alpha1(ls *openmcpv1alpha1.Landscaper, apiServerKubeconfig string) *laasv1alpha1.LandscaperDeployment { + if ls == nil { + return nil + } + ld := &laasv1alpha1.LandscaperDeployment{} + ld.SetName(ls.Name) + ld.SetNamespace(ls.Namespace) + + // set backreference label + labels := map[string]string{} + labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName] = ls.Name + labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace] = ls.Namespace + ld.SetLabels(labels) + + // set spec + ld.Spec.TenantId = utils.K8sNameHash(ls.Namespace)[:8] + ld.Spec.Purpose = "undefined" // TODO + ld.Spec.LandscaperConfiguration = LandscaperConfig_v1alpha1_from_lsConfig_v1alpha1(ls.Spec.LandscaperConfiguration) + + ld.Spec.DataPlane = &laasv1alpha1.DataPlane{ + Kubeconfig: apiServerKubeconfig, + } + + return ld +} + +func LandscaperConfig_v1alpha1_from_lsConfig_v1alpha1(src openmcpv1alpha1.LandscaperConfiguration) laasv1alpha1.LandscaperConfiguration { + var deployers []string + if src.Deployers != nil { + deployers = make([]string, len(src.Deployers)) + for i := range deployers { + deployers[i] = src.Deployers[i] + } + } + + return laasv1alpha1.LandscaperConfiguration{ + Deployers: deployers, + } +} diff --git a/internal/controller/core/landscaper/landscaper_suite_test.go b/internal/controller/core/landscaper/landscaper_suite_test.go new file mode 100644 index 0000000..63f000f --- /dev/null +++ b/internal/controller/core/landscaper/landscaper_suite_test.go @@ -0,0 +1,13 @@ +package landscaper_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Landscaper Connector Test Suite") +} diff --git a/internal/controller/core/landscaper/testdata/test-01/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-01/landscaper.yaml new file mode 100644 index 0000000..b575d46 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-01/landscaper.yaml @@ -0,0 +1,11 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + deployers: + - "helm" + - "manifest" diff --git a/internal/controller/core/landscaper/testdata/test-02/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-02/apiserver.yaml new file mode 100644 index 0000000..39df31a --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-02/apiserver.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "False" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-02/authentication.yaml b/internal/controller/core/landscaper/testdata/test-02/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-02/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-02/authorization.yaml b/internal/controller/core/landscaper/testdata/test-02/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-02/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-02/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-02/landscaper.yaml new file mode 100644 index 0000000..b575d46 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-02/landscaper.yaml @@ -0,0 +1,11 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + deployers: + - "helm" + - "manifest" diff --git a/internal/controller/core/landscaper/testdata/test-03/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-03/apiserver.yaml new file mode 100644 index 0000000..aa619eb --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-03/apiserver.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-03/authentication.yaml b/internal/controller/core/landscaper/testdata/test-03/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-03/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-03/authorization.yaml b/internal/controller/core/landscaper/testdata/test-03/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-03/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-03/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-03/landscaper.yaml new file mode 100644 index 0000000..b575d46 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-03/landscaper.yaml @@ -0,0 +1,11 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + deployers: + - "helm" + - "manifest" diff --git a/internal/controller/core/landscaper/testdata/test-04/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-04/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-04/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-04/authentication.yaml b/internal/controller/core/landscaper/testdata/test-04/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-04/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-04/authorization.yaml b/internal/controller/core/landscaper/testdata/test-04/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-04/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-04/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-04/landscaper.yaml new file mode 100644 index 0000000..b575d46 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-04/landscaper.yaml @@ -0,0 +1,11 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + deployers: + - "helm" + - "manifest" diff --git a/internal/controller/core/landscaper/testdata/test-05/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-05/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-05/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-05/authentication.yaml b/internal/controller/core/landscaper/testdata/test-05/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-05/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-05/authorization.yaml b/internal/controller/core/landscaper/testdata/test-05/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-05/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-05/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-05/landscaper.yaml new file mode 100644 index 0000000..5d6d77c --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-05/landscaper.yaml @@ -0,0 +1,20 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy + landscaperDeployment: + name: test + namespace: test + diff --git a/internal/controller/core/landscaper/testdata/test-06/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-06/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-06/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-06/authentication.yaml b/internal/controller/core/landscaper/testdata/test-06/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-06/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-06/authorization.yaml b/internal/controller/core/landscaper/testdata/test-06/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-06/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-06/laas/landscpaper_deployments.yaml b/internal/controller/core/landscaper/testdata/test-06/laas/landscpaper_deployments.yaml new file mode 100644 index 0000000..9fb457f --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-06/laas/landscpaper_deployments.yaml @@ -0,0 +1,20 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test1 + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} +--- +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test2 + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} + diff --git a/internal/controller/core/landscaper/testdata/test-06/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-06/landscaper.yaml new file mode 100644 index 0000000..e710531 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-06/landscaper.yaml @@ -0,0 +1,16 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy diff --git a/internal/controller/core/landscaper/testdata/test-07/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-07/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-07/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-07/authentication.yaml b/internal/controller/core/landscaper/testdata/test-07/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-07/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-07/authorization.yaml b/internal/controller/core/landscaper/testdata/test-07/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-07/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-07/laas/landscpaper_deployments.yaml b/internal/controller/core/landscaper/testdata/test-07/laas/landscpaper_deployments.yaml new file mode 100644 index 0000000..46293ba --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-07/laas/landscpaper_deployments.yaml @@ -0,0 +1,9 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test1 + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} diff --git a/internal/controller/core/landscaper/testdata/test-07/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-07/landscaper.yaml new file mode 100644 index 0000000..e710531 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-07/landscaper.yaml @@ -0,0 +1,16 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy diff --git a/internal/controller/core/landscaper/testdata/test-08/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-08/apiserver.yaml new file mode 100644 index 0000000..f54e9a4 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-08/apiserver.yaml @@ -0,0 +1,42 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-08/authentication.yaml b/internal/controller/core/landscaper/testdata/test-08/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-08/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-08/authorization.yaml b/internal/controller/core/landscaper/testdata/test-08/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-08/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-08/laas/landscpaper_deployments.yaml b/internal/controller/core/landscaper/testdata/test-08/laas/landscpaper_deployments.yaml new file mode 100644 index 0000000..a9aed8d --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-08/laas/landscpaper_deployments.yaml @@ -0,0 +1,15 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} +status: + phase: Failed + lastError: + Operation: "Deploy" + reason: "DeploymentFailed" + message: "failed to create Landscaper deployment" diff --git a/internal/controller/core/landscaper/testdata/test-08/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-08/landscaper.yaml new file mode 100644 index 0000000..7f554d2 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-08/landscaper.yaml @@ -0,0 +1,19 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy + landscaperDeployment: + name: test + namespace: test \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-09/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-09/apiserver.yaml new file mode 100644 index 0000000..b658fb2 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-09/apiserver.yaml @@ -0,0 +1,44 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - dependency.openmcp.cloud/landscaper +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-09/authentication.yaml b/internal/controller/core/landscaper/testdata/test-09/authentication.yaml new file mode 100644 index 0000000..82a57ff --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-09/authentication.yaml @@ -0,0 +1,12 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + finalizers: + - dependency.openmcp.cloud/landscaper + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-09/authorization.yaml b/internal/controller/core/landscaper/testdata/test-09/authorization.yaml new file mode 100644 index 0000000..6399a65 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-09/authorization.yaml @@ -0,0 +1,20 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + finalizers: + - dependency.openmcp.cloud/landscaper + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-09/laas/landscpaper_deployments.yaml b/internal/controller/core/landscaper/testdata/test-09/laas/landscpaper_deployments.yaml new file mode 100644 index 0000000..9cd635c --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-09/laas/landscpaper_deployments.yaml @@ -0,0 +1,9 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} diff --git a/internal/controller/core/landscaper/testdata/test-09/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-09/landscaper.yaml new file mode 100644 index 0000000..4bff236 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-09/landscaper.yaml @@ -0,0 +1,25 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - landscaper.openmcp.cloud +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy + landscaperDeployment: + name: test + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-10/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-10/apiserver.yaml new file mode 100644 index 0000000..b658fb2 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-10/apiserver.yaml @@ -0,0 +1,44 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - dependency.openmcp.cloud/landscaper +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-10/authentication.yaml b/internal/controller/core/landscaper/testdata/test-10/authentication.yaml new file mode 100644 index 0000000..82a57ff --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-10/authentication.yaml @@ -0,0 +1,12 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + finalizers: + - dependency.openmcp.cloud/landscaper + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-10/authorization.yaml b/internal/controller/core/landscaper/testdata/test-10/authorization.yaml new file mode 100644 index 0000000..6399a65 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-10/authorization.yaml @@ -0,0 +1,20 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + finalizers: + - dependency.openmcp.cloud/landscaper + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-10/laas/landscpaper_deployments.yaml b/internal/controller/core/landscaper/testdata/test-10/laas/landscpaper_deployments.yaml new file mode 100644 index 0000000..9cd635c --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-10/laas/landscpaper_deployments.yaml @@ -0,0 +1,9 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} diff --git a/internal/controller/core/landscaper/testdata/test-10/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-10/landscaper.yaml new file mode 100644 index 0000000..0810dfd --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-10/landscaper.yaml @@ -0,0 +1,26 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - landscaper.openmcp.cloud + - dependency.openmcp.cloud/foo +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy + landscaperDeployment: + name: test + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-11/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-11/landscaper.yaml new file mode 100644 index 0000000..ad4b7d3 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-11/landscaper.yaml @@ -0,0 +1,27 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - landscaper.openmcp.cloud + annotations: + openmcp.cloud/operation: reconcile +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy + landscaperDeployment: + name: test + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-12/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-12/apiserver.yaml new file mode 100644 index 0000000..b658fb2 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-12/apiserver.yaml @@ -0,0 +1,44 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - dependency.openmcp.cloud/landscaper +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-12/authentication.yaml b/internal/controller/core/landscaper/testdata/test-12/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-12/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-12/authorization.yaml b/internal/controller/core/landscaper/testdata/test-12/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-12/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-12/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-12/landscaper.yaml new file mode 100644 index 0000000..af858d6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-12/landscaper.yaml @@ -0,0 +1,27 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - landscaper.openmcp.cloud + annotations: + openmcp.cloud/operation: ignore +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy + landscaperDeployment: + name: test + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-13/apiserver.yaml b/internal/controller/core/landscaper/testdata/test-13/apiserver.yaml new file mode 100644 index 0000000..b658fb2 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-13/apiserver.yaml @@ -0,0 +1,44 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - dependency.openmcp.cloud/landscaper +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/controller/core/landscaper/testdata/test-13/authentication.yaml b/internal/controller/core/landscaper/testdata/test-13/authentication.yaml new file mode 100644 index 0000000..8953d41 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-13/authentication.yaml @@ -0,0 +1,10 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + enableSystemIdentityProvider: true diff --git a/internal/controller/core/landscaper/testdata/test-13/authorization.yaml b/internal/controller/core/landscaper/testdata/test-13/authorization.yaml new file mode 100644 index 0000000..270eff6 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-13/authorization.yaml @@ -0,0 +1,18 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authorization +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "1" + name: test + namespace: test +spec: + roleBindings: + - role: admin + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: User + name: john.doe@example.com + - role: view + subjects: [] + \ No newline at end of file diff --git a/internal/controller/core/landscaper/testdata/test-13/landscaper.yaml b/internal/controller/core/landscaper/testdata/test-13/landscaper.yaml new file mode 100644 index 0000000..e0e9a89 --- /dev/null +++ b/internal/controller/core/landscaper/testdata/test-13/landscaper.yaml @@ -0,0 +1,22 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + name: test + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" + finalizers: + - landscaper.openmcp.cloud +spec: + deployers: + - "helm" + - "manifest" +status: + conditions: + - lastTransitionTime: "2024-05-27T08:45:03Z" + status: "True" + type: landscaperHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 \ No newline at end of file diff --git a/internal/controller/core/landscaper/utils/testdata/test-01/landscaper_deployments.yaml b/internal/controller/core/landscaper/utils/testdata/test-01/landscaper_deployments.yaml new file mode 100644 index 0000000..9cd635c --- /dev/null +++ b/internal/controller/core/landscaper/utils/testdata/test-01/landscaper_deployments.yaml @@ -0,0 +1,9 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} diff --git a/internal/controller/core/landscaper/utils/testdata/test-02/landscaper_deployments.yaml b/internal/controller/core/landscaper/utils/testdata/test-02/landscaper_deployments.yaml new file mode 100644 index 0000000..9fb457f --- /dev/null +++ b/internal/controller/core/landscaper/utils/testdata/test-02/landscaper_deployments.yaml @@ -0,0 +1,20 @@ +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test1 + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} +--- +apiVersion: landscaper-service.gardener.cloud/v1alpha1 +kind: LandscaperDeployment +metadata: + name: test2 + namespace: test + labels: + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test +spec: {} + diff --git a/internal/controller/core/landscaper/utils/utils.go b/internal/controller/core/landscaper/utils/utils.go new file mode 100644 index 0000000..c15f660 --- /dev/null +++ b/internal/controller/core/landscaper/utils/utils.go @@ -0,0 +1,51 @@ +package utils + +import ( + "context" + "fmt" + + laasv1alpha1 "github.com/gardener/landscaper-service/pkg/apis/core/v1alpha1" + "github.com/openmcp-project/controller-utils/pkg/logging" + "sigs.k8s.io/controller-runtime/pkg/client" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// GetCorrespondingLandscaperDeployment fetches the LandscaperDeployment belonging to the given ManagedControlPlane. +// If there is a reference but the referenced LandscaperDeployment is not found, an IsNotFound error is returned. +// If there is no reference, the method tries to find a LandscaperDeployment with a label pointing to the given ManagedControlPlane, returning nil - but no error - if none is found. +func GetCorrespondingLandscaperDeployment(ctx context.Context, laasClient client.Client, ls *openmcpv1alpha1.Landscaper) (*laasv1alpha1.LandscaperDeployment, error) { + log := logging.FromContextOrPanic(ctx) + var ld *laasv1alpha1.LandscaperDeployment + + if ls.Status.LandscaperDeploymentInfo != nil { + // Try to get the referenced LandscaperDeployment + ld = &laasv1alpha1.LandscaperDeployment{} + ld.SetName(ls.Status.LandscaperDeploymentInfo.Name) + ld.SetNamespace(ls.Status.LandscaperDeploymentInfo.Namespace) + log.Debug("Found reference to LandscaperDeployment", "resource", client.ObjectKeyFromObject(ld).String()) + if err := laasClient.Get(ctx, client.ObjectKeyFromObject(ld), ld); err != nil { + return nil, err + } + } else { + // Check if the status has somehow been lost, but there is a LandscaperDeployment referencing this ManagedControlPlane + log.Debug("No reference to LandscaperDeployment found, searching for resource with matching back-reference") + lds := &laasv1alpha1.LandscaperDeploymentList{} + if err := laasClient.List(ctx, lds, client.MatchingLabels{ + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName: ls.Name, + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace: ls.Namespace, + }); err != nil { + return nil, err + } + + if len(lds.Items) > 0 { + if len(lds.Items) > 1 { + return nil, fmt.Errorf("found %d LandscaperDeployments referencing ManagedControlPlane '%s', there should never be more than one", len(lds.Items), client.ObjectKeyFromObject(ls).String()) + } + ld = &lds.Items[0] + log.Info("Reference is missing, but found a LandscaperDeployment with a matching back-reference", "resource", client.ObjectKeyFromObject(ld).String()) + } + } + + return ld, nil +} diff --git a/internal/controller/core/landscaper/utils/utils_suite_test.go b/internal/controller/core/landscaper/utils/utils_suite_test.go new file mode 100644 index 0000000..970a905 --- /dev/null +++ b/internal/controller/core/landscaper/utils/utils_suite_test.go @@ -0,0 +1,13 @@ +package utils_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Landscaper Utils Test Suite") +} diff --git a/internal/controller/core/landscaper/utils/utils_test.go b/internal/controller/core/landscaper/utils/utils_test.go new file mode 100644 index 0000000..3b30a07 --- /dev/null +++ b/internal/controller/core/landscaper/utils/utils_test.go @@ -0,0 +1,76 @@ +package utils_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/landscaper/utils" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +var _ = Describe("GetCorrespondingLandscaperDeployment", func() { + It("returns an error when the LandscaperDeployment is not found", func() { + env := testutils.DefaultTestSetupBuilder().Build() + ls := &openmcpv1alpha1.Landscaper{ + Status: openmcpv1alpha1.LandscaperStatus{ + LandscaperDeploymentInfo: &openmcpv1alpha1.LandscaperDeploymentInfo{ + Name: "test", + Namespace: "test", + }, + }, + } + ld, err := utils.GetCorrespondingLandscaperDeployment(env.Ctx, env.Client(testutils.CrateCluster), ls) + Expect(ld).To(BeNil()) + Expect(err).ToNot(BeNil()) + }) + + It("returns LandscaperDeployment when LandscaperDeploymentInfo is set", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").Build() + ls := &openmcpv1alpha1.Landscaper{ + Status: openmcpv1alpha1.LandscaperStatus{ + LandscaperDeploymentInfo: &openmcpv1alpha1.LandscaperDeploymentInfo{ + Name: "test", + Namespace: "test", + }, + }, + } + ld, err := utils.GetCorrespondingLandscaperDeployment(env.Ctx, env.Client(testutils.CrateCluster), ls) + Expect(err).To(BeNil()) + Expect(ld).ToNot(BeNil()) + Expect(ld.GetName()).To(Equal("test")) + Expect(ld.GetNamespace()).To(Equal("test")) + + }) + + It("returns LandscaperDeployment when LandscaperDeploymentInfo is not set but matching back-reference found", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").Build() + ls := &openmcpv1alpha1.Landscaper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + ld, err := utils.GetCorrespondingLandscaperDeployment(env.Ctx, env.Client(testutils.CrateCluster), ls) + Expect(err).To(BeNil()) + Expect(ld).ToNot(BeNil()) + Expect(ld.GetName()).To(Equal("test")) + Expect(ld.GetNamespace()).To(Equal("test")) + }) + + It("returns error when multiple LandscaperDeployments referencing ManagedControlPlane", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + + ls := &openmcpv1alpha1.Landscaper{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + ld, err := utils.GetCorrespondingLandscaperDeployment(env.Ctx, env.Client(testutils.CrateCluster), ls) + Expect(ld).To(BeNil()) + Expect(err).ToNot(BeNil()) + }) +}) diff --git a/internal/controller/core/managedcontrolplane/controller.go b/internal/controller/core/managedcontrolplane/controller.go new file mode 100644 index 0000000..adba07f --- /dev/null +++ b/internal/controller/core/managedcontrolplane/controller.go @@ -0,0 +1,496 @@ +package managedcontrolplane + +import ( + "context" + "errors" + "fmt" + "slices" + "strings" + "unicode" + + "github.com/openmcp-project/mcp-operator/internal/components" + "github.com/openmcp-project/mcp-operator/internal/utils" + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + "github.com/openmcp-project/controller-utils/pkg/collections/filters" + "github.com/openmcp-project/controller-utils/pkg/collections/maps" + openmcpctrlutil "github.com/openmcp-project/controller-utils/pkg/controller" + "github.com/openmcp-project/controller-utils/pkg/logging" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +const ControllerName = "ManagedControlPlane" + +// isMCPKeyFilter is a filter that returns true for all map keys that are prefixed with the ManagedControlPlane base domain. +var isMCPKeyFilter = filters.ApplyToNthArgument(0, filters.Wrap(strings.HasPrefix, map[int]any{1: openmcpv1alpha1.BaseDomain})) + +// ManagedControlPlaneController reconciles a ManagedControlPlane object +type ManagedControlPlaneController struct { + Client client.Client +} + +func NewManagedControlPlaneController(c client.Client) *ManagedControlPlaneController { + return &ManagedControlPlaneController{ + Client: c, + } +} + +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=managedcontrolplanes,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=managedcontrolplanes/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=core.openmcp.cloud,resources=managedcontrolplanes/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +func (r *ManagedControlPlaneController) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log, ctx := utils.InitializeControllerLogger(ctx, ControllerName) + log.Debug(cconst.MsgStartReconcile) + + cp := &openmcpv1alpha1.ManagedControlPlane{} + if err := r.Client.Get(ctx, req.NamespacedName, cp); err != nil { + if apierrors.IsNotFound(err) { + log.Debug("Resource not found") + return ctrl.Result{}, nil + } + return ctrl.Result{}, err + } + + // handle operation annotation + hadReconcileAnnotation := false + if cp.GetAnnotations() != nil { + op, ok := cp.GetAnnotations()[openmcpv1alpha1.OperationAnnotation] + if ok { + switch op { + case openmcpv1alpha1.OperationAnnotationValueIgnore: + log.Info("Ignoring resource due to ignore operation annotation") + return ctrl.Result{}, nil + case openmcpv1alpha1.OperationAnnotationValueReconcile: + hadReconcileAnnotation = true + log.Debug("Removing reconcile operation annotation from resource") + if err := componentutils.PatchAnnotation(ctx, r.Client, cp, openmcpv1alpha1.OperationAnnotation, "", componentutils.ANNOTATION_DELETE); err != nil { + return ctrl.Result{}, fmt.Errorf("error removing operation annotation: %w", err) + } + } + } + } + + // fetch MCP namespace for project/workspace metadata + ns := &corev1.Namespace{} + if err := r.Client.Get(ctx, client.ObjectKey{Name: cp.Namespace}, ns); err != nil { + // this is not crucial for the reconciliation, so we just log the error + log.Error(err, "unable to fetch MCP namespace") + ns = nil + } + + // check if an InternalConfiguration resource exists for this ManagedControlPlane + icfg := &openmcpv1alpha1.InternalConfiguration{} + icfg.SetName(cp.Name) + icfg.SetNamespace(cp.Namespace) + if err := r.Client.Get(ctx, client.ObjectKeyFromObject(icfg), icfg); err != nil { + if !apierrors.IsNotFound(err) { + return ctrl.Result{}, fmt.Errorf("error fetching InternalConfiguration '%s/%s': %w", icfg.Namespace, icfg.Name, err) + } + icfg = nil + } else { + // ensure OwnerReference + log.Debug("Corresponding InternalConfiguration found") + oIdx, err := openmcpctrlutil.HasOwnerReference(icfg, cp, r.Client.Scheme()) + if err != nil { + return ctrl.Result{}, fmt.Errorf("error checking for owner reference on InternalConfiguration object: %w", err) + } + if oIdx < 0 { + // add OwnerReference + log.Debug("Patching OwnerReference into InternalConfiguration") + icfgOld := icfg.DeepCopy() + if err := controllerutil.SetControllerReference(cp, icfg, r.Client.Scheme()); err != nil { + return ctrl.Result{}, fmt.Errorf("error setting owner reference on InternalConfiguration object: %w", err) + } + if err := r.Client.Patch(ctx, icfg, client.MergeFrom(icfgOld)); err != nil { + return ctrl.Result{}, fmt.Errorf("error patching owner reference on InternalConfiguration object: %w", err) + } + } + } + + // handle deployment or deletion + var cons []openmcpv1alpha1.ManagedControlPlaneComponentCondition + var res ctrl.Result + var err error + inDeletion := !cp.DeletionTimestamp.IsZero() + if !inDeletion { + log.Info("Handling creation/update of ManagedControlPlane") + cons, res, err = r.handleCreateOrUpdate(ctx, cp, icfg, ns, hadReconcileAnnotation) + } else { + log.Info("Handling deletion of ManagedControlPlane") + cons, res, err = r.handleDelete(ctx, cp, ns, hadReconcileAnnotation) + } + + // set ManagedControlPlane meta status + cp.Status.ObservedGeneration = cp.Generation + cp.Status.Status = openmcpv1alpha1.MCPStatusReady + if err != nil { + cp.Status.Message = fmt.Sprintf("reconcile error: %s", err.Error()) + cp.Status.Status = openmcpv1alpha1.MCPStatusNotReady + } else { + cp.Status.Message = "" + } + if cons != nil { + cp.Status.Conditions = cons + for _, con := range cons { + if con.Status != openmcpv1alpha1.ComponentConditionStatusTrue { + cp.Status.Status = openmcpv1alpha1.MCPStatusNotReady + break + } + } + } + if inDeletion { + cp.Status.Status = openmcpv1alpha1.MCPStatusDeleting + } + + errs := []error{} + if err := r.Client.Status().Update(ctx, cp); err != nil { + errs = append(errs, fmt.Errorf("error updating ManagedControlPlane status: %w", err)) + } + + return res, errors.Join(errs...) +} + +func (r *ManagedControlPlaneController) handleCreateOrUpdate(ctx context.Context, mcp *openmcpv1alpha1.ManagedControlPlane, icfg *openmcpv1alpha1.InternalConfiguration, ns *corev1.Namespace, hadReconcileAnnotation bool) ([]openmcpv1alpha1.ManagedControlPlaneComponentCondition, ctrl.Result, error) { + log := logging.FromContextOrPanic(ctx) + + // add finalizer and potentially project-workspace-labels, if they doesn't exist + old := mcp.DeepCopy() + finalizerChanged := controllerutil.AddFinalizer(mcp, openmcpv1alpha1.ManagedControlPlaneFinalizer) + labelsChanged := false + var nsLabels map[string]string + if ns != nil { + nsLabels = ns.Labels + } + if mcp.Labels == nil { + mcp.Labels = map[string]string{} + } + if project, ok := nsLabels[openmcpv1alpha1.ProjectWorkspaceOperatorProjectLabel]; ok { + mcp.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject] = project + labelsChanged = true + } else { + if _, ok := mcp.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject]; ok { + delete(mcp.Labels, openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject) + labelsChanged = true + } + } + if workspace, ok := nsLabels[openmcpv1alpha1.ProjectWorkspaceOperatorWorkspaceLabel]; ok { + mcp.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace] = workspace + labelsChanged = true + } else { + if _, ok := mcp.Labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace]; ok { + delete(mcp.Labels, openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace) + labelsChanged = true + } + } + if finalizerChanged || labelsChanged { + log.Debug("Adding finalizer and/or project/workspace lables to ManagedControlPlane", "finalizerChanged", finalizerChanged, "labelsChanged", labelsChanged) + if err := r.Client.Patch(ctx, mcp, client.MergeFrom(old)); err != nil { + return nil, ctrl.Result{}, fmt.Errorf("error adding finalizer and/or project/workspace labels: %w", err) + } + // old = mcp.DeepCopy() + } + + // generate internal resources and fetch status + allCompHandlers := components.Registry.GetKnownComponents() + curCompHandlers, err := componentutils.GetComponents[*components.ComponentHandler](components.Registry, ctx, r.Client, mcp.Name, mcp.Namespace) + if err != nil { + return nil, ctrl.Result{}, fmt.Errorf("error fetching current components from cluster: %w", err) + } + genCompHandlers, err := r.ManagedControlPlaneToSplitInternalResources(mcp, icfg, ns, r.Client.Scheme(), hadReconcileAnnotation) + if err != nil { + return nil, ctrl.Result{}, fmt.Errorf("unable to convert ManagedControlPlane to internal resources: %w", err) + } + log.Info("Generated and existing components", "generatedComponents", keyStringList(genCompHandlers, true), "existingComponents", keyStringList(curCompHandlers, true)) + allErrs := []error{} + mcpSuccessful := true + componentErrors := []string{} + componentMessages := []string{} + cpcConditions := map[string]openmcpv1alpha1.ManagedControlPlaneComponentCondition{} + for ct := range allCompHandlers { + clog := log.WithValues("component", string(ct)) + ch, existingOk := curCompHandlers[ct] + genCh, generatedOk := genCompHandlers[ct] + + var cons openmcpv1alpha1.ComponentConditionList + if existingOk { + // collect conditions from all existing components + cons = ch.Resource().GetCommonStatus().Conditions + } + if len(cons) == 0 && generatedOk { + // if the component resource doesn't have any conditions (most probably due to just being created), + // create the expected conditions with status 'Unknown' on the ManagedControlPlane + cons = openmcpv1alpha1.ComponentConditionList{ + missingCondition(string(genCh.Resource().Type())), + } + } + for _, con := range cons { + if unicode.IsLower(rune(con.Type[0])) { + // don't export conditions starting with a lowercase letter + continue + } + if con.Type == ct.ReconciliationCondition() { + if con.Status != openmcpv1alpha1.ComponentConditionStatusTrue { + mcpSuccessful = false + componentErrors = append(componentErrors, fmt.Sprintf("\t%s", componentErrorFromCondition(ct, con))) + } else if con.Message != "" { + componentMessages = append(componentMessages, fmt.Sprintf("%s: %s", string(ct), strings.ReplaceAll(con.Message, "\n", "\n\t"))) + } + continue + } + if ex, ok := cpcConditions[con.Type]; ok && ex.ManagedBy != ct { + allErrs = append(allErrs, fmt.Errorf("internal error: component '%s' has condition '%s', but that condition is already managed by component '%s'", string(ct), con.Type, string(ex.ManagedBy))) + } else { + cpcConditions[con.Type] = componentConditionToCPCondition(ct, con) + } + } + + if !existingOk && !generatedOk { + // component is not defined in managedcontrolplane spec and there is no leftover component resource + continue + } else if existingOk && !generatedOk { + // component has been deleted from managedcontrolplane spec, remove it + if !ch.Resource().GetDeletionTimestamp().IsZero() { + clog.Debug("Component not in ManagedControlPlane spec, resource still exists but already has deletion timestamp, nothing to do") + } else { + clog.Debug("Component not in ManagedControlPlane spec but resource exists in cluster, removing it") + if err := r.Client.Delete(ctx, ch.Resource()); err != nil { + if !apierrors.IsNotFound(err) { + allErrs = append(allErrs, err) + } + } + } + cpGen, icGen, err := componentutils.GetCreatedFromGeneration(ch.Resource()) + if err != nil { + clog.Error(err, "error checking for deleted resource's created-from labels, trying to patch them anyway") + } + if err != nil || cpGen != mcp.Generation || (icfg == nil && icGen != -1) || (icfg != nil && icGen != icfg.Generation) { + clog.Debug("Patching outdated created-from generation labels on resource") + if err := r.Client.Patch(ctx, ch.Resource(), componentutils.GenerateCreatedFromGenerationPatch(mcp, icfg, hadReconcileAnnotation)); err != nil { + if !apierrors.IsNotFound(err) { + allErrs = append(allErrs, err) + } + } + } + continue + } + // component needs to be either created or updated + if ch == nil { + clog.Debug("Creating resource for component") + ch = allCompHandlers[ct] + ch.Resource().SetName(genCh.Resource().GetName()) + ch.Resource().SetNamespace(genCh.Resource().GetNamespace()) + } else { + clog.Debug("Updating resource for component") + } + if _, err := controllerutil.CreateOrUpdate(ctx, r.Client, ch.Resource(), func() error { + // remove potentially leftover ignore annotation + if openmcpctrlutil.HasAnnotationWithValue(ch.Resource(), openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueIgnore) && !openmcpctrlutil.HasAnnotationWithValue(genCh.Resource(), openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueIgnore) { + anns := ch.Resource().GetAnnotations() + // since removing an annotation usually doesn't trigger a reconciliation, add the reconcile annotation instead + anns[openmcpv1alpha1.OperationAnnotation] = openmcpv1alpha1.OperationAnnotationValueReconcile + ch.Resource().SetAnnotations(anns) + } + ch.Resource().SetAnnotations(maps.Merge(filters.FilterMap(ch.Resource().GetAnnotations(), filters.Not(isMCPKeyFilter)), genCh.Resource().GetAnnotations())) + ch.Resource().SetLabels(maps.Merge(filters.FilterMap(ch.Resource().GetLabels(), filters.Not(isMCPKeyFilter)), genCh.Resource().GetLabels())) + ch.Resource().SetOwnerReferences(genCh.Resource().GetOwnerReferences()) + if err := ch.Resource().SetSpec(genCh.Resource().GetSpec()); err != nil { + return fmt.Errorf("internal error transferring generated spec to existing resource for component '%s': %w", string(ct), err) + } + return nil + }); err != nil { + allErrs = append(allErrs, fmt.Errorf("error creating/updating component resource for component '%s': %w", string(ct), err)) + } + + if err := ch.Converter().InjectStatus(ch.Resource().GetExternalStatus(), &mcp.Status); err != nil { + allErrs = append(allErrs, fmt.Errorf("internal error transferring status of component '%s' into ManagedControlPlane: %w", string(ct), err)) + } + + } + + slices.Sort(componentMessages) + var oldMCPSuccessfulCon *openmcpv1alpha1.ManagedControlPlaneComponentCondition + if len(mcp.Status.Conditions) > 0 { + for i := len(mcp.Status.Conditions) - 1; i >= 0; i-- { + // the sorting usually puts the MCPSuccessful condition last, so let's search for it from the end of the list + if mcp.Status.Conditions[i].Type == cconst.ConditionMCPSuccessful { + oldMCPSuccessfulCon = mcp.Status.Conditions[i].DeepCopy() + break + } + } + } + var mcpSuccessfulCon openmcpv1alpha1.ComponentCondition + if oldMCPSuccessfulCon == nil { + // MCPSuccessful condition not found, create a new one + mcpSuccessfulCon = componentutils.NewCondition(cconst.ConditionMCPSuccessful, openmcpv1alpha1.ComponentConditionStatusFromBool(mcpSuccessful), cconst.ReasonAllComponentsReconciledSuccessfully, strings.Join(componentMessages, "\n")) + } else { + // update the existing MCPSuccessful condition to keep the lastTransitionTimestamp intact + mcpSuccessfulCon = componentutils.ConditionUpdater([]openmcpv1alpha1.ComponentCondition{oldMCPSuccessfulCon.ComponentCondition}, false).UpdateCondition(cconst.ConditionMCPSuccessful, openmcpv1alpha1.ComponentConditionStatusFromBool(mcpSuccessful), cconst.ReasonAllComponentsReconciledSuccessfully, strings.Join(componentMessages, "\n")).Conditions()[0] + } + if !mcpSuccessful { + slices.Sort(componentErrors) + mcpSuccessfulCon.Reason = cconst.ReasonNotAllComponentsReconciledSuccessfully + mcpSuccessfulCon.Message = fmt.Sprintf("The following components could not be reconciled successfully:\n%s", strings.Join(componentErrors, "\n")) + } + return append(sortConditions(cpcConditions), openmcpv1alpha1.ManagedControlPlaneComponentCondition{ComponentCondition: mcpSuccessfulCon}), ctrl.Result{}, errors.Join(allErrs...) +} + +func (r *ManagedControlPlaneController) handleDelete(ctx context.Context, mcp *openmcpv1alpha1.ManagedControlPlane, _ *corev1.Namespace, hadReconcileAnnotation bool) ([]openmcpv1alpha1.ManagedControlPlaneComponentCondition, ctrl.Result, error) { + // get all component resources + log := logging.FromContextOrPanic(ctx) + compHandlers, err := componentutils.GetComponents[*components.ComponentHandler](components.Registry, ctx, r.Client, mcp.Name, mcp.Namespace) + if err != nil { + return nil, ctrl.Result{}, fmt.Errorf("error fetching current components from cluster: %w", err) + } + + if len(compHandlers) == 0 { + // all components have been successfully deleted + log.Info("All components have been deleted") + old := mcp.DeepCopy() + changed := controllerutil.RemoveFinalizer(mcp, openmcpv1alpha1.ManagedControlPlaneFinalizer) + if changed { + if err := r.Client.Patch(ctx, mcp, client.MergeFrom(old)); err != nil { + return nil, ctrl.Result{}, fmt.Errorf("error removing finalizer from ManagedControlPlane: %w", err) + } + } + return nil, ctrl.Result{}, nil + } + + log.Info("Deleting remaining components", "existingComponents", keyStringList(compHandlers, true)) + allErrs := []error{} + mcpSuccessful := true + componentErrors := []string{} + componentMessages := []string{} + cpcConditions := map[string]openmcpv1alpha1.ManagedControlPlaneComponentCondition{} + for ct, ch := range compHandlers { + // delete components + if err := r.Client.Delete(ctx, ch.Resource()); err != nil { + allErrs = append(allErrs, fmt.Errorf("error deleting resource for component '%s': %w", string(ct), err)) + } + if hadReconcileAnnotation { + if err := componentutils.PatchAnnotation(ctx, r.Client, ch.Resource(), openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile); err != nil && !componentutils.IsAnnotationAlreadyExistsError(err) { + allErrs = append(allErrs, fmt.Errorf("error patching reconcile operation annotation on resource for component '%s': %w", string(ct), err)) + } + } + + for _, con := range ch.Resource().GetCommonStatus().Conditions { + if unicode.IsLower(rune(con.Type[0])) { + // don't export conditions starting with a lowercase letter + continue + } + if con.Type == ct.ReconciliationCondition() { + if con.Status != openmcpv1alpha1.ComponentConditionStatusTrue { + mcpSuccessful = false + componentErrors = append(componentErrors, fmt.Sprintf("\t%s", componentErrorFromCondition(ct, con))) + } else if con.Message != "" { + componentMessages = append(componentMessages, fmt.Sprintf("%s: %s", string(ct), strings.ReplaceAll(con.Message, "\n", "\n\t"))) + } + continue + } + if ex, ok := cpcConditions[con.Type]; ok && ex.ManagedBy != ct { + allErrs = append(allErrs, fmt.Errorf("internal error: component '%s' has condition '%s', but that condition is already managed by component '%s'", string(ct), con.Type, string(ex.ManagedBy))) + } else { + cpcConditions[con.Type] = componentConditionToCPCondition(ct, con) + } + } + + if err := ch.Converter().InjectStatus(ch.Resource().GetExternalStatus(), &mcp.Status); err != nil { + allErrs = append(allErrs, fmt.Errorf("internal error transferring status of component '%s' into ManagedControlPlane: %w", string(ct), err)) + } + } + + slices.Sort(componentMessages) + mcpReadyCon := componentutils.NewCondition(cconst.ConditionMCPSuccessful, openmcpv1alpha1.ComponentConditionStatusFromBool(mcpSuccessful), cconst.ReasonAllComponentsReconciledSuccessfully, strings.Join(componentMessages, "\n")) + if !mcpSuccessful { + slices.Sort(componentErrors) + mcpReadyCon.Reason = cconst.ReasonNotAllComponentsReconciledSuccessfully + mcpReadyCon.Message = fmt.Sprintf("The following components could not be reconciled successfully:\n%s", strings.Join(componentErrors, "\n")) + } + return append(sortConditions(cpcConditions), openmcpv1alpha1.ManagedControlPlaneComponentCondition{ComponentCondition: mcpReadyCon}), ctrl.Result{}, errors.Join(allErrs...) +} + +// keyStringList returns the keys of the given map as list. +// Uses fmt.Sprint to convert the key into a string. +// If sort is true, the returned slice is sorted lexically. +func keyStringList[K comparable, V any](source map[K]V, sort bool) []string { + res := make([]string, 0, len(source)) + for k := range source { + res = append(res, fmt.Sprint(k)) + } + if sort { + slices.Sort(res) + } + return res +} + +// valueList returns the values of a map as a slice. +func valueList[K comparable, V any](source map[K]V) []V { + res := make([]V, 0, len(source)) + for _, v := range source { + res = append(res, v) + } + return res +} + +// componentConditionToCPCondition converts component conditions into ManagedControlPlane conditions by adding the managing component. +func componentConditionToCPCondition(ct openmcpv1alpha1.ComponentType, con openmcpv1alpha1.ComponentCondition) openmcpv1alpha1.ManagedControlPlaneComponentCondition { + return openmcpv1alpha1.ManagedControlPlaneComponentCondition{ + ComponentCondition: con, + ManagedBy: ct, + } +} + +// missingCondition is used to create a condition with status 'Unknown' when a component's resource does not contain any conditions. +func missingCondition(conType string) openmcpv1alpha1.ComponentCondition { + return openmcpv1alpha1.ComponentCondition{ + Type: conType, + Status: openmcpv1alpha1.ComponentConditionStatusUnknown, + Reason: cconst.ReasonNoConditions, + Message: "This component does not expose any conditions.", + } +} + +// sortConditions takes a map of CPC conditions, extracts the values into a slice and returns the slice after sorting it. +// It is sorted lexically by the ManagedBy field first and the Type field second. +func sortConditions(mappedCons map[string]openmcpv1alpha1.ManagedControlPlaneComponentCondition) []openmcpv1alpha1.ManagedControlPlaneComponentCondition { + cons := valueList(mappedCons) + slices.SortFunc(cons, func(a, b openmcpv1alpha1.ManagedControlPlaneComponentCondition) int { + res := strings.Compare(string(a.ManagedBy), string(b.ManagedBy)) + if res == 0 { + res = strings.Compare(a.Type, b.Type) + } + return res + }) + return cons +} + +// componentErrorFromCondition creates a one-liner error message from a component condition. +func componentErrorFromCondition(ct openmcpv1alpha1.ComponentType, con openmcpv1alpha1.ComponentCondition) string { + return fmt.Sprintf("%s: [%s] %s", string(ct), con.Reason, con.Message) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ManagedControlPlaneController) SetupWithManager(mgr ctrl.Manager) error { + ctrlbuild := ctrl.NewControllerManagedBy(mgr).For(&openmcpv1alpha1.ManagedControlPlane{}, builder.WithPredicates(predicate.Or( + predicate.GenerationChangedPredicate{}, + predicate.LabelChangedPredicate{}, + openmcpctrlutil.GotAnnotationPredicate(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile), + openmcpctrlutil.LostAnnotationPredicate(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueIgnore), + ))) + ctrlbuild.Owns(&openmcpv1alpha1.InternalConfiguration{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})) + for _, ch := range components.Registry.GetKnownComponents() { + ctrlbuild.Owns(ch.Resource(), builder.WithPredicates(componentutils.StatusChangedPredicate{})) + } + return ctrlbuild.Complete(r) +} diff --git a/internal/controller/core/managedcontrolplane/controller_test.go b/internal/controller/core/managedcontrolplane/controller_test.go new file mode 100644 index 0000000..5cda107 --- /dev/null +++ b/internal/controller/core/managedcontrolplane/controller_test.go @@ -0,0 +1,524 @@ +package managedcontrolplane_test + +import ( + "fmt" + "os" + "path" + "testing" + "time" + + "github.com/openmcp-project/mcp-operator/internal/components" + + "github.com/openmcp-project/mcp-operator/internal/controller/core/managedcontrolplane" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gstruct" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + openmcptesting "github.com/openmcp-project/controller-utils/pkg/testing" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +func getReconciler(c ...client.Client) reconcile.Reconciler { + return managedcontrolplane.NewManagedControlPlaneController(c[0]) +} + +const ( + mcpReconciler = "mcp" +) + +var _ = Describe("CO-1153 ManagedControlPlane Controller", func() { + It("should create all component resources that are configured in the MCP and delete them again when they are unconfigured", func() { + var err error + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").WithReconcilerConstructor(mcpReconciler, getReconciler, testutils.CrateCluster).Build() + + // get ManagedControlPlane + mcp := &openmcpv1alpha1.ManagedControlPlane{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, mcp) + Expect(err).NotTo(HaveOccurred()) + + req := openmcptesting.RequestFromObject(mcp) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // check for all component resources + for ct, ch := range components.Registry.GetKnownComponents() { + if ch != nil && ch.Resource() != nil && ch.Converter() != nil && ch.Converter().IsConfigured(mcp) { + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed(), "unable to get resource for component %s", ct) + + // verify that the resource looks like expected + genSpec, err := ch.Converter().ConvertToResourceSpec(mcp, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(ch.Resource().GetSpec()).To(Equal(genSpec)) + + // check for mcp labels + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName, mcp.Name)) + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace, mcp.Namespace)) + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneGenerationLabel, fmt.Sprint(mcp.Generation))) + Expect(ch.Resource().GetLabels()).ToNot(HaveKey(openmcpv1alpha1.InternalConfigurationGenerationLabel)) + } + } + + // unconfigure all components one by one and verify their deletion + // CloudOrchestrator + mcp.Spec.Components.BTPServiceOperator = nil + mcp.Spec.Components.Crossplane = nil + mcp.Spec.Components.ExternalSecretsOperator = nil + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch := components.Registry.GetComponent(openmcpv1alpha1.CloudOrchestratorComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeFalse(), "despite removal from spec, CloudOrchestrator is still considered to be configured - most likely, the spec was expanded without adapting this test") + env.ShouldReconcile(mcpReconciler, req) + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource()) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // Landscaper + mcp.Spec.Components.Landscaper = nil + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch = components.Registry.GetComponent(openmcpv1alpha1.LandscaperComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeFalse(), "despite removal from spec, Landscaper is still considered to be configured - most likely, the spec was expanded without adapting this test") + env.ShouldReconcile(mcpReconciler, req) + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource()) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // APIServer + mcp.Spec.Components.APIServer = nil + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch = components.Registry.GetComponent(openmcpv1alpha1.APIServerComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeFalse(), "despite removal from spec, APIServer is still considered to be configured - most likely, the spec was expanded without adapting this test") + env.ShouldReconcile(mcpReconciler, req) + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource()) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // Authentication + mcp.Spec.Authentication = nil + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch = components.Registry.GetComponent(openmcpv1alpha1.AuthenticationComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeFalse(), "despite removal from spec, Authentication is still considered to be configured - most likely, the spec was expanded without adapting this test") + env.ShouldReconcile(mcpReconciler, req) + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource()) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // Authorization + mcp.Spec.Authorization = nil + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch = components.Registry.GetComponent(openmcpv1alpha1.AuthorizationComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeFalse(), "despite removal from spec, Authorization is still considered to be configured - most likely, the spec was expanded without adapting this test") + env.ShouldReconcile(mcpReconciler, req) + err = env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource()) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + }) + + It("should not create unconfigured components, add/modify them if added/modified later, and delete all component resources when the MCP is deleted", func() { + var err error + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").WithReconcilerConstructor(mcpReconciler, getReconciler, testutils.CrateCluster).Build() + + // get ManagedControlPlane + mcp := &openmcpv1alpha1.ManagedControlPlane{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, mcp) + Expect(err).NotTo(HaveOccurred()) + + req := openmcptesting.RequestFromObject(mcp) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // check for all component resources + for ct, ch := range components.Registry.GetKnownComponents() { + if ch != nil && ch.Resource() != nil && ch.Converter() != nil && ch.Converter().IsConfigured(mcp) { + err := env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource()) + Expect(err).To(HaveOccurred(), "resource for component %s should not exist", ct) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + } + } + + // load MCP from test-01 because it is fully configured + fullMcp := &openmcpv1alpha1.ManagedControlPlane{} + data, err := os.ReadFile(path.Join("testdata", "test-01", "mcp.yaml")) + Expect(err).NotTo(HaveOccurred()) + decoder := serializer.NewCodecFactory(env.Client(testutils.CrateCluster).Scheme()).UniversalDeserializer() + obj, _, err := decoder.Decode(data, nil, fullMcp) + Expect(err).NotTo(HaveOccurred()) + fullMcp, ok := obj.(*openmcpv1alpha1.ManagedControlPlane) + Expect(ok).To(BeTrue()) + + // configure all components one by one and verify their creation + // Authentication + mcp.Spec.Authentication = fullMcp.Spec.Authentication.DeepCopy() + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch := components.Registry.GetComponent(openmcpv1alpha1.AuthenticationComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeTrue()) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // Authorization + mcp.Spec.Authorization = fullMcp.Spec.Authorization.DeepCopy() + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch = components.Registry.GetComponent(openmcpv1alpha1.AuthorizationComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeTrue()) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // APIServer + mcp.Spec.Components.APIServer = fullMcp.Spec.Components.APIServer.DeepCopy() + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch = components.Registry.GetComponent(openmcpv1alpha1.APIServerComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeTrue()) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // Landscaper + mcp.Spec.Components.Landscaper = fullMcp.Spec.Components.Landscaper.DeepCopy() + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch = components.Registry.GetComponent(openmcpv1alpha1.LandscaperComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeTrue()) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // CloudOrchestrator + mcp.Spec.Components.CloudOrchestratorConfiguration = *(&fullMcp.Spec.Components.CloudOrchestratorConfiguration).DeepCopy() + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, mcp)).To(Succeed()) + ch = components.Registry.GetComponent(openmcpv1alpha1.CloudOrchestratorComponent) + Expect(ch.Converter().IsConfigured(mcp)).To(BeTrue()) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // delete the MCP and verify that all component resources are deleted + Expect(env.Client(testutils.CrateCluster).Delete(env.Ctx, mcp)).To(Succeed()) + env.ShouldReconcile(mcpReconciler, req) + for ct, ch := range components.Registry.GetKnownComponents() { + if ch != nil && ch.Resource() != nil && ch.Converter() != nil && ch.Converter().IsConfigured(mcp) { + err := env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource()) + Expect(err).To(HaveOccurred(), "resource for component %s should not exist", ct) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + } + } + }) + + It("shouldn't show status conditions of CloudOrchestrator internal conditions in lower case when MCP is in deletion", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-05").WithReconcilerConstructor(mcpReconciler, getReconciler, testutils.CrateCluster).Build() + + // get ManagedControlPlane + mcp := &openmcpv1alpha1.ManagedControlPlane{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, mcp)).To(Succeed()) + + // get CloudOrchestrator + co := &openmcpv1alpha1.CloudOrchestrator{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), co)).To(Succeed()) + + // delete MCP + Expect(env.Client(testutils.CrateCluster).Delete(env.Ctx, mcp)).To(Succeed()) + + req := openmcptesting.RequestFromObject(mcp) + env.ShouldReconcile(mcpReconciler, req) + + // check if CloudOrchestrator status has condition additionalUnexportedCondition starting with lower case + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(co), co)).To(Succeed()) + Expect(co.Status.Conditions).To(ContainElements( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: "additionalUnexportedCondition", + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + Reason: "", + Message: "", + }), + )) + Expect(co.Status.Conditions).To(HaveLen(2)) + + // check if MCP status conditions to not have additionalUnexportedCondition while MCP is in deletion + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + Expect(mcp.Status.Conditions).ToNot(ConsistOf( + MatchFields(0, Fields{ + "ManagedBy": Equal(openmcpv1alpha1.CloudOrchestratorComponent), + "ComponentCondition": MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: "additionalUnexportedCondition", + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + }), + )) + Expect(mcp.Status.Conditions).To(HaveLen(2)) + }) + + It("should apply InternalConfigurations correctly", func() { + var err error + env := testutils.DefaultTestSetupBuilder("testdata", "test-03").WithReconcilerConstructor(mcpReconciler, getReconciler, testutils.CrateCluster).Build() + + // get ManagedControlPlane + mcp := &openmcpv1alpha1.ManagedControlPlane{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, mcp) + Expect(err).NotTo(HaveOccurred()) + + // get InternalConfiguration + ic := &openmcpv1alpha1.InternalConfiguration{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, ic) + Expect(err).NotTo(HaveOccurred()) + + req := openmcptesting.RequestFromObject(mcp) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // check for all component resources + for ct, ch := range components.Registry.GetKnownComponents() { + if ch != nil && ch.Resource() != nil && ch.Converter() != nil && ch.Converter().IsConfigured(mcp) { + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed(), "unable to get resource for component %s", ct) + + // verify that the resource looks like expected + genSpec, err := ch.Converter().ConvertToResourceSpec(mcp, ic) + Expect(err).NotTo(HaveOccurred()) + Expect(ch.Resource().GetSpec()).To(Equal(genSpec)) + + // check for mcp labels + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName, mcp.Name)) + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace, mcp.Namespace)) + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneGenerationLabel, fmt.Sprint(mcp.Generation))) + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.InternalConfigurationGenerationLabel, fmt.Sprint(ic.Generation))) + } + } + + // delete InternalConfiguration and verify that component resources are updated + Expect(env.Client(testutils.CrateCluster).Delete(env.Ctx, ic)).To(Succeed()) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + // check for all component resources + for ct, ch := range components.Registry.GetKnownComponents() { + if ch != nil && ch.Resource() != nil && ch.Converter() != nil && ch.Converter().IsConfigured(mcp) { + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed(), "unable to get resource for component %s", ct) + + // verify that the resource looks like expected + genSpec, err := ch.Converter().ConvertToResourceSpec(mcp, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(ch.Resource().GetSpec()).To(Equal(genSpec)) + + // check for mcp labels + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName, mcp.Name)) + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace, mcp.Namespace)) + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneGenerationLabel, fmt.Sprint(mcp.Generation))) + Expect(ch.Resource().GetLabels()).ToNot(HaveKey(openmcpv1alpha1.InternalConfigurationGenerationLabel)) + } + } + }) + + It("should sync conditions and status back to ManagedControlPlane from component resources", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").WithReconcilerConstructor(mcpReconciler, getReconciler, testutils.CrateCluster).Build() + + // get ManagedControlPlane + mcp := &openmcpv1alpha1.ManagedControlPlane{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, mcp)).To(Succeed()) + + // get Authentication + auth := &openmcpv1alpha1.Authentication{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), auth)).To(Succeed()) + + req := openmcptesting.RequestFromObject(mcp) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + time.Sleep(1 * time.Second) // without this, it cannot be verified that the lastTransitionTime is not updated if nothing changes, because the test is too fast for the second precision of the timestamps + + // verify status + Expect(mcp.Status.Components.Authentication).To(Equal(auth.Status.ExternalAuthenticationStatus)) + + // verify conditions + Expect(mcp.Status.Conditions).To(ConsistOf( + MatchFields(0, Fields{ + "ManagedBy": Equal(openmcpv1alpha1.LandscaperComponent), + "ComponentCondition": MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: "AdditionalCondition", + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + }), + MatchFields(0, Fields{ + "ManagedBy": BeEmpty(), + "ComponentCondition": MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: cconst.ConditionMCPSuccessful, + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + Reason: cconst.ReasonAllComponentsReconciledSuccessfully, + }), + }), + MatchFields(0, Fields{ + "ManagedBy": Equal(openmcpv1alpha1.AuthenticationComponent), + "ComponentCondition": MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: string(openmcpv1alpha1.AuthenticationComponent), + Status: openmcpv1alpha1.ComponentConditionStatusUnknown, + Reason: cconst.ReasonNoConditions, + }), + }), + )) + + oldConditions := map[string]*openmcpv1alpha1.ManagedControlPlaneComponentCondition{} + for _, con := range mcp.Status.Conditions { + oldConditions[con.Type] = con.DeepCopy() + } + + // reconcile again to ensure that conditions' lastTransitionTime remains stable if nothing changes + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + for _, oldCon := range oldConditions { + Expect(mcp.Status.Conditions).To(ContainElement(MatchFields(0, Fields{ + "ManagedBy": Equal(oldCon.ManagedBy), + "ComponentCondition": MatchComponentCondition(oldCon.ComponentCondition), + })), "displaying expected timestamp for better debugging: %s", oldCon.LastTransitionTime.Format(time.RFC3339)) + } + time.Sleep(1 * time.Second) // without this, it cannot be verified that the lastTransitionTime is properly updated, because the test is too fast for the second precision of the timestamps + + // set authentication condition to false and verify again + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), auth)).To(Succeed()) + auth.Status.Conditions = append(auth.Status.Conditions, openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.AuthenticationComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: "TestReason", + Message: "Test Error Message", + LastTransitionTime: metav1.Now(), + }) + Expect(env.Client(testutils.CrateCluster).Status().Update(env.Ctx, auth)).To(Succeed()) + // reconcile MCP + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + // verify conditions + Expect(mcp.Status.Conditions).To(ConsistOf( + MatchFields(0, Fields{ + "ManagedBy": Equal(openmcpv1alpha1.LandscaperComponent), + "ComponentCondition": MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: "AdditionalCondition", + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + }), + }), + MatchFields(0, Fields{ + "ManagedBy": BeEmpty(), + "ComponentCondition": MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: cconst.ConditionMCPSuccessful, + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonNotAllComponentsReconciledSuccessfully, + Message: "The following components could not be reconciled successfully:\n\tAuthentication: [TestReason] Test Error Message", + }), + }), + )) + // verify that lastTransitionTime has changed for the conditions where the status changed + for _, con := range mcp.Status.Conditions { + if oldCon, ok := oldConditions[con.Type]; ok { + if oldCon.Status == con.Status { + Expect(con.LastTransitionTime).To(Equal(oldCon.LastTransitionTime)) + } else { + Expect(oldCon.LastTransitionTime.Before(&con.LastTransitionTime)).To(BeTrue(), "lastTransitionTime '%s' of condition '%s' should have been updated", oldCon.LastTransitionTime.Format(time.RFC3339), con.Type) + } + } + } + }) + + It("should add project and workspace metadata to MCP and all component resources, if present in namespace", func() { + var err error + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").WithReconcilerConstructor(mcpReconciler, getReconciler, testutils.CrateCluster).Build() + ns := &corev1.Namespace{} + ns.SetName("test") + ns.SetLabels(map[string]string{ + "core.openmcp.cloud/project": "test-project", + }) + Expect(env.Client(testutils.CrateCluster).Create(env.Ctx, ns)).To(Succeed()) + + // get ManagedControlPlane + mcp := &openmcpv1alpha1.ManagedControlPlane{} + err = env.Client(testutils.CrateCluster).Get(env.Ctx, types.NamespacedName{Name: "test", Namespace: "test"}, mcp) + Expect(err).NotTo(HaveOccurred()) + + expectProjectLabel := true + expectWorkspaceLabel := false + + reconcileAndTest := func() { + req := openmcptesting.RequestFromObject(mcp) + env.ShouldReconcile(mcpReconciler, req) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), mcp)).To(Succeed()) + if expectProjectLabel { + Expect(mcp.GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject, "test-project")) + } else { + Expect(mcp.GetLabels()).ToNot(HaveKey(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject)) + } + if expectWorkspaceLabel { + Expect(mcp.GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace, "test-workspace")) + } else { + Expect(mcp.GetLabels()).ToNot(HaveKey(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace)) + } + + // check for all component resources + for ct, ch := range components.Registry.GetKnownComponents() { + if ch != nil && ch.Resource() != nil && ch.Converter() != nil && ch.Converter().IsConfigured(mcp) { + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(mcp), ch.Resource())).To(Succeed(), "unable to get resource for component %s", ct) + + // verify that the resource looks like expected + genSpec, err := ch.Converter().ConvertToResourceSpec(mcp, nil) + Expect(err).NotTo(HaveOccurred()) + Expect(ch.Resource().GetSpec()).To(Equal(genSpec)) + + // check for mcp labels + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName, mcp.Name)) + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace, mcp.Namespace)) + if expectProjectLabel { + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject, "test-project")) + } else { + Expect(ch.Resource().GetLabels()).ToNot(HaveKey(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject)) + } + if expectWorkspaceLabel { + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace, "test-workspace")) + } else { + Expect(ch.Resource().GetLabels()).ToNot(HaveKey(openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace)) + } + Expect(ch.Resource().GetLabels()).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneGenerationLabel, fmt.Sprint(mcp.Generation))) + Expect(ch.Resource().GetLabels()).ToNot(HaveKey(openmcpv1alpha1.InternalConfigurationGenerationLabel)) + } + } + } + + // test with project label only + By("project label only") + reconcileAndTest() + + // test with project and workspace label + By("project and workspace label") + ns.SetLabels(map[string]string{ + "core.openmcp.cloud/project": "test-project", + "core.openmcp.cloud/workspace": "test-workspace", + }) + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, ns)).To(Succeed()) + expectWorkspaceLabel = true + reconcileAndTest() + + // test without project and workspace label + By("no project or workspace label") + ns.SetLabels(map[string]string{}) + Expect(env.Client(testutils.CrateCluster).Update(env.Ctx, ns)).To(Succeed()) + expectProjectLabel = false + expectWorkspaceLabel = false + reconcileAndTest() + }) + +}) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ManagedControlPlane Controller Test Suite") +} diff --git a/internal/controller/core/managedcontrolplane/conversion.go b/internal/controller/core/managedcontrolplane/conversion.go new file mode 100644 index 0000000..3880390 --- /dev/null +++ b/internal/controller/core/managedcontrolplane/conversion.go @@ -0,0 +1,81 @@ +package managedcontrolplane + +import ( + "fmt" + "slices" + + "github.com/openmcp-project/mcp-operator/internal/components" + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// ManagedControlPlaneToSplitInternalResources converts the given v1alpha1.ManagedControlPlane into multiple internal resources. +// The returned map contains only those componentes, for which the ManagedControlPlane contains configuration. +func (*ManagedControlPlaneController) ManagedControlPlaneToSplitInternalResources(mcp *openmcpv1alpha1.ManagedControlPlane, icfg *openmcpv1alpha1.InternalConfiguration, ns *corev1.Namespace, scheme *runtime.Scheme, addReconcileAnnotation bool) (map[openmcpv1alpha1.ComponentType]*components.ComponentHandler, error) { + if mcp == nil { + return nil, nil + } + + labels := map[string]string{ + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelName: mcp.Name, + openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelNamespace: mcp.Namespace, + } + if ns != nil && ns.Labels != nil { + if project, ok := ns.Labels[openmcpv1alpha1.ProjectWorkspaceOperatorProjectLabel]; ok { + labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelProject] = project + } + if workspace, ok := ns.Labels[openmcpv1alpha1.ProjectWorkspaceOperatorWorkspaceLabel]; ok { + labels[openmcpv1alpha1.ManagedControlPlaneBackReferenceLabelWorkspace] = workspace + } + } + + res := map[openmcpv1alpha1.ComponentType]*components.ComponentHandler{} + allCompHandlerss := components.Registry.GetKnownComponents() + for ct, ch := range allCompHandlerss { + if ch != nil && ch.Resource() != nil && ch.Converter() != nil && ch.Converter().IsConfigured(mcp) { + ch.Resource().SetName(mcp.Name) + ch.Resource().SetNamespace(mcp.Namespace) + + spec, err := ch.Converter().ConvertToResourceSpec(mcp, icfg) + if err != nil { + return nil, fmt.Errorf("error converting configuration for component '%s' into spec for that component's resource: %w", string(ct), err) + } + if err := ch.Resource().SetSpec(spec); err != nil { + return nil, fmt.Errorf("internal error: the spec for component '%s' cannot be passed into the resource for this component", string(ct)) + } + + if componentIsDisabled(mcp, ct) { + ch.Resource().SetAnnotations(map[string]string{ + openmcpv1alpha1.OperationAnnotation: openmcpv1alpha1.OperationAnnotationValueIgnore, + }) + } else if addReconcileAnnotation { + ch.Resource().SetAnnotations(map[string]string{ + openmcpv1alpha1.OperationAnnotation: openmcpv1alpha1.OperationAnnotationValueReconcile, + }) + } + + ch.Resource().SetLabels(labels) + componentutils.SetCreatedFromGeneration(ch.Resource(), mcp, icfg) + if err := controllerutil.SetControllerReference(mcp, ch.Resource(), scheme); err != nil { + return nil, fmt.Errorf("unable to set owner reference: %w", err) + } + res[ct] = ch + } + } + + return res, nil +} + +// componentIsDisabled returns true if the given component type is disabled in the given managedcontrolplane's spec. +func componentIsDisabled(mcp *openmcpv1alpha1.ManagedControlPlane, ct openmcpv1alpha1.ComponentType) bool { + if mcp == nil || len(mcp.Spec.DisabledComponents) == 0 { + return false + } + + return slices.Contains(mcp.Spec.DisabledComponents, ct) +} diff --git a/internal/controller/core/managedcontrolplane/testdata/test-01/mcp.yaml b/internal/controller/core/managedcontrolplane/testdata/test-01/mcp.yaml new file mode 100644 index 0000000..8a2a57c --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-01/mcp.yaml @@ -0,0 +1,29 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ManagedControlPlane +metadata: + name: test + namespace: test + generation: 5 +spec: + desiredRegion: + name: europe + direction: central + authentication: + enableSystemIdentityProvider: true + authorization: + roleBindings: + - role: admin + subjects: + - kind: User + name: idp:john.doe@example.org + components: + apiServer: + type: Gardener + landscaper: {} + crossplane: + version: 1.17.0 + providers: + - name: cloudfoundry + version: 2.2.3 + btpServiceOperator: + version: 0.8.0 diff --git a/internal/controller/core/managedcontrolplane/testdata/test-02/mcp.yaml b/internal/controller/core/managedcontrolplane/testdata/test-02/mcp.yaml new file mode 100644 index 0000000..111514c --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-02/mcp.yaml @@ -0,0 +1,11 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ManagedControlPlane +metadata: + name: test + namespace: test + generation: 5 +spec: + desiredRegion: + name: europe + direction: central + components: {} diff --git a/internal/controller/core/managedcontrolplane/testdata/test-03/ic.yaml b/internal/controller/core/managedcontrolplane/testdata/test-03/ic.yaml new file mode 100644 index 0000000..de11d11 --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-03/ic.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: InternalConfiguration +metadata: + name: test + namespace: test + generation: 3 +spec: + components: + apiServer: + gardener: + shootOverwrite: + name: foo + namespace: bar \ No newline at end of file diff --git a/internal/controller/core/managedcontrolplane/testdata/test-03/mcp.yaml b/internal/controller/core/managedcontrolplane/testdata/test-03/mcp.yaml new file mode 100644 index 0000000..8a2a57c --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-03/mcp.yaml @@ -0,0 +1,29 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ManagedControlPlane +metadata: + name: test + namespace: test + generation: 5 +spec: + desiredRegion: + name: europe + direction: central + authentication: + enableSystemIdentityProvider: true + authorization: + roleBindings: + - role: admin + subjects: + - kind: User + name: idp:john.doe@example.org + components: + apiServer: + type: Gardener + landscaper: {} + crossplane: + version: 1.17.0 + providers: + - name: cloudfoundry + version: 2.2.3 + btpServiceOperator: + version: 0.8.0 diff --git a/internal/controller/core/managedcontrolplane/testdata/test-04/auth.yaml b/internal/controller/core/managedcontrolplane/testdata/test-04/auth.yaml new file mode 100644 index 0000000..4751919 --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-04/auth.yaml @@ -0,0 +1,21 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Authentication +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "5" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test +spec: + enableSystemIdentityProvider: true +status: + access: + key: config + name: test-access + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 5 + resource: 1 \ No newline at end of file diff --git a/internal/controller/core/managedcontrolplane/testdata/test-04/ls.yaml b/internal/controller/core/managedcontrolplane/testdata/test-04/ls.yaml new file mode 100644 index 0000000..9997898 --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-04/ls.yaml @@ -0,0 +1,33 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "5" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test +spec: + deployers: + - helm + - manifest + - container +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "True" + type: LandscaperReconciliation + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "True" + type: AdditionalCondition + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "True" + type: additionalUnexportedCondition + landscaperDeployment: + name: test + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 5 + resource: 1 \ No newline at end of file diff --git a/internal/controller/core/managedcontrolplane/testdata/test-04/mcp.yaml b/internal/controller/core/managedcontrolplane/testdata/test-04/mcp.yaml new file mode 100644 index 0000000..13762b4 --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-04/mcp.yaml @@ -0,0 +1,14 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ManagedControlPlane +metadata: + name: test + namespace: test + generation: 5 +spec: + desiredRegion: + name: europe + direction: central + authentication: + enableSystemIdentityProvider: true + components: + landscaper: {} diff --git a/internal/controller/core/managedcontrolplane/testdata/test-05/co.yaml b/internal/controller/core/managedcontrolplane/testdata/test-05/co.yaml new file mode 100644 index 0000000..206a1b9 --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-05/co.yaml @@ -0,0 +1,29 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: CloudOrchestrator +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "5" + openmcp.cloud/mcp-name: test + openmcp.cloud/mcp-namespace: test + name: test + namespace: test + finalizers: + - test +spec: + crossplane: + version: 1.17.0 +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + message: "Crossplane is healthy." + reason: "Healthy" + status: "True" + type: CrossplaneReady + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "True" + type: additionalUnexportedCondition + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 5 + resource: 1 \ No newline at end of file diff --git a/internal/controller/core/managedcontrolplane/testdata/test-05/mcp.yaml b/internal/controller/core/managedcontrolplane/testdata/test-05/mcp.yaml new file mode 100644 index 0000000..df35d77 --- /dev/null +++ b/internal/controller/core/managedcontrolplane/testdata/test-05/mcp.yaml @@ -0,0 +1,25 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ManagedControlPlane +metadata: + name: test + namespace: test + generation: 5 + finalizers: + - test +spec: + desiredRegion: + name: europe + direction: central + authentication: + enableSystemIdentityProvider: true + authorization: + roleBindings: + - role: admin + subjects: + - kind: User + name: idp:john.doe@example.org + components: + apiServer: + type: GardenerDedicated + crossplane: + version: 1.17.0 \ No newline at end of file diff --git a/internal/releasechannel/managedcomponents.go b/internal/releasechannel/managedcomponents.go new file mode 100644 index 0000000..28a987b --- /dev/null +++ b/internal/releasechannel/managedcomponents.go @@ -0,0 +1,148 @@ +package releasechannel + +import ( + "context" + "slices" + "time" + + cpoev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +const interval = 15 * time.Minute + +type ReleasechannelRunnable struct { + crateClient client.Client + coreClient client.Client +} + +func NewReleasechannelRunnable(crateClient client.Client, coreClient client.Client) ReleasechannelRunnable { + return ReleasechannelRunnable{ + crateClient: crateClient, + coreClient: coreClient, + } +} + +func (r *ReleasechannelRunnable) NeedLeaderElection() bool { + return true +} + +func (r *ReleasechannelRunnable) Start(ctx context.Context) error { + ch := time.Tick(interval) + for { + select { + case <-ctx.Done(): + return nil + case <-ch: + err := r.loop(ctx) + if err != nil { + return err + } + } + } +} + +func (r *ReleasechannelRunnable) loop(ctx context.Context) error { + log := log.FromContext(ctx) + + // Get a list of all managedComponents in the crate cluster + currentManagedComponentList := v1alpha1.ManagedComponentList{} + err := r.crateClient.List(ctx, ¤tManagedComponentList) + if err != nil { + return err + } + + // Get a list of all releasechannels in the core cluster + releasechannelList := cpoev1beta1.ReleaseChannelList{} + err = r.coreClient.List(ctx, &releasechannelList) + if err != nil { + return err + } + + // Flat the components of all releasechannels + releasechannelComponents := flatAndRemoveDuplicatesReleasechannelComponents(releasechannelList.Items) + + for _, managedcomponent := range currentManagedComponentList.Items { + // check if the managedComponent is still in any of the releasechannels + contains := slices.ContainsFunc(releasechannelComponents, func(component cpoev1beta1.Component) bool { + return component.Name == managedcomponent.Name + }) + + // the managed component is not in any releasechannel anymore, delete it + if !contains { + err := r.crateClient.Delete(ctx, &managedcomponent) + if err != nil { + log.Error(err, "Failed to delete managedComponent", "name", managedcomponent.Name) + } + } + } + +outer: + for _, component := range releasechannelComponents { + versions := make([]string, 0, len(component.Versions)) + for _, version := range component.Versions { + versions = append(versions, version.Version) + } + + // check wether the crate cluster already has a managedComponent for this component + for _, managedComponent := range currentManagedComponentList.Items { + if managedComponent.Name == component.Name { + managedComponent.Status.Versions = versions + // The managedComponent is inside the releasechannel Update the managedComponent status + err := r.crateClient.Status().Update(ctx, &managedComponent) + if err != nil { + log.Error(err, "Failed to update managedComponent status", "name", &managedComponent.Name) + } + continue outer + } + } + + newMc := v1alpha1.ManagedComponent{ + ObjectMeta: metav1.ObjectMeta{ + Name: component.Name, + }, + Status: v1alpha1.ManagedComponentStatus{ + Versions: versions, + }, + } + + err := r.crateClient.Create(ctx, &newMc) + if err != nil { + log.Error(err, "Failed to create managedComponent", "name", component.Name) + } + + err = r.crateClient.Status().Update(ctx, &newMc) + if err != nil { + log.Error(err, "Failed to update managedComponent status", "name", component.Name) + } + } + + return nil +} + +func flatAndRemoveDuplicatesReleasechannelComponents(releasechannels []cpoev1beta1.ReleaseChannel) []cpoev1beta1.Component { + releasechannelComponents := make([]cpoev1beta1.Component, 0) + for _, releasechannel := range releasechannels { + // Check if there are components in multiple releasechannels + for _, component := range releasechannel.Status.Components { + contains := false + for _, c := range releasechannelComponents { + // If the already added component has the same name, append the versions + if c.Name == component.Name { + contains = true + c.Versions = append(c.Versions, component.Versions...) + } + } + // If there is no component with the same name, add the component + if !contains { + releasechannelComponents = append(releasechannelComponents, component) + } + } + } + + return releasechannelComponents +} diff --git a/internal/releasechannel/managedcomponents_test.go b/internal/releasechannel/managedcomponents_test.go new file mode 100644 index 0000000..0c978d7 --- /dev/null +++ b/internal/releasechannel/managedcomponents_test.go @@ -0,0 +1,149 @@ +package releasechannel + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/openmcp-project/controller-utils/pkg/testing" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +func testEnvSetup(crateObjectsPath, coObjectsPath string, crDynamicObjects ...client.Object) *testing.ComplexEnvironment { + builder := testutils.DefaultTestSetupBuilder(crateObjectsPath).WithFakeClient(testutils.COCoreCluster, testutils.Scheme) + if coObjectsPath != "" { + builder.WithInitObjectPath(testutils.COCoreCluster, coObjectsPath) + } + if len(crDynamicObjects) > 0 { + builder.WithDynamicObjectsWithStatus(testutils.CrateCluster, crDynamicObjects...) + } + return builder.Build() +} + +var _ = Describe("CO-1153 ReleasechannelRunnable", func() { + It("Should create managedcomponents", func() { + env := testEnvSetup("", "testdata/core") + + var crateClient client.Client + var coreClient client.Client + for key, client := range env.Clusters { + if key == testutils.COCoreCluster { + coreClient = client + } + if key == testutils.CrateCluster { + crateClient = client + } + } + + Expect(coreClient).ToNot(BeNil()) + Expect(crateClient).ToNot(BeNil()) + + runnable := NewReleasechannelRunnable(crateClient, coreClient) + err := runnable.loop(env.Ctx) + if err != nil { + Fail(err.Error()) + } + + managedComponents := v1alpha1.ManagedComponentList{} + err = crateClient.List(env.Ctx, &managedComponents) + if err != nil { + Fail(err.Error()) + } + + Expect(len(managedComponents.Items)).To(Equal(23)) + + }) + It("Should update managedcomponents", func() { + managedComponent := v1alpha1.ManagedComponent{ + ObjectMeta: v1.ObjectMeta{Name: "crossplane"}, + Status: v1alpha1.ManagedComponentStatus{ + Versions: []string{ + "0.14.0", + }, + }, + } + env := testEnvSetup("testdata/crate", "testdata/core", &managedComponent) + + var crateClient client.Client + var coreClient client.Client + for key, client := range env.Clusters { + if key == testutils.COCoreCluster { + coreClient = client + } + if key == testutils.CrateCluster { + crateClient = client + } + } + + //err := crateClient.Create(env.Ctx, &managedComponent) + //if err != nil { + // Fail(err.Error()) + //} + + Expect(coreClient).ToNot(BeNil()) + Expect(crateClient).ToNot(BeNil()) + + managedComponents := v1alpha1.ManagedComponentList{} + err := crateClient.List(env.Ctx, &managedComponents) + if err != nil { + Fail(err.Error()) + } + + Expect(len(managedComponents.Items)).To(Equal(1)) + + runnable := NewReleasechannelRunnable(crateClient, coreClient) + err = runnable.loop(env.Ctx) + if err != nil { + Fail(err.Error()) + } + + managedCrossplaneComponent := v1alpha1.ManagedComponent{} + err = crateClient.Get(env.Ctx, client.ObjectKey{Name: "crossplane"}, &managedCrossplaneComponent) + if err != nil { + Fail(err.Error()) + } + + Expect(len(managedCrossplaneComponent.Status.Versions), 1) + }) + It("Should delete managedcomponents", func() { + env := testEnvSetup("testdata/crate", "") + + var crateClient client.Client + var coreClient client.Client + for key, client := range env.Clusters { + if key == testutils.COCoreCluster { + coreClient = client + } + if key == testutils.CrateCluster { + crateClient = client + } + } + + Expect(coreClient).ToNot(BeNil()) + Expect(crateClient).ToNot(BeNil()) + + managedComponents := v1alpha1.ManagedComponentList{} + err := crateClient.List(env.Ctx, &managedComponents) + if err != nil { + Fail(err.Error()) + } + + Expect(len(managedComponents.Items)).To(Equal(1)) + + runnable := NewReleasechannelRunnable(crateClient, coreClient) + err = runnable.loop(env.Ctx) + if err != nil { + Fail(err.Error()) + } + + managedComponents = v1alpha1.ManagedComponentList{} + err = crateClient.List(env.Ctx, &managedComponents) + if err != nil { + Fail(err.Error()) + } + + Expect(len(managedComponents.Items)).To(Equal(0)) + }) +}) diff --git a/internal/releasechannel/mc_suite_test.go b/internal/releasechannel/mc_suite_test.go new file mode 100644 index 0000000..0546bc2 --- /dev/null +++ b/internal/releasechannel/mc_suite_test.go @@ -0,0 +1,13 @@ +package releasechannel + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ManagedComponents ReleasechannelRunnable") +} diff --git a/internal/releasechannel/testdata/core/releasechannel.yaml b/internal/releasechannel/testdata/core/releasechannel.yaml new file mode 100644 index 0000000..9ada657 --- /dev/null +++ b/internal/releasechannel/testdata/core/releasechannel.yaml @@ -0,0 +1,221 @@ +apiVersion: core.orchestrate.cloud.sap/v1beta1 +kind: ReleaseChannel +metadata: + creationTimestamp: "2025-01-27T14:03:12Z" + generation: 1 + labels: + kustomize.toolkit.fluxcd.io/name: bootstrap + kustomize.toolkit.fluxcd.io/namespace: co-system + name: cloudorchestration-default + resourceVersion: "108334211" + uid: a53b5f82-ee31-4175-8095-056fe8e1d8a6 +spec: + interval: 15m + ocmRegistryUrl: ghcr.io/openmcp-project/ocm + prefixFilter: ghcr.io/openmcp-project + pullSecretRef: + name: artifactory-readonly-ocm-openmcp + namespace: co-system +status: + components: + - name: cert-manager + versions: + - helmChart: cert-manager + helmRepo: https://charts.jetstack.io + version: 1.13.1 + - helmChart: cert-manager + helmRepo: https://charts.jetstack.io + version: 1.16.1 + - name: crossplane + versions: + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.15.0 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.15.5 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.16.0 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.16.1 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.16.2 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.17.0 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.17.1 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.17.2 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.17.3 + - helmChart: crossplane + helmRepo: https://charts.crossplane.io/stable + version: 1.18.0 + - name: external-secrets + versions: + - helmChart: external-secrets + helmRepo: https://charts.external-secrets.io + version: 0.10.7 + - helmChart: external-secrets + helmRepo: https://charts.external-secrets.io + version: 0.11.0 + - helmChart: external-secrets + helmRepo: https://charts.external-secrets.io + version: 0.12.1 + - helmChart: external-secrets + helmRepo: https://charts.external-secrets.io + version: 0.13.0 + - helmChart: external-secrets + helmRepo: https://charts.external-secrets.io + version: 0.8.0 + - name: flux + versions: + - helmChart: flux2 + helmRepo: https://fluxcd-community.github.io/helm-charts + version: 2.12.4 + - helmChart: flux2 + helmRepo: https://fluxcd-community.github.io/helm-charts + version: 2.13.0 + - helmChart: flux2 + helmRepo: https://fluxcd-community.github.io/helm-charts + version: 2.14.0 + - name: kyverno + versions: + - helmChart: kyverno + helmRepo: https://kyverno.github.io/kyverno + version: 3.2.4 + - name: provider-argocd + versions: + - dockerRef: xpkg.upbound.io/crossplane-contrib/provider-argocd:v0.8.0 + version: 0.8.0 + - dockerRef: xpkg.upbound.io/crossplane-contrib/provider-argocd:v0.8.1 + version: 0.8.1 + - dockerRef: xpkg.upbound.io/crossplane-contrib/provider-argocd:v0.9.0 + version: 0.9.0 + - dockerRef: xpkg.upbound.io/crossplane-contrib/provider-argocd:v0.9.1 + version: 0.9.1 + - name: provider-btp + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-btp:v1.0.0 + version: 1.0.0 + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-btp:v1.0.1 + version: 1.0.1 + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-btp:v1.0.2 + version: 1.0.2 + - name: provider-btp-account + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-btp-account:0.7.5 + version: 0.7.5 + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-btp-account:0.7.6 + version: 0.7.6 + - name: provider-cloudfoundry + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-cloudfoundry:2.2.3 + version: 2.2.3 + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-cloudfoundry:2.2.4 + version: 2.2.4 + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-cloudfoundry:2.2.5 + version: 2.2.5 + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-cloudfoundry:2.3.0 + version: 2.3.0 + - name: provider-destinations + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-destinations:1.0.3 + version: 1.0.3 + - name: provider-dynatrace + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-dynatrace:1.1.2 + version: 1.1.2 + - name: provider-gardener-auth + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-gardener-auth:0.0.4 + version: 0.0.4 + - name: provider-hana + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-hana:0.1.0 + version: 0.1.0 + - name: provider-helm + versions: + - dockerRef: xpkg.upbound.io/crossplane-contrib/provider-helm:v0.19.0 + version: 0.19.0 + - name: provider-hyperscaler + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-hyperscaler:0.0.1 + version: 0.0.1 + - name: provider-ias + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-ias:0.2.0 + version: 0.2.0 + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-ias:0.2.1 + version: 0.2.1 + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-ias:0.2.2 + version: 0.2.2 + - name: provider-kubernetes + versions: + - dockerRef: xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.14.0 + version: 0.14.0 + - dockerRef: xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.14.1 + version: 0.14.1 + - dockerRef: xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.15.0 + version: 0.15.0 + - name: provider-message-queue + versions: + - dockerRef: ghcr.io/sap/crossplane-provider-btp/crossplane/provider-message-queue:1.0.1 + version: 1.0.1 + - name: provider-terraform + versions: + - dockerRef: xpkg.upbound.io/upbound/provider-terraform:v0.16.0 + version: 0.16.0 + - name: provider-vault + versions: + - dockerRef: xpkg.upbound.io/upbound/provider-vault:v1.0.0 + version: 1.0.0 + - name: sap-btp-service-operator + versions: + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.5.4 + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.6.0 + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.6.1 + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.6.2 + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.6.3 + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.6.4 + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.6.5 + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.6.6 + - helmChart: sap-btp-operator + helmRepo: https://sap.github.io/sap-btp-service-operator + version: 0.6.8 + - name: syncer + versions: + - helmChart: co-syncer + helmRepo: https://example.com/artifactory/api/helm/deploy-releases-hyperspace-helm + version: 0.3.1 + - helmChart: co-syncer + helmRepo: https://example.com/artifactory/api/helm/deploy-releases-hyperspace-helm + version: 0.3.2 + - name: velero + versions: + - helmChart: velero + helmRepo: https://vmware-tanzu.github.io/helm-charts/ + version: 7.1.0 diff --git a/internal/releasechannel/testdata/crate/managedcomponent.yaml b/internal/releasechannel/testdata/crate/managedcomponent.yaml new file mode 100644 index 0000000..2f80ecf --- /dev/null +++ b/internal/releasechannel/testdata/crate/managedcomponent.yaml @@ -0,0 +1,7 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: ManagedComponent +metadata: + name: crossplane +status: + versions: + - 1.1.0 diff --git a/internal/utils/apiserver/access.go b/internal/utils/apiserver/access.go new file mode 100644 index 0000000..79beab5 --- /dev/null +++ b/internal/utils/apiserver/access.go @@ -0,0 +1,68 @@ +package apiserver + +import ( + "fmt" + + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// APIServerAccess provides access to an APIServer's admin kubeconfig. +type APIServerAccess interface { + // GetAdminAccessClient returns the admin access kubeconfig for the given APIServer. + GetAdminAccessClient(as *openmcpv1alpha1.APIServer, options client.Options) (client.Client, error) + // GetAdminAccessConfig returns the admin access kubeconfig for the given APIServer. + GetAdminAccessConfig(as *openmcpv1alpha1.APIServer) (*restclient.Config, error) + // GetAdminAccessRaw returns the admin access kubeconfig for the given APIServer. + GetAdminAccessRaw(as *openmcpv1alpha1.APIServer) (string, error) +} + +// APIServerAccessImpl is the default implementation of APIServerAccess. +type APIServerAccessImpl struct { + NewClient client.NewClientFunc +} + +// GetAdminAccessClient implements APIServerAccess.GetAdminAccessClient. +func (a *APIServerAccessImpl) GetAdminAccessClient(apiServer *openmcpv1alpha1.APIServer, options client.Options) (client.Client, error) { + if a.NewClient == nil { + return nil, fmt.Errorf("NewClient function not set") + } + + config, err := a.GetAdminAccessConfig(apiServer) + if err != nil { + return nil, err + } + + apiServerClient, err := a.NewClient(config, options) + if err != nil { + return nil, fmt.Errorf("failed to create client: %w", err) + } + + return apiServerClient, nil +} + +// GetAdminAccessConfig implements APIServerAccess.GetAdminAccessConfig. +func (a *APIServerAccessImpl) GetAdminAccessConfig(apiServer *openmcpv1alpha1.APIServer) (*restclient.Config, error) { + if apiServer.Status.AdminAccess == nil || apiServer.Status.AdminAccess.Kubeconfig == "" { + return nil, fmt.Errorf("admin access kubeconfig not available") + } + + config, err := clientcmd.RESTConfigFromKubeConfig([]byte(apiServer.Status.AdminAccess.Kubeconfig)) + if err != nil { + return nil, fmt.Errorf("failed to create REST config from kubeconfig: %w", err) + } + + return config, nil +} + +// GetAdminAccessRaw implements APIServerAccess.GetAdminAccessRaw. +func (a *APIServerAccessImpl) GetAdminAccessRaw(apiServer *openmcpv1alpha1.APIServer) (string, error) { + if apiServer.Status.AdminAccess == nil || apiServer.Status.AdminAccess.Kubeconfig == "" { + return "", fmt.Errorf("admin access kubeconfig not available") + } + + return apiServer.Status.AdminAccess.Kubeconfig, nil +} diff --git a/internal/utils/apiserver/access_test.go b/internal/utils/apiserver/access_test.go new file mode 100644 index 0000000..89a8982 --- /dev/null +++ b/internal/utils/apiserver/access_test.go @@ -0,0 +1,85 @@ +package apiserver_test + +import ( + "os" + "path" + + "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + restclient "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + "github.com/openmcp-project/mcp-operator/test/utils" +) + +var _ = Describe("APIServerAccess", func() { + + var ( + apiServerAccess *apiserver.APIServerAccessImpl + as *openmcpv1alpha1.APIServer + ) + + BeforeEach(func() { + kubeConfig, err := os.ReadFile(path.Join("testdata", "kubeconfig.yaml")) + Expect(err).ToNot(HaveOccurred()) + env := testing.NewEnvironmentBuilder().WithFakeClient(utils.Scheme).Build() + + apiServerAccess = &apiserver.APIServerAccessImpl{} + as = &openmcpv1alpha1.APIServer{ + Status: openmcpv1alpha1.APIServerStatus{ + AdminAccess: &openmcpv1alpha1.APIServerAccess{ + Kubeconfig: string(kubeConfig), + }, + }, + } + + apiServerAccess.NewClient = func(config *restclient.Config, options client.Options) (client.Client, error) { + return env.Client(), nil + } + }) + + Context("GetAdminAccessClient", func() { + It("returns error when GetAdminAccessClient fails", func() { + as.Status.AdminAccess.Kubeconfig = "" + _, err := apiServerAccess.GetAdminAccessClient(as, client.Options{}) + Expect(err).To(HaveOccurred()) + }) + + It("returns client when GetAdminAccessConfig succeeds", func() { + _, err := apiServerAccess.GetAdminAccessClient(as, client.Options{}) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("GetAdminAccessConfig", func() { + It("returns error when admin access kubeconfig is not available", func() { + as.Status.AdminAccess.Kubeconfig = "" + _, err := apiServerAccess.GetAdminAccessConfig(as) + Expect(err).To(HaveOccurred()) + }) + + It("returns config when admin access kubeconfig is available", func() { + _, err := apiServerAccess.GetAdminAccessConfig(as) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Context("GetAdminAccessRaw", func() { + It("returns error when admin access kubeconfig is not available", func() { + as.Status.AdminAccess.Kubeconfig = "" + _, err := apiServerAccess.GetAdminAccessRaw(as) + Expect(err).To(HaveOccurred()) + }) + + It("returns kubeconfig when admin access kubeconfig is available", func() { + kubeconfig, err := apiServerAccess.GetAdminAccessRaw(as) + Expect(err).ToNot(HaveOccurred()) + Expect(kubeconfig).ToNot(BeEmpty()) + }) + }) +}) diff --git a/internal/utils/apiserver/suite_test.go b/internal/utils/apiserver/suite_test.go new file mode 100644 index 0000000..de87cfc --- /dev/null +++ b/internal/utils/apiserver/suite_test.go @@ -0,0 +1,13 @@ +package apiserver_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "APIServer Utils Test Suite") +} diff --git a/internal/utils/apiserver/testdata/kubeconfig.yaml b/internal/utils/apiserver/testdata/kubeconfig.yaml new file mode 100644 index 0000000..972be29 --- /dev/null +++ b/internal/utils/apiserver/testdata/kubeconfig.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Config +clusters: +- name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK +contexts: +- name: apiserver + context: + cluster: apiserver + user: apiserver +current-context: apiserver +users: +- name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK diff --git a/internal/utils/apiserver/testdata/worker/apiserver/secret.yaml b/internal/utils/apiserver/testdata/worker/apiserver/secret.yaml new file mode 100644 index 0000000..7f76bfb --- /dev/null +++ b/internal/utils/apiserver/testdata/worker/apiserver/secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: test + namespace: test +stringData: + foo: "bar" diff --git a/internal/utils/apiserver/testdata/worker/apiservers.yaml b/internal/utils/apiserver/testdata/worker/apiservers.yaml new file mode 100644 index 0000000..5ca775d --- /dev/null +++ b/internal/utils/apiserver/testdata/worker/apiservers.yaml @@ -0,0 +1,85 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test1 + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK +--- +apiVersion: core.openmcp.cloud/v1alpha1 +kind: APIServer +metadata: + name: test2 + namespace: test + labels: + "openmcp.cloud/mcp-generation": "1" +spec: + desiredRegion: + direction: central + name: europe + type: GardenerDedicated +status: + conditions: + - lastTransitionTime: "2024-05-22T08:23:47Z" + status: "True" + type: apiServerHealthy + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 1 + resource: 0 + adminAccess: + creationTimestamp: "2024-05-22T08:23:47Z" + expirationTimestamp: "2024-11-18T08:23:47Z" + kubeconfig: | + apiVersion: v1 + clusters: + - name: apiserver + cluster: + server: https://apiserver.dummy + certificate-authority-data: ZHVtbXkK + contexts: + - name: apiserver + context: + cluster: apiserver + user: apiserver + current-context: apiserver + users: + - name: apiserver + user: + client-certificate-data: ZHVtbXkK + client-key-data: ZHVtbXkK \ No newline at end of file diff --git a/internal/utils/apiserver/worker.go b/internal/utils/apiserver/worker.go new file mode 100644 index 0000000..3c1b8f8 --- /dev/null +++ b/internal/utils/apiserver/worker.go @@ -0,0 +1,239 @@ +package apiserver + +import ( + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/alitto/pond/v2" + "github.com/openmcp-project/controller-utils/pkg/logging" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// Task is a function that is executed for each APIServer in the cluster +// The first client is for the Crate cluster +// The second client is for the APIServer cluster +type Task func(context.Context, *openmcpv1alpha1.APIServer, client.Client, client.Client) error + +// OnExit is a channel that is used to signal when the worker is stopped +type OnExit chan bool + +// OnNextInterval is a channel that is used to signal when the next interval is executed +type OnNextInterval chan bool + +// Worker has a pool of workers which execute all tasks for each APIServer in the cluster +type Worker interface { + // RegisterTask registers a new task with the given name + // The name must be unique + RegisterTask(name string, task Task) + // UnregisterTask removes the task with the given name + UnregisterTask(name string) + // Start starts the worker + // This function will not block. + // The worker will be stopped when the context is canceled + // The OnExit channel will be used to signal when the worker is stopped (when not nil) + // The OnNextInterval channel will be used to signal when the next interval is executed (when not nil) + Start(ctx context.Context, onExit OnExit, onNextInterval OnNextInterval, waitFor <-chan struct{}) error +} + +// WorkerImpl is the implementation of the Worker interface +type WorkerImpl struct { + crateClient client.Client + interval time.Duration + maxWorkers int + taskList sync.Map + taskListLen atomic.Int32 + scheme *runtime.Scheme + NewClient client.NewClientFunc +} + +// Options is used to configure the Worker +type Options struct { + // MaxWorkers is the maximum number of workers that can be executed concurrently + MaxWorkers *int + // Interval is the time between each execution of the tasks + Interval *time.Duration + // NewClient is the function to create a new client for the APIServer + NewClient client.NewClientFunc +} + +// SetDefaultsIfNotSet sets the default values for the options if they are not set +func (o *Options) SetDefaultsIfNotSet() { + if o.MaxWorkers == nil { + o.MaxWorkers = &maxWorkersDefault + } + + if o.Interval == nil { + o.Interval = &intervalDefault + } + + if o.NewClient == nil { + o.NewClient = client.New + } +} + +var ( + // maxWorkersDefault is the default value for the maximum number of workers + maxWorkersDefault = 1 + // intervalDefault is the default value for the interval between each execution of the tasks + intervalDefault = time.Second * 10 +) + +// NewWorker creates a new Worker instance +// crateClient is the client for the crate cluster +// options is used to configure the Worker. If nil, the default values will be used. +func NewWorker(crateClient client.Client, options *Options) (Worker, error) { + sc := runtime.NewScheme() + + if err := clientgoscheme.AddToScheme(sc); err != nil { + return nil, fmt.Errorf("error adding client-go scheme to runtime scheme: %w", err) + } + + if options == nil { + options = &Options{} + } + + options.SetDefaultsIfNotSet() + + res := &WorkerImpl{ + crateClient: crateClient, + maxWorkers: *options.MaxWorkers, + interval: *options.Interval, + taskList: sync.Map{}, + taskListLen: atomic.Int32{}, + scheme: sc, + NewClient: options.NewClient, + } + + res.taskListLen.Store(0) + return res, nil +} + +// RegisterTask implements Worker.RegisterTask +// Thread-safe +func (w *WorkerImpl) RegisterTask(name string, task Task) { + // check if the task is already stored + if _, ok := w.taskList.Load(name); ok { + return + } + + w.taskList.Store(name, task) + w.taskListLen.Add(1) +} + +// UnregisterTask implements Worker.UnregisterTask +// Thread-safe +func (w *WorkerImpl) UnregisterTask(name string) { + if _, ok := w.taskList.Load(name); ok { + w.taskList.Delete(name) + w.taskListLen.Add(-1) + } +} + +// Start implements Worker.Start +func (w *WorkerImpl) Start(ctx context.Context, onExit OnExit, onNextInterval OnNextInterval, waitFor <-chan struct{}) error { + log := logging.FromContextOrPanic(ctx) + pool := pond.NewPool(w.maxWorkers, pond.WithContext(ctx)) + group := pool.NewGroup() + + go func() { + if waitFor != nil { + // wait for the given channel to be closed + <-waitFor + } + + log.Info("Worker started") + + for { + if w.taskListLen.Load() > 0 { + // get all existing APIServers from the crate cluster + log.Debug("Listing APIServers") + apiServers, err := w.listAPIServers(ctx) + if err != nil { + log.Error(err, "error listing APIServers") + } else { + for i := range apiServers.Items { + as := &apiServers.Items[i] + log := log.WithValues("apiserver", fmt.Sprintf("%s/%s", as.Namespace, as.Name)) + // create the kubernetes client for the APIServer + apiServerClient, err := w.createAPIServerClient(ctx, as) + + if err != nil { + log.Error(err, "error creating APIServer client", cconst.KeyResource, as.Name) + continue + } + + // execute all tasks for the current APIServer + w.taskList.Range(func(key, value interface{}) bool { + task := value.(Task) + taskName := key.(string) + log := log.WithValues("task", taskName) + log.Debug("Executing task") + ctx := logging.NewContext(ctx, log) + group.SubmitErr(func() error { + if err = task(ctx, as, w.crateClient, apiServerClient); err != nil { + log.Error(err, "error executing task") + } + return nil + }) + + return true + }) + } + } + } + + // wait for next interval + select { + case <-time.After(w.interval): + case <-ctx.Done(): + log.Info("APIServer worker stopped") + if onExit != nil { + onExit <- true + } + return + } + + if onNextInterval != nil { + onNextInterval <- true + } + } + }() + + return nil +} + +// listAPIServers lists all APIServers in the crate cluster +func (w *WorkerImpl) listAPIServers(ctx context.Context) (*openmcpv1alpha1.APIServerList, error) { + apiServerList := &openmcpv1alpha1.APIServerList{} + if err := w.crateClient.List(ctx, apiServerList); err != nil { + return nil, fmt.Errorf("error listing APIServers: %w", err) + } + + return apiServerList, nil +} + +// createAPIServerClient creates a new client for the given APIServer +func (w *WorkerImpl) createAPIServerClient(_ context.Context, as *openmcpv1alpha1.APIServer) (client.Client, error) { + if as.Status.AdminAccess == nil { + return nil, fmt.Errorf("no admin access found in APIServer status") + } + + // create a new client for the APIServer kubeconfig string + apiServerConfig, err := clientcmd.RESTConfigFromKubeConfig([]byte(as.Status.AdminAccess.Kubeconfig)) + if err != nil { + return nil, fmt.Errorf("error creating REST config from kubeconfig: %w", err) + } + + return w.NewClient(apiServerConfig, client.Options{ + Scheme: w.scheme, + }) +} diff --git a/internal/utils/apiserver/worker_test.go b/internal/utils/apiserver/worker_test.go new file mode 100644 index 0000000..f561fe9 --- /dev/null +++ b/internal/utils/apiserver/worker_test.go @@ -0,0 +1,150 @@ +package apiserver_test + +import ( + "context" + "sync" + "time" + + "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/rest" + "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + "github.com/openmcp-project/mcp-operator/test/utils" +) + +var _ = Describe("APIServerWorker", func() { + var ( + worker apiserver.Worker + env *testing.ComplexEnvironment + ) + + const ( + timeout = time.Millisecond * 500 + ) + + BeforeEach(func() { + var err error + env = utils.DefaultTestSetupBuilder("testdata", "worker").WithFakeClient(utils.APIServerCluster, utils.Scheme).Build() + opts := apiserver.Options{ + MaxWorkers: ptr.To(2), + Interval: ptr.To(time.Millisecond * 10), + NewClient: func(config *rest.Config, options client.Options) (client.Client, error) { + return env.Client(utils.APIServerCluster), nil + }, + } + worker, err = apiserver.NewWorker(env.Client(utils.CrateCluster), &opts) + Expect(err).ToNot(HaveOccurred()) + }) + + It("should execute each task for each APIServer and stop execution", func() { + ctx, cancel := context.WithCancel(env.Ctx) + + var ( + task1 = sync.Map{} + task2 = sync.Map{} + onExit = make(chan bool) + ) + + worker.RegisterTask("task1", func(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client, apiServerClient client.Client) error { + err := apiServerClient.Get(ctx, client.ObjectKey{Name: "test", Namespace: "test"}, &v1.Secret{}) + Expect(err).ToNot(HaveOccurred()) + task1.Store(as.Name, true) + return nil + }) + + worker.RegisterTask("task2", func(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client, apiServerClient client.Client) error { + err := apiServerClient.Get(ctx, client.ObjectKey{Name: "test", Namespace: "test"}, &v1.Secret{}) + Expect(err).ToNot(HaveOccurred()) + task2.Store(as.Name, true) + return nil + }) + + err := worker.Start(ctx, onExit, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + Eventually(func() bool { + _, t1Ok1 := task1.Load("test1") + _, t1Ok2 := task1.Load("test2") + _, t2Ok1 := task2.Load("test1") + _, t2Ok2 := task2.Load("test2") + return t1Ok1 && t1Ok2 && t2Ok1 && t2Ok2 + }).WithTimeout(timeout).WithContext(ctx).Should(BeTrue()) + + cancel() + Eventually(onExit).WithTimeout(timeout).Should(Receive(ptr.To(true))) + }) + + It("should remove tasks", func() { + ctx, cancel := context.WithCancel(env.Ctx) + + var ( + task1 = sync.Map{} + task2 = sync.Map{} + onNextInterval = make(chan bool) + ) + + worker.RegisterTask("task1", func(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client, apiServerClient client.Client) error { + task1.Store(as.Name, true) + return nil + }) + + worker.RegisterTask("task2", func(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client, apiServerClient client.Client) error { + task2.Store(as.Name, true) + return nil + }) + + err := worker.Start(ctx, nil, onNextInterval, nil) + Expect(err).ToNot(HaveOccurred()) + + worker.UnregisterTask("task1") + Eventually(onNextInterval).WithTimeout(timeout).Should(Receive(ptr.To(true))) + task1 = sync.Map{} + Eventually(onNextInterval).WithTimeout(timeout).Should(Receive(ptr.To(true))) + _, ok := task1.Load("test1") + Expect(ok).To(BeFalse()) + + cancel() + }) + + It("should not replace tasks", func() { + ctx, cancel := context.WithCancel(env.Ctx) + + var ( + task1 = sync.Map{} + task2 = sync.Map{} + onNextInterval = make(chan bool) + ) + + worker.RegisterTask("task1", func(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client, apiServerClient client.Client) error { + task1.Store(as.Name, true) + return nil + }) + + worker.RegisterTask("task2", func(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client, apiServerClient client.Client) error { + task2.Store(as.Name, true) + return nil + }) + + err := worker.Start(ctx, nil, onNextInterval, nil) + Expect(err).ToNot(HaveOccurred()) + + worker.RegisterTask("task1", func(ctx context.Context, as *openmcpv1alpha1.APIServer, crateClient client.Client, apiServerClient client.Client) error { + return nil + }) + Eventually(onNextInterval).WithTimeout(timeout).Should(Receive(ptr.To(true))) + task1 = sync.Map{} + Eventually(onNextInterval).WithTimeout(timeout).Should(Receive(ptr.To(true))) + _, ok := task1.Load("test1") + Expect(ok).To(BeTrue()) + + cancel() + }) +}) diff --git a/internal/utils/components/components.go b/internal/utils/components/components.go new file mode 100644 index 0000000..a2b8635 --- /dev/null +++ b/internal/utils/components/components.go @@ -0,0 +1,256 @@ +package components + +import ( + "context" + "fmt" + "reflect" + "strconv" + "time" + + components "github.com/openmcp-project/mcp-operator/internal/components" + "github.com/openmcp-project/mcp-operator/internal/utils" + + ctrl "sigs.k8s.io/controller-runtime" + + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/openmcp-project/controller-utils/pkg/logging" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" +) + +const ( + // ErrorReasonInvalidManagedControlPlaneLabels means that the resource does not have the expected labels in the expected formats. + ErrorReasonInvalidManagedControlPlaneLabels = "InvalidManagedControlPlaneLabels" +) + +// GetComponents iterates over the list of known components and tries to fetch each corresponding resource. +// The component-specific resources are expected to have the same name and namespace as the ManagedControlPlane. +// The resulting map contains only entries for component resources that were found. A missing resource will not result in an error. +func GetComponents[T components.ManagedComponent](reg components.ComponentRegistry[T], ctx context.Context, c client.Client, name, namespace string) (map[openmcpv1alpha1.ComponentType]T, error) { + res := reg.GetKnownComponents() + for ct, ch := range res { + ch.Resource().SetName(name) + ch.Resource().SetNamespace(namespace) + if err := c.Get(ctx, client.ObjectKeyFromObject(ch.Resource()), ch.Resource()); err != nil { + if apierrors.IsNotFound(err) { + delete(res, ct) + continue + } + return nil, fmt.Errorf("error getting resource '%s/%s' for component type '%s': %w", namespace, name, string(ct), err) + } + } + return res, nil +} + +// GetComponent fetches a single component resource from the cluster. +func GetComponent[T components.ManagedComponent](reg components.ComponentRegistry[T], ctx context.Context, c client.Client, component openmcpv1alpha1.ComponentType, name, namespace string) (T, error) { + var zero T + ch := reg.GetComponent(component) + if reflect.DeepEqual(ch, zero) { + return zero, fmt.Errorf("component '%s' is not in the list of known components", string(component)) + } + ch.Resource().SetName(name) + ch.Resource().SetNamespace(namespace) + if err := c.Get(ctx, client.ObjectKeyFromObject(ch.Resource()), ch.Resource()); err != nil { + if apierrors.IsNotFound(err) { + return zero, nil + } + return zero, err + } + return ch, nil +} + +var ErrNoControlPlaneGenerationLabel = openmcperrors.WithReason(fmt.Errorf("object does not have a '%s' label", openmcpv1alpha1.ManagedControlPlaneGenerationLabel), ErrorReasonInvalidManagedControlPlaneLabels) + +type InvalidGenerationLabelValueError struct { + value string + culpritIsIC bool +} + +var _ openmcperrors.ReasonableError = &InvalidGenerationLabelValueError{} + +func NewInvalidGenerationLabelValueError(value string, culpritIsIC bool) *InvalidGenerationLabelValueError { + return &InvalidGenerationLabelValueError{ + value: value, + culpritIsIC: culpritIsIC, + } +} +func (e *InvalidGenerationLabelValueError) Error() string { + invalidLabel := openmcpv1alpha1.ManagedControlPlaneGenerationLabel + if e.culpritIsIC { + invalidLabel = openmcpv1alpha1.InternalConfigurationGenerationLabel + } + return fmt.Sprintf("value '%s' of label '%s' cannot be parsed into an int64", e.value, invalidLabel) +} +func (e *InvalidGenerationLabelValueError) Reason() string { + return ErrorReasonInvalidManagedControlPlaneLabels +} + +// GetCreatedFromGeneration reads the generation labels for the ManagedControlPlane and the InternalConfiguration from the object and returns their values as int64. +// If the object does not have an InternalConfiguration generation label, -1 is returned for its value. +// Returns an error if the ManagedControlPlane generation label does not exist or either label's value cannot be parsed into an int64. +func GetCreatedFromGeneration(obj client.Object) (int64, int64, openmcperrors.ReasonableError) { + labels := obj.GetLabels() + if labels == nil { + return -1, -1, ErrNoControlPlaneGenerationLabel + } + rawCP, ok := labels[openmcpv1alpha1.ManagedControlPlaneGenerationLabel] + if !ok { + return -1, -1, ErrNoControlPlaneGenerationLabel + } + valCP, err := strconv.ParseInt(rawCP, 10, 64) + if err != nil { + return -1, -1, NewInvalidGenerationLabelValueError(rawCP, false) + } + var valIC int64 = -1 + rawIC, ok := labels[openmcpv1alpha1.InternalConfigurationGenerationLabel] + if ok { + valIC, err = strconv.ParseInt(rawIC, 10, 64) + if err != nil { + return -1, -1, NewInvalidGenerationLabelValueError(rawIC, true) + } + } + return valCP, valIC, nil +} + +// SetCreatedFromGeneration sets the managedcontrolplane generation label to the generation of the given ManagedControlPlane. +// If the passed-in InternalConfiguration is not nil, its generation is also added as a label. +func SetCreatedFromGeneration(obj client.Object, cp client.Object, ic client.Object) { + labels := obj.GetLabels() + if labels == nil { + labels = map[string]string{} + } + if !utils.IsNil(cp) { + labels[openmcpv1alpha1.ManagedControlPlaneGenerationLabel] = fmt.Sprintf("%d", cp.GetGeneration()) + } + if !utils.IsNil(ic) { + labels[openmcpv1alpha1.InternalConfigurationGenerationLabel] = fmt.Sprintf("%d", ic.GetGeneration()) + } else { + delete(labels, openmcpv1alpha1.InternalConfigurationGenerationLabel) + } + obj.SetLabels(labels) +} + +// GenerateCreatedFromGenerationPatch returns a patch which sets the generation labels according to the given ManagedControlPlane. +// If cp is nil, the value of the corresponding generation value is defaulted to -1. This should never happen. +// if ic is nil, the generated patch will remove the corresponding generation label, if it exists. +func GenerateCreatedFromGenerationPatch(cp, ic client.Object, addReconcileAnnotation bool) client.Patch { + cpLabelValue := `"-1"` + if !utils.IsNil(cp) { + cpLabelValue = fmt.Sprintf(`"%d"`, cp.GetGeneration()) + } + icLabelValue := `null` + if !utils.IsNil(ic) { + icLabelValue = fmt.Sprintf(`"%d"`, ic.GetGeneration()) + } + reconcileAnnotationPatch := "" + if addReconcileAnnotation { + reconcileAnnotationPatch = fmt.Sprintf(`,"annotations":{"%s":"%s"}`, openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile) + } + return client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf(`{"metadata":{"labels":{"%s":%s,"%s":%s}%s}}`, openmcpv1alpha1.ManagedControlPlaneGenerationLabel, cpLabelValue, openmcpv1alpha1.InternalConfigurationGenerationLabel, icLabelValue, reconcileAnnotationPatch))) +} + +// UpdateStatus updates the status of the given component resource. +// The passed-in reconcile result is returned unmodified to be able to call this function in a reconcile return statement. +// The ready, reconcileError, reason, and msg parameters are used to update the common status of the component resource. +// If the status of oldComponent and component differs, the changes are applied too. +// Note: In order to only update the common status, call this with comp.DeepCopy() as oldComponent and comp as component. +// oldComponent and component MUST NOT point to the same object! +func UpdateStatus[T components.Component](ctx context.Context, c client.Client, rr ReconcileResult[T]) (ctrl.Result, openmcperrors.ReasonableError) { + if utils.IsNil(rr.OldComponent) { + rr.OldComponent = rr.Component.DeepCopyObject().(T) + } + + errs := openmcperrors.NewReasonableErrorList(rr.ReconcileError) + + cpGen, icGen, err := GetCreatedFromGeneration(rr.Component) + if err != nil { + errs.Append(err) + } + + aggErr := errs.Aggregate() + if aggErr != nil { + if rr.Message != "" { + rr.Message += "\n" + } + rr.Message += aggErr.Error() + if aggErr.Reason() != "" { + rr.Reason = aggErr.Reason() + } + } + + commonStatus := rr.Component.GetCommonStatus() + cu := ConditionUpdater(commonStatus.Conditions, true).UpdateCondition(rr.Component.Type().ReconciliationCondition(), openmcpv1alpha1.ComponentConditionStatusFromBool(aggErr == nil), rr.Reason, rr.Message) + for _, con := range rr.Conditions { + cu.UpdateConditionFromTemplate(con) + } + reqCons := rr.Component.GetRequiredConditions() + if reqCons != nil && reqCons.Has(rr.Component.Type().HealthyCondition()) && !cu.HasCondition(rr.Component.Type().HealthyCondition()) && rr.ReconcileError != nil { + // if an error occured during the reconciliation and the component is expected to expose a Healthy condition, which is missing, put a reconciliation error into the condition + cu.UpdateCondition(rr.Component.Type().HealthyCondition(), openmcpv1alpha1.ComponentConditionStatusFalse, cconst.ReasonReconciliationError, cconst.MessageReconciliationError) + } + for eCon := range rr.Component.GetRequiredConditions() { + if !cu.HasCondition(eCon) { + cu.UpdateCondition(eCon, openmcpv1alpha1.ComponentConditionStatusUnknown, cconst.ReasonMissingExpectedCondition, "The component did not expose this condition.") + } + } + commonStatus.Conditions = cu.Conditions() + commonStatus.ObservedGenerations = openmcpv1alpha1.ObservedGenerations{ + Resource: rr.Component.GetGeneration(), + ManagedControlPlane: cpGen, + InternalConfiguration: icGen, + } + rr.Component.SetCommonStatus(commonStatus) + + oldStatus := getStatus(rr.OldComponent) + status := getStatus(rr.Component) + if !reflect.DeepEqual(oldStatus, status) { + err := c.Status().Patch(ctx, rr.Component, client.MergeFrom(rr.OldComponent)) + if client.IgnoreNotFound(err) != nil { + errs2 := openmcperrors.NewReasonableErrorList(fmt.Errorf("error patching status: %w", err), aggErr) + return rr.Result, errs2.Aggregate() + } + } + return rr.Result, aggErr +} + +// The result of a reconciliation. +type ReconcileResult[T components.Component] struct { + // The old component, before it was modified. + // Basically, if OldComponent.Status differs from Component.Status, the status will be patched during UpdateStatus. + // May be nil, in this case only the common status is updated. + // Changes to anything except the status are ignored. + OldComponent T + // The current components. + // If nil, UpdateStatus becomes a no-op. + Component T + // The result of the reconciliation. + // Is simply passed through. + Result ctrl.Result + // The error that occurred during reconciliation, if any. + ReconcileError openmcperrors.ReasonableError + // The reason for the component's condition. + // If empty, it is taken from the error, if any. + Reason string + // The message for the component's condition. + // Potential error messages from the reconciliation error are appended. + Message string + // Conditions contains a list of conditions that should be updated on the component. + // Note that this must not contain the Reconciliation condition, as that one is constructed from this struct's other fields. + // Also note that names of conditions are globally unique, so take care to avoid conflicts with other components. + // Futhermore, all conditions on the component resource that are not included in this list anymore will be removed. + // The lastTransition timestamp of the condition will be overwritten with the current time while updating. + Conditions []openmcpv1alpha1.ComponentCondition +} + +// LogRequeue logs a message with the given logger at the given verbosity if the currently reconciled object is requeued for another reconciliation. +func (rr *ReconcileResult[T]) LogRequeue(log logging.Logger, verbosity logging.LogLevel) { + if rr.Result.Requeue || rr.Result.RequeueAfter > 0 { + log.Log(verbosity, "Object requeued for reconciliation", "requeueAfter", rr.Result.RequeueAfter.String(), "reconcileAt", time.Now().Add(rr.Result.RequeueAfter).Format(time.RFC3339)) + } +} diff --git a/internal/utils/components/components_test.go b/internal/utils/components/components_test.go new file mode 100644 index 0000000..7b5bbd8 --- /dev/null +++ b/internal/utils/components/components_test.go @@ -0,0 +1,367 @@ +package components_test + +import ( + "context" + "errors" + "fmt" + + "github.com/openmcp-project/mcp-operator/internal/components" + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + . "github.com/openmcp-project/mcp-operator/test/matchers" + + "github.com/openmcp-project/controller-utils/pkg/testing" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + openmcperrors "github.com/openmcp-project/mcp-operator/api/errors" + "github.com/openmcp-project/mcp-operator/test/utils" +) + +type TestManagedComponent struct { + Component components.Component +} + +func (tmc TestManagedComponent) Resource() components.Component { + return tmc.Component +} + +// TestComponentRegistry is a simple testing implementation of the ComponentRegistry interface. +type TestComponentRegistry struct { + Components map[openmcpv1alpha1.ComponentType]TestManagedComponent +} + +func (tcr *TestComponentRegistry) GetKnownComponents() map[openmcpv1alpha1.ComponentType]TestManagedComponent { + return tcr.Components +} + +func (tcr *TestComponentRegistry) GetComponent(ct openmcpv1alpha1.ComponentType) TestManagedComponent { + return tcr.Components[ct] +} + +func (tcr *TestComponentRegistry) Register(ct openmcpv1alpha1.ComponentType, f func() TestManagedComponent) { + if f == nil { + delete(tcr.Components, ct) + return + } + tcr.Components[ct] = f() +} + +func (tcr *TestComponentRegistry) Has(ct openmcpv1alpha1.ComponentType) bool { + _, ok := tcr.Components[ct] + return ok +} + +var _ = Describe("Components", func() { + Context("GetComponents", func() { + It("should get an existing component", func() { + obj := &openmcpv1alpha1.APIServer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + component := components.Component(obj) + + registry := &TestComponentRegistry{ + Components: make(map[openmcpv1alpha1.ComponentType]TestManagedComponent), + } + registry.Register("apiserver", func() TestManagedComponent { + return TestManagedComponent{ + Component: component, + } + }) + + env := testing.NewEnvironmentBuilder().WithFakeClient(utils.Scheme).WithInitObjects(obj).Build() + + receivedComponent, err := componentutils.GetComponent[TestManagedComponent](registry, context.Background(), env.Client(), "apiserver", "test", "test") + Expect(err).ToNot(HaveOccurred()) + Expect(receivedComponent.Resource()).To(Equal(component)) + }) + + It("should return a nil component if it does not exist", func() { + obj := &openmcpv1alpha1.APIServer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + component := components.Component(obj) + + registry := &TestComponentRegistry{ + Components: make(map[openmcpv1alpha1.ComponentType]TestManagedComponent), + } + registry.Register("apiserver", func() TestManagedComponent { + return TestManagedComponent{ + Component: component, + } + }) + + env := testing.NewEnvironmentBuilder().WithFakeClient(utils.Scheme).Build() + + receivedComponent, err := componentutils.GetComponent[TestManagedComponent](registry, context.Background(), env.Client(), "apiserver", "test", "test") + Expect(err).ToNot(HaveOccurred()) + Expect(receivedComponent.Component).To(BeNil()) + }) + }) + + Context("GetComponents", func() { + It("should get all existing components", func() { + objAPIServer := &openmcpv1alpha1.APIServer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + componentAPIServer := components.Component(objAPIServer) + + objAuth := &openmcpv1alpha1.Authentication{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + componentAuth := components.Component(objAuth) + + objAuthz := &openmcpv1alpha1.Authorization{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + }, + } + componentAuthz := components.Component(objAuthz) + + registry := &TestComponentRegistry{ + Components: make(map[openmcpv1alpha1.ComponentType]TestManagedComponent), + } + registry.Register("apiserver", func() TestManagedComponent { + return TestManagedComponent{ + Component: componentAPIServer, + } + }) + registry.Register("authentication", func() TestManagedComponent { + return TestManagedComponent{ + Component: componentAuth, + } + }) + registry.Register("authorization", func() TestManagedComponent { + return TestManagedComponent{ + Component: componentAuthz, + } + }) + + env := testing.NewEnvironmentBuilder().WithFakeClient(utils.Scheme).WithInitObjects([]client.Object{ + objAPIServer, + objAuth, + }...).Build() + + components, err := componentutils.GetComponents[TestManagedComponent](registry, context.Background(), env.Client(), "test", "test") + Expect(err).ToNot(HaveOccurred()) + Expect(components).To(HaveLen(2)) + Expect(components).To(HaveKey(openmcpv1alpha1.ComponentType("apiserver"))) + Expect(components).To(HaveKey(openmcpv1alpha1.ComponentType("authentication"))) + }) + }) + + Context("InvalidGenerationLabelValueError", func() { + It("should return a correct error message", func() { + err := componentutils.NewInvalidGenerationLabelValueError("test", true) + Expect(err.Error()).To(Equal(fmt.Sprintf("value 'test' of label '%s' cannot be parsed into an int64", openmcpv1alpha1.InternalConfigurationGenerationLabel))) + + err = componentutils.NewInvalidGenerationLabelValueError("test", false) + Expect(err.Error()).To(Equal(fmt.Sprintf("value 'test' of label '%s' cannot be parsed into an int64", openmcpv1alpha1.ManagedControlPlaneGenerationLabel))) + }) + + It("should return a correct error reason", func() { + err := componentutils.NewInvalidGenerationLabelValueError("test", true) + Expect(err.Reason()).To(Equal(componentutils.ErrorReasonInvalidManagedControlPlaneLabels)) + + err = componentutils.NewInvalidGenerationLabelValueError("test", false) + Expect(err.Reason()).To(Equal(componentutils.ErrorReasonInvalidManagedControlPlaneLabels)) + }) + }) + + Context("GetCreatedFromGeneration", func() { + It("should return the correct values", func() { + obj := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + openmcpv1alpha1.ManagedControlPlaneGenerationLabel: "1", + openmcpv1alpha1.InternalConfigurationGenerationLabel: "2", + }, + }, + } + + valCP, valIC, err := componentutils.GetCreatedFromGeneration(obj) + Expect(err).ToNot(HaveOccurred()) + Expect(valCP).To(Equal(int64(1))) + Expect(valIC).To(Equal(int64(2))) + }) + }) + + Context("SetCreatedFromGeneration", func() { + It("should set the correct labels", func() { + obj := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{}, + }, + } + + cp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + } + + ic := &openmcpv1alpha1.InternalConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 2, + }, + } + + componentutils.SetCreatedFromGeneration(obj, cp, ic) + labels := obj.GetLabels() + Expect(labels).To(HaveKeyWithValue(openmcpv1alpha1.ManagedControlPlaneGenerationLabel, "1")) + Expect(labels).To(HaveKeyWithValue(openmcpv1alpha1.InternalConfigurationGenerationLabel, "2")) + }) + }) + + Context("GenerateCreatedFromGenerationPatch", func() { + It("returns patch with correct generation labels when both cp and ic are not nil", func() { + cp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + } + ic := &openmcpv1alpha1.InternalConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 2, + }, + } + patch := componentutils.GenerateCreatedFromGenerationPatch(cp, ic, false) + Expect(patch.Type()).To(Equal(types.MergePatchType)) + + cpPatch, err := patch.Data(cp) + Expect(err).ToNot(HaveOccurred()) + icPatch, err := patch.Data(ic) + Expect(err).ToNot(HaveOccurred()) + + Expect(string(cpPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":"1"`, openmcpv1alpha1.ManagedControlPlaneGenerationLabel))) + Expect(string(cpPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":"2"`, openmcpv1alpha1.InternalConfigurationGenerationLabel))) + Expect(string(icPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":"1"`, openmcpv1alpha1.ManagedControlPlaneGenerationLabel))) + Expect(string(icPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":"2"`, openmcpv1alpha1.InternalConfigurationGenerationLabel))) + }) + + It("returns patch with correct generation labels when ic is nil", func() { + cp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + } + + patch := componentutils.GenerateCreatedFromGenerationPatch(cp, nil, false) + Expect(patch.Type()).To(Equal(types.MergePatchType)) + + cpPatch, err := patch.Data(cp) + Expect(err).ToNot(HaveOccurred()) + + Expect(string(cpPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":"1"`, openmcpv1alpha1.ManagedControlPlaneGenerationLabel))) + Expect(string(cpPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":null`, openmcpv1alpha1.InternalConfigurationGenerationLabel))) + }) + + It("returns patch with correct generation labels and reconcile annotation when addReconcileAnnotation is true", func() { + cp := &openmcpv1alpha1.ManagedControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Generation: 1, + }, + } + + patch := componentutils.GenerateCreatedFromGenerationPatch(cp, nil, true) + Expect(patch.Type()).To(Equal(types.MergePatchType)) + + cpPatch, err := patch.Data(cp) + Expect(err).ToNot(HaveOccurred()) + + Expect(string(cpPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":"1"`, openmcpv1alpha1.ManagedControlPlaneGenerationLabel))) + Expect(string(cpPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":null`, openmcpv1alpha1.InternalConfigurationGenerationLabel))) + Expect(string(cpPatch)).To(ContainSubstring(fmt.Sprintf(`"%s":"%s"`, openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile))) + }) + }) + + Context("UpdateStatus", func() { + It("should update the status of the object", func() { + as := &openmcpv1alpha1.APIServer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test", + Labels: map[string]string{ + openmcpv1alpha1.ManagedControlPlaneGenerationLabel: "1", + }, + }, + Status: openmcpv1alpha1.APIServerStatus{ + CommonComponentStatus: openmcpv1alpha1.CommonComponentStatus{ + Conditions: openmcpv1alpha1.ComponentConditionList{ + { + Type: openmcpv1alpha1.APIServerComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + Reason: "All Good", + }, + }, + ObservedGenerations: openmcpv1alpha1.ObservedGenerations{ + Resource: 0, + ManagedControlPlane: 1, + InternalConfiguration: -1, + }, + }, + }, + } + + env := testing.NewEnvironmentBuilder().WithFakeClient(utils.Scheme).WithInitObjects(as).Build() + + rr := componentutils.ReconcileResult[*openmcpv1alpha1.APIServer]{ + Component: as, + Message: "Internal Error", + Reason: "InternalError", + ReconcileError: openmcperrors.NewReasonableErrorList(fmt.Errorf("quota exceeded")).Aggregate(), + Conditions: []openmcpv1alpha1.ComponentCondition{ + componentutils.NewCondition("additionalCondition", openmcpv1alpha1.ComponentConditionStatusTrue, "DummyReason", "This is a dummy message."), + }, + } + + var err error + result, err := componentutils.UpdateStatus(context.Background(), env.Client(), rr) + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, rr.ReconcileError)).To(BeTrue()) + Expect(result.Requeue).To(BeFalse()) + + err = env.Client().Get(context.Background(), client.ObjectKeyFromObject(as), as) + Expect(err).ToNot(HaveOccurred()) + Expect(as.Status.Conditions).To(ConsistOf( + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.HealthyCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: cconst.ReasonReconciliationError, + Message: cconst.MessageReconciliationError, + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: openmcpv1alpha1.APIServerComponent.ReconciliationCondition(), + Status: openmcpv1alpha1.ComponentConditionStatusFalse, + Reason: "InternalError", + Message: "Internal Error\nquota exceeded", + }), + MatchComponentCondition(openmcpv1alpha1.ComponentCondition{ + Type: "additionalCondition", + Status: openmcpv1alpha1.ComponentConditionStatusTrue, + Reason: "DummyReason", + Message: "This is a dummy message.", + }), + )) + }) + }) +}) diff --git a/internal/utils/components/conditions.go b/internal/utils/components/conditions.go new file mode 100644 index 0000000..39196c6 --- /dev/null +++ b/internal/utils/components/conditions.go @@ -0,0 +1,179 @@ +package components + +import ( + "slices" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/openmcp-project/mcp-operator/internal/components" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// ComponentConditionListUpdater is a helper struct for updating a component's conditions. +// Use the ConditionUpdater constructor for initializing. +type ComponentConditionListUpdater struct { + Now metav1.Time + conditions map[string]openmcpv1alpha1.ComponentCondition + updated sets.Set[string] +} + +// ConditionUpdater creates a builder-like helper struct for updating a list of ComponentConditions. +// The 'conditions' argument contains the old condition list. +// If removeUntouched is true, the condition list returned with Conditions() will have all conditions removed that have not been updated. +// If false, all conditions will be kept. +// Note that calling this function stores the current time as timestamp that is used as LastTransitionTime if a condition's status changed. +// To overwrite this timestamp, modify the 'Now' field of the returned struct manually. +// +// The given condition list is not modified. +// +// Usage example: +// status.conditions = ConditionUpdater(status.conditions, true).UpdateCondition(...).UpdateCondition(...).Conditions() +func ConditionUpdater(conditions openmcpv1alpha1.ComponentConditionList, removeUntouched bool) *ComponentConditionListUpdater { + res := &ComponentConditionListUpdater{ + Now: metav1.Now(), + conditions: make(map[string]openmcpv1alpha1.ComponentCondition, len(conditions)), + } + for _, con := range conditions { + res.conditions[con.Type] = con + } + if removeUntouched { + res.updated = sets.New[string]() + } + return res +} + +// UpdateCondition updates or creates the condition with the specified type. +// All fields of the condition are updated with the values given in the arguments, but the condition's LastTransitionTime is only updated (with the timestamp contained in the receiver struct) if the status changed. +// Returns the receiver for easy chaining. +func (c *ComponentConditionListUpdater) UpdateCondition(conType string, status openmcpv1alpha1.ComponentConditionStatus, reason, message string) *ComponentConditionListUpdater { + con := openmcpv1alpha1.ComponentCondition{ + Type: conType, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: c.Now, + } + old, ok := c.conditions[conType] + if ok && old.Status == con.Status { + // update LastTransitionTime only if status changed + con.LastTransitionTime = old.LastTransitionTime + } + c.conditions[conType] = con + if c.updated != nil { + c.updated.Insert(conType) + } + return c +} + +// UpdateConditionFromTemplate is a convenience wrapper around UpdateCondition which allows it to be called with a preconstructed ComponentCondition. +func (c *ComponentConditionListUpdater) UpdateConditionFromTemplate(con openmcpv1alpha1.ComponentCondition) *ComponentConditionListUpdater { + return c.UpdateCondition(con.Type, con.Status, con.Reason, con.Message) +} + +// HasCondition returns true if a condition with the given type exists in the updated condition list. +func (c *ComponentConditionListUpdater) HasCondition(conType string) bool { + _, ok := c.conditions[conType] + return ok && (c.updated == nil || c.updated.Has(conType)) +} + +// Conditions returns the updated condition list. +// If the condition updater was initialized with removeUntouched=true, this list will only contain the conditions which have been updated +// in between the condition updater creation and this method call. Otherwise, it will potentially also contain old conditions. +// The conditions are returned sorted by their type. +func (c *ComponentConditionListUpdater) Conditions() openmcpv1alpha1.ComponentConditionList { + res := openmcpv1alpha1.ComponentConditionList{} + for _, con := range c.conditions { + if c.updated == nil || c.updated.Has(con.Type) { + res = append(res, con) + } + } + slices.SortStableFunc(res, func(a, b openmcpv1alpha1.ComponentCondition) int { + return strings.Compare(a.Type, b.Type) + }) + return res +} + +// GetCondition returns a pointer to the condition for the given type, if it exists. +// Otherwise, nil is returned. +func GetCondition(ccl openmcpv1alpha1.ComponentConditionList, t string) *openmcpv1alpha1.ComponentCondition { + for i := range ccl { + if ccl[i].Type == t { + return &ccl[i] + } + } + return nil +} + +// NewCondition creates a new ComponentCondition with the given values and the current time as LastTransitionTime. +func NewCondition(conType string, status openmcpv1alpha1.ComponentConditionStatus, reason, message string) openmcpv1alpha1.ComponentCondition { + return openmcpv1alpha1.ComponentCondition{ + Type: conType, + Status: status, + Reason: reason, + Message: message, + LastTransitionTime: metav1.Now(), + } +} + +// IsComponentReady returns true if the component's observedGenerations are up-to-date and all of its relevant conditions are "True". +// If relevantConditions is empty, all of the component's conditions are deemed relevant. +// Condition types in relevantConditions for which no condition exists on the component are considered "Unknown" and cause the method to return false. +func IsComponentReady(comp components.Component, relevantConditions ...string) bool { + if comp == nil { + return false + } + cpGen, icGen, err := GetCreatedFromGeneration(comp) + if err != nil { + return false + } + cs := comp.GetCommonStatus() + cons := []openmcpv1alpha1.ComponentCondition{} + if len(relevantConditions) == 0 { + cons = cs.Conditions + } else { + for _, rc := range relevantConditions { + found := false + for _, con := range cs.Conditions { + if con.Type == rc { + cons = append(cons, con) + found = true + break + } + } + if !found { + return false + } + } + } + return IsComponentReadyRaw(cpGen, icGen, comp.GetGeneration(), cs.ObservedGenerations, cons...) +} + +// IsComponentReadyRaw returns true if the component's observed generations are up-to-date and all of the given conditions are "True". +// The first three arguments contain the generations of the ManagedControlPlane, InternalConfiguration, and component resource, respectively. +// They are compared to the observedGenerations in the given ObservedGenerations struct (which usually comes from the component resource's status). +// The generation of the InternalConfiguration is expected to be -1, if no InternalConfiguration exists. +// The generation of the component resource can be set to -1 if it is not known (it will then be ignored). +func IsComponentReadyRaw(cpGen, icGen, rGen int64, obsGen openmcpv1alpha1.ObservedGenerations, conditions ...openmcpv1alpha1.ComponentCondition) bool { + if !(obsGen.ManagedControlPlane == cpGen && obsGen.InternalConfiguration == icGen && (rGen < 0 || obsGen.Resource == rGen)) { + return false + } + for _, con := range conditions { + if con.Status != openmcpv1alpha1.ComponentConditionStatusTrue { + return false + } + } + return true +} + +// AllConditionsTrue returns true if all given conditions are "True". +func AllConditionsTrue(conditions ...openmcpv1alpha1.ComponentCondition) bool { + for _, con := range conditions { + if con.Status != openmcpv1alpha1.ComponentConditionStatusTrue { + return false + } + } + return true +} diff --git a/internal/utils/components/conditions_test.go b/internal/utils/components/conditions_test.go new file mode 100644 index 0000000..4eb93d0 --- /dev/null +++ b/internal/utils/components/conditions_test.go @@ -0,0 +1,135 @@ +package components_test + +import ( + "slices" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + "sigs.k8s.io/controller-runtime/pkg/client" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +var _ = Describe("Conditions", func() { + + Context("GetCondition", func() { + + It("should return the requested condition", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "test", Namespace: "test"}, ls)).To(Succeed()) + con := componentutils.GetCondition(ls.Status.Conditions, "true") + Expect(con).ToNot(BeNil()) + Expect(con.Type).To(Equal("true")) + }) + + It("should return nil if the condition does not exist", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "test", Namespace: "test"}, ls)).To(Succeed()) + con := componentutils.GetCondition(ls.Status.Conditions, "doesNotExist") + Expect(con).To(BeNil()) + }) + + It("should return a pointer to the condition which allows changes to the original object", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "test", Namespace: "test"}, ls)).To(Succeed()) + con := componentutils.GetCondition(ls.Status.Conditions, ls.Status.Conditions[0].Type) // fetch first condition from list + Expect(con).ToNot(BeNil()) + con.Status = "SomethingElse" + Expect(ls.Status.Conditions[0].Status).To(Equal(con.Status)) + }) + + }) + + Context("ConditionUpdater", func() { + + It("should update the condition (same value, keep other cons)", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "test", Namespace: "test"}, ls)).To(Succeed()) + con := componentutils.GetCondition(ls.Status.Conditions, "true") + Expect(con).ToNot(BeNil()) + updated := componentutils.ConditionUpdater(ls.Status.Conditions, false).UpdateCondition(con.Type, con.Status, "", "").Conditions() + Expect(len(updated)).To(Equal(len(ls.Status.Conditions))) + ucon := componentutils.GetCondition(updated, con.Type) + Expect(ucon).ToNot(BeNil()) + Expect(ucon.Status).To(Equal(con.Status)) + Expect(ucon.LastTransitionTime).To(Equal(con.LastTransitionTime)) + Expect(ucon.Reason).To(Equal("")) + Expect(ucon.Message).To(Equal("")) + }) + + It("should update the condition (different value, keep other cons)", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "test", Namespace: "test"}, ls)).To(Succeed()) + con := componentutils.GetCondition(ls.Status.Conditions, "true") + Expect(con).ToNot(BeNil()) + updated := componentutils.ConditionUpdater(ls.Status.Conditions, false).UpdateCondition(con.Type, "SomethingElse", "asdf", "foobar").Conditions() + Expect(len(updated)).To(Equal(len(ls.Status.Conditions))) + ucon := componentutils.GetCondition(updated, con.Type) + Expect(ucon).ToNot(BeNil()) + Expect(ucon.Status).To(BeEquivalentTo("SomethingElse")) + Expect(ucon.LastTransitionTime).ToNot(Equal(con.LastTransitionTime)) + Expect(ucon.Reason).To(Equal("asdf")) + Expect(ucon.Message).To(Equal("foobar")) + }) + + It("should update the condition (same value, discard other cons)", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "test", Namespace: "test"}, ls)).To(Succeed()) + con := componentutils.GetCondition(ls.Status.Conditions, "true") + Expect(con).ToNot(BeNil()) + updated := componentutils.ConditionUpdater(ls.Status.Conditions, true).UpdateCondition(con.Type, con.Status, "", "").Conditions() + Expect(len(updated)).To(Equal(1)) + ucon := componentutils.GetCondition(updated, con.Type) + Expect(ucon).ToNot(BeNil()) + Expect(ucon.Status).To(Equal(con.Status)) + Expect(ucon.LastTransitionTime).To(Equal(con.LastTransitionTime)) + Expect(ucon.Reason).To(Equal("")) + Expect(ucon.Message).To(Equal("")) + }) + + It("should update the condition (different value, discard other cons)", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "test", Namespace: "test"}, ls)).To(Succeed()) + con := componentutils.GetCondition(ls.Status.Conditions, "true") + Expect(con).ToNot(BeNil()) + updated := componentutils.ConditionUpdater(ls.Status.Conditions, true).UpdateCondition(con.Type, "SomethingElse", "asdf", "foobar").Conditions() + Expect(len(updated)).To(Equal(1)) + ucon := componentutils.GetCondition(updated, con.Type) + Expect(ucon).ToNot(BeNil()) + Expect(ucon.Status).To(BeEquivalentTo("SomethingElse")) + Expect(ucon.LastTransitionTime).ToNot(Equal(con.LastTransitionTime)) + Expect(ucon.Reason).To(Equal("asdf")) + Expect(ucon.Message).To(Equal("foobar")) + }) + + It("should sort the conditions by type", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-04").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "test", Namespace: "test"}, ls)).To(Succeed()) + oldConditions := ls.Status.Conditions.DeepCopy() + compareConditions := func(a, b openmcpv1alpha1.ComponentCondition) int { + return strings.Compare(a.Type, b.Type) + } + Expect(slices.IsSortedFunc(oldConditions, compareConditions)).To(BeFalse(), "conditions in the test object are already sorted, unable to test sorting") + updated := componentutils.ConditionUpdater(ls.Status.Conditions, false).Conditions() + Expect(len(updated)).To(BeNumerically(">", 1), "test object does not contain enough conditions to test sorting") + Expect(len(updated)).To(Equal(len(ls.Status.Conditions))) + Expect(slices.IsSortedFunc(updated, compareConditions)).To(BeTrue(), "conditions are not sorted") + }) + + }) + +}) diff --git a/internal/utils/components/dependencies.go b/internal/utils/components/dependencies.go new file mode 100644 index 0000000..1e22cf4 --- /dev/null +++ b/internal/utils/components/dependencies.go @@ -0,0 +1,153 @@ +package components + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "sync" + + "github.com/openmcp-project/mcp-operator/internal/components" + + "github.com/openmcp-project/controller-utils/pkg/logging" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + + cconst "github.com/openmcp-project/mcp-operator/api/constants" + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var ( + mutex *sync.Mutex +) + +func init() { + mutex = &sync.Mutex{} +} + +// GetDependents returns all dependency finalizers present on the given component, with the dependency finalizer prefix removed. +func GetDependents(requirement components.Component) sets.Set[string] { + res := sets.New[string]() + for _, fin := range requirement.GetFinalizers() { + if strings.HasPrefix(fin, openmcpv1alpha1.DependencyFinalizerPrefix) { + res.Insert(strings.TrimPrefix(fin, openmcpv1alpha1.DependencyFinalizerPrefix)) + } + } + return res +} + +// HasAnyDependencyFinalizer returns true if the given resource has a dependency finalizer from any other component. +func HasAnyDependencyFinalizer(obj client.Object) bool { + if obj == nil { + return false + } + finalizers := obj.GetFinalizers() + if len(finalizers) == 0 { + return false + } + for _, fin := range finalizers { + if strings.HasPrefix(fin, openmcpv1alpha1.DependencyFinalizerPrefix) { + return true + } + } + return false +} + +// HasDepedencyFinalizer returns true if the given resource has a dependency finalizer from the given component. +func HasDepedencyFinalizer(obj client.Object, ct openmcpv1alpha1.ComponentType) bool { + if obj == nil { + return false + } + finalizers := obj.GetFinalizers() + if len(finalizers) == 0 { + return false + } + cFin := ct.DependencyFinalizer() + for _, fin := range finalizers { + if fin == cFin { + return true + } + } + return false +} + +// EnsureDependencyFinalizer ensures that the dependency finalizer of component 'depComp' either exists or doesn't exist (based on argument 'expected') on the resource of component 'reqComp'. +func EnsureDependencyFinalizer(ctx context.Context, c client.Client, reqComp components.Component, depComp components.Component, expected bool) error { + // since this function is called from multiple controller goroutines, we need to lock the access to the resource + // otherwise, we might end up with an inconsistent list of finalizers + mutex.Lock() + defer mutex.Unlock() + + // get the latest version of the resource so that we only remove or add the requested dependency finalizer + if err := c.Get(ctx, client.ObjectKeyFromObject(reqComp), reqComp); err != nil { + return fmt.Errorf("error getting resource %s/%s: %w", reqComp.GetNamespace(), reqComp.GetName(), err) + } + + // log finalizers before changing them + log, err := logging.FromContext(ctx) + if err == nil { + logFinalizers(log, reqComp) + } + + exists := HasDepedencyFinalizer(reqComp, depComp.Type()) + if exists == expected { + // either finalizer exists and should be there or doesn't exist and should not + return nil + } + + fins := reqComp.GetFinalizers() + if fins == nil { + fins = []string{} + } + + cFin := depComp.Type().DependencyFinalizer() + if expected { + fins = append(fins, cFin) + } else { + for i := len(fins) - 1; i >= 0; i-- { + if fins[i] == cFin { + fins = append(fins[:i], fins[i+1:]...) + } + } + } + + finsj, err := json.Marshal(fins) + if err != nil { + return fmt.Errorf("error converting list of finalizers to JSON: %w", err) + } + if err := c.Patch(ctx, reqComp, client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf(`{"metadata":{"finalizers":%s}}`, finsj)))); err != nil { + return err + } + return nil +} + +// IsDependencyReady checks if a dependency is ready. +// The first argument is the resource (components.Component). +// The second argument is the generation of the ManagedControlPlane the current component was generated from. +// The third argument is the generation of the InternalConfiguration the current component was generated from, or -1 if no InternalConfiguration exists. +// Further arguments can be used to specify which conditions should be checked for readiness. If none are specified, all existing ones have to be "True". +// In addition to the IsComponentReady/IsComponentReadyRaw functions, this one only returns true if the depended on component was created from the same generation of the ManagedControlPlane as the current component. +func IsDependencyReady(dep components.Component, ownCPGeneration, ownICGeneration int64, relevantConditions ...string) bool { + if dep == nil { + return false + } + return IsComponentReady(dep, relevantConditions...) && + dep.GetCommonStatus().ObservedGenerations.ManagedControlPlane == ownCPGeneration && + dep.GetCommonStatus().ObservedGenerations.InternalConfiguration == ownICGeneration +} + +func logFinalizers(log logging.Logger, comp components.Component) { + if comp == nil { + return + } + finalizers := comp.GetFinalizers() + compName := client.ObjectKeyFromObject(comp).String() + compType := comp.GetObjectKind().GroupVersionKind().String() + if len(finalizers) == 0 { + log.Debug("Finalizers", cconst.KeyResource, compName, cconst.KeyReconciledResourceKind, compType, "finalizers", "none") + } else { + log.Debug("Finalizers", cconst.KeyResource, compName, cconst.KeyReconciledResourceKind, compType, "finalizers", strings.Join(finalizers, ",")) + } +} diff --git a/internal/utils/components/dependencies_test.go b/internal/utils/components/dependencies_test.go new file mode 100644 index 0000000..163501d --- /dev/null +++ b/internal/utils/components/dependencies_test.go @@ -0,0 +1,271 @@ +package components_test + +import ( + "sync" + "time" + + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "sigs.k8s.io/controller-runtime/pkg/client" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" + + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +var _ = Describe("Dependencies", func() { + + Context("GetDependents", func() { + + It("should return no dependencies if no finalizers at all are present", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-finalizers", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.GetDependents(ls)).To(BeEmpty()) + }) + + It("should return no dependencies if no dependency finalizers are present", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-dependencies", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.GetDependents(ls)).To(BeEmpty()) + }) + + It("should return all dependencies if dependency finalizers are present", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "with-dependencies", Namespace: "test"}, ls)).To(Succeed()) + deps := componentutils.GetDependents(ls) + Expect(deps.UnsortedList()).To(ConsistOf("authentication", "apiserver")) + }) + + }) + + Context("HasAnyDependencyFinalizer", func() { + + It("should determine existence of dependency finalizers correctly", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-finalizers", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.HasAnyDependencyFinalizer(ls)).To(BeFalse()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-dependencies", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.HasAnyDependencyFinalizer(ls)).To(BeFalse()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "with-dependencies", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.HasAnyDependencyFinalizer(ls)).To(BeTrue()) + }) + + }) + + Context("HasDepedencyFinalizer", func() { + + It("should correctly identify dependency finalizers", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-finalizers", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.AuthenticationComponent)).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.AuthorizationComponent)).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.APIServerComponent)).To(BeFalse()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-dependencies", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.AuthenticationComponent)).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.AuthorizationComponent)).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.APIServerComponent)).To(BeFalse()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "with-dependencies", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.AuthenticationComponent)).To(BeTrue()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.AuthorizationComponent)).To(BeFalse()) + Expect(componentutils.HasDepedencyFinalizer(ls, openmcpv1alpha1.APIServerComponent)).To(BeTrue()) + }) + + }) + + Context("EnsureDependencyFinalizer", func() { + + It("should add the finalizer if it does not exist and expected is true", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-finalizers", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authentication{}, true)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(ls.GetFinalizers()).To(ConsistOf(openmcpv1alpha1.AuthenticationComponent.DependencyFinalizer())) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-dependencies", Namespace: "test"}, ls)).To(Succeed()) + fins := ls.GetFinalizers() + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authentication{}, true)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(ls.GetFinalizers()).To(ConsistOf(append(fins, openmcpv1alpha1.AuthenticationComponent.DependencyFinalizer()))) + }) + + It("should not do anything if the finalizer already exists and expected is true", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "with-dependencies", Namespace: "test"}, ls)).To(Succeed()) + oldLs := ls.DeepCopy() + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authentication{}, true)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(ls).To(Equal(oldLs)) + }) + + It("should remove the finalizer if it exists and expected is false", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "with-dependencies", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authentication{}, false)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(ls.GetFinalizers()).NotTo(ContainElement(openmcpv1alpha1.AuthenticationComponent.DependencyFinalizer())) + }) + + It("should not do anything if the finalizer does not exist and expected is false", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-finalizers", Namespace: "test"}, ls)).To(Succeed()) + oldLs := ls.DeepCopy() + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authentication{}, false)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(ls).To(Equal(oldLs)) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-dependencies", Namespace: "test"}, ls)).To(Succeed()) + oldLs = ls.DeepCopy() + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authentication{}, false)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(ls).To(Equal(oldLs)) + }) + + It("should handle updating the finalizers correctly when updated from multiple controllers", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-02").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-dependencies", Namespace: "test"}, ls)).To(Succeed()) + + wg := sync.WaitGroup{} + wg.Add(4) + + go func() { + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authentication{}, true)).To(Succeed()) + wg.Done() + }() + + go func() { + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.APIServer{}, true)).To(Succeed()) + wg.Done() + }() + + go func() { + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authorization{}, true)).To(Succeed()) + wg.Done() + }() + + go func() { + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.CloudOrchestrator{}, true)).To(Succeed()) + wg.Done() + }() + + Eventually(func() bool { + wg.Wait() + return true + }).WithTimeout(100 * time.Millisecond).Should(BeTrue()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(ls.GetFinalizers()).To(ConsistOf( + "foo.bar.baz/foobar", + openmcpv1alpha1.APIServerComponent.DependencyFinalizer(), + openmcpv1alpha1.AuthenticationComponent.DependencyFinalizer(), + openmcpv1alpha1.AuthorizationComponent.DependencyFinalizer(), + openmcpv1alpha1.CloudOrchestratorComponent.DependencyFinalizer(), + )) + + wg = sync.WaitGroup{} + wg.Add(4) + + go func() { + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authentication{}, false)).To(Succeed()) + wg.Done() + }() + + go func() { + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.APIServer{}, false)).To(Succeed()) + wg.Done() + }() + + go func() { + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.Authorization{}, false)).To(Succeed()) + wg.Done() + }() + + go func() { + Expect(componentutils.EnsureDependencyFinalizer(env.Ctx, env.Client(testutils.CrateCluster), ls, &openmcpv1alpha1.CloudOrchestrator{}, false)).To(Succeed()) + wg.Done() + }() + + Eventually(func() bool { + wg.Wait() + return true + }).WithTimeout(100 * time.Millisecond).Should(BeTrue()) + + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ls), ls)).To(Succeed()) + Expect(ls.GetFinalizers()).To(ConsistOf("foo.bar.baz/foobar")) + }) + }) + + Context("IsDependencyReady", func() { + + var mcpGen int64 = 3 + var icGen int64 = -1 + + It("should return false if the dependency's condition is not True", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-03").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "notready-false", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen)).To(BeFalse()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "notready-unknown", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen)).To(BeFalse()) + }) + + It("should return false if the dependency's observed generations are outdated", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-03").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "notready-mcpgen", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen)).To(BeFalse()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "notready-icgen", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen)).To(BeFalse()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "notready-rgen", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen)).To(BeFalse()) + }) + + It("should return false if the dependency's generations differ from the own ones", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-03").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "ready", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen+1, icGen)).To(BeFalse()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "ready", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen+1)).To(BeFalse()) + }) + + It("should return false if a relevant condition does not exist", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-03").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "ready", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen, "doesNotExistCondition")).To(BeFalse()) + }) + + It("should return true if all of the relevant conditions are True", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-03").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "notready-false", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen, "isReady")).To(BeTrue()) + }) + + It("should return true if no conditions are specified and all existing ones are True", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-03").Build() + ls := &openmcpv1alpha1.Landscaper{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "ready", Namespace: "test"}, ls)).To(Succeed()) + Expect(componentutils.IsDependencyReady(ls, mcpGen, icGen)).To(BeTrue()) + }) + + }) + +}) diff --git a/internal/utils/components/patch.go b/internal/utils/components/patch.go new file mode 100644 index 0000000..5f3e33f --- /dev/null +++ b/internal/utils/components/patch.go @@ -0,0 +1,86 @@ +package components + +import ( + "context" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type AnnotationAlreadyExistsError struct { + Annotation string + DesiredValue string + ActualValue string +} + +func NewAnnotationAlreadyExistsError(ann, desired, actual string) *AnnotationAlreadyExistsError { + return &AnnotationAlreadyExistsError{ + Annotation: ann, + DesiredValue: desired, + ActualValue: actual, + } +} + +func (e *AnnotationAlreadyExistsError) Error() string { + return fmt.Sprintf("annotation '%s' already exists on the object and value '%s' could not be updated to '%s'", e.Annotation, e.ActualValue, e.DesiredValue) +} + +func IsAnnotationAlreadyExistsError(err error) bool { + _, ok := err.(*AnnotationAlreadyExistsError) + return ok +} + +// PatchAnnotation patches the given annotation into the given object. +// Returns a AnnotationAlreadyExistsError if the annotation exists with a different value on the object and mode ANNOTATION_OVERWRITE is not set. +// To remove an annotation, set mode to ANNOTATION_DELETE. The given annValue does not matter in this case. +// Note that mode is meant to be a single optional mode argument. The behavior if multiple modes are specified at the same time is undefined. +func PatchAnnotation(ctx context.Context, c client.Client, obj client.Object, annKey, annValue string, mode ...PatchAnnotationMode) error { + modeDelete := false + modeOverwrite := false + for _, m := range mode { + switch m { + case ANNOTATION_DELETE: + modeDelete = true + case ANNOTATION_OVERWRITE: + modeOverwrite = true + } + } + quote := "\"" + anns := obj.GetAnnotations() + if anns == nil { + anns = map[string]string{} + } + val, ok := anns[annKey] + if ok { + if !modeDelete { + if val == annValue { + // annotation already exists on the object, nothing to do + return nil + } + if !modeOverwrite { + return NewAnnotationAlreadyExistsError(annKey, annValue, val) + } + } else { + // delete annotation + annValue = "null" + quote = "" + } + } else { + if modeDelete { + // annotation does not exist, nothing to do + return nil + } + } + if err := c.Patch(ctx, obj, client.RawPatch(types.MergePatchType, []byte(fmt.Sprintf(`{"metadata":{"annotations":{"%s":%s%s%s}}}`, annKey, quote, annValue, quote)))); err != nil { + return err + } + return nil +} + +type PatchAnnotationMode string + +const ( + ANNOTATION_OVERWRITE PatchAnnotationMode = "overwrite" + ANNOTATION_DELETE PatchAnnotationMode = "delete" +) diff --git a/internal/utils/components/patch_test.go b/internal/utils/components/patch_test.go new file mode 100644 index 0000000..f24dfac --- /dev/null +++ b/internal/utils/components/patch_test.go @@ -0,0 +1,82 @@ +package components_test + +import ( + "fmt" + + componentutils "github.com/openmcp-project/mcp-operator/internal/utils/components" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + testutils "github.com/openmcp-project/mcp-operator/test/utils" +) + +var _ = Describe("Patch", func() { + + Context("IsAnnotationAlreadyExistsError", func() { + + It("should return true if the error is of type AnnotationAlreadyExistsError", func() { + var err error = componentutils.NewAnnotationAlreadyExistsError("test-annotation", "desired-value", "actual-value") + Expect(componentutils.IsAnnotationAlreadyExistsError(err)).To(BeTrue()) + }) + + It("should return false if the error is not of type AnnotationAlreadyExistsError", func() { + var err error = fmt.Errorf("test-error") + Expect(componentutils.IsAnnotationAlreadyExistsError(err)).To(BeFalse()) + }) + + }) + + Context("PatchAnnotation", func() { + + It("should patch the annotation on the object, if it does not exist", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").Build() + ns := &corev1.Namespace{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "no-annotation"}, ns)).To(Succeed()) + Expect(componentutils.PatchAnnotation(env.Ctx, env.Client(testutils.CrateCluster), ns, "foo.bar.baz/foo", "bar")).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ns), ns)).To(Succeed()) + Expect(ns.GetAnnotations()).To(HaveKeyWithValue("foo.bar.baz/foo", "bar")) + }) + + It("should not fail if the annotation already exists with the desired value", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").Build() + ns := &corev1.Namespace{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "foo-annotation"}, ns)).To(Succeed()) + oldNs := ns.DeepCopy() + Expect(componentutils.PatchAnnotation(env.Ctx, env.Client(testutils.CrateCluster), ns, "foo.bar.baz/foo", "bar")).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ns), ns)).To(Succeed()) + Expect(ns.GetAnnotations()).To(HaveKeyWithValue("foo.bar.baz/foo", "bar")) + Expect(ns).To(Equal(oldNs)) + }) + + It("should return an AnnotationAlreadyExistsError if the annotation already exists with a different value", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").Build() + ns := &corev1.Namespace{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "foo-annotation"}, ns)).To(Succeed()) + Expect(componentutils.PatchAnnotation(env.Ctx, env.Client(testutils.CrateCluster), ns, "foo.bar.baz/foo", "baz")).To(MatchError(componentutils.NewAnnotationAlreadyExistsError("foo.bar.baz/foo", "baz", "bar"))) + }) + + It("should overwrite the annotation if the mode is set to ANNOTATION_OVERWRITE", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").Build() + ns := &corev1.Namespace{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "foo-annotation"}, ns)).To(Succeed()) + Expect(componentutils.PatchAnnotation(env.Ctx, env.Client(testutils.CrateCluster), ns, "foo.bar.baz/foo", "baz", componentutils.ANNOTATION_OVERWRITE)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ns), ns)).To(Succeed()) + Expect(ns.GetAnnotations()).To(HaveKeyWithValue("foo.bar.baz/foo", "baz")) + }) + + It("should delete the annotation if the mode is set to ANNOTATION_DELETE", func() { + env := testutils.DefaultTestSetupBuilder("testdata", "test-01").Build() + ns := &corev1.Namespace{} + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKey{Name: "foo-annotation"}, ns)).To(Succeed()) + Expect(componentutils.PatchAnnotation(env.Ctx, env.Client(testutils.CrateCluster), ns, "foo.bar.baz/foo", "", componentutils.ANNOTATION_DELETE)).To(Succeed()) + Expect(env.Client(testutils.CrateCluster).Get(env.Ctx, client.ObjectKeyFromObject(ns), ns)).To(Succeed()) + Expect(ns.GetAnnotations()).NotTo(HaveKey("foo.bar.baz/foo")) + }) + + }) + +}) diff --git a/internal/utils/components/predicates.go b/internal/utils/components/predicates.go new file mode 100644 index 0000000..61163e7 --- /dev/null +++ b/internal/utils/components/predicates.go @@ -0,0 +1,82 @@ +package components + +// This package contains predicates which can be used for constructing controllers. + +import ( + "reflect" + + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + colactrlutil "github.com/openmcp-project/controller-utils/pkg/controller" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// DefaultComponentControllerPredicates returns a predicate combination which should be useful for most - if not all - component controllers. +func DefaultComponentControllerPredicates() predicate.Predicate { + return predicate.And( + predicate.Or( + predicate.GenerationChangedPredicate{}, + colactrlutil.GotAnnotationPredicate(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueReconcile), + colactrlutil.LostAnnotationPredicate(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueIgnore), + GenerationLabelsChangedPredicate{}, + ), + predicate.Not( + colactrlutil.HasAnnotationPredicate(openmcpv1alpha1.OperationAnnotation, openmcpv1alpha1.OperationAnnotationValueIgnore), + ), + ) +} + +// GenerationLabelsChangedPredicate reacts on changes to the cp/ir generation labels. +type GenerationLabelsChangedPredicate struct { + predicate.Funcs +} + +var _ predicate.Predicate = GenerationLabelsChangedPredicate{} + +func (GenerationLabelsChangedPredicate) Update(e event.UpdateEvent) bool { + if e.ObjectOld == nil { + return false + } + if e.ObjectNew == nil { + return false + } + + // old labels + // an error means that either the CP generation label did not exist or is not an int, we can ignore the former and can't do anything about the latter case here + oldCPGen, oldIRGen, _ := GetCreatedFromGeneration(e.ObjectOld) + newCPGen, newIRGen, _ := GetCreatedFromGeneration(e.ObjectNew) + return oldCPGen != newCPGen || oldIRGen != newIRGen +} + +var _ predicate.Predicate = StatusChangedPredicate{} + +// StatusChangedPredicate returns true if the object's status changed. +// Getting the status is done via reflection and only works if the corresponding field is named 'Status'. +// If getting the status fails, this predicate always returns true. +type StatusChangedPredicate struct { + predicate.Funcs +} + +func (p StatusChangedPredicate) Update(e event.UpdateEvent) bool { + oldStatus := getStatus(e.ObjectOld) + newStatus := getStatus(e.ObjectNew) + if oldStatus == nil || newStatus == nil { + return true + } + return !reflect.DeepEqual(oldStatus, newStatus) +} + +func getStatus(obj any) any { + if obj == nil { + return nil + } + val := reflect.ValueOf(obj).Elem() + for i := 0; i < val.NumField(); i++ { + if val.Type().Field(i).Name == "Status" { + return val.Field(i).Interface() + } + } + return nil +} diff --git a/internal/utils/components/predicates_test.go b/internal/utils/components/predicates_test.go new file mode 100644 index 0000000..bba6c65 --- /dev/null +++ b/internal/utils/components/predicates_test.go @@ -0,0 +1,75 @@ +package components_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/openmcp-project/mcp-operator/internal/utils/components" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("Predicates", func() { + + var base *openmcpv1alpha1.ManagedControlPlane + var changed *openmcpv1alpha1.ManagedControlPlane + + BeforeEach(func() { + base = &openmcpv1alpha1.ManagedControlPlane{} + base.SetName("foo") + base.SetNamespace("bar") + base.SetGeneration(1) + changed = base.DeepCopy() + }) + + It("should detect changes to the generation labels", func() { + p := components.GenerationLabelsChangedPredicate{} + components.SetCreatedFromGeneration(base, mcpWithGeneration(1), nil) + components.SetCreatedFromGeneration(changed, mcpWithGeneration(1), nil) + Expect(p.Update(updateEvent(base, changed))).To(BeFalse(), "GenerationLabelsChangedPredicate should return false if the generation labels did not change") + By("change mcp generation label") + components.SetCreatedFromGeneration(changed, mcpWithGeneration(2), nil) + Expect(p.Update(updateEvent(base, changed))).To(BeTrue(), "GenerationLabelsChangedPredicate should return true if the MCP generation label changed") + By("add ic generation label") + base = changed.DeepCopy() + components.SetCreatedFromGeneration(changed, mcpWithGeneration(2), mcpWithGeneration(2)) + Expect(p.Update(updateEvent(base, changed))).To(BeTrue(), "GenerationLabelsChangedPredicate should return true if the IC generation label was added") + By("change ic generation label") + base = changed.DeepCopy() + components.SetCreatedFromGeneration(changed, mcpWithGeneration(2), mcpWithGeneration(3)) + Expect(p.Update(updateEvent(base, changed))).To(BeTrue(), "GenerationLabelsChangedPredicate should return true if the IC generation label was changed") + By("remove ic generation label") + base = changed.DeepCopy() + components.SetCreatedFromGeneration(changed, mcpWithGeneration(2), nil) + Expect(p.Update(updateEvent(base, changed))).To(BeTrue(), "GenerationLabelsChangedPredicate should return true if the IC generation label was removed") + }) + + It("should detect changes to the status", func() { + p := components.StatusChangedPredicate{} + Expect(p.Update(updateEvent(base, changed))).To(BeFalse(), "StatusChangedPredicate should return false if the status did not change") + By("change status") + changed.Status = openmcpv1alpha1.ManagedControlPlaneStatus{ + ManagedControlPlaneMetaStatus: openmcpv1alpha1.ManagedControlPlaneMetaStatus{ + ObservedGeneration: 1, + }, + } + Expect(p.Update(updateEvent(base, changed))).To(BeTrue(), "StatusChangedPredicate should return true if the status changed") + }) + +}) + +func updateEvent(old, new client.Object) event.UpdateEvent { + return event.UpdateEvent{ + ObjectOld: old, + ObjectNew: new, + } +} + +func mcpWithGeneration(gen int64) *openmcpv1alpha1.ManagedControlPlane { + res := &openmcpv1alpha1.ManagedControlPlane{} + res.SetGeneration(gen) + return res +} diff --git a/internal/utils/components/suite_test.go b/internal/utils/components/suite_test.go new file mode 100644 index 0000000..5b390f9 --- /dev/null +++ b/internal/utils/components/suite_test.go @@ -0,0 +1,14 @@ +package components_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestComponentUtils(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Component Utils Suite") +} diff --git a/internal/utils/components/testdata/test-01/ns-foo-ann.yaml b/internal/utils/components/testdata/test-01/ns-foo-ann.yaml new file mode 100644 index 0000000..8fd8973 --- /dev/null +++ b/internal/utils/components/testdata/test-01/ns-foo-ann.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: foo-annotation + annotations: + foo.bar.baz/foo: "bar" diff --git a/internal/utils/components/testdata/test-01/ns-no-ann.yaml b/internal/utils/components/testdata/test-01/ns-no-ann.yaml new file mode 100644 index 0000000..53e0be6 --- /dev/null +++ b/internal/utils/components/testdata/test-01/ns-no-ann.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: no-annotation diff --git a/internal/utils/components/testdata/test-02/no-deps.yaml b/internal/utils/components/testdata/test-02/no-deps.yaml new file mode 100644 index 0000000..2b003ab --- /dev/null +++ b/internal/utils/components/testdata/test-02/no-deps.yaml @@ -0,0 +1,13 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + name: no-dependencies + namespace: test + finalizers: + - foo.bar.baz/foobar +spec: + deployers: + - helm + - manifest + - container diff --git a/internal/utils/components/testdata/test-02/no-fins.yaml b/internal/utils/components/testdata/test-02/no-fins.yaml new file mode 100644 index 0000000..a105e4d --- /dev/null +++ b/internal/utils/components/testdata/test-02/no-fins.yaml @@ -0,0 +1,11 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + name: no-finalizers + namespace: test +spec: + deployers: + - helm + - manifest + - container diff --git a/internal/utils/components/testdata/test-02/with-deps.yaml b/internal/utils/components/testdata/test-02/with-deps.yaml new file mode 100644 index 0000000..0d137ef --- /dev/null +++ b/internal/utils/components/testdata/test-02/with-deps.yaml @@ -0,0 +1,15 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + name: with-dependencies + namespace: test + finalizers: + - dependency.openmcp.cloud/apiserver + - dependency.openmcp.cloud/authentication + - foo.bar.baz/foobar +spec: + deployers: + - helm + - manifest + - container diff --git a/internal/utils/components/testdata/test-03/notready-false.yaml b/internal/utils/components/testdata/test-03/notready-false.yaml new file mode 100644 index 0000000..7b3582e --- /dev/null +++ b/internal/utils/components/testdata/test-03/notready-false.yaml @@ -0,0 +1,28 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "3" + name: notready-false + namespace: test +spec: + deployers: + - helm + - manifest + - container +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "False" + type: landscaperHealthy + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "True" + type: isReady + landscaperDeployment: + name: notready-false + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 3 + resource: 1 diff --git a/internal/utils/components/testdata/test-03/notready-icgen.yaml b/internal/utils/components/testdata/test-03/notready-icgen.yaml new file mode 100644 index 0000000..a754d3d --- /dev/null +++ b/internal/utils/components/testdata/test-03/notready-icgen.yaml @@ -0,0 +1,26 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "3" + openmcp.cloud/ic-generation: "1" + name: notready-icgen + namespace: test +spec: + deployers: + - helm + - manifest + - container +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "False" + type: landscaperHealthy + landscaperDeployment: + name: notready-icgen + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 3 + resource: 1 diff --git a/internal/utils/components/testdata/test-03/notready-mcpgen.yaml b/internal/utils/components/testdata/test-03/notready-mcpgen.yaml new file mode 100644 index 0000000..c4b51a5 --- /dev/null +++ b/internal/utils/components/testdata/test-03/notready-mcpgen.yaml @@ -0,0 +1,25 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "5" + name: notready-mcpgen + namespace: test +spec: + deployers: + - helm + - manifest + - container +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "False" + type: landscaperHealthy + landscaperDeployment: + name: notready-mcpgen + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 3 + resource: 1 diff --git a/internal/utils/components/testdata/test-03/notready-rgen.yaml b/internal/utils/components/testdata/test-03/notready-rgen.yaml new file mode 100644 index 0000000..7149dfc --- /dev/null +++ b/internal/utils/components/testdata/test-03/notready-rgen.yaml @@ -0,0 +1,25 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 2 + labels: + openmcp.cloud/mcp-generation: "3" + name: notready-rgen + namespace: test +spec: + deployers: + - helm + - manifest + - container +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "False" + type: landscaperHealthy + landscaperDeployment: + name: notready-rgen + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 3 + resource: 1 diff --git a/internal/utils/components/testdata/test-03/notready-unknown.yaml b/internal/utils/components/testdata/test-03/notready-unknown.yaml new file mode 100644 index 0000000..2e81930 --- /dev/null +++ b/internal/utils/components/testdata/test-03/notready-unknown.yaml @@ -0,0 +1,25 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "3" + name: notready-unknown + namespace: test +spec: + deployers: + - helm + - manifest + - container +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "Unknown" + type: landscaperHealthy + landscaperDeployment: + name: notready-unknown + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 3 + resource: 1 diff --git a/internal/utils/components/testdata/test-03/ready.yaml b/internal/utils/components/testdata/test-03/ready.yaml new file mode 100644 index 0000000..4204e6d --- /dev/null +++ b/internal/utils/components/testdata/test-03/ready.yaml @@ -0,0 +1,25 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "3" + name: ready + namespace: test +spec: + deployers: + - helm + - manifest + - container +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "True" + type: landscaperHealthy + landscaperDeployment: + name: ready + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 3 + resource: 1 diff --git a/internal/utils/components/testdata/test-04/ls.yaml b/internal/utils/components/testdata/test-04/ls.yaml new file mode 100644 index 0000000..0f94dd0 --- /dev/null +++ b/internal/utils/components/testdata/test-04/ls.yaml @@ -0,0 +1,31 @@ +apiVersion: core.openmcp.cloud/v1alpha1 +kind: Landscaper +metadata: + generation: 1 + labels: + openmcp.cloud/mcp-generation: "3" + name: test + namespace: test +spec: + deployers: + - helm + - manifest + - container +status: + conditions: + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "True" + type: "true" + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "False" + type: "false" + - lastTransitionTime: "2024-05-16T11:50:14Z" + status: "Unknown" + type: "unknown" + landscaperDeployment: + name: test + namespace: test + observedGenerations: + internalConfiguration: -1 + managedControlPlane: 3 + resource: 1 diff --git a/internal/utils/region/region.go b/internal/utils/region/region.go new file mode 100644 index 0000000..677620c --- /dev/null +++ b/internal/utils/region/region.go @@ -0,0 +1,602 @@ +package region + +import ( + "fmt" + "regexp" + "slices" + "strings" + + "github.com/openmcp-project/controller-utils/pkg/collections" + "k8s.io/apimachinery/pkg/util/sets" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// DirectionProximityMapping maps directions to lists of neighboring directions. +// The lists are considered to be ordered. +// By default, all directions (except CENTRAL) are considered to be neighbored to CENTRAL first and their respective two non-opposite directions second. +// CENTRAL is neighbored to all other directions. +var DirectionProximityMapping = map[openmcpv1alpha1.Direction][]openmcpv1alpha1.Direction{ + openmcpv1alpha1.CENTRAL: {openmcpv1alpha1.NORTH, openmcpv1alpha1.EAST, openmcpv1alpha1.SOUTH, openmcpv1alpha1.WEST}, + openmcpv1alpha1.NORTH: {openmcpv1alpha1.CENTRAL, openmcpv1alpha1.WEST, openmcpv1alpha1.EAST}, + openmcpv1alpha1.EAST: {openmcpv1alpha1.CENTRAL, openmcpv1alpha1.NORTH, openmcpv1alpha1.SOUTH}, + openmcpv1alpha1.SOUTH: {openmcpv1alpha1.CENTRAL, openmcpv1alpha1.EAST, openmcpv1alpha1.WEST}, + openmcpv1alpha1.WEST: {openmcpv1alpha1.CENTRAL, openmcpv1alpha1.SOUTH, openmcpv1alpha1.NORTH}, +} + +var DirectionOppositeMapping = map[openmcpv1alpha1.Direction]openmcpv1alpha1.Direction{ + openmcpv1alpha1.NORTH: openmcpv1alpha1.SOUTH, + openmcpv1alpha1.EAST: openmcpv1alpha1.WEST, + openmcpv1alpha1.SOUTH: openmcpv1alpha1.NORTH, + openmcpv1alpha1.WEST: openmcpv1alpha1.EAST, +} + +type Location struct { + Region openmcpv1alpha1.Region + Neighbor map[openmcpv1alpha1.Direction]*Location +} + +type Geography map[openmcpv1alpha1.Region]*Location + +var World Geography = Geography{ + openmcpv1alpha1.AFRICA: {Region: openmcpv1alpha1.AFRICA, Neighbor: map[openmcpv1alpha1.Direction]*Location{}}, + openmcpv1alpha1.ASIA: {Region: openmcpv1alpha1.ASIA, Neighbor: map[openmcpv1alpha1.Direction]*Location{}}, + openmcpv1alpha1.AUSTRALIA: {Region: openmcpv1alpha1.AUSTRALIA, Neighbor: map[openmcpv1alpha1.Direction]*Location{}}, + openmcpv1alpha1.EUROPE: {Region: openmcpv1alpha1.EUROPE, Neighbor: map[openmcpv1alpha1.Direction]*Location{}}, + openmcpv1alpha1.NORTHAMERICA: {Region: openmcpv1alpha1.NORTHAMERICA, Neighbor: map[openmcpv1alpha1.Direction]*Location{}}, + openmcpv1alpha1.SOUTHAMERICA: {Region: openmcpv1alpha1.SOUTHAMERICA, Neighbor: map[openmcpv1alpha1.Direction]*Location{}}, +} + +func init() { + // connect the Locations within the World + // Africa + World[openmcpv1alpha1.AFRICA].Neighbor[openmcpv1alpha1.NORTH] = World[openmcpv1alpha1.EUROPE] + World[openmcpv1alpha1.AFRICA].Neighbor[openmcpv1alpha1.EAST] = World[openmcpv1alpha1.AUSTRALIA] + World[openmcpv1alpha1.AFRICA].Neighbor[openmcpv1alpha1.WEST] = World[openmcpv1alpha1.SOUTHAMERICA] + + // Asia + World[openmcpv1alpha1.ASIA].Neighbor[openmcpv1alpha1.EAST] = World[openmcpv1alpha1.NORTHAMERICA] + World[openmcpv1alpha1.ASIA].Neighbor[openmcpv1alpha1.SOUTH] = World[openmcpv1alpha1.AUSTRALIA] + World[openmcpv1alpha1.ASIA].Neighbor[openmcpv1alpha1.WEST] = World[openmcpv1alpha1.EUROPE] + + // Australia + World[openmcpv1alpha1.AUSTRALIA].Neighbor[openmcpv1alpha1.NORTH] = World[openmcpv1alpha1.ASIA] + World[openmcpv1alpha1.AUSTRALIA].Neighbor[openmcpv1alpha1.EAST] = World[openmcpv1alpha1.SOUTHAMERICA] + World[openmcpv1alpha1.AUSTRALIA].Neighbor[openmcpv1alpha1.WEST] = World[openmcpv1alpha1.AFRICA] + + // Europe + World[openmcpv1alpha1.EUROPE].Neighbor[openmcpv1alpha1.EAST] = World[openmcpv1alpha1.ASIA] + World[openmcpv1alpha1.EUROPE].Neighbor[openmcpv1alpha1.SOUTH] = World[openmcpv1alpha1.AFRICA] + World[openmcpv1alpha1.EUROPE].Neighbor[openmcpv1alpha1.WEST] = World[openmcpv1alpha1.NORTHAMERICA] + + // Northamerica + World[openmcpv1alpha1.NORTHAMERICA].Neighbor[openmcpv1alpha1.EAST] = World[openmcpv1alpha1.EUROPE] + World[openmcpv1alpha1.NORTHAMERICA].Neighbor[openmcpv1alpha1.SOUTH] = World[openmcpv1alpha1.SOUTHAMERICA] + World[openmcpv1alpha1.NORTHAMERICA].Neighbor[openmcpv1alpha1.WEST] = World[openmcpv1alpha1.ASIA] + + // Southamerica + World[openmcpv1alpha1.SOUTHAMERICA].Neighbor[openmcpv1alpha1.NORTH] = World[openmcpv1alpha1.NORTHAMERICA] + World[openmcpv1alpha1.SOUTHAMERICA].Neighbor[openmcpv1alpha1.EAST] = World[openmcpv1alpha1.AFRICA] + World[openmcpv1alpha1.SOUTHAMERICA].Neighbor[openmcpv1alpha1.WEST] = World[openmcpv1alpha1.AUSTRALIA] +} + +func SortByProximity(start openmcpv1alpha1.RegionSpecification, preferSameRegion bool) [][]openmcpv1alpha1.RegionSpecification { + return World.SortByProximity(start, preferSameRegion) +} + +// SortByProximity basically sorts the elements of this Geography based on a breadth-first search starting from the given element. +// The returned list is two-dimensional, where the index of the outer elements corresponds to the relative distance of all corrensponding inner elements. +// If preferSameRegion is set to true, the result list is ordered in a way that other regions only appear after all combinations of the start region with all directions. +func (g Geography) SortByProximity(start openmcpv1alpha1.RegionSpecification, preferSameRegion bool) [][]openmcpv1alpha1.RegionSpecification { + dummy := openmcpv1alpha1.RegionSpecification{Name: openmcpv1alpha1.Region("DUMMY"), Direction: openmcpv1alpha1.Direction("DUMMY")} + var q collections.Queue[openmcpv1alpha1.RegionSpecification] = collections.NewLinkedList[openmcpv1alpha1.RegionSpecification](start, dummy) + visited := sets.New[openmcpv1alpha1.RegionSpecification]() + sameRegionPriorityMode := preferSameRegion + res := [][]openmcpv1alpha1.RegionSpecification{} + curRes := []openmcpv1alpha1.RegionSpecification{} + addDummy := false + for { + if q.Size() == 0 { + if sameRegionPriorityMode { + // all preferred region/direction combinations have been added, start breadth-first search again to include other regions + visited.Clear() + q.Add(start, dummy) + sameRegionPriorityMode = false + } else { + break + } + } + cur := q.Poll() + if cur == dummy { + if len(curRes) > 0 { + res = append(res, curRes) + curRes = []openmcpv1alpha1.RegionSpecification{} + } + if addDummy { + q.Add(dummy) + addDummy = false + } + continue + } + if cur.Direction == "" { + cur.Direction = openmcpv1alpha1.CENTRAL + } + if visited.Has(cur) { + // current node has already been seen + continue + } + addDummy = true + if !(preferSameRegion && !sameRegionPriorityMode && cur.Name == start.Name) { + curRes = append(curRes, cur) + } + visited.Insert(cur) + loc, ok := g[cur.Name] + if !ok { + // no location found in Geography for current region specification + continue + } + // add new elements to queue based on proximity to current location + // first, add neighboring directions from same region + dNeighbors, ok := DirectionProximityMapping[cur.Direction] + if ok { + for _, dn := range dNeighbors { + q.Add(openmcpv1alpha1.RegionSpecification{ + Name: cur.Name, + Direction: dn, + }) + } + } + // second, add neighboring region to queue + // use opposite direction if known + // skip this in sameRegionPriorityMode, as we are only interested in the starting region at that time + if !sameRegionPriorityMode { + rNeighbor, ok := loc.Neighbor[cur.Direction] + if ok { + q.Add(openmcpv1alpha1.RegionSpecification{ + Name: rNeighbor.Region, + Direction: DirectionOppositeMapping[cur.Direction], + }) + } + } + } + return res +} + +// GetClosestRegion tries to find the region(s) that are closest to the origin region. +// First, the SortByProximity function is used to generate multiple lists of generic regions, ordered by their 'distance' to the origin region. +// Then, for each of these lists, all of its entries are mapped using the specified mapper and extended with the specified pre- and suffix. +// For each string generated this way, the list of available regions is filtered for entries that match the resulting regular expression. +// If any matches are found, the function is aborted and returns only the matches found for the current list. +// If no match is found for any list, nil is returned. +// An error is only returned if the generated regular expression is invalid. +// +// The idea is, that all returned specific regions are considered to have the same minimal distance to the specified origin. +// +// The preferSameRegion field controls whether other directions within the same region should be prioritized over neighboring directions of neighboring regions. +// For example: GCP currently only has regions in the south(east) of Australia, so there is no exact match for the generic region specification (AUSTRALIA, NORTH). +// With preferSameRegion set to false, the function returns GCPs asia-south* regions because Asia's south is considered only one step away from Australia's north, opposed to Australia's south, which is considered to be two steps away. +// With preferSameRegion set to true, the function returns GCPs australia-south* regions because even though it is the opposite direction, they still share the same region and are therefore preferred. +// If GCP had zones in central, east, or west of Australia, all of these would be considered one step away from (AUSTRALIA, NORTH). In this case, with preferSameRegion set to true, only these regions would be returned, +// neither asia-south*, nor australia-south*. With preferSameRegion set to false, the returned selection would contain australia-central*, -east*, and -west*, as well as asia-south*, because all of them are considered the same distance away from (AUSTRALIA, NORTH). +// +// The returned list is sorted alphabetically to ensure consistent results. +func GetClosestRegions(origin openmcpv1alpha1.RegionSpecification, mapper GenericToSpecificRegionMapper, availableRegions []string, preferSameRegion bool) ([]string, error) { + groups := SortByProximity(origin, preferSameRegion) + for _, group := range groups { + var groupMatches []string + for _, region := range group { + regex := mapper.MapGenericToSpecific(region.Name, region.Direction) + if regex == "" { + continue + } + var err error + regionMatches, err := Filter(availableRegions, regex) + if err != nil { + return nil, err + } + if len(regionMatches) > 0 { + groupMatches = append(groupMatches, regionMatches...) + } + } + if len(groupMatches) > 0 { + slices.Sort(groupMatches) + return groupMatches, nil + } + } + return nil, nil +} + +// Filter filters a list of strings and returns a new list containing only the elements which are matched by the given regular expression. +// Returns an error if the regular expression cannot be compiled. +func Filter(data []string, regex string) ([]string, error) { + matcher, err := regexp.Compile(regex) + if err != nil { + return nil, fmt.Errorf("error compiling regex '%s': %w", regex, err) + } + res := []string{} + for _, elem := range data { + if matcher.MatchString(elem) { + res = append(res, elem) + } + } + return res, nil +} + +var DefaultRegion = openmcpv1alpha1.RegionSpecification{ + Name: openmcpv1alpha1.EUROPE, + Direction: openmcpv1alpha1.CENTRAL, +} + +// GenericToSpecificRegionMapper maps between the generic region specification and a specific implementation. +type GenericToSpecificRegionMapper interface { + // MapGenericToSpecific takes a generic region and direction and maps them to an implementation-specific regex string. + // This regex can be used to filter a list of existing regions for matches. + // The behavior if (parts of) the arguments are empty or no mapping can be found depends on the implementation of the interface. + MapGenericToSpecific(openmcpv1alpha1.Region, openmcpv1alpha1.Direction) string +} + +var _ GenericToSpecificRegionMapper = &BasicRegionMapper{} + +// BasicRegionMapper is a basic implementation of the GenericToSpecificRegionMapper interface. +type BasicRegionMapper struct { + // In format string, %R will be replaced by the region and %D will be replaced by the direction. + Format string + RegionGenericToSpecific map[openmcpv1alpha1.Region]string + DirectionGenericToSpecific map[openmcpv1alpha1.Direction]string + DefaultMissingDirection bool +} + +// NewRegionMapper creates a new BasicRegionMapper. +// format is a regex string where %R represents the region and %D represents the direction. +// The mappings map from generic regions and directions to the specific ones. +// If defaultMissingDirection is set to true, the direction is defaulted to CENTRAL if not specified. Otherwise, it is kept empty. +// The region is always defaulted to the value from DefaultRegion if left empty. +// +// Example: If EUROPE maps to 'eu' and CENTRAL maps to 'central' and the format string is '%R-%D-[0-9]+', the mapper would map (EUROPE, CENTRAL) to 'eu-central-[0-9]+'. +// This regex could then be used to filter e.g. ['eu-central-1', 'eu-central-2', 'eu-central-3'] out of a list of available specific regions. +func NewRegionMapper(format string, regionMapping map[openmcpv1alpha1.Region]string, directionMapping map[openmcpv1alpha1.Direction]string, defaultMissingDirection bool) *BasicRegionMapper { + res := &BasicRegionMapper{ + Format: format, + RegionGenericToSpecific: make(map[openmcpv1alpha1.Region]string, len(regionMapping)), + DirectionGenericToSpecific: make(map[openmcpv1alpha1.Direction]string, len(directionMapping)), + DefaultMissingDirection: defaultMissingDirection, + } + + for g, s := range regionMapping { + res.RegionGenericToSpecific[g] = s + } + for g, s := range directionMapping { + res.DirectionGenericToSpecific[g] = s + } + + return res +} + +func (m *BasicRegionMapper) MapGenericToSpecific(reg openmcpv1alpha1.Region, dir openmcpv1alpha1.Direction) string { + if reg == "" { + reg = DefaultRegion.Name + } + mReg, ok := m.RegionGenericToSpecific[reg] + if !ok { + // no mapping specified for region, return empty string + return "" + } + mDir := "" + if dir == "" && m.DefaultMissingDirection { + dir = DefaultRegion.Direction + } + mDir = m.DirectionGenericToSpecific[dir] + rep := strings.NewReplacer("%R", mReg, "%D", mDir) + return rep.Replace(m.Format) +} + +// GetPredefinedMapperByCloudprovider returns a predefined mapper for the specified cloud provider, if any exists. +// Otherwise, nil is returned. +// The name of the cloudprovider is case-insensitive. +// +// Currently supported: aws, gcp +func GetPredefinedMapperByCloudprovider(provider string) GenericToSpecificRegionMapper { + switch strings.ToLower(provider) { + case "aws": + return AWSMapper() + case "gcp": + return GCPMapper() + } + return nil +} + +// AWSMapper returns a pre-configured mapper for AWS regions. +func AWSMapper() *BasicRegionMapper { + return NewRegionMapper("^%R-%D[a-z]{0,4}-[0-9]+[a-z]*$", map[openmcpv1alpha1.Region]string{ + openmcpv1alpha1.AFRICA: "af", + openmcpv1alpha1.ASIA: "ap", + openmcpv1alpha1.AUSTRALIA: "ap", + openmcpv1alpha1.EUROPE: "eu", + openmcpv1alpha1.NORTHAMERICA: "us", + openmcpv1alpha1.SOUTHAMERICA: "sa", + }, map[openmcpv1alpha1.Direction]string{ + openmcpv1alpha1.CENTRAL: "central", + openmcpv1alpha1.NORTH: "north", + openmcpv1alpha1.EAST: "east", + openmcpv1alpha1.SOUTH: "south", + openmcpv1alpha1.WEST: "west", + }, true) +} + +// AWSRegions contains a list of all known AWS regions. May not be up-to-date. +var AWSRegions = []string{ + "us-east-2", + "us-east-1", + "us-west-1", + "us-west-2", + "af-south-1", + "ap-east-1", + "ap-south-2", + "ap-southeast-3", + "ap-southeast-4", + "ap-south-1", + "ap-northeast-3", + "ap-northeast-2", + "ap-southeast-1", + "ap-southeast-2", + "ap-northeast-1", + "ca-central-1", + "ca-west-1", + "eu-central-1", + "eu-west-1", + "eu-west-2", + "eu-south-1", + "eu-west-3", + "eu-south-2", + "eu-north-1", + "eu-central-2", + "il-central-1", + "me-south-1", + "me-central-1", + "sa-east-1", + "us-gov-east-1", + "us-gov-west-1", +} + +// AWSZones contains a list of all known AWS availability zones. May not be up-to-date. +// Note that special zones or zones belonging to special regions are excluded, so not every region from the AWSRegions list has matching zones in here. +var AWSZones = []string{ + "us-east-2a", + "us-east-2b", + "us-east-2c", + "us-east-1a", + "us-east-1b", + "us-east-1c", + "us-east-1d", + "us-east-1e", + "us-east-1f", + "us-west-1a", + "us-west-1b", + "us-west-2a", + "us-west-2b", + "us-west-2c", + "us-west-2d", + "ap-south-1a", + "ap-south-1b", + "ap-south-1c", + "ap-northeast-3a", + "ap-northeast-3b", + "ap-northeast-3c", + "ap-northeast-2a", + "ap-northeast-2b", + "ap-northeast-2c", + "ap-northeast-2d", + "ap-southeast-1a", + "ap-southeast-1b", + "ap-southeast-1c", + "ap-southeast-2a", + "ap-southeast-2b", + "ap-southeast-2c", + "ap-northeast-1a", + "ap-northeast-1c", + "ap-northeast-1d", + "ca-central-1a", + "ca-central-1b", + "ca-central-1d", + "eu-central-1a", + "eu-central-1b", + "eu-central-1c", + "eu-west-1a", + "eu-west-1b", + "eu-west-1c", + "eu-west-2a", + "eu-west-2b", + "eu-west-2c", + "eu-west-3a", + "eu-west-3b", + "eu-west-3c", + "eu-north-1a", + "eu-north-1b", + "eu-north-1c", + "sa-east-1a", + "sa-east-1b", + "sa-east-1c", +} + +// GCPMapper returns a pre-configured mapper for GCP regions. +func GCPMapper() *BasicRegionMapper { + return NewRegionMapper("^%R-%D[a-z]{0,4}[0-9]+(-[a-z]+)?$", map[openmcpv1alpha1.Region]string{ + openmcpv1alpha1.AFRICA: "me", // not really, but close enough + openmcpv1alpha1.ASIA: "asia", + openmcpv1alpha1.AUSTRALIA: "australia", + openmcpv1alpha1.EUROPE: "europe", + openmcpv1alpha1.NORTHAMERICA: "us", + openmcpv1alpha1.SOUTHAMERICA: "southamerica", + }, map[openmcpv1alpha1.Direction]string{ + openmcpv1alpha1.CENTRAL: "central", + openmcpv1alpha1.NORTH: "north", + openmcpv1alpha1.EAST: "east", + openmcpv1alpha1.SOUTH: "south", + openmcpv1alpha1.WEST: "west", + }, true) +} + +// GCPRegions contains a list of all known GCP regions. May not be up-to-date. +var GCPRegions = []string{ + "asia-east1", + "asia-east2", + "asia-northeast1", + "asia-northeast2", + "asia-northeast3", + "asia-south1", + "asia-south2", + "asia-southeast1", + "asia-southeast2", + "australia-southeast1", + "australia-southeast2", + "europe-central2", + "europe-north1", + "europe-southwest1", + "europe-west1", + "europe-west10", + "europe-west12", + "europe-west2", + "europe-west3", + "europe-west4", + "europe-west6", + "europe-west8", + "europe-west9", + "me-central1", + "me-central2", + "me-west1", + "northamerica-northeast1", + "northamerica-northeast2", + "southamerica-east1", + "southamerica-west1", + "us-central1", + "us-east1", + "us-east4", + "us-east5", + "us-south1", + "us-west1", + "us-west2", + "us-west3", + "us-west4", +} + +// GCPZones contains a list of all known GCP availability zones. May not be up-to-date. +var GCPZones = []string{ + "asia-east1-a", + "asia-east1-b", + "asia-east1-c", + "asia-east2-a", + "asia-east2-b", + "asia-east2-c", + "asia-northeast1-a", + "asia-northeast1-b", + "asia-northeast1-c", + "asia-northeast2-a", + "asia-northeast2-b", + "asia-northeast2-c", + "asia-northeast3-a", + "asia-northeast3-b", + "asia-northeast3-c", + "asia-south1-a", + "asia-south1-b", + "asia-south1-c", + "asia-south2-a", + "asia-south2-b", + "asia-south2-c", + "asia-southeast1-a", + "asia-southeast1-b", + "asia-southeast1-c", + "asia-southeast2-a", + "asia-southeast2-b", + "asia-southeast2-c", + "australia-southeast1-a", + "australia-southeast1-b", + "australia-southeast1-c", + "australia-southeast2-a", + "australia-southeast2-b", + "australia-southeast2-c", + "europe-central2-a", + "europe-central2-b", + "europe-central2-c", + "europe-north1-a", + "europe-north1-b", + "europe-north1-c", + "europe-southwest1-a", + "europe-southwest1-b", + "europe-southwest1-c", + "europe-west1-b", + "europe-west1-c", + "europe-west1-d", + "europe-west10-a", + "europe-west10-b", + "europe-west10-c", + "europe-west12-a", + "europe-west12-b", + "europe-west12-c", + "europe-west2-a", + "europe-west2-b", + "europe-west2-c", + "europe-west3-a", + "europe-west3-b", + "europe-west3-c", + "europe-west4-a", + "europe-west4-b", + "europe-west4-c", + "europe-west6-a", + "europe-west6-b", + "europe-west6-c", + "europe-west8-a", + "europe-west8-b", + "europe-west8-c", + "europe-west9-a", + "europe-west9-b", + "europe-west9-c", + "me-central1-a", + "me-central1-b", + "me-central1-c", + "me-central2-a", + "me-central2-b", + "me-central2-c", + "me-west1-a", + "me-west1-b", + "me-west1-c", + "northamerica-northeast1-a", + "northamerica-northeast1-b", + "northamerica-northeast1-c", + "northamerica-northeast2-a", + "northamerica-northeast2-b", + "northamerica-northeast2-c", + "southamerica-east1-a", + "southamerica-east1-b", + "southamerica-east1-c", + "southamerica-west1-a", + "southamerica-west1-b", + "southamerica-west1-c", + "us-central1-a", + "us-central1-b", + "us-central1-c", + "us-central1-f", + "us-east1-b", + "us-east1-c", + "us-east1-d", + "us-east4-a", + "us-east4-b", + "us-east4-c", + "us-east5-a", + "us-east5-b", + "us-east5-c", + "us-south1-a", + "us-south1-b", + "us-south1-c", + "us-west1-a", + "us-west1-b", + "us-west1-c", + "us-west2-a", + "us-west2-b", + "us-west2-c", + "us-west3-a", + "us-west3-b", + "us-west3-c", + "us-west4-a", + "us-west4-b", + "us-west4-c", +} diff --git a/internal/utils/region/region_test.go b/internal/utils/region/region_test.go new file mode 100644 index 0000000..dc975c4 --- /dev/null +++ b/internal/utils/region/region_test.go @@ -0,0 +1,35 @@ +package region + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +var _ = Describe("Regions", func() { + + It("should find one closest region", func() { + availableRegions := []string{"europe-west1", "us-east1", "us-west1"} + origin := openmcpv1alpha1.RegionSpecification{ + Name: openmcpv1alpha1.ASIA, + Direction: openmcpv1alpha1.CENTRAL, + } + regions, err := GetClosestRegions(origin, GCPMapper(), availableRegions, true) + Expect(err).NotTo(HaveOccurred()) + Expect(regions).To(HaveLen(1)) + Expect(regions[0]).To(Equal(availableRegions[2])) + }) + + It("should find the closest regions", func() { + availableRegions := []string{"europe-west1", "us-east1", "us-west1"} + origin := openmcpv1alpha1.RegionSpecification{ + Name: openmcpv1alpha1.NORTHAMERICA, + Direction: openmcpv1alpha1.NORTH, + } + regions, err := GetClosestRegions(origin, GCPMapper(), availableRegions, true) + Expect(err).NotTo(HaveOccurred()) + Expect(regions).To(HaveLen(2)) + Expect(regions).To(ContainElements(availableRegions[1], availableRegions[2])) + }) +}) diff --git a/internal/utils/region/suite_test.go b/internal/utils/region/suite_test.go new file mode 100644 index 0000000..43903e3 --- /dev/null +++ b/internal/utils/region/suite_test.go @@ -0,0 +1,13 @@ +package region + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestControllers(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Controller Suite") +} diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..c4b41c2 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,82 @@ +package utils + +import ( + "context" + "crypto/sha1" + "encoding/base32" + "fmt" + "hash/fnv" + "reflect" + "strings" + + "github.com/openmcp-project/controller-utils/pkg/logging" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + maxLength int = 63 + Base32EncodeStdLowerCase = "abcdefghijklmnopqrstuvwxyz234567" +) + +// K8sNameHash takes any number of string arguments and computes a hash out of it, which is then base32-encoded to be a valid k8s resource name. +// The arguments are joined with '/' before being hashed. +func K8sNameHash(ids ...string) string { + name := strings.Join(ids, "/") + h := sha1.New() + _, _ = h.Write([]byte(name)) + // we need base32 encoding as some base64 (even url safe base64) characters are not supported by k8s + // see https://kubernetes.io/docs/concepts/overview/working-with-objects/names/ + return base32.NewEncoding(Base32EncodeStdLowerCase).WithPadding(base32.NoPadding).EncodeToString(h.Sum(nil)) +} + +// ScopeToControlPlane is a convenience function which wraps K8sNameHash(cpMeta.Namespace, cpMeta.Name, ids...). +func ScopeToControlPlane(cpMeta *metav1.ObjectMeta, ids ...string) string { + return K8sNameHash(append([]string{cpMeta.Namespace, cpMeta.Name}, ids...)...) +} + +// IsNil checks if a given pointer is nil. +// Opposed to 'i == nil', this works for typed and untyped nil values. +func IsNil(i any) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Array, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false +} + +// InitializeControllerLogger initializes a new logger. +// Panics if the context doesn't already contain a logger. +// The given name is added to the logger (it's supposed to be the controller's name). +// Returns the logger and the context containing it. +func InitializeControllerLogger(ctx context.Context, name string) (logging.Logger, context.Context) { + log := logging.FromContextOrPanic(ctx).WithName(name) + ctx = logging.NewContext(ctx, log) + return log, ctx +} + +// PrefixWithNamespace prefixes the given name with the sourceNamespace. +func PrefixWithNamespace(sourceNamespace, name string) string { + if len(sourceNamespace) == 0 { + sourceNamespace = "default" + } + + return shorten(fmt.Sprintf("%s--%s", sourceNamespace, name), maxLength) +} + +// shorten shortens the given string to the provided length +func shorten(input string, maxLength int) string { + if len(input) <= maxLength { + return input + } + + hash := fnv.New32a() + hash.Write([]byte(input)) + + suffix := fmt.Sprintf("--%x", hash.Sum32()) + trimLength := maxLength - len(suffix) + + return input[:trimLength] + suffix +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go new file mode 100644 index 0000000..b6bacfa --- /dev/null +++ b/internal/utils/utils_test.go @@ -0,0 +1,169 @@ +package utils + +import ( + "context" + "strings" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/openmcp-project/controller-utils/pkg/logging" + "github.com/stretchr/testify/assert" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +func TestPrefixWithNamespace(t *testing.T) { + tests := []struct { + description string + inputNS string + inputName string + expected string + }{ + { + description: "automatically assumes 'default' as namespace if no namespace is given", + inputName: "test", + expected: "default--test", + }, + { + description: "should not modify the namespace if 'default' is given", + inputNS: "default", + inputName: "test", + expected: "default--test", + }, + { + description: "should work with namespaces other than 'default'", + inputNS: "test", + inputName: "test", + expected: "test--test", + }, + { + description: "should shorten long control-plane-names", + inputNS: "my-control-plane-ns", + inputName: "my-way-too-long-control-plane-name-0123456789", + expected: "my-control-plane-ns--my-way-too-long-control-plane-na--536b070d", + }, + { + description: "should work with long namespace names #1", + inputNS: strings.Repeat("a", 253), // max length of kubernetes namespace names + inputName: "test1", + expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--20950d95", + }, + { + description: "should work with long namespace names #2", + inputNS: strings.Repeat("a", 253), // max length of kubernetes namespace names + inputName: "test2", + expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--1d9508dc", + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + uut := PrefixWithNamespace(test.inputNS, test.inputName) + + assert.LessOrEqual(t, len(uut), 63) + assert.Equal(t, test.expected, uut) + }) + } +} + +func TestShorten(t *testing.T) { + tests := []struct { + description string + input string + expected string + }{ + { + description: "SHORTEN string which is longer than 63 characters", + input: strings.Repeat("a", maxLength+1), + expected: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--d96f0f85", + }, + { + description: "NOP for empty string", + }, + { + description: "NOP if string length smaller than or equal to 63", + input: strings.Repeat("a", maxLength), + expected: strings.Repeat("a", maxLength), + }, + } + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + uut := shorten(test.input, maxLength) + + assert.LessOrEqual(t, len(uut), 63) + assert.Equal(t, test.expected, uut) + }) + } +} + +var _ = Describe("Utils", func() { + Context("K8sNameHash", func() { + It("should return a valid k8s resource name", func() { + result1 := K8sNameHash("test", "name") + Expect(result1).To(MatchRegexp("^[a-z2-7]+$")) + + result2 := K8sNameHash("test", "name") + Expect(result2).To(Equal(result1)) + }) + }) + + Context("ScopeToControlPlane", func() { + It("should return a valid k8s resource name", func() { + meta := &metav1.ObjectMeta{ + Namespace: "test-namespace", + Name: "test-name", + } + result := ScopeToControlPlane(meta, "additional", "ids") + Expect(result).To(MatchRegexp("^[a-z2-7]+$")) + }) + }) + + Context("IsNil", func() { + It("should return true for nil values", func() { + Expect(IsNil(map[string]string(nil))).To(BeTrue()) + Expect(IsNil(nil)).To(BeTrue()) + Expect(IsNil((*int)(nil))).To(BeTrue()) + Expect(IsNil([]string(nil))).To(BeTrue()) + Expect(IsNil((chan int)(nil))).To(BeTrue()) + var c client.Client + Expect(IsNil(c)).To(BeTrue()) + var s *struct{} + Expect(IsNil(s)).To(BeTrue()) + Expect(IsNil((*client.Client)(nil))).To(BeTrue()) + }) + + It("should return false for non-nil values", func() { + nonNilMap := map[string]string{"key": "value"} + Expect(IsNil(nonNilMap)).To(BeFalse()) + nonNilSlice := []string{"value"} + Expect(IsNil(nonNilSlice)).To(BeFalse()) + nonNilPointer := new(int) + Expect(IsNil(nonNilPointer)).To(BeFalse()) + nonNilStruct := struct{}{} + Expect(IsNil(nonNilStruct)).To(BeFalse()) + nonNilChannel := make(chan int) + Expect(IsNil(nonNilChannel)).To(BeFalse()) + }) + }) + + Context("InitializeControllerLogger", func() { + It("should panic if context doesn't contain a logger", func() { + Expect(func() { InitializeControllerLogger(context.Background(), "test") }).To(Panic()) + }) + + It("should return a logger and context containing it", func() { + log, err := logging.GetLogger() + Expect(err).ToNot(HaveOccurred()) + ctx := logging.NewContext(context.Background(), log) + _, ctxWithLogger := InitializeControllerLogger(ctx, "test") + Expect(logging.FromContext(ctxWithLogger)).ToNot(BeNil()) + }) + }) +}) + +func TestConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Utils Test Suite") +} diff --git a/test/apiserver/PLACEHOLDER b/test/apiserver/PLACEHOLDER new file mode 100644 index 0000000..96ac816 --- /dev/null +++ b/test/apiserver/PLACEHOLDER @@ -0,0 +1 @@ +git doesn't track empty folders, so let's use this dummy file until we have actual content ... \ No newline at end of file diff --git a/test/cloudorchestrator/PLACEHOLDER b/test/cloudorchestrator/PLACEHOLDER new file mode 100644 index 0000000..96ac816 --- /dev/null +++ b/test/cloudorchestrator/PLACEHOLDER @@ -0,0 +1 @@ +git doesn't track empty folders, so let's use this dummy file until we have actual content ... \ No newline at end of file diff --git a/test/landscaper/PLACEHOLDER b/test/landscaper/PLACEHOLDER new file mode 100644 index 0000000..96ac816 --- /dev/null +++ b/test/landscaper/PLACEHOLDER @@ -0,0 +1 @@ +git doesn't track empty folders, so let's use this dummy file until we have actual content ... \ No newline at end of file diff --git a/test/managedcontrolplane/PLACEHOLDER b/test/managedcontrolplane/PLACEHOLDER new file mode 100644 index 0000000..96ac816 --- /dev/null +++ b/test/managedcontrolplane/PLACEHOLDER @@ -0,0 +1 @@ +git doesn't track empty folders, so let's use this dummy file until we have actual content ... \ No newline at end of file diff --git a/test/matchers/conditions.go b/test/matchers/conditions.go new file mode 100644 index 0000000..cc08380 --- /dev/null +++ b/test/matchers/conditions.go @@ -0,0 +1,56 @@ +package matchers + +import ( + "fmt" + + "github.com/onsi/gomega/types" + + corev1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// MatchComponentCondition returns a Gomega matcher that checks if a ComponentCondition is equal to the expected one. +// If the passed in 'actual' is not a ComponentCondition, the matcher will fail. +// All fields which are set to their zero value in the expected condition will be ignored. +func MatchComponentCondition(con corev1alpha1.ComponentCondition) types.GomegaMatcher { + return &conditionMatcher{expected: con} +} + +type conditionMatcher struct { + expected corev1alpha1.ComponentCondition +} + +var _ types.GomegaMatcher = &conditionMatcher{} + +// Match implements types.GomegaMatcher. +func (c *conditionMatcher) Match(actualRaw interface{}) (success bool, err error) { + actual, ok := actualRaw.(corev1alpha1.ComponentCondition) + if !ok { + return false, fmt.Errorf("expected actual to be of type ComponentCondition, got %T", actualRaw) + } + if c.expected.Type != "" && c.expected.Type != actual.Type { + return false, nil + } + if c.expected.Status != "" && c.expected.Status != actual.Status { + return false, nil + } + if c.expected.Reason != "" && c.expected.Reason != actual.Reason { + return false, nil + } + if c.expected.Message != "" && c.expected.Message != actual.Message { + return false, nil + } + if !c.expected.LastTransitionTime.IsZero() && !c.expected.LastTransitionTime.Equal(&actual.LastTransitionTime) { + return false, nil + } + return true, nil +} + +// FailureMessage implements types.GomegaMatcher. +func (c *conditionMatcher) FailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%#v\nto equal \n\t%#v", actual, c.expected) +} + +// NegatedFailureMessage implements types.GomegaMatcher. +func (c *conditionMatcher) NegatedFailureMessage(actual interface{}) (message string) { + return fmt.Sprintf("Expected\n\t%#v\nto not equal \n\t%#v", actual, c.expected) +} diff --git a/test/utils/controller.go b/test/utils/controller.go new file mode 100644 index 0000000..2269a6f --- /dev/null +++ b/test/utils/controller.go @@ -0,0 +1,74 @@ +package utils + +import ( + "context" + "errors" + + apiserverutils "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" + + restclient "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + openmcpv1alpha1 "github.com/openmcp-project/mcp-operator/api/core/v1alpha1" +) + +// TestAPIServerAccess is a test implementation of APIServerAccess. +type TestAPIServerAccess struct { + APIServerAccess apiserverutils.APIServerAccessImpl + Client client.Client + Error error +} + +// GetAdminAccessClient implements APIServerAccess.GetAdminAccessClient. +func (t *TestAPIServerAccess) GetAdminAccessClient(as *openmcpv1alpha1.APIServer, _ client.Options) (client.Client, error) { + if t.Error != nil { + return nil, t.Error + } + return t.Client, nil +} + +// GetAdminAccessConfig implements APIServerAccess.GetAdminAccessConfig. +func (t *TestAPIServerAccess) GetAdminAccessConfig(as *openmcpv1alpha1.APIServer) (*restclient.Config, error) { + return t.APIServerAccess.GetAdminAccessConfig(as) +} + +// GetAdminAccessRaw implements APIServerAccess.GetAdminAccessRaw. +func (t *TestAPIServerAccess) GetAdminAccessRaw(as *openmcpv1alpha1.APIServer) (string, error) { + return t.APIServerAccess.GetAdminAccessRaw(as) +} + +// TestWorker is a test implementation of Worker. +type TestWorker struct { + taskList map[string]apiserverutils.Task + crateClient client.Client + apiServerClient client.Client +} + +// NewTestWorker creates a new TestWorker. +func NewTestWorker(crateClient, apiServerClient client.Client) *TestWorker { + return &TestWorker{ + taskList: make(map[string]apiserverutils.Task), + crateClient: crateClient, + apiServerClient: apiServerClient, + } +} + +func (w *TestWorker) RegisterTask(name string, task apiserverutils.Task) { + w.taskList[name] = task +} + +func (w *TestWorker) UnregisterTask(name string) { + delete(w.taskList, name) +} + +func (w *TestWorker) Start(_ context.Context, _ apiserverutils.OnExit, _ apiserverutils.OnNextInterval, _ <-chan struct{}) error { + return nil +} + +func (w *TestWorker) RunTasks(ctx context.Context, as *openmcpv1alpha1.APIServer) error { + var err error + for _, task := range w.taskList { + err = errors.Join(err, task(ctx, as, w.crateClient, w.apiServerClient)) + } + return err +} diff --git a/test/utils/test.go b/test/utils/test.go new file mode 100644 index 0000000..59c8c9a --- /dev/null +++ b/test/utils/test.go @@ -0,0 +1,60 @@ +package utils + +import ( + "os" + "path" + + "github.com/openmcp-project/mcp-operator/internal/utils/apiserver" + + laasinstall "github.com/gardener/landscaper-service/pkg/apis/core/install" + cocorev1beta1 "github.com/openmcp-project/control-plane-operator/api/v1beta1" + "github.com/openmcp-project/controller-utils/pkg/testing" + "k8s.io/apimachinery/pkg/runtime" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + + gardenauthenticationv1alpha1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/authentication/v1alpha1" + gardenv1beta1 "github.com/openmcp-project/mcp-operator/api/external/gardener/pkg/apis/core/v1beta1" + openmcpinstall "github.com/openmcp-project/mcp-operator/api/install" +) + +var ( + Scheme *runtime.Scheme +) + +func init() { + Scheme = runtime.NewScheme() + openmcpinstall.Install(Scheme) + laasinstall.Install(Scheme) + utilruntime.Must(cocorev1beta1.AddToScheme(Scheme)) + utilruntime.Must(gardenv1beta1.AddToScheme(Scheme)) + utilruntime.Must(gardenauthenticationv1alpha1.AddToScheme(Scheme)) + utilruntime.Must(clientgoscheme.AddToScheme(Scheme)) +} + +const ( + CrateCluster = "crate" + APIServerCluster = "apiserver" + LaaSCoreCluster = "laas" + COCoreCluster = "cloudOrchestrator" +) + +type ReconcilerWithAPIServerAccess interface { + SetAPIServerAccess(access apiserver.APIServerAccess) +} + +func DefaultTestSetupBuilder(testDirPathSegments ...string) *testing.ComplexEnvironmentBuilder { + builder := testing.NewComplexEnvironmentBuilder(). + WithFakeClient(CrateCluster, Scheme) + + if len(testDirPathSegments) > 0 && !(len(testDirPathSegments) == 1 && testDirPathSegments[0] == "") { + builder.WithInitObjectPath(CrateCluster, testDirPathSegments...) + apiServerDir := path.Join(path.Join(testDirPathSegments...), "apiserver") + _, err := os.Stat(apiServerDir) + if err == nil { + builder.WithInitObjectPath(APIServerCluster, apiServerDir) + } + } + + return builder +}