From f5abf3f887a44fa3afcc9ebe39eaebf8b264d9dc Mon Sep 17 00:00:00 2001 From: Luca Miccini Date: Fri, 19 Dec 2025 16:41:13 +0100 Subject: [PATCH] Rabbitmq vhost and user support Add new messagingBus and notificationsBus interfaces to hold cluster, user and vhost names for optional usage. The controller adds these values to the TransportURL create request when present. Additionally, we migrate RabbitMQ cluster name to RabbitMq config struct using DefaultRabbitMqConfig from infra-operator to automatically populate the new Cluster field from legacy RabbitMqClusterName. Example usage: spec: messagingBus: cluster: rpc-rabbitmq user: rpc-user vhost: rpc-vhost notificationsBus: cluster: notifications-rabbitmq user: notifications-user vhost: notifications-vhost Jira: https://issues.redhat.com/browse/OSPRH-23882 --- api/bases/watcher.openstack.org_watchers.yaml | 35 ++ api/go.mod | 28 +- api/go.sum | 51 +- api/v1beta1/common_types.go | 14 +- api/v1beta1/watcher_webhook.go | 55 ++- api/v1beta1/zz_generated.deepcopy.go | 7 + .../bases/watcher.openstack.org_watchers.yaml | 35 ++ go.mod | 42 +- go.sum | 76 +-- internal/controller/watcher_controller.go | 21 +- test/functional/watcher_controller_test.go | 108 ++++- test/functional/watcher_webhook_test.go | 444 +++++++++++++++++- .../00-cleanup-watcher.yaml | 6 + .../watcher-notificationsbus/01-assert.yaml | 65 +++ .../watcher-notificationsbus/01-deploy.yaml | 18 + .../02-cleanup-watcher.yaml | 6 + .../00-cleanup-watcher.yaml | 6 + .../watcher-same-cluster/01-assert.yaml | 82 ++++ .../watcher-same-cluster/01-deploy.yaml | 18 + .../02-cleanup-watcher.yaml | 6 + 20 files changed, 1004 insertions(+), 119 deletions(-) create mode 100644 test/kuttl/test-suites/default/watcher-notificationsbus/00-cleanup-watcher.yaml create mode 100644 test/kuttl/test-suites/default/watcher-notificationsbus/01-assert.yaml create mode 100644 test/kuttl/test-suites/default/watcher-notificationsbus/01-deploy.yaml create mode 100644 test/kuttl/test-suites/default/watcher-notificationsbus/02-cleanup-watcher.yaml create mode 100644 test/kuttl/test-suites/default/watcher-same-cluster/00-cleanup-watcher.yaml create mode 100644 test/kuttl/test-suites/default/watcher-same-cluster/01-assert.yaml create mode 100644 test/kuttl/test-suites/default/watcher-same-cluster/01-deploy.yaml create mode 100644 test/kuttl/test-suites/default/watcher-same-cluster/02-cleanup-watcher.yaml diff --git a/api/bases/watcher.openstack.org_watchers.yaml b/api/bases/watcher.openstack.org_watchers.yaml index 6ffefcda..5082253a 100644 --- a/api/bases/watcher.openstack.org_watchers.yaml +++ b/api/bases/watcher.openstack.org_watchers.yaml @@ -608,6 +608,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 @@ -615,6 +631,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 @@ -623,6 +656,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: @@ -650,6 +684,7 @@ spec: 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 diff --git a/api/go.mod b/api/go.mod index d97bd608..02b91c1f 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.20251223124749-eedb97238c5f - 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.20251217131115-0f117a938d4e + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251122131503-b76943960b6c 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 @@ -45,6 +44,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 @@ -52,9 +52,9 @@ require ( golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.12.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect google.golang.org/protobuf v1.36.7 // indirect @@ -71,21 +71,21 @@ require ( ) // pin these to avoid later versions pulled by rabbitmq -replace k8s.io/apimachinery => k8s.io/apimachinery v0.31.14 //allow-merging +replace k8s.io/apimachinery => k8s.io/apimachinery v0.31.13 //allow-merging -replace k8s.io/api => k8s.io/api v0.31.14 //allow-merging +replace k8s.io/api => k8s.io/api v0.31.13 //allow-merging -replace k8s.io/apiserver => k8s.io/apiserver v0.31.14 //allow-merging +replace k8s.io/apiserver => k8s.io/apiserver v0.31.13 //allow-merging -replace k8s.io/client-go => k8s.io/client-go v0.31.14 //allow-merging +replace k8s.io/client-go => k8s.io/client-go v0.31.13 //allow-merging -replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.31.14 //allow-merging +replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.31.13 //allow-merging -replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.31.14 //allow-merging +replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.31.13 //allow-merging -replace k8s.io/code-generator => k8s.io/code-generator v0.31.14 //allow-merging +replace k8s.io/code-generator => k8s.io/code-generator v0.31.13 //allow-merging -replace k8s.io/component-base => k8s.io/component-base v0.31.14 //allow-merging +replace k8s.io/component-base => k8s.io/component-base v0.31.13 //allow-merging // custom RabbitmqClusterSpecCore for OpenStackControlplane (v2.16.0_patches) replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec //allow-merging diff --git a/api/go.sum b/api/go.sum index 373d2d0b..f6a8aa90 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= @@ -74,14 +75,16 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -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.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= -github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251223124749-eedb97238c5f h1:xcCGJ/g5vvbWhtEJCbv8UeBneI5yrMawm+CXRsJrJZo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251223124749-eedb97238c5f/go.mod h1:ex8ou6/3ms6ovR+CMXD6XhTlNakm1GhB6UZgagVRNW8= -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/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251217131115-0f117a938d4e h1:PIjcXzMMwfvBRFgFpaq/W9tqy0t2cYvcWX+kq6uNtTM= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251217131115-0f117a938d4e/go.mod h1:ex8ou6/3ms6ovR+CMXD6XhTlNakm1GhB6UZgagVRNW8= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251122131503-b76943960b6c h1:wM8qXCB5mQwSosCvtaydzuXitWVVKBHTzH0A2znQ+Jg= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251122131503-b76943960b6c/go.mod h1:+Me0raWPPdz8gRi9D4z1khmvUgS9vIKAVC8ckg1yJZU= +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= @@ -137,19 +140,19 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -175,14 +178,14 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.14 h1:xYn/S/WFJsksI7dk/5uBRd3Umm/D8W5g7sRnd4csotA= -k8s.io/api v0.31.14/go.mod h1:K8fvRey4z73RAuxBZCma7WtY8WFvkViYhfFLCMT4xgA= -k8s.io/apiextensions-apiserver v0.31.14 h1:1KupD0PyU7CgiT/PiZPSgZhTCL2KGwvXd1ejGcxjEfg= -k8s.io/apiextensions-apiserver v0.31.14/go.mod h1:Odk14fSl/zaciI8DRUSPMSH74UXtz4gfinw7zY7YHvE= -k8s.io/apimachinery v0.31.14 h1:/eMIwjv+GFm6A/sSGlB1NupBU6wTDPhEWsju0Fj69kY= -k8s.io/apimachinery v0.31.14/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/client-go v0.31.14 h1:d4/G0xfksNIbMWH7ghjzOwC5bTAwQ20gABTjZw7fLlQ= -k8s.io/client-go v0.31.14/go.mod h1:0uRpRB7r5QwtsbxEngZPkbcIVoNdAQAPIcopgiXjhQc= +k8s.io/api v0.31.13 h1:sco9Cq2pY4Ysv9qZiWzcR97MmA/35nwYQ/VCTzOcWmc= +k8s.io/api v0.31.13/go.mod h1:4D8Ry8RqqLDemNLwGYC6v5wOy51N7hitr4WQ6oSWfLY= +k8s.io/apiextensions-apiserver v0.31.13 h1:8xtWKVpV/YbYX0UX2k6w+cgxfxKhX0UNGuo/VXAdg8g= +k8s.io/apiextensions-apiserver v0.31.13/go.mod h1:zxpMLWXBxnJqKUIruJ+ulP+Xlfe5lPZPxq1z0cLwA2U= +k8s.io/apimachinery v0.31.13 h1:rkG0EiBkBkEzURo/8dKGx/oBF202Z2LqHuSD8Cm3bG4= +k8s.io/apimachinery v0.31.13/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/client-go v0.31.13 h1:Q0LG51uFbzNd9fzIj5ilA0Sm1wUholHvDaNwVKzqdCA= +k8s.io/client-go v0.31.13/go.mod h1:UB4yTzQeRAv+vULOKp2jdqA5LSwV55bvc3RQ5tM48LM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e h1:UGI9rv1A2cV87NhXr4s+AUBxIuoo/SME/IyJ3b6KztE= diff --git a/api/v1beta1/common_types.go b/api/v1beta1/common_types.go index 09a711a7..5d17b6e4 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,10 +83,19 @@ type WatcherSpecCore struct { // Important: Run "make" to regenerate code after modifying this file WatcherCommon `json:",inline"` + // +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=rabbitmq // RabbitMQ instance name // Needed to request a transportURL that is created and used in Watcher + // Deprecated: Use MessagingBus.Cluster instead RabbitMqClusterName *string `json:"rabbitMqClusterName"` // +kubebuilder:validation:Optional @@ -136,6 +145,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..2fb8a5e4 100644 --- a/api/v1beta1/watcher_webhook.go +++ b/api/v1beta1/watcher_webhook.go @@ -19,6 +19,7 @@ package v1beta1 import ( "fmt" + rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1" topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" @@ -59,12 +60,34 @@ 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 + // Apply kubebuilder default for RabbitMqClusterName if not set + if spec.RabbitMqClusterName == nil { + spec.RabbitMqClusterName = ptr.To("rabbitmq") + } + + // Default MessagingBus.Cluster from RabbitMqClusterName if not already set + if spec.MessagingBus.Cluster == "" { + 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) + } + } } var _ webhook.Validator = &Watcher{} @@ -95,7 +118,7 @@ func (spec *WatcherSpec) ValidateCreate(basePath *field.Path, namespace string) func (spec *WatcherSpecCore) ValidateCreate(basePath *field.Path, namespace string) field.ErrorList { var allErrs field.ErrorList - if *spec.DatabaseInstance == "" || spec.DatabaseInstance == nil { + if spec.DatabaseInstance == nil || *spec.DatabaseInstance == "" { allErrs = append( allErrs, field.Invalid( @@ -103,11 +126,12 @@ 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"), ) } @@ -148,7 +172,7 @@ func (spec *WatcherSpec) ValidateUpdate(old WatcherSpec, basePath *field.Path, n func (spec *WatcherSpecCore) ValidateUpdate(old WatcherSpecCore, basePath *field.Path, namespace string) field.ErrorList { var allErrs field.ErrorList - if *spec.DatabaseInstance == "" || spec.DatabaseInstance == nil { + if spec.DatabaseInstance == nil || *spec.DatabaseInstance == "" { allErrs = append( allErrs, field.Invalid( @@ -156,14 +180,31 @@ 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"), ) } + // Reject changes to deprecated RabbitMqClusterName field + if spec.RabbitMqClusterName != nil && old.RabbitMqClusterName != nil && + *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")) + } + allErrs = append(allErrs, spec.ValidateWatcherTopology(basePath, namespace)...) return allErrs diff --git a/api/v1beta1/zz_generated.deepcopy.go b/api/v1beta1/zz_generated.deepcopy.go index a3555616..eaf1ac14 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" @@ -665,6 +666,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/config/crd/bases/watcher.openstack.org_watchers.yaml b/config/crd/bases/watcher.openstack.org_watchers.yaml index 6ffefcda..5082253a 100644 --- a/config/crd/bases/watcher.openstack.org_watchers.yaml +++ b/config/crd/bases/watcher.openstack.org_watchers.yaml @@ -608,6 +608,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 @@ -615,6 +631,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 @@ -623,6 +656,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: @@ -650,6 +684,7 @@ spec: 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 diff --git a/go.mod b/go.mod index 517c4850..46e5b210 100644 --- a/go.mod +++ b/go.mod @@ -5,14 +5,14 @@ go 1.24.4 require ( github.com/go-logr/logr v1.4.3 github.com/google/uuid v1.6.0 - github.com/onsi/ginkgo/v2 v2.27.3 - github.com/onsi/gomega v1.38.3 + github.com/onsi/ginkgo/v2 v2.27.2 + github.com/onsi/gomega v1.38.2 github.com/openshift/api v3.9.0+incompatible - github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251223124749-eedb97238c5f - github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251206133124-593df0a7a9e1 - 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.20251227161101-0139ab4049df + github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251217131115-0f117a938d4e + github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251027074845-ed8154b20ad1 + github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251122131503-b76943960b6c + github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef + github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251110170510-e669472c745c 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 @@ -62,8 +62,8 @@ 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/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251122131503-b76943960b6c // indirect - github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251122131503-b76943960b6c // indirect + github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251021145236-2b84ec9fd9bb // indirect + github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20250929092825-4c2402451077 // 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 @@ -90,10 +90,10 @@ require ( golang.org/x/mod v0.27.0 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.36.0 // indirect - golang.org/x/term v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect + golang.org/x/sync v0.16.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/term v0.34.0 // indirect + golang.org/x/text v0.28.0 // indirect golang.org/x/time v0.12.0 // indirect golang.org/x/tools v0.36.0 // indirect gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect @@ -122,21 +122,21 @@ replace github.com/openstack-k8s-operators/watcher-operator/api => ./api replace github.com/openshift/api => github.com/openshift/api v0.0.0-20250711200046-c86d80652a9e //allow-merging // pin these to avoid later versions pulled by rabbitmq -replace k8s.io/apimachinery => k8s.io/apimachinery v0.31.14 //allow-merging +replace k8s.io/apimachinery => k8s.io/apimachinery v0.31.13 //allow-merging -replace k8s.io/api => k8s.io/api v0.31.14 //allow-merging +replace k8s.io/api => k8s.io/api v0.31.13 //allow-merging -replace k8s.io/apiserver => k8s.io/apiserver v0.31.14 //allow-merging +replace k8s.io/apiserver => k8s.io/apiserver v0.31.13 //allow-merging -replace k8s.io/client-go => k8s.io/client-go v0.31.14 //allow-merging +replace k8s.io/client-go => k8s.io/client-go v0.31.13 //allow-merging -replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.31.14 //allow-merging +replace k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.31.13 //allow-merging -replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.31.14 //allow-merging +replace k8s.io/cli-runtime => k8s.io/cli-runtime v0.31.13 //allow-merging -replace k8s.io/code-generator => k8s.io/code-generator v0.31.14 //allow-merging +replace k8s.io/code-generator => k8s.io/code-generator v0.31.13 //allow-merging -replace k8s.io/component-base => k8s.io/component-base v0.31.14 //allow-merging +replace k8s.io/component-base => k8s.io/component-base v0.31.13 //allow-merging // custom RabbitmqClusterSpecCore for OpenStackControlplane (v2.16.0_patches) replace github.com/rabbitmq/cluster-operator/v2 => github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec //allow-merging diff --git a/go.sum b/go.sum index e5935173..411bc567 100644 --- a/go.sum +++ b/go.sum @@ -112,26 +112,26 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -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.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM= -github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= 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.20251223124749-eedb97238c5f h1:xcCGJ/g5vvbWhtEJCbv8UeBneI5yrMawm+CXRsJrJZo= -github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251223124749-eedb97238c5f/go.mod h1:ex8ou6/3ms6ovR+CMXD6XhTlNakm1GhB6UZgagVRNW8= -github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251206133124-593df0a7a9e1 h1:qcgbrF9c0axkaDcFGfIA2wGz8bkaxPuXHj3mdKAyz6M= -github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251206133124-593df0a7a9e1/go.mod h1:0XsZ6Fc4hTV6a/BBP8+jiH8LR+IP5z9aStdPTDHALNk= -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/lib-common/modules/openstack v0.6.1-0.20251122131503-b76943960b6c h1:l7FO+XoQRnD4aT5p/JXVY2uezQLdC7D50KrwrTmzCfg= -github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251122131503-b76943960b6c/go.mod h1:zOX7Y05keiSppIvLabuyh42QHBMhCcoskAtxFRbwXKo= -github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251122131503-b76943960b6c h1:dVIaDL5BeIdJjERGaN/XlcvZVplfkzh0uUfiVUHj/6Q= -github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251122131503-b76943960b6c/go.mod h1:fy1lvz3uuzzh01DKKdgroXvmJgMpJBsvl2r9eTtAll0= -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.20251227161101-0139ab4049df h1:zGxVcMXypAz4wfSsYty1YsopUQxVWbAuB5YKxL6TeSg= -github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251227161101-0139ab4049df/go.mod h1:4xrzu3BhXAGidaAKmfNi1oDlWr8T8e8n4G2k39V/zKw= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251217131115-0f117a938d4e h1:PIjcXzMMwfvBRFgFpaq/W9tqy0t2cYvcWX+kq6uNtTM= +github.com/openstack-k8s-operators/infra-operator/apis v0.6.1-0.20251217131115-0f117a938d4e/go.mod h1:ex8ou6/3ms6ovR+CMXD6XhTlNakm1GhB6UZgagVRNW8= +github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251027074845-ed8154b20ad1 h1:QohvX44nxoV2GwvvOURGXYyDuCn4SCrnwubTKJtzehY= +github.com/openstack-k8s-operators/keystone-operator/api v0.6.1-0.20251027074845-ed8154b20ad1/go.mod h1:FMFoO4MjEQ85JpdLtDHxYSZxvJ9KzHua+HdKhpl0KRI= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251122131503-b76943960b6c h1:wM8qXCB5mQwSosCvtaydzuXitWVVKBHTzH0A2znQ+Jg= +github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251122131503-b76943960b6c/go.mod h1:+Me0raWPPdz8gRi9D4z1khmvUgS9vIKAVC8ckg1yJZU= +github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251021145236-2b84ec9fd9bb h1:wToXqX7AS1JV3Kna7RcJfkRart8rSGun2biKNfyY6Zg= +github.com/openstack-k8s-operators/lib-common/modules/openstack v0.6.1-0.20251021145236-2b84ec9fd9bb/go.mod h1:yf13jWb60XV26eA7A8o86ZCXNWBLNK9dPkTSWFaTPCw= +github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20250929092825-4c2402451077 h1:9tpPDBV2RLXMDgt13ec8XR2OatFriItseqg+Oyvx9GA= +github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20250929092825-4c2402451077/go.mod h1:JPQHkExlxeT6MU3DNJgXXJJG0NMQHlZwxxfbYRaP3eg= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef h1:U9cgXJs/GuO6/0bRn6oaS7ovDrabyGPZpmZyAWksUuQ= +github.com/openstack-k8s-operators/lib-common/modules/test v0.6.1-0.20251103072528-9eb684fef4ef/go.mod h1:lgYyrXEYA2BPsq4Kg6dqa+QsHgOjMPyOsEYrvyYW3jk= +github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251110170510-e669472c745c h1:Fx2ZD3jNoqprCJSEkvU59vnkNNluLH1QUstpZN6M6JU= +github.com/openstack-k8s-operators/mariadb-operator/api v0.6.1-0.20251110170510-e669472c745c/go.mod h1:GlqBKaTSZBxhUD2W1WMp80NHO2JsjLGMO7XKJSemoq0= 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= @@ -224,19 +224,19 @@ golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKl golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= +golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= -golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= -golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= +golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -271,18 +271,18 @@ gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.14 h1:xYn/S/WFJsksI7dk/5uBRd3Umm/D8W5g7sRnd4csotA= -k8s.io/api v0.31.14/go.mod h1:K8fvRey4z73RAuxBZCma7WtY8WFvkViYhfFLCMT4xgA= -k8s.io/apiextensions-apiserver v0.31.14 h1:1KupD0PyU7CgiT/PiZPSgZhTCL2KGwvXd1ejGcxjEfg= -k8s.io/apiextensions-apiserver v0.31.14/go.mod h1:Odk14fSl/zaciI8DRUSPMSH74UXtz4gfinw7zY7YHvE= -k8s.io/apimachinery v0.31.14 h1:/eMIwjv+GFm6A/sSGlB1NupBU6wTDPhEWsju0Fj69kY= -k8s.io/apimachinery v0.31.14/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/apiserver v0.31.14 h1:DORopWIu2qg7gmVyA9UUGGGmO1Rmnq5Oe+GmsKen3yo= -k8s.io/apiserver v0.31.14/go.mod h1:q81QJuh85u/HN74pdw5Ci4EnrRmCOonZj9FvLwf8DWc= -k8s.io/client-go v0.31.14 h1:d4/G0xfksNIbMWH7ghjzOwC5bTAwQ20gABTjZw7fLlQ= -k8s.io/client-go v0.31.14/go.mod h1:0uRpRB7r5QwtsbxEngZPkbcIVoNdAQAPIcopgiXjhQc= -k8s.io/component-base v0.31.14 h1:VNjBuEMmvlwL4twRlMmlaVmsodIRaNivXcZoAx1/x7Q= -k8s.io/component-base v0.31.14/go.mod h1:9ogYcJBUdB4VQ/OMgInYVRScC9bguXxSEEZPsInY+uM= +k8s.io/api v0.31.13 h1:sco9Cq2pY4Ysv9qZiWzcR97MmA/35nwYQ/VCTzOcWmc= +k8s.io/api v0.31.13/go.mod h1:4D8Ry8RqqLDemNLwGYC6v5wOy51N7hitr4WQ6oSWfLY= +k8s.io/apiextensions-apiserver v0.31.13 h1:8xtWKVpV/YbYX0UX2k6w+cgxfxKhX0UNGuo/VXAdg8g= +k8s.io/apiextensions-apiserver v0.31.13/go.mod h1:zxpMLWXBxnJqKUIruJ+ulP+Xlfe5lPZPxq1z0cLwA2U= +k8s.io/apimachinery v0.31.13 h1:rkG0EiBkBkEzURo/8dKGx/oBF202Z2LqHuSD8Cm3bG4= +k8s.io/apimachinery v0.31.13/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/apiserver v0.31.13 h1:Ke9/X2m3vHSgsminpAbUxULDNMbvAfjrRX73Gqx6CZc= +k8s.io/apiserver v0.31.13/go.mod h1:5nBPhL2g7am/CS+/OI5A6+olEbo0C7tQ8QNDODLd+WY= +k8s.io/client-go v0.31.13 h1:Q0LG51uFbzNd9fzIj5ilA0Sm1wUholHvDaNwVKzqdCA= +k8s.io/client-go v0.31.13/go.mod h1:UB4yTzQeRAv+vULOKp2jdqA5LSwV55bvc3RQ5tM48LM= +k8s.io/component-base v0.31.13 h1:/uVLq7yHk9azReqeCFAZSr/8NXydzpz7yDZ6p/yiwBQ= +k8s.io/component-base v0.31.13/go.mod h1:uMXtKNyDqeNdZYL6SRCr9wB6FutL9pOlQmkK2dRVAKQ= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250627150254-e9823e99808e h1:UGI9rv1A2cV87NhXr4s+AUBxIuoo/SME/IyJ3b6KztE= diff --git a/internal/controller/watcher_controller.go b/internal/controller/watcher_controller.go index 9c02e7b4..0012a8b8 100644 --- a/internal/controller/watcher_controller.go +++ b/internal/controller/watcher_controller.go @@ -204,7 +204,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 +233,20 @@ 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 + // (the webhook defaults this from the deprecated NotificationsBusInstance field) + 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 + + notificationURL, op, err := r.ensureMQ(ctx, instance, helper, instance.Name+"-watcher-notification", notificationsRabbitMqConfig, serviceLabels) if err != nil { instance.Status.Conditions.Set(condition.FalseCondition( watcherv1beta1.WatcherNotificationTransportURLReadyCondition, @@ -660,7 +666,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) @@ -675,7 +681,12 @@ func (r *WatcherReconciler) ensureMQ( } op, err := controllerutil.CreateOrUpdate(ctx, r.Client, transportURL, func() error { - transportURL.Spec.RabbitmqClusterName = messageBusInstance + transportURL.Spec.RabbitmqClusterName = rabbitMqConfig.Cluster + if rabbitMqConfig.User != "" { + transportURL.Spec.Username = rabbitMqConfig.User + } + // Always set Vhost - empty string means use default "/" vhost + transportURL.Spec.Vhost = rabbitMqConfig.Vhost err := controllerutil.SetControllerReference(instance, transportURL, r.Scheme) return err diff --git a/test/functional/watcher_controller_test.go b/test/functional/watcher_controller_test.go index 076bc1b2..5630202f 100644 --- a/test/functional/watcher_controller_test.go +++ b/test/functional/watcher_controller_test.go @@ -710,6 +710,7 @@ var _ = Describe("Watcher controller", func() { It("should raise an error for empty databaseInstance", func() { spec := GetDefaultWatcherAPISpec() spec["databaseInstance"] = "" + spec["rabbitMqClusterName"] = "rabbitmq" raw := map[string]any{ "apiVersion": "watcher.openstack.org/v1beta1", @@ -741,6 +742,7 @@ var _ = Describe("Watcher controller", func() { spec := GetDefaultWatcherAPISpec() spec["topologyRef"] = map[string]any{"name": "foo", "namespace": "bar"} + spec["rabbitMqClusterName"] = "rabbitmq" raw := map[string]any{ "apiVersion": "watcher.openstack.org/v1beta1", @@ -767,10 +769,13 @@ var _ = Describe("Watcher controller", func() { }) }) - When("Watcher is created with empty RabbitMqClusterName", func() { - It("should raise an error for empty RabbitMqClusterName", func() { + When("Watcher is created with empty messagingBus.cluster", func() { + It("should raise an error for empty messagingBus.cluster", func() { spec := GetDefaultWatcherAPISpec() spec["rabbitMqClusterName"] = "" + spec["messagingBus"] = map[string]any{ + "cluster": "", + } raw := map[string]any{ "apiVersion": "watcher.openstack.org/v1beta1", @@ -786,12 +791,12 @@ var _ = Describe("Watcher controller", func() { _, err := controllerutil.CreateOrPatch( th.Ctx, th.K8sClient, unstructuredObj, func() error { return nil }) Expect(err).To(HaveOccurred()) + // Error comes from CRD schema validation (minLength: 1) before webhook validation 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"), + "Watcher.watcher.openstack.org \"watcher\" is invalid: " + + "spec.messagingBus.cluster: Invalid value: \"\": " + + "spec.messagingBus.cluster in body should be at least 1 chars long"), ) }) }) @@ -1748,4 +1753,95 @@ 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 + infra.SimulateTransportURLReady(watcherTest.WatcherNotificationTransportURL) + + // 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_webhook_test.go b/test/functional/watcher_webhook_test.go index ed9b7c69..c09d5a22 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,442 @@ var _ = Describe("SetDefaultRouteAnnotations", func() { }) }) + +var _ = Describe("Watcher Webhook Messaging and Notifications", func() { + + Describe("RabbitMqClusterName defaulting to messagingBus.cluster", func() { + var spec *watcherv1.WatcherSpecCore + + BeforeEach(func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("my-rabbitmq"), + } + }) + + It("should default messagingBus.cluster from RabbitMqClusterName when messagingBus is empty", func() { + spec.Default() + + Expect(spec.MessagingBus.Cluster).To(Equal("my-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("NotificationsBusInstance defaulting to notificationsBus.cluster", func() { + var spec *watcherv1.WatcherSpecCore + + BeforeEach(func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + NotificationsBusInstance: ptr.To("rabbitmq-notifications"), + } + }) + + It("should default notificationsBus.cluster from NotificationsBusInstance", func() { + spec.Default() + + Expect(spec.NotificationsBus).NotTo(BeNil()) + Expect(spec.NotificationsBus.Cluster).To(Equal("rabbitmq-notifications")) + }) + + It("should inherit user from messagingBus when NotificationsBusInstance is set", func() { + spec.Default() + + Expect(spec.NotificationsBus).NotTo(BeNil()) + // User is inherited from messagingBus, which is empty by default + Expect(spec.NotificationsBus.User).To(Equal("")) + }) + + It("should inherit vhost from messagingBus when NotificationsBusInstance is set", func() { + spec.Default() + + Expect(spec.NotificationsBus).NotTo(BeNil()) + // Vhost is inherited from messagingBus, which is empty by default + Expect(spec.NotificationsBus.Vhost).To(Equal("")) + }) + + It("should not create notificationsBus when NotificationsBusInstance is nil", func() { + spec.NotificationsBusInstance = nil + spec.Default() + + Expect(spec.NotificationsBus).To(BeNil()) + }) + + It("should not create notificationsBus when NotificationsBusInstance is empty string", func() { + spec.NotificationsBusInstance = ptr.To("") + spec.Default() + + Expect(spec.NotificationsBus).To(BeNil()) + }) + + 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 separation from messagingBus", func() { + var spec *watcherv1.WatcherSpecCore + + It("should NOT inherit user and vhost from messagingBus when notificationsBus is created", func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + NotificationsBusInstance: ptr.To("rabbitmq-notifications"), + MessagingBus: rabbitmqv1.RabbitMqConfig{ + User: "custom-user", + Vhost: "/custom-vhost", + }, + } + spec.Default() + + Expect(spec.NotificationsBus).NotTo(BeNil()) + // User and vhost should be empty (not inherited) to ensure separation + Expect(spec.NotificationsBus.User).To(Equal("")) + Expect(spec.NotificationsBus.Vhost).To(Equal("")) + Expect(spec.NotificationsBus.Cluster).To(Equal("rabbitmq-notifications")) + }) + + It("should not override notificationsBus fields if already set", func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + NotificationsBusInstance: ptr.To("rabbitmq-notifications"), + NotificationsBus: &rabbitmqv1.RabbitMqConfig{ + Cluster: "custom-notifications-cluster", + User: "custom-notifications-user", + Vhost: "/custom-notifications-vhost", + }, + } + spec.Default() + + Expect(spec.NotificationsBus.Cluster).To(Equal("custom-notifications-cluster")) + Expect(spec.NotificationsBus.User).To(Equal("custom-notifications-user")) + Expect(spec.NotificationsBus.Vhost).To(Equal("/custom-notifications-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 all deprecated and new fields together correctly", func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("rabbitmq"), + NotificationsBusInstance: ptr.To("rabbitmq-notifications"), + MessagingBus: rabbitmqv1.RabbitMqConfig{ + User: "messaging-user", + Vhost: "/messaging-vhost", + }, + } + spec.Default() + + // messagingBus should be defaulted from RabbitMqClusterName + Expect(spec.MessagingBus.Cluster).To(Equal("rabbitmq")) + Expect(spec.MessagingBus.User).To(Equal("messaging-user")) + Expect(spec.MessagingBus.Vhost).To(Equal("/messaging-vhost")) + + // notificationsBus should NOT inherit user/vhost (for separation), only cluster from NotificationsBusInstance + Expect(spec.NotificationsBus).NotTo(BeNil()) + Expect(spec.NotificationsBus.Cluster).To(Equal("rabbitmq-notifications")) + Expect(spec.NotificationsBus.User).To(Equal("")) + Expect(spec.NotificationsBus.Vhost).To(Equal("")) + }) + + It("should prioritize new fields over deprecated fields", func() { + spec = &watcherv1.WatcherSpecCore{ + RabbitMqClusterName: ptr.To("old-rabbitmq"), + NotificationsBusInstance: ptr.To("old-notifications"), + MessagingBus: rabbitmqv1.RabbitMqConfig{ + Cluster: "new-rabbitmq", + User: "new-user", + Vhost: "/new-vhost", + }, + NotificationsBus: &rabbitmqv1.RabbitMqConfig{ + Cluster: "new-notifications", + User: "new-notifications-user", + Vhost: "/new-notifications-vhost", + }, + } + spec.Default() + + Expect(spec.MessagingBus.Cluster).To(Equal("new-rabbitmq")) + Expect(spec.MessagingBus.User).To(Equal("new-user")) + Expect(spec.MessagingBus.Vhost).To(Equal("/new-vhost")) + + Expect(spec.NotificationsBus.Cluster).To(Equal("new-notifications")) + Expect(spec.NotificationsBus.User).To(Equal("new-notifications-user")) + Expect(spec.NotificationsBus.Vhost).To(Equal("/new-notifications-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(errs).To(HaveLen(1)) + Expect(errs[0].Type).To(Equal(field.ErrorTypeForbidden)) + Expect(errs[0].Field).To(Equal("spec.rabbitMqClusterName")) + Expect(errs[0].Detail).To(ContainSubstring("deprecated and cannot be changed")) + Expect(errs[0].Detail).To(ContainSubstring("messagingBus.cluster")) + }) + + 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") + // Call Default() again to populate NotificationsBus from NotificationsBusInstance + oldSpec.Default() + + newSpec.NotificationsBusInstance = ptr.To("rabbitmq-notifications") + // Call Default() again to populate NotificationsBus from NotificationsBusInstance + newSpec.Default() + }) + + It("should reject changes to NotificationsBusInstance", func() { + newSpec.NotificationsBusInstance = ptr.To("new-rabbitmq-notifications") + + errs := newSpec.ValidateUpdate(*oldSpec, basePath, "test-namespace") + + Expect(errs).To(HaveLen(1)) + Expect(errs[0].Type).To(Equal(field.ErrorTypeForbidden)) + Expect(errs[0].Field).To(Equal("spec.notificationsBusInstance")) + Expect(errs[0].Detail).To(ContainSubstring("deprecated and cannot be changed")) + Expect(errs[0].Detail).To(ContainSubstring("notificationsBus.cluster")) + }) + + 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(errs).To(HaveLen(2)) + + // Check for RabbitMqClusterName error + rabbitMqErr := false + notificationsErr := false + for _, err := range errs { + if err.Field == "spec.rabbitMqClusterName" { + rabbitMqErr = true + Expect(err.Type).To(Equal(field.ErrorTypeForbidden)) + } + if err.Field == "spec.notificationsBusInstance" { + notificationsErr = true + Expect(err.Type).To(Equal(field.ErrorTypeForbidden)) + } + } + Expect(rabbitMqErr).To(BeTrue(), "Expected error for rabbitMqClusterName") + Expect(notificationsErr).To(BeTrue(), "Expected 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-notificationsbus/00-cleanup-watcher.yaml b/test/kuttl/test-suites/default/watcher-notificationsbus/00-cleanup-watcher.yaml new file mode 100644 index 00000000..72051465 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-notificationsbus/00-cleanup-watcher.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: watcher.openstack.org/v1beta1 + kind: Watcher + name: watcher-kuttl diff --git a/test/kuttl/test-suites/default/watcher-notificationsbus/01-assert.yaml b/test/kuttl/test-suites/default/watcher-notificationsbus/01-assert.yaml new file mode 100644 index 00000000..c51bdfb3 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-notificationsbus/01-assert.yaml @@ -0,0 +1,65 @@ +apiVersion: watcher.openstack.org/v1beta1 +kind: Watcher +metadata: + name: watcher-kuttl + namespace: watcher-kuttl-default +status: + conditions: + - message: Setup complete + reason: Ready + status: "True" + type: Ready + - message: WatcherRabbitMQTransportURL successfully created + reason: Ready + status: "True" + type: WatcherRabbitMQTransportURLReady + - message: WatcherNotificationTransportURL successfully created + reason: Ready + status: "True" + type: WatcherNotificationTransportURLReady +--- +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: watcher-kuttl-watcher-transport +spec: + rabbitmqClusterName: rabbitmq + username: watcher-rpc + vhost: watcher-rpc +status: + conditions: + - message: TransportURL successfully created + reason: Ready + status: "True" + type: Ready +--- +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 +status: + conditions: + - message: TransportURL successfully created + reason: Ready + status: "True" + type: Ready +--- +# Verify that watcher.conf contains the notifications transport_url +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: | + set -euxo pipefail + APIPOD=$(kubectl get pods -n $NAMESPACE -l "service=watcher-api" -o custom-columns=:metadata.name --no-headers | grep -v ^$ | head -1) + if [ -z "${APIPOD}" ]; then + echo "No watcher-api pod found" + exit 1 + fi + # Verify oslo_messaging_notifications section has transport_url configured + kubectl exec -n $NAMESPACE ${APIPOD} -c watcher-api -- cat /etc/watcher/watcher.conf.d/00-default.conf | grep -A 2 '\[oslo_messaging_notifications\]' | grep -q 'transport_url' + echo "Successfully verified notifications transport_url in watcher.conf" + exit 0 diff --git a/test/kuttl/test-suites/default/watcher-notificationsbus/01-deploy.yaml b/test/kuttl/test-suites/default/watcher-notificationsbus/01-deploy.yaml new file mode 100644 index 00000000..e6e989aa --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-notificationsbus/01-deploy.yaml @@ -0,0 +1,18 @@ +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-notificationsbus/02-cleanup-watcher.yaml b/test/kuttl/test-suites/default/watcher-notificationsbus/02-cleanup-watcher.yaml new file mode 100644 index 00000000..72051465 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-notificationsbus/02-cleanup-watcher.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: watcher.openstack.org/v1beta1 + kind: Watcher + name: watcher-kuttl diff --git a/test/kuttl/test-suites/default/watcher-same-cluster/00-cleanup-watcher.yaml b/test/kuttl/test-suites/default/watcher-same-cluster/00-cleanup-watcher.yaml new file mode 100644 index 00000000..72051465 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-same-cluster/00-cleanup-watcher.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: watcher.openstack.org/v1beta1 + kind: Watcher + name: watcher-kuttl diff --git a/test/kuttl/test-suites/default/watcher-same-cluster/01-assert.yaml b/test/kuttl/test-suites/default/watcher-same-cluster/01-assert.yaml new file mode 100644 index 00000000..5433acb5 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-same-cluster/01-assert.yaml @@ -0,0 +1,82 @@ +apiVersion: watcher.openstack.org/v1beta1 +kind: Watcher +metadata: + name: watcher-kuttl + namespace: watcher-kuttl-default +status: + conditions: + - message: Setup complete + reason: Ready + status: "True" + type: Ready + - message: WatcherRabbitMQTransportURL successfully created + reason: Ready + status: "True" + type: WatcherRabbitMQTransportURLReady + - message: WatcherNotificationTransportURL successfully created + reason: Ready + status: "True" + type: WatcherNotificationTransportURLReady +--- +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: watcher-kuttl-watcher-transport +spec: + rabbitmqClusterName: rabbitmq + username: watcher-rpc + vhost: watcher-rpc +status: + conditions: + - message: TransportURL successfully created + reason: Ready + status: "True" + type: Ready +--- +apiVersion: rabbitmq.openstack.org/v1beta1 +kind: TransportURL +metadata: + name: watcher-kuttl-watcher-notification-rabbitmq +spec: + rabbitmqClusterName: rabbitmq + username: watcher-notifications + vhost: watcher-notifications +status: + conditions: + - message: TransportURL successfully created + reason: Ready + status: "True" + type: Ready +--- +# Verify that two TransportURLs were created with different users/vhosts on the same cluster +apiVersion: kuttl.dev/v1beta1 +kind: TestAssert +commands: +- script: | + set -euxo pipefail + # Count TransportURL CRs for watcher - should be exactly 2 (one for RPC, one for notifications) + watcher_transport_count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "watcher-kuttl-watcher-transport" | wc -l) + notification_count=$(kubectl get transporturl -n $NAMESPACE -o name | grep "watcher-kuttl-watcher-notification" | wc -l) + + if [ "$watcher_transport_count" -ne "1" ]; then + echo "Expected 1 watcher-transport TransportURL, found $watcher_transport_count" + exit 1 + fi + + if [ "$notification_count" -ne "1" ]; then + echo "Expected 1 notification TransportURL (different user/vhost on same cluster), found $notification_count" + exit 1 + fi + + echo "Correctly found 2 separate TransportURLs with different users/vhosts on same cluster" + + # Verify that watcher.conf has notifications transport_url configured + APIPOD=$(kubectl get pods -n $NAMESPACE -l "service=watcher-api" -o custom-columns=:metadata.name --no-headers | grep -v ^$ | head -1) + if [ -z "${APIPOD}" ]; then + echo "No watcher-api pod found" + exit 1 + fi + # Verify oslo_messaging_notifications section has transport_url configured + kubectl exec -n $NAMESPACE ${APIPOD} -c watcher-api -- cat /etc/watcher/watcher.conf.d/00-default.conf | grep -A 2 '\[oslo_messaging_notifications\]' | grep -q 'transport_url' + echo "Successfully verified notifications transport_url in watcher.conf (using same cluster with different credentials)" + exit 0 diff --git a/test/kuttl/test-suites/default/watcher-same-cluster/01-deploy.yaml b/test/kuttl/test-suites/default/watcher-same-cluster/01-deploy.yaml new file mode 100644 index 00000000..99487442 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-same-cluster/01-deploy.yaml @@ -0,0 +1,18 @@ +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 + user: watcher-notifications + vhost: watcher-notifications diff --git a/test/kuttl/test-suites/default/watcher-same-cluster/02-cleanup-watcher.yaml b/test/kuttl/test-suites/default/watcher-same-cluster/02-cleanup-watcher.yaml new file mode 100644 index 00000000..72051465 --- /dev/null +++ b/test/kuttl/test-suites/default/watcher-same-cluster/02-cleanup-watcher.yaml @@ -0,0 +1,6 @@ +apiVersion: kuttl.dev/v1beta1 +kind: TestStep +delete: +- apiVersion: watcher.openstack.org/v1beta1 + kind: Watcher + name: watcher-kuttl