Skip to content

Commit e4e4271

Browse files
ansdMarcialRosales
andauthored
Support Vault via vault-k8s (#846)
Co-authored-by: Marcial Rosales <[email protected]>
1 parent 82cfd8b commit e4e4271

24 files changed

+1240
-229
lines changed

api/v1beta1/rabbitmqcluster_types.go

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
package v1beta1
1010

1111
import (
12+
"fmt"
1213
"strconv"
1314
"strings"
1415

@@ -84,6 +85,70 @@ type RabbitmqClusterSpec struct {
8485
// +kubebuilder:validation:Minimum:=0
8586
// +kubebuilder:default:=604800
8687
TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"`
88+
// Secret backend configuration for the RabbitmqCluster.
89+
// Enables to fetch default user credentials and certificates from K8s external secret stores.
90+
SecretBackend SecretBackend `json:"secretBackend,omitempty"`
91+
}
92+
93+
// SecretBackend configures a single secret backend.
94+
// Today, only Vault exists as supported secret backend.
95+
// Future secret backends could be Secrets Store CSI Driver.
96+
// If not configured, K8s Secrets will be used.
97+
type SecretBackend struct {
98+
Vault *VaultSpec `json:"vault,omitempty"`
99+
}
100+
101+
// VaultSpec will add Vault annotations (see https://www.vaultproject.io/docs/platform/k8s/injector/annotations)
102+
// to RabbitMQ Pods. It requires a Vault Agent Sidecar Injector (https://www.vaultproject.io/docs/platform/k8s/injector)
103+
// to be installed in the K8s cluster. The injector is a K8s Mutation Webhook Controller that alters RabbitMQ Pod specifications
104+
// (based on the added Vault annotations) to include Vault Agent containers that render Vault secrets to the volume.
105+
type VaultSpec struct {
106+
// Role in Vault.
107+
// If vault.defaultUserPath is set, this role must have capability to read the pre-created default user credential in Vault.
108+
// If vault.tls is set, this role must have capability to create and update certificates in the Vault PKI engine for the domains
109+
// "<namespace>" and "<namespace>.svc".
110+
Role string `json:"role,omitempty"`
111+
// Vault annotations that override the Vault annotations set by the cluster-operator.
112+
// For a list of valid Vault annotations, see https://www.vaultproject.io/docs/platform/k8s/injector/annotations
113+
// +optional
114+
Annotations map[string]string `json:"annotations,omitempty"`
115+
// Path in Vault to access a KV (Key-Value) secret with the fields username and password for the default user.
116+
// For example "secret/data/rabbitmq/config".
117+
DefaultUserPath string `json:"defaultUserPath,omitempty"`
118+
// Sidecar container that updates the default user's password in RabbitMQ when it changes in Vault.
119+
// Additionally, it updates /var/lib/rabbitmq/.rabbitmqadmin.conf (used by rabbitmqadmin CLI).
120+
// Set to empty string to disable the sidecar container.
121+
// +kubebuilder:default:="rabbitmqoperator/default-user-credential-updater:0.1.1"
122+
DefaultUserUpdaterImage *string `json:"defaultUserUpdaterImage,omitempty"`
123+
TLS VaultTLSSpec `json:"tls,omitempty"`
124+
}
125+
126+
type VaultTLSSpec struct {
127+
// Path in Vault PKI engine.
128+
// For example "pki/issue/hashicorp-com".
129+
// required
130+
PKIIssuerPath string `json:"pkiIssuerPath,omitempty"`
131+
// Specifies the requested certificate Common Name (CN).
132+
// Defaults to <serviceName>.<namespace>.svc if not provided.
133+
// +optional
134+
CommonName string `json:"commonName,omitempty"`
135+
// Specifies the requested Subject Alternative Names (SANs), in a comma-delimited list.
136+
// These will be appended to the SANs added by the cluster-operator.
137+
// The cluster-operator will add SANs:
138+
// "<RabbitmqCluster name>-server-<index>.<RabbitmqCluster name>-nodes.<namespace>" for each pod,
139+
// e.g. "myrabbit-server-0.myrabbit-nodes.default".
140+
// +optional
141+
AltNames string `json:"altNames,omitempty"`
142+
// Specifies the requested IP Subject Alternative Names, in a comma-delimited list.
143+
// +optional
144+
IpSans string `json:"ipSans,omitempty"`
145+
}
146+
147+
func (spec *VaultSpec) TLSEnabled() bool {
148+
return spec.TLS.PKIIssuerPath != ""
149+
}
150+
func (spec *VaultSpec) DefaultUserSecretEnabled() bool {
151+
return spec.DefaultUserPath != ""
87152
}
88153

89154
// Provides the ability to override the generated manifest of several child resources.
@@ -323,11 +388,14 @@ type RabbitmqClusterServiceSpec struct {
323388
}
324389

325390
func (cluster *RabbitmqCluster) TLSEnabled() bool {
391+
return cluster.SecretTLSEnabled() || cluster.VaultTLSEnabled()
392+
}
393+
func (cluster *RabbitmqCluster) SecretTLSEnabled() bool {
326394
return cluster.Spec.TLS.SecretName != ""
327395
}
328396

329397
func (cluster *RabbitmqCluster) MutualTLSEnabled() bool {
330-
return cluster.TLSEnabled() && cluster.Spec.TLS.CaSecretName != ""
398+
return (cluster.SecretTLSEnabled() && cluster.Spec.TLS.CaSecretName != "") || cluster.VaultTLSEnabled()
331399
}
332400

333401
func (cluster *RabbitmqCluster) MemoryLimited() bool {
@@ -356,6 +424,22 @@ func (cluster *RabbitmqCluster) StreamNeeded() bool {
356424
return cluster.AdditionalPluginEnabled("rabbitmq_stream") || cluster.AdditionalPluginEnabled("rabbitmq_multi_dc_replication")
357425
}
358426

427+
func (cluster *RabbitmqCluster) VaultEnabled() bool {
428+
return cluster.Spec.SecretBackend.Vault != nil
429+
}
430+
431+
func (cluster *RabbitmqCluster) VaultDefaultUserSecretEnabled() bool {
432+
return cluster.VaultEnabled() && cluster.Spec.SecretBackend.Vault.DefaultUserSecretEnabled()
433+
}
434+
435+
func (cluster *RabbitmqCluster) VaultTLSEnabled() bool {
436+
return cluster.VaultEnabled() && cluster.Spec.SecretBackend.Vault.TLSEnabled()
437+
}
438+
439+
func (cluster *RabbitmqCluster) ServiceSubDomain() string {
440+
return fmt.Sprintf("%s.%s.svc", cluster.Name, cluster.Namespace)
441+
}
442+
359443
// +kubebuilder:object:root=true
360444

361445
// RabbitmqClusterList contains a list of RabbitmqClusters.

api/v1beta1/rabbitmqcluster_types_test.go

Lines changed: 86 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ package v1beta1
1111
import (
1212
. "github.com/onsi/ginkgo"
1313
. "github.com/onsi/gomega"
14+
. "github.com/onsi/gomega/gstruct"
1415
"github.com/rabbitmq/cluster-operator/internal/status"
1516
appsv1 "k8s.io/api/apps/v1"
1617
corev1 "k8s.io/api/core/v1"
@@ -31,39 +32,39 @@ var _ = Describe("RabbitmqCluster", func() {
3132
It("can be created with a single replica", func() {
3233
created := generateRabbitmqClusterObject("rabbit1")
3334
created.Spec.Replicas = pointer.Int32Ptr(1)
34-
Expect(k8sClient.Create(context.TODO(), created)).To(Succeed())
35+
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())
3536

3637
fetched := &RabbitmqCluster{}
37-
Expect(k8sClient.Get(context.TODO(), getKey(created), fetched)).To(Succeed())
38+
Expect(k8sClient.Get(context.Background(), getKey(created), fetched)).To(Succeed())
3839
Expect(fetched).To(Equal(created))
3940
})
4041

4142
It("can be created with three replicas", func() {
4243
created := generateRabbitmqClusterObject("rabbit2")
4344
created.Spec.Replicas = pointer.Int32Ptr(3)
44-
Expect(k8sClient.Create(context.TODO(), created)).To(Succeed())
45+
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())
4546

4647
fetched := &RabbitmqCluster{}
47-
Expect(k8sClient.Get(context.TODO(), getKey(created), fetched)).To(Succeed())
48+
Expect(k8sClient.Get(context.Background(), getKey(created), fetched)).To(Succeed())
4849
Expect(fetched).To(Equal(created))
4950
})
5051

5152
It("can be created with five replicas", func() {
5253
created := generateRabbitmqClusterObject("rabbit3")
5354
created.Spec.Replicas = pointer.Int32Ptr(5)
54-
Expect(k8sClient.Create(context.TODO(), created)).To(Succeed())
55+
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())
5556

5657
fetched := &RabbitmqCluster{}
57-
Expect(k8sClient.Get(context.TODO(), getKey(created), fetched)).To(Succeed())
58+
Expect(k8sClient.Get(context.Background(), getKey(created), fetched)).To(Succeed())
5859
Expect(fetched).To(Equal(created))
5960
})
6061

6162
It("can be deleted", func() {
6263
created := generateRabbitmqClusterObject("rabbit4")
63-
Expect(k8sClient.Create(context.TODO(), created)).To(Succeed())
64+
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())
6465

65-
Expect(k8sClient.Delete(context.TODO(), created)).To(Succeed())
66-
Expect(k8sClient.Get(context.TODO(), getKey(created), created)).ToNot(Succeed())
66+
Expect(k8sClient.Delete(context.Background(), created)).To(Succeed())
67+
Expect(k8sClient.Get(context.Background(), getKey(created), created)).ToNot(Succeed())
6768
})
6869

6970
It("can be created with resource requests", func() {
@@ -78,13 +79,13 @@ var _ = Describe("RabbitmqCluster", func() {
7879
corev1.ResourceMemory: k8sresource.MustParse("100Mi"),
7980
},
8081
}
81-
Expect(k8sClient.Create(context.TODO(), created)).To(Succeed())
82+
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())
8283
})
8384

8485
It("can be created with server side TLS", func() {
8586
created := generateRabbitmqClusterObject("rabbit-tls")
8687
created.Spec.TLS.SecretName = "tls-secret-name"
87-
Expect(k8sClient.Create(context.TODO(), created)).To(Succeed())
88+
Expect(k8sClient.Create(context.Background(), created)).To(Succeed())
8889
})
8990

9091
It("can be queried if TLS is enabled", func() {
@@ -125,15 +126,15 @@ var _ = Describe("RabbitmqCluster", func() {
125126
By("checking the replica count", func() {
126127
invalidReplica := generateRabbitmqClusterObject("rabbit4")
127128
invalidReplica.Spec.Replicas = pointer.Int32Ptr(-1)
128-
Expect(apierrors.IsInvalid(k8sClient.Create(context.TODO(), invalidReplica))).To(BeTrue())
129-
Expect(k8sClient.Create(context.TODO(), invalidReplica)).To(MatchError(ContainSubstring("spec.replicas in body should be greater than or equal to 0")))
129+
Expect(apierrors.IsInvalid(k8sClient.Create(context.Background(), invalidReplica))).To(BeTrue())
130+
Expect(k8sClient.Create(context.Background(), invalidReplica)).To(MatchError(ContainSubstring("spec.replicas in body should be greater than or equal to 0")))
130131
})
131132

132133
By("checking the service type", func() {
133134
invalidService := generateRabbitmqClusterObject("rabbit5")
134135
invalidService.Spec.Service.Type = "ihateservices"
135-
Expect(apierrors.IsInvalid(k8sClient.Create(context.TODO(), invalidService))).To(BeTrue())
136-
Expect(k8sClient.Create(context.TODO(), invalidService)).To(MatchError(ContainSubstring("supported values: \"ClusterIP\", \"LoadBalancer\", \"NodePort\"")))
136+
Expect(apierrors.IsInvalid(k8sClient.Create(context.Background(), invalidService))).To(BeTrue())
137+
Expect(k8sClient.Create(context.Background(), invalidService)).To(MatchError(ContainSubstring("supported values: \"ClusterIP\", \"LoadBalancer\", \"NodePort\"")))
137138
})
138139
})
139140

@@ -163,12 +164,9 @@ var _ = Describe("RabbitmqCluster", func() {
163164
},
164165
}
165166

166-
Expect(k8sClient.Create(context.TODO(), &rmqClusterInstance)).To(Succeed())
167+
Expect(k8sClient.Create(context.Background(), &rmqClusterInstance)).To(Succeed())
167168
fetchedRabbit := &RabbitmqCluster{}
168-
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{
169-
Name: "rabbitmq-defaults",
170-
Namespace: "default",
171-
}, fetchedRabbit)).To(Succeed())
169+
Expect(k8sClient.Get(context.Background(), getKey(&rmqClusterInstance), fetchedRabbit)).To(Succeed())
172170
Expect(fetchedRabbit.Spec).To(Equal(expectedClusterInstance.Spec))
173171
})
174172
})
@@ -241,12 +239,9 @@ var _ = Describe("RabbitmqCluster", func() {
241239
},
242240
}
243241

