diff --git a/api/bases/cinder.openstack.org_cinders.yaml b/api/bases/cinder.openstack.org_cinders.yaml index bfa1b22f..e4042ad4 100644 --- a/api/bases/cinder.openstack.org_cinders.yaml +++ b/api/bases/cinder.openstack.org_cinders.yaml @@ -2024,6 +2024,22 @@ spec: default: memcached description: Memcached instance name. type: string + messagingBus: + description: MessagingBus configuration (username, vhost, and cluster) + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object nodeSelector: additionalProperties: type: string @@ -2032,6 +2048,23 @@ spec: NodeSelector here acts as a default value and can be overridden by service specific NodeSelector Settings. type: object + notificationsBus: + description: NotificationsBus configuration (username, vhost, and + cluster) for notifications + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object notificationsBusInstance: description: |- RabbitMQ instance name used to request a transportURL that is used for diff --git a/api/go.mod b/api/go.mod index ea8eb214..44da61ce 100644 --- a/api/go.mod +++ b/api/go.mod @@ -17,7 +17,6 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emicklei/go-restful/v3 v3.12.2 // indirect - github.com/evanphx/json-patch v5.9.11+incompatible // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -44,6 +43,7 @@ require ( github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/common v0.65.0 // indirect github.com/prometheus/procfs v0.16.1 // indirect + github.com/rabbitmq/cluster-operator/v2 v2.16.0 // indirect github.com/spf13/pflag v1.0.7 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect diff --git a/api/go.sum b/api/go.sum index a27413e2..082291e5 100644 --- a/api/go.sum +++ b/api/go.sum @@ -1,3 +1,4 @@ +github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -84,6 +85,8 @@ github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.2025123021 github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:kycZyoe7OZdW1HUghr2nI3N7wSJtNahXf6b/ypD14f4= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251230215914-6ba873b49a35 h1:8WZYfCt1VJHa5sJRX0UhpmoXud/fn8LHQhXsakdYXuQ= github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:H0aQANk8iJPRhS2Bg9n6cYb/IHF0Cks9g7+uZG04Rhk= +github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec h1:saovr368HPAKHN0aRPh8h8n9s9dn3d8Frmfua0UYRlc= +github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec/go.mod h1:Nh2NEePLjovUQof2krTAg4JaAoLacqtPTZQXK6izNfg= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= diff --git a/api/v1beta1/cinder_types.go b/api/v1beta1/cinder_types.go index dbdc5426..67783b73 100644 --- a/api/v1beta1/cinder_types.go +++ b/api/v1beta1/cinder_types.go @@ -17,6 +17,7 @@ limitations under the License. package v1beta1 import ( + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/storage" @@ -70,6 +71,14 @@ type CinderSpecBase struct { // Needed to request a transportURL that is created and used in Cinder RabbitMqClusterName string `json:"rabbitMqClusterName"` + // +kubebuilder:validation:Optional + // MessagingBus configuration (username, vhost, and cluster) + MessagingBus rabbitmqv1.RabbitMqConfig `json:"messagingBus,omitempty"` + + // +kubebuilder:validation:Optional + // NotificationsBus configuration (username, vhost, and cluster) for notifications + NotificationsBus *rabbitmqv1.RabbitMqConfig `json:"notificationsBus,omitempty"` + // +kubebuilder:validation:Required // +kubebuilder:default=memcached // Memcached instance name. diff --git a/api/v1beta1/cinder_webhook.go b/api/v1beta1/cinder_webhook.go index 20e94d09..3876cc73 100644 --- a/api/v1beta1/cinder_webhook.go +++ b/api/v1beta1/cinder_webhook.go @@ -26,6 +26,7 @@ import ( "golang.org/x/exp/maps" + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/service" "github.com/openstack-k8s-operators/lib-common/modules/common/util" @@ -111,6 +112,20 @@ func (r *Cinder) Default() { // Default - set defaults for this Cinder spec func (spec *CinderSpecBase) Default() { + rabbitmqv1.DefaultRabbitMqConfig(&spec.MessagingBus, spec.RabbitMqClusterName) + + // Default NotificationsBus if NotificationsBusInstance is specified + if spec.NotificationsBusInstance != nil && *spec.NotificationsBusInstance != "" { + if spec.NotificationsBus == nil { + // Initialize empty NotificationsBus - credentials will be created dynamically + // to ensure separation from MessagingBus (RPC and notifications should never share credentials) + spec.NotificationsBus = &rabbitmqv1.RabbitMqConfig{} + } + // Always default the Cluster field from NotificationsBusInstance if it's empty + if spec.NotificationsBus.Cluster == "" { + rabbitmqv1.DefaultRabbitMqConfig(spec.NotificationsBus, *spec.NotificationsBusInstance) + } + } if spec.DBPurge.Age == 0 { spec.DBPurge.Age = cinderDefaults.DBPurgeAge @@ -275,6 +290,21 @@ func (spec *CinderSpec) ValidateUpdate( var allErrs field.ErrorList var allWarns []string + // Reject changes to deprecated RabbitMqClusterName field - users should use the new messagingBus.cluster field instead + if spec.RabbitMqClusterName != old.RabbitMqClusterName { + allErrs = append(allErrs, field.Forbidden( + basePath.Child("rabbitMqClusterName"), + "rabbitMqClusterName is deprecated and cannot be changed. Please use messagingBus.cluster instead")) + } + + // Reject changes to deprecated NotificationsBusInstance field + if spec.NotificationsBusInstance != nil && old.NotificationsBusInstance != nil && + *spec.NotificationsBusInstance != *old.NotificationsBusInstance { + allErrs = append(allErrs, field.Forbidden( + basePath.Child("notificationsBusInstance"), + "notificationsBusInstance is deprecated and cannot be changed. Please use notificationsBus.cluster instead")) + } + // validate the service override key is valid allErrs = append(allErrs, service.ValidateRoutedOverrides( basePath.Child("cinderAPI").Child("override").Child("service"), @@ -301,6 +331,21 @@ func (spec *CinderSpecCore) ValidateUpdate( var allErrs field.ErrorList var allWarns []string + // Reject changes to deprecated RabbitMqClusterName field - users should use the new messagingBus.cluster field instead + if spec.RabbitMqClusterName != old.RabbitMqClusterName { + allErrs = append(allErrs, field.Forbidden( + basePath.Child("rabbitMqClusterName"), + "rabbitMqClusterName is deprecated and cannot be changed. Please use messagingBus.cluster instead")) + } + + // Reject changes to deprecated NotificationsBusInstance field + if spec.NotificationsBusInstance != nil && old.NotificationsBusInstance != nil && + *spec.NotificationsBusInstance != *old.NotificationsBusInstance { + allErrs = append(allErrs, field.Forbidden( + basePath.Child("notificationsBusInstance"), + "notificationsBusInstance is deprecated and cannot be changed. Please use notificationsBus.cluster instead")) + } + // validate the service override key is valid allErrs = append(allErrs, service.ValidateRoutedOverrides( basePath.Child("cinderAPI").Child("override").Child("service"), diff --git a/api/v1beta1/conditions.go b/api/v1beta1/conditions.go index 49a0a94f..05238919 100644 --- a/api/v1beta1/conditions.go +++ b/api/v1beta1/conditions.go @@ -32,6 +32,9 @@ const ( // CinderVolumeReadyCondition Status=True condition which indicates if the CinderVolume is configured and operational CinderVolumeReadyCondition condition.Type = "CinderVolumeReady" + + // CinderNotificationBusReadyCondition Status=True condition which indicates if the NotificationBus is configured + CinderNotificationBusReadyCondition condition.Type = "CinderNotificationBusReady" ) // Cinder Reasons used by API objects. @@ -77,4 +80,19 @@ const ( // CinderVolumeReadyRunningMessage CinderVolumeReadyRunningMessage = "CinderVolume deployments in progress" + + // + // CinderNotificationBusReady condition messages + // + // CinderNotificationBusReadyInitMessage + CinderNotificationBusReadyInitMessage = "CinderNotificationBus not started" + + // CinderNotificationBusReadyRunningMessage + CinderNotificationBusReadyRunningMessage = "CinderNotificationBus creation in progress" + + // CinderNotificationBusReadyMessage + CinderNotificationBusReadyMessage = "CinderNotificationBus successfully created" + + // CinderNotificationBusReadyErrorMessage + CinderNotificationBusReadyErrorMessage = "CinderNotificationBus error occured %s" ) diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 5cb37ef8..9f7217b1 100644 --- a/api/v1beta1/zz_generated.deepcopy.go +++ b/api/v1beta1/zz_generated.deepcopy.go @@ -21,6 +21,7 @@ limitations under the License. package v1beta1 import ( + rabbitmqv1beta1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1beta1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" "github.com/openstack-k8s-operators/lib-common/modules/common/condition" "github.com/openstack-k8s-operators/lib-common/modules/common/service" @@ -783,6 +784,12 @@ func (in *CinderSpec) DeepCopy() *CinderSpec { func (in *CinderSpecBase) DeepCopyInto(out *CinderSpecBase) { *out = *in out.CinderTemplate = in.CinderTemplate + out.MessagingBus = in.MessagingBus + if in.NotificationsBus != nil { + in, out := &in.NotificationsBus, &out.NotificationsBus + *out = new(rabbitmqv1beta1.RabbitMqConfig) + **out = **in + } if in.ExtraMounts != nil { in, out := &in.ExtraMounts, &out.ExtraMounts *out = make([]CinderExtraVolMounts, len(*in)) diff --git a/config/crd/bases/cinder.openstack.org_cinders.yaml b/config/crd/bases/cinder.openstack.org_cinders.yaml index bfa1b22f..e4042ad4 100644 --- a/config/crd/bases/cinder.openstack.org_cinders.yaml +++ b/config/crd/bases/cinder.openstack.org_cinders.yaml @@ -2024,6 +2024,22 @@ spec: default: memcached description: Memcached instance name. type: string + messagingBus: + description: MessagingBus configuration (username, vhost, and cluster) + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object nodeSelector: additionalProperties: type: string @@ -2032,6 +2048,23 @@ spec: NodeSelector here acts as a default value and can be overridden by service specific NodeSelector Settings. type: object + notificationsBus: + description: NotificationsBus configuration (username, vhost, and + cluster) for notifications + properties: + cluster: + description: Name of the cluster + minLength: 1 + type: string + user: + description: User - RabbitMQ username + type: string + vhost: + description: Vhost - RabbitMQ vhost name + type: string + required: + - cluster + type: object notificationsBusInstance: description: |- RabbitMQ instance name used to request a transportURL that is used for diff --git a/internal/controller/cinder_controller.go b/internal/controller/cinder_controller.go index 7d166dfb..3b602eac 100644 --- a/internal/controller/cinder_controller.go +++ b/internal/controller/cinder_controller.go @@ -525,7 +525,7 @@ func (r *CinderReconciler) reconcileNormal(ctx context.Context, instance *cinder // create RabbitMQ transportURL CR and get the actual URL from the associated secret that is created // - transportURL, op, err := r.transportURLCreateOrUpdate(ctx, instance, serviceLabels, "") + transportURL, op, err := r.transportURLCreateOrUpdate(ctx, instance, serviceLabels, "", instance.Spec.MessagingBus) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( condition.RabbitMqTransportURLReadyCondition, @@ -561,19 +561,26 @@ func (r *CinderReconciler) reconcileNormal(ctx context.Context, instance *cinder // associated secret that is created // - // Request TransportURL when the parameter is provided in the CR - // and it does not match with the existing RabbitMqClusterName - if instance.Spec.NotificationsBusInstance != nil { - // init .Status.NotificationURLSecret - instance.Status.NotificationsURLSecret = ptr.To("") - + // Determine if notifications are enabled by checking NotificationsBus.Cluster + // (the webhook defaults this from the deprecated NotificationsBusInstance field) + var notificationBusName string + notificationsEnabled := instance.Spec.NotificationsBus != nil && instance.Spec.NotificationsBus.Cluster != "" + if notificationsEnabled { // setting notificationBusName to an empty string ensures that we do not // request a new transportURL unless the two spec fields do not match - var notificationBusName string - if *instance.Spec.NotificationsBusInstance != instance.Spec.RabbitMqClusterName { - notificationBusName = *instance.Spec.NotificationsBusInstance + if instance.Spec.NotificationsBus.Cluster != instance.Spec.RabbitMqClusterName { + notificationBusName = instance.Spec.NotificationsBus.Cluster } - notificationBusInstanceURL, op, err := r.transportURLCreateOrUpdate(ctx, instance, serviceLabels, notificationBusName) + } + + // Request TransportURL when notifications are enabled + if notificationsEnabled { + // init .Status.NotificationURLSecret + instance.Status.NotificationsURLSecret = ptr.To("") + + // Use NotificationsBus config (never fall back to MessagingBus to ensure separation) + notificationsRabbitMqConfig := *instance.Spec.NotificationsBus + notificationBusInstanceURL, op, err := r.transportURLCreateOrUpdate(ctx, instance, serviceLabels, notificationBusName, notificationsRabbitMqConfig) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( condition.NotificationBusInstanceReadyCondition, @@ -603,7 +610,7 @@ func (r *CinderReconciler) reconcileNormal(ctx context.Context, instance *cinder instance.Status.Conditions.MarkTrue(condition.NotificationBusInstanceReadyCondition, condition.NotificationBusInstanceReadyMessage) } else { // make sure we do not have an entry in the status if - // .Spec.NotificationsURLSecret is not provided + // notifications are not enabled instance.Status.NotificationsURLSecret = nil } @@ -1192,6 +1199,7 @@ func (r *CinderReconciler) transportURLCreateOrUpdate( instance *cinderv1beta1.Cinder, serviceLabels map[string]string, rabbitMqClusterName string, + rabbitMqConfig rabbitmqv1.RabbitMqConfig, ) (*rabbitmqv1.TransportURL, controllerutil.OperationResult, error) { // Default values used for regular messagingBus transportURL and explicitly @@ -1215,9 +1223,12 @@ func (r *CinderReconciler) transportURLCreateOrUpdate( op, err := controllerutil.CreateOrUpdate(ctx, r.Client, transportURL, func() error { transportURL.Spec.RabbitmqClusterName = transportURLName - - err := controllerutil.SetControllerReference(instance, transportURL, r.Scheme) - return err + if rabbitMqConfig.User != "" { + transportURL.Spec.Username = rabbitMqConfig.User + } + // Always set Vhost - empty string means use default "/" vhost + transportURL.Spec.Vhost = rabbitMqConfig.Vhost + return controllerutil.SetControllerReference(instance, transportURL, r.Scheme) }) return transportURL, op, err diff --git a/test/functional/cinder_controller_test.go b/test/functional/cinder_controller_test.go index 3c23ac91..35ebaafc 100644 --- a/test/functional/cinder_controller_test.go +++ b/test/functional/cinder_controller_test.go @@ -36,6 +36,7 @@ import ( cinderv1 "github.com/openstack-k8s-operators/cinder-operator/api/v1beta1" "github.com/openstack-k8s-operators/cinder-operator/internal/cinder" memcachedv1 "github.com/openstack-k8s-operators/infra-operator/apis/memcached/v1beta1" + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition" util "github.com/openstack-k8s-operators/lib-common/modules/common/util" @@ -1299,44 +1300,6 @@ var _ = Describe("Cinder controller", func() { *cinder.Status.NotificationsURLSecret)) }, timeout, interval).Should(Succeed()) }) - It("overrides cinder CR notifications", func() { - // add new-rabbit in cinder CR - DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.NotificationSecretName)) - - // update cinder CR to point to the new (dedicated) rabbit instance - Eventually(func(g Gomega) { - cinder := GetCinder(cinderTest.Instance) - *cinder.Spec.NotificationsBusInstance = "rabbitmq-notification" - g.Expect(k8sClient.Update(ctx, cinder)).To(Succeed()) - }, timeout, interval).Should(Succeed()) - - th.ExpectCondition( - cinderTest.Instance, - ConditionGetterFunc(CinderConditionGetter), - condition.NotificationBusInstanceReadyCondition, - corev1.ConditionTrue, - ) - - Eventually(func(g Gomega) { - cinder := GetCinder(cinderTest.Instance) - g.Expect(*cinder.Status.NotificationsURLSecret).ToNot( - Equal(cinder.Status.TransportURLSecret)) - }, timeout, interval).Should(Succeed()) - }) - - It("updates cinder CR and disable notifications", func() { - Eventually(func(g Gomega) { - cinder := GetCinder(cinderTest.Instance) - cinder.Spec.NotificationsBusInstance = nil - g.Expect(k8sClient.Update(ctx, cinder)).To(Succeed()) - }, timeout, interval).Should(Succeed()) - - Eventually(func(g Gomega) { - cinder := GetCinder(cinderTest.Instance) - g.Expect(cinder.Status.NotificationsURLSecret).To(BeNil()) - g.Expect(cinder.Status.TransportURLSecret).ToNot(Equal("")) - }, timeout, interval).Should(Succeed()) - }) }) // Run MariaDBAccount suite tests. these are pre-packaged ginkgo tests // that exercise standard account create / update patterns that should be @@ -1806,3 +1769,377 @@ var _ = Describe("Cinder Webhook", func() { Expect(errors.As(err, &statusError)).To(BeFalse()) }) }) + +var _ = Describe("Cinder with RabbitMQ custom vhost and user", func() { + var memcachedSpec memcachedv1.MemcachedSpec + + BeforeEach(func() { + err := os.Setenv("OPERATOR_TEMPLATES", "../../templates") + Expect(err).NotTo(HaveOccurred()) + + memcachedSpec = memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(3)), + }, + } + }) + + When("Cinder is created with custom RabbitMQ vhost and user", func() { + BeforeEach(func() { + spec := GetDefaultCinderSpec() + spec["messagingBus"] = map[string]any{ + "user": "custom-user", + "vhost": "custom-vhost", + } + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.RabbitmqSecretName)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, cinderTest.MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace)) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + }) + + It("should create TransportURL with custom vhost and user", func() { + Eventually(func(g Gomega) { + transportURL := infra.GetTransportURL(cinderTest.CinderTransportURL) + g.Expect(transportURL.Spec.Username).To(Equal("custom-user")) + g.Expect(transportURL.Spec.Vhost).To(Equal("custom-vhost")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Cinder is created with default RabbitMQ configuration", func() { + BeforeEach(func() { + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, GetDefaultCinderSpec())) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.RabbitmqSecretName)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, cinderTest.MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace)) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + }) + + It("should create TransportURL with default vhost and user", func() { + Eventually(func(g Gomega) { + transportURL := infra.GetTransportURL(cinderTest.CinderTransportURL) + g.Expect(transportURL.Spec.Username).To(Equal("")) + g.Expect(transportURL.Spec.Vhost).To(Equal("")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Cinder is created with only custom RabbitMQ user", func() { + BeforeEach(func() { + spec := GetDefaultCinderSpec() + spec["messagingBus"] = map[string]any{ + "user": "custom-user-only", + } + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.RabbitmqSecretName)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, cinderTest.MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace)) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + }) + + It("should create TransportURL with custom user and default vhost", func() { + Eventually(func(g Gomega) { + transportURL := infra.GetTransportURL(cinderTest.CinderTransportURL) + g.Expect(transportURL.Spec.Username).To(Equal("custom-user-only")) + g.Expect(transportURL.Spec.Vhost).To(Equal("")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Cinder is created with only custom RabbitMQ vhost", func() { + BeforeEach(func() { + spec := GetDefaultCinderSpec() + spec["messagingBus"] = map[string]any{ + "vhost": "custom-vhost-only", + } + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.RabbitmqSecretName)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, cinderTest.MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace)) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + }) + + It("should create TransportURL with custom vhost and default user", func() { + Eventually(func(g Gomega) { + transportURL := infra.GetTransportURL(cinderTest.CinderTransportURL) + g.Expect(transportURL.Spec.Username).To(Equal("")) + g.Expect(transportURL.Spec.Vhost).To(Equal("custom-vhost-only")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Cinder RabbitMQ configuration is updated", func() { + BeforeEach(func() { + spec := GetDefaultCinderSpec() + spec["messagingBus"] = map[string]any{ + "user": "initial-user", + "vhost": "initial-vhost", + } + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.RabbitmqSecretName)) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, cinderTest.MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace)) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + }) + + It("should update TransportURL when RabbitMQ configuration changes", func() { + // Verify initial configuration + Eventually(func(g Gomega) { + transportURL := infra.GetTransportURL(cinderTest.CinderTransportURL) + g.Expect(transportURL.Spec.Username).To(Equal("initial-user")) + g.Expect(transportURL.Spec.Vhost).To(Equal("initial-vhost")) + }, timeout, interval).Should(Succeed()) + + // Update the Cinder CR with new RabbitMQ configuration + Eventually(func(g Gomega) { + cinder := GetCinder(cinderTest.Instance) + cinder.Spec.MessagingBus.User = "updated-user" + cinder.Spec.MessagingBus.Vhost = "updated-vhost" + g.Expect(k8sClient.Update(ctx, cinder)).To(Succeed()) + }, timeout, interval).Should(Succeed()) + + // Verify the TransportURL is updated + Eventually(func(g Gomega) { + transportURL := infra.GetTransportURL(cinderTest.CinderTransportURL) + g.Expect(transportURL.Spec.Username).To(Equal("updated-user")) + g.Expect(transportURL.Spec.Vhost).To(Equal("updated-vhost")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Cinder is created with different RabbitMQ configs for main and notifications", func() { + BeforeEach(func() { + spec := GetDefaultCinderSpec() + spec["messagingBus"] = map[string]any{ + "user": "main-user", + "vhost": "main-vhost", + } + spec["notificationsBus"] = map[string]any{ + "user": "notifications-user", + "vhost": "notifications-vhost", + } + spec["notificationsBusInstance"] = "rabbitmq-notifications" + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.RabbitmqSecretName)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, "rabbitmq-notifications-secret")) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, cinderTest.MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace)) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + }) + + It("should use different credentials for main and notifications TransportURLs", func() { + // Verify main TransportURL has main-specific config + Eventually(func(g Gomega) { + transportURL := infra.GetTransportURL(cinderTest.CinderTransportURL) + g.Expect(transportURL.Spec.Username).To(Equal("main-user")) + g.Expect(transportURL.Spec.Vhost).To(Equal("main-vhost")) + g.Expect(transportURL.Spec.RabbitmqClusterName).To(Equal(cinderTest.RabbitmqClusterName)) + }, timeout, interval).Should(Succeed()) + + // Verify notifications TransportURL has notifications-specific config + notificationsTransportURLName := types.NamespacedName{ + Namespace: cinderTest.Instance.Namespace, + Name: fmt.Sprintf("%s-cinder-transport-rabbitmq-notifications", cinderTest.Instance.Name), + } + infra.SimulateTransportURLReady(notificationsTransportURLName) + + Eventually(func(g Gomega) { + notificationsTransportURL := infra.GetTransportURL(notificationsTransportURLName) + g.Expect(notificationsTransportURL.Spec.Username).To(Equal("notifications-user")) + g.Expect(notificationsTransportURL.Spec.Vhost).To(Equal("notifications-vhost")) + g.Expect(notificationsTransportURL.Spec.RabbitmqClusterName).To(Equal("rabbitmq-notifications")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Cinder is created with notifications bus using default credentials", func() { + BeforeEach(func() { + spec := GetDefaultCinderSpec() + spec["notificationsBusInstance"] = "rabbitmq-notifications" + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.RabbitmqSecretName)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, "rabbitmq-notifications-secret")) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, cinderTest.MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace)) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + }) + + It("should create both TransportURLs with default credentials", func() { + // Verify main TransportURL has default config + Eventually(func(g Gomega) { + transportURL := infra.GetTransportURL(cinderTest.CinderTransportURL) + g.Expect(transportURL.Spec.Username).To(Equal("")) + g.Expect(transportURL.Spec.Vhost).To(Equal("")) + }, timeout, interval).Should(Succeed()) + + // Verify notifications TransportURL also has default config + notificationsTransportURLName := types.NamespacedName{ + Namespace: cinderTest.Instance.Namespace, + Name: fmt.Sprintf("%s-cinder-transport-rabbitmq-notifications", cinderTest.Instance.Name), + } + infra.SimulateTransportURLReady(notificationsTransportURLName) + + Eventually(func(g Gomega) { + notificationsTransportURL := infra.GetTransportURL(notificationsTransportURLName) + g.Expect(notificationsTransportURL.Spec.Username).To(Equal("")) + g.Expect(notificationsTransportURL.Spec.Vhost).To(Equal("")) + g.Expect(notificationsTransportURL.Spec.RabbitmqClusterName).To(Equal("rabbitmq-notifications")) + }, timeout, interval).Should(Succeed()) + }) + }) + + When("Cinder notifications are disabled after creation", func() { + BeforeEach(func() { + spec := GetDefaultCinderSpec() + // Start with notifications enabled + spec["notificationsBusInstance"] = "rabbitmq-notifications" + DeferCleanup(th.DeleteInstance, CreateCinder(cinderTest.Instance, spec)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, cinderTest.RabbitmqSecretName)) + DeferCleanup(k8sClient.Delete, ctx, CreateCinderMessageBusSecret(cinderTest.Instance.Namespace, "rabbitmq-notifications-secret")) + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + cinderTest.Instance.Namespace, + GetCinder(cinderTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + infra.SimulateTransportURLReady(cinderTest.CinderTransportURL) + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, cinderTest.MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(cinderTest.CinderMemcached) + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(cinderTest.Instance.Namespace)) + mariadb.SimulateMariaDBAccountCompleted(cinderTest.Database) + mariadb.SimulateMariaDBDatabaseCompleted(cinderTest.Database) + }) + + It("should remove NotificationsURLSecret when notificationsBus is set to empty", func() { + // Wait for the notifications TransportURL to be created and simulate it being ready + notificationsTransportURLName := types.NamespacedName{ + Namespace: cinderTest.Instance.Namespace, + Name: fmt.Sprintf("%s-cinder-transport-rabbitmq-notifications", cinderTest.Instance.Name), + } + Eventually(func(g Gomega) { + transportURL := &rabbitmqv1.TransportURL{} + err := k8sClient.Get(ctx, notificationsTransportURLName, transportURL) + g.Expect(err).ToNot(HaveOccurred()) + }, timeout, interval).Should(Succeed()) + infra.SimulateTransportURLReady(notificationsTransportURLName) + + // Verify notifications are enabled + Eventually(func(g Gomega) { + cinder := GetCinder(cinderTest.Instance) + g.Expect(cinder.Status.NotificationsURLSecret).ToNot(BeNil()) + g.Expect(*cinder.Status.NotificationsURLSecret).ToNot(Equal(cinder.Status.TransportURLSecret)) + }, timeout, interval).Should(Succeed()) + + // Now disable notifications by clearing both the deprecated field and the new field + Eventually(func(g Gomega) { + cinder := GetCinder(cinderTest.Instance) + cinder.Spec.NotificationsBusInstance = nil + cinder.Spec.NotificationsBus = nil + g.Expect(k8sClient.Update(ctx, cinder)).To(Succeed()) + }, timeout, interval).Should(Succeed()) + + // Verify NotificationsURLSecret is now nil + Eventually(func(g Gomega) { + cinder := GetCinder(cinderTest.Instance) + g.Expect(cinder.Status.NotificationsURLSecret).To(BeNil()) + g.Expect(cinder.Status.TransportURLSecret).ToNot(Equal("")) + }, timeout, interval).Should(Succeed()) + }) + }) +}) diff --git a/test/functional/cinder_webhook_test.go b/test/functional/cinder_webhook_test.go new file mode 100644 index 00000000..1d2dd54c --- /dev/null +++ b/test/functional/cinder_webhook_test.go @@ -0,0 +1,110 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package functional + +import ( + "errors" + + . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports + . "github.com/onsi/gomega" //revive:disable:dot-imports + + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +var _ = Describe("Cinder webhook", func() { + It("rejects update to deprecated rabbitMqClusterName field", func() { + spec := GetDefaultCinderSpec() + spec["rabbitMqClusterName"] = "rabbitmq" + + raw := map[string]any{ + "apiVersion": "cinder.openstack.org/v1beta1", + "kind": "Cinder", + "metadata": map[string]any{ + "name": cinderName.Name, + "namespace": cinderName.Namespace, + }, + "spec": spec, + } + + // Create the Cinder instance + unstructuredObj := &unstructured.Unstructured{Object: raw} + _, err := controllerutil.CreateOrPatch( + ctx, k8sClient, unstructuredObj, func() error { return nil }) + Expect(err).ShouldNot(HaveOccurred()) + + // Try to update rabbitMqClusterName + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, cinderName, unstructuredObj)).Should(Succeed()) + specMap := unstructuredObj.Object["spec"].(map[string]any) + specMap["rabbitMqClusterName"] = "rabbitmq2" + err := k8sClient.Update(ctx, unstructuredObj) + g.Expect(err).Should(HaveOccurred()) + + var statusError *k8s_errors.StatusError + g.Expect(errors.As(err, &statusError)).To(BeTrue()) + g.Expect(statusError.ErrStatus.Details.Kind).To(Equal("Cinder")) + g.Expect(statusError.ErrStatus.Message).To( + ContainSubstring("rabbitMqClusterName is deprecated and cannot be changed")) + g.Expect(statusError.ErrStatus.Message).To( + ContainSubstring("Please use messagingBus.cluster instead")) + }, timeout, interval).Should(Succeed()) + }) + + It("rejects update to deprecated notificationsBusInstance field", func() { + spec := GetDefaultCinderSpec() + notificationsBusInstance := "notifications-rabbitmq" + spec["notificationsBusInstance"] = notificationsBusInstance + + cinderName2 := cinderName + cinderName2.Name = cinderName.Name + "-notifications" + + raw := map[string]any{ + "apiVersion": "cinder.openstack.org/v1beta1", + "kind": "Cinder", + "metadata": map[string]any{ + "name": cinderName2.Name, + "namespace": cinderName2.Namespace, + }, + "spec": spec, + } + + // Create the Cinder instance + unstructuredObj := &unstructured.Unstructured{Object: raw} + _, err := controllerutil.CreateOrPatch( + ctx, k8sClient, unstructuredObj, func() error { return nil }) + Expect(err).ShouldNot(HaveOccurred()) + + // Try to update notificationsBusInstance + Eventually(func(g Gomega) { + g.Expect(k8sClient.Get(ctx, cinderName2, unstructuredObj)).Should(Succeed()) + specMap := unstructuredObj.Object["spec"].(map[string]any) + specMap["notificationsBusInstance"] = "notifications-rabbitmq2" + err := k8sClient.Update(ctx, unstructuredObj) + g.Expect(err).Should(HaveOccurred()) + + var statusError *k8s_errors.StatusError + g.Expect(errors.As(err, &statusError)).To(BeTrue()) + g.Expect(statusError.ErrStatus.Details.Kind).To(Equal("Cinder")) + g.Expect(statusError.ErrStatus.Message).To( + ContainSubstring("notificationsBusInstance is deprecated and cannot be changed")) + g.Expect(statusError.ErrStatus.Message).To( + ContainSubstring("Please use notificationsBus.cluster instead")) + }, timeout, interval).Should(Succeed()) + }) +})