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() {