Skip to content

Commit b8b67df

Browse files
authored
Support RabbitMQ operator policies (#752)
* Bump Golang Use Golang 1.21 locally on the dev machine and bump Golang to 1.21 in Dockerfile make manifests * Support RabbitMQ operator policies Closes #202 * Add workaround for Go 1.21 since codeql does not (yet) support Go 1.21, see github/codeql-action#1842 * Fix test flake * Add missing test * Apply feedback
1 parent 2f527d4 commit b8b67df

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+2738
-432
lines changed

.github/workflows/build-test-publish.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ on:
1212
tags: [ "v*" ]
1313

1414
env:
15-
GO_VERSION: '1.20.x' # Require Go 1.20 minor
15+
GO_VERSION: '1.21.x' # Require Go 1.21 minor
1616

1717
jobs:
1818
unit_integration_tests:

.github/workflows/codeql-analysis.yml

+7
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ jobs:
3737
- name: Checkout repository
3838
uses: actions/checkout@v4
3939

40+
# Manually install the right version of Go
41+
# See https://github.com/github/codeql-action/issues/1842 and https://github.com/github/codeql/issues/13992
42+
- name: Install Go
43+
uses: actions/setup-go@v5
44+
with:
45+
go-version-file: go.mod
46+
4047
# Initializes the CodeQL tools for scanning.
4148
- name: Initialize CodeQL
4249
uses: github/codeql-action/init@v3

.github/workflows/pr.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
branches: [ main ]
66

77
env:
8-
GO_VERSION: '1.20.x' # Require Go 1.20.x
8+
GO_VERSION: '1.21.x' # Require Go 1.21.x
99

1010
jobs:
1111

Dockerfile

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Build the manager binary
2-
FROM --platform=$BUILDPLATFORM golang:1.20 as builder
2+
FROM --platform=$BUILDPLATFORM golang:1.21 as builder
33

44
WORKDIR /workspace
55
# Copy the Go Modules manifests

api/v1beta1/operatorpolicy_types.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package v1beta1
2+
3+
import (
4+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
5+
"k8s.io/apimachinery/pkg/runtime"
6+
"k8s.io/apimachinery/pkg/runtime/schema"
7+
)
8+
9+
// OperatorPolicySpec defines the desired state of OperatorPolicy
10+
// https://www.rabbitmq.com/parameters.html#operator-policies
11+
type OperatorPolicySpec struct {
12+
// Required property; cannot be updated
13+
// +kubebuilder:validation:Required
14+
Name string `json:"name"`
15+
// Default to vhost '/'; cannot be updated
16+
// +kubebuilder:default:=/
17+
Vhost string `json:"vhost,omitempty"`
18+
// Regular expression pattern used to match queues, e.g. "^my-queue$".
19+
// Required property.
20+
// +kubebuilder:validation:Required
21+
Pattern string `json:"pattern"`
22+
// What this operator policy applies to: 'queues', 'classic_queues', 'quorum_queues', 'streams'.
23+
// Default to 'queues'.
24+
// +kubebuilder:validation:Enum=queues;classic_queues;quorum_queues;streams
25+
// +kubebuilder:default:=queues
26+
ApplyTo string `json:"applyTo,omitempty"`
27+
// Default to '0'.
28+
// In the event that more than one operator policy can match a given queue, the operator policy with the greatest priority applies.
29+
// +kubebuilder:default:=0
30+
Priority int `json:"priority,omitempty"`
31+
// OperatorPolicy definition. Required property.
32+
// +kubebuilder:validation:Type=object
33+
// +kubebuilder:pruning:PreserveUnknownFields
34+
// +kubebuilder:validation:Required
35+
Definition *runtime.RawExtension `json:"definition"`
36+
// Reference to the RabbitmqCluster that the operator policy will be created in.
37+
// Required property.
38+
// +kubebuilder:validation:Required
39+
RabbitmqClusterReference RabbitmqClusterReference `json:"rabbitmqClusterReference"`
40+
}
41+
42+
// OperatorPolicyStatus defines the observed state of OperatorPolicy
43+
type OperatorPolicyStatus struct {
44+
// observedGeneration is the most recent successful generation observed for this OperatorPolicy. It corresponds to the
45+
// OperatorPolicy's generation, which is updated on mutation by the API Server.
46+
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
47+
Conditions []Condition `json:"conditions,omitempty"`
48+
}
49+
50+
// +genclient
51+
// +kubebuilder:object:root=true
52+
// +kubebuilder:resource:categories=all;rabbitmq
53+
// +kubebuilder:subresource:status
54+
55+
// OperatorPolicy is the Schema for the operator policies API
56+
type OperatorPolicy struct {
57+
metav1.TypeMeta `json:",inline"`
58+
metav1.ObjectMeta `json:"metadata,omitempty"`
59+
60+
Spec OperatorPolicySpec `json:"spec,omitempty"`
61+
Status OperatorPolicyStatus `json:"status,omitempty"`
62+
}
63+
64+
// +kubebuilder:object:root=true
65+
66+
// OperatorPolicyList contains a list of OperatorPolicy
67+
type OperatorPolicyList struct {
68+
metav1.TypeMeta `json:",inline"`
69+
metav1.ListMeta `json:"metadata,omitempty"`
70+
Items []OperatorPolicy `json:"items"`
71+
}
72+
73+
func (p *OperatorPolicy) GroupResource() schema.GroupResource {
74+
return schema.GroupResource{
75+
Group: p.GroupVersionKind().Group,
76+
Resource: p.GroupVersionKind().Kind,
77+
}
78+
}
79+
80+
func (p *OperatorPolicy) RabbitReference() RabbitmqClusterReference {
81+
return p.Spec.RabbitmqClusterReference
82+
}
83+
84+
func (p *OperatorPolicy) SetStatusConditions(c []Condition) {
85+
p.Status.Conditions = c
86+
}
87+
88+
func init() {
89+
SchemeBuilder.Register(&OperatorPolicy{}, &OperatorPolicyList{})
90+
}
+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package v1beta1
2+
3+
import (
4+
"context"
5+
6+
"k8s.io/apimachinery/pkg/runtime"
7+
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11+
"k8s.io/apimachinery/pkg/types"
12+
)
13+
14+
var _ = Describe("OperatorPolicy", func() {
15+
var (
16+
namespace = "default"
17+
ctx = context.Background()
18+
)
19+
20+
It("creates an operator policy with minimal configurations", func() {
21+
policy := OperatorPolicy{
22+
ObjectMeta: metav1.ObjectMeta{
23+
Name: "test-operator-policy",
24+
Namespace: namespace,
25+
},
26+
Spec: OperatorPolicySpec{
27+
Name: "test-operator-policy",
28+
Pattern: "^some-prefix",
29+
Definition: &runtime.RawExtension{
30+
Raw: []byte(`{"max-length": 10}`),
31+
},
32+
RabbitmqClusterReference: RabbitmqClusterReference{
33+
Name: "some-cluster",
34+
},
35+
},
36+
}
37+
Expect(k8sClient.Create(ctx, &policy)).To(Succeed())
38+
fetched := &OperatorPolicy{}
39+
Expect(k8sClient.Get(ctx, types.NamespacedName{
40+
Name: policy.Name,
41+
Namespace: policy.Namespace,
42+
}, fetched)).To(Succeed())
43+
Expect(fetched.Spec.RabbitmqClusterReference).To(Equal(RabbitmqClusterReference{
44+
Name: "some-cluster",
45+
}))
46+
Expect(fetched.Spec.Name).To(Equal("test-operator-policy"))
47+
Expect(fetched.Spec.Vhost).To(Equal("/"))
48+
Expect(fetched.Spec.Pattern).To(Equal("^some-prefix"))
49+
Expect(fetched.Spec.ApplyTo).To(Equal("queues"))
50+
Expect(fetched.Spec.Priority).To(Equal(0))
51+
Expect(fetched.Spec.Definition.Raw).To(Equal([]byte(`{"max-length":10}`)))
52+
})
53+
54+
It("creates operator policy with configurations", func() {
55+
policy := OperatorPolicy{
56+
ObjectMeta: metav1.ObjectMeta{
57+
Name: "random-policy",
58+
Namespace: namespace,
59+
},
60+
Spec: OperatorPolicySpec{
61+
Name: "test-policy",
62+
Vhost: "/hello",
63+
Pattern: "*.",
64+
ApplyTo: "quorum_queues",
65+
Priority: 100,
66+
Definition: &runtime.RawExtension{
67+
Raw: []byte(`{"max-length":10}`),
68+
},
69+
RabbitmqClusterReference: RabbitmqClusterReference{
70+
Name: "random-cluster",
71+
},
72+
},
73+
}
74+
Expect(k8sClient.Create(ctx, &policy)).To(Succeed())
75+
fetched := &OperatorPolicy{}
76+
Expect(k8sClient.Get(ctx, types.NamespacedName{
77+
Name: policy.Name,
78+
Namespace: policy.Namespace,
79+
}, fetched)).To(Succeed())
80+
81+
Expect(fetched.Spec.Name).To(Equal("test-policy"))
82+
Expect(fetched.Spec.Vhost).To(Equal("/hello"))
83+
Expect(fetched.Spec.Pattern).To(Equal("*."))
84+
Expect(fetched.Spec.ApplyTo).To(Equal("quorum_queues"))
85+
Expect(fetched.Spec.Priority).To(Equal(100))
86+
Expect(fetched.Spec.RabbitmqClusterReference).To(Equal(
87+
RabbitmqClusterReference{
88+
Name: "random-cluster",
89+
}))
90+
Expect(fetched.Spec.Definition.Raw).To(Equal([]byte(`{"max-length":10}`)))
91+
})
92+
93+
When("creating a policy with an invalid 'ApplyTo' value", func() {
94+
It("fails with validation errors", func() {
95+
policy := OperatorPolicy{
96+
ObjectMeta: metav1.ObjectMeta{
97+
Name: "invalid",
98+
Namespace: namespace,
99+
},
100+
Spec: OperatorPolicySpec{
101+
Name: "test-policy",
102+
Pattern: "a-queue-name",
103+
Definition: &runtime.RawExtension{
104+
Raw: []byte(`{"max-length":10}`),
105+
},
106+
ApplyTo: "yo-yo",
107+
RabbitmqClusterReference: RabbitmqClusterReference{
108+
Name: "some-cluster",
109+
},
110+
},
111+
}
112+
Expect(k8sClient.Create(ctx, &policy)).To(HaveOccurred())
113+
Expect(k8sClient.Create(ctx, &policy)).To(MatchError(`OperatorPolicy.rabbitmq.com "invalid" is invalid: spec.applyTo: Unsupported value: "yo-yo": supported values: "queues", "classic_queues", "quorum_queues", "streams"`))
114+
})
115+
})
116+
117+
})

api/v1beta1/operatorpolicy_webhook.go

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package v1beta1
2+
3+
import (
4+
"fmt"
5+
6+
apierrors "k8s.io/apimachinery/pkg/api/errors"
7+
"k8s.io/apimachinery/pkg/runtime"
8+
"k8s.io/apimachinery/pkg/util/validation/field"
9+
ctrl "sigs.k8s.io/controller-runtime"
10+
"sigs.k8s.io/controller-runtime/pkg/webhook"
11+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
12+
)
13+
14+
func (p *OperatorPolicy) SetupWebhookWithManager(mgr ctrl.Manager) error {
15+
return ctrl.NewWebhookManagedBy(mgr).
16+
For(p).
17+
Complete()
18+
}
19+
20+
// +kubebuilder:webhook:verbs=create;update,path=/validate-rabbitmq-com-v1beta1-operatorpolicy,mutating=false,failurePolicy=fail,groups=rabbitmq.com,resources=operatorpolicies,versions=v1beta1,name=voperatorpolicy.kb.io,sideEffects=none,admissionReviewVersions=v1
21+
22+
var _ webhook.Validator = &OperatorPolicy{}
23+
24+
// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
25+
// either rabbitmqClusterReference.name or rabbitmqClusterReference.connectionSecret must be provided but not both
26+
func (p *OperatorPolicy) ValidateCreate() (admission.Warnings, error) {
27+
return p.Spec.RabbitmqClusterReference.ValidateOnCreate(p.GroupResource(), p.Name)
28+
}
29+
30+
// ValidateUpdate returns error type 'forbidden' for updates on operator policy name, vhost and rabbitmqClusterReference
31+
func (p *OperatorPolicy) ValidateUpdate(old runtime.Object) (admission.Warnings, error) {
32+
oldOperatorPolicy, ok := old.(*OperatorPolicy)
33+
if !ok {
34+
return nil, apierrors.NewBadRequest(fmt.Sprintf("expected an operator policy but got a %T", old))
35+
}
36+
37+
detailMsg := "updates on name, vhost and rabbitmqClusterReference are all forbidden"
38+
if p.Spec.Name != oldOperatorPolicy.Spec.Name {
39+
return nil, apierrors.NewForbidden(p.GroupResource(), p.Name,
40+
field.Forbidden(field.NewPath("spec", "name"), detailMsg))
41+
}
42+
43+
if p.Spec.Vhost != oldOperatorPolicy.Spec.Vhost {
44+
return nil, apierrors.NewForbidden(p.GroupResource(), p.Name,
45+
field.Forbidden(field.NewPath("spec", "vhost"), detailMsg))
46+
}
47+
48+
if !oldOperatorPolicy.Spec.RabbitmqClusterReference.Matches(&p.Spec.RabbitmqClusterReference) {
49+
return nil, apierrors.NewForbidden(p.GroupResource(), p.Name,
50+
field.Forbidden(field.NewPath("spec", "rabbitmqClusterReference"), detailMsg))
51+
}
52+
return nil, nil
53+
}
54+
55+
func (p *OperatorPolicy) ValidateDelete() (admission.Warnings, error) {
56+
return nil, nil
57+
}

0 commit comments

Comments
 (0)