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/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/hack/api-reference/api.md b/hack/api-reference/api.md index 8bbbc206..773e7d08 100644 --- a/hack/api-reference/api.md +++ b/hack/api-reference/api.md @@ -215,11 +215,32 @@ string

+ + +compatibilityMode
+ +string + + + +

+ + +

CSICompatibilityMode +

+

Underlying type: string

+ + +

+ +

+ +

CSIManila

diff --git a/pkg/apis/stackit/v1alpha1/constants.go b/pkg/apis/stackit/v1alpha1/constants.go index 86bbdc81..42400ae6 100644 --- a/pkg/apis/stackit/v1alpha1/constants.go +++ b/pkg/apis/stackit/v1alpha1/constants.go @@ -5,6 +5,8 @@ package v1alpha1 const ( // DefaultCSIName defines the default CSI (Container Storage Interface) name for STACKIT DefaultCSIName = "stackit" + // DefaultCSICompatibilityMode defines the default CSI driver's compatibility mode. + DefaultCSICompatibilityMode = "default" // DefaultCCMName defines the default CCM (Cloud Controller Manager) controller to use DefaultCCMName = "stackit" ) @@ -15,3 +17,11 @@ const ( STACKIT ControllerName = "stackit" OPENSTACK ControllerName = "openstack" ) + +type CSICompatibilityMode string + +const ( + DEFAULT CSICompatibilityMode = "default" + COMPAT CSICompatibilityMode = "compat" + COMPATBLOCK CSICompatibilityMode = "compatblock" +) diff --git a/pkg/apis/stackit/v1alpha1/defaults.go b/pkg/apis/stackit/v1alpha1/defaults.go index d72cba93..e91caf34 100644 --- a/pkg/apis/stackit/v1alpha1/defaults.go +++ b/pkg/apis/stackit/v1alpha1/defaults.go @@ -43,4 +43,7 @@ func SetDefaults_ControlPlaneConfig(obj *ControlPlaneConfig) { if obj.Storage.CSI.Name == "" { obj.Storage.CSI.Name = DefaultCSIName } + if obj.Storage.CSI.CompatibilityMode == "" { + obj.Storage.CSI.CompatibilityMode = DefaultCSICompatibilityMode + } } diff --git a/pkg/apis/stackit/v1alpha1/types_controlplane.go b/pkg/apis/stackit/v1alpha1/types_controlplane.go index 5c10c28a..5b28637d 100644 --- a/pkg/apis/stackit/v1alpha1/types_controlplane.go +++ b/pkg/apis/stackit/v1alpha1/types_controlplane.go @@ -58,7 +58,8 @@ type Storage struct { } type CSI struct { - Name string `json:"name"` + Name string `json:"name"` + CompatibilityMode string `json:"compatibilityMode,omitempty"` } // CSIManila contains configuration for CSI Manila driver (support for NFS volumes) diff --git a/pkg/controller/controlplane/add.go b/pkg/controller/controlplane/add.go index a0e11e6f..ab687708 100644 --- a/pkg/controller/controlplane/add.go +++ b/pkg/controller/controlplane/add.go @@ -43,10 +43,15 @@ type AddOptions struct { // AddToManagerWithOptions adds a controller with the given Options to the given manager. // The opts.Reconciler is being set with a newly instantiated actuator. func AddToManagerWithOptions(ctx context.Context, mgr manager.Manager, opts AddOptions) error { + csiCompatibilityHandler, err := NewCompatCSICompatibilityHandler(mgr.GetClient(), mgr.GetConfig()) + if err != nil { + return err + } genericActuator, err := genericactuator.NewActuator(mgr, stackit.Name, secretConfigsFunc, shootAccessSecretsFunc, configChart, controlPlaneChart, controlPlaneShootChart, controlPlaneShootCRDsChart, storageClassChart, - NewValuesProvider(mgr, DeployALBIngressController, opts.CustomLabelDomain), extensionscontroller.ChartRendererFactoryFunc(util.NewChartRendererForShoot), + NewValuesProvider(mgr, DeployALBIngressController, opts.CustomLabelDomain, csiCompatibilityHandler), + extensionscontroller.ChartRendererFactoryFunc(util.NewChartRendererForShoot), imagevector.ImageVector(), "", nil, opts.WebhookServerNamespace) if err != nil { return err diff --git a/pkg/controller/controlplane/csi_compatibility.go b/pkg/controller/controlplane/csi_compatibility.go new file mode 100644 index 00000000..1b9a72f3 --- /dev/null +++ b/pkg/controller/controlplane/csi_compatibility.go @@ -0,0 +1,181 @@ +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, error) { + renderer, err := chartrenderer.NewForConfig(config) + if err != nil { + return nil, err + } + return &CompatCSICompatibilityHandler{ + client: client, + renderer: renderer, + }, nil +} + +type CompatCSICompatibilityHandler struct { + client client.Client + renderer chartrenderer.Interface +} + +func (ch *CompatCSICompatibilityHandler) HandleSeedCSICompatibility(ctx context.Context, namespace string, cpConfig *stackitv1alpha1.ControlPlaneConfig, controlPlaneValues map[string]any) error { + 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) + } + 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 seed CSI compatibility mode: %w", err) + } + } + return nil +} + +func (ch *CompatCSICompatibilityHandler) renderSeedCSICompatibilityMode(values map[string]any) (*chartrenderer.RenderedChart, error) { + // 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 nil, err + } + imageMap[image] = foundImage.String() + } + chartValues["images"] = imageMap + + return ch.renderer.RenderEmbeddedFS( + charts.InternalChart, + filepath.Join(charts.InternalChartsPath, "seed-controlplane/charts/stackit-blockstorage-csi-driver"), + chartName, + "kube-system", + chartValues, + ) +} + +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) +} + +func (ch *CompatCSICompatibilityHandler) deleteSeedCSICompatibilityMode(ctx context.Context, namespace string) error { + return managedresources.DeleteForSeed(ctx, ch.client, namespace, "stackit-csi-compat-chart") +} + +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 + 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 { + err := ch.deleteShootCSICompatibilityMode(ctx, namespace) + if err != nil { + return fmt.Errorf("delete shoot CSI compatibility mode: %w", err) + } + } + return nil +} + +func (ch *CompatCSICompatibilityHandler) renderShootCSICompatibilityMode(values map[string]any, blockLegacyCreation bool) (*chartrenderer.RenderedChart, error) { + // 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 nil, 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 + + return ch.renderer.RenderEmbeddedFS( + charts.InternalChart, + filepath.Join(charts.InternalChartsPath, "shoot-system-components/charts/stackit-blockstorage-csi-driver"), + chartName, + "kube-system", + chartValues, + ) +} + +func (ch *CompatCSICompatibilityHandler) deployShootCSICompatibilityMode(ctx context.Context, namespace string, renderedChart *chartrenderer.RenderedChart) error { + data := renderedChart.AsSecretData() + return managedresources.CreateForShoot(ctx, ch.client, namespace, "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/csi_compatibility_test.go b/pkg/controller/controlplane/csi_compatibility_test.go new file mode 100644 index 00000000..b8e68841 --- /dev/null +++ b/pkg/controller/controlplane/csi_compatibility_test.go @@ -0,0 +1,326 @@ +// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors +// +// SPDX-License-Identifier: Apache-2.0 + +package controlplane + +import ( + "bytes" + "context" + "fmt" + "io" + "net/http" + "strings" + + resourcesv1alpha1 "github.com/gardener/gardener/pkg/apis/resources/v1alpha1" + "github.com/gardener/gardener/pkg/client/kubernetes" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/rest" + "sigs.k8s.io/controller-runtime/pkg/client" + fakeclient "sigs.k8s.io/controller-runtime/pkg/client/fake" + "sigs.k8s.io/yaml" + + stackitv1alpha1 "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/apis/stackit/v1alpha1" + "github.com/stackitcloud/gardener-extension-provider-stackit/v2/pkg/openstack" +) + +type mockRoundTripper struct{} + +func (m *mockRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + var body string + switch req.URL.Path { + case "/version": + body = `{"major":"1","minor":"29","gitVersion":"v1.29.0"}` + case "/api": + body = `{"kind":"APIVersions","versions":["v1"]}` + case "/apis": + body = `{"kind":"APIGroupList","groups":[]}` + default: + body = `{"kind":"Status","status":"Failure","message":"Not Found","reason":"NotFound","code":404}` + } + + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(bytes.NewReader([]byte(body))), + }, nil +} + +var _ = Describe("CompatCSICompatibilityHandler", func() { + var ( + ctx context.Context + fakeClient client.Client + handler *CompatCSICompatibilityHandler + namespace string + config *rest.Config + ) + + 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) + }) + + // 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() { + 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()) + + 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" { + 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")) + }) + }) + + 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()) + + 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" { + 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")) + }) + }) + }) +}) + +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 +} diff --git a/pkg/controller/controlplane/valuesprovider.go b/pkg/controller/controlplane/valuesprovider.go index 180ac234..86a2768f 100644 --- a/pkg/controller/controlplane/valuesprovider.go +++ b/pkg/controller/controlplane/valuesprovider.go @@ -43,6 +43,7 @@ import ( "k8s.io/apimachinery/pkg/types" utilruntime "k8s.io/apimachinery/pkg/util/runtime" vpaautoscalingv1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" + "k8s.io/client-go/rest" "k8s.io/utils/ptr" k8sclient "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -355,13 +356,19 @@ var ( } ) +type CSICompatibilityHandler interface { + HandleSeedCSICompatibility(context.Context, string, *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. -func NewValuesProvider(mgr manager.Manager, deployALBIngressController bool, customLabelDomain string) genericactuator.ValuesProvider { +func NewValuesProvider(mgr manager.Manager, deployALBIngressController bool, customLabelDomain string, csiCompatibilityHandler CSICompatibilityHandler) genericactuator.ValuesProvider { return &valuesProvider{ client: mgr.GetClient(), decoder: serializer.NewCodecFactory(mgr.GetScheme(), serializer.EnableStrict).UniversalDecoder(), deployALBIngressController: deployALBIngressController, customLabelDomain: customLabelDomain, + csiCompatibilityHandler: csiCompatibilityHandler, } } @@ -369,9 +376,11 @@ func NewValuesProvider(mgr manager.Manager, deployALBIngressController bool, cus type valuesProvider struct { genericactuator.NoopValuesProvider client k8sclient.Client + config *rest.Config decoder runtime.Decoder deployALBIngressController bool customLabelDomain string + csiCompatibilityHandler CSICompatibilityHandler } // GetConfigChartValues returns the values for the config chart applied by the generic actuator. @@ -736,6 +745,15 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf return nil, err } + maps.Copy(controlPlaneValues, map[string]any{ + "global": map[string]any{ + "genericTokenKubeconfigSecretName": extensionscontroller.GenericTokenKubeconfigSecretNameFromCluster(cluster), + }, + openstack.CloudControllerManagerName: ccm, + openstack.STACKITCloudControllerManagerName: stackitccm, + stackit.PodIdentityWebhookName: podIdentityWebhook, + }) + storageCSIDriver := getCSIDriver(cpConfig) switch storageCSIDriver { case stackitv1alpha1.OPENSTACK: @@ -750,19 +768,14 @@ func (vp *valuesProvider) getControlPlaneChartValues(ctx context.Context, cpConf controlPlaneValues[openstack.CSIControllerName] = map[string]any{ "enabled": false, } + err := vp.csiCompatibilityHandler.HandleSeedCSICompatibility(ctx, cp.Namespace, cpConfig, controlPlaneValues) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("unsupported storage CSI Driver: %s", storageCSIDriver) } - maps.Copy(controlPlaneValues, map[string]any{ - "global": map[string]any{ - "genericTokenKubeconfigSecretName": extensionscontroller.GenericTokenKubeconfigSecretNameFromCluster(cluster), - }, - openstack.CloudControllerManagerName: ccm, - openstack.STACKITCloudControllerManagerName: stackitccm, - stackit.PodIdentityWebhookName: podIdentityWebhook, - }) - if vp.deployALBIngressController { fmt.Println("deploying ALB Ingress Controller") albcm, err := getSTACKITALBCMChartValues(cpConfig, cluster, infra, stackitCredentialsConfig, apiEndpoints, scaledDown, stackitRegion) @@ -1070,12 +1083,16 @@ func (vp *valuesProvider) getControlPlaneShootChartValues(ctx context.Context, c csiDriverInUse := getCSIDriver(cpConfig) switch csiDriverInUse { - case stackitv1alpha1.STACKIT: - values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues - values[openstack.CSINodeName] = map[string]any{"enabled": false} case stackitv1alpha1.OPENSTACK: values[openstack.CSINodeName] = csiNodeDriverValues values[openstack.CSISTACKITNodeName] = map[string]any{"enabled": false} + case stackitv1alpha1.STACKIT: + values[openstack.CSISTACKITNodeName] = csiDriverSTACKITValues + values[openstack.CSINodeName] = map[string]any{"enabled": false} + err := vp.csiCompatibilityHandler.HandleShootCSICompatibility(ctx, cp.Namespace, cpConfig, values) + if err != nil { + return nil, err + } default: return nil, fmt.Errorf("unsupported CSI driver type: %s", csiDriverInUse) } @@ -1216,6 +1233,10 @@ func getCSIDriver(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1. return stackitv1alpha1.ControllerName(cpConfig.Storage.CSI.Name) } +func getCSICompatibilityMode(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1.CSICompatibilityMode { + return stackitv1alpha1.CSICompatibilityMode(cpConfig.Storage.CSI.CompatibilityMode) +} + func getCCMController(cpConfig *stackitv1alpha1.ControlPlaneConfig) stackitv1alpha1.ControllerName { return stackitv1alpha1.ControllerName(cpConfig.CloudControllerManager.Name) } diff --git a/pkg/controller/controlplane/valuesprovider_test.go b/pkg/controller/controlplane/valuesprovider_test.go index d7da5fb4..ac14253f 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) HandleShootCSICompatibility(context.Context, string, *stackitv1alpha1.ControlPlaneConfig, map[string]any) error { + return nil +}