244-
Expect(k8sClient.Create(context.TODO(), &rmqClusterInstance)).To(Succeed())
242+
Expect(k8sClient.Create(context.Background(), &rmqClusterInstance)).To(Succeed())
245243
fetchedRabbit := &RabbitmqCluster{}
246-
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{
247-
Name: "rabbitmq-full-manifest",
248-
Namespace: "default",
249-
}, fetchedRabbit)).To(Succeed())
244+
Expect(k8sClient.Get(context.Background(), getKey(&rmqClusterInstance), fetchedRabbit)).To(Succeed())
250245
Expect(fetchedRabbit.Spec).To(Equal(rmqClusterInstance.Spec))
251246
})
252247
})
@@ -266,12 +261,9 @@ var _ = Describe("RabbitmqCluster", func() {
266261

267262
expectedClusterInstance.Spec.Image = "test-image"
268263

269-
Expect(k8sClient.Create(context.TODO(), &rmqClusterInstance)).To(Succeed())
264+
Expect(k8sClient.Create(context.Background(), &rmqClusterInstance)).To(Succeed())
270265
fetchedRabbit := &RabbitmqCluster{}
271-
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{
272-
Name: "rabbitmq-image",
273-
Namespace: "default",
274-
}, fetchedRabbit)).To(Succeed())
266+
Expect(k8sClient.Get(context.Background(), getKey(&rmqClusterInstance), fetchedRabbit)).To(Succeed())
275267
Expect(fetchedRabbit.Spec).To(Equal(expectedClusterInstance.Spec))
276268
})
277269

@@ -289,12 +281,9 @@ var _ = Describe("RabbitmqCluster", func() {
289281

290282
expectedClusterInstance.Spec.Resources = expectedResources
291283

292-
Expect(k8sClient.Create(context.TODO(), &rmqClusterInstance)).To(Succeed())
284+
Expect(k8sClient.Create(context.Background(), &rmqClusterInstance)).To(Succeed())
293285
fetchedRabbit := &RabbitmqCluster{}
294-
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{
295-
Name: "rabbitmq-empty-resource",
296-
Namespace: "default",
297-
}, fetchedRabbit)).To(Succeed())
286+
Expect(k8sClient.Get(context.Background(), getKey(&rmqClusterInstance), fetchedRabbit)).To(Succeed())
298287
Expect(fetchedRabbit.Spec).To(Equal(expectedClusterInstance.Spec))
299288
})
300289

@@ -316,12 +305,9 @@ var _ = Describe("RabbitmqCluster", func() {
316305

317306
expectedClusterInstance.Spec.Resources = expectedResources
318307

319-
Expect(k8sClient.Create(context.TODO(), &rmqClusterInstance)).To(Succeed())
308+
Expect(k8sClient.Create(context.Background(), &rmqClusterInstance)).To(Succeed())
320309
fetchedRabbit := &RabbitmqCluster{}
321-
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{
322-
Name: "rabbitmq-partial-resource",
323-
Namespace: "default",
324-
}, fetchedRabbit)).To(Succeed())
310+
Expect(k8sClient.Get(context.Background(), getKey(&rmqClusterInstance), fetchedRabbit)).To(Succeed())
325311
Expect(fetchedRabbit.Spec).To(Equal(expectedClusterInstance.Spec))
326312
})
327313

