diff --git a/api/bases/watcher.openstack.org_watchers.yaml b/api/bases/watcher.openstack.org_watchers.yaml index 00c8e78c..16286b84 100644 --- a/api/bases/watcher.openstack.org_watchers.yaml +++ b/api/bases/watcher.openstack.org_watchers.yaml @@ -617,6 +617,22 @@ spec: description: MemcachedInstance is the name of the Memcached CR that all watcher service will use. 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 @@ -624,6 +640,23 @@ spec: NodeSelector to target subset of worker nodes running this component. Setting here overrides any global NodeSelector settings within the Watcher CR. 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: |- NotificationsBusInstance is the name of the RabbitMqCluster CR to select @@ -632,6 +665,7 @@ spec: If undefined, the value will be inherited from OpenStackControlPlane. An empty value "" leaves the notification drivers unconfigured and emitting no notifications at all. Avoid colocating it with RabbitMqClusterName or other message bus instances used for RPC. + Deprecated: Use NotificationsBus.Cluster instead type: string passwordSelectors: default: @@ -655,10 +689,10 @@ spec: description: Secret containing prometheus connection parameters type: string rabbitMqClusterName: - default: rabbitmq description: |- RabbitMQ instance name Needed to request a transportURL that is created and used in Watcher + Deprecated: Use MessagingBus.Cluster instead type: string secret: default: osp-secret @@ -694,7 +728,6 @@ spec: - databaseInstance - decisionengineContainerImageURL - decisionengineServiceTemplate - - rabbitMqClusterName type: object status: description: WatcherStatus defines the observed state of Watcher diff --git a/api/go.mod b/api/go.mod index 10c61eee..4ec9bae2 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,8 +5,8 @@ go 1.24.4 toolchain go1.24.6 require ( - github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260115124008-0121df869109 - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35 + github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09 + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb k8s.io/api v0.31.14 k8s.io/apimachinery v0.31.14 k8s.io/utils v0.0.0-20250820121507-0af2bda4dd1d @@ -18,7 +18,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 @@ -40,12 +39,12 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/onsi/gomega v1.39.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.22.0 // indirect 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 0f83c68f..633693ab 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= @@ -78,10 +79,12 @@ github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8 github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260115124008-0121df869109 h1:S+A67nntHZrL1lIL3qr91CpJj+A67M/G4t1cTKzeGdo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260115124008-0121df869109/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35 h1:pF3mJ3nwq6r4qwom+rEWZNquZpcQW/iftHlJ1KPIDsk= -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/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09 h1:vhAGLKZitJIffj7ONiPpKmOX7Tmt/LGJpaY0Z2LeyfQ= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb h1:S7tnYO/E1f1KQfcp7N5bam8+ax/ExDTOhZ1WqG4Bfu0= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb/go.mod h1:ndqfy1KbVorHH6+zlUFPIrCRhMSxO3ImYJUGaooE0x0= +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/common_types.go b/api/v1beta1/common_types.go index 94e79470..dc3e7132 100644 --- a/api/v1beta1/common_types.go +++ b/api/v1beta1/common_types.go @@ -17,9 +17,9 @@ limitations under the License. package v1beta1 import ( - "github.com/openstack-k8s-operators/lib-common/modules/common/util" - + 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/util" corev1 "k8s.io/api/core/v1" ) @@ -83,11 +83,19 @@ type WatcherSpecCore struct { // Important: Run "make" to regenerate code after modifying this file WatcherCommon `json:",inline"` - // +kubebuilder:validation:Required - // +kubebuilder:default=rabbitmq + // +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:Optional // RabbitMQ instance name // Needed to request a transportURL that is created and used in Watcher - RabbitMqClusterName *string `json:"rabbitMqClusterName"` + // Deprecated: Use MessagingBus.Cluster instead + RabbitMqClusterName *string `json:"rabbitMqClusterName,omitempty"` // +kubebuilder:validation:Optional // +kubebuilder:default=osp-secret @@ -141,6 +149,7 @@ type WatcherSpecCore struct { // If undefined, the value will be inherited from OpenStackControlPlane. // An empty value "" leaves the notification drivers unconfigured and emitting no notifications at all. // Avoid colocating it with RabbitMqClusterName or other message bus instances used for RPC. + // Deprecated: Use NotificationsBus.Cluster instead NotificationsBusInstance *string `json:"notificationsBusInstance,omitempty"` } diff --git a/api/v1beta1/watcher_webhook.go b/api/v1beta1/watcher_webhook.go index b6971327..1ad17f61 100644 --- a/api/v1beta1/watcher_webhook.go +++ b/api/v1beta1/watcher_webhook.go @@ -20,6 +20,7 @@ import ( "fmt" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" + common_webhook "github.com/openstack-k8s-operators/lib-common/modules/common/webhook" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -59,12 +60,52 @@ func (r *Watcher) Default() { // Default - set defaults for this WatcherCore spec. func (spec *WatcherSpec) Default() { + spec.WatcherSpecCore.Default() spec.WatcherImages.Default(watcherDefaults) } // Default - set defaults for this WatcherSpecCore spec. func (spec *WatcherSpecCore) Default() { - // no validations . Placeholder for defaulting webhook integrated in the OpenStackControlPlane + // Default MessagingBus.Cluster if not set + // Migration from deprecated fields is handled by openstack-operator + if spec.MessagingBus.Cluster == "" { + spec.MessagingBus.Cluster = "rabbitmq" + } + + // NotificationsBus.Cluster is not defaulted - it must be explicitly set if NotificationsBus is configured + // This ensures users make a conscious choice about which cluster to use for notifications +} + +// getDeprecatedFields returns the centralized list of deprecated fields for WatcherSpecCore +func (spec *WatcherSpecCore) getDeprecatedFields(old *WatcherSpecCore) []common_webhook.DeprecatedFieldUpdate { + // Get new field value (handle nil NotificationsBus) + var newNotifBusCluster *string + if spec.NotificationsBus != nil { + newNotifBusCluster = &spec.NotificationsBus.Cluster + } + + deprecatedFields := []common_webhook.DeprecatedFieldUpdate{ + { + DeprecatedFieldName: "rabbitMqClusterName", + NewFieldPath: []string{"messagingBus", "cluster"}, + NewDeprecatedValue: spec.RabbitMqClusterName, + NewValue: &spec.MessagingBus.Cluster, + }, + { + DeprecatedFieldName: "notificationsBusInstance", + NewFieldPath: []string{"notificationsBus", "cluster"}, + NewDeprecatedValue: spec.NotificationsBusInstance, + NewValue: newNotifBusCluster, + }, + } + + // If old spec is provided (UPDATE operation), add old values + if old != nil { + deprecatedFields[0].OldDeprecatedValue = old.RabbitMqClusterName + deprecatedFields[1].OldDeprecatedValue = old.NotificationsBusInstance + } + + return deprecatedFields } var _ webhook.Validator = &Watcher{} @@ -73,29 +114,69 @@ var _ webhook.Validator = &Watcher{} func (r *Watcher) ValidateCreate() (admission.Warnings, error) { watcherlog.Info("validate create", "name", r.Name) - allErrs := r.Spec.ValidateCreate(field.NewPath("spec"), r.Namespace) + var allErrs field.ErrorList + var allWarns admission.Warnings + + basePath := field.NewPath("spec") + + if warns, errs := r.Spec.ValidateCreate(basePath, r.Namespace); errs != nil { + allErrs = append(allErrs, errs...) + allWarns = append(allWarns, warns...) + } if len(allErrs) != 0 { - return nil, apierrors.NewInvalid( + return allWarns, apierrors.NewInvalid( schema.GroupKind{Group: "watcher.openstack.org", Kind: "Watcher"}, r.Name, allErrs) } - return nil, nil + return allWarns, nil } // ValidateCreate validates the WatcherSpec during the webhook invocation. -func (spec *WatcherSpec) ValidateCreate(basePath *field.Path, namespace string) field.ErrorList { +func (spec *WatcherSpec) ValidateCreate(basePath *field.Path, namespace string) ([]string, field.ErrorList) { return spec.WatcherSpecCore.ValidateCreate(basePath, namespace) } +// validateDeprecatedFieldsCreate validates deprecated fields during CREATE operations +func (spec *WatcherSpecCore) validateDeprecatedFieldsCreate(basePath *field.Path) ([]string, field.ErrorList) { + // Get deprecated fields list (without old values for CREATE) + deprecatedFieldsUpdate := spec.getDeprecatedFields(nil) + + // Convert to DeprecatedField list for CREATE validation + deprecatedFields := make([]common_webhook.DeprecatedField, len(deprecatedFieldsUpdate)) + for i, df := range deprecatedFieldsUpdate { + deprecatedFields[i] = common_webhook.DeprecatedField{ + DeprecatedFieldName: df.DeprecatedFieldName, + NewFieldPath: df.NewFieldPath, + DeprecatedValue: df.NewDeprecatedValue, + NewValue: df.NewValue, + } + } + + return common_webhook.ValidateDeprecatedFieldsCreate(deprecatedFields, basePath), nil +} + +// validateDeprecatedFieldsUpdate validates deprecated fields during UPDATE operations +func (spec *WatcherSpecCore) validateDeprecatedFieldsUpdate(old WatcherSpecCore, basePath *field.Path) ([]string, field.ErrorList) { + // Get deprecated fields list with old values + deprecatedFields := spec.getDeprecatedFields(&old) + return common_webhook.ValidateDeprecatedFieldsUpdate(deprecatedFields, basePath) +} + // ValidateCreate validates the WatcherSpecCore during the webhook invocation. It is // expected to be called by the validation webhook in the higher level meta // operator -func (spec *WatcherSpecCore) ValidateCreate(basePath *field.Path, namespace string) field.ErrorList { +func (spec *WatcherSpecCore) ValidateCreate(basePath *field.Path, namespace string) ([]string, field.ErrorList) { var allErrs field.ErrorList + var allWarns admission.Warnings - if *spec.DatabaseInstance == "" || spec.DatabaseInstance == nil { + // Validate deprecated fields using shared helper + warns, errs := spec.validateDeprecatedFieldsCreate(basePath) + allWarns = append(allWarns, warns...) + allErrs = append(allErrs, errs...) + + if spec.DatabaseInstance == nil || *spec.DatabaseInstance == "" { allErrs = append( allErrs, field.Invalid( @@ -103,17 +184,18 @@ func (spec *WatcherSpecCore) ValidateCreate(basePath *field.Path, namespace stri ) } - if *spec.RabbitMqClusterName == "" || spec.RabbitMqClusterName == nil { + // Validate messagingBus.cluster instead of deprecated rabbitMqClusterName + if spec.MessagingBus.Cluster == "" { allErrs = append( allErrs, field.Invalid( - basePath.Child("rabbitMqClusterName"), "", "rabbitMqClusterName field should not be empty"), + basePath.Child("messagingBus").Child("cluster"), "", "messagingBus.cluster field should not be empty"), ) } allErrs = append(allErrs, spec.ValidateWatcherTopology(basePath, namespace)...) - return allErrs + return allWarns, allErrs } // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type @@ -125,30 +207,39 @@ func (r *Watcher) ValidateUpdate(old runtime.Object) (admission.Warnings, error) return nil, apierrors.NewInternalError(fmt.Errorf("unable to convert existing object")) } - allErrs := r.Spec.ValidateUpdate(oldWatcher.Spec, field.NewPath("spec"), r.Namespace) + var allErrs field.ErrorList + var allWarns admission.Warnings + + basePath := field.NewPath("spec") + + if warns, errs := r.Spec.ValidateUpdate(oldWatcher.Spec, basePath, r.Namespace); errs != nil { + allErrs = append(allErrs, errs...) + allWarns = append(allWarns, warns...) + } if len(allErrs) != 0 { - return nil, apierrors.NewInvalid( + return allWarns, apierrors.NewInvalid( schema.GroupKind{Group: "watcher.openstack.org", Kind: "Watcher"}, r.Name, allErrs) } - return nil, nil + return allWarns, nil } // ValidateUpdate validates the WatcherSpec during the webhook update invocation. -func (spec *WatcherSpec) ValidateUpdate(old WatcherSpec, basePath *field.Path, namespace string) field.ErrorList { +func (spec *WatcherSpec) ValidateUpdate(old WatcherSpec, basePath *field.Path, namespace string) ([]string, field.ErrorList) { return spec.WatcherSpecCore.ValidateUpdate(old.WatcherSpecCore, basePath, namespace) } // ValidateUpdate validates the WatcherSpecCore during the webhook invocation. It is // expected to be called by the validation webhook in the higher level meta // operator -func (spec *WatcherSpecCore) ValidateUpdate(old WatcherSpecCore, basePath *field.Path, namespace string) field.ErrorList { +func (spec *WatcherSpecCore) ValidateUpdate(old WatcherSpecCore, basePath *field.Path, namespace string) ([]string, field.ErrorList) { var allErrs field.ErrorList + var allWarns admission.Warnings - if *spec.DatabaseInstance == "" || spec.DatabaseInstance == nil { + if spec.DatabaseInstance == nil || *spec.DatabaseInstance == "" { allErrs = append( allErrs, field.Invalid( @@ -156,17 +247,23 @@ func (spec *WatcherSpecCore) ValidateUpdate(old WatcherSpecCore, basePath *field ) } - if *spec.RabbitMqClusterName == "" || spec.RabbitMqClusterName == nil { + // Validate messagingBus.cluster instead of deprecated rabbitMqClusterName + if spec.MessagingBus.Cluster == "" { allErrs = append( allErrs, field.Invalid( - basePath.Child("rabbitMqClusterName"), "", "rabbitMqClusterName field should not be empty"), + basePath.Child("messagingBus").Child("cluster"), "", "messagingBus.cluster field should not be empty"), ) } + // Validate deprecated fields using shared helper + warns, errs := spec.validateDeprecatedFieldsUpdate(old, basePath) + allWarns = append(allWarns, warns...) + allErrs = append(allErrs, errs...) + allErrs = append(allErrs, spec.ValidateWatcherTopology(basePath, namespace)...) - return allErrs + return allWarns, allErrs } // ValidateDelete implements webhook.Validator so a webhook will be registered for the type diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index 6718cba6..cd9d024f 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" @@ -680,6 +681,12 @@ func (in *WatcherSpec) DeepCopy() *WatcherSpec { func (in *WatcherSpecCore) DeepCopyInto(out *WatcherSpecCore) { *out = *in in.WatcherCommon.DeepCopyInto(&out.WatcherCommon) + out.MessagingBus = in.MessagingBus + if in.NotificationsBus != nil { + in, out := &in.NotificationsBus, &out.NotificationsBus + *out = new(rabbitmqv1beta1.RabbitMqConfig) + **out = **in + } if in.RabbitMqClusterName != nil { in, out := &in.RabbitMqClusterName, &out.RabbitMqClusterName *out = new(string) diff --git a/ci/scenarios/edpm.yml b/ci/scenarios/edpm.yml index 2bc02499..f19b2910 100644 --- a/ci/scenarios/edpm.yml +++ b/ci/scenarios/edpm.yml @@ -63,7 +63,8 @@ cifmw_edpm_prepare_kustomizations: metadata: name: controlplane spec: - notificationsBusInstance: rabbitmq-notifications + notificationsBus: + cluster: rabbitmq-notifications target: kind: OpenStackControlPlane - patch: |- diff --git a/config/crd/bases/watcher.openstack.org_watchers.yaml b/config/crd/bases/watcher.openstack.org_watchers.yaml index 00c8e78c..16286b84 100644 --- a/config/crd/bases/watcher.openstack.org_watchers.yaml +++ b/config/crd/bases/watcher.openstack.org_watchers.yaml @@ -617,6 +617,22 @@ spec: description: MemcachedInstance is the name of the Memcached CR that all watcher service will use. 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 @@ -624,6 +640,23 @@ spec: NodeSelector to target subset of worker nodes running this component. Setting here overrides any global NodeSelector settings within the Watcher CR. 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: |- NotificationsBusInstance is the name of the RabbitMqCluster CR to select @@ -632,6 +665,7 @@ spec: If undefined, the value will be inherited from OpenStackControlPlane. An empty value "" leaves the notification drivers unconfigured and emitting no notifications at all. Avoid colocating it with RabbitMqClusterName or other message bus instances used for RPC. + Deprecated: Use NotificationsBus.Cluster instead type: string passwordSelectors: default: @@ -655,10 +689,10 @@ spec: description: Secret containing prometheus connection parameters type: string rabbitMqClusterName: - default: rabbitmq description: |- RabbitMQ instance name Needed to request a transportURL that is created and used in Watcher + Deprecated: Use MessagingBus.Cluster instead type: string secret: default: osp-secret @@ -694,7 +728,6 @@ spec: - databaseInstance - decisionengineContainerImageURL - decisionengineServiceTemplate - - rabbitMqClusterName type: object status: description: WatcherStatus defines the observed state of Watcher diff --git a/go.mod b/go.mod index 19ef0bd5..104c807b 100644 --- a/go.mod +++ b/go.mod @@ -8,11 +8,11 @@ require ( github.com/onsi/ginkgo/v2 v2.27.3 github.com/onsi/gomega v1.39.0 github.com/openshift/api v3.9.0+incompatible - github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260115124008-0121df869109 - github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260120112029-cd452f0497ba - github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35 - github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251230215914-6ba873b49a35 - github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260105160121-f7a8ef85ce8d + github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09 + github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260126094240-81b572e8f653 + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb + github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260126081203-efc2df9207eb + github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260124124804-c82210f7a636 github.com/openstack-k8s-operators/watcher-operator/api v0.0.0-00010101000000-000000000000 go.uber.org/zap v1.27.1 gopkg.in/yaml.v3 v3.0.1 diff --git a/go.sum b/go.sum index d7c52b6e..6e6f41d8 100644 --- a/go.sum +++ b/go.sum @@ -118,20 +118,20 @@ github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e h1:E1OdwSpqWuDPCedyUt0GEdoAE+r5TXy7YS21yNEo+2U= github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e/go.mod h1:Shkl4HanLwDiiBzakv+con/aMGnVE2MAGvoKp5oyYUo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260115124008-0121df869109 h1:S+A67nntHZrL1lIL3qr91CpJj+A67M/G4t1cTKzeGdo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260115124008-0121df869109/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= -github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260120112029-cd452f0497ba h1:4VaDkZFawGCkzwvfijnFLz0Gduxh17buj9fIwk0WULo= -github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260120112029-cd452f0497ba/go.mod h1:xqvebn9DqLavxp2z8Rz/7i1S6M9MJhxmZVHC+S1uHX0= -github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35 h1:pF3mJ3nwq6r4qwom+rEWZNquZpcQW/iftHlJ1KPIDsk= -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/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09 h1:vhAGLKZitJIffj7ONiPpKmOX7Tmt/LGJpaY0Z2LeyfQ= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20260126091827-7758173fbb09/go.mod h1:ZXwFlspJCdZEUjMbmaf61t5AMB4u2vMyAMMoe/vJroE= +github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260126094240-81b572e8f653 h1:WZgTxkUrXMquCJbT9ckkHwg0qwEQXR8vZb3NBz6r314= +github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20260126094240-81b572e8f653/go.mod h1:PDOQwlNVjHCygfqUw3k8OY1VcV/Fo6r1Fsjrdm8IhGk= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb h1:S7tnYO/E1f1KQfcp7N5bam8+ax/ExDTOhZ1WqG4Bfu0= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20260126081203-efc2df9207eb/go.mod h1:ndqfy1KbVorHH6+zlUFPIrCRhMSxO3ImYJUGaooE0x0= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251230215914-6ba873b49a35 h1:IdcI8DFvW8rXtchONSzbDmhhRp1YyO2YaBJDBXr44Gk= github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:zOX7Y05keiSppIvLabuyh42QHBMhCcoskAtxFRbwXKo= 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/lib-common/modules/test v0.6.1-0.20251230215914-6ba873b49a35 h1:8rQc4Fsfe6yqRU5Xjt9lWXqUqfBjRubr0utnUpUBKTE= -github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:QWzyC+tTBB2OGuYyIiLLo1oA0+I/0NUMXD+dj4Quv4M= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260105160121-f7a8ef85ce8d h1:cbQpEHW404M+QBrevqh+MyrtPRUFlHTLmSAHflEth6U= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260105160121-f7a8ef85ce8d/go.mod h1:X6W8pIULiWUc6smaTqiNocjxoXaRLgXediwpI/dxD9s= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260126081203-efc2df9207eb h1:Fh9yjyogiR9P4oV3a2pSlSUyYzfbWbvlU6RFIjZoxsg= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20260126081203-efc2df9207eb/go.mod h1:sqKTKvYhSzu4Opnjx/J+zzetXKRqYrhxsfvrST/NjoU= +github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260124124804-c82210f7a636 h1:Mtjy0cc2pBdyP44/5K6TB/VxIDvtjU2EHer17JKUaLg= +github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20260124124804-c82210f7a636/go.mod h1:X6W8pIULiWUc6smaTqiNocjxoXaRLgXediwpI/dxD9s= 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= diff --git a/internal/controller/watcher_controller.go b/internal/controller/watcher_controller.go index c0c7755b..76b19c17 100644 --- a/internal/controller/watcher_controller.go +++ b/internal/controller/watcher_controller.go @@ -19,6 +19,7 @@ package controller import ( "context" "fmt" + "strings" "time" "github.com/go-logr/logr" @@ -204,7 +205,7 @@ func (r *WatcherReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re // not-ready condition is managed here instead of in ensureMQ to distinguish between Error (when receiving) // an error, or Running when transportURL is empty. // - transportURL, op, err := r.ensureMQ(ctx, instance, helper, instance.Name+"-watcher-transport", *instance.Spec.RabbitMqClusterName, serviceLabels) + transportURL, op, err := r.ensureMQ(ctx, instance, helper, instance.Name+"-watcher-transport", instance.Spec.MessagingBus, serviceLabels) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( watcherv1beta1.WatcherRabbitMQTransportURLReadyCondition, @@ -233,14 +234,22 @@ func (r *WatcherReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re // create Notification RabbitMQ transportURL CR and get the actual URL from the associated secret that is created notificationURLSecret := &corev1.Secret{} - if instance.Spec.NotificationsBusInstance != nil && *instance.Spec.NotificationsBusInstance != "" { + // Determine if notifications are enabled by checking NotificationsBus.Cluster + if instance.Spec.NotificationsBus != nil && instance.Spec.NotificationsBus.Cluster != "" { instance.Status.Conditions.Set(condition.FalseCondition( watcherv1beta1.WatcherNotificationTransportURLReadyCondition, condition.RequestedReason, condition.SeverityInfo, watcherv1beta1.WatcherNotificationTransportURLReadyRunningMessage, )) - notificationURL, op, err := r.ensureMQ(ctx, instance, helper, instance.Name+"-watcher-notification", *instance.Spec.NotificationsBusInstance, serviceLabels) + + // Use NotificationsBus config (never fall back to MessagingBus to ensure separation) + notificationsRabbitMqConfig := *instance.Spec.NotificationsBus + + // Append cluster name to the TransportURL name to make it unique when using different clusters + notificationTransportURLName := fmt.Sprintf("%s-watcher-notification-%s", instance.Name, notificationsRabbitMqConfig.Cluster) + + notificationURL, op, err := r.ensureMQ(ctx, instance, helper, notificationTransportURLName, notificationsRabbitMqConfig, serviceLabels) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( watcherv1beta1.WatcherNotificationTransportURLReadyCondition, @@ -263,6 +272,27 @@ func (r *WatcherReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re } instance.Status.Conditions.MarkTrue(watcherv1beta1.WatcherNotificationTransportURLReadyCondition, watcherv1beta1.WatcherNotificationTransportURLReadyMessage) + // Cleanup orphaned notification TransportURLs that don't match the current name + // This happens AFTER successful creation to avoid deleting resources if creation fails + transportURLList := &rabbitmqv1.TransportURLList{} + listOpts := []client.ListOption{ + client.InNamespace(instance.Namespace), + } + if err := r.Client.List(ctx, transportURLList, listOpts...); err != nil { + return ctrl.Result{}, err + } + + notificationTransportPrefix := fmt.Sprintf("%s-watcher-notification-", instance.Name) + for _, url := range transportURLList.Items { + // Delete notification TransportURLs that don't match the current name + if strings.HasPrefix(url.Name, notificationTransportPrefix) && url.Name != notificationTransportURLName { + err = r.ensureMQDeleted(ctx, instance, url.Name) + if err != nil { + return ctrl.Result{}, err + } + } + } + // NotificationURL Secret hashNotificationURL, _, notificationSecret, err := ensureSecret( ctx, @@ -280,6 +310,27 @@ func (r *WatcherReconciler) Reconcile(ctx context.Context, req ctrl.Request) (re } notificationURLSecret = ¬ificationSecret _ = op + } else { + instance.Status.Conditions.Remove(watcherv1beta1.WatcherNotificationTransportURLReadyCondition) + + // Ensure to delete all notification TransportURLs when notifications are disabled + transportURLList := &rabbitmqv1.TransportURLList{} + listOpts := []client.ListOption{ + client.InNamespace(instance.Namespace), + } + if err := r.Client.List(ctx, transportURLList, listOpts...); err != nil { + return ctrl.Result{}, err + } + + notificationTransportPrefix := fmt.Sprintf("%s-watcher-notification-", instance.Name) + for _, url := range transportURLList.Items { + if strings.HasPrefix(url.Name, notificationTransportPrefix) { + err = r.ensureMQDeleted(ctx, instance, url.Name) + if err != nil { + return ctrl.Result{}, err + } + } + } } // end of Notification TransportURL creation @@ -701,7 +752,7 @@ func (r *WatcherReconciler) ensureMQ( instance *watcherv1beta1.Watcher, h *helper.Helper, transportURLName string, - messageBusInstance string, + rabbitMqConfig rabbitmqv1.RabbitMqConfig, serviceLabels map[string]string, ) (*rabbitmqv1.TransportURL, controllerutil.OperationResult, error) { Log := r.GetLogger(ctx) @@ -716,7 +767,13 @@ func (r *WatcherReconciler) ensureMQ( } op, err := controllerutil.CreateOrUpdate(ctx, r.Client, transportURL, func() error { - transportURL.Spec.RabbitmqClusterName = messageBusInstance + transportURL.Spec.RabbitmqClusterName = rabbitMqConfig.Cluster + // Always set Username and Vhost to allow clearing/resetting them + // The infra-operator TransportURL controller handles empty values: + // - Empty Username: uses default cluster admin credentials + // - Empty Vhost: defaults to "/" vhost + transportURL.Spec.Username = rabbitMqConfig.User + transportURL.Spec.Vhost = rabbitMqConfig.Vhost err := controllerutil.SetControllerReference(instance, transportURL, r.Scheme) return err @@ -1390,3 +1447,27 @@ func (r *WatcherReconciler) findObjectsForSrc(ctx context.Context, src client.Ob return requests } + +func (r *WatcherReconciler) ensureMQDeleted( + ctx context.Context, + instance *watcherv1beta1.Watcher, + transportURLName string, +) error { + Log := r.GetLogger(ctx) + transportURL := &rabbitmqv1.TransportURL{ + ObjectMeta: metav1.ObjectMeta{ + Name: transportURLName, + Namespace: instance.Namespace, + }, + } + + err := r.Client.Delete(ctx, transportURL) + if err != nil && !k8s_errors.IsNotFound(err) { + Log.Info(fmt.Sprintf("Could not delete TransportURL %s err: %s", transportURLName, err)) + return err + } + + Log.Info("Deleted TransportURL", "name", transportURLName) + + return nil +} diff --git a/test/functional/watcher_controller_test.go b/test/functional/watcher_controller_test.go index 40a6b089..95365828 100644 --- a/test/functional/watcher_controller_test.go +++ b/test/functional/watcher_controller_test.go @@ -52,7 +52,7 @@ var _ = Describe("Watcher controller with minimal spec values", func() { Expect(*(Watcher.Spec.DatabaseAccount)).Should(Equal("watcher")) Expect(*(Watcher.Spec.Secret)).Should(Equal("osp-secret")) Expect(*(Watcher.Spec.PasswordSelectors.Service)).Should(Equal("WatcherPassword")) - Expect(*(Watcher.Spec.RabbitMqClusterName)).Should(Equal("rabbitmq")) + Expect(Watcher.Spec.MessagingBus.Cluster).Should(Equal("rabbitmq")) Expect(*(Watcher.Spec.ServiceUser)).Should(Equal("watcher")) Expect(Watcher.Spec.PreserveJobs).Should(BeFalse()) Expect(Watcher.Spec.APIServiceTemplate.TLS.CaBundleSecretName).Should(Equal("")) @@ -101,7 +101,7 @@ var _ = Describe("Watcher controller", func() { Expect(*(Watcher.Spec.DatabaseAccount)).Should(Equal("watcher")) Expect(*(Watcher.Spec.ServiceUser)).Should(Equal("watcher")) Expect(*(Watcher.Spec.Secret)).Should(Equal("test-osp-secret")) - Expect(*(Watcher.Spec.RabbitMqClusterName)).Should(Equal("rabbitmq")) + Expect(Watcher.Spec.MessagingBus.Cluster).Should(Equal("rabbitmq")) Expect(Watcher.Spec.PreserveJobs).Should(BeFalse()) Expect(*(Watcher.Spec.APITimeout)).To(Equal(60)) @@ -711,6 +711,9 @@ var _ = Describe("Watcher controller", func() { It("should raise an error for empty databaseInstance", func() { spec := GetDefaultWatcherAPISpec() spec["databaseInstance"] = "" + spec["messagingBus"] = map[string]any{ + "cluster": "rabbitmq", + } raw := map[string]any{ "apiVersion": "watcher.openstack.org/v1beta1", @@ -742,6 +745,9 @@ var _ = Describe("Watcher controller", func() { spec := GetDefaultWatcherAPISpec() spec["topologyRef"] = map[string]any{"name": "foo", "namespace": "bar"} + spec["messagingBus"] = map[string]any{ + "cluster": "rabbitmq", + } raw := map[string]any{ "apiVersion": "watcher.openstack.org/v1beta1", @@ -768,32 +774,19 @@ var _ = Describe("Watcher controller", func() { }) }) - When("Watcher is created with empty RabbitMqClusterName", func() { - It("should raise an error for empty RabbitMqClusterName", func() { - spec := GetDefaultWatcherAPISpec() - spec["rabbitMqClusterName"] = "" - - raw := map[string]any{ - "apiVersion": "watcher.openstack.org/v1beta1", - "kind": "watcher", - "metadata": map[string]any{ - "name": watcherName.Name, - "namespace": watcherName.Namespace, - }, - "spec": spec, + When("Watcher is created with empty messagingBus.cluster", func() { + It("should default messagingBus.cluster to rabbitmq", func() { + spec := GetDefaultWatcherSpec() + spec["messagingBus"] = map[string]any{ + "cluster": "", } + DeferCleanup(th.DeleteInstance, CreateWatcher(watcherTest.Instance, spec)) - unstructuredObj := &unstructured.Unstructured{Object: raw} - _, err := controllerutil.CreateOrPatch( - th.Ctx, th.K8sClient, unstructuredObj, func() error { return nil }) - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To( - ContainSubstring( - "admission webhook \"vwatcher-v1beta1.kb.io\" denied the request: " + - "Watcher.watcher.openstack.org \"watcher\" is invalid: " + - "spec.rabbitMqClusterName: Invalid value: \"\": " + - "rabbitMqClusterName field should not be empty"), - ) + // Webhook should default empty cluster to "rabbitmq" + Eventually(func(g Gomega) { + watcher := GetWatcher(watcherTest.Instance) + g.Expect(watcher.Spec.MessagingBus.Cluster).To(Equal("rabbitmq")) + }, timeout, interval).Should(Succeed()) }) }) @@ -846,7 +839,7 @@ var _ = Describe("Watcher controller", func() { Expect(*(Watcher.Spec.ServiceUser)).Should(Equal("fakeuser")) Expect(*(Watcher.Spec.Secret)).Should(Equal("test-osp-secret")) Expect(Watcher.Spec.PreserveJobs).Should(BeTrue()) - Expect(*(Watcher.Spec.RabbitMqClusterName)).Should(Equal("rabbitmq")) + Expect(Watcher.Spec.MessagingBus.Cluster).Should(Equal("rabbitmq")) Expect(Watcher.Spec.APIServiceTemplate.TLS.CaBundleSecretName).Should(Equal("combined-ca-bundle")) Expect(Watcher.Spec.CustomServiceConfig).Should(Equal("# Global config")) Expect(*(Watcher.Spec.PrometheusSecret)).Should(Equal("custom-prometheus-config")) @@ -1628,7 +1621,9 @@ var _ = Describe("Watcher controller", func() { When("Watcher with notification bus instance is created", func() { BeforeEach(func() { spec := GetDefaultWatcherSpec() - spec["notificationsBusInstance"] = ptr.To("rabbitmq-notification") + spec["notificationsBus"] = map[string]any{ + "cluster": "rabbitmq-notification", + } DeferCleanup(th.DeleteInstance, CreateWatcher(watcherTest.Instance, spec)) DeferCleanup(k8sClient.Delete, ctx, CreateWatcherMessageBusSecret(watcherTest.Instance.Namespace, "rabbitmq-secret")) memcachedSpec := memcachedv1.MemcachedSpec{ @@ -1671,8 +1666,8 @@ var _ = Describe("Watcher controller", func() { It("should have the Spec fields with the expected values", func() { Watcher := GetWatcher(watcherTest.Instance) - Expect(*(Watcher.Spec.RabbitMqClusterName)).Should(Equal("rabbitmq")) - Expect(*(Watcher.Spec.NotificationsBusInstance)).Should(Equal("rabbitmq-notification")) + Expect(Watcher.Spec.MessagingBus.Cluster).Should(Equal("rabbitmq")) + Expect(Watcher.Spec.NotificationsBus.Cluster).Should(Equal("rabbitmq-notification")) }) It("should have the condition WatcherNotificationTransportURLReadyCondition set to false", func() { @@ -1697,7 +1692,13 @@ var _ = Describe("Watcher controller", func() { It("should have WatcherNotificationTransportURLReadyCondition set to true when creating the notification transportURL", func() { DeferCleanup(k8sClient.Delete, ctx, CreateWatcherMessageBusSecret(watcherTest.Instance.Namespace, "rabbitmq-notification-secret")) - infra.SimulateTransportURLReady(watcherTest.WatcherNotificationTransportURL) + // Get the notification TransportURL name based on the cluster name + Watcher := GetWatcher(watcherTest.Instance) + notificationTransportURLName := types.NamespacedName{ + Namespace: watcherTest.Instance.Namespace, + Name: fmt.Sprintf("%s-watcher-notification-%s", watcherTest.Instance.Name, Watcher.Spec.NotificationsBus.Cluster), + } + infra.SimulateTransportURLReady(notificationTransportURLName) // simulate that it becomes ready i.e. the keystone-operator // did its job and registered the watcher service @@ -2001,4 +2002,106 @@ var _ = Describe("Watcher controller", func() { }) }) + When("Watcher with custom messagingBus and notificationsBus is created", func() { + BeforeEach(func() { + spec := GetDefaultWatcherSpec() + spec["messagingBus"] = map[string]any{ + "cluster": "custom-rabbitmq", + "user": "custom-rpc-user", + "vhost": "custom-rpc-vhost", + } + spec["notificationsBus"] = map[string]any{ + "cluster": "custom-notifications-rabbitmq", + "user": "custom-notifications-user", + "vhost": "custom-notifications-vhost", + } + // Create secrets for custom cluster names BEFORE creating the Watcher + DeferCleanup(k8sClient.Delete, ctx, CreateWatcherMessageBusSecret(watcherTest.Instance.Namespace, "custom-rabbitmq-secret")) + DeferCleanup(k8sClient.Delete, ctx, CreateWatcherMessageBusSecret(watcherTest.Instance.Namespace, "custom-notifications-rabbitmq-secret")) + DeferCleanup(th.DeleteInstance, CreateWatcher(watcherTest.Instance, spec)) + + memcachedSpec := memcachedv1.MemcachedSpec{ + MemcachedSpecCore: memcachedv1.MemcachedSpecCore{ + Replicas: ptr.To(int32(1)), + }, + } + DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(watcherTest.Watcher.Namespace, MemcachedInstance, memcachedSpec)) + infra.SimulateMemcachedReady(watcherTest.MemcachedNamespace) + + DeferCleanup( + mariadb.DeleteDBService, + mariadb.CreateDBService( + watcherTest.Instance.Namespace, + *GetWatcher(watcherTest.Instance).Spec.DatabaseInstance, + corev1.ServiceSpec{ + Ports: []corev1.ServicePort{{Port: 3306}}, + }, + ), + ) + + DeferCleanup( + k8sClient.Delete, ctx, th.CreateSecret( + types.NamespacedName{Namespace: watcherTest.Instance.Namespace, Name: SecretName}, + map[string][]byte{ + "WatcherPassword": []byte("password"), + }, + )) + + DeferCleanup(keystone.DeleteKeystoneAPI, keystone.CreateKeystoneAPI(watcherTest.WatcherAPI.Namespace)) + + DeferCleanup( + k8sClient.Delete, ctx, th.CreateSecret( + types.NamespacedName{Namespace: watcherTest.Instance.Namespace, Name: "metric-storage-prometheus-endpoint"}, + map[string][]byte{ + "host": []byte("prometheus.example.com"), + "port": []byte("9090"), + }, + )) + + mariadb.SimulateMariaDBAccountCompleted(watcherTest.WatcherDatabaseAccount) + mariadb.SimulateMariaDBDatabaseCompleted(watcherTest.WatcherDatabaseName) + infra.SimulateTransportURLReady(watcherTest.WatcherTransportURL) + }) + + It("should have the MessagingBus spec fields with custom values", func() { + Watcher := GetWatcher(watcherTest.Instance) + Expect(Watcher.Spec.MessagingBus.Cluster).Should(Equal("custom-rabbitmq")) + Expect(Watcher.Spec.MessagingBus.User).Should(Equal("custom-rpc-user")) + Expect(Watcher.Spec.MessagingBus.Vhost).Should(Equal("custom-rpc-vhost")) + }) + + It("should have the NotificationsBus spec fields with custom values", func() { + Watcher := GetWatcher(watcherTest.Instance) + Expect(Watcher.Spec.NotificationsBus.Cluster).Should(Equal("custom-notifications-rabbitmq")) + Expect(Watcher.Spec.NotificationsBus.User).Should(Equal("custom-notifications-user")) + Expect(Watcher.Spec.NotificationsBus.Vhost).Should(Equal("custom-notifications-vhost")) + }) + + It("should create separate TransportURLs for RPC and notifications", func() { + // Secrets already created in BeforeEach + // Get the notification TransportURL name based on the cluster name + Watcher := GetWatcher(watcherTest.Instance) + notificationTransportURLName := types.NamespacedName{ + Namespace: watcherTest.Instance.Namespace, + Name: fmt.Sprintf("%s-watcher-notification-%s", watcherTest.Instance.Name, Watcher.Spec.NotificationsBus.Cluster), + } + infra.SimulateTransportURLReady(notificationTransportURLName) + + // Verify that both transport URLs are created + th.ExpectCondition( + watcherTest.Instance, + ConditionGetterFunc(WatcherConditionGetter), + watcherv1beta1.WatcherRabbitMQTransportURLReadyCondition, + corev1.ConditionTrue, + ) + + th.ExpectCondition( + watcherTest.Instance, + ConditionGetterFunc(WatcherConditionGetter), + watcherv1beta1.WatcherNotificationTransportURLReadyCondition, + corev1.ConditionTrue, + ) + }) + }) + }) diff --git a/test/functional/watcher_test_data.go b/test/functional/watcher_test_data.go index e6701af4..0ab6a4ad 100644 --- a/test/functional/watcher_test_data.go +++ b/test/functional/watcher_test_data.go @@ -63,7 +63,6 @@ type WatcherTestData struct { WatcherPublicCertSecret types.NamespacedName WatcherInternalCertSecret types.NamespacedName WatcherApplierSecret types.NamespacedName - WatcherNotificationTransportURL types.NamespacedName } // GetWatcherTestData is a function that initialize the WatcherTestData @@ -192,9 +191,5 @@ func GetWatcherTestData(watcherName types.NamespacedName) WatcherTestData { Namespace: watcherName.Namespace, Name: "watcher-applier-config-data", }, - WatcherNotificationTransportURL: types.NamespacedName{ - Namespace: watcherName.Namespace, - Name: fmt.Sprintf("%s-watcher-notification", watcherName.Name), - }, } } diff --git a/test/functional/watcher_webhook_test.go b/test/functional/watcher_webhook_test.go index ed9b7c69..a5ad7954 100644 --- a/test/functional/watcher_webhook_test.go +++ b/test/functional/watcher_webhook_test.go @@ -20,9 +20,10 @@ import ( . "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports . "github.com/onsi/gomega" //revive:disable:dot-imports - "k8s.io/utils/ptr" - + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" watcherv1 "github.com/openstack-k8s-operators/watcher-operator/api/v1beta1" + "k8s.io/apimachinery/pkg/util/validation/field" + "k8s.io/utils/ptr" ) var _ = Describe("SetDefaultRouteAnnotations", func() { @@ -141,3 +142,437 @@ var _ = Describe("SetDefaultRouteAnnotations", func() { }) }) + +var _ = Describe("Watcher Webhook Messaging and Notifications", func() { + + Describe("MessagingBus defaulting", func() { + var spec *watcherv1.WatcherSpecCore + + BeforeEach(func() { + spec = &watcherv1.WatcherSpecCore{} + }) + + It("should default messagingBus.cluster to rabbitmq when empty", func() { + spec.Default() + + Expect(spec.MessagingBus.Cluster).To(Equal("rabbitmq")) + // Note: User and Vhost don't have defaults and remain empty unless explicitly set + Expect(spec.MessagingBus.User).To(Equal("")) + Expect(spec.MessagingBus.Vhost).To(Equal("")) + }) + + It("should not override messagingBus.cluster if already set", func() { + spec.MessagingBus.Cluster = "existing-cluster" + spec.Default() + + Expect(spec.MessagingBus.Cluster).To(Equal("existing-cluster")) + }) + }) + + Describe("Direct messagingBus field usage", func() { + var spec *watcherv1.WatcherSpecCore + + It("should preserve messagingBus fields when set directly", func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + MessagingBus: rabbitmqv1.RabbitMqConfig{ + Cluster: "direct-cluster", + User: "custom-user", + Vhost: "/custom-vhost", + }, + } + spec.Default() + + Expect(spec.MessagingBus.Cluster).To(Equal("direct-cluster")) + Expect(spec.MessagingBus.User).To(Equal("custom-user")) + Expect(spec.MessagingBus.Vhost).To(Equal("/custom-vhost")) + }) + + It("should use messagingBus.cluster when both old and new fields are set", func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("old-rabbitmq"), + MessagingBus: rabbitmqv1.RabbitMqConfig{ + Cluster: "new-cluster", + }, + } + spec.Default() + + // New field should take precedence + Expect(spec.MessagingBus.Cluster).To(Equal("new-cluster")) + }) + }) + + Describe("NotificationsBus defaulting", func() { + var spec *watcherv1.WatcherSpecCore + + BeforeEach(func() { + spec = &watcherv1.WatcherSpecCore{} + }) + + It("should not default notificationsBus.cluster when notificationsBus is present but cluster is empty", func() { + spec.NotificationsBus = &rabbitmqv1.RabbitMqConfig{} + spec.Default() + + Expect(spec.NotificationsBus).NotTo(BeNil()) + Expect(spec.NotificationsBus.Cluster).To(Equal(""), "notificationsBus.cluster should not be defaulted - it must be explicitly set") + }) + + It("should not initialize notificationsBus when it is nil", func() { + spec.Default() + + Expect(spec.NotificationsBus).To(BeNil()) + }) + + It("should preserve notificationsBus fields when already set", func() { + spec.NotificationsBus = &rabbitmqv1.RabbitMqConfig{ + Cluster: "custom-cluster", + User: "custom-user", + Vhost: "custom-vhost", + } + spec.Default() + + Expect(spec.NotificationsBus.Cluster).To(Equal("custom-cluster")) + Expect(spec.NotificationsBus.User).To(Equal("custom-user")) + Expect(spec.NotificationsBus.Vhost).To(Equal("custom-vhost")) + }) + + It("should preserve existing notificationsBus.cluster if already set", func() { + spec.NotificationsBus = &rabbitmqv1.RabbitMqConfig{ + Cluster: "existing-notifications-cluster", + } + spec.Default() + + Expect(spec.NotificationsBus.Cluster).To(Equal("existing-notifications-cluster")) + }) + }) + + Describe("NotificationsBus independence from messagingBus", func() { + var spec *watcherv1.WatcherSpecCore + + It("should keep notificationsBus and messagingBus separate", func() { + spec = &watcherv1.WatcherSpecCore{ + MessagingBus: rabbitmqv1.RabbitMqConfig{ + Cluster: "rabbitmq-rpc", + User: "rpc-user", + Vhost: "/rpc-vhost", + }, + NotificationsBus: &rabbitmqv1.RabbitMqConfig{ + Cluster: "rabbitmq-notifications", + User: "notif-user", + Vhost: "/notif-vhost", + }, + } + spec.Default() + + // Verify MessagingBus fields are preserved + Expect(spec.MessagingBus.Cluster).To(Equal("rabbitmq-rpc")) + Expect(spec.MessagingBus.User).To(Equal("rpc-user")) + Expect(spec.MessagingBus.Vhost).To(Equal("/rpc-vhost")) + + // Verify NotificationsBus fields are preserved and independent + Expect(spec.NotificationsBus.Cluster).To(Equal("rabbitmq-notifications")) + Expect(spec.NotificationsBus.User).To(Equal("notif-user")) + Expect(spec.NotificationsBus.Vhost).To(Equal("/notif-vhost")) + }) + }) + + Describe("Direct notificationsBus field usage", func() { + var spec *watcherv1.WatcherSpecCore + + It("should preserve notificationsBus fields when set directly without NotificationsBusInstance", func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + NotificationsBus: &rabbitmqv1.RabbitMqConfig{ + Cluster: "direct-notifications-cluster", + User: "custom-user", + Vhost: "/custom-vhost", + }, + } + spec.Default() + + Expect(spec.NotificationsBus.Cluster).To(Equal("direct-notifications-cluster")) + Expect(spec.NotificationsBus.User).To(Equal("custom-user")) + Expect(spec.NotificationsBus.Vhost).To(Equal("/custom-vhost")) + }) + + It("should use notificationsBus.cluster when both old and new fields are set", func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + NotificationsBusInstance: ptr.To("old-notifications"), + NotificationsBus: &rabbitmqv1.RabbitMqConfig{ + Cluster: "new-notifications-cluster", + }, + } + spec.Default() + + // New field should take precedence (already set, so defaulting shouldn't override) + Expect(spec.NotificationsBus.Cluster).To(Equal("new-notifications-cluster")) + }) + }) + + Describe("Complex scenarios with multiple fields", func() { + var spec *watcherv1.WatcherSpecCore + + It("should handle messagingBus with only partial fields set", func() { + spec = &watcherv1.WatcherSpecCore{ + MessagingBus: rabbitmqv1.RabbitMqConfig{ + User: "messaging-user", + Vhost: "/messaging-vhost", + }, + } + spec.Default() + + // messagingBus.cluster should be defaulted + Expect(spec.MessagingBus.Cluster).To(Equal("rabbitmq")) + Expect(spec.MessagingBus.User).To(Equal("messaging-user")) + Expect(spec.MessagingBus.Vhost).To(Equal("/messaging-vhost")) + }) + + It("should preserve all fields when both buses are fully specified", func() { + spec = &watcherv1.WatcherSpecCore{ + MessagingBus: rabbitmqv1.RabbitMqConfig{ + Cluster: "custom-rabbitmq", + User: "rpc-user", + Vhost: "/rpc-vhost", + }, + NotificationsBus: &rabbitmqv1.RabbitMqConfig{ + Cluster: "custom-notifications", + User: "notif-user", + Vhost: "/notif-vhost", + }, + } + spec.Default() + + Expect(spec.MessagingBus.Cluster).To(Equal("custom-rabbitmq")) + Expect(spec.MessagingBus.User).To(Equal("rpc-user")) + Expect(spec.MessagingBus.Vhost).To(Equal("/rpc-vhost")) + + Expect(spec.NotificationsBus.Cluster).To(Equal("custom-notifications")) + Expect(spec.NotificationsBus.User).To(Equal("notif-user")) + Expect(spec.NotificationsBus.Vhost).To(Equal("/notif-vhost")) + }) + }) + +}) + +var _ = Describe("Watcher Webhook Update Validation", func() { + + Describe("Validation of deprecated field changes", func() { + var ( + oldSpec *watcherv1.WatcherSpecCore + newSpec *watcherv1.WatcherSpecCore + basePath *field.Path + ) + + BeforeEach(func() { + basePath = field.NewPath("spec") + oldSpec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + DatabaseInstance: ptr.To("openstack"), + } + // Call Default() to populate messagingBus from rabbitMqClusterName + oldSpec.Default() + + newSpec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + DatabaseInstance: ptr.To("openstack"), + } + // Call Default() to populate messagingBus from rabbitMqClusterName + newSpec.Default() + }) + + Describe("RabbitMqClusterName field changes", func() { + It("should reject changes to RabbitMqClusterName", func() { + newSpec.RabbitMqClusterName = ptr.To("new-rabbitmq") + + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Expect 2 errors: conflict + forbidden change + Expect(errs).To(HaveLen(2)) + + // Check for both expected errors + foundForbidden := false + foundConflict := false + for _, err := range errs { + if err.Field == "spec.rabbitMqClusterName" && err.Type == field.ErrorTypeForbidden { + foundForbidden = true + Expect(err.Detail).To(ContainSubstring("is deprecated, use")) + Expect(err.Detail).To(ContainSubstring("messagingBus.cluster")) + } + // Conflict error is also on rabbitMqClusterName field (not messagingBus.cluster) + if err.Field == "spec.rabbitMqClusterName" && err.Type == field.ErrorTypeInvalid { + foundConflict = true + Expect(err.Detail).To(ContainSubstring("cannot set both deprecated field")) + Expect(err.Detail).To(ContainSubstring("messagingBus.cluster")) + } + } + Expect(foundForbidden).To(BeTrue(), "Expected forbidden error for rabbitMqClusterName") + Expect(foundConflict).To(BeTrue(), "Expected conflict error for rabbitMqClusterName") + }) + + It("should allow update when RabbitMqClusterName remains unchanged", func() { + // Both specs have the same RabbitMqClusterName + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Should have no errors related to RabbitMqClusterName + for _, err := range errs { + Expect(err.Field).NotTo(Equal("spec.rabbitMqClusterName")) + } + }) + + It("should have validation error when messagingBus.cluster is empty", func() { + // Set RabbitMqClusterName to nil and messagingBus.cluster to empty + oldSpec.RabbitMqClusterName = nil + oldSpec.MessagingBus.Cluster = "" + newSpec.RabbitMqClusterName = nil + newSpec.MessagingBus.Cluster = "" + + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Should have validation error for empty messagingBus.cluster + found := false + for _, err := range errs { + if err.Field == "spec.messagingBus.cluster" { + found = true + Expect(err.Type).To(Equal(field.ErrorTypeInvalid)) + } + } + Expect(found).To(BeTrue(), "Expected validation error for empty messagingBus.cluster") + }) + }) + + Describe("NotificationsBusInstance field changes", func() { + BeforeEach(func() { + oldSpec.NotificationsBusInstance = ptr.To("rabbitmq-notifications") + oldSpec.Default() + + newSpec.NotificationsBusInstance = ptr.To("rabbitmq-notifications") + newSpec.Default() + }) + + It("should reject changes to NotificationsBusInstance", func() { + newSpec.NotificationsBusInstance = ptr.To("new-rabbitmq-notifications") + + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Expect 1 error: forbidden change to deprecated field + Expect(errs).To(HaveLen(1)) + + // Check for forbidden error + foundForbidden := false + for _, err := range errs { + if err.Field == "spec.notificationsBusInstance" && err.Type == field.ErrorTypeForbidden { + foundForbidden = true + Expect(err.Detail).To(ContainSubstring("is deprecated, use")) + Expect(err.Detail).To(ContainSubstring("notificationsBus.cluster")) + } + } + Expect(foundForbidden).To(BeTrue(), "Expected forbidden error for notificationsBusInstance") + }) + + It("should allow update when NotificationsBusInstance remains unchanged", func() { + // Both specs have the same NotificationsBusInstance + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Should have no errors related to NotificationsBusInstance + for _, err := range errs { + Expect(err.Field).NotTo(Equal("spec.notificationsBusInstance")) + } + }) + + It("should allow update when NotificationsBusInstance is nil in both specs", func() { + oldSpec.NotificationsBusInstance = nil + newSpec.NotificationsBusInstance = nil + + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Should have no errors related to NotificationsBusInstance + for _, err := range errs { + Expect(err.Field).NotTo(Equal("spec.notificationsBusInstance")) + } + }) + }) + + Describe("Multiple deprecated field changes", func() { + It("should reject changes to both deprecated fields and return multiple errors", func() { + oldSpec.NotificationsBusInstance = ptr.To("rabbitmq-notifications") + newSpec.RabbitMqClusterName = ptr.To("new-rabbitmq") + newSpec.NotificationsBusInstance = ptr.To("new-rabbitmq-notifications") + + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Expect 3 errors: 2 for rabbitMqClusterName (conflict + forbidden) and 1 for notificationsBusInstance (forbidden) + Expect(errs).To(HaveLen(3)) + + // Check for all expected errors + rabbitMqConflict := false + rabbitMqForbidden := false + notificationsForbidden := false + for _, err := range errs { + if err.Field == "spec.rabbitMqClusterName" && err.Type == field.ErrorTypeInvalid { + rabbitMqConflict = true + Expect(err.Detail).To(ContainSubstring("cannot set both deprecated field")) + } + if err.Field == "spec.rabbitMqClusterName" && err.Type == field.ErrorTypeForbidden { + rabbitMqForbidden = true + Expect(err.Detail).To(ContainSubstring("is deprecated, use")) + } + if err.Field == "spec.notificationsBusInstance" && err.Type == field.ErrorTypeForbidden { + notificationsForbidden = true + Expect(err.Detail).To(ContainSubstring("is deprecated, use")) + } + } + Expect(rabbitMqConflict).To(BeTrue(), "Expected conflict error for rabbitMqClusterName") + Expect(rabbitMqForbidden).To(BeTrue(), "Expected forbidden error for rabbitMqClusterName") + Expect(notificationsForbidden).To(BeTrue(), "Expected forbidden error for notificationsBusInstance") + }) + }) + + Describe("New messagingBus and notificationsBus field changes", func() { + It("should allow changes to messagingBus fields", func() { + oldSpec.MessagingBus = rabbitmqv1.RabbitMqConfig{ + Cluster: "old-cluster", + User: "old-user", + Vhost: "/old-vhost", + } + newSpec.MessagingBus = rabbitmqv1.RabbitMqConfig{ + Cluster: "new-cluster", + User: "new-user", + Vhost: "/new-vhost", + } + + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Should have no forbidden errors for messagingBus fields + for _, err := range errs { + if err.Type == field.ErrorTypeForbidden { + Expect(err.Field).NotTo(ContainSubstring("messagingBus")) + } + } + }) + + It("should allow changes to notificationsBus fields", func() { + oldSpec.NotificationsBus = &rabbitmqv1.RabbitMqConfig{ + Cluster: "old-notifications-cluster", + User: "old-user", + Vhost: "/old-vhost", + } + newSpec.NotificationsBus = &rabbitmqv1.RabbitMqConfig{ + Cluster: "new-notifications-cluster", + User: "new-user", + Vhost: "/new-vhost", + } + + _, errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + // Should have no forbidden errors for notificationsBus fields + for _, err := range errs { + if err.Type == field.ErrorTypeForbidden { + Expect(err.Field).NotTo(ContainSubstring("notificationsBus")) + } + } + }) + }) + }) + +}) diff --git a/test/kuttl/test-suites/default/watcher-api-scaling/01-assert.yaml b/test/kuttl/test-suites/default/watcher-api-scaling/01-assert.yaml index a871accc..1931403f 100644 --- a/test/kuttl/test-suites/default/watcher-api-scaling/01-assert.yaml +++ b/test/kuttl/test-suites/default/watcher-api-scaling/01-assert.yaml @@ -11,7 +11,8 @@ spec: passwordSelectors: service: WatcherPassword preserveJobs: false - rabbitMqClusterName: rabbitmq + messagingBus: + cluster: rabbitmq secret: osp-secret serviceUser: watcher apiServiceTemplate: diff --git a/test/kuttl/test-suites/default/watcher-cinder/02-deploy-cinder.yaml b/test/kuttl/test-suites/default/watcher-cinder/02-deploy-cinder.yaml index fb315e1d..226fd993 100644 --- a/test/kuttl/test-suites/default/watcher-cinder/02-deploy-cinder.yaml +++ b/test/kuttl/test-suites/default/watcher-cinder/02-deploy-cinder.yaml @@ -9,5 +9,7 @@ spec: databaseInstance: openstack databaseAccount: cinder secret: osp-secret + messagingBus: + cluster: rabbitmq cinderAPI: replicas: 1 diff --git a/test/kuttl/test-suites/default/watcher-notification/01-assert.yaml b/test/kuttl/test-suites/default/watcher-notification/01-assert.yaml index 8f28f71d..e0b0c911 100644 --- a/test/kuttl/test-suites/default/watcher-notification/01-assert.yaml +++ b/test/kuttl/test-suites/default/watcher-notification/01-assert.yaml @@ -12,7 +12,8 @@ spec: service: WatcherPassword secret: osp-secret apiTimeout: 60 - notificationsBusInstance: rabbitmq-notifications + notificationsBus: + cluster: rabbitmq-notifications status: apiServiceReadyCount: 1 conditions: @@ -132,7 +133,7 @@ metadata: generation: 1 labels: service: watcher - name: watcher-kuttl-watcher-notification + name: watcher-kuttl-watcher-notification-rabbitmq-notifications namespace: watcher-kuttl-default spec: rabbitmqClusterName: rabbitmq-notifications @@ -146,7 +147,7 @@ status: reason: Ready status: "True" type: TransportURLReady - secretName: rabbitmq-transport-url-watcher-kuttl-watcher-notification + secretName: rabbitmq-transport-url-watcher-kuttl-watcher-notification-rabbitmq-notifications --- apiVersion: v1 kind: Secret @@ -157,7 +158,7 @@ metadata: apiVersion: v1 kind: Secret metadata: - name: rabbitmq-transport-url-watcher-kuttl-watcher-notification + name: rabbitmq-transport-url-watcher-kuttl-watcher-notification-rabbitmq-notifications namespace: watcher-kuttl-default --- apiVersion: keystone.openstack.org/v1beta1 diff --git a/test/kuttl/test-suites/default/watcher-notification/01-deploy-with-notification.yaml b/test/kuttl/test-suites/default/watcher-notification/01-deploy-with-notification.yaml index 665086f4..9f0068e5 100644 --- a/test/kuttl/test-suites/default/watcher-notification/01-deploy-with-notification.yaml +++ b/test/kuttl/test-suites/default/watcher-notification/01-deploy-with-notification.yaml @@ -9,4 +9,5 @@ spec: apiServiceTemplate: tls: caBundleSecretName: "combined-ca-bundle" - notificationsBusInstance: "rabbitmq-notifications" + notificationsBus: + cluster: "rabbitmq-notifications" diff --git a/test/kuttl/test-suites/default/watcher-rmquser/00-cleanup-watcher.yaml b/test/kuttl/test-suites/default/watcher-rmquser/00-cleanup-watcher.yaml new file mode 120000 index 00000000..92ed6e0b --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-rmquser/00-cleanup-watcher.yaml @@ -0,0 +1 @@ +../common/cleanup-watcher.yaml \ No newline at end of file diff --git a/test/kuttl/test-suites/default/watcher-rmquser/01-assert.yaml b/test/kuttl/test-suites/default/watcher-rmquser/01-assert.yaml new file mode 100644 index 00000000..b38b942a --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-rmquser/01-assert.yaml @@ -0,0 +1,126 @@ +# Verify the TransportURLs have correct cluster, user, and vhost configured +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: watcher-kuttl-watcher-transport +spec: + rabbitmqClusterName: rabbitmq + username: watcher-rpc + vhost: watcher-rpc +--- +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: watcher-kuttl-watcher-notification-rabbitmq-notifications +spec: + rabbitmqClusterName: rabbitmq-notifications + username: watcher-notifications + vhost: watcher-notifications +--- +# Verify that 2 TransportURL CRs were created (separate clusters) +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: | + set -euxo pipefail + + # Wait for Watcher to be Ready + kubectl wait --for=condition=Ready watcher/watcher-kuttl -n $NAMESPACE --timeout=300s + + # Verify WatcherNotificationTransportURLReady condition exists and is True + kubectl get watcher watcher-kuttl -n $NAMESPACE -o jsonpath='{.status.conditions[?(@.type=="WatcherNotificationTransportURLReady")].status}' | grep -q "True" + echo "WatcherNotificationTransportURLReady condition is True" + + # Count TransportURL CRs - should be exactly 2 (one for messaging, one for notifications) + transport_count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "watcher-kuttl-watcher-transport" | wc -l) + notification_transport_count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "watcher-kuttl-watcher-notification" | wc -l) + + if [ "$transport_count" -ne "1" ]; then + echo "Expected 1 watcher-transport TransportURL, found $transport_count" + exit 1 + fi + + if [ "$notification_transport_count" -ne "1" ]; then + echo "Expected 1 notification-transport TransportURL, found $notification_transport_count" + exit 1 + fi + + echo "Correctly found 2 TransportURLs (separate clusters: transport and notification)" + + # Verify watcher-transport has correct user and vhost + transport_user=$(kubectl get transporturl watcher-kuttl-watcher-transport -n $NAMESPACE -o jsonpath='{.spec.username}') + transport_vhost=$(kubectl get transporturl watcher-kuttl-watcher-transport -n $NAMESPACE -o jsonpath='{.spec.vhost}') + if [ "$transport_user" != "watcher-rpc" ]; then + echo "Expected watcher-transport username 'watcher-rpc', found '$transport_user'" + exit 1 + fi + if [ "$transport_vhost" != "watcher-rpc" ]; then + echo "Expected watcher-transport vhost 'watcher-rpc', found '$transport_vhost'" + exit 1 + fi + echo "Watcher transport has correct user (watcher-rpc) and vhost (watcher-rpc)" + + # Verify notification-transport has correct user and vhost + notif_user=$(kubectl get transporturl watcher-kuttl-watcher-notification-rabbitmq-notifications -n $NAMESPACE -o jsonpath='{.spec.username}') + notif_vhost=$(kubectl get transporturl watcher-kuttl-watcher-notification-rabbitmq-notifications -n $NAMESPACE -o jsonpath='{.spec.vhost}') + if [ "$notif_user" != "watcher-notifications" ]; then + echo "Expected notification-transport username 'watcher-notifications', found '$notif_user'" + exit 1 + fi + if [ "$notif_vhost" != "watcher-notifications" ]; then + echo "Expected notification-transport vhost 'watcher-notifications', found '$notif_vhost'" + exit 1 + fi + echo "Notification transport has correct user (watcher-notifications) and vhost (watcher-notifications)" + + # Verify that watcher.conf contains the notifications transport_url + WATCHER_API_POD=$(kubectl get pods -n $NAMESPACE -l "service=watcher-api" -o custom-columns=:metadata.name --no-headers | grep -v ^$ | head -1) + if [ -z "${WATCHER_API_POD}" ]; then + echo "No watcher-api pod found" + exit 1 + fi + # Verify RPC transport_url in DEFAULT section + rpc_transport_url=$(kubectl exec -n $NAMESPACE ${WATCHER_API_POD} -c watcher-api -- cat /etc/watcher/watcher.conf.d/00-default.conf | grep -E '^\[DEFAULT\]' -A 50 | grep 'transport_url' | head -1 || true) + if [ -z "$rpc_transport_url" ]; then + echo "transport_url not found in DEFAULT section" + exit 1 + fi + echo "Found RPC transport_url: $rpc_transport_url" + + # Verify the RPC transport_url contains the correct vhost (watcher-rpc) + if ! echo "$rpc_transport_url" | grep -q '/watcher-rpc'; then + echo "RPC transport_url does not contain expected vhost '/watcher-rpc'" + exit 1 + fi + echo "Successfully verified vhost 'watcher-rpc' in RPC transport_url" + + # Verify the RPC transport_url contains the correct username (watcher-rpc) + if ! echo "$rpc_transport_url" | grep -q 'watcher-rpc:'; then + echo "RPC transport_url does not contain expected username 'watcher-rpc:'" + exit 1 + fi + echo "Successfully verified username 'watcher-rpc' in RPC transport_url" + + # Verify oslo_messaging_notifications section has transport_url configured + notif_transport_url=$(kubectl exec -n $NAMESPACE ${WATCHER_API_POD} -c watcher-api -- cat /etc/watcher/watcher.conf.d/00-default.conf | grep -A 5 '\[oslo_messaging_notifications\]' | grep 'transport_url' || true) + if [ -z "$notif_transport_url" ]; then + echo "transport_url not found in oslo_messaging_notifications section" + exit 1 + fi + echo "Found notifications transport_url: $notif_transport_url" + + # Verify the notifications transport_url contains the correct vhost (watcher-notifications) + if ! echo "$notif_transport_url" | grep -q '/watcher-notifications'; then + echo "Notifications transport_url does not contain expected vhost '/watcher-notifications'" + exit 1 + fi + echo "Successfully verified vhost 'watcher-notifications' in notifications transport_url" + + # Verify the notifications transport_url contains the correct username (watcher-notifications) + if ! echo "$notif_transport_url" | grep -q 'watcher-notifications:'; then + echo "Notifications transport_url does not contain expected username 'watcher-notifications:'" + exit 1 + fi + echo "Successfully verified username 'watcher-notifications' in notifications transport_url" + + exit 0 diff --git a/test/kuttl/test-suites/default/watcher-rmquser/01-deploy.yaml b/test/kuttl/test-suites/default/watcher-rmquser/01-deploy.yaml new file mode 100644 index 00000000..dba94817 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-rmquser/01-deploy.yaml @@ -0,0 +1,19 @@ +--- +apiVersion: watcher.openstack.org/v1beta1 +kind: Watcher +metadata: + name: watcher-kuttl + namespace: watcher-kuttl-default +spec: + databaseInstance: "openstack" + apiServiceTemplate: + tls: + caBundleSecretName: "combined-ca-bundle" + messagingBus: + cluster: rabbitmq + user: watcher-rpc + vhost: watcher-rpc + notificationsBus: + cluster: rabbitmq-notifications + user: watcher-notifications + vhost: watcher-notifications diff --git a/test/kuttl/test-suites/default/watcher-rmquser/02-cleanup-watcher.yaml b/test/kuttl/test-suites/default/watcher-rmquser/02-cleanup-watcher.yaml new file mode 120000 index 00000000..92ed6e0b --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-rmquser/02-cleanup-watcher.yaml @@ -0,0 +1 @@ +../common/cleanup-watcher.yaml \ No newline at end of file diff --git a/test/kuttl/test-suites/default/watcher-rmquser/02-errors.yaml b/test/kuttl/test-suites/default/watcher-rmquser/02-errors.yaml new file mode 120000 index 00000000..a632ed81 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-rmquser/02-errors.yaml @@ -0,0 +1 @@ +../common/cleanup-errors.yaml \ No newline at end of file diff --git a/test/kuttl/test-suites/default/watcher-tls-certs-change/01-assert.yaml b/test/kuttl/test-suites/default/watcher-tls-certs-change/01-assert.yaml index 75937295..b395ca65 100644 --- a/test/kuttl/test-suites/default/watcher-tls-certs-change/01-assert.yaml +++ b/test/kuttl/test-suites/default/watcher-tls-certs-change/01-assert.yaml @@ -10,7 +10,8 @@ spec: passwordSelectors: service: WatcherPassword preserveJobs: false - rabbitMqClusterName: rabbitmq + messagingBus: + cluster: rabbitmq secret: osp-secret serviceUser: watcher apiServiceTemplate: diff --git a/test/kuttl/test-suites/default/watcher-tls/01-assert.yaml b/test/kuttl/test-suites/default/watcher-tls/01-assert.yaml index 21cd7188..c5933066 100644 --- a/test/kuttl/test-suites/default/watcher-tls/01-assert.yaml +++ b/test/kuttl/test-suites/default/watcher-tls/01-assert.yaml @@ -10,7 +10,8 @@ spec: passwordSelectors: service: WatcherPassword preserveJobs: false - rabbitMqClusterName: rabbitmq + messagingBus: + cluster: rabbitmq secret: osp-secret serviceUser: watcher apiServiceTemplate: diff --git a/test/kuttl/test-suites/default/watcher-tls/03-assert.yaml b/test/kuttl/test-suites/default/watcher-tls/03-assert.yaml index aebe54bf..22642962 100644 --- a/test/kuttl/test-suites/default/watcher-tls/03-assert.yaml +++ b/test/kuttl/test-suites/default/watcher-tls/03-assert.yaml @@ -10,7 +10,8 @@ spec: passwordSelectors: service: WatcherPassword preserveJobs: false - rabbitMqClusterName: rabbitmq + messagingBus: + cluster: rabbitmq secret: osp-secret serviceUser: watcher apiServiceTemplate: diff --git a/test/kuttl/test-suites/default/watcher-tls/04-assert.yaml b/test/kuttl/test-suites/default/watcher-tls/04-assert.yaml index 8bd54204..43ccd2fe 100644 --- a/test/kuttl/test-suites/default/watcher-tls/04-assert.yaml +++ b/test/kuttl/test-suites/default/watcher-tls/04-assert.yaml @@ -10,7 +10,8 @@ spec: passwordSelectors: service: WatcherPassword preserveJobs: false - rabbitMqClusterName: rabbitmq + messagingBus: + cluster: rabbitmq secret: osp-secret serviceUser: watcher apiServiceTemplate: diff --git a/test/kuttl/test-suites/default/watcher-tls/05-assert.yaml b/test/kuttl/test-suites/default/watcher-tls/05-assert.yaml index fa4b3428..ec811b4b 100644 --- a/test/kuttl/test-suites/default/watcher-tls/05-assert.yaml +++ b/test/kuttl/test-suites/default/watcher-tls/05-assert.yaml @@ -10,7 +10,8 @@ spec: passwordSelectors: service: WatcherPassword preserveJobs: false - rabbitMqClusterName: rabbitmq + messagingBus: + cluster: rabbitmq secret: osp-secret serviceUser: watcher apiServiceTemplate: diff --git a/test/kuttl/test-suites/default/watcher-topology/01-assert.yaml b/test/kuttl/test-suites/default/watcher-topology/01-assert.yaml index a0f7762f..d6487c12 100644 --- a/test/kuttl/test-suites/default/watcher-topology/01-assert.yaml +++ b/test/kuttl/test-suites/default/watcher-topology/01-assert.yaml @@ -11,7 +11,8 @@ spec: passwordSelectors: service: WatcherPassword preserveJobs: false - rabbitMqClusterName: rabbitmq + messagingBus: + cluster: rabbitmq secret: osp-secret serviceUser: watcher apiServiceTemplate: diff --git a/test/kuttl/test-suites/default/watcher/01-assert.yaml b/test/kuttl/test-suites/default/watcher/01-assert.yaml index 873010dc..b12c733c 100644 --- a/test/kuttl/test-suites/default/watcher/01-assert.yaml +++ b/test/kuttl/test-suites/default/watcher/01-assert.yaml @@ -11,7 +11,8 @@ spec: passwordSelectors: service: WatcherPassword preserveJobs: false - rabbitMqClusterName: rabbitmq + messagingBus: + cluster: rabbitmq secret: osp-secret serviceUser: watcher apiServiceTemplate: