From d18679469679a1637bf507e681ae5a2c059b4343 Mon Sep 17 00:00:00 2001 From: Mike Bayer Date: Tue, 24 Feb 2026 16:16:25 -0500 Subject: [PATCH] OSPRH-25976: prevent openstack operator from forcing Galera Secret Remove the webhook rule that forces the Galera template to be copied from the top level openstack control plane secret field when blank, instead allowing the Galera level secret to be blank. This allows the Galera operator to use newer "automatic root password generation" logic delivered as part of OSPRH-14916. Workflows include deployment of an openstack control plane with no galera secret (blank or omitted) which will fully auto-generate a mariadb root password for the new install, or modifying an existing deployment's secret to be blank, which will generate a mariadb root password and update the existing galera DB to use the new root pw. References: OSPRH-25976 --- .../v1beta1/openstackcontrolplane_webhook.go | 5 +- .../openstackcontrolplane_webhook_test.go | 67 ++++++++ .../openstackoperator_controller_test.go | 162 ++++++++++++++++++ 3 files changed, 231 insertions(+), 3 deletions(-) diff --git a/api/core/v1beta1/openstackcontrolplane_webhook.go b/api/core/v1beta1/openstackcontrolplane_webhook.go index f4a32fce3..a267baa19 100644 --- a/api/core/v1beta1/openstackcontrolplane_webhook.go +++ b/api/core/v1beta1/openstackcontrolplane_webhook.go @@ -990,9 +990,8 @@ func (r *OpenStackControlPlane) DefaultServices() { if template.StorageClass == "" { template.StorageClass = r.Spec.StorageClass } - if template.Secret == "" { - template.Secret = r.Spec.Secret - } + // Don't default Secret here - it's handled conditionally in reconciliation + // to support both default (osp-secret) and auto-generated (blank) passwords template.Default() // By-value copy, need to update (*r.Spec.Galera.Templates)[key] = template diff --git a/api/core/v1beta1/openstackcontrolplane_webhook_test.go b/api/core/v1beta1/openstackcontrolplane_webhook_test.go index 877393361..fc8433c1a 100644 --- a/api/core/v1beta1/openstackcontrolplane_webhook_test.go +++ b/api/core/v1beta1/openstackcontrolplane_webhook_test.go @@ -11,6 +11,7 @@ import ( ironicv1 "github.com/openstack-k8s-operators/ironic-operator/api/v1beta1" keystonev1 "github.com/openstack-k8s-operators/keystone-operator/api/v1beta1" manilav1 "github.com/openstack-k8s-operators/manila-operator/api/v1beta1" + mariadbv1 "github.com/openstack-k8s-operators/mariadb-operator/api/v1beta1" neutronv1 "github.com/openstack-k8s-operators/neutron-operator/api/v1beta1" novav1 "github.com/openstack-k8s-operators/nova-operator/api/v1beta1" octaviav1 "github.com/openstack-k8s-operators/octavia-operator/api/v1beta1" @@ -19,6 +20,7 @@ import ( watcherv1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" ) var _ = Describe("OpenStackControlPlane Webhook", func() { @@ -942,4 +944,69 @@ var _ = Describe("OpenStackControlPlane Webhook", func() { }) }) }) + + Context("Galera Secret field defaulting behavior", func() { + var instance *OpenStackControlPlane + + BeforeEach(func() { + instance = &OpenStackControlPlane{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test", + Namespace: "test-namespace", + }, + Spec: OpenStackControlPlaneSpec{ + Secret: "osp-secret", + StorageClass: "local-storage", + Galera: GaleraSection{ + Enabled: true, + }, + }, + } + }) + + It("should not default template Secret when omitted in webhook", func() { + instance.Spec.Galera.Templates = ptr.To(map[string]mariadbv1.GaleraSpecCore{ + "openstack": { + StorageRequest: "500M", + // Secret field is omitted/empty + }, + }) + + instance.DefaultServices() + + template := (*instance.Spec.Galera.Templates)["openstack"] + Expect(template.Secret).To(Equal("")) + }) + + It("should preserve explicitly set Secret value", func() { + // Create a Galera template with explicit Secret + instance.Spec.Galera.Templates = ptr.To(map[string]mariadbv1.GaleraSpecCore{ + "openstack": { + StorageRequest: "500M", + Secret: "custom-secret", + }, + }) + + instance.DefaultServices() + + template := (*instance.Spec.Galera.Templates)["openstack"] + Expect(template.Secret).To(Equal("custom-secret")) + }) + + It("should preserve explicitly blank Secret for auto-generation", func() { + // Create a Galera template with explicitly blank Secret + instance.Spec.Galera.Templates = ptr.To(map[string]mariadbv1.GaleraSpecCore{ + "openstack": { + StorageRequest: "500M", + Secret: "", // Explicitly blank for auto-generation + }, + }) + + instance.DefaultServices() + + template := (*instance.Spec.Galera.Templates)["openstack"] + // Should remain blank to allow mariadb-operator auto-generation + Expect(template.Secret).To(Equal("")) + }) + }) }) diff --git a/test/functional/ctlplane/openstackoperator_controller_test.go b/test/functional/ctlplane/openstackoperator_controller_test.go index f237bc269..8a91df3d6 100644 --- a/test/functional/ctlplane/openstackoperator_controller_test.go +++ b/test/functional/ctlplane/openstackoperator_controller_test.go @@ -3083,6 +3083,168 @@ var _ = Describe("OpenStackOperator controller", func() { // to nil does not clear template-level NotificationsBus configuration. // Template-level takes precedence over top-level. }) + + // + // Galera Secret field behavior tests + // + When("A OpenStackControlPlane with blank Galera secret is created", func() { + BeforeEach(func() { + spec := GetDefaultOpenStackControlPlaneSpec() + spec["tls"] = GetTLSPublicSpec() + + // Modify galera template to have blank secret for auto-generation + galeraTemplate := spec["galera"].(map[string]interface{}) + templates := galeraTemplate["templates"].(map[string]interface{}) + dbTemplate := templates[names.DBName.Name].(map[string]interface{}) + dbTemplate["secret"] = "" // Explicitly blank for auto-generated password + + DeferCleanup( + th.DeleteInstance, + CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec), + ) + }) + + It("should create Galera CR with blank secret allowing auto-generation", func() { + OSCtlplane := GetOpenStackControlPlane(names.OpenStackControlplaneName) + Expect(OSCtlplane.Spec.Galera.Enabled).Should(BeTrue()) + + Eventually(func(g Gomega) { + db := mariadb.GetGalera(names.DBName) + g.Expect(db).Should(Not(BeNil())) + // When created fresh with blank secret, it should remain blank + // (allowing mariadb-operator to auto-generate the root password) + g.Expect(db.Spec.Secret).To(Equal("")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A OpenStackControlPlane with omitted Galera secret is created", func() { + BeforeEach(func() { + spec := GetDefaultOpenStackControlPlaneSpec() + spec["tls"] = GetTLSPublicSpec() + + // Modify galera template to omit secret entirely (not set) + galeraTemplate := spec["galera"].(map[string]interface{}) + templates := galeraTemplate["templates"].(map[string]interface{}) + dbTemplate := templates[names.DBName.Name].(map[string]interface{}) + delete(dbTemplate, "secret") // Omit the field entirely + + DeferCleanup(th.DeleteInstance, CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec)) + }) + + It("should create Galera CR with blank secret for auto-generation", func() { + Eventually(func(g Gomega) { + db := mariadb.GetGalera(names.DBName) + g.Expect(db).ShouldNot(BeNil()) + // When omitted (not explicitly set), should remain blank + // allowing mariadb-operator to auto-generate the password + g.Expect(db.Spec.Secret).To(Equal("")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("A OpenStackControlPlane with explicit custom Galera secret is created", func() { + BeforeEach(func() { + spec := GetDefaultOpenStackControlPlaneSpec() + spec["tls"] = GetTLSPublicSpec() + + // Modify galera template to use custom secret + galeraTemplate := spec["galera"].(map[string]interface{}) + templates := galeraTemplate["templates"].(map[string]interface{}) + dbTemplate := templates[names.DBName.Name].(map[string]interface{}) + dbTemplate["secret"] = "custom-galera-secret" + + DeferCleanup(th.DeleteInstance, CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec)) + }) + + It("should create Galera CR with the custom secret", func() { + Eventually(func(g Gomega) { + db := mariadb.GetGalera(names.DBName) + g.Expect(db).ShouldNot(BeNil()) + g.Expect(db.Spec.Secret).To(Equal("custom-galera-secret")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Multiple Galera templates with different secret configurations are created", func() { + BeforeEach(func() { + spec := GetDefaultOpenStackControlPlaneSpec() + spec["tls"] = GetTLSPublicSpec() + + // Modify galera templates to have different secret configurations + galeraTemplate := spec["galera"].(map[string]interface{}) + templates := map[string]interface{}{ + names.DBName.Name: map[string]interface{}{ + "storageRequest": "500M", + // secret is omitted + }, + names.DBCell1Name.Name: map[string]interface{}{ + "storageRequest": "500M", + "secret": "cell1-secret", // Explicit secret + }, + } + galeraTemplate["templates"] = templates + + DeferCleanup(th.DeleteInstance, CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec)) + }) + + It("should create each Galera CR with its respective secret configuration", func() { + // Verify main DB with blank secret + Eventually(func(g Gomega) { + db := mariadb.GetGalera(names.DBName) + g.Expect(db).ShouldNot(BeNil()) + g.Expect(db.Spec.Secret).To(Equal("")) + }, timeout, interval).Should(Succeed()) + + // Verify cell1 DB with explicit secret + Eventually(func(g Gomega) { + db := mariadb.GetGalera(names.DBCell1Name) + g.Expect(db).ShouldNot(BeNil()) + g.Expect(db.Spec.Secret).To(Equal("cell1-secret")) + }, timeout, interval).Should(Succeed()) + }) + }) + + // Test that we can change from explicit secret to auto-generated + When("An OpenStackControlPlane Galera secret starts as osp-secret", func() { + BeforeEach(func() { + spec := GetDefaultOpenStackControlPlaneSpec() + spec["tls"] = GetTLSPublicSpec() + + // Start with an EXPLICIT secret (old deployment style) + galeraTemplate := spec["galera"].(map[string]interface{}) + templates := galeraTemplate["templates"].(map[string]interface{}) + dbTemplate := templates[names.DBName.Name].(map[string]interface{}) + dbTemplate["secret"] = "osp-secret" // Explicit secret, as in pre-FR6 versions + + DeferCleanup(th.DeleteInstance, CreateOpenStackControlPlane(names.OpenStackControlplaneName, spec)) + }) + + It("should allow changing to blank secret for auto-generation", func() { + Eventually(func(g Gomega) { + db := mariadb.GetGalera(names.DBName) + g.Expect(db).ShouldNot(BeNil()) + g.Expect(db.Spec.Secret).To(Equal("osp-secret")) + }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + oscp := GetOpenStackControlPlane(names.OpenStackControlplaneName) + templates := *oscp.Spec.Galera.Templates + t := templates[names.DBName.Name] + t.Secret = "" // User removes secret to enable auto-gen + templates[names.DBName.Name] = t + oscp.Spec.Galera.Templates = &templates + g.Expect(k8sClient.Update(ctx, oscp)).Should(Succeed()) + }, timeout, interval).Should(Succeed()) + + Eventually(func(g Gomega) { + db := mariadb.GetGalera(names.DBName) + g.Expect(db).ShouldNot(BeNil()) + g.Expect(db.Spec.Secret).To(Equal("")) + }, timeout, interval).Should(Succeed()) + }) + + }) }) var _ = Describe("OpenStackOperator Webhook", func() {