@@ -343,12 +329,9 @@ var _ = Describe("RabbitmqCluster", func() {
343329
Type: "ClusterIP",
344330
}
345331

346-
Expect(k8sClient.Create(context.TODO(), &rmqClusterInstance)).To(Succeed())
332+
Expect(k8sClient.Create(context.Background(), &rmqClusterInstance)).To(Succeed())
347333
fetchedRabbit := &RabbitmqCluster{}
348-
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{
349-
Name: "rabbit-service-type",
350-
Namespace: "default",
351-
}, fetchedRabbit)).To(Succeed())
334+
Expect(k8sClient.Get(context.Background(), getKey(&rmqClusterInstance), fetchedRabbit)).To(Succeed())
352335
Expect(fetchedRabbit.Spec).To(Equal(expectedClusterInstance.Spec))
353336
})
354337

@@ -372,16 +355,69 @@ var _ = Describe("RabbitmqCluster", func() {
372355
Storage: &tenGi,
373356
}
374357

375-
Expect(k8sClient.Create(context.TODO(), &rmqClusterInstance)).To(Succeed())
358+
Expect(k8sClient.Create(context.Background(), &rmqClusterInstance)).To(Succeed())
376359
fetchedRabbit := &RabbitmqCluster{}
377-
Expect(k8sClient.Get(context.TODO(), types.NamespacedName{
378-
Name: "rabbit-storage",
379-
Namespace: "default",
380-
}, fetchedRabbit)).To(Succeed())
360+
Expect(k8sClient.Get(context.Background(), getKey(&rmqClusterInstance), fetchedRabbit)).To(Succeed())
381361
Expect(fetchedRabbit.Spec).To(Equal(expectedClusterInstance.Spec))
382362
})
383363
})
384364
})
365+
Context("Vault", func() {
366+
It("is disabled by default", func() {
367+
rabbit := generateRabbitmqClusterObject("rabbit-without-vault")
368+
Expect(k8sClient.Create(context.Background(), rabbit)).To(Succeed())
369+
fetchedRabbit := &RabbitmqCluster{}
370+
Expect(k8sClient.Get(context.Background(), getKey(rabbit), fetchedRabbit)).To(Succeed())
371+
372+
Expect(fetchedRabbit.VaultEnabled()).To(BeFalse())
373+
})
374+
When("only default user is configured", func() {
375+
It("sets vault configuration correctly", func() {
376+
rabbit := generateRabbitmqClusterObject("rabbit-vault-default-user")
377+
rabbit.Spec.SecretBackend.Vault = &VaultSpec{
378+
Role: "test-role",
379+
DefaultUserPath: "test-path",
380+
}
381+
Expect(k8sClient.Create(context.Background(), rabbit)).To(Succeed())
382+
fetchedRabbit := &RabbitmqCluster{}
383+
Expect(k8sClient.Get(context.Background(), getKey(rabbit), fetchedRabbit)).To(Succeed())
384+
385+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.Role).To(Equal("test-role"))
386+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.DefaultUserPath).To(Equal("test-path"))
387+
Expect(fetchedRabbit.VaultEnabled()).To(BeTrue())
388+
Expect(fetchedRabbit.VaultDefaultUserSecretEnabled()).To(BeTrue())
389+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.DefaultUserSecretEnabled()).To(BeTrue())
390+
Expect(fetchedRabbit.VaultTLSEnabled()).To(BeFalse())
391+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.TLSEnabled()).To(BeFalse())
392+
393+
By("setting the default-user-credential-updater image by default")
394+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.DefaultUserUpdaterImage).To(
395+
PointTo(HavePrefix("rabbitmqoperator/default-user-credential-updater:")))
396+
})
397+
})
398+
When("only TLS is configured", func() {
399+
It("sets vault configuration correctly", func() {
400+
rabbit := generateRabbitmqClusterObject("rabbit-vault-tls")
401+
rabbit.Spec.SecretBackend.Vault = &VaultSpec{
402+
Role: "test-role",
403+
TLS: VaultTLSSpec{
404+
PKIIssuerPath: "pki/issue/hashicorp-com",
405+
},
406+
}
407+
Expect(k8sClient.Create(context.Background(), rabbit)).To(Succeed())
408+
fetchedRabbit := &RabbitmqCluster{}
409+
Expect(k8sClient.Get(context.Background(), getKey(rabbit), fetchedRabbit)).To(Succeed())
410+
411+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.Role).To(Equal("test-role"))
412+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.TLS.PKIIssuerPath).To(Equal("pki/issue/hashicorp-com"))
413+
Expect(fetchedRabbit.VaultEnabled()).To(BeTrue())
414+
Expect(fetchedRabbit.VaultDefaultUserSecretEnabled()).To(BeFalse())
415+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.DefaultUserSecretEnabled()).To(BeFalse())
416+
Expect(fetchedRabbit.VaultTLSEnabled()).To(BeTrue())
417+
Expect(fetchedRabbit.Spec.SecretBackend.Vault.TLSEnabled()).To(BeTrue())
418+
})
419+
})
420+
})
385421
})
386422
Context("RabbitmqClusterStatus", func() {
387423
It("sets conditions based on inputs", func() {

0 commit comments

Comments
 (0)