Skip to content

Commit 3a68095

Browse files
committed
Rabbitmq vhost and user support
Add new notificationsBus interface 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: notificationsBus: cluster: rabbitmq user: custom-user vhost: custom-vhost Jira: https://issues.redhat.com/browse/OSPRH-23739
1 parent 8d5156c commit 3a68095

File tree

10 files changed

+261
-9
lines changed

10 files changed

+261
-9
lines changed

api/bases/glance.openstack.org_glances.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,23 @@ spec:
16291629
Needed to request a transportURL that is created and used for notification
16301630
purposes
16311631
type: string
1632+
notificationsBus:
1633+
description: NotificationsBus configuration (username, vhost, and
1634+
cluster) for notifications
1635+
properties:
1636+
cluster:
1637+
description: Name of the cluster
1638+
minLength: 1
1639+
type: string
1640+
user:
1641+
description: User - RabbitMQ username
1642+
type: string
1643+
vhost:
1644+
description: Vhost - RabbitMQ vhost name
1645+
type: string
1646+
required:
1647+
- cluster
1648+
type: object
16321649
passwordSelectors:
16331650
default:
16341651
service: GlancePassword

api/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ require (
1717
github.com/cespare/xxhash/v2 v2.3.0 // indirect
1818
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1919
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
20-
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
2120
github.com/evanphx/json-patch/v5 v5.9.11 // indirect
2221
github.com/fsnotify/fsnotify v1.9.0 // indirect
2322
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
@@ -46,6 +45,7 @@ require (
4645
github.com/prometheus/client_model v0.6.2 // indirect
4746
github.com/prometheus/common v0.65.0 // indirect
4847
github.com/prometheus/procfs v0.16.1 // indirect
48+
github.com/rabbitmq/cluster-operator/v2 v2.16.0 // indirect
4949
github.com/spf13/pflag v1.0.7 // indirect
5050
github.com/stretchr/testify v1.11.1 // indirect
5151
github.com/x448/float16 v0.8.4 // indirect

api/go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww=
12
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
23
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
34
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -86,6 +87,8 @@ github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.2025123021
8687
github.com/openstack-k8s-operators/lib-common/modules/common v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:kycZyoe7OZdW1HUghr2nI3N7wSJtNahXf6b/ypD14f4=
8788
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251230215914-6ba873b49a35 h1:8WZYfCt1VJHa5sJRX0UhpmoXud/fn8LHQhXsakdYXuQ=
8889
github.com/openstack-k8s-operators/lib-common/modules/storage v0.6.1-0.20251230215914-6ba873b49a35/go.mod h1:H0aQANk8iJPRhS2Bg9n6cYb/IHF0Cks9g7+uZG04Rhk=
90+
github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec h1:saovr368HPAKHN0aRPh8h8n9s9dn3d8Frmfua0UYRlc=
91+
github.com/openstack-k8s-operators/rabbitmq-cluster-operator/v2 v2.6.1-0.20250929174222-a0d328fa4dec/go.mod h1:Nh2NEePLjovUQof2krTAg4JaAoLacqtPTZQXK6izNfg=
8992
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
9093
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
9194
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=

api/v1beta1/glance_types.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,10 @@ limitations under the License.
1717
package v1beta1
1818

1919
import (
20+
rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1"
21+
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
2022
condition "github.com/openstack-k8s-operators/lib-common/modules/common/condition"
2123
"github.com/openstack-k8s-operators/lib-common/modules/storage"
22-
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
2324
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2425
)
2526

@@ -139,6 +140,10 @@ type GlanceSpecCore struct {
139140
// Needed to request a transportURL that is created and used for notification
140141
// purposes
141142
NotificationBusInstance *string `json:"notificationBusInstance,omitempty"`
143+
144+
// +kubebuilder:validation:Optional
145+
// NotificationsBus configuration (username, vhost, and cluster) for notifications
146+
NotificationsBus *rabbitmqv1.RabbitMqConfig `json:"notificationsBus,omitempty"`
142147
}
143148

144149
// GlanceSpec defines the desired state of Glance

api/v1beta1/glance_webhook.go

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,17 +21,17 @@ import (
2121
"strings"
2222

2323
"github.com/google/go-cmp/cmp"
24+
rabbitmqv1 "github.com/openstack-k8s-operators/infra-operator/apis/rabbitmq/v1beta1"
25+
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
2426
"github.com/openstack-k8s-operators/lib-common/modules/common/service"
27+
common_webhook "github.com/openstack-k8s-operators/lib-common/modules/common/webhook"
2528
apierrors "k8s.io/apimachinery/pkg/api/errors"
2629
"k8s.io/apimachinery/pkg/runtime"
2730
"k8s.io/apimachinery/pkg/runtime/schema"
2831
"k8s.io/apimachinery/pkg/util/validation/field"
2932
logf "sigs.k8s.io/controller-runtime/pkg/log"
3033
"sigs.k8s.io/controller-runtime/pkg/webhook"
3134
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
32-
topologyv1 "github.com/openstack-k8s-operators/infra-operator/apis/topology/v1beta1"
33-
34-
common_webhook "github.com/openstack-k8s-operators/lib-common/modules/common/webhook"
3535
)
3636

3737
// GlanceDefaults -
@@ -111,6 +111,14 @@ func (r *GlanceSpecCore) Default() {
111111
if r.DBPurge.Schedule == "" {
112112
r.DBPurge.Schedule = glanceDefaults.DBPurgeSchedule
113113
}
114+
115+
// Default NotificationsBus if NotificationBusInstance is specified
116+
if r.NotificationBusInstance != nil && *r.NotificationBusInstance != "" {
117+
if r.NotificationsBus == nil {
118+
r.NotificationsBus = &rabbitmqv1.RabbitMqConfig{}
119+
}
120+
rabbitmqv1.DefaultRabbitMqConfig(r.NotificationsBus, *r.NotificationBusInstance)
121+
}
114122
// When no glanceAPI(s) are specified in the top-level CR
115123
// we build one by default, but we set replicas=0 and we
116124
// build a "CustomServiceConfig" template that should be
@@ -352,6 +360,28 @@ func (r *GlanceSpec) ValidateUpdate(old GlanceSpec, basePath *field.Path, namesp
352360
func (r *GlanceSpecCore) ValidateUpdate(old GlanceSpecCore, basePath *field.Path, namespace string) field.ErrorList {
353361
var allErrs field.ErrorList
354362

363+
// Validate deprecated fields and their new equivalents
364+
// Don't allow setting both old and new fields with different values
365+
if r.NotificationBusInstance != nil && *r.NotificationBusInstance != "" &&
366+
r.NotificationsBus != nil && r.NotificationsBus.Cluster != "" &&
367+
*r.NotificationBusInstance != r.NotificationsBus.Cluster {
368+
allErrs = append(allErrs, field.Invalid(
369+
basePath.Child("notificationsBus").Child("cluster"),
370+
r.NotificationsBus.Cluster,
371+
fmt.Sprintf("notificationsBus.cluster cannot differ from deprecated notificationBusInstance (%s). "+
372+
"Either use the new notificationsBus.cluster field or the deprecated notificationBusInstance, but not both with different values",
373+
*r.NotificationBusInstance)))
374+
}
375+
376+
// Reject changes to deprecated NotificationBusInstance field unless nulling it out
377+
if r.NotificationBusInstance != nil && old.NotificationBusInstance != nil &&
378+
*r.NotificationBusInstance != *old.NotificationBusInstance &&
379+
*r.NotificationBusInstance != "" {
380+
allErrs = append(allErrs, field.Forbidden(
381+
basePath.Child("notificationBusInstance"),
382+
"notificationBusInstance is deprecated and cannot be changed. Please use notificationsBus.cluster instead"))
383+
}
384+
355385
// fail if a wrong topology is referenced
356386
allErrs = append(allErrs, topologyv1.ValidateTopologyRef(
357387
r.TopologyRef, *basePath.Child("topologyRef"), namespace)...)

api/v1beta1/zz_generated.deepcopy.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

config/crd/bases/glance.openstack.org_glances.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1629,6 +1629,23 @@ spec:
16291629
Needed to request a transportURL that is created and used for notification
16301630
purposes
16311631
type: string
1632+
notificationsBus:
1633+
description: NotificationsBus configuration (username, vhost, and
1634+
cluster) for notifications
1635+
properties:
1636+
cluster:
1637+
description: Name of the cluster
1638+
minLength: 1
1639+
type: string
1640+
user:
1641+
description: User - RabbitMQ username
1642+
type: string
1643+
vhost:
1644+
description: Vhost - RabbitMQ vhost name
1645+
type: string
1646+
required:
1647+
- cluster
1648+
type: object
16321649
passwordSelectors:
16331650
default:
16341651
service: GlancePassword

internal/controller/glance_controller.go

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,11 @@ func (r *GlanceReconciler) reconcileNormal(ctx context.Context, instance *glance
583583
// create RabbitMQ transportURL CR and get the actual URL from the associated secret that is created
584584
//
585585
if instance.Spec.NotificationBusInstance != nil && *instance.Spec.NotificationBusInstance != "" {
586-
notificationTransportURL, op, err := r.transportURLCreateOrUpdate(ctx, instance, serviceLabels)
586+
notificationsRabbitMqConfig := rabbitmqv1.RabbitMqConfig{}
587+
if instance.Spec.NotificationsBus != nil {
588+
notificationsRabbitMqConfig = *instance.Spec.NotificationsBus
589+
}
590+
notificationTransportURL, op, err := r.transportURLCreateOrUpdate(ctx, instance, serviceLabels, notificationsRabbitMqConfig)
587591
if err != nil {
588592
instance.Status.Conditions.Set(condition.FalseCondition(
589593
condition.NotificationBusInstanceReadyCondition,
@@ -1373,6 +1377,7 @@ func (r *GlanceReconciler) transportURLCreateOrUpdate(
13731377
ctx context.Context,
13741378
instance *glancev1.Glance,
13751379
serviceLabels map[string]string,
1380+
rabbitMqConfig rabbitmqv1.RabbitMqConfig,
13761381
) (*rabbitmqv1.TransportURL, controllerutil.OperationResult, error) {
13771382
transportURL := &rabbitmqv1.TransportURL{
13781383
ObjectMeta: metav1.ObjectMeta{
@@ -1384,9 +1389,12 @@ func (r *GlanceReconciler) transportURLCreateOrUpdate(
13841389

13851390
op, err := controllerutil.CreateOrUpdate(ctx, r.Client, transportURL, func() error {
13861391
transportURL.Spec.RabbitmqClusterName = *instance.Spec.NotificationBusInstance
1387-
1388-
err := controllerutil.SetControllerReference(instance, transportURL, r.Scheme)
1389-
return err
1392+
if rabbitMqConfig.User != "" {
1393+
transportURL.Spec.Username = rabbitMqConfig.User
1394+
}
1395+
// Always set Vhost - empty string means use default "/" vhost
1396+
transportURL.Spec.Vhost = rabbitMqConfig.Vhost
1397+
return controllerutil.SetControllerReference(instance, transportURL, r.Scheme)
13901398
})
13911399

13921400
return transportURL, op, err

test/functional/glance_controller_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,91 @@ var _ = Describe("Glance controller", func() {
812812
})
813813
})
814814

815+
When("Glance is created with RabbitMQ user and vhost", func() {
816+
BeforeEach(func() {
817+
DeferCleanup(k8sClient.Delete, ctx, CreateGlanceMessageBusSecret(glanceTest.Instance.Namespace, glanceTest.RabbitmqSecretName))
818+
DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, glanceTest.MemcachedInstance, memcachedSpec))
819+
infra.SimulateMemcachedReady(glanceTest.GlanceMemcached)
820+
spec := GetGlanceDefaultSpec()
821+
spec["notificationBusInstance"] = glanceTest.NotificationsBusInstance
822+
spec["notificationsBus"] = map[string]interface{}{
823+
"user": "glance-user",
824+
"vhost": "glance-vhost",
825+
}
826+
DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, spec))
827+
})
828+
It("sets custom RabbitMQ user and vhost in TransportURL", func() {
829+
Eventually(func(g Gomega) {
830+
transportURL := infra.GetTransportURL(glanceTest.GlanceTransportURL)
831+
g.Expect(transportURL.Spec.Username).To(Equal("glance-user"))
832+
g.Expect(transportURL.Spec.Vhost).To(Equal("glance-vhost"))
833+
}, timeout, interval).Should(Succeed())
834+
})
835+
})
836+
837+
When("Glance is created without custom RabbitMQ config", func() {
838+
BeforeEach(func() {
839+
DeferCleanup(k8sClient.Delete, ctx, CreateGlanceMessageBusSecret(glanceTest.Instance.Namespace, glanceTest.RabbitmqSecretName))
840+
DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, glanceTest.MemcachedInstance, memcachedSpec))
841+
infra.SimulateMemcachedReady(glanceTest.GlanceMemcached)
842+
spec := GetGlanceDefaultSpec()
843+
spec["notificationBusInstance"] = glanceTest.NotificationsBusInstance
844+
DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, spec))
845+
})
846+
It("uses default RabbitMQ configuration in TransportURL", func() {
847+
Eventually(func(g Gomega) {
848+
transportURL := infra.GetTransportURL(glanceTest.GlanceTransportURL)
849+
g.Expect(transportURL.Spec.Username).To(BeEmpty())
850+
g.Expect(transportURL.Spec.Vhost).To(BeEmpty())
851+
}, timeout, interval).Should(Succeed())
852+
})
853+
})
854+
855+
When("Glance starts with notifications enabled and then disables them", func() {
856+
BeforeEach(func() {
857+
DeferCleanup(k8sClient.Delete, ctx, CreateGlanceMessageBusSecret(glanceTest.Instance.Namespace, glanceTest.RabbitmqSecretName))
858+
DeferCleanup(infra.DeleteMemcached, infra.CreateMemcached(namespace, glanceTest.MemcachedInstance, memcachedSpec))
859+
infra.SimulateMemcachedReady(glanceTest.GlanceMemcached)
860+
spec := GetGlanceDefaultSpec()
861+
spec["notificationBusInstance"] = glanceTest.NotificationsBusInstance
862+
spec["notificationsBus"] = map[string]interface{}{
863+
"user": "glance-notifications",
864+
"vhost": "glance-notifications-vhost",
865+
}
866+
DeferCleanup(th.DeleteInstance, CreateGlance(glanceTest.Instance, spec))
867+
infra.SimulateTransportURLReady(glanceTest.GlanceTransportURL)
868+
})
869+
870+
It("should initially have notifications enabled", func() {
871+
Eventually(func(g Gomega) {
872+
glance := GetGlance(glanceTest.Instance)
873+
g.Expect(glance.Status.NotificationBusSecret).ToNot(BeEmpty())
874+
}, timeout, interval).Should(Succeed())
875+
})
876+
877+
It("should disable notifications when notificationBusInstance and notificationsBus are removed", func() {
878+
// Verify notifications are initially enabled
879+
Eventually(func(g Gomega) {
880+
glance := GetGlance(glanceTest.Instance)
881+
g.Expect(glance.Status.NotificationBusSecret).ToNot(BeEmpty())
882+
}, timeout, interval).Should(Succeed())
883+
884+
// Update the Glance spec to remove notifications
885+
Eventually(func(g Gomega) {
886+
glance := GetGlance(glanceTest.Instance)
887+
glance.Spec.NotificationBusInstance = nil
888+
glance.Spec.NotificationsBus = nil
889+
g.Expect(k8sClient.Update(ctx, glance)).To(Succeed())
890+
}, timeout, interval).Should(Succeed())
891+
892+
// Wait for notifications to be disabled
893+
Eventually(func(g Gomega) {
894+
glance := GetGlance(glanceTest.Instance)
895+
g.Expect(glance.Status.NotificationBusSecret).To(BeEmpty())
896+
}, timeout, interval).Should(Succeed())
897+
})
898+
})
899+
815900
// Run MariaDBAccount suite tests. these are pre-packaged ginkgo tests
816901
// that exercise standard account create / update patterns that should be
817902
// common to all controllers that ensure MariaDBAccount CRs.
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
Copyright 2025.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package functional
18+
19+
import (
20+
"errors"
21+
22+
. "github.com/onsi/ginkgo/v2" //revive:disable:dot-imports
23+
. "github.com/onsi/gomega" //revive:disable:dot-imports
24+
25+
k8s_errors "k8s.io/apimachinery/pkg/api/errors"
26+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
27+
"k8s.io/apimachinery/pkg/types"
28+
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
29+
)
30+
31+
var _ = Describe("Glance webhook", func() {
32+
It("rejects update to deprecated notificationBusInstance field", func() {
33+
spec := GetDefaultGlanceSpec()
34+
notificationBusInstance := "notifications-rabbitmq"
35+
spec["notificationBusInstance"] = notificationBusInstance
36+
37+
// Set replicas to 0 to skip backend validation since this test
38+
// is focused on testing notificationBusInstance field validation
39+
glanceAPIs := spec["glanceAPIs"].(map[string]any)
40+
defaultAPI := glanceAPIs["default"].(map[string]any)
41+
defaultAPI["replicas"] = 0
42+
43+
glanceName := types.NamespacedName{
44+
Namespace: namespace,
45+
Name: "glance-webhook-test",
46+
}
47+
48+
raw := map[string]any{
49+
"apiVersion": "glance.openstack.org/v1beta1",
50+
"kind": "Glance",
51+
"metadata": map[string]any{
52+
"name": glanceName.Name,
53+
"namespace": glanceName.Namespace,
54+
},
55+
"spec": spec,
56+
}
57+
58+
// Create the Glance instance
59+
unstructuredObj := &unstructured.Unstructured{Object: raw}
60+
_, err := controllerutil.CreateOrPatch(
61+
ctx, k8sClient, unstructuredObj, func() error { return nil })
62+
Expect(err).ShouldNot(HaveOccurred())
63+
64+
// Try to update notificationBusInstance
65+
Eventually(func(g Gomega) {
66+
g.Expect(k8sClient.Get(ctx, glanceName, unstructuredObj)).Should(Succeed())
67+
specMap := unstructuredObj.Object["spec"].(map[string]any)
68+
specMap["notificationBusInstance"] = "notifications-rabbitmq2"
69+
err := k8sClient.Update(ctx, unstructuredObj)
70+
g.Expect(err).Should(HaveOccurred())
71+
72+
var statusError *k8s_errors.StatusError
73+
g.Expect(errors.As(err, &statusError)).To(BeTrue())
74+
g.Expect(statusError.ErrStatus.Details.Kind).To(Equal("Glance"))
75+
g.Expect(statusError.ErrStatus.Message).To(
76+
ContainSubstring("notificationBusInstance is deprecated and cannot be changed"))
77+
g.Expect(statusError.ErrStatus.Message).To(
78+
ContainSubstring("Please use notificationsBus.cluster instead"))
79+
}, timeout, interval).Should(Succeed())
80+
})
81+
})

0 commit comments

Comments
 (0)