From 2aa5c4610a501a1635b5ff0b3b7728e203f997e9 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 11 Jun 2026 11:34:46 +0200 Subject: [PATCH 01/49] define the CompatibilityMode field --- hack/api-reference/api.md | 11 +++++++++++ pkg/apis/stackit/v1alpha1/types_controlplane.go | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/hack/api-reference/api.md b/hack/api-reference/api.md index 8bbbc206..6169a6c6 100644 --- a/hack/api-reference/api.md +++ b/hack/api-reference/api.md @@ -215,6 +215,17 @@ string

+ + +compatibilityMode
+ +string + + + +

+ + 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) From 34b1e30b362558a49782286a96ae7d801d910bfc Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 11 Jun 2026 12:02:18 +0200 Subject: [PATCH 02/49] add the default value for CSI.CompatibilityMode --- pkg/apis/stackit/v1alpha1/constants.go | 2 ++ pkg/apis/stackit/v1alpha1/defaults.go | 3 +++ 2 files changed, 5 insertions(+) diff --git a/pkg/apis/stackit/v1alpha1/constants.go b/pkg/apis/stackit/v1alpha1/constants.go index 86bbdc81..c7270e7f 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" ) 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 + } } From b26aa991eb3e55801d61269811c983b8e873e867 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Fri, 12 Jun 2026 09:31:46 +0200 Subject: [PATCH 03/49] provide access to the new CompatibilityMode field --- pkg/apis/stackit/v1alpha1/constants.go | 8 ++++++++ pkg/controller/controlplane/valuesprovider.go | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/pkg/apis/stackit/v1alpha1/constants.go b/pkg/apis/stackit/v1alpha1/constants.go index c7270e7f..42400ae6 100644 --- a/pkg/apis/stackit/v1alpha1/constants.go +++ b/pkg/apis/stackit/v1alpha1/constants.go @@ -17,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/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 180ac234..c8490254 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1216,6 +1216,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) } From 92697d7cfb0fee6738b281428ae64a2bfacc8b61 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Fri, 12 Jun 2026 13:01:58 +0200 Subject: [PATCH 04/49] render the second DaemonSet in compatibility mode, if parameterized accordingly --- .../templates/daemonset.yaml | 150 ++++++++++++++++++ .../values.yaml | 4 + 2 files changed, 154 insertions(+) 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..e5e4e83d 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 @@ -144,3 +144,153 @@ spec: configMap: defaultMode: 420 name: {{ .Values.prefix }}-cloud-provider-config +--- +{{- if .Values.csiDriverCompatibility.enabled }} +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: {{ .Values.prefix }}-csi-driver-compatibility-node + namespace: {{ .Release.Namespace }} + labels: + node.gardener.cloud/critical-component: "true" + app: {{ .Values.prefix }}-csi-compatibility + role: disk-driver +spec: + selector: + matchLabels: + app: {{ .Values.prefix }}-csi-compatibility + role: disk-driver + template: + 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 + labels: + node.gardener.cloud/critical-component: "true" + app: {{ .Values.prefix }}-csi-compatibility + role: disk-driver + spec: + hostNetwork: true + priorityClassName: system-node-critical + serviceAccountName: {{ .Values.prefix }}-csi-driver-node + tolerations: + - effect: NoSchedule + operator: Exists + - key: CriticalAddonsOnly + operator: Exists + - effect: NoExecute + operator: Exists + securityContext: + seccompProfile: + type: RuntimeDefault + containers: + - name: csi-driver-stackit + image: {{ index .Values.images "csi-driver-stackit" }} + args: + - /bin/stackit-csi-plugin + - --endpoint=$(CSI_ENDPOINT) + - --cloud-config=/etc/config/cloud.yaml + {{- range $userAgentHeader := .Values.userAgentHeaders }} + - --user-agent={{ $userAgentHeader }} + {{- end }} + - --v=2 + - --provide-controller-service=false + - --legacy-storage-mode=true + - --legacy-volume-creation={{ .Values.csiDriverCompatibility.legacyVolumeCreation }} + env: + - name: CSI_ENDPOINT + value: unix://{{ .Values.socketPath }} +{{- if .Values.resources.driver }} + resources: +{{ toYaml .Values.resources.driver | indent 10 }} +{{- end }} + securityContext: + privileged: true + capabilities: + add: ["SYS_ADMIN"] + allowPrivilegeEscalation: true + ports: + - name: healthz + containerPort: 9909 + protocol: TCP + livenessProbe: + httpGet: + path: /healthz + port: healthz + initialDelaySeconds: 10 + timeoutSeconds: 180 + periodSeconds: 30 + failureThreshold: 5 + volumeMounts: + - name: kubelet-dir + mountPath: /var/lib/kubelet + mountPropagation: "Bidirectional" + - name: plugin-dir + mountPath: /csi + - name: device-dir + mountPath: /dev + mountPropagation: "HostToContainer" + - name: cloud-provider-config + mountPath: /etc/config + + - name: csi-node-driver-registrar + image: {{ index .Values.images "csi-node-driver-registrar" }} + args: + - --csi-address=$(ADDRESS) + - --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH) + - --v=5 + env: + - name: ADDRESS + value: {{ .Values.socketPath }} + - name: DRIVER_REG_SOCK_PATH + value: /var/lib/kubelet/plugins/block-storage.csi.stackit.cloud/csi.sock +{{- if .Values.resources.nodeDriverRegistrar }} + resources: +{{ toYaml .Values.resources.nodeDriverRegistrar | indent 10 }} +{{- end }} + volumeMounts: + - name: plugin-dir + mountPath: /csi + - name: registration-dir + mountPath: /registration + securityContext: + allowPrivilegeEscalation: false + + - name: csi-liveness-probe + image: {{ index .Values.images "csi-liveness-probe" }} + args: + - --probe-timeout=3m + - --csi-address={{ .Values.socketPath }} + - --health-port=9909 +{{- if .Values.resources.livenessProbe }} + resources: +{{ toYaml .Values.resources.livenessProbe | indent 10 }} +{{- end }} + volumeMounts: + - name: plugin-dir + mountPath: /csi + securityContext: + allowPrivilegeEscalation: false + + volumes: + - name: kubelet-dir + hostPath: + path: /var/lib/kubelet + type: Directory + - name: plugin-dir + hostPath: + path: /var/lib/kubelet/plugins/block-storage.csi.stackit.cloud/ + type: DirectoryOrCreate + - name: registration-dir + hostPath: + path: /var/lib/kubelet/plugins_registry/ + type: Directory + - name: device-dir + hostPath: + path: /dev + type: Directory + - name: cloud-provider-config + configMap: + defaultMode: 420 + name: {{ .Values.prefix }}-cloud-provider-config + {{- end }} 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..5eceba97 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,10 @@ driverName: block-storage.csi.stackit.cloud rescanBlockStorageOnResize: "true" +csiDriverCompatibility: + enabled: false + legacyVolumeCreation: true + images: csi-driver-stackit: image-repository:image-tag csi-node-driver-registrar: image-repository:image-tag From 05cd918e1d94dd84d176ab585d08f6d9fa8a160d Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 11:08:37 +0200 Subject: [PATCH 05/49] install control-plane stackit-blockstorage chart (compat) as ManagedResource Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index c8490254..3f4e7d02 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -21,10 +21,12 @@ import ( "github.com/gardener/gardener/pkg/apis/core/v1beta1" v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" + "github.com/gardener/gardener/pkg/chartrenderer" gardenerutils "github.com/gardener/gardener/pkg/utils" "github.com/gardener/gardener/pkg/utils/chart" gutil "github.com/gardener/gardener/pkg/utils/gardener" kutil "github.com/gardener/gardener/pkg/utils/kubernetes" + "github.com/gardener/gardener/pkg/utils/managedresources" secretutils "github.com/gardener/gardener/pkg/utils/secrets" secretsmanager "github.com/gardener/gardener/pkg/utils/secrets/manager" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" @@ -43,6 +45,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" @@ -359,6 +362,7 @@ var ( func NewValuesProvider(mgr manager.Manager, deployALBIngressController bool, customLabelDomain string) genericactuator.ValuesProvider { return &valuesProvider{ client: mgr.GetClient(), + config: mgr.GetConfig(), decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(), deployALBIngressController: deployALBIngressController, customLabelDomain: customLabelDomain, @@ -369,6 +373,7 @@ 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 @@ -750,6 +755,12 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf controlPlaneValues[openstack.CSIControllerName] = map[string]any{ "enabled": false, } + if getCSICompatibilityMode(cpConfig) == stackitv1alpha1.COMPAT { + err := vp.deploySeedCSICompatibilityMode(ctx, cluster.Shoot.GetNamespace(), csiSTACKIT) + if err != nil { + return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) + } + } default: return nil, fmt.Errorf("unsupported storage CSI Driver: %s", storageCSIDriver) } @@ -1176,6 +1187,31 @@ func (vp *valuesProvider) checkEmergencyLoadBalancerAccess(ctx context.Context, return apiURL, apiToken, nil } +func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any) error { + renderer, err := chartrenderer.NewForConfig(vp.config) + if err != nil { + return nil + } + + releaseName := "csi-compatibility-mode" + chartName := "stackit-blockstorage-csi-driver" + + values["prefix"] = "stackit-compat" + + renderedChart, err := renderer.RenderEmbeddedFS( + charts.InternalChart, "shoot-system-components/stackit-blockstorage-csi-driver", releaseName, v1beta1constants.GardenNamespace, values, + ) + if err != nil { + return err + } + + data := map[string][]byte{chartName: renderedChart.Manifest()} + return managedresources.Create( + ctx, vp.client, namespace, chartName, map[string]string{}, + false, "seed", data, new(false), nil, new(false), + ) +} + // decodeLoadBalancerAPIEmergencySecret decodes a [corev1.Secret] for emergency loadbalancer access and // returns the apiURL and apiToken to use or an error. // The apiURL and apiToken are only set if both values exist inside the secret and are not empty. From 3703159e5f8dd807e5899e140372ac3ab41d54bb Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 11:10:55 +0200 Subject: [PATCH 06/49] fix chart path Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 3f4e7d02..cd8893e6 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1199,7 +1199,7 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na values["prefix"] = "stackit-compat" renderedChart, err := renderer.RenderEmbeddedFS( - charts.InternalChart, "shoot-system-components/stackit-blockstorage-csi-driver", releaseName, v1beta1constants.GardenNamespace, values, + charts.InternalChart, "seed-controlplane/stackit-blockstorage-csi-driver", releaseName, v1beta1constants.GardenNamespace, values, ) if err != nil { return err From 3c1fef45ff9b25addedca94e251dc3e454397183 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 11:39:04 +0200 Subject: [PATCH 07/49] fix chart path and releaseName Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index cd8893e6..e20a898e 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1193,13 +1193,12 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na return nil } - releaseName := "csi-compatibility-mode" chartName := "stackit-blockstorage-csi-driver" values["prefix"] = "stackit-compat" renderedChart, err := renderer.RenderEmbeddedFS( - charts.InternalChart, "seed-controlplane/stackit-blockstorage-csi-driver", releaseName, v1beta1constants.GardenNamespace, values, + charts.InternalChart, "seed-controlplane/charts/stackit-blockstorage-csi-driver", chartName, namespace, values, ) if err != nil { return err From 2f32d470c96d1b4de8675229aabe23504bce3faf Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 11:49:51 +0200 Subject: [PATCH 08/49] fix path? Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index e20a898e..f6162512 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1198,7 +1198,7 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na values["prefix"] = "stackit-compat" renderedChart, err := renderer.RenderEmbeddedFS( - charts.InternalChart, "seed-controlplane/charts/stackit-blockstorage-csi-driver", chartName, namespace, values, + charts.InternalChart, filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), chartName, namespace, values, ) if err != nil { return err From 8dc17ef2c4b739717a25341a0fa493c68ca2f6b6 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 11:58:27 +0200 Subject: [PATCH 09/49] Move some things around Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index f6162512..b3de76a8 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -741,6 +741,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: @@ -756,7 +765,7 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf "enabled": false, } if getCSICompatibilityMode(cpConfig) == stackitv1alpha1.COMPAT { - err := vp.deploySeedCSICompatibilityMode(ctx, cluster.Shoot.GetNamespace(), csiSTACKIT) + err := vp.deploySeedCSICompatibilityMode(ctx, cluster.Shoot.GetNamespace(), controlPlaneValues) if err != nil { return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) } @@ -765,15 +774,6 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf 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) From a03c8df9979f84ad6c9e0f1ed40cf8695ff7ae19 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 12:00:36 +0200 Subject: [PATCH 10/49] fix values? Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index b3de76a8..2a018f14 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1195,7 +1195,8 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na chartName := "stackit-blockstorage-csi-driver" - values["prefix"] = "stackit-compat" + csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]string) + csiStackitValues["prefix"] = "stackit-compat" renderedChart, err := renderer.RenderEmbeddedFS( charts.InternalChart, filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), chartName, namespace, values, From 4c59bcd1e48521fbc011ecab86ade9f89540d918 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 12:06:34 +0200 Subject: [PATCH 11/49] fix type assertion Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 2a018f14..68e71247 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1195,7 +1195,7 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na chartName := "stackit-blockstorage-csi-driver" - csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]string) + csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]interface{}) csiStackitValues["prefix"] = "stackit-compat" renderedChart, err := renderer.RenderEmbeddedFS( From f46ef40346f800963456c3661e4bd59b1c2b421c Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 12:11:30 +0200 Subject: [PATCH 12/49] fix namespace Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 68e71247..7f12b38e 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -765,7 +765,7 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf "enabled": false, } if getCSICompatibilityMode(cpConfig) == stackitv1alpha1.COMPAT { - err := vp.deploySeedCSICompatibilityMode(ctx, cluster.Shoot.GetNamespace(), controlPlaneValues) + err := vp.deploySeedCSICompatibilityMode(ctx, cp.GetNamespace(), controlPlaneValues) if err != nil { return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) } From 5d15fee2d0298c65e941c0f36256144ab2745b08 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 12:29:42 +0200 Subject: [PATCH 13/49] use different name for MR release Signed-off-by: Niclas Schad --- pkg/controller/controlplane/valuesprovider.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 7f12b38e..c852aaa2 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1207,7 +1207,7 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na data := map[string][]byte{chartName: renderedChart.Manifest()} return managedresources.Create( - ctx, vp.client, namespace, chartName, map[string]string{}, + ctx, vp.client, namespace, "stackit-csi-compat-chart", map[string]string{}, false, "seed", data, new(false), nil, new(false), ) } From f6b0a546989767def78d83b8a04191260adf2c24 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Mon, 15 Jun 2026 14:55:53 +0200 Subject: [PATCH 14/49] apply fixes and inject images manually Signed-off-by: Niclas Schad --- .../values.yaml | 1 - pkg/controller/controlplane/valuesprovider.go | 40 +++++++++++++++++-- 2 files changed, 36 insertions(+), 5 deletions(-) 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..02f0e30b 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 @@ -12,7 +12,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/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index c852aaa2..b242c926 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -764,6 +764,7 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf controlPlaneValues[openstack.CSIControllerName] = map[string]any{ "enabled": false, } + // TODO: make it nice if getCSICompatibilityMode(cpConfig) == stackitv1alpha1.COMPAT { err := vp.deploySeedCSICompatibilityMode(ctx, cp.GetNamespace(), controlPlaneValues) if err != nil { @@ -1195,17 +1196,48 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na chartName := "stackit-blockstorage-csi-driver" - csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]interface{}) - csiStackitValues["prefix"] = "stackit-compat" + foo := map[string]interface{}{} + foo = maps.Clone(values) + // Get the chart Values + csiStackitValues := foo[openstack.CSISTACKITControllerName].(map[string]interface{}) + // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key + chartValues := gardenerutils.MergeMaps(foo, csiStackitValues) + // Override chart values + chartValues["prefix"] = "stackit-compat" + + //TODO: Use gardener tools for this? If possible + imagesToFind := []string{ + "csi-driver-stackit", + "csi-provisioner", + "csi-attacher", + "csi-snapshotter", + "csi-resizer", + "csi-liveness-probe", + "csi-snapshot-controller"} + images := imagevector.ImageVector() + imageMap := make(map[string]interface{}) + + for _, image := range imagesToFind { + foundImage, err := images.FindImage(image) + if err != nil { + return err + } + imageMap[image] = foundImage.String() + } + chartValues["images"] = imageMap renderedChart, err := renderer.RenderEmbeddedFS( - charts.InternalChart, filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), chartName, namespace, values, + charts.InternalChart, + filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), + chartName, + namespace, + chartValues, ) if err != nil { return err } - data := map[string][]byte{chartName: renderedChart.Manifest()} + data := renderedChart.AsSecretData() return managedresources.Create( ctx, vp.client, namespace, "stackit-csi-compat-chart", map[string]string{}, false, "seed", data, new(false), nil, new(false), From c923b5dd4bfcf45bc6d5b052a8bd0bd0db613a74 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Tue, 16 Jun 2026 10:12:51 +0200 Subject: [PATCH 15/49] remove the duplicated DaemonSet as it is not needed --- .../templates/daemonset.yaml | 150 ------------------ .../values.yaml | 4 - 2 files changed, 154 deletions(-) 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 e5e4e83d..cb04e3cf 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 @@ -144,153 +144,3 @@ spec: configMap: defaultMode: 420 name: {{ .Values.prefix }}-cloud-provider-config ---- -{{- if .Values.csiDriverCompatibility.enabled }} -apiVersion: apps/v1 -kind: DaemonSet -metadata: - name: {{ .Values.prefix }}-csi-driver-compatibility-node - namespace: {{ .Release.Namespace }} - labels: - node.gardener.cloud/critical-component: "true" - app: {{ .Values.prefix }}-csi-compatibility - role: disk-driver -spec: - selector: - matchLabels: - app: {{ .Values.prefix }}-csi-compatibility - role: disk-driver - template: - 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 - labels: - node.gardener.cloud/critical-component: "true" - app: {{ .Values.prefix }}-csi-compatibility - role: disk-driver - spec: - hostNetwork: true - priorityClassName: system-node-critical - serviceAccountName: {{ .Values.prefix }}-csi-driver-node - tolerations: - - effect: NoSchedule - operator: Exists - - key: CriticalAddonsOnly - operator: Exists - - effect: NoExecute - operator: Exists - securityContext: - seccompProfile: - type: RuntimeDefault - containers: - - name: csi-driver-stackit - image: {{ index .Values.images "csi-driver-stackit" }} - args: - - /bin/stackit-csi-plugin - - --endpoint=$(CSI_ENDPOINT) - - --cloud-config=/etc/config/cloud.yaml - {{- range $userAgentHeader := .Values.userAgentHeaders }} - - --user-agent={{ $userAgentHeader }} - {{- end }} - - --v=2 - - --provide-controller-service=false - - --legacy-storage-mode=true - - --legacy-volume-creation={{ .Values.csiDriverCompatibility.legacyVolumeCreation }} - env: - - name: CSI_ENDPOINT - value: unix://{{ .Values.socketPath }} -{{- if .Values.resources.driver }} - resources: -{{ toYaml .Values.resources.driver | indent 10 }} -{{- end }} - securityContext: - privileged: true - capabilities: - add: ["SYS_ADMIN"] - allowPrivilegeEscalation: true - ports: - - name: healthz - containerPort: 9909 - protocol: TCP - livenessProbe: - httpGet: - path: /healthz - port: healthz - initialDelaySeconds: 10 - timeoutSeconds: 180 - periodSeconds: 30 - failureThreshold: 5 - volumeMounts: - - name: kubelet-dir - mountPath: /var/lib/kubelet - mountPropagation: "Bidirectional" - - name: plugin-dir - mountPath: /csi - - name: device-dir - mountPath: /dev - mountPropagation: "HostToContainer" - - name: cloud-provider-config - mountPath: /etc/config - - - name: csi-node-driver-registrar - image: {{ index .Values.images "csi-node-driver-registrar" }} - args: - - --csi-address=$(ADDRESS) - - --kubelet-registration-path=$(DRIVER_REG_SOCK_PATH) - - --v=5 - env: - - name: ADDRESS - value: {{ .Values.socketPath }} - - name: DRIVER_REG_SOCK_PATH - value: /var/lib/kubelet/plugins/block-storage.csi.stackit.cloud/csi.sock -{{- if .Values.resources.nodeDriverRegistrar }} - resources: -{{ toYaml .Values.resources.nodeDriverRegistrar | indent 10 }} -{{- end }} - volumeMounts: - - name: plugin-dir - mountPath: /csi - - name: registration-dir - mountPath: /registration - securityContext: - allowPrivilegeEscalation: false - - - name: csi-liveness-probe - image: {{ index .Values.images "csi-liveness-probe" }} - args: - - --probe-timeout=3m - - --csi-address={{ .Values.socketPath }} - - --health-port=9909 -{{- if .Values.resources.livenessProbe }} - resources: -{{ toYaml .Values.resources.livenessProbe | indent 10 }} -{{- end }} - volumeMounts: - - name: plugin-dir - mountPath: /csi - securityContext: - allowPrivilegeEscalation: false - - volumes: - - name: kubelet-dir - hostPath: - path: /var/lib/kubelet - type: Directory - - name: plugin-dir - hostPath: - path: /var/lib/kubelet/plugins/block-storage.csi.stackit.cloud/ - type: DirectoryOrCreate - - name: registration-dir - hostPath: - path: /var/lib/kubelet/plugins_registry/ - type: Directory - - name: device-dir - hostPath: - path: /dev - type: Directory - - name: cloud-provider-config - configMap: - defaultMode: 420 - name: {{ .Values.prefix }}-cloud-provider-config - {{- end }} 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 5eceba97..c5e073af 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,10 +3,6 @@ driverName: block-storage.csi.stackit.cloud rescanBlockStorageOnResize: "true" -csiDriverCompatibility: - enabled: false - legacyVolumeCreation: true - images: csi-driver-stackit: image-repository:image-tag csi-node-driver-registrar: image-repository:image-tag From 96a59fdc57415baa13c3f167f5c87b95701f8745 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Wed, 17 Jun 2026 13:52:34 +0200 Subject: [PATCH 16/49] add a symmetric method for the Shoot controlplane in Compatibility Mode --- pkg/controller/controlplane/valuesprovider.go | 73 +++++++++++++++++-- 1 file changed, 67 insertions(+), 6 deletions(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index b242c926..57f094a1 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1088,6 +1088,11 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c case stackitv1alpha1.OPENSTACK: values[openstack.CSINodeName] = csiNodeDriverValues values[openstack.CSISTACKITNodeName] = map[string]any{"enabled": false} + if getCSICompatibilityMode(cpConfig) == stackitv1alpha1.COMPAT { + if err := vp.deployShootCSICompatibilityMode(ctx, cp.Namespace, values); err != nil { + return nil, fmt.Errorf("deploy shoot CSI compatibility mode: %w", err) + } + } default: return nil, fmt.Errorf("unsupported CSI driver type: %s", csiDriverInUse) } @@ -1194,14 +1199,13 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na return nil } + // TODO: constant chartName := "stackit-blockstorage-csi-driver" - foo := map[string]interface{}{} - foo = maps.Clone(values) // Get the chart Values - csiStackitValues := foo[openstack.CSISTACKITControllerName].(map[string]interface{}) + csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]any) // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key - chartValues := gardenerutils.MergeMaps(foo, csiStackitValues) + chartValues := gardenerutils.MergeMaps(values, csiStackitValues) // Override chart values chartValues["prefix"] = "stackit-compat" @@ -1213,9 +1217,10 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na "csi-snapshotter", "csi-resizer", "csi-liveness-probe", - "csi-snapshot-controller"} + "csi-snapshot-controller", + } images := imagevector.ImageVector() - imageMap := make(map[string]interface{}) + imageMap := make(map[string]any) for _, image := range imagesToFind { foundImage, err := images.FindImage(image) @@ -1244,6 +1249,62 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na ) } +func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any) error { + renderer, err := chartrenderer.NewForConfig(vp.config) + if err != nil { + return err + } + + // TODO: constant + chartName := "stackit-blockstorage-csi-driver" + + // Get the chart Values + csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]any) + // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key + chartValues := gardenerutils.MergeMaps(values, csiStackitValues) + // Override chart values + chartValues["prefix"] = "stackit-compat" + + //TODO: Use gardener tools for this? If possible + imagesToFind := []string{ + "csi-driver-stackit", + "csi-provisioner", + "csi-attacher", + "csi-snapshotter", + "csi-resizer", + "csi-liveness-probe", + "csi-snapshot-controller", + } + images := imagevector.ImageVector() + imageMap := make(map[string]any) + + for _, image := range imagesToFind { + foundImage, err := images.FindImage(image) + if err != nil { + return err + } + imageMap[image] = foundImage.String() + } + chartValues["images"] = imageMap + + renderedChart, err := renderer.RenderEmbeddedFS( + charts.InternalChart, + filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), + chartName, + namespace, + chartValues, + ) + if err != nil { + return err + } + + data := renderedChart.AsSecretData() + return managedresources.Create( + ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart", map[string]string{}, + false, "shoot", data, new(false), nil, new(false), + ) +} + // decodeLoadBalancerAPIEmergencySecret decodes a [corev1.Secret] for emergency loadbalancer access and // returns the apiURL and apiToken to use or an error. // The apiURL and apiToken are only set if both values exist inside the secret and are not empty. From 00827ca07436b6ebf2fd82adb5993581bcb66659 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Wed, 17 Jun 2026 14:13:51 +0200 Subject: [PATCH 17/49] use the correct image vector for the Shoot controlplane --- pkg/controller/controlplane/valuesprovider.go | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 57f094a1..af0a341b 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1268,12 +1268,8 @@ func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, n //TODO: Use gardener tools for this? If possible imagesToFind := []string{ "csi-driver-stackit", - "csi-provisioner", - "csi-attacher", - "csi-snapshotter", - "csi-resizer", + "csi-node-driver-registrar", "csi-liveness-probe", - "csi-snapshot-controller", } images := imagevector.ImageVector() imageMap := make(map[string]any) From 5b71759186f5f71c9f7e16e232128a8f30a69097 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 18 Jun 2026 12:46:55 +0200 Subject: [PATCH 18/49] delete compatibility mode managed resources when not needed --- pkg/controller/controlplane/valuesprovider.go | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index af0a341b..a8b30333 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -765,11 +765,17 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf "enabled": false, } // TODO: make it nice - if getCSICompatibilityMode(cpConfig) == stackitv1alpha1.COMPAT { + if getCSICompatibilityMode(cpConfig) != stackitv1alpha1.DEFAULT { + // TODO: handle COMPATBLOCK err := vp.deploySeedCSICompatibilityMode(ctx, cp.GetNamespace(), controlPlaneValues) if err != nil { return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) } + } else { + err := vp.deleteSeedCSICompatibilityMode(ctx, cp.GetNamespace()) + if err != nil { + return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) + } } default: return nil, fmt.Errorf("unsupported storage CSI Driver: %s", storageCSIDriver) @@ -1082,16 +1088,21 @@ 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} - if getCSICompatibilityMode(cpConfig) == stackitv1alpha1.COMPAT { + case stackitv1alpha1.STACKIT: + values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues + values[openstack.CSINodeName] = map[string]any{"enabled": false} + if getCSICompatibilityMode(cpConfig) != stackitv1alpha1.DEFAULT { + // TODO: handle COMPATBLOCk if err := vp.deployShootCSICompatibilityMode(ctx, cp.Namespace, values); err != nil { return nil, fmt.Errorf("deploy shoot CSI compatibility mode: %w", err) } + } else { + if err := vp.deleteShootCSICompatibilityMode(ctx, cp.Namespace); err != nil { + return nil, fmt.Errorf("delete shoot CSI compatibility mode: %w", err) + } } default: return nil, fmt.Errorf("unsupported CSI driver type: %s", csiDriverInUse) @@ -1249,6 +1260,10 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na ) } +func (vp *valuesProvider) deleteSeedCSICompatibilityMode(ctx context.Context, namespace string) error { + return managedresources.Delete(ctx, vp.client, namespace, "stackit-csi-compat-chart", false) +} + func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any) error { renderer, err := chartrenderer.NewForConfig(vp.config) if err != nil { @@ -1301,6 +1316,10 @@ func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, n ) } +func (vp *valuesProvider) deleteShootCSICompatibilityMode(ctx context.Context, namespace string) error { + return managedresources.Delete(ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart", false) +} + // decodeLoadBalancerAPIEmergencySecret decodes a [corev1.Secret] for emergency loadbalancer access and // returns the apiURL and apiToken to use or an error. // The apiURL and apiToken are only set if both values exist inside the secret and are not empty. From 14e2fcce64fb245315a9f302e3827c70fdb89b45 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 18 Jun 2026 15:31:04 +0200 Subject: [PATCH 19/49] control the legacy-volume-creation flag --- .../templates/daemonset.yaml | 10 ++++-- .../values.yaml | 6 ++++ pkg/controller/controlplane/valuesprovider.go | 31 ++++++++++--------- 3 files changed, 31 insertions(+), 16 deletions(-) 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..103209a8 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 @@ -48,6 +48,12 @@ spec: {{- end }} - --v=2 - --provide-controller-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 }} @@ -62,7 +68,7 @@ spec: allowPrivilegeEscalation: true ports: - name: healthz - containerPort: 9908 + containerPort: {{ .Values.healthzPort }} protocol: TCP livenessProbe: httpGet: @@ -112,7 +118,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 }} 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..d2ac0bad 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,12 @@ driverName: block-storage.csi.stackit.cloud rescanBlockStorageOnResize: "true" +healthzPort: 9908 + +csi: + enableCompatibilityMode: false + blockLegacyCreation: false + images: csi-driver-stackit: image-repository:image-tag csi-node-driver-registrar: image-repository:image-tag diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index a8b30333..a0d5c182 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1094,9 +1094,10 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c case stackitv1alpha1.STACKIT: values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues values[openstack.CSINodeName] = map[string]any{"enabled": false} - if getCSICompatibilityMode(cpConfig) != stackitv1alpha1.DEFAULT { - // TODO: handle COMPATBLOCk - if err := vp.deployShootCSICompatibilityMode(ctx, cp.Namespace, values); err != nil { + compatibilityMode := getCSICompatibilityMode(cpConfig) + if compatibilityMode != stackitv1alpha1.DEFAULT { + blockLegacyCreation := (compatibilityMode == stackitv1alpha1.COMPATBLOCK) + if err := vp.deployShootCSICompatibilityMode(ctx, cp.Namespace, values, blockLegacyCreation); err != nil { return nil, fmt.Errorf("deploy shoot CSI compatibility mode: %w", err) } } else { @@ -1254,17 +1255,14 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na } data := renderedChart.AsSecretData() - return managedresources.Create( - ctx, vp.client, namespace, "stackit-csi-compat-chart", map[string]string{}, - false, "seed", data, new(false), nil, new(false), - ) + return managedresources.CreateForSeed(ctx, vp.client, namespace, "stackit-csi-compat-chart", false, data) } func (vp *valuesProvider) deleteSeedCSICompatibilityMode(ctx context.Context, namespace string) error { - return managedresources.Delete(ctx, vp.client, namespace, "stackit-csi-compat-chart", false) + return managedresources.DeleteForSeed(ctx, vp.client, namespace, "stackit-csi-compat-chart") } -func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any) error { +func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any, blockLegacyCreation bool) error { renderer, err := chartrenderer.NewForConfig(vp.config) if err != nil { return err @@ -1297,6 +1295,14 @@ func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, n imageMap[image] = foundImage.String() } chartValues["images"] = imageMap + chartValues["healthzPort"] = 9909 + csiValues := map[string]any{ + "enableCompatibilityMode": true, + } + if blockLegacyCreation { + csiValues["blockLegacyCreation"] = true + } + chartValues["csi"] = csiValues renderedChart, err := renderer.RenderEmbeddedFS( charts.InternalChart, @@ -1310,14 +1316,11 @@ func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, n } data := renderedChart.AsSecretData() - return managedresources.Create( - ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart", map[string]string{}, - false, "shoot", data, new(false), nil, new(false), - ) + return managedresources.CreateForShoot(ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart", "gardener", false, data) } func (vp *valuesProvider) deleteShootCSICompatibilityMode(ctx context.Context, namespace string) error { - return managedresources.Delete(ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart", false) + return managedresources.DeleteForShoot(ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart") } // decodeLoadBalancerAPIEmergencySecret decodes a [corev1.Secret] for emergency loadbalancer access and From 6f827936e5582ba9f154747fef1998231a9f1739 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 18 Jun 2026 16:50:55 +0200 Subject: [PATCH 20/49] make sure to use the correct namespace and origin --- pkg/controller/controlplane/valuesprovider.go | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index a0d5c182..80e67119 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -765,14 +765,14 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf "enabled": false, } // TODO: make it nice + namespace := cp.Namespace if getCSICompatibilityMode(cpConfig) != stackitv1alpha1.DEFAULT { - // TODO: handle COMPATBLOCK - err := vp.deploySeedCSICompatibilityMode(ctx, cp.GetNamespace(), controlPlaneValues) + err := vp.deploySeedCSICompatibilityMode(ctx, namespace, controlPlaneValues) if err != nil { return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) } } else { - err := vp.deleteSeedCSICompatibilityMode(ctx, cp.GetNamespace()) + err := vp.deleteSeedCSICompatibilityMode(ctx, namespace) if err != nil { return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) } @@ -1095,13 +1095,14 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues values[openstack.CSINodeName] = map[string]any{"enabled": false} compatibilityMode := getCSICompatibilityMode(cpConfig) + namespace := cp.Namespace if compatibilityMode != stackitv1alpha1.DEFAULT { - blockLegacyCreation := (compatibilityMode == stackitv1alpha1.COMPATBLOCK) - if err := vp.deployShootCSICompatibilityMode(ctx, cp.Namespace, values, blockLegacyCreation); err != nil { + blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK + if err := vp.deployShootCSICompatibilityMode(ctx, namespace, values, blockLegacyCreation); err != nil { return nil, fmt.Errorf("deploy shoot CSI compatibility mode: %w", err) } } else { - if err := vp.deleteShootCSICompatibilityMode(ctx, cp.Namespace); err != nil { + if err := vp.deleteShootCSICompatibilityMode(ctx, namespace); err != nil { return nil, fmt.Errorf("delete shoot CSI compatibility mode: %w", err) } } @@ -1247,7 +1248,7 @@ func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, na charts.InternalChart, filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), chartName, - namespace, + "kube-system", chartValues, ) if err != nil { @@ -1308,7 +1309,7 @@ func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, n charts.InternalChart, filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), chartName, - namespace, + "kube-system", chartValues, ) if err != nil { @@ -1316,7 +1317,7 @@ func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, n } data := renderedChart.AsSecretData() - return managedresources.CreateForShoot(ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart", "gardener", false, data) + return managedresources.CreateForShoot(ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart", "gardener-extension-provider-stackit", false, data) } func (vp *valuesProvider) deleteShootCSICompatibilityMode(ctx context.Context, namespace string) error { From b353e21011e3dab487151296aaee70f195f6ce0a Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Fri, 19 Jun 2026 13:08:54 +0200 Subject: [PATCH 21/49] make generate --- hack/api-reference/api.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/hack/api-reference/api.md b/hack/api-reference/api.md index 6169a6c6..773e7d08 100644 --- a/hack/api-reference/api.md +++ b/hack/api-reference/api.md @@ -231,6 +231,16 @@ string +

CSICompatibilityMode +

+

Underlying type: string

+ + +

+ +

+ +

CSIManila

From 285362059badcffd67b5cbd9ce6dbd4babfb7978 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Mon, 22 Jun 2026 15:42:49 +0200 Subject: [PATCH 22/49] extract the CSI compatibility related code and use dependency injections to minimize the impact on existing test cases --- pkg/controller/controlplane/add.go | 3 +- .../controlplane/csi_compatibility.go | 178 ++++++++++++++++++ pkg/controller/controlplane/valuesprovider.go | 159 ++-------------- .../controlplane/valuesprovider_test.go | 15 +- 4 files changed, 206 insertions(+), 149 deletions(-) create mode 100644 pkg/controller/controlplane/csi_compatibility.go diff --git a/pkg/controller/controlplane/add.go b/pkg/controller/controlplane/add.go index a0e11e6f..980766c9 100644 --- a/pkg/controller/controlplane/add.go +++ b/pkg/controller/controlplane/add.go @@ -46,7 +46,8 @@ func AddToManagerWithOptions(ctx context.Context, mgr manager.Manager, opts AddO 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, NewCompatCSICompatibilityHandler(mgr.GetClient(), mgr.GetConfig())), + 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..5dcb5986 --- /dev/null +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -0,0 +1,178 @@ +package controlplane + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/gardener/gardener/pkg/chartrenderer" + gardenerutils "github.com/gardener/gardener/pkg/utils" + "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" +) + +func NewCompatCSICompatibilityHandler(client client.Client, config *rest.Config) *CompatCSICompatibilityHandler { + return &CompatCSICompatibilityHandler{ + client: client, + config: config, + } +} + +type CompatCSICompatibilityHandler struct { + client client.Client + config *rest.Config +} + +func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, controlPlaneValues map[string]any) error { + if getCSICompatibilityMode(cpConfig) != stackitv1alpha1.DEFAULT { + err := ch.deploySeedCSICompatibilityMode(ctx, namespace, controlPlaneValues) + if err != nil { + return fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) + } + } else { + err := ch.deleteSeedCSICompatibilityMode(ctx, namespace) + if err != nil { + return fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) + } + } + return nil +} + +func (ch *CompatCSICompatibilityHandler) deploySeedCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any) error { + renderer, err := chartrenderer.NewForConfig(ch.config) + if err != nil { + return nil + } + + // TODO: constant + chartName := "stackit-blockstorage-csi-driver" + + // Get the chart Values + csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]any) + // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key + chartValues := gardenerutils.MergeMaps(values, csiStackitValues) + // Override chart values + chartValues["prefix"] = "stackit-compat" + + //TODO: Use gardener tools for this? If possible + imagesToFind := []string{ + "csi-driver-stackit", + "csi-provisioner", + "csi-attacher", + "csi-snapshotter", + "csi-resizer", + "csi-liveness-probe", + "csi-snapshot-controller", + } + images := imagevector.ImageVector() + imageMap := make(map[string]any) + + for _, image := range imagesToFind { + foundImage, err := images.FindImage(image) + if err != nil { + return err + } + imageMap[image] = foundImage.String() + } + chartValues["images"] = imageMap + + renderedChart, err := renderer.RenderEmbeddedFS( + charts.InternalChart, + filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), + chartName, + "kube-system", + chartValues, + ) + if err != nil { + return err + } + + data := renderedChart.AsSecretData() + return managedresources.CreateForSeed(ctx, ch.client, namespace, "stackit-csi-compat-chart", false, data) +} + +func (ch *CompatCSICompatibilityHandler) deleteSeedCSICompatibilityMode(ctx context.Context, namespace string) error { + return managedresources.DeleteForSeed(ctx, ch.client, namespace, "stackit-csi-compat-chart") +} + +func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, values map[string]any) error { + compatibilityMode := getCSICompatibilityMode(cpConfig) + if compatibilityMode != stackitv1alpha1.DEFAULT { + blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK + if err := ch.deployShootCSICompatibilityMode(ctx, namespace, values, blockLegacyCreation); err != nil { + return fmt.Errorf("deploy shoot CSI compatibility mode: %w", err) + } + } else { + if err := ch.deleteShootCSICompatibilityMode(ctx, namespace); err != nil { + return fmt.Errorf("delete shoot CSI compatibility mode: %w", err) + } + } + return nil +} + +func (ch *CompatCSICompatibilityHandler) deployShootCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any, blockLegacyCreation bool) error { + renderer, err := chartrenderer.NewForConfig(ch.config) + if err != nil { + return err + } + + // TODO: constant + chartName := "stackit-blockstorage-csi-driver" + + // Get the chart Values + csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]any) + // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key + chartValues := gardenerutils.MergeMaps(values, csiStackitValues) + // Override chart values + chartValues["prefix"] = "stackit-compat" + + //TODO: Use gardener tools for this? If possible + imagesToFind := []string{ + "csi-driver-stackit", + "csi-node-driver-registrar", + "csi-liveness-probe", + } + images := imagevector.ImageVector() + imageMap := make(map[string]any) + + for _, image := range imagesToFind { + foundImage, err := images.FindImage(image) + if err != nil { + return err + } + imageMap[image] = foundImage.String() + } + chartValues["images"] = imageMap + chartValues["healthzPort"] = 9909 + csiValues := map[string]any{ + "enableCompatibilityMode": true, + } + if blockLegacyCreation { + csiValues["blockLegacyCreation"] = true + } + chartValues["csi"] = csiValues + + renderedChart, err := renderer.RenderEmbeddedFS( + charts.InternalChart, + filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), + chartName, + "kube-system", + chartValues, + ) + if err != nil { + return err + } + + data := renderedChart.AsSecretData() + return managedresources.CreateForShoot(ctx, ch.client, namespace, "stackit-csi-compat-shoot-chart", "gardener-extension-provider-stackit", false, data) +} + +func (ch *CompatCSICompatibilityHandler) deleteShootCSICompatibilityMode(ctx context.Context, namespace string) error { + return managedresources.DeleteForShoot(ctx, ch.client, namespace, "stackit-csi-compat-shoot-chart") +} diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 80e67119..ea9d5c94 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -21,12 +21,10 @@ import ( "github.com/gardener/gardener/pkg/apis/core/v1beta1" v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" extensionsv1alpha1 "github.com/gardener/gardener/pkg/apis/extensions/v1alpha1" - "github.com/gardener/gardener/pkg/chartrenderer" gardenerutils "github.com/gardener/gardener/pkg/utils" "github.com/gardener/gardener/pkg/utils/chart" gutil "github.com/gardener/gardener/pkg/utils/gardener" kutil "github.com/gardener/gardener/pkg/utils/kubernetes" - "github.com/gardener/gardener/pkg/utils/managedresources" secretutils "github.com/gardener/gardener/pkg/utils/secrets" secretsmanager "github.com/gardener/gardener/pkg/utils/secrets/manager" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" @@ -358,14 +356,19 @@ var ( } ) +type CSICompatibilityHandler interface { + HandleSeedCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error + HandleShootCSICompatility(context.Context, 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(), - config: mgr.GetConfig(), decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(), deployALBIngressController: deployALBIngressController, customLabelDomain: customLabelDomain, + csiCompatibilityHandler: csiCompatibilityHandler, } } @@ -377,6 +380,7 @@ type valuesProvider struct { decoder runtime.Decoder deployALBIngressController bool customLabelDomain string + csiCompatibilityHandler CSICompatibilityHandler } // GetConfigChartValues returns the values for the config chart applied by the generic actuator. @@ -764,18 +768,9 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf controlPlaneValues[openstack.CSIControllerName] = map[string]any{ "enabled": false, } - // TODO: make it nice - namespace := cp.Namespace - if getCSICompatibilityMode(cpConfig) != stackitv1alpha1.DEFAULT { - err := vp.deploySeedCSICompatibilityMode(ctx, namespace, controlPlaneValues) - if err != nil { - return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) - } - } else { - err := vp.deleteSeedCSICompatibilityMode(ctx, namespace) - if err != nil { - return nil, fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) - } + err := vp.csiCompatibilityHandler.HandleSeedCSICompatibility(ctx, cp.Namespace, cpConfig, controlPlaneValues) + if err != nil { + return nil, err } default: return nil, fmt.Errorf("unsupported storage CSI Driver: %s", storageCSIDriver) @@ -1094,17 +1089,9 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c case stackitv1alpha1.STACKIT: values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues values[openstack.CSINodeName] = map[string]any{"enabled": false} - compatibilityMode := getCSICompatibilityMode(cpConfig) - namespace := cp.Namespace - if compatibilityMode != stackitv1alpha1.DEFAULT { - blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK - if err := vp.deployShootCSICompatibilityMode(ctx, namespace, values, blockLegacyCreation); err != nil { - return nil, fmt.Errorf("deploy shoot CSI compatibility mode: %w", err) - } - } else { - if err := vp.deleteShootCSICompatibilityMode(ctx, namespace); err != nil { - return nil, fmt.Errorf("delete shoot CSI compatibility mode: %w", err) - } + err := vp.csiCompatibilityHandler.HandleShootCSICompatility(ctx, cp.Namespace, cpConfig, values) + if err != nil { + return nil, err } default: return nil, fmt.Errorf("unsupported CSI driver type: %s", csiDriverInUse) @@ -1206,124 +1193,6 @@ func (vp *valuesProvider) checkEmergencyLoadBalancerAccess(ctx context.Context, return apiURL, apiToken, nil } -func (vp *valuesProvider) deploySeedCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any) error { - renderer, err := chartrenderer.NewForConfig(vp.config) - if err != nil { - return nil - } - - // TODO: constant - chartName := "stackit-blockstorage-csi-driver" - - // Get the chart Values - csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]any) - // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key - chartValues := gardenerutils.MergeMaps(values, csiStackitValues) - // Override chart values - chartValues["prefix"] = "stackit-compat" - - //TODO: Use gardener tools for this? If possible - imagesToFind := []string{ - "csi-driver-stackit", - "csi-provisioner", - "csi-attacher", - "csi-snapshotter", - "csi-resizer", - "csi-liveness-probe", - "csi-snapshot-controller", - } - images := imagevector.ImageVector() - imageMap := make(map[string]any) - - for _, image := range imagesToFind { - foundImage, err := images.FindImage(image) - if err != nil { - return err - } - imageMap[image] = foundImage.String() - } - chartValues["images"] = imageMap - - renderedChart, err := renderer.RenderEmbeddedFS( - charts.InternalChart, - filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), - chartName, - "kube-system", - chartValues, - ) - if err != nil { - return err - } - - data := renderedChart.AsSecretData() - return managedresources.CreateForSeed(ctx, vp.client, namespace, "stackit-csi-compat-chart", false, data) -} - -func (vp *valuesProvider) deleteSeedCSICompatibilityMode(ctx context.Context, namespace string) error { - return managedresources.DeleteForSeed(ctx, vp.client, namespace, "stackit-csi-compat-chart") -} - -func (vp *valuesProvider) deployShootCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any, blockLegacyCreation bool) error { - renderer, err := chartrenderer.NewForConfig(vp.config) - if err != nil { - return err - } - - // TODO: constant - chartName := "stackit-blockstorage-csi-driver" - - // Get the chart Values - csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]any) - // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key - chartValues := gardenerutils.MergeMaps(values, csiStackitValues) - // Override chart values - chartValues["prefix"] = "stackit-compat" - - //TODO: Use gardener tools for this? If possible - imagesToFind := []string{ - "csi-driver-stackit", - "csi-node-driver-registrar", - "csi-liveness-probe", - } - images := imagevector.ImageVector() - imageMap := make(map[string]any) - - for _, image := range imagesToFind { - foundImage, err := images.FindImage(image) - if err != nil { - return err - } - imageMap[image] = foundImage.String() - } - chartValues["images"] = imageMap - chartValues["healthzPort"] = 9909 - csiValues := map[string]any{ - "enableCompatibilityMode": true, - } - if blockLegacyCreation { - csiValues["blockLegacyCreation"] = true - } - chartValues["csi"] = csiValues - - renderedChart, err := renderer.RenderEmbeddedFS( - charts.InternalChart, - filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), - chartName, - "kube-system", - chartValues, - ) - if err != nil { - return err - } - - data := renderedChart.AsSecretData() - return managedresources.CreateForShoot(ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart", "gardener-extension-provider-stackit", false, data) -} - -func (vp *valuesProvider) deleteShootCSICompatibilityMode(ctx context.Context, namespace string) error { - return managedresources.DeleteForShoot(ctx, vp.client, namespace, "stackit-csi-compat-shoot-chart") -} - // decodeLoadBalancerAPIEmergencySecret decodes a [corev1.Secret] for emergency loadbalancer access and // returns the apiURL and apiToken to use or an error. // The apiURL and apiToken are only set if both values exist inside the secret and are not empty. diff --git a/pkg/controller/controlplane/valuesprovider_test.go b/pkg/controller/controlplane/valuesprovider_test.go index d7da5fb4..ba05b79e 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, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { + return nil +} +func (*noopCSICompatibilityHandler) HandleShootCSICompatility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { + return nil +} From ce762b9ea9815bcfefed69f4562bf700e411408b Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Mon, 22 Jun 2026 16:39:36 +0200 Subject: [PATCH 23/49] fix typo --- pkg/controller/controlplane/csi_compatibility.go | 2 +- pkg/controller/controlplane/valuesprovider.go | 6 +++--- pkg/controller/controlplane/valuesprovider_test.go | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index 5dcb5986..e6c29b46 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -101,7 +101,7 @@ func (ch *CompatCSICompatibilityHandler) deleteSeedCSICompatibilityMode(ctx cont return managedresources.DeleteForSeed(ctx, ch.client, namespace, "stackit-csi-compat-chart") } -func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, values map[string]any) error { +func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, values map[string]any) error { compatibilityMode := getCSICompatibilityMode(cpConfig) if compatibilityMode != stackitv1alpha1.DEFAULT { blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index ea9d5c94..8ca99d19 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -358,7 +358,7 @@ var ( type CSICompatibilityHandler interface { HandleSeedCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error - HandleShootCSICompatility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error + HandleShootCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error } // NewValuesProvider creates a new ValuesProvider for the generic actuator. @@ -1089,7 +1089,7 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c case stackitv1alpha1.STACKIT: values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues values[openstack.CSINodeName] = map[string]any{"enabled": false} - err := vp.csiCompatibilityHandler.HandleShootCSICompatility(ctx, cp.Namespace, cpConfig, values) + err := vp.csiCompatibilityHandler.HandleShootCSICompatibility(ctx, cp.Namespace, cpConfig, values) if err != nil { return nil, err } @@ -1234,7 +1234,7 @@ func getCSIDriver(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1. } func getCSICompatibilityMode(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1.CSICompatibilityMode { - return stackitv1alpha1.CSICompatibilityMode(cpConfig.Storage.CSI.CompatibilityMode) + return stackitv1alpha1.CSICompatibilityMode(cpConfig.Storage.CSI.Name) } func getCCMController(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1.ControllerName { diff --git a/pkg/controller/controlplane/valuesprovider_test.go b/pkg/controller/controlplane/valuesprovider_test.go index ba05b79e..ac14253f 100644 --- a/pkg/controller/controlplane/valuesprovider_test.go +++ b/pkg/controller/controlplane/valuesprovider_test.go @@ -1148,6 +1148,6 @@ type noopCSICompatibilityHandler struct{} func (*noopCSICompatibilityHandler) HandleSeedCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { return nil } -func (*noopCSICompatibilityHandler) HandleShootCSICompatility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { +func (*noopCSICompatibilityHandler) HandleShootCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { return nil } From a8ed75e199aa45cd5d1789b261656430049894ee Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Tue, 23 Jun 2026 13:07:43 +0200 Subject: [PATCH 24/49] add a unit test scaffolding --- .../controlplane/csi_compatibility_test.go | 234 ++++++++++++++++++ 1 file changed, 234 insertions(+) create mode 100644 pkg/controller/controlplane/csi_compatibility_test.go diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go new file mode 100644 index 00000000..da45c6ed --- /dev/null +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -0,0 +1,234 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package controlplane + +import ( + "bytes" + "context" + "io" + "net/http" + + 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" + corev1 "k8s.io/api/core/v1" + 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" + + 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 + ) + + BeforeEach(func() { + ctx = context.Background() + namespace = "test-namespace" + + fakeClient = fakeclient.NewClientBuilder(). + WithScheme(kubernetes.SeedScheme). + Build() + + config = &rest.Config{ + Host: "https://localhost", + Transport: &mockRoundTripper{}, + } + + handler = NewCompatCSICompatibilityHandler(fakeClient, config) + }) + + 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{ + Name: string(stackitv1alpha1.DEFAULT), + }, + }, + } + + // Create the managed resource and secret beforehand to ensure deletion works + mr := &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stackit-csi-compat-chart", + Namespace: namespace, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "managedresource-stackit-csi-compat-chart", + Namespace: namespace, + }, + } + Expect(fakeClient.Create(ctx, mr)).To(Succeed()) + Expect(fakeClient.Create(ctx, secret)).To(Succeed()) + + err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, nil) + Expect(err).NotTo(HaveOccurred()) + + // Check deletion + err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-chart", Namespace: namespace}, mr) + Expect(client.IgnoreNotFound(err)).To(Succeed()) + Expect(err).ToNot(Succeed()) + }) + }) + + Context("when CSICompatibilityMode is COMPAT", func() { + It("should deploy the seed csi compatibility mode", func() { + cpConfig := &stackitv1alpha1.ControlPlaneConfig{ + Storage: &stackitv1alpha1.Storage{ + CSI: &stackitv1alpha1.CSI{ + Name: string(stackitv1alpha1.COMPAT), + }, + }, + } + + controlPlaneValues := map[string]any{ + "global": map[string]any{ + "genericTokenKubeconfigSecretName": "generic-token-kubeconfig-92e9ae14", + }, + openstack.CSISTACKITControllerName: map[string]any{ + "foo": "bar", + }, + } + + err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + mr := &resourcesv1alpha1.ManagedResource{} + err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-chart", Namespace: namespace}, mr) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) + + 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{ + Name: string(stackitv1alpha1.DEFAULT), + }, + }, + } + + // Create the managed resource and secret beforehand to ensure deletion works + mr := &resourcesv1alpha1.ManagedResource{ + ObjectMeta: metav1.ObjectMeta{ + Name: "stackit-csi-compat-shoot-chart", + Namespace: namespace, + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "managedresource-stackit-csi-compat-shoot-chart", + Namespace: namespace, + }, + } + Expect(fakeClient.Create(ctx, mr)).To(Succeed()) + Expect(fakeClient.Create(ctx, secret)).To(Succeed()) + + err := handler.HandleShootCSICompatibility(ctx, namespace, cpConfig, nil) + Expect(err).NotTo(HaveOccurred()) + + // Check deletion + err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) + Expect(client.IgnoreNotFound(err)).To(Succeed()) + Expect(err).ToNot(Succeed()) + }) + }) + + 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{ + Name: string(stackitv1alpha1.COMPAT), + }, + }, + } + + 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, cpConfig, values) + Expect(err).NotTo(HaveOccurred()) + + mr := &resourcesv1alpha1.ManagedResource{} + err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) + Expect(err).NotTo(HaveOccurred()) + }) + }) + + 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{ + Name: 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, cpConfig, values) + Expect(err).NotTo(HaveOccurred()) + + mr := &resourcesv1alpha1.ManagedResource{} + err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) + Expect(err).NotTo(HaveOccurred()) + }) + }) + }) +}) From 16445c6be48b2bb08775dd4fc4cd25d1f709d6ab Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Tue, 23 Jun 2026 14:26:16 +0200 Subject: [PATCH 25/49] add checks for the relevant command line flags in the DaemonSets --- .../controlplane/csi_compatibility_test.go | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index da45c6ed..6250a1f7 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -7,19 +7,23 @@ 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" 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" @@ -72,6 +76,38 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { handler = NewCompatCSICompatibilityHandler(fakeClient, config) }) + getDaemonSetFromSecret := func(prefix 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, prefix) { + matchedSecret = &s + break + } + } + + if matchedSecret == nil { + Fail(fmt.Sprintf("Secret starting with prefix %s not found. Found secrets: %v", prefix, names)) + } + + for _, data := range matchedSecret.Data { + docs := bytes.Split(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 + } + Describe("#HandleSeedCSICompatibility", func() { Context("when CSICompatibilityMode is DEFAULT", func() { It("should delete the managed resource", func() { @@ -200,6 +236,18 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { mr := &resourcesv1alpha1.ManagedResource{} err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) Expect(err).NotTo(HaveOccurred()) + + ds := getDaemonSetFromSecret("managedresource-stackit-csi-compat-shoot-chart-") + var csiContainer *corev1.Container + for i := range ds.Spec.Template.Spec.Containers { + if ds.Spec.Template.Spec.Containers[i].Name == "csi-driver-stackit" { + csiContainer = &ds.Spec.Template.Spec.Containers[i] + break + } + } + Expect(csiContainer).NotTo(BeNil(), "csi-driver-stackit container not found") + Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) + Expect(csiContainer.Args).NotTo(ContainElement("--legacy-volume-creation=false")) }) }) @@ -228,6 +276,18 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { mr := &resourcesv1alpha1.ManagedResource{} err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) Expect(err).NotTo(HaveOccurred()) + + ds := getDaemonSetFromSecret("managedresource-stackit-csi-compat-shoot-chart-") + var csiContainer *corev1.Container + for i := range ds.Spec.Template.Spec.Containers { + if ds.Spec.Template.Spec.Containers[i].Name == "csi-driver-stackit" { + csiContainer = &ds.Spec.Template.Spec.Containers[i] + break + } + } + Expect(csiContainer).NotTo(BeNil(), "csi-driver-stackit container not found") + Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) + Expect(csiContainer.Args).To(ContainElement("--legacy-volume-creation=false")) }) }) }) From 0fe76532e96b11e2474ca87ba8ed413ff275b143 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Tue, 23 Jun 2026 14:32:58 +0200 Subject: [PATCH 26/49] extract function --- .../controlplane/csi_compatibility_test.go | 98 ++++++++++++------- 1 file changed, 65 insertions(+), 33 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index 6250a1f7..d340c889 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -76,37 +76,37 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { handler = NewCompatCSICompatibilityHandler(fakeClient, config) }) - getDaemonSetFromSecret := func(prefix 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, prefix) { - matchedSecret = &s - break - } - } - - if matchedSecret == nil { - Fail(fmt.Sprintf("Secret starting with prefix %s not found. Found secrets: %v", prefix, names)) - } - - for _, data := range matchedSecret.Data { - docs := bytes.Split(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 - } + // getDaemonSetFromSecret := func(prefix 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, prefix) { + // matchedSecret = &s + // break + // } + // } + + // if matchedSecret == nil { + // Fail(fmt.Sprintf("Secret starting with prefix %s not found. Found secrets: %v", prefix, names)) + // } + + // for _, data := range matchedSecret.Data { + // docs := bytes.Split(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 + // } Describe("#HandleSeedCSICompatibility", func() { Context("when CSICompatibilityMode is DEFAULT", func() { @@ -237,7 +237,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) Expect(err).NotTo(HaveOccurred()) - ds := getDaemonSetFromSecret("managedresource-stackit-csi-compat-shoot-chart-") + ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-stackit-csi-compat-shoot-chart-") var csiContainer *corev1.Container for i := range ds.Spec.Template.Spec.Containers { if ds.Spec.Template.Spec.Containers[i].Name == "csi-driver-stackit" { @@ -277,7 +277,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) Expect(err).NotTo(HaveOccurred()) - ds := getDaemonSetFromSecret("managedresource-stackit-csi-compat-shoot-chart-") + ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-stackit-csi-compat-shoot-chart-") var csiContainer *corev1.Container for i := range ds.Spec.Template.Spec.Containers { if ds.Spec.Template.Spec.Containers[i].Name == "csi-driver-stackit" { @@ -292,3 +292,35 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { }) }) }) + +func getDaemonSetFromSecret(ctx context.Context, fakeClient client.Client, namespace string, prefix 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, prefix) { + matchedSecret = &s + break + } + } + + if matchedSecret == nil { + Fail(fmt.Sprintf("Secret starting with prefix %s not found. Found secrets: %v", prefix, names)) + } + + for _, data := range matchedSecret.Data { + docs := bytes.Split(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 +} From f5bc1317649af02a9946e3a96f44569abbae7a0c Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Tue, 23 Jun 2026 14:57:33 +0200 Subject: [PATCH 27/49] separate chart rendering and deployment --- .../controlplane/csi_compatibility.go | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index e6c29b46..ecc548b1 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -31,23 +31,27 @@ type CompatCSICompatibilityHandler struct { func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, controlPlaneValues map[string]any) error { if getCSICompatibilityMode(cpConfig) != stackitv1alpha1.DEFAULT { - err := ch.deploySeedCSICompatibilityMode(ctx, namespace, controlPlaneValues) + chart, err := ch.renderSeedCSICompatibilityMode(controlPlaneValues) if err != nil { - return fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) + 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) } } else { err := ch.deleteSeedCSICompatibilityMode(ctx, namespace) if err != nil { - return fmt.Errorf("failed to deploy CSI CSI compatibility mode: %w", err) + return fmt.Errorf("failed to deploy seed CSI compatibility mode: %w", err) } } return nil } -func (ch *CompatCSICompatibilityHandler) deploySeedCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any) error { +func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values map[string]any) (*chartrenderer.RenderedChart, error) { renderer, err := chartrenderer.NewForConfig(ch.config) if err != nil { - return nil + return nil, err } // TODO: constant @@ -76,23 +80,22 @@ func (ch *CompatCSICompatibilityHandler) deploySeedCSICompatibilityMode(ctx cont for _, image := range imagesToFind { foundImage, err := images.FindImage(image) if err != nil { - return err + return nil, err } imageMap[image] = foundImage.String() } chartValues["images"] = imageMap - renderedChart, err := renderer.RenderEmbeddedFS( + return renderer.RenderEmbeddedFS( charts.InternalChart, filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), chartName, "kube-system", chartValues, ) - if err != nil { - return err - } +} +func (ch *CompatCSICompatibilityHandler) deploySeedCSICompatibilityMode(ctx context.Context, namespace string, renderedChart *chartrenderer.RenderedChart) error { data := renderedChart.AsSecretData() return managedresources.CreateForSeed(ctx, ch.client, namespace, "stackit-csi-compat-chart", false, data) } @@ -105,21 +108,27 @@ func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context compatibilityMode := getCSICompatibilityMode(cpConfig) if compatibilityMode != stackitv1alpha1.DEFAULT { blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK - if err := ch.deployShootCSICompatibilityMode(ctx, namespace, values, blockLegacyCreation); err != nil { + chart, err := ch.renderShootCSICompatibilityMode(values, blockLegacyCreation) + 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) } } else { - if err := ch.deleteShootCSICompatibilityMode(ctx, namespace); err != nil { + err := ch.deleteShootCSICompatibilityMode(ctx, namespace) + if err != nil { return fmt.Errorf("delete shoot CSI compatibility mode: %w", err) } } return nil } -func (ch *CompatCSICompatibilityHandler) deployShootCSICompatibilityMode(ctx context.Context, namespace string, values map[string]any, blockLegacyCreation bool) error { +func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values map[string]any, blockLegacyCreation bool) (*chartrenderer.RenderedChart, error) { renderer, err := chartrenderer.NewForConfig(ch.config) if err != nil { - return err + return nil, err } // TODO: constant @@ -144,7 +153,7 @@ func (ch *CompatCSICompatibilityHandler) deployShootCSICompatibilityMode(ctx con for _, image := range imagesToFind { foundImage, err := images.FindImage(image) if err != nil { - return err + return nil, err } imageMap[image] = foundImage.String() } @@ -158,17 +167,16 @@ func (ch *CompatCSICompatibilityHandler) deployShootCSICompatibilityMode(ctx con } chartValues["csi"] = csiValues - renderedChart, err := renderer.RenderEmbeddedFS( + return renderer.RenderEmbeddedFS( charts.InternalChart, filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), chartName, "kube-system", chartValues, ) - if err != nil { - return err - } +} +func (ch *CompatCSICompatibilityHandler) deployShootCSICompatibilityMode(ctx context.Context, namespace string, renderedChart *chartrenderer.RenderedChart) error { data := renderedChart.AsSecretData() return managedresources.CreateForShoot(ctx, ch.client, namespace, "stackit-csi-compat-shoot-chart", "gardener-extension-provider-stackit", false, data) } From cb98a8959d4e8e900c395d7fe19d031411f4ec68 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Tue, 23 Jun 2026 16:14:07 +0200 Subject: [PATCH 28/49] create the renderer only once --- pkg/controller/controlplane/add.go | 6 +++- .../controlplane/csi_compatibility.go | 30 ++++++++----------- .../controlplane/csi_compatibility_test.go | 2 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/pkg/controller/controlplane/add.go b/pkg/controller/controlplane/add.go index 980766c9..ab687708 100644 --- a/pkg/controller/controlplane/add.go +++ b/pkg/controller/controlplane/add.go @@ -43,10 +43,14 @@ 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, NewCompatCSICompatibilityHandler(mgr.GetClient(), mgr.GetConfig())), + NewValuesProvider(mgr, DeployALBIngressController, opts.CustomLabelDomain, csiCompatibilityHandler), extensionscontroller.ChartRendererFactoryFunc(util.NewChartRendererForShoot), imagevector.ImageVector(), "", nil, opts.WebhookServerNamespace) if err != nil { diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index ecc548b1..d7414115 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -17,16 +17,20 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/openstack" ) -func NewCompatCSICompatibilityHandler(client client.Client, config *rest.Config) *CompatCSICompatibilityHandler { - return &CompatCSICompatibilityHandler{ - client: client, - config: config, +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 - config *rest.Config + client client.Client + renderer chartrenderer.Interface } func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, controlPlaneValues map[string]any) error { @@ -49,11 +53,6 @@ func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context. } func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values map[string]any) (*chartrenderer.RenderedChart, error) { - renderer, err := chartrenderer.NewForConfig(ch.config) - if err != nil { - return nil, err - } - // TODO: constant chartName := "stackit-blockstorage-csi-driver" @@ -86,7 +85,7 @@ func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values m } chartValues["images"] = imageMap - return renderer.RenderEmbeddedFS( + return ch.renderer.RenderEmbeddedFS( charts.InternalChart, filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), chartName, @@ -126,11 +125,6 @@ func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context } func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values map[string]any, blockLegacyCreation bool) (*chartrenderer.RenderedChart, error) { - renderer, err := chartrenderer.NewForConfig(ch.config) - if err != nil { - return nil, err - } - // TODO: constant chartName := "stackit-blockstorage-csi-driver" @@ -167,7 +161,7 @@ func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values } chartValues["csi"] = csiValues - return renderer.RenderEmbeddedFS( + return ch.renderer.RenderEmbeddedFS( charts.InternalChart, filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), chartName, diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index d340c889..b8e68841 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -73,7 +73,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Transport: &mockRoundTripper{}, } - handler = NewCompatCSICompatibilityHandler(fakeClient, config) + handler, _ = NewCompatCSICompatibilityHandler(fakeClient, config) }) // getDaemonSetFromSecret := func(prefix string) *appsv1.DaemonSet { From 2ca5373576088bcae43076fcfe2f40579726af1b Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Tue, 23 Jun 2026 16:36:30 +0200 Subject: [PATCH 29/49] fix the compatibility mode accessor --- pkg/controller/controlplane/csi_compatibility.go | 3 ++- pkg/controller/controlplane/valuesprovider.go | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index d7414115..1b9a72f3 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -34,7 +34,8 @@ type CompatCSICompatibilityHandler struct { } func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, controlPlaneValues map[string]any) error { - if getCSICompatibilityMode(cpConfig) != stackitv1alpha1.DEFAULT { + compatibilityMode := getCSICompatibilityMode(cpConfig) + if compatibilityMode != stackitv1alpha1.DEFAULT { chart, err := ch.renderSeedCSICompatibilityMode(controlPlaneValues) if err != nil { return fmt.Errorf("failed to render seed CSI compatibility mode: %w", err) diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 8ca99d19..86a2768f 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -1234,7 +1234,7 @@ func getCSIDriver(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1. } func getCSICompatibilityMode(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1.CSICompatibilityMode { - return stackitv1alpha1.CSICompatibilityMode(cpConfig.Storage.CSI.Name) + return stackitv1alpha1.CSICompatibilityMode(cpConfig.Storage.CSI.CompatibilityMode) } func getCCMController(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1.ControllerName { From 802210a49710c7c4e145d31bae48e06d73a9b5a7 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Wed, 24 Jun 2026 09:42:12 +0200 Subject: [PATCH 30/49] simplify test assertions --- pkg/controller/controlplane/csi_compatibility_test.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index b8e68841..fad75411 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -18,6 +18,7 @@ import ( . "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" @@ -140,8 +141,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { // Check deletion err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-chart", Namespace: namespace}, mr) - Expect(client.IgnoreNotFound(err)).To(Succeed()) - Expect(err).ToNot(Succeed()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) }) @@ -206,8 +206,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { // Check deletion err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) - Expect(client.IgnoreNotFound(err)).To(Succeed()) - Expect(err).ToNot(Succeed()) + Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) }) From 2c7b98f7bd3d6107c43ec133cb2f22a0e1c0f452 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Wed, 24 Jun 2026 11:16:58 +0200 Subject: [PATCH 31/49] extract helper methods and constants --- .../controlplane/csi_compatibility.go | 96 ++++++++------- .../controlplane/csi_compatibility_test.go | 111 ++++++------------ 2 files changed, 88 insertions(+), 119 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index 1b9a72f3..5c365765 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -17,6 +17,13 @@ import ( "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/openstack" ) +const ( + csiDriverChartName = "stackit-blockstorage-csi-driver" + csiCompatibilityPrefix = "stackit-csi-compat" + csiCompatSeedChartName = csiCompatibilityPrefix + "-chart" + csiCompatShootChartName = csiCompatibilityPrefix + "-shoort-chart" +) + func NewCompatCSICompatibilityHandler(client client.Client, config *rest.Config) (*CompatCSICompatibilityHandler, error) { renderer, err := chartrenderer.NewForConfig(config) if err != nil { @@ -54,18 +61,12 @@ func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context. } func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values map[string]any) (*chartrenderer.RenderedChart, error) { - // TODO: constant - chartName := "stackit-blockstorage-csi-driver" + chartValues := composeCompatibilityChartValues(values) - // Get the chart Values - csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]any) - // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key - chartValues := gardenerutils.MergeMaps(values, csiStackitValues) // Override chart values - chartValues["prefix"] = "stackit-compat" + chartValues["prefix"] = csiCompatibilityPrefix - //TODO: Use gardener tools for this? If possible - imagesToFind := []string{ + imageMap, err := findImages( "csi-driver-stackit", "csi-provisioner", "csi-attacher", @@ -73,23 +74,16 @@ func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values m "csi-resizer", "csi-liveness-probe", "csi-snapshot-controller", - } - images := imagevector.ImageVector() - imageMap := make(map[string]any) - - for _, image := range imagesToFind { - foundImage, err := images.FindImage(image) - if err != nil { - return nil, err - } - imageMap[image] = foundImage.String() + ) + if err != nil { + return nil, err } chartValues["images"] = imageMap return ch.renderer.RenderEmbeddedFS( charts.InternalChart, filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), - chartName, + csiDriverChartName, "kube-system", chartValues, ) @@ -97,11 +91,11 @@ func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values m func (ch *CompatCSICompatibilityHandler) deploySeedCSICompatibilityMode(ctx context.Context, namespace string, renderedChart *chartrenderer.RenderedChart) error { data := renderedChart.AsSecretData() - return managedresources.CreateForSeed(ctx, ch.client, namespace, "stackit-csi-compat-chart", false, data) + return managedresources.CreateForSeed(ctx, ch.client, namespace, csiCompatSeedChartName, false, data) } func (ch *CompatCSICompatibilityHandler) deleteSeedCSICompatibilityMode(ctx context.Context, namespace string) error { - return managedresources.DeleteForSeed(ctx, ch.client, namespace, "stackit-csi-compat-chart") + return client.IgnoreNotFound(managedresources.DeleteForSeed(ctx, ch.client, namespace, csiCompatSeedChartName)) } func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, values map[string]any) error { @@ -126,33 +120,21 @@ func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context } func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values map[string]any, blockLegacyCreation bool) (*chartrenderer.RenderedChart, error) { - // TODO: constant - chartName := "stackit-blockstorage-csi-driver" + chartValues := composeCompatibilityChartValues(values) - // Get the chart Values - csiStackitValues := values[openstack.CSISTACKITControllerName].(map[string]any) - // Merge csiStackitValues to topLevel. Basically removes the openstack.CSISTACKITControllerName key - chartValues := gardenerutils.MergeMaps(values, csiStackitValues) // Override chart values - chartValues["prefix"] = "stackit-compat" + chartValues["prefix"] = csiCompatibilityPrefix - //TODO: Use gardener tools for this? If possible - imagesToFind := []string{ + imageMap, err := findImages( "csi-driver-stackit", "csi-node-driver-registrar", "csi-liveness-probe", - } - images := imagevector.ImageVector() - imageMap := make(map[string]any) - - for _, image := range imagesToFind { - foundImage, err := images.FindImage(image) - if err != nil { - return nil, err - } - imageMap[image] = foundImage.String() + ) + if err != nil { + return nil, err } chartValues["images"] = imageMap + chartValues["healthzPort"] = 9909 csiValues := map[string]any{ "enableCompatibilityMode": true, @@ -165,7 +147,7 @@ func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values return ch.renderer.RenderEmbeddedFS( charts.InternalChart, filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), - chartName, + csiDriverChartName, "kube-system", chartValues, ) @@ -173,9 +155,35 @@ func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values func (ch *CompatCSICompatibilityHandler) deployShootCSICompatibilityMode(ctx context.Context, namespace string, renderedChart *chartrenderer.RenderedChart) error { data := renderedChart.AsSecretData() - return managedresources.CreateForShoot(ctx, ch.client, namespace, "stackit-csi-compat-shoot-chart", "gardener-extension-provider-stackit", false, data) + 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 managedresources.DeleteForShoot(ctx, ch.client, namespace, "stackit-csi-compat-shoot-chart") + 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(imagesToFind ...string) (map[string]any, error) { + images := imagevector.ImageVector() + result := make(map[string]any) + for _, image := range imagesToFind { + foundImage, err := images.FindImage(image) + 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 index fad75411..dc9863d4 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -54,11 +54,12 @@ func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) var _ = Describe("CompatCSICompatibilityHandler", func() { var ( - ctx context.Context - fakeClient client.Client - handler *CompatCSICompatibilityHandler - namespace string - config *rest.Config + ctx context.Context + fakeClient client.Client + handler *CompatCSICompatibilityHandler + namespace string + config *rest.Config + controlPlaneValues map[string]any ) BeforeEach(func() { @@ -75,39 +76,16 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { } handler, _ = NewCompatCSICompatibilityHandler(fakeClient, config) - }) - // getDaemonSetFromSecret := func(prefix 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, prefix) { - // matchedSecret = &s - // break - // } - // } - - // if matchedSecret == nil { - // Fail(fmt.Sprintf("Secret starting with prefix %s not found. Found secrets: %v", prefix, names)) - // } - - // for _, data := range matchedSecret.Data { - // docs := bytes.Split(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 - // } + 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() { @@ -115,7 +93,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { cpConfig := &stackitv1alpha1.ControlPlaneConfig{ Storage: &stackitv1alpha1.Storage{ CSI: &stackitv1alpha1.CSI{ - Name: string(stackitv1alpha1.DEFAULT), + CompatibilityMode: string(stackitv1alpha1.DEFAULT), }, }, } @@ -123,24 +101,25 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { // Create the managed resource and secret beforehand to ensure deletion works mr := &resourcesv1alpha1.ManagedResource{ ObjectMeta: metav1.ObjectMeta{ - Name: "stackit-csi-compat-chart", - Namespace: namespace, + Name: csiCompatSeedChartName, + Namespace: "kube-system", }, } secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "managedresource-stackit-csi-compat-chart", - Namespace: namespace, + Name: "managedresource-" + csiCompatSeedChartName, + Namespace: "kube-system", }, } Expect(fakeClient.Create(ctx, mr)).To(Succeed()) Expect(fakeClient.Create(ctx, secret)).To(Succeed()) - err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, nil) + err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) // Check deletion - err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-chart", Namespace: namespace}, mr) + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatSeedChartName, Namespace: namespace}, mr) + Expect(err).To(HaveOccurred()) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) }) @@ -150,20 +129,11 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { cpConfig := &stackitv1alpha1.ControlPlaneConfig{ Storage: &stackitv1alpha1.Storage{ CSI: &stackitv1alpha1.CSI{ - Name: string(stackitv1alpha1.COMPAT), + CompatibilityMode: string(stackitv1alpha1.COMPAT), }, }, } - controlPlaneValues := map[string]any{ - "global": map[string]any{ - "genericTokenKubeconfigSecretName": "generic-token-kubeconfig-92e9ae14", - }, - openstack.CSISTACKITControllerName: map[string]any{ - "foo": "bar", - }, - } - err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) @@ -180,7 +150,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { cpConfig := &stackitv1alpha1.ControlPlaneConfig{ Storage: &stackitv1alpha1.Storage{ CSI: &stackitv1alpha1.CSI{ - Name: string(stackitv1alpha1.DEFAULT), + CompatibilityMode: string(stackitv1alpha1.DEFAULT), }, }, } @@ -188,24 +158,24 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { // Create the managed resource and secret beforehand to ensure deletion works mr := &resourcesv1alpha1.ManagedResource{ ObjectMeta: metav1.ObjectMeta{ - Name: "stackit-csi-compat-shoot-chart", + Name: csiCompatShootChartName, Namespace: namespace, }, } secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: "managedresource-stackit-csi-compat-shoot-chart", + Name: "managedresource-" + csiCompatShootChartName, Namespace: namespace, }, } Expect(fakeClient.Create(ctx, mr)).To(Succeed()) Expect(fakeClient.Create(ctx, secret)).To(Succeed()) - err := handler.HandleShootCSICompatibility(ctx, namespace, cpConfig, nil) + err := handler.HandleShootCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) // Check deletion - err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatShootChartName, Namespace: namespace}, mr) Expect(apierrors.IsNotFound(err)).To(BeTrue()) }) }) @@ -215,28 +185,19 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { cpConfig := &stackitv1alpha1.ControlPlaneConfig{ Storage: &stackitv1alpha1.Storage{ CSI: &stackitv1alpha1.CSI{ - Name: string(stackitv1alpha1.COMPAT), + CompatibilityMode: string(stackitv1alpha1.COMPAT), }, }, } - 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, cpConfig, values) + err := handler.HandleShootCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) mr := &resourcesv1alpha1.ManagedResource{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatShootChartName, Namespace: namespace}, mr) Expect(err).NotTo(HaveOccurred()) - ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-stackit-csi-compat-shoot-chart-") + ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatShootChartName) var csiContainer *corev1.Container for i := range ds.Spec.Template.Spec.Containers { if ds.Spec.Template.Spec.Containers[i].Name == "csi-driver-stackit" { @@ -255,7 +216,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { cpConfig := &stackitv1alpha1.ControlPlaneConfig{ Storage: &stackitv1alpha1.Storage{ CSI: &stackitv1alpha1.CSI{ - Name: string(stackitv1alpha1.COMPATBLOCK), + CompatibilityMode: string(stackitv1alpha1.COMPATBLOCK), }, }, } @@ -273,10 +234,10 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(err).NotTo(HaveOccurred()) mr := &resourcesv1alpha1.ManagedResource{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-shoot-chart", Namespace: namespace}, mr) + err = fakeClient.Get(ctx, types.NamespacedName{Name: csiCompatShootChartName, Namespace: namespace}, mr) Expect(err).NotTo(HaveOccurred()) - ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-stackit-csi-compat-shoot-chart-") + ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatShootChartName) var csiContainer *corev1.Container for i := range ds.Spec.Template.Spec.Containers { if ds.Spec.Template.Spec.Containers[i].Name == "csi-driver-stackit" { From 5c464af017d8dbc4080398dfea2ca00be76baac8 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Wed, 24 Jun 2026 13:26:31 +0200 Subject: [PATCH 32/49] handle unset compatibility mode --- .../controlplane/csi_compatibility.go | 10 +-- .../controlplane/csi_compatibility_test.go | 71 +++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index 5c365765..b3344075 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -42,7 +42,8 @@ type CompatCSICompatibilityHandler struct { func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, controlPlaneValues map[string]any) error { compatibilityMode := getCSICompatibilityMode(cpConfig) - if compatibilityMode != stackitv1alpha1.DEFAULT { + switch compatibilityMode { + case stackitv1alpha1.COMPAT, stackitv1alpha1.COMPATBLOCK: chart, err := ch.renderSeedCSICompatibilityMode(controlPlaneValues) if err != nil { return fmt.Errorf("failed to render seed CSI compatibility mode: %w", err) @@ -51,7 +52,7 @@ func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context. if err != nil { return fmt.Errorf("failed to deploy seed CSI compatibility mode: %w", err) } - } else { + default: err := ch.deleteSeedCSICompatibilityMode(ctx, namespace) if err != nil { return fmt.Errorf("failed to deploy seed CSI compatibility mode: %w", err) @@ -100,7 +101,8 @@ func (ch *CompatCSICompatibilityHandler) deleteSeedCSICompatibilityMode(ctx cont func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, values map[string]any) error { compatibilityMode := getCSICompatibilityMode(cpConfig) - if compatibilityMode != stackitv1alpha1.DEFAULT { + switch compatibilityMode { + case stackitv1alpha1.COMPAT, stackitv1alpha1.COMPATBLOCK: blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK chart, err := ch.renderShootCSICompatibilityMode(values, blockLegacyCreation) if err != nil { @@ -110,7 +112,7 @@ func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context if err != nil { return fmt.Errorf("deploy shoot CSI compatibility mode: %w", err) } - } else { + default: err := ch.deleteShootCSICompatibilityMode(ctx, namespace) if err != nil { return fmt.Errorf("delete shoot CSI compatibility mode: %w", err) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index dc9863d4..bf5ba7d6 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -124,6 +124,42 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { }) }) + 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: "kube-system", + }, + } + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "managedresource-" + csiCompatSeedChartName, + Namespace: "kube-system", + }, + } + Expect(fakeClient.Create(ctx, mr)).To(Succeed()) + Expect(fakeClient.Create(ctx, secret)).To(Succeed()) + + err := handler.HandleSeedCSICompatibility(ctx, namespace, 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{ @@ -180,6 +216,41 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { }) }) + 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, 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{ From e6f19094dcb45d9a0e69725694c216deece7d684 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Wed, 24 Jun 2026 13:32:20 +0200 Subject: [PATCH 33/49] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 53d65ee1..dd548474 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 ( @@ -237,5 +238,4 @@ require ( 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 ) From d5cc0b468c308833b730205479538b0743f19865 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Wed, 24 Jun 2026 13:37:36 +0200 Subject: [PATCH 34/49] modernize --- pkg/controller/controlplane/csi_compatibility_test.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index bf5ba7d6..d1d6f2a7 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -340,11 +340,12 @@ func getDaemonSetFromSecret(ctx context.Context, fakeClient client.Client, names if matchedSecret == nil { Fail(fmt.Sprintf("Secret starting with prefix %s not found. Found secrets: %v", prefix, names)) + return nil // will never be reached, but makes the linter very happy } for _, data := range matchedSecret.Data { - docs := bytes.Split(data, []byte("\n---")) - for _, doc := range docs { + 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()) From bcf30d3384dec81f39e2a1d5b2c144c516b721ee Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 25 Jun 2026 11:15:24 +0200 Subject: [PATCH 35/49] move the legacy-volume-creation flag to the seed control plane --- .../deployment-csi-driver-controller.yaml | 6 ++ .../values.yaml | 4 + .../templates/daemonset.yaml | 3 - .../values.yaml | 1 - .../controlplane/csi_compatibility.go | 21 +++-- .../controlplane/csi_compatibility_test.go | 85 ++++++++++++++++++- 6 files changed, 104 insertions(+), 16 deletions(-) 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..ad2da8e5 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 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 02f0e30b..bfec1436 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,10 @@ kubernetesVersion: 1.30.0 prefix: stackit-blockstorage projectID: "" +csi: + enableCompatibilityMode: false + blockLegacyCreation: false + images: csi-driver-stackit: image-repository:image-tag csi-provisioner: image-repository:image-tag 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 103209a8..fc95e890 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 @@ -51,9 +51,6 @@ spec: {{- 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 }} 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 d2ac0bad..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 @@ -7,7 +7,6 @@ healthzPort: 9908 csi: enableCompatibilityMode: false - blockLegacyCreation: false images: csi-driver-stackit: image-repository:image-tag diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index b3344075..26eb322a 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -44,7 +44,8 @@ func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context. compatibilityMode := getCSICompatibilityMode(cpConfig) switch compatibilityMode { case stackitv1alpha1.COMPAT, stackitv1alpha1.COMPATBLOCK: - chart, err := ch.renderSeedCSICompatibilityMode(controlPlaneValues) + blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK + chart, err := ch.renderSeedCSICompatibilityMode(controlPlaneValues, blockLegacyCreation) if err != nil { return fmt.Errorf("failed to render seed CSI compatibility mode: %w", err) } @@ -61,7 +62,7 @@ func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context. return nil } -func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values map[string]any) (*chartrenderer.RenderedChart, error) { +func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values map[string]any, blockLegacyCreation bool) (*chartrenderer.RenderedChart, error) { chartValues := composeCompatibilityChartValues(values) // Override chart values @@ -81,6 +82,14 @@ func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values m } chartValues["images"] = imageMap + 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"), @@ -103,8 +112,7 @@ func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context compatibilityMode := getCSICompatibilityMode(cpConfig) switch compatibilityMode { case stackitv1alpha1.COMPAT, stackitv1alpha1.COMPATBLOCK: - blockLegacyCreation := compatibilityMode == stackitv1alpha1.COMPATBLOCK - chart, err := ch.renderShootCSICompatibilityMode(values, blockLegacyCreation) + chart, err := ch.renderShootCSICompatibilityMode(values) if err != nil { return fmt.Errorf("render shoot CSI compatibility mode: %w", err) } @@ -121,7 +129,7 @@ func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context return nil } -func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values map[string]any, blockLegacyCreation bool) (*chartrenderer.RenderedChart, error) { +func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values map[string]any) (*chartrenderer.RenderedChart, error) { chartValues := composeCompatibilityChartValues(values) // Override chart values @@ -141,9 +149,6 @@ func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values csiValues := map[string]any{ "enableCompatibilityMode": true, } - if blockLegacyCreation { - csiValues["blockLegacyCreation"] = true - } chartValues["csi"] = csiValues return ch.renderer.RenderEmbeddedFS( diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index d1d6f2a7..145e6992 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -176,6 +176,49 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { mr := &resourcesv1alpha1.ManagedResource{} err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-chart", Namespace: namespace}, mr) Expect(err).NotTo(HaveOccurred()) + + deployment := getDeploymentFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-csi-compat-csi-driver-controller") + var csiContainer *corev1.Container + for i := range deployment.Spec.Template.Spec.Containers { + if deployment.Spec.Template.Spec.Containers[i].Name == "stackit-csi-driver" { + csiContainer = &deployment.Spec.Template.Spec.Containers[i] + break + } + } + Expect(csiContainer).NotTo(BeNil(), "stackit-csi-driver container not found") + Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) + Expect(csiContainer.Args).NotTo(ContainElement("--legacy-volume-creation=false")) + }) + }) + + 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, cpConfig, controlPlaneValues) + Expect(err).NotTo(HaveOccurred()) + + mr := &resourcesv1alpha1.ManagedResource{} + err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-chart", Namespace: namespace}, mr) + Expect(err).NotTo(HaveOccurred()) + + deployment := getDeploymentFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-csi-compat-csi-driver-controller") + var csiContainer *corev1.Container + for i := range deployment.Spec.Template.Spec.Containers { + if deployment.Spec.Template.Spec.Containers[i].Name == "stackit-csi-driver" { + csiContainer = &deployment.Spec.Template.Spec.Containers[i] + break + } + } + Expect(csiContainer).NotTo(BeNil(), "stackit-csi-driver container not found") + Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) + Expect(csiContainer.Args).To(ContainElement("--legacy-volume-creation=false")) }) }) }) @@ -318,13 +361,12 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { } Expect(csiContainer).NotTo(BeNil(), "csi-driver-stackit container not found") Expect(csiContainer.Args).To(ContainElement("--legacy-storage-mode=true")) - Expect(csiContainer.Args).To(ContainElement("--legacy-volume-creation=false")) }) }) }) }) -func getDaemonSetFromSecret(ctx context.Context, fakeClient client.Client, namespace string, prefix string) *appsv1.DaemonSet { +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()) @@ -332,14 +374,14 @@ func getDaemonSetFromSecret(ctx context.Context, fakeClient client.Client, names var names []string for _, s := range secretList.Items { names = append(names, s.Name) - if strings.HasPrefix(s.Name, prefix) { + 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", prefix, names)) + 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 } @@ -356,3 +398,38 @@ func getDaemonSetFromSecret(ctx context.Context, fakeClient client.Client, names 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 +} From 5d45900fe0c922af076801f39b1db7fdaf042d4d Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 25 Jun 2026 11:17:25 +0200 Subject: [PATCH 36/49] use a different healthz port in the second csi drive instance, also in the seed control plane --- .../templates/deployment-csi-driver-controller.yaml | 2 +- .../charts/stackit-blockstorage-csi-driver/values.yaml | 2 ++ pkg/controller/controlplane/csi_compatibility.go | 1 + 3 files changed, 4 insertions(+), 1 deletion(-) 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 ad2da8e5..9e85fb81 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 @@ -75,7 +75,7 @@ spec: {{- end }} ports: - name: healthz - containerPort: 9808 + containerPort: {{ .Values.healthzPort }} protocol: TCP livenessProbe: httpGet: 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 bfec1436..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,8 @@ kubernetesVersion: 1.30.0 prefix: stackit-blockstorage projectID: "" +healthzPort: 9808 + csi: enableCompatibilityMode: false blockLegacyCreation: false diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index 26eb322a..e0cb492f 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -82,6 +82,7 @@ func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values m } chartValues["images"] = imageMap + chartValues["healthzPort"] = 9809 csiValues := map[string]any{ "enableCompatibilityMode": true, } From 2c876d01e3e2342f2a092b7e1a182a6f90d78799 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 25 Jun 2026 12:12:42 +0200 Subject: [PATCH 37/49] fix typo --- pkg/controller/controlplane/csi_compatibility.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index e0cb492f..382a3194 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -21,7 +21,7 @@ const ( csiDriverChartName = "stackit-blockstorage-csi-driver" csiCompatibilityPrefix = "stackit-csi-compat" csiCompatSeedChartName = csiCompatibilityPrefix + "-chart" - csiCompatShootChartName = csiCompatibilityPrefix + "-shoort-chart" + csiCompatShootChartName = csiCompatibilityPrefix + "-shoot-chart" ) func NewCompatCSICompatibilityHandler(client client.Client, config *rest.Config) (*CompatCSICompatibilityHandler, error) { From 1dc86cd9dcf87c78ba2c6567f5e50486001d0ad2 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 25 Jun 2026 13:01:32 +0200 Subject: [PATCH 38/49] use a better prefix --- pkg/controller/controlplane/csi_compatibility.go | 2 +- pkg/controller/controlplane/csi_compatibility_test.go | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index 382a3194..57221c2c 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -19,7 +19,7 @@ import ( const ( csiDriverChartName = "stackit-blockstorage-csi-driver" - csiCompatibilityPrefix = "stackit-csi-compat" + csiCompatibilityPrefix = "stackit-compatibility" csiCompatSeedChartName = csiCompatibilityPrefix + "-chart" csiCompatShootChartName = csiCompatibilityPrefix + "-shoot-chart" ) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index 145e6992..4069b26d 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -174,10 +174,10 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(err).NotTo(HaveOccurred()) mr := &resourcesv1alpha1.ManagedResource{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-chart", Namespace: namespace}, mr) + 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-csi-compat-csi-driver-controller") + deployment := getDeploymentFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-compatibility-csi-driver-controller") var csiContainer *corev1.Container for i := range deployment.Spec.Template.Spec.Containers { if deployment.Spec.Template.Spec.Containers[i].Name == "stackit-csi-driver" { @@ -205,10 +205,10 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(err).NotTo(HaveOccurred()) mr := &resourcesv1alpha1.ManagedResource{} - err = fakeClient.Get(ctx, types.NamespacedName{Name: "stackit-csi-compat-chart", Namespace: namespace}, mr) + 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-csi-compat-csi-driver-controller") + deployment := getDeploymentFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-compatibility-csi-driver-controller") var csiContainer *corev1.Container for i := range deployment.Spec.Template.Spec.Containers { if deployment.Spec.Template.Spec.Containers[i].Name == "stackit-csi-driver" { From 405195211273981cc4e6e3caf1f25195d297336e Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 25 Jun 2026 15:39:10 +0200 Subject: [PATCH 39/49] fix: deploy seed part in correct namespace; findImage by specific version Signed-off-by: Niclas Schad --- .../controlplane/csi_compatibility.go | 35 +++++++------------ .../controlplane/csi_compatibility_test.go | 25 +++++++------ pkg/controller/controlplane/valuesprovider.go | 8 ++--- .../controlplane/valuesprovider_test.go | 4 +-- 4 files changed, 32 insertions(+), 40 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index 57221c2c..a37a64c7 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -7,6 +7,7 @@ import ( "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" @@ -40,12 +41,12 @@ type CompatCSICompatibilityHandler struct { renderer chartrenderer.Interface } -func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, controlPlaneValues map[string]any) error { +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, blockLegacyCreation) + chart, err := ch.renderSeedCSICompatibilityMode(controlPlaneValues, namespace, version, blockLegacyCreation) if err != nil { return fmt.Errorf("failed to render seed CSI compatibility mode: %w", err) } @@ -62,21 +63,13 @@ func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context. return nil } -func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values map[string]any, blockLegacyCreation bool) (*chartrenderer.RenderedChart, error) { +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( - "csi-driver-stackit", - "csi-provisioner", - "csi-attacher", - "csi-snapshotter", - "csi-resizer", - "csi-liveness-probe", - "csi-snapshot-controller", - ) + 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 } @@ -95,7 +88,7 @@ func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values m charts.InternalChart, filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), csiDriverChartName, - "kube-system", + namespace, chartValues, ) } @@ -109,11 +102,11 @@ func (ch *CompatCSICompatibilityHandler) deleteSeedCSICompatibilityMode(ctx cont return client.IgnoreNotFound(managedresources.DeleteForSeed(ctx, ch.client, namespace, csiCompatSeedChartName)) } -func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, values map[string]any) error { +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) + chart, err := ch.renderShootCSICompatibilityMode(values, version) if err != nil { return fmt.Errorf("render shoot CSI compatibility mode: %w", err) } @@ -130,17 +123,13 @@ func (ch *CompatCSICompatibilityHandler) HandleShootCSICompatibility(ctx context return nil } -func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values map[string]any) (*chartrenderer.RenderedChart, error) { +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( - "csi-driver-stackit", - "csi-node-driver-registrar", - "csi-liveness-probe", - ) + imageMap, err := findImages(version, "csi-driver-stackit", "csi-node-driver-registrar", "csi-liveness-probe") if err != nil { return nil, err } @@ -183,11 +172,11 @@ func composeCompatibilityChartValues(values map[string]any) map[string]any { return gardenerutils.MergeMaps(values, csiStackitValues) } -func findImages(imagesToFind ...string) (map[string]any, error) { +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) + foundImage, err := images.FindImage(image, imagevectorutils.TargetVersion(version)) if err != nil { return nil, err } diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index 4069b26d..b2488a8c 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -59,12 +59,14 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { 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). @@ -90,6 +92,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Describe("#HandleSeedCSICompatibility", func() { Context("when CSICompatibilityMode is DEFAULT", func() { It("should delete the managed resource", func() { + namespace = "shoot--foo--bar" cpConfig := &stackitv1alpha1.ControlPlaneConfig{ Storage: &stackitv1alpha1.Storage{ CSI: &stackitv1alpha1.CSI{ @@ -102,19 +105,19 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { mr := &resourcesv1alpha1.ManagedResource{ ObjectMeta: metav1.ObjectMeta{ Name: csiCompatSeedChartName, - Namespace: "kube-system", + Namespace: namespace, }, } secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "managedresource-" + csiCompatSeedChartName, - Namespace: "kube-system", + Namespace: namespace, }, } Expect(fakeClient.Create(ctx, mr)).To(Succeed()) Expect(fakeClient.Create(ctx, secret)).To(Succeed()) - err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) + err := handler.HandleSeedCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) // Check deletion @@ -138,7 +141,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { mr := &resourcesv1alpha1.ManagedResource{ ObjectMeta: metav1.ObjectMeta{ Name: csiCompatSeedChartName, - Namespace: "kube-system", + Namespace: "shoot--foo--bar", }, } secret := &corev1.Secret{ @@ -150,7 +153,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(fakeClient.Create(ctx, mr)).To(Succeed()) Expect(fakeClient.Create(ctx, secret)).To(Succeed()) - err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) + err := handler.HandleSeedCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) // Check deletion @@ -170,7 +173,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { }, } - err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) + err := handler.HandleSeedCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) mr := &resourcesv1alpha1.ManagedResource{} @@ -201,7 +204,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { }, } - err := handler.HandleSeedCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) + err := handler.HandleSeedCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) mr := &resourcesv1alpha1.ManagedResource{} @@ -250,7 +253,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(fakeClient.Create(ctx, mr)).To(Succeed()) Expect(fakeClient.Create(ctx, secret)).To(Succeed()) - err := handler.HandleShootCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) + err := handler.HandleShootCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) // Check deletion @@ -285,7 +288,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(fakeClient.Create(ctx, mr)).To(Succeed()) Expect(fakeClient.Create(ctx, secret)).To(Succeed()) - err := handler.HandleShootCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) + err := handler.HandleShootCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) // Check deletion @@ -304,7 +307,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { }, } - err := handler.HandleShootCSICompatibility(ctx, namespace, cpConfig, controlPlaneValues) + err := handler.HandleShootCSICompatibility(ctx, namespace, shootVersion, cpConfig, controlPlaneValues) Expect(err).NotTo(HaveOccurred()) mr := &resourcesv1alpha1.ManagedResource{} @@ -344,7 +347,7 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { }, } - err := handler.HandleShootCSICompatibility(ctx, namespace, cpConfig, values) + err := handler.HandleShootCSICompatibility(ctx, namespace, shootVersion, cpConfig, values) Expect(err).NotTo(HaveOccurred()) mr := &resourcesv1alpha1.ManagedResource{} diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 86a2768f..9f97ee88 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -357,8 +357,8 @@ var ( ) type CSICompatibilityHandler interface { - HandleSeedCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error - HandleShootCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error + 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. @@ -768,7 +768,7 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf controlPlaneValues[openstack.CSIControllerName] = map[string]any{ "enabled": false, } - err := vp.csiCompatibilityHandler.HandleSeedCSICompatibility(ctx, cp.Namespace, cpConfig, controlPlaneValues) + err := vp.csiCompatibilityHandler.HandleSeedCSICompatibility(ctx, cp.Namespace, cluster.Shoot.Spec.Kubernetes.Version, cpConfig, controlPlaneValues) if err != nil { return nil, err } @@ -1089,7 +1089,7 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c case stackitv1alpha1.STACKIT: values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues values[openstack.CSINodeName] = map[string]any{"enabled": false} - err := vp.csiCompatibilityHandler.HandleShootCSICompatibility(ctx, cp.Namespace, cpConfig, values) + err := vp.csiCompatibilityHandler.HandleShootCSICompatibility(ctx, cp.Namespace, cluster.Shoot.Spec.Kubernetes.Version, cpConfig, values) if err != nil { return nil, err } diff --git a/pkg/controller/controlplane/valuesprovider_test.go b/pkg/controller/controlplane/valuesprovider_test.go index ac14253f..336176cd 100644 --- a/pkg/controller/controlplane/valuesprovider_test.go +++ b/pkg/controller/controlplane/valuesprovider_test.go @@ -1145,9 +1145,9 @@ func stackitCCMDeletion(ctx context.Context, c *mockclient.MockClient) { type noopCSICompatibilityHandler struct{} -func (*noopCSICompatibilityHandler) HandleSeedCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { +func (*noopCSICompatibilityHandler) HandleSeedCSICompatibility(context.Context, string, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { return nil } -func (*noopCSICompatibilityHandler) HandleShootCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { +func (*noopCSICompatibilityHandler) HandleShootCSICompatibility(context.Context, string, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { return nil } From 64ca6f69365bb911fe53a8498377b0f57890a515 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 25 Jun 2026 16:38:29 +0200 Subject: [PATCH 40/49] make stackit-blockstorage-csi-driver (shoot) chart customizable for different driverName Signed-off-by: Niclas Schad --- .../templates/daemonset.yaml | 10 +++++++--- pkg/controller/controlplane/csi_compatibility.go | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) 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 fc95e890..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 @@ -97,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 }} @@ -133,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/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go index a37a64c7..db59f27a 100644 --- a/pkg/controller/controlplane/csi_compatibility.go +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -136,6 +136,7 @@ func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values chartValues["images"] = imageMap chartValues["healthzPort"] = 9909 + chartValues["driverName"] = openstack.CSIStorageProvisioner csiValues := map[string]any{ "enableCompatibilityMode": true, } From d3e7c7aa030f84e562d1d232c89bc7443e850da7 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 25 Jun 2026 16:59:42 +0200 Subject: [PATCH 41/49] make health-port of CSI livenessprobe configurable Signed-off-by: Niclas Schad --- .../templates/deployment-csi-driver-controller.yaml | 1 + 1 file changed, 1 insertion(+) 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 9e85fb81..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 @@ -257,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 }} From cfb67877d1448207f06556b32413748116047a47 Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Thu, 25 Jun 2026 18:36:09 +0200 Subject: [PATCH 42/49] only deploy snapshot-controller once Signed-off-by: Niclas Schad --- .../templates/csi-snapshot-controller-poddisruptionbudget.yaml | 2 ++ .../templates/csi-snapshot-controller-vpa.yaml | 3 ++- .../templates/deployment-csi-snapshot-controller.yaml | 2 ++ 3 files changed, 6 insertions(+), 1 deletion(-) 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..9e5e3eef 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 .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..d5e49601 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 .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-snapshot-controller.yaml b/charts/internal/seed-controlplane/charts/stackit-blockstorage-csi-driver/templates/deployment-csi-snapshot-controller.yaml index 66c4daf0..5dbe585a 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 .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 }} From e9664582bcd3bbcd3773e2b91ceedbce1afc6726 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Fri, 26 Jun 2026 07:58:45 +0200 Subject: [PATCH 43/49] use the correct namespace --- pkg/controller/controlplane/csi_compatibility_test.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index b2488a8c..6d57d256 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -92,7 +92,6 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Describe("#HandleSeedCSICompatibility", func() { Context("when CSICompatibilityMode is DEFAULT", func() { It("should delete the managed resource", func() { - namespace = "shoot--foo--bar" cpConfig := &stackitv1alpha1.ControlPlaneConfig{ Storage: &stackitv1alpha1.Storage{ CSI: &stackitv1alpha1.CSI{ @@ -141,13 +140,13 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { mr := &resourcesv1alpha1.ManagedResource{ ObjectMeta: metav1.ObjectMeta{ Name: csiCompatSeedChartName, - Namespace: "shoot--foo--bar", + Namespace: namespace, }, } secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: "managedresource-" + csiCompatSeedChartName, - Namespace: "kube-system", + Namespace: namespace, }, } Expect(fakeClient.Create(ctx, mr)).To(Succeed()) From 08f10151b0bc98f9e113a1675e921ef4e2788afe Mon Sep 17 00:00:00 2001 From: Niclas Schad Date: Fri, 26 Jun 2026 10:20:39 +0200 Subject: [PATCH 44/49] fix: deploy snapshotcontroller by default; but NOT if in compatibility mode Signed-off-by: Niclas Schad --- .../templates/csi-snapshot-controller-poddisruptionbudget.yaml | 2 +- .../templates/csi-snapshot-controller-vpa.yaml | 2 +- .../templates/deployment-csi-snapshot-controller.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 9e5e3eef..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,4 +1,4 @@ -{{- if .Values.csi.enableCompatibilityMode }} +{{- if not .Values.csi.enableCompatibilityMode }} apiVersion: policy/v1 kind: PodDisruptionBudget metadata: 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 d5e49601..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 .Values.csi.enableCompatibilityMode }} +{{- if not .Values.csi.enableCompatibilityMode }} apiVersion: autoscaling.k8s.io/v1 kind: VerticalPodAutoscaler metadata: 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 5dbe585a..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,4 +1,4 @@ -{{- if .Values.csi.enableCompatibilityMode }} +{{- if not .Values.csi.enableCompatibilityMode }} apiVersion: apps/v1 kind: Deployment metadata: From 2787b75db7abf8794e9be759786a5fb8d32f9043 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Mon, 29 Jun 2026 11:26:53 +0200 Subject: [PATCH 45/49] check the liveness-probe healthz port --- .../controlplane/csi_compatibility_test.go | 68 ++++++++++--------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index 6d57d256..0e2a2780 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -180,16 +180,14 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(err).NotTo(HaveOccurred()) deployment := getDeploymentFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-compatibility-csi-driver-controller") - var csiContainer *corev1.Container - for i := range deployment.Spec.Template.Spec.Containers { - if deployment.Spec.Template.Spec.Containers[i].Name == "stackit-csi-driver" { - csiContainer = &deployment.Spec.Template.Spec.Containers[i] - break - } - } - Expect(csiContainer).NotTo(BeNil(), "stackit-csi-driver container not found") + 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")) }) }) @@ -211,16 +209,15 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(err).NotTo(HaveOccurred()) deployment := getDeploymentFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatSeedChartName, "stackit-compatibility-csi-driver-controller") - var csiContainer *corev1.Container - for i := range deployment.Spec.Template.Spec.Containers { - if deployment.Spec.Template.Spec.Containers[i].Name == "stackit-csi-driver" { - csiContainer = &deployment.Spec.Template.Spec.Containers[i] - break - } - } - Expect(csiContainer).NotTo(BeNil(), "stackit-csi-driver container not found") + + 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")) }) }) }) @@ -314,16 +311,15 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(err).NotTo(HaveOccurred()) ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatShootChartName) - var csiContainer *corev1.Container - for i := range ds.Spec.Template.Spec.Containers { - if ds.Spec.Template.Spec.Containers[i].Name == "csi-driver-stackit" { - csiContainer = &ds.Spec.Template.Spec.Containers[i] - break - } - } - Expect(csiContainer).NotTo(BeNil(), "csi-driver-stackit container not found") + + 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")) }) }) @@ -354,15 +350,14 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { Expect(err).NotTo(HaveOccurred()) ds := getDaemonSetFromSecret(ctx, fakeClient, namespace, "managedresource-"+csiCompatShootChartName) - var csiContainer *corev1.Container - for i := range ds.Spec.Template.Spec.Containers { - if ds.Spec.Template.Spec.Containers[i].Name == "csi-driver-stackit" { - csiContainer = &ds.Spec.Template.Spec.Containers[i] - break - } - } - Expect(csiContainer).NotTo(BeNil(), "csi-driver-stackit container not found") + + 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")) }) }) }) @@ -435,3 +430,12 @@ func getDeploymentFromSecret(ctx context.Context, fakeClient client.Client, name 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 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 +} From 913f3da44ae63caf425d457e2f840a72f1714098 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Mon, 29 Jun 2026 12:03:11 +0200 Subject: [PATCH 46/49] check the csi drive name in COMPAT(BLOCK) mode --- pkg/controller/controlplane/csi_compatibility_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index 0e2a2780..f32cf08c 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -320,6 +320,9 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { 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")) }) }) @@ -358,6 +361,9 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { 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")) }) }) }) From c047568cf1e68818c1c210aa4070da1886b2cff4 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Mon, 29 Jun 2026 12:21:51 +0200 Subject: [PATCH 47/49] check that there is no snapshot controller deployed with the compatibility driver --- .../controlplane/csi_compatibility_test.go | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/pkg/controller/controlplane/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go index f32cf08c..253d6adb 100644 --- a/pkg/controller/controlplane/csi_compatibility_test.go +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -188,6 +188,8 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { 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()) }) }) @@ -218,6 +220,8 @@ var _ = Describe("CompatCSICompatibilityHandler", func() { 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()) }) }) }) @@ -437,6 +441,29 @@ func getDeploymentFromSecret(ctx context.Context, fakeClient client.Client, 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 { From eef3bddaf652b015946555d5c5648ef2588565a2 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Mon, 29 Jun 2026 16:37:48 +0200 Subject: [PATCH 48/49] create an integration test that checks if the compatibility related ManagedResource CRs are deployed --- go.mod | 15 + go.sum | 44 ++ .../controlplane/controlplane_suite_test.go | 23 + .../controlplane/controlplane_test.go | 276 ++++++++ ...scaling.k8s.io_verticalpodautoscalers.yaml | 630 ++++++++++++++++++ ...tensions.gardener.cloud_controlplanes.yaml | 260 ++++++++ ...urces.gardener.cloud_managedresources.yaml | 254 +++++++ 7 files changed, 1502 insertions(+) create mode 100644 test/integration/controlplane/controlplane_suite_test.go create mode 100644 test/integration/controlplane/controlplane_test.go create mode 100644 test/integration/testdata/upstream-crds/10-crd-autoscaling.k8s.io_verticalpodautoscalers.yaml create mode 100644 test/integration/testdata/upstream-crds/10-crd-extensions.gardener.cloud_controlplanes.yaml create mode 100644 test/integration/testdata/upstream-crds/10-crd-resources.gardener.cloud_managedresources.yaml diff --git a/go.mod b/go.mod index dd548474..c132ec7f 100644 --- a/go.mod +++ b/go.mod @@ -54,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 @@ -84,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 @@ -91,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 @@ -114,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 @@ -126,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 @@ -135,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 @@ -161,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 @@ -176,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 @@ -219,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 @@ -229,10 +242,12 @@ 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 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/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..298dcac7 --- /dev/null +++ b/test/integration/controlplane/controlplane_test.go @@ -0,0 +1,276 @@ +// 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" + 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()) + }) +}) 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: {} From 5677f7671ab33fc6bb3e3be2c32c5816dab6faf0 Mon Sep 17 00:00:00 2001 From: Florian Thienel Date: Thu, 2 Jul 2026 15:37:57 +0200 Subject: [PATCH 49/49] add test to cover the transition from compat to default --- .../controlplane/controlplane_test.go | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/test/integration/controlplane/controlplane_test.go b/test/integration/controlplane/controlplane_test.go index 298dcac7..1be7b533 100644 --- a/test/integration/controlplane/controlplane_test.go +++ b/test/integration/controlplane/controlplane_test.go @@ -20,6 +20,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" @@ -273,4 +274,123 @@ var _ = Describe("ControlPlane CSI compatibility mode", func() { }, 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()) + }) })