diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/csi-snapshot-controller-poddisruptionbudget.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/csi-snapshot-controller-poddisruptionbudget.yaml index 3b1f29ad..e8de9025 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/csi-snapshot-controller-poddisruptionbudget.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/csi-snapshot-controller-poddisruptionbudget.yaml @@ -1,3 +1,4 @@ +{{- if not .Values.csi.enableCompatibilityMode }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: @@ -13,3 +14,4 @@ spec: app: {{.Values.prefix}}-csi-snapshot-controller role: controller unhealthyPodEvictionPolicy: AlwaysAllow +{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/csi-snapshot-controller-vpa.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/csi-snapshot-controller-vpa.yaml index bbe8278f..fd4164ec 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/csi-snapshot-controller-vpa.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/csi-snapshot-controller-vpa.yaml @@ -1,4 +1,4 @@ ---- +{{- if not .Values.csi.enableCompatibilityMode }} apiVersion: autoscaling.k8s.io/v1 kind: VerticalPodAutoscaler metadata: @@ -15,3 +15,4 @@ spec: name: {{.Values.prefix}}-csi-snapshot-controller updatePolicy: updateMode: Auto +{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml index 391bf0f5..32f20b73 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-driver-controller.yaml @@ -52,6 +52,12 @@ spec: {{- end }} - --v=3 - --provide-node-service=false + {{- if .Values.csi.enableCompatibilityMode }} + - --legacy-storage-mode=true + {{- end }} + {{- if .Values.csi.blockLegacyCreation }} + - --legacy-volume-creation=false + {{- end }} env: - name: CSI_ENDPOINT value: unix://{{ .Values.socketPath }}/csi.sock @@ -69,7 +75,7 @@ spec: {{- end }} ports: - name: healthz - containerPort: 9808 + containerPort: {{ .Values.healthzPort }} protocol: TCP livenessProbe: httpGet: @@ -251,6 +257,7 @@ spec: args: - --probe-timeout=3m - --csi-address=/csi/csi.sock + - --health-port={{.Values.healthzPort}} {{- if .Values.resources.livenessProbe }} resources: {{ toYaml .Values.resources.livenessProbe | indent 10 }} diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-snapshot-controller.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-snapshot-controller.yaml index 66c4daf0..f3e26cfc 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-snapshot-controller.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-snapshot-controller.yaml @@ -1,3 +1,4 @@ +{{- if not .Values.csi.enableCompatibilityMode }} apiVersion: apps/v1 kind: Deployment metadata: @@ -70,3 +71,4 @@ spec: path: token name: shoot-access-csi-snapshot-controller optional: false +{{- end }} diff --git a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml index 60ff609a..cf5a4243 100644 --- a/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml +++ b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/values.yaml @@ -4,6 +4,12 @@ kubernetesVersion: 1.30.0 prefix: stackit-blockstorage projectID: "" +healthzPort: 9808 + +csi: + enableCompatibilityMode: false + blockLegacyCreation: false + images: csi-driver-stackit: image-repository:image-tag csi-provisioner: image-repository:image-tag @@ -12,7 +18,6 @@ images: csi-resizer: image-repository:image-tag csi-liveness-probe: image-repository:image-tag csi-snapshot-controller: image-repository:image-tag - csi-snapshot-validation-webhook: image-repository:image-tag socketPath: /var/lib/csi/sockets/pluginproxy region: "" diff --git a/charts/internal/shoot-system-components/charts/stackit-blockstorage-csi-driver/templates/daemonset.yaml b/charts/internal/shoot-system-components/charts/stackit-blockstorage-csi-driver/templates/daemonset.yaml index cb04e3cf..273f2941 100644 --- a/charts/internal/shoot-system-components/charts/stackit-blockstorage-csi-driver/templates/daemonset.yaml +++ b/charts/internal/shoot-system-components/charts/stackit-blockstorage-csi-driver/templates/daemonset.yaml @@ -17,7 +17,11 @@ spec: metadata: annotations: checksum/configmap-cloud-provider-config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} - node.gardener.cloud/wait-for-csi-node-stackit: block-storage.csi.stackit.cloud + {{- if eq .Values.driverName "cinder.csi.openstack.org" }} + node.gardener.cloud/wait-for-csi-node-openstack: {{ .Values.driverName }} + {{- else }} + node.gardener.cloud/wait-for-csi-node-stackit: {{ .Values.driverName }} + {{- end }} labels: node.gardener.cloud/critical-component: "true" app: {{ .Values.prefix }}-csi @@ -48,6 +52,9 @@ spec: {{- end }} - --v=2 - --provide-controller-service=false + {{- if .Values.csi.enableCompatibilityMode }} + - --legacy-storage-mode=true + {{- end }} env: - name: CSI_ENDPOINT value: unix://{{ .Values.socketPath }} @@ -62,7 +69,7 @@ spec: allowPrivilegeEscalation: true ports: - name: healthz - containerPort: 9908 + containerPort: {{ .Values.healthzPort }} protocol: TCP livenessProbe: httpGet: @@ -94,7 +101,7 @@ spec: - name: ADDRESS value: {{ .Values.socketPath }} - name: DRIVER_REG_SOCK_PATH - value: /var/lib/kubelet/plugins/block-storage.csi.stackit.cloud/csi.sock + value: /var/lib/kubelet/plugins/{{ .Values.driverName }}/csi.sock {{- if .Values.resources.nodeDriverRegistrar }} resources: {{ toYaml .Values.resources.nodeDriverRegistrar | indent 10 }} @@ -112,7 +119,7 @@ spec: args: - --probe-timeout=3m - --csi-address={{ .Values.socketPath }} - - --health-port=9908 + - --health-port={{ .Values.healthzPort }} {{- if .Values.resources.livenessProbe }} resources: {{ toYaml .Values.resources.livenessProbe | indent 10 }} @@ -130,7 +137,7 @@ spec: type: Directory - name: plugin-dir hostPath: - path: /var/lib/kubelet/plugins/block-storage.csi.stackit.cloud/ + path: /var/lib/kubelet/plugins/{{ .Values.driverName }}/ type: DirectoryOrCreate - name: registration-dir hostPath: diff --git a/charts/internal/shoot-system-components/charts/stackit-blockstorage-csi-driver/values.yaml b/charts/internal/shoot-system-components/charts/stackit-blockstorage-csi-driver/values.yaml index c5e073af..89178b01 100644 --- a/charts/internal/shoot-system-components/charts/stackit-blockstorage-csi-driver/values.yaml +++ b/charts/internal/shoot-system-components/charts/stackit-blockstorage-csi-driver/values.yaml @@ -3,6 +3,11 @@ driverName: block-storage.csi.stackit.cloud rescanBlockStorageOnResize: "true" +healthzPort: 9908 + +csi: + enableCompatibilityMode: false + images: csi-driver-stackit: image-repository:image-tag csi-node-driver-registrar: image-repository:image-tag diff --git a/go.mod b/go.mod index 53d65ee1..c132ec7f 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,7 @@ require ( k8s.io/utils v0.0.0-20260507154919-ff6756f316d2 sigs.k8s.io/controller-runtime v0.23.3 sigs.k8s.io/controller-runtime/tools/setup-envtest v0.0.0-20231015215740-bf15e44028f9 + sigs.k8s.io/yaml v1.6.0 ) require ( @@ -53,6 +54,7 @@ require ( github.com/BurntSushi/toml v1.6.0 // indirect github.com/Masterminds/goutils v1.1.1 // indirect github.com/Masterminds/sprig/v3 v3.3.0 // indirect + github.com/NYTimes/gziphandler v1.1.1 // indirect github.com/PaesslerAG/gval v1.2.4 // indirect github.com/PaesslerAG/jsonpath v0.1.2-0.20240726212847-3a740cf7976f // indirect github.com/VictoriaMetrics/VictoriaLogs v1.36.2-0.20251008164716-21c0fb3de84d // indirect @@ -83,6 +85,7 @@ require ( github.com/brunoga/deep v1.3.1 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect github.com/cyphar/filepath-securejoin v0.6.1 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elastic/crd-ref-docs v0.3.0 // indirect @@ -90,6 +93,7 @@ require ( github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fatih/color v1.19.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fluent/fluent-operator/v3 v3.7.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -113,6 +117,7 @@ require ( github.com/go-openapi/swag/typeutils v0.25.5 // indirect github.com/go-openapi/swag/yamlutils v0.25.5 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/go-test/deep v1.1.1 // indirect github.com/gobuffalo/flect v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/goccy/go-yaml v1.19.2 // indirect @@ -125,6 +130,7 @@ require ( github.com/google/gnostic-models v0.7.1 // indirect github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect @@ -134,6 +140,7 @@ require ( github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect github.com/klauspost/compress v1.18.6 // indirect github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/labstack/echo/v4 v4.15.1 // indirect github.com/labstack/gommon v0.4.2 // indirect github.com/mattn/go-colorable v0.1.14 // indirect @@ -160,6 +167,7 @@ require ( github.com/prometheus/otlptranslator v1.0.0 // indirect github.com/prometheus/procfs v0.20.1 // indirect github.com/prometheus/sigv4 v0.3.0 // indirect + github.com/robfig/cron v1.2.0 // indirect github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/sirupsen/logrus v1.9.4 // indirect @@ -175,7 +183,12 @@ require ( github.com/x448/float16 v0.8.4 // indirect github.com/zitadel/oidc/v3 v3.45.4 // indirect github.com/zitadel/schema v1.3.2 // indirect + go.etcd.io/etcd/api/v3 v3.6.5 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.5 // indirect + go.etcd.io/etcd/client/v3 v3.6.5 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect go.opentelemetry.io/contrib/otelconf v0.23.0 // indirect go.opentelemetry.io/otel v1.43.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.19.0 // indirect @@ -218,6 +231,7 @@ require ( google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect helm.sh/helm/v4 v4.1.4 // indirect @@ -228,14 +242,15 @@ require ( k8s.io/component-helpers v0.35.5 // indirect k8s.io/gengo/v2 v2.0.0-20251215205346-5ee0d033ba5b // indirect k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kms v0.35.5 // indirect k8s.io/kube-aggregator v0.35.5 // indirect k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect k8s.io/metrics v0.35.5 // indirect k8s.io/pod-security-admission v0.35.5 // indirect + sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.33.0 // indirect sigs.k8s.io/controller-tools v0.20.1 // indirect sigs.k8s.io/gateway-api v1.5.0 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect ) diff --git a/go.sum b/go.sum index 9fba0ce7..4bc2b3e0 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU= github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac= @@ -112,6 +114,8 @@ github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWR github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -126,6 +130,8 @@ github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxK github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96/go.mod h1:Qh8CwZgvJUkLughtfhJv5dyTYa91l1fOUCrgjqmcifM= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/elastic/crd-ref-docs v0.3.0 h1:9bGSUkBR56Z7TuDGQAu3KGbBkagwwZ6RkZmS+qvDuDM= github.com/elastic/crd-ref-docs v0.3.0/go.mod h1:8td3UC8CaO5M+G115O3FRKLmplmX+p0EqLMLGM6uNdk= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= @@ -238,6 +244,8 @@ github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvA github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= 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/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= +github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= @@ -311,6 +319,12 @@ github.com/gophercloud/utils/v2 v2.0.0-20260424064311-2eeed4ceb3e9/go.mod h1:bIE github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1 h1:qnpSQwGEnkcRpTqNOIR6bJbR0gAorgP9CSALpRcKoAA= +github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.1/go.mod h1:lXGCsh6c22WGtjr+qGHj1otzZpV/1kwTMAqkwZsnWRU= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0 h1:FbSCl+KggFl+Ocym490i/EyXF4lPgLoUtcSWquBM0Rs= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.0/go.mod h1:qOchhhIlmRcqk/O9uCo/puJlyo07YINaIqdZfZG3Jkc= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -327,6 +341,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= @@ -442,6 +458,8 @@ github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEy github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= github.com/prometheus/sigv4 v0.3.0 h1:QIG7nTbu0JTnNidGI1Uwl5AGVIChWUACxn2B/BQ1kms= github.com/prometheus/sigv4 v0.3.0/go.mod h1:fKtFYDus2M43CWKMNtGvFNHGXnAJJEGZbiYCmVp/F8I= +github.com/robfig/cron v1.2.0 h1:ZjScXvvxeQ63Dbyxy76Fj3AT3Ut0aKsyd2/tl3DTMuQ= +github.com/robfig/cron v1.2.0/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= @@ -453,6 +471,8 @@ github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= @@ -500,6 +520,8 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE= +github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= @@ -516,6 +538,8 @@ github.com/valyala/quicktemplate v1.8.0 h1:zU0tjbIqTRgKQzFY1L42zq0qR3eh4WoQQdIdq github.com/valyala/quicktemplate v1.8.0/go.mod h1:qIqW8/igXt8fdrUln5kOSb+KWMaJ4Y8QUsfd1k6L2jM= 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/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk= +github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -524,11 +548,27 @@ github.com/zitadel/oidc/v3 v3.45.4 h1:GKyWaPRVQ8sCu9XgJ3NgNGtG52FzwVJpzXjIUG2+Yr github.com/zitadel/oidc/v3 v3.45.4/go.mod h1:XALmFXS9/kSom9B6uWin1yJ2WTI/E4Ti5aXJdewAVEs= github.com/zitadel/schema v1.3.2 h1:gfJvt7dOMfTmxzhscZ9KkapKo3Nei3B6cAxjav+lyjI= github.com/zitadel/schema v1.3.2/go.mod h1:IZmdfF9Wu62Zu6tJJTH3UsArevs3Y4smfJIj3L8fzxw= +go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= +go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= +go.etcd.io/etcd/api/v3 v3.6.5 h1:pMMc42276sgR1j1raO/Qv3QI9Af/AuyQUW6CBAWuntA= +go.etcd.io/etcd/api/v3 v3.6.5/go.mod h1:ob0/oWA/UQQlT1BmaEkWQzI0sJ1M0Et0mMpaABxguOQ= +go.etcd.io/etcd/client/pkg/v3 v3.6.5 h1:Duz9fAzIZFhYWgRjp/FgNq2gO1jId9Yae/rLn3RrBP8= +go.etcd.io/etcd/client/pkg/v3 v3.6.5/go.mod h1:8Wx3eGRPiy0qOFMZT/hfvdos+DjEaPxdIDiCDUv/FQk= +go.etcd.io/etcd/client/v3 v3.6.5 h1:yRwZNFBx/35VKHTcLDeO7XVLbCBFbPi+XV4OC3QJf2U= +go.etcd.io/etcd/client/v3 v3.6.5/go.mod h1:ZqwG/7TAFZ0BJ0jXRPoJjKQJtbFo/9NIY8uoFFKcCyo= +go.etcd.io/etcd/pkg/v3 v3.6.5 h1:byxWB4AqIKI4SBmquZUG1WGtvMfMaorXFoCcFbVeoxM= +go.etcd.io/etcd/pkg/v3 v3.6.5/go.mod h1:uqrXrzmMIJDEy5j00bCqhVLzR5jEJIwDp5wTlLwPGOU= +go.etcd.io/etcd/server/v3 v3.6.5 h1:4RbUb1Bd4y1WkBHmuF+cZII83JNQMuNXzyjwigQ06y0= +go.etcd.io/etcd/server/v3 v3.6.5/go.mod h1:PLuhyVXz8WWRhzXDsl3A3zv/+aK9e4A9lpQkqawIaH0= +go.etcd.io/raft/v3 v3.6.0 h1:5NtvbDVYpnfZWcIHgGRk9DyzkBIXOi8j+DDp1IcnUWQ= +go.etcd.io/raft/v3 v3.6.0/go.mod h1:nLvLevg6+xrVtHUmVaTcTz603gQPHfh7kUAwV6YpfGo= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo= go.opentelemetry.io/contrib/otelconf v0.23.0 h1:s3C7KdMYiutf4rC8hKFA0WOIDG+gIru8ajjQKS59ir8= @@ -786,6 +826,8 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWM gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= 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.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -839,6 +881,8 @@ k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kms v0.35.5 h1:KTbr4tWIl7+OeYcY30Vwzv0G0Pz8EeSWcAnx5P7gBPY= +k8s.io/kms v0.35.5/go.mod h1:c/uQe/eKrWdBkvizLFW+ThLA6tTzR0RkkwJJyzDRT1g= k8s.io/kube-aggregator v0.35.5 h1:oLflHAqh8tEoEcXtrzGhr4hctwhcRr5B1sM+T96N1rs= k8s.io/kube-aggregator v0.35.5/go.mod h1:L3GflyN8a8CDjej2UxgeGwRiXYuI+aTZ0GE7qssdN2w= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= diff --git a/hack/api-reference/api.md b/hack/api-reference/api.md index 8bbbc206..773e7d08 100644 --- a/hack/api-reference/api.md +++ b/hack/api-reference/api.md @@ -215,11 +215,32 @@ string

+ + +compatibilityMode
+ +string + + + +

+ + +

CSICompatibilityMode +

+

Underlying type: string

+ + +

+ +

+ +

CSIManila

diff --git a/pkg/apis/stackit/v1alpha1/constants.go b/pkg/apis/stackit/v1alpha1/constants.go index 86bbdc81..42400ae6 100644 --- a/pkg/apis/stackit/v1alpha1/constants.go +++ b/pkg/apis/stackit/v1alpha1/constants.go @@ -5,6 +5,8 @@ package v1alpha1 const ( // DefaultCSIName defines the default CSI (Container Storage Interface) name for STACKIT DefaultCSIName = "stackit" + // DefaultCSICompatibilityMode defines the default CSI driver's compatibility mode. + DefaultCSICompatibilityMode = "default" // DefaultCCMName defines the default CCM (Cloud Controller Manager) controller to use DefaultCCMName = "stackit" ) @@ -15,3 +17,11 @@ const ( STACKIT ControllerName = "stackit" OPENSTACK ControllerName = "openstack" ) + +type CSICompatibilityMode string + +const ( + DEFAULT CSICompatibilityMode = "default" + COMPAT CSICompatibilityMode = "compat" + COMPATBLOCK CSICompatibilityMode = "compatblock" +) diff --git a/pkg/apis/stackit/v1alpha1/defaults.go b/pkg/apis/stackit/v1alpha1/defaults.go index d72cba93..e91caf34 100644 --- a/pkg/apis/stackit/v1alpha1/defaults.go +++ b/pkg/apis/stackit/v1alpha1/defaults.go @@ -43,4 +43,7 @@ func SetDefaults_ControlPlaneConfig(obj *ControlPlaneConfig) { if obj.Storage.CSI.Name == "" { obj.Storage.CSI.Name = DefaultCSIName } + if obj.Storage.CSI.CompatibilityMode == "" { + obj.Storage.CSI.CompatibilityMode = DefaultCSICompatibilityMode + } } diff --git a/pkg/apis/stackit/v1alpha1/types_controlplane.go b/pkg/apis/stackit/v1alpha1/types_controlplane.go index 5c10c28a..5b28637d 100644 --- a/pkg/apis/stackit/v1alpha1/types_controlplane.go +++ b/pkg/apis/stackit/v1alpha1/types_controlplane.go @@ -58,7 +58,8 @@ type Storage struct { } type CSI struct { - Name string `json:"name"` + Name string `json:"name"` + CompatibilityMode string `json:"compatibilityMode,omitempty"` } // CSIManila contains configuration for CSI Manila driver (support for NFS volumes) diff --git a/pkg/controller/controlplane/add.go b/pkg/controller/controlplane/add.go index a0e11e6f..ab687708 100644 --- a/pkg/controller/controlplane/add.go +++ b/pkg/controller/controlplane/add.go @@ -43,10 +43,15 @@ type AddOptions struct { // AddToManagerWithOptions adds a controller with the given Options to the given manager. // The opts.Reconciler is being set with a newly instantiated actuator. func AddToManagerWithOptions(ctx context.Context, mgr manager.Manager, opts AddOptions) error { + csiCompatibilityHandler, err := NewCompatCSICompatibilityHandler(mgr.GetClient(), mgr.GetConfig()) + if err != nil { + return err + } genericActuator, err := genericactuator.NewActuator(mgr, stackit.Name, secretConfigsFunc, shootAccessSecretsFunc, configChart, controlPlaneChart, controlPlaneShootChart, controlPlaneShootCRDsChart, storageClassChart, - NewValuesProvider(mgr, DeployALBIngressController, opts.CustomLabelDomain), extensionscontroller.ChartRendererFactoryFunc(util.NewChartRendererForShoot), + NewValuesProvider(mgr, DeployALBIngressController, opts.CustomLabelDomain, csiCompatibilityHandler), + extensionscontroller.ChartRendererFactoryFunc(util.NewChartRendererForShoot), imagevector.ImageVector(), "", nil, opts.WebhookServerNamespace) if err != nil { return err diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go new file mode 100644 index 00000000..db59f27a --- /dev/null +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -0,0 +1,187 @@ +package controlplane + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/gardener/gardener/pkg/chartrenderer" + gardenerutils "github.com/gardener/gardener/pkg/utils" + imagevectorutils "github.com/gardener/gardener/pkg/utils/imagevector" + "github.com/gardener/gardener/pkg/utils/managedresources" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/charts" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/imagevector" + stackitv1alpha1 "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/apis/stackit/v1alpha1" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/openstack" +) + +const ( + csiDriverChartName = "stackit-blockstorage-csi-driver" + csiCompatibilityPrefix = "stackit-compatibility" + csiCompatSeedChartName = csiCompatibilityPrefix + "-chart" + csiCompatShootChartName = csiCompatibilityPrefix + "-shoot-chart" +) + +func NewCompatCSICompatibilityHandler(client client.Client, config *rest.Config) (*CompatCSICompatibilityHandler, error) { + renderer, err := chartrenderer.NewForConfig(config) + if err != nil { + return nil, err + } + return &CompatCSICompatibilityHandler{ + client: client, + renderer: renderer, + }, nil +} + +type CompatCSICompatibilityHandler struct { + client client.Client + renderer chartrenderer.Interface +} + +func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context.Context, namespace string, version string, cpConfig *stackitv1alpha1.ControlPlaneConfig, controlPlaneValues map[string]any) error { + compatibilityMode := getCSICompatibilityMode(cpConfig) + switch compatibilityMode { + case stackitv1alpha1.COMPAT, stackitv1alpha1.COMPATBLOCK: + blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK + chart, err := ch.renderSeedCSICompatibilityMode(controlPlaneValues, namespace, version, blockLegacyCreation) + if err != nil { + return fmt.Errorf("failed to render seed CSI compatibility mode: %w", err) + } + err = ch.deploySeedCSICompatibilityMode(ctx, namespace, chart) + if err != nil { + return fmt.Errorf("failed to deploy seed CSI compatibility mode: %w", err) + } + default: + err := ch.deleteSeedCSICompatibilityMode(ctx, namespace) + if err != nil { + return fmt.Errorf("failed to deploy seed CSI compatibility mode: %w", err) + } + } + return nil +} + +func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values map[string]any, namespace string, version string, blockLegacyCreation bool) (*chartrenderer.RenderedChart, error) { + chartValues := composeCompatibilityChartValues(values) + + // Override chart values + chartValues["prefix"] = csiCompatibilityPrefix + + imageMap, err := findImages(version, "csi-driver-stackit", "csi-provisioner", "csi-attacher", "csi-snapshotter", "csi-resizer", "csi-liveness-probe", "csi-snapshot-controller") + if err != nil { + return nil, err + } + chartValues["images"] = imageMap + + chartValues["healthzPort"] = 9809 + csiValues := map[string]any{ + "enableCompatibilityMode": true, + } + if blockLegacyCreation { + csiValues["blockLegacyCreation"] = true + } + chartValues["csi"] = csiValues + + return ch.renderer.RenderEmbeddedFS( + charts.InternalChart, + filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), + csiDriverChartName, + namespace, + chartValues, + ) +} + +func (ch *CompatCSICompatibilityHandler) deploySeedCSICompatibilityMode(ctx context.Context, namespace string, renderedChart *chartrenderer.RenderedChart) error { + data := renderedChart.AsSecretData() + return managedresources.CreateForSeed(ctx, ch.client, namespace, csiCompatSeedChartName, false, data) +} + +func (ch *CompatCSICompatibilityHandler) deleteSeedCSICompatibilityMode(ctx context.Context, namespace string) error { + return client.IgnoreNotFound(managedresources.DeleteForSeed(ctx, ch.client, namespace, csiCompatSeedChartName)) +} + +func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context.Context, namespace string, version string, cpConfig *stackitv1alpha1.ControlPlaneConfig, values map[string]any) error { + compatibilityMode := getCSICompatibilityMode(cpConfig) + switch compatibilityMode { + case stackitv1alpha1.COMPAT, stackitv1alpha1.COMPATBLOCK: + chart, err := ch.renderShootCSICompatibilityMode(values, version) + if err != nil { + return fmt.Errorf("render shoot CSI compatibility mode: %w", err) + } + err = ch.deployShootCSICompatibilityMode(ctx, namespace, chart) + if err != nil { + return fmt.Errorf("deploy shoot CSI compatibility mode: %w", err) + } + default: + err := ch.deleteShootCSICompatibilityMode(ctx, namespace) + if err != nil { + return fmt.Errorf("delete shoot CSI compatibility mode: %w", err) + } + } + return nil +} + +func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values map[string]any, version string) (*chartrenderer.RenderedChart, error) { + chartValues := composeCompatibilityChartValues(values) + + // Override chart values + chartValues["prefix"] = csiCompatibilityPrefix + + imageMap, err := findImages(version, "csi-driver-stackit", "csi-node-driver-registrar", "csi-liveness-probe") + if err != nil { + return nil, err + } + chartValues["images"] = imageMap + + chartValues["healthzPort"] = 9909 + chartValues["driverName"] = openstack.CSIStorageProvisioner + csiValues := map[string]any{ + "enableCompatibilityMode": true, + } + chartValues["csi"] = csiValues + + return ch.renderer.RenderEmbeddedFS( + charts.InternalChart, + filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), + csiDriverChartName, + "kube-system", + chartValues, + ) +} + +func (ch *CompatCSICompatibilityHandler) deployShootCSICompatibilityMode(ctx context.Context, namespace string, renderedChart *chartrenderer.RenderedChart) error { + data := renderedChart.AsSecretData() + return managedresources.CreateForShoot(ctx, ch.client, namespace, csiCompatShootChartName, "gardener-extension-provider-stackit", false, data) +} + +func (ch *CompatCSICompatibilityHandler) deleteShootCSICompatibilityMode(ctx context.Context, namespace string) error { + return client.IgnoreNotFound(managedresources.DeleteForShoot(ctx, ch.client, namespace, csiCompatShootChartName)) +} + +// composeCompatibilityChartValues returns a copy of the given values map merged with the csiStackitValues on topLevel. +// Basically removes the openstack.CSISTACKITControllerName key +func composeCompatibilityChartValues(values map[string]any) map[string]any { + if values == nil { + return map[string]any{} + } + csiStackitValues, ok := values[openstack.CSISTACKITControllerName].(map[string]any) + if !ok { + csiStackitValues = nil + } + return gardenerutils.MergeMaps(values, csiStackitValues) +} + +func findImages(version string, imagesToFind ...string) (map[string]any, error) { + images := imagevector.ImageVector() + result := make(map[string]any) + for _, image := range imagesToFind { + foundImage, err := images.FindImage(image, imagevectorutils.TargetVersion(version)) + if err != nil { + return nil, err + } + result[image] = foundImage.String() + } + return result, nil +} diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go new file mode 100644 index 00000000..253d6adb --- /dev/null +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -0,0 +1,474 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package controlplane + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strings" + + resourcesv1alpha1 "github.com/gardener/gardener/pkg/apis/resources/v1alpha1" + "github.com/gardener/gardener/pkg/client/kubernetes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + 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/types" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/yaml" + + stackitv1alpha1 "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/apis/stackit/v1alpha1" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/openstack" +) + +type mockRoundTripper struct{} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + var body string + switch req.URL.Path { + case "/version": + body = `{"major":"1","minor":"29","gitVersion":"v1.29.0"}` + case "/api": + body = `{"kind":"APIVersions","versions":["v1"]}` + case "/apis": + body = `{"kind":"APIGroupList","groups":[]}` + default: + body = `{"kind":"Status","status":"Failure","message":"Not Found","reason":"NotFound","code":404}` + } + + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte(body))), + }, nil +} + +var _ = Describe("CompatCSICompatibilityHandler", func() { + var ( + ctx context.Context + fakeClient client.Client + handler *CompatCSICompatibilityHandler + namespace string + config *rest.Config + shootVersion string + controlPlaneValues map[string]any + ) + + BeforeEach(func() { + ctx = context.Background() + namespace = "test-namespace" + shootVersion = "1.33.5" + + fakeClient = fakeclient.NewClientBuilder(). + WithScheme(kubernetes.SeedScheme). + Build() + + config = &rest.Config{ + Host: "https://localhost", + Transport: &mockRoundTripper{}, + } + + handler, _ = NewCompatCSICompatibilityHandler(fakeClient, config) + + controlPlaneValues = map[string]any{ + "global": map[string]any{ + "genericTokenKubeconfigSecretName": "generic-token-kubeconfig-92e9ae14", + }, + openstack.CSISTACKITControllerName: map[string]any{ + "foo": "bar", + }, + } + }) + + Describe("#HandleSeedCSICompatibility", func() { + Context("when CSICompatibilityMode is DEFAULT", func() { + It("should delete the managed resource", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + CompatibilityMode: string(stackitv1alpha1.DEFAULT), + }, + }, + } + + // Create the managed resource and secret beforehand to ensure deletion works + mr := &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: csiCompatSeedChartName, + Namespace: namespace, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "managedresource-" + csiCompatSeedChartName, + Namespace: namespace, + }, + } + Expect(fakeClient.Create(ctx, mr)).To(Succeed()) + Expect(fakeClient.Create(ctx, secret)).To(Succeed()) + + err := handler.HandleSeedCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + // Check deletion + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatSeedChartName, Namespace: namespace}, mr) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + }) + + Context("when CSICompatibilityMode is not set", func() { + It("should delete the managed resource", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + CompatibilityMode: "", + }, + }, + } + + // Create the managed resource and secret beforehand to ensure deletion works + mr := &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: csiCompatSeedChartName, + Namespace: namespace, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "managedresource-" + csiCompatSeedChartName, + Namespace: namespace, + }, + } + Expect(fakeClient.Create(ctx, mr)).To(Succeed()) + Expect(fakeClient.Create(ctx, secret)).To(Succeed()) + + err := handler.HandleSeedCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + // Check deletion + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatSeedChartName, Namespace: namespace}, mr) + Expect(err).To(HaveOccurred()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + }) + + Context("when CSICompatibilityMode is COMPAT", func() { + It("should deploy the seed csi compatibility mode", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + CompatibilityMode: string(stackitv1alpha1.COMPAT), + }, + }, + } + + err := handler.HandleSeedCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + mr := &resourcesv1alpha1.ManagedResource{} + err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-compatibility-chart", Namespace: namespace}, mr) + Expect(err).NotTo(HaveOccurred()) + + deployment := getDeploymentFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-compatibility-csi-driver-controller") + csiContainer, found := findContainerInPod(&deployment.Spec.Template.Spec, "stackit-csi-driver") + Expect(found).To(BeTrue(), "stackit-csi-driver container not found") + Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) + Expect(csiContainer.Args).NotTo(ContainElement("--legacy-volume-creation=false")) + + livenessContainer, found := findContainerInPod(&deployment.Spec.Template.Spec, "stackit-csi-liveness-probe") + Expect(found).To(BeTrue(), "stackit-csi-liveness-probe container not found") + Expect(livenessContainer.Args).To(ContainElement("--health-port=9809")) + + Expect(deploymentExistsInSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-compatibility-csi-snapshot-controller")).To(BeFalse()) + }) + }) + + Context("when CSICompatibilityMode is COMPATBLOCK", func() { + It("should deploy the seed csi compatibility mode", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + CompatibilityMode: string(stackitv1alpha1.COMPATBLOCK), + }, + }, + } + + err := handler.HandleSeedCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + mr := &resourcesv1alpha1.ManagedResource{} + err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-compatibility-chart", Namespace: namespace}, mr) + Expect(err).NotTo(HaveOccurred()) + + deployment := getDeploymentFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-compatibility-csi-driver-controller") + + csiContainer, found := findContainerInPod(&deployment.Spec.Template.Spec, "stackit-csi-driver") + Expect(found).To(BeTrue(), "stackit-csi-driver container not found") + Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) + Expect(csiContainer.Args).To(ContainElement("--legacy-volume-creation=false")) + + livenessContainer, found := findContainerInPod(&deployment.Spec.Template.Spec, "stackit-csi-liveness-probe") + Expect(found).To(BeTrue(), "stackit-csi-liveness-probe container not found") + Expect(livenessContainer.Args).To(ContainElement("--health-port=9809")) + + Expect(deploymentExistsInSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-compatibility-csi-snapshot-controller")).To(BeFalse()) + }) + }) + }) + + Describe("#HandleShootCSICompatibility", func() { + Context("when CSICompatibilityMode is DEFAULT", func() { + It("should delete the managed resource", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + CompatibilityMode: string(stackitv1alpha1.DEFAULT), + }, + }, + } + + // Create the managed resource and secret beforehand to ensure deletion works + mr := &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: csiCompatShootChartName, + Namespace: namespace, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "managedresource-" + csiCompatShootChartName, + Namespace: namespace, + }, + } + Expect(fakeClient.Create(ctx, mr)).To(Succeed()) + Expect(fakeClient.Create(ctx, secret)).To(Succeed()) + + err := handler.HandleShootCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + // Check deletion + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatShootChartName, Namespace: namespace}, mr) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + }) + + Context("when CSICompatibilityMode is not set", func() { + It("should delete the managed resource", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + CompatibilityMode: "", + }, + }, + } + + // Create the managed resource and secret beforehand to ensure deletion works + mr := &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: csiCompatShootChartName, + Namespace: namespace, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "managedresource-" + csiCompatShootChartName, + Namespace: namespace, + }, + } + Expect(fakeClient.Create(ctx, mr)).To(Succeed()) + Expect(fakeClient.Create(ctx, secret)).To(Succeed()) + + err := handler.HandleShootCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + // Check deletion + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatShootChartName, Namespace: namespace}, mr) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) + }) + }) + + Context("when CSICompatibilityMode is COMPAT", func() { + It("should deploy the shoot csi compatibility mode with blockLegacyCreation = false", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + CompatibilityMode: string(stackitv1alpha1.COMPAT), + }, + }, + } + + err := handler.HandleShootCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + mr := &resourcesv1alpha1.ManagedResource{} + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatShootChartName, Namespace: namespace}, mr) + Expect(err).NotTo(HaveOccurred()) + + ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatShootChartName) + + csiContainer, found := findContainerInPod(&ds.Spec.Template.Spec, "csi-driver-stackit") + Expect(found).To(BeTrue(), "csi-driver-stackit container not found") + Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) + Expect(csiContainer.Args).NotTo(ContainElement("--legacy-volume-creation=false")) + + livenessContainer, found := findContainerInPod(&ds.Spec.Template.Spec, "csi-liveness-probe") + Expect(found).To(BeTrue(), "csi-liveness-probe container not found") + Expect(livenessContainer.Args).To(ContainElement("--health-port=9909")) + + Expect(ds.Spec.Template.Annotations).To(HaveKeyWithValue("node.gardener.cloud/wait-for-csi-node-openstack", openstack.CSIStorageProvisioner)) + Expect(ds.Spec.Template.Annotations).NotTo(HaveKey("node.gardener.cloud/wait-for-csi-node-stackit")) + }) + }) + + Context("when CSICompatibilityMode is COMPATBLOCK", func() { + It("should deploy the shoot csi compatibility mode with blockLegacyCreation = true", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + CompatibilityMode: string(stackitv1alpha1.COMPATBLOCK), + }, + }, + } + + values := map[string]any{ + "global": map[string]any{ + "genericTokenKubeconfigSecretName": "generic-token-kubeconfig-92e9ae14", + }, + openstack.CSISTACKITControllerName: map[string]any{ + "foo": "bar", + }, + } + + err := handler.HandleShootCSICompatibility(ctx, namespace, shootVersion, cpConfig, values) + Expect(err).NotTo(HaveOccurred()) + + mr := &resourcesv1alpha1.ManagedResource{} + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatShootChartName, Namespace: namespace}, mr) + Expect(err).NotTo(HaveOccurred()) + + ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatShootChartName) + + csiContainer, found := findContainerInPod(&ds.Spec.Template.Spec, "csi-driver-stackit") + Expect(found).To(BeTrue(), "csi-driver-stackit container not found") + Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) + + livenessContainer, found := findContainerInPod(&ds.Spec.Template.Spec, "csi-liveness-probe") + Expect(found).To(BeTrue(), "csi-liveness-probe container not found") + Expect(livenessContainer.Args).To(ContainElement("--health-port=9909")) + + Expect(ds.Spec.Template.Annotations).To(HaveKeyWithValue("node.gardener.cloud/wait-for-csi-node-openstack", openstack.CSIStorageProvisioner)) + Expect(ds.Spec.Template.Annotations).NotTo(HaveKey("node.gardener.cloud/wait-for-csi-node-stackit")) + }) + }) + }) +}) + +func getDaemonSetFromSecret(ctx context.Context, fakeClient client.Client, namespace string, secretNamePrefix string) *appsv1.DaemonSet { + GinkgoHelper() + secretList := &corev1.SecretList{} + Expect(fakeClient.List(ctx, secretList, client.InNamespace(namespace))).To(Succeed()) + var matchedSecret *corev1.Secret + var names []string + for _, s := range secretList.Items { + names = append(names, s.Name) + if strings.HasPrefix(s.Name, secretNamePrefix) { + matchedSecret = &s + break + } + } + + if matchedSecret == nil { + Fail(fmt.Sprintf("Secret starting with prefix %s not found. Found secrets: %v", secretNamePrefix, names)) + return nil // will never be reached, but makes the linter very happy + } + + for _, data := range matchedSecret.Data { + docs := bytes.SplitSeq(data, []byte("\n---")) + for doc := range docs { + if bytes.Contains(doc, []byte("kind: DaemonSet")) { + ds := &appsv1.DaemonSet{} + Expect(yaml.Unmarshal(doc, ds)).To(Succeed()) + return ds + } + } + } + Fail("DaemonSet not found in secret " + matchedSecret.Name) + return nil +} + +func getDeploymentFromSecret(ctx context.Context, fakeClient client.Client, namespace string, secretName string, deploymentName string) *appsv1.Deployment { + GinkgoHelper() + secretList := &corev1.SecretList{} + Expect(fakeClient.List(ctx, secretList, client.InNamespace(namespace))).To(Succeed()) + var matchedSecret *corev1.Secret + var names []string + for _, s := range secretList.Items { + names = append(names, s.Name) + if strings.HasPrefix(s.Name, secretName) { + matchedSecret = &s + break + } + } + + if matchedSecret == nil { + Fail(fmt.Sprintf("Secret starting with prefix %s not found. Found secrets: %v", secretName, names)) + return nil // will never be reached, but makes the linter very happy + } + + for _, data := range matchedSecret.Data { + docs := bytes.SplitSeq(data, []byte("\n---")) + for doc := range docs { + if bytes.Contains(doc, []byte("kind: Deployment")) { + deployment := &appsv1.Deployment{} + Expect(yaml.Unmarshal(doc, deployment)).To(Succeed()) + if deployment.Name == deploymentName { + return deployment + } + } + } + } + Fail(fmt.Sprintf("Deployment %s not found in secret %s", deploymentName, matchedSecret.Name)) + return nil // will never be reached, but makes the linter very happy +} + +func deploymentExistsInSecret(ctx context.Context, fakeClient client.Client, namespace string, secretNamePrefix string, deploymentName string) bool { + GinkgoHelper() + secretList := &corev1.SecretList{} + Expect(fakeClient.List(ctx, secretList, client.InNamespace(namespace))).To(Succeed()) + for _, s := range secretList.Items { + if !strings.HasPrefix(s.Name, secretNamePrefix) { + continue + } + for _, data := range s.Data { + for doc := range bytes.SplitSeq(data, []byte("\n---")) { + if bytes.Contains(doc, []byte("kind: Deployment")) { + deployment := &appsv1.Deployment{} + Expect(yaml.Unmarshal(doc, deployment)).To(Succeed()) + if deployment.Name == deploymentName { + return true + } + } + } + } + } + return false +} + +func findContainerInPod(podSpec *corev1.PodSpec, name string) (*corev1.Container, bool) { + for i := range podSpec.Containers { + if podSpec.Containers[i].Name == name { + return &podSpec.Containers[i], true + } + } + return nil, false +} diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 180ac234..9f97ee88 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -43,6 +43,7 @@ import ( "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" vpaautoscalingv1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + "k8s.io/client-go/rest" "k8s.io/utils/ptr" k8sclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -355,13 +356,19 @@ var ( } ) +type CSICompatibilityHandler interface { + HandleSeedCSICompatibility(context.Context, string, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error + HandleShootCSICompatibility(context.Context, string, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error +} + // NewValuesProvider creates a new ValuesProvider for the generic actuator. -func NewValuesProvider(mgr manager.Manager, deployALBIngressController bool, customLabelDomain string) genericactuator.ValuesProvider { +func NewValuesProvider(mgr manager.Manager, deployALBIngressController bool, customLabelDomain string, csiCompatibilityHandler CSICompatibilityHandler) genericactuator.ValuesProvider { return &valuesProvider{ client: mgr.GetClient(), decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(), deployALBIngressController: deployALBIngressController, customLabelDomain: customLabelDomain, + csiCompatibilityHandler: csiCompatibilityHandler, } } @@ -369,9 +376,11 @@ func NewValuesProvider(mgr manager.Manager, deployALBIngressController bool, cus type valuesProvider struct { genericactuator.NoopValuesProvider client k8sclient.Client + config *rest.Config decoder runtime.Decoder deployALBIngressController bool customLabelDomain string + csiCompatibilityHandler CSICompatibilityHandler } // GetConfigChartValues returns the values for the config chart applied by the generic actuator. @@ -736,6 +745,15 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf return nil, err } + maps.Copy(controlPlaneValues, map[string]any{ + "global": map[string]any{ + "genericTokenKubeconfigSecretName": extensionscontroller.GenericTokenKubeconfigSecretNameFromCluster(cluster), + }, + openstack.CloudControllerManagerName: ccm, + openstack.STACKITCloudControllerManagerName: stackitccm, + stackit.PodIdentityWebhookName: podIdentityWebhook, + }) + storageCSIDriver := getCSIDriver(cpConfig) switch storageCSIDriver { case stackitv1alpha1.OPENSTACK: @@ -750,19 +768,14 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf controlPlaneValues[openstack.CSIControllerName] = map[string]any{ "enabled": false, } + err := vp.csiCompatibilityHandler.HandleSeedCSICompatibility(ctx, cp.Namespace, cluster.Shoot.Spec.Kubernetes.Version, cpConfig, controlPlaneValues) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("unsupported storage CSI Driver: %s", storageCSIDriver) } - maps.Copy(controlPlaneValues, map[string]any{ - "global": map[string]any{ - "genericTokenKubeconfigSecretName": extensionscontroller.GenericTokenKubeconfigSecretNameFromCluster(cluster), - }, - openstack.CloudControllerManagerName: ccm, - openstack.STACKITCloudControllerManagerName: stackitccm, - stackit.PodIdentityWebhookName: podIdentityWebhook, - }) - if vp.deployALBIngressController { fmt.Println("deploying ALB Ingress Controller") albcm, err := getSTACKITALBCMChartValues(cpConfig, cluster, infra, stackitCredentialsConfig, apiEndpoints, scaledDown, stackitRegion) @@ -1070,12 +1083,16 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c csiDriverInUse := getCSIDriver(cpConfig) switch csiDriverInUse { - case stackitv1alpha1.STACKIT: - values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues - values[openstack.CSINodeName] = map[string]any{"enabled": false} case stackitv1alpha1.OPENSTACK: values[openstack.CSINodeName] = csiNodeDriverValues values[openstack.CSISTACKITNodeName] = map[string]any{"enabled": false} + case stackitv1alpha1.STACKIT: + values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues + values[openstack.CSINodeName] = map[string]any{"enabled": false} + err := vp.csiCompatibilityHandler.HandleShootCSICompatibility(ctx, cp.Namespace, cluster.Shoot.Spec.Kubernetes.Version, cpConfig, values) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("unsupported CSI driver type: %s", csiDriverInUse) } @@ -1216,6 +1233,10 @@ func getCSIDriver(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1. return stackitv1alpha1.ControllerName(cpConfig.Storage.CSI.Name) } +func getCSICompatibilityMode(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1.CSICompatibilityMode { + return stackitv1alpha1.CSICompatibilityMode(cpConfig.Storage.CSI.CompatibilityMode) +} + func getCCMController(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1.ControllerName { return stackitv1alpha1.ControllerName(cpConfig.CloudControllerManager.Name) } diff --git a/pkg/controller/controlplane/valuesprovider_test.go b/pkg/controller/controlplane/valuesprovider_test.go index d7da5fb4..336176cd 100644 --- a/pkg/controller/controlplane/valuesprovider_test.go +++ b/pkg/controller/controlplane/valuesprovider_test.go @@ -313,7 +313,7 @@ var _ = Describe("ValuesProvider", func() { mgr = &testutils.FakeManager{Scheme: scheme, Client: c} - vp = NewValuesProvider(mgr, true, "kubernetes.io") + vp = NewValuesProvider(mgr, true, "kubernetes.io", new(noopCSICompatibilityHandler)) }) AfterEach(func() { @@ -665,7 +665,7 @@ var _ = Describe("ValuesProvider", func() { stackitCCMDeletion(ctx, c) } - vpStackitConf := NewValuesProvider(mgr, true, "kubernetes.io") + vpStackitConf := NewValuesProvider(mgr, true, "kubernetes.io", new(noopCSICompatibilityHandler)) values, err := vpStackitConf.GetControlPlaneChartValues(ctx, cp, &testCluster, fakeSecretsManager, checksums, false) Expect(err).NotTo(HaveOccurred()) Expect(values).To(HaveKey(openstack.STACKITCloudControllerManagerName)) @@ -777,7 +777,7 @@ var _ = Describe("ValuesProvider", func() { } testCluster.CloudProfile = cloudProfile - vpCustomDomain := NewValuesProvider(mgr, true, customDomain) + vpCustomDomain := NewValuesProvider(mgr, true, customDomain, new(noopCSICompatibilityHandler)) values, err := vpCustomDomain.GetControlPlaneChartValues(ctx, cp, &testCluster, fakeSecretsManager, checksums, false) Expect(err).NotTo(HaveOccurred()) @@ -1142,3 +1142,12 @@ func stackitCCMDeletion(ctx context.Context, c *mockclient.MockClient) { c.EXPECT().Delete(ctx, &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{Name: openstack.STACKITCloudControllerManagerName, Namespace: namespace}}) c.EXPECT().Delete(ctx, &vpaautoscalingv1.VerticalPodAutoscaler{ObjectMeta: metav1.ObjectMeta{Name: openstack.STACKITCloudControllerManagerName + "-vpa", Namespace: namespace}}) } + +type noopCSICompatibilityHandler struct{} + +func (*noopCSICompatibilityHandler) HandleSeedCSICompatibility(context.Context, string, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { + return nil +} +func (*noopCSICompatibilityHandler) HandleShootCSICompatibility(context.Context, string, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { + return nil +} diff --git a/test/integration/controlplane/controlplane_suite_test.go b/test/integration/controlplane/controlplane_suite_test.go new file mode 100644 index 00000000..bae167c5 --- /dev/null +++ b/test/integration/controlplane/controlplane_suite_test.go @@ -0,0 +1,23 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package controlplane + +import ( + "flag" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func init() { + // Accepted for compatibility with the shared make test-integration invocation. + flag.String("region", "eu01", "Region") +} + +func TestControlPlane(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "ControlPlane Controller Suite") +} diff --git a/test/integration/controlplane/controlplane_test.go b/test/integration/controlplane/controlplane_test.go new file mode 100644 index 00000000..1be7b533 --- /dev/null +++ b/test/integration/controlplane/controlplane_test.go @@ -0,0 +1,396 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package controlplane + +import ( + "bytes" + "context" + "encoding/json" + "path/filepath" + "time" + + gardenerv1beta1 "github.com/gardener/gardener/pkg/apis/core/v1beta1" + extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" + resourcesv1alpha1 "github.com/gardener/gardener/pkg/apis/resources/v1alpha1" + "github.com/gardener/gardener/pkg/client/kubernetes" + "github.com/gardener/gardener/pkg/logger" + gardenerenvtest "github.com/gardener/gardener/test/envtest" + . "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/labels" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" + runtimejson "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/uuid" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + "sigs.k8s.io/controller-runtime/pkg/manager" + metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" + + stackitv1alpha1 "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/apis/stackit/v1alpha1" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/controller/controlplane" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/openstack" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/stackit" +) + +var ( + ctx = context.Background() + + testEnv *gardenerenvtest.GardenerTestEnvironment + mgrCancel context.CancelFunc + k8sclient client.Client + encoder runtime.Encoder + + testID = string(uuid.NewUUID()) +) + +var _ = BeforeSuite(func() { + logf.SetLogger(logger.MustNewZapLogger(logger.DebugLevel, logger.FormatJSON, zap.WriteTo(GinkgoWriter))) + + repoRoot := filepath.Join("..", "..", "..") + + DeferCleanup(func() { + By("stopping manager") + mgrCancel() + + By("stopping test environment") + Expect(testEnv.Stop()).To(Succeed()) + }) + + By("starting test environment") + testEnv = &gardenerenvtest.GardenerTestEnvironment{ + Environment: &envtest.Environment{ + CRDInstallOptions: envtest.CRDInstallOptions{ + Paths: []string{ + filepath.Join(repoRoot, "test", "integration", "testdata", "upstream-crds", "10-crd-extensions.gardener.cloud_clusters.yaml"), + filepath.Join(repoRoot, "test", "integration", "testdata", "upstream-crds", "10-crd-extensions.gardener.cloud_controlplanes.yaml"), + filepath.Join(repoRoot, "test", "integration", "testdata", "upstream-crds", "10-crd-resources.gardener.cloud_managedresources.yaml"), + filepath.Join(repoRoot, "test", "integration", "testdata", "upstream-crds", "10-crd-autoscaling.k8s.io_verticalpodautoscalers.yaml"), + }, + }, + }, + GardenerAPIServer: &gardenerenvtest.GardenerAPIServer{}, + } + + restConfig, err := testEnv.Start() + Expect(err).ToNot(HaveOccurred()) + Expect(restConfig).ToNot(BeNil()) + + scheme := runtime.NewScheme() + Expect(kubernetes.AddSeedSchemeToScheme(scheme)).To(Succeed()) + Expect(stackitv1alpha1.AddToScheme(scheme)).To(Succeed()) + Expect(gardenerv1beta1.AddToScheme(scheme)).To(Succeed()) + + By("setup manager") + mgr, err := manager.New(restConfig, manager.Options{ + Scheme: scheme, + Metrics: metricsserver.Options{ + BindAddress: "0", + }, + Cache: cache.Options{ + ByObject: map[client.Object]cache.ByObject{ + &extensionsv1alpha1.ControlPlane{}: { + Label: labels.SelectorFromSet(labels.Set{"test-id": testID}), + }, + }, + }, + }) + Expect(err).NotTo(HaveOccurred()) + + Expect(controlplane.AddToManagerWithOptions(ctx, mgr, controlplane.AddOptions{ + Controller: controller.Options{ + MaxConcurrentReconciles: 1, + }, + })).To(Succeed()) + + var mgrCtx context.Context + mgrCtx, mgrCancel = context.WithCancel(ctx) + + By("start manager") + go func() { + defer GinkgoRecover() + Expect(mgr.Start(mgrCtx)).To(Succeed()) + }() + + k8sclient = mgr.GetClient() + + gv := schema.GroupVersions{ + stackitv1alpha1.SchemeGroupVersion, + gardenerv1beta1.SchemeGroupVersion, + } + encoder = serializer.NewCodecFactory(scheme).EncoderForVersion(&runtimejson.Serializer{}, gv) +}) + +var _ = Describe("ControlPlane CSI compatibility mode", func() { + var namespace string + + BeforeEach(func() { + namespace = "shoot--test--" + testID[:8] + + By("create namespace") + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: namespace}} + Expect(k8sclient.Create(ctx, ns)).To(Succeed()) + DeferCleanup(func() { Expect(client.IgnoreNotFound(k8sclient.Delete(ctx, ns))).To(Succeed()) }) + + By("create cluster") + shoot := &gardenerv1beta1.Shoot{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "core.gardener.cloud/v1beta1", + Kind: "Shoot", + }, + ObjectMeta: metav1.ObjectMeta{Name: "test"}, + Spec: gardenerv1beta1.ShootSpec{ + Kubernetes: gardenerv1beta1.Kubernetes{Version: "1.33.5"}, + }, + Status: gardenerv1beta1.ShootStatus{TechnicalID: namespace}, + } + shootBytes := new(bytes.Buffer) + Expect(encoder.Encode(shoot, shootBytes)).To(Succeed()) + + cluster := &extensionsv1alpha1.Cluster{ + ObjectMeta: metav1.ObjectMeta{Name: namespace}, + Spec: extensionsv1alpha1.ClusterSpec{ + CloudProfile: runtime.RawExtension{Raw: []byte(`{}`)}, + Seed: runtime.RawExtension{Raw: []byte(`{}`)}, + Shoot: runtime.RawExtension{Raw: shootBytes.Bytes()}, + }, + } + Expect(k8sclient.Create(ctx, cluster)).To(Succeed()) + DeferCleanup(func() { Expect(client.IgnoreNotFound(k8sclient.Delete(ctx, cluster))).To(Succeed()) }) + + By("create cloudprovider secret") + cloudproviderSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: "cloudprovider", Namespace: namespace}, + Data: map[string][]byte{ + stackit.ProjectID: []byte("test-project-id"), + stackit.SaKeyJSON: []byte(`{"key":"value"}`), + }, + } + Expect(k8sclient.Create(ctx, cloudproviderSecret)).To(Succeed()) + DeferCleanup(func() { Expect(client.IgnoreNotFound(k8sclient.Delete(ctx, cloudproviderSecret))).To(Succeed()) }) + + By("create cloud-provider-config secret") + cpConfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: openstack.CloudProviderConfigName, Namespace: namespace}, + Data: map[string][]byte{"config": []byte("placeholder")}, + } + Expect(k8sclient.Create(ctx, cpConfigSecret)).To(Succeed()) + DeferCleanup(func() { Expect(client.IgnoreNotFound(k8sclient.Delete(ctx, cpConfigSecret))).To(Succeed()) }) + + By("create cloud-provider-disk-config-csi secret") + diskConfigSecret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{Name: openstack.CloudProviderCSIDiskConfigName, Namespace: namespace}, + Data: map[string][]byte{"config": []byte("placeholder")}, + } + Expect(k8sclient.Create(ctx, diskConfigSecret)).To(Succeed()) + DeferCleanup(func() { Expect(client.IgnoreNotFound(k8sclient.Delete(ctx, diskConfigSecret))).To(Succeed()) }) + }) + + It("should create both ManagedResources when compatibilityMode=compat", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: stackitv1alpha1.SchemeGroupVersion.String(), + Kind: "ControlPlaneConfig", + }, + CloudControllerManager: &stackitv1alpha1.CloudControllerManagerConfig{ + Name: string(stackitv1alpha1.STACKIT), + }, + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + Name: string(stackitv1alpha1.STACKIT), + CompatibilityMode: string(stackitv1alpha1.COMPAT), + }, + }, + } + cpConfigBytes, err := json.Marshal(cpConfig) + Expect(err).NotTo(HaveOccurred()) + + infraStatus := &stackitv1alpha1.InfrastructureStatus{ + TypeMeta: metav1.TypeMeta{ + APIVersion: stackitv1alpha1.SchemeGroupVersion.String(), + Kind: "InfrastructureStatus", + }, + Networks: stackitv1alpha1.NetworkStatus{ + ID: "test-network-id", + Name: "test-network-name", + Router: stackitv1alpha1.RouterStatus{ + ID: "test-router-id", + }, + }, + Node: stackitv1alpha1.NodeStatus{KeyName: "test-key"}, + } + infraStatusBytes, err := json.Marshal(infraStatus) + Expect(err).NotTo(HaveOccurred()) + + By("create ControlPlane CR") + cp := &extensionsv1alpha1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "controlplane", + Namespace: namespace, + Labels: map[string]string{"test-id": testID}, + }, + Spec: extensionsv1alpha1.ControlPlaneSpec{ + DefaultSpec: extensionsv1alpha1.DefaultSpec{ + Type: stackit.Type, + ProviderConfig: &runtime.RawExtension{Raw: cpConfigBytes}, + }, + Region: "eu01", + SecretRef: corev1.SecretReference{ + Name: "cloudprovider", + Namespace: namespace, + }, + InfrastructureProviderStatus: &runtime.RawExtension{Raw: infraStatusBytes}, + }, + } + Expect(k8sclient.Create(ctx, cp)).To(Succeed()) + DeferCleanup(func() { Expect(client.IgnoreNotFound(k8sclient.Delete(ctx, cp))).To(Succeed()) }) + + By("wait for seed ManagedResource") + seedMR := &resourcesv1alpha1.ManagedResource{} + Eventually(func() error { + return k8sclient.Get(ctx, types.NamespacedName{ + Name: "stackit-compatibility-chart", + Namespace: namespace, + }, seedMR) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + + By("wait for shoot ManagedResource") + shootMR := &resourcesv1alpha1.ManagedResource{} + Eventually(func() error { + return k8sclient.Get(ctx, types.NamespacedName{ + Name: "stackit-compatibility-shoot-chart", + Namespace: namespace, + }, shootMR) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + }) + + It("should delete both ManagedResources when transitioning from compat to default", func() { + cpConfigCompat := &stackitv1alpha1.ControlPlaneConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: stackitv1alpha1.SchemeGroupVersion.String(), + Kind: "ControlPlaneConfig", + }, + CloudControllerManager: &stackitv1alpha1.CloudControllerManagerConfig{ + Name: string(stackitv1alpha1.STACKIT), + }, + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + Name: string(stackitv1alpha1.STACKIT), + CompatibilityMode: string(stackitv1alpha1.COMPAT), + }, + }, + } + cpConfigCompatBytes, err := json.Marshal(cpConfigCompat) + Expect(err).NotTo(HaveOccurred()) + + infraStatus := &stackitv1alpha1.InfrastructureStatus{ + TypeMeta: metav1.TypeMeta{ + APIVersion: stackitv1alpha1.SchemeGroupVersion.String(), + Kind: "InfrastructureStatus", + }, + Networks: stackitv1alpha1.NetworkStatus{ + ID: "test-network-id", + Name: "test-network-name", + Router: stackitv1alpha1.RouterStatus{ + ID: "test-router-id", + }, + }, + Node: stackitv1alpha1.NodeStatus{KeyName: "test-key"}, + } + infraStatusBytes, err := json.Marshal(infraStatus) + Expect(err).NotTo(HaveOccurred()) + + By("create ControlPlane CR with compat mode") + cp := &extensionsv1alpha1.ControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "controlplane", + Namespace: namespace, + Labels: map[string]string{"test-id": testID}, + }, + Spec: extensionsv1alpha1.ControlPlaneSpec{ + DefaultSpec: extensionsv1alpha1.DefaultSpec{ + Type: stackit.Type, + ProviderConfig: &runtime.RawExtension{Raw: cpConfigCompatBytes}, + }, + Region: "eu01", + SecretRef: corev1.SecretReference{ + Name: "cloudprovider", + Namespace: namespace, + }, + InfrastructureProviderStatus: &runtime.RawExtension{Raw: infraStatusBytes}, + }, + } + Expect(k8sclient.Create(ctx, cp)).To(Succeed()) + DeferCleanup(func() { Expect(client.IgnoreNotFound(k8sclient.Delete(ctx, cp))).To(Succeed()) }) + + By("wait for seed ManagedResource to be created") + Eventually(func() error { + return k8sclient.Get(ctx, types.NamespacedName{ + Name: "stackit-compatibility-chart", + Namespace: namespace, + }, &resourcesv1alpha1.ManagedResource{}) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + + By("wait for shoot ManagedResource to be created") + Eventually(func() error { + return k8sclient.Get(ctx, types.NamespacedName{ + Name: "stackit-compatibility-shoot-chart", + Namespace: namespace, + }, &resourcesv1alpha1.ManagedResource{}) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(Succeed()) + + By("update ControlPlane to default compatibility mode") + cpConfigDefault := &stackitv1alpha1.ControlPlaneConfig{ + TypeMeta: metav1.TypeMeta{ + APIVersion: stackitv1alpha1.SchemeGroupVersion.String(), + Kind: "ControlPlaneConfig", + }, + CloudControllerManager: &stackitv1alpha1.CloudControllerManagerConfig{ + Name: string(stackitv1alpha1.STACKIT), + }, + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + Name: string(stackitv1alpha1.STACKIT), + CompatibilityMode: string(stackitv1alpha1.DEFAULT), + }, + }, + } + cpConfigDefaultBytes, err := json.Marshal(cpConfigDefault) + Expect(err).NotTo(HaveOccurred()) + + By("trigger reconciliation") + Expect(k8sclient.Get(ctx, types.NamespacedName{Name: cp.Name, Namespace: cp.Namespace}, cp)).To(Succeed()) + cp.Spec.ProviderConfig = &runtime.RawExtension{Raw: cpConfigDefaultBytes} + metav1.SetMetaDataAnnotation(&cp.ObjectMeta, "gardener.cloud/operation", "reconcile") + Expect(k8sclient.Update(ctx, cp)).To(Succeed()) + + By("wait for seed ManagedResource to be deleted") + Eventually(func() bool { + err := k8sclient.Get(ctx, types.NamespacedName{ + Name: "stackit-compatibility-chart", + Namespace: namespace, + }, &resourcesv1alpha1.ManagedResource{}) + return apierrors.IsNotFound(err) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(BeTrue()) + + By("wait for shoot ManagedResource to be deleted") + Eventually(func() bool { + err := k8sclient.Get(ctx, types.NamespacedName{ + Name: "stackit-compatibility-shoot-chart", + Namespace: namespace, + }, &resourcesv1alpha1.ManagedResource{}) + return apierrors.IsNotFound(err) + }).WithTimeout(2 * time.Minute).WithPolling(2 * time.Second).Should(BeTrue()) + }) +}) diff --git a/test/integration/testdata/upstream-crds/10-crd-autoscaling.k8s.io_verticalpodautoscalers.yaml b/test/integration/testdata/upstream-crds/10-crd-autoscaling.k8s.io_verticalpodautoscalers.yaml new file mode 100644 index 00000000..bf6f0b3d --- /dev/null +++ b/test/integration/testdata/upstream-crds/10-crd-autoscaling.k8s.io_verticalpodautoscalers.yaml @@ -0,0 +1,630 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + api-approved.kubernetes.io: https://github.com/kubernetes/kubernetes/pull/63797 + controller-gen.kubebuilder.io/version: v0.20.1 + name: verticalpodautoscalers.autoscaling.k8s.io +spec: + group: autoscaling.k8s.io + names: + kind: VerticalPodAutoscaler + listKind: VerticalPodAutoscalerList + plural: verticalpodautoscalers + shortNames: + - vpa + singular: verticalpodautoscaler + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.updatePolicy.updateMode + name: Mode + type: string + - jsonPath: .status.recommendation.containerRecommendations[0].target.cpu + name: CPU + type: string + - jsonPath: .status.recommendation.containerRecommendations[0].target.memory + name: Mem + type: string + - jsonPath: .status.conditions[?(@.type=='RecommendationProvided')].status + name: Provided + type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1 + schema: + openAPIV3Schema: + description: |- + VerticalPodAutoscaler is the configuration for a vertical pod + autoscaler, which automatically manages pod resources based on historical and + real time resource utilization. + 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: |- + Specification of the behavior of the autoscaler. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status. + properties: + recommenders: + description: |- + Recommender responsible for generating recommendation for this object. + List should be empty (then the default recommender will generate the + recommendation) or contain exactly one recommender. + items: + description: |- + VerticalPodAutoscalerRecommenderSelector points to a specific Vertical Pod Autoscaler recommender. + In the future it might pass parameters to the recommender. + properties: + name: + description: Name of the recommender responsible for generating + recommendation for this object. + type: string + required: + - name + type: object + type: array + resourcePolicy: + description: |- + Controls how the autoscaler computes recommended resources. + The resource policy may be used to set constraints on the recommendations + for individual containers. + If any individual containers need to be excluded from getting the VPA recommendations, then + it must be disabled explicitly by setting mode to "Off" under containerPolicies. + If not specified, the autoscaler computes recommended resources for all containers in the pod, + without additional constraints. + properties: + containerPolicies: + description: Per-container resource policies. + items: + description: |- + ContainerResourcePolicy controls how autoscaler computes the recommended + resources for a specific container. + properties: + containerName: + description: |- + Name of the container or DefaultContainerResourcePolicy, in which + case the policy is used by the containers that don't have their own + policy specified. + type: string + controlledResources: + description: |- + Specifies the type of recommendations that will be computed + (and possibly applied) by VPA. + If not specified, the default of [ResourceCPU, ResourceMemory] will be used. + items: + description: ResourceName is the name identifying various + resources in a ResourceList. + type: string + type: array + controlledValues: + description: |- + Specifies which resource values should be controlled. + The default is "RequestsAndLimits". + enum: + - RequestsAndLimits + - RequestsOnly + type: string + maxAllowed: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Specifies the maximum amount of resources that will be recommended + for the container. The default is no maximum. + type: object + minAllowed: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Specifies the minimal amount of resources that will be recommended + for the container. The default is no minimum. + type: object + mode: + description: Whether autoscaler is enabled for the container. + The default is "Auto". + enum: + - Auto + - "Off" + type: string + oomBumpUpRatio: + anyOf: + - type: integer + - type: string + description: oomBumpUpRatio is the ratio to increase memory + when OOM is detected. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + oomMinBumpUp: + anyOf: + - type: integer + - type: string + description: oomMinBumpUp is the minimum increase in memory + when OOM is detected. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + type: array + type: object + targetRef: + description: |- + TargetRef points to the controller managing the set of pods for the + autoscaler to control - e.g. Deployment, StatefulSet. VerticalPodAutoscaler + can be targeted at controller implementing scale subresource (the pod set is + retrieved from the controller's ScaleStatus) or some well known controllers + (e.g. for DaemonSet the pod set is read from the controller's spec). + If VerticalPodAutoscaler cannot use specified target it will report + ConfigUnsupported condition. + Note that VerticalPodAutoscaler does not require full implementation + of scale subresource - it will not use it to modify the replica count. + The only thing retrieved is a label selector matching pods grouped by + the target resource. + properties: + apiVersion: + description: apiVersion is the API version of the referent + type: string + kind: + description: 'kind is the kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'name is the name of the referent; More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + updatePolicy: + description: |- + Describes the rules on how changes are applied to the pods. + If not specified, all fields in the `PodUpdatePolicy` are set to their + default values. + properties: + evictionRequirements: + description: |- + EvictionRequirements is a list of EvictionRequirements that need to + evaluate to true in order for a Pod to be evicted. If more than one + EvictionRequirement is specified, all of them need to be fulfilled to allow eviction. + items: + description: |- + EvictionRequirement defines a single condition which needs to be true in + order to evict a Pod + properties: + changeRequirement: + description: EvictionChangeRequirement refers to the relationship + between the new target recommendation for a Pod and its + current requests, what kind of change is necessary for + the Pod to be evicted + enum: + - TargetHigherThanRequests + - TargetLowerThanRequests + type: string + resources: + description: |- + Resources is a list of one or more resources that the condition applies + to. If more than one resource is given, the EvictionRequirement is fulfilled + if at least one resource meets `changeRequirement`. + items: + description: ResourceName is the name identifying various + resources in a ResourceList. + type: string + type: array + required: + - changeRequirement + - resources + type: object + type: array + minReplicas: + description: |- + Minimal number of replicas which need to be alive for Updater to attempt + pod eviction (pending other checks like PDB). Only positive values are + allowed. Overrides global '--min-replicas' flag. + format: int32 + type: integer + updateMode: + description: |- + Controls when autoscaler applies changes to the pod resources. + The default is 'Recreate'. + enum: + - "Off" + - Initial + - Recreate + - InPlaceOrRecreate + - Auto + type: string + type: object + required: + - targetRef + type: object + status: + description: Current information about the autoscaler. + properties: + conditions: + description: |- + Conditions is the set of conditions required for this autoscaler to scale its target, + and indicates whether or not those conditions are met. + items: + description: |- + VerticalPodAutoscalerCondition describes the state of + a VerticalPodAutoscaler at a certain point. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from + one status to another + format: date-time + type: string + message: + description: |- + message is a human-readable explanation containing details about + the transition + type: string + reason: + description: reason is the reason for the condition's last transition. + type: string + status: + description: status is the status of the condition (True, False, + Unknown) + type: string + type: + description: type describes the current condition + type: string + required: + - status + - type + type: object + type: array + recommendation: + description: |- + The most recently computed amount of resources recommended by the + autoscaler for the controlled pods. + properties: + containerRecommendations: + description: Resources recommended by the autoscaler for each + container. + items: + description: |- + RecommendedContainerResources is the recommendation of resources computed by + autoscaler for a specific container. Respects the container resource policy + if present in the spec. In particular the recommendation is not produced for + containers with `ContainerScalingMode` set to 'Off'. + properties: + containerName: + description: Name of the container. + type: string + lowerBound: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Minimum recommended amount of resources. Observes ContainerResourcePolicy. + This amount is not guaranteed to be sufficient for the application to operate in a stable way, however + running with less resources is likely to have significant impact on performance/availability. + type: object + target: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Recommended amount of resources. Observes ContainerResourcePolicy. + type: object + uncappedTarget: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + The most recent recommended resources target computed by the autoscaler + for the controlled pods, based only on actual resource usage, not taking + into account the ContainerResourcePolicy. + May differ from the Recommendation if the actual resource usage causes + the target to violate the ContainerResourcePolicy (lower than MinAllowed + or higher that MaxAllowed). + Used only as status indication, will not affect actual resource assignment. + type: object + upperBound: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Maximum recommended amount of resources. Observes ContainerResourcePolicy. + Any resources allocated beyond this value are likely wasted. This value may be larger than the maximum + amount of application is actually capable of consuming. + type: object + required: + - target + type: object + type: array + type: object + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} + - deprecated: true + deprecationWarning: autoscaling.k8s.io/v1beta2 API is deprecated + name: v1beta2 + schema: + openAPIV3Schema: + description: |- + VerticalPodAutoscaler is the configuration for a vertical pod + autoscaler, which automatically manages pod resources based on historical and + real time resource utilization. + 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: |- + Specification of the behavior of the autoscaler. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status. + properties: + resourcePolicy: + description: |- + Controls how the autoscaler computes recommended resources. + The resource policy may be used to set constraints on the recommendations + for individual containers. If not specified, the autoscaler computes recommended + resources for all containers in the pod, without additional constraints. + properties: + containerPolicies: + description: Per-container resource policies. + items: + description: |- + ContainerResourcePolicy controls how autoscaler computes the recommended + resources for a specific container. + properties: + containerName: + description: |- + Name of the container or DefaultContainerResourcePolicy, in which + case the policy is used by the containers that don't have their own + policy specified. + type: string + maxAllowed: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Specifies the maximum amount of resources that will be recommended + for the container. The default is no maximum. + type: object + minAllowed: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Specifies the minimal amount of resources that will be recommended + for the container. The default is no minimum. + type: object + mode: + description: Whether autoscaler is enabled for the container. + The default is "Auto". + enum: + - Auto + - "Off" + type: string + type: object + type: array + type: object + targetRef: + description: |- + TargetRef points to the controller managing the set of pods for the + autoscaler to control - e.g. Deployment, StatefulSet. VerticalPodAutoscaler + can be targeted at controller implementing scale subresource (the pod set is + retrieved from the controller's ScaleStatus) or some well known controllers + (e.g. for DaemonSet the pod set is read from the controller's spec). + If VerticalPodAutoscaler cannot use specified target it will report + ConfigUnsupported condition. + Note that VerticalPodAutoscaler does not require full implementation + of scale subresource - it will not use it to modify the replica count. + The only thing retrieved is a label selector matching pods grouped by + the target resource. + properties: + apiVersion: + description: apiVersion is the API version of the referent + type: string + kind: + description: 'kind is the kind of the referent; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'name is the name of the referent; More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + updatePolicy: + description: |- + Describes the rules on how changes are applied to the pods. + If not specified, all fields in the `PodUpdatePolicy` are set to their + default values. + properties: + updateMode: + description: |- + Controls when autoscaler applies changes to the pod resources. + The default is 'Auto'. + enum: + - "Off" + - Initial + - Recreate + - Auto + type: string + type: object + required: + - targetRef + type: object + status: + description: Current information about the autoscaler. + properties: + conditions: + description: |- + Conditions is the set of conditions required for this autoscaler to scale its target, + and indicates whether or not those conditions are met. + items: + description: |- + VerticalPodAutoscalerCondition describes the state of + a VerticalPodAutoscaler at a certain point. + properties: + lastTransitionTime: + description: |- + lastTransitionTime is the last time the condition transitioned from + one status to another + format: date-time + type: string + message: + description: |- + message is a human-readable explanation containing details about + the transition + type: string + reason: + description: reason is the reason for the condition's last transition. + type: string + status: + description: status is the status of the condition (True, False, + Unknown) + type: string + type: + description: type describes the current condition + type: string + required: + - status + - type + type: object + type: array + recommendation: + description: |- + The most recently computed amount of resources recommended by the + autoscaler for the controlled pods. + properties: + containerRecommendations: + description: Resources recommended by the autoscaler for each + container. + items: + description: |- + RecommendedContainerResources is the recommendation of resources computed by + autoscaler for a specific container. Respects the container resource policy + if present in the spec. In particular the recommendation is not produced for + containers with `ContainerScalingMode` set to 'Off'. + properties: + containerName: + description: Name of the container. + type: string + lowerBound: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Minimum recommended amount of resources. Observes ContainerResourcePolicy. + This amount is not guaranteed to be sufficient for the application to operate in a stable way, however + running with less resources is likely to have significant impact on performance/availability. + type: object + target: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: Recommended amount of resources. Observes ContainerResourcePolicy. + type: object + uncappedTarget: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + The most recent recommended resources target computed by the autoscaler + for the controlled pods, based only on actual resource usage, not taking + into account the ContainerResourcePolicy. + May differ from the Recommendation if the actual resource usage causes + the target to violate the ContainerResourcePolicy (lower than MinAllowed + or higher that MaxAllowed). + Used only as status indication, will not affect actual resource assignment. + type: object + upperBound: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: |- + Maximum recommended amount of resources. Observes ContainerResourcePolicy. + Any resources allocated beyond this value are likely wasted. This value may be larger than the maximum + amount of application is actually capable of consuming. + type: object + required: + - target + type: object + type: array + type: object + type: object + required: + - spec + type: object + served: false + storage: false + subresources: + status: {} diff --git a/test/integration/testdata/upstream-crds/10-crd-extensions.gardener.cloud_controlplanes.yaml b/test/integration/testdata/upstream-crds/10-crd-extensions.gardener.cloud_controlplanes.yaml new file mode 100644 index 00000000..bbb7d68b --- /dev/null +++ b/test/integration/testdata/upstream-crds/10-crd-extensions.gardener.cloud_controlplanes.yaml @@ -0,0 +1,260 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: controlplanes.extensions.gardener.cloud +spec: + group: extensions.gardener.cloud + names: + kind: ControlPlane + listKind: ControlPlaneList + plural: controlplanes + shortNames: + - cp + singular: controlplane + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The control plane type. + jsonPath: .spec.type + name: Type + type: string + - description: Status of control plane resource. + jsonPath: .status.lastOperation.state + name: Status + type: string + - description: creation timestamp + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ControlPlane is a specification for a ControlPlane resource. + 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: |- + Specification of the ControlPlane. + If the object's deletion timestamp is set, this field is immutable. + properties: + class: + description: Class holds the extension class used to control the responsibility + for multiple provider extensions. + type: string + x-kubernetes-validations: + - message: Value is immutable + rule: self == oldSelf + infrastructureProviderStatus: + description: |- + InfrastructureProviderStatus contains the provider status that has + been generated by the controller responsible for the `Infrastructure` resource. + type: object + x-kubernetes-preserve-unknown-fields: true + providerConfig: + description: ProviderConfig is the provider specific configuration. + type: object + x-kubernetes-preserve-unknown-fields: true + region: + description: Region is the region of this control plane. This field + is immutable. + type: string + secretRef: + description: SecretRef is a reference to a secret that contains the + cloud provider specific credentials. + properties: + name: + description: name is unique within a namespace to reference a + secret resource. + type: string + namespace: + description: namespace defines the space within which the secret + name must be unique. + type: string + type: object + x-kubernetes-map-type: atomic + type: + description: Type contains the instance of the resource's kind. + type: string + required: + - region + - secretRef + - type + type: object + status: + description: ControlPlaneStatus is the status of a ControlPlane resource. + properties: + conditions: + description: Conditions represents the latest available observations + of a Seed's current state. + items: + description: Condition holds the information about the state of + a resource. + properties: + codes: + description: Well-defined error codes in case the condition + reports a problem. + items: + description: ErrorCode is a string alias. + type: string + type: array + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: Last time the condition was updated. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of the condition. + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + lastError: + description: LastError holds information about the last occurred error + during an operation. + properties: + codes: + description: Well-defined error codes of the last error(s). + items: + description: ErrorCode is a string alias. + type: string + type: array + description: + description: A human readable message indicating details about + the last error. + type: string + lastUpdateTime: + description: Last time the error was reported + format: date-time + type: string + taskID: + description: ID of the task which caused this last error + type: string + required: + - description + type: object + lastOperation: + description: LastOperation holds information about the last operation + on the resource. + properties: + description: + description: A human readable message indicating details about + the last operation. + type: string + lastUpdateTime: + description: Last time the operation state transitioned from one + to another. + format: date-time + type: string + progress: + description: The progress in percentage (0-100) of the last operation. + format: int32 + type: integer + state: + description: Status of the last operation, one of Aborted, Processing, + Succeeded, Error, Failed. + type: string + type: + description: Type of the last operation, one of Create, Reconcile, + Delete, Migrate, Restore. + type: string + required: + - description + - lastUpdateTime + - progress + - state + - type + type: object + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this resource. + format: int64 + type: integer + providerStatus: + description: ProviderStatus contains provider-specific status. + type: object + x-kubernetes-preserve-unknown-fields: true + resources: + description: Resources holds a list of named resource references that + can be referred to in the state by their names. + items: + description: NamedResourceReference is a named reference to a resource. + properties: + name: + description: Name of the resource reference. + type: string + resourceRef: + description: ResourceRef is a reference to a resource. + properties: + apiVersion: + description: apiVersion is the API version of the referent + type: string + kind: + description: 'kind is the kind of the referent; More info: + https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'name is the name of the referent; More info: + https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + required: + - kind + - name + type: object + x-kubernetes-map-type: atomic + required: + - name + - resourceRef + type: object + type: array + state: + description: State can be filled by the operating controller with + what ever data it needs. + type: object + x-kubernetes-preserve-unknown-fields: true + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {} diff --git a/test/integration/testdata/upstream-crds/10-crd-resources.gardener.cloud_managedresources.yaml b/test/integration/testdata/upstream-crds/10-crd-resources.gardener.cloud_managedresources.yaml new file mode 100644 index 00000000..dfdb95b7 --- /dev/null +++ b/test/integration/testdata/upstream-crds/10-crd-resources.gardener.cloud_managedresources.yaml @@ -0,0 +1,254 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.1 + name: managedresources.resources.gardener.cloud +spec: + group: resources.gardener.cloud + names: + kind: ManagedResource + listKind: ManagedResourceList + plural: managedresources + shortNames: + - mr + singular: managedresource + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: The class identifies which resource manager is responsible for + this ManagedResource. + jsonPath: .spec.class + name: Class + type: string + - description: ' Indicates whether all resources have been applied.' + jsonPath: .status.conditions[?(@.type=="ResourcesApplied")].status + name: Applied + type: string + - description: Indicates whether all resources are healthy. + jsonPath: .status.conditions[?(@.type=="ResourcesHealthy")].status + name: Healthy + type: string + - description: Indicates whether some resources are still progressing to be rolled + out. + jsonPath: .status.conditions[?(@.type=="ResourcesProgressing")].status + name: Progressing + type: string + - description: creation timestamp + jsonPath: .metadata.creationTimestamp + name: Age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ManagedResource describes a list of managed resources. + 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: Spec contains the specification of this managed resource. + properties: + class: + description: Class holds the resource class used to control the responsibility + for multiple resource manager instances + type: string + deletePersistentVolumeClaims: + description: |- + DeletePersistentVolumeClaims specifies if PersistentVolumeClaims created by StatefulSets, which are managed by this + resource, should also be deleted when the corresponding StatefulSet is deleted (defaults to false). + type: boolean + equivalences: + description: Equivalences specifies possible group/kind equivalences + for objects. + items: + items: + description: |- + GroupKind specifies a Group and a Kind, but does not force a version. This is useful for identifying + concepts during lookup stages without having partially valid types + properties: + group: + type: string + kind: + type: string + required: + - group + - kind + type: object + type: array + type: array + forceOverwriteAnnotations: + description: ForceOverwriteAnnotations specifies that all existing + annotations should be overwritten. Defaults to false. + type: boolean + forceOverwriteLabels: + description: ForceOverwriteLabels specifies that all existing labels + should be overwritten. Defaults to false. + type: boolean + injectLabels: + additionalProperties: + type: string + description: InjectLabels injects the provided labels into every resource + that is part of the referenced secrets. + type: object + keepObjects: + description: |- + KeepObjects specifies whether the objects should be kept although the managed resource has already been deleted. + Defaults to false. + type: boolean + secretRefs: + description: SecretRefs is a list of secret references. + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + 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 + type: array + required: + - secretRefs + type: object + status: + description: Status contains the status of this managed resource. + properties: + conditions: + items: + description: Condition holds the information about the state of + a resource. + properties: + codes: + description: Well-defined error codes in case the condition + reports a problem. + items: + description: ErrorCode is a string alias. + type: string + type: array + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + lastUpdateTime: + description: Last time the condition was updated. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of the condition. + type: string + required: + - lastTransitionTime + - lastUpdateTime + - message + - reason + - status + - type + type: object + type: array + observedGeneration: + description: ObservedGeneration is the most recent generation observed + for this resource. + format: int64 + type: integer + resources: + description: Resources is a list of objects that have been created. + items: + description: ObjectReference is a reference to another object. + properties: + annotations: + additionalProperties: + type: string + description: Annotations is a map of annotations that were used + during last update of the resource. + type: object + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: |- + If referring to a piece of an object instead of an entire object, this string + should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within a pod, this would take on a value like: + "spec.containers{name}" (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" (container with + index 2 in this pod). This syntax is chosen only to have some well-defined way of + referencing a part of an object. + type: string + kind: + description: |- + Kind of the referent. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + labels: + additionalProperties: + type: string + description: Labels is a map of labels that were used during + last update of the resource. + type: object + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + namespace: + description: |- + Namespace of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ + type: string + resourceVersion: + description: |- + Specific resourceVersion to which this reference is made, if any. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency + type: string + uid: + description: |- + UID of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids + type: string + type: object + x-kubernetes-map-type: atomic + type: array + secretsDataChecksum: + description: SecretsDataChecksum is the checksum of referenced secrets + data. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {}