Skip to content

Commit 690dfc1

Browse files
committed
Replicate medusa bucket secrets to each Cass DC
1 parent 75cc260 commit 690dfc1

File tree

14 files changed

+315
-25
lines changed

14 files changed

+315
-25
lines changed

CHANGELOG/CHANGELOG-1.12.md

+1
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ When cutting a new release, update the `unreleased` heading to the tag being gen
1515

1616
## unreleased
1717

18+
* [ENHANCEMENT] [#1159](https://github.com/k8ssandra/k8ssandra-operator/issues/1159) Replicate bucket key secrets to namespaces hosting clusters
1819
* [CHANGE] Upgrade to Medusa v0.17.2
1920
* [CHANGE] [#1158](https://github.com/k8ssandra/k8ssandra-operator/issues/1158) Use the MedusaConfiguration API when creating Medusa configuration
2021
* [CHANGE] [#1050](https://github.com/k8ssandra/k8ssandra-operator/issues/1050) Remove unnecessary requeues in the Medusa controllers

controllers/k8ssandra/k8ssandracluster_controller_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ func TestK8ssandraCluster(t *testing.T) {
103103
t.Run("CreateMultiDcClusterWithMedusa", testEnv.ControllerTest(ctx, createMultiDcClusterWithMedusa))
104104
t.Run("CreateSingleDcClusterWithMedusaConfigRef", testEnv.ControllerTest(ctx, createSingleDcClusterWithMedusaConfigRef))
105105
t.Run("CreatingSingleDcClusterWithoutPrefixInClusterSpecFail", testEnv.ControllerTest(ctx, creatingSingleDcClusterWithoutPrefixInClusterSpecFails))
106+
t.Run("CreateMultiDcClusterWithReplicatedSecrets", testEnv.ControllerTest(ctx, createMultiDcClusterWithReplicatedSecrets))
106107
t.Run("CreateSingleDcClusterNoAuth", testEnv.ControllerTest(ctx, createSingleDcClusterNoAuth))
107108
t.Run("CreateSingleDcClusterAuth", testEnv.ControllerTest(ctx, createSingleDcClusterAuth))
108109
t.Run("CreateSingleDcClusterAuthExternalSecrets", testEnv.ControllerTest(ctx, createSingleDcClusterAuthExternalSecrets))

controllers/k8ssandra/medusa_reconciler.go

+73-9
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,9 @@ import (
44
"context"
55
"fmt"
66
"github.com/adutra/goalesce"
7-
medusaapi "github.com/k8ssandra/k8ssandra-operator/apis/medusa/v1alpha1"
8-
"k8s.io/apimachinery/pkg/types"
9-
107
"github.com/go-logr/logr"
118
api "github.com/k8ssandra/k8ssandra-operator/apis/k8ssandra/v1alpha1"
9+
medusaapi "github.com/k8ssandra/k8ssandra-operator/apis/medusa/v1alpha1"
1210
cassandra "github.com/k8ssandra/k8ssandra-operator/pkg/cassandra"
1311
"github.com/k8ssandra/k8ssandra-operator/pkg/labels"
1412
medusa "github.com/k8ssandra/k8ssandra-operator/pkg/medusa"
@@ -18,6 +16,8 @@ import (
1816
"github.com/k8ssandra/k8ssandra-operator/pkg/utils"
1917
appsv1 "k8s.io/api/apps/v1"
2018
corev1 "k8s.io/api/core/v1"
19+
"k8s.io/apimachinery/pkg/api/errors"
20+
"k8s.io/apimachinery/pkg/types"
2121
"sigs.k8s.io/controller-runtime/pkg/client"
2222
)
2323

@@ -32,11 +32,11 @@ func (r *K8ssandraClusterReconciler) reconcileMedusa(
3232
kc := desiredKc.DeepCopy()
3333
namespace := utils.FirstNonEmptyString(dcConfig.Meta.Namespace, kc.Namespace)
3434
logger.Info("Medusa reconcile for " + dcConfig.CassDcName() + " on namespace " + namespace)
35-
medusaSpec := kc.Spec.Medusa
36-
if medusaSpec != nil {
35+
if kc.Spec.Medusa != nil {
3736
logger.Info("Medusa is enabled")
3837

39-
mergeResult := mergeStorageProperties(ctx, remoteClient, namespace, medusaSpec, logger, kc)
38+
mergeResult := r.mergeStorageProperties(ctx, r.Client, namespace, kc.Spec.Medusa, logger, kc)
39+
medusaSpec := kc.Spec.Medusa
4040
if mergeResult.IsError() {
4141
return result.Error(mergeResult.GetError())
4242
}
@@ -49,6 +49,7 @@ func (r *K8ssandraClusterReconciler) reconcileMedusa(
4949
return result.Error(fmt.Errorf("medusa encryption certificates were not provided despite client encryption being enabled"))
5050
}
5151
}
52+
5253
if medusaSpec.StorageProperties.StorageSecretRef.Name == "" {
5354
return result.Error(fmt.Errorf("medusa storage secret is not defined for storage provider %s", medusaSpec.StorageProperties.StorageProvider))
5455
}
@@ -119,7 +120,7 @@ func (r *K8ssandraClusterReconciler) reconcileMedusa(
119120
}
120121
if !ready {
121122
logger.Info("Medusa standalone deployment is not ready yet")
122-
return result.RequeueSoon(r.DefaultDelay)
123+
return result.Error(err)
123124
}
124125
// Create a cron job to purge Medusa backups
125126
purgeCronJob, err := medusa.PurgeCronJob(dcConfig, kc.SanitizedName(), namespace, logger)
@@ -183,6 +184,11 @@ func (r *K8ssandraClusterReconciler) reconcileMedusaSecrets(
183184
}
184185
}
185186

187+
if err := r.reconcileBucketSecrets(ctx, r.ClientCache.GetLocalClient(), kc, logger); err != nil {
188+
logger.Error(err, "Failed to reconcile Medusa bucket secrets")
189+
return result.Error(err)
190+
}
191+
186192
logger.Info("Medusa user secrets successfully reconciled")
187193
return result.Continue()
188194
}
@@ -213,7 +219,7 @@ func (r *K8ssandraClusterReconciler) reconcileMedusaConfigMap(
213219
return result.Continue()
214220
}
215221

216-
func mergeStorageProperties(
222+
func (r *K8ssandraClusterReconciler) mergeStorageProperties(
217223
ctx context.Context,
218224
remoteClient client.Client,
219225
namespace string,
@@ -229,7 +235,7 @@ func mergeStorageProperties(
229235
configKey := types.NamespacedName{Namespace: namespace, Name: medusaSpec.MedusaConfigurationRef.Name}
230236
if err := remoteClient.Get(ctx, configKey, storageProperties); err != nil {
231237
logger.Error(err, fmt.Sprintf("failed to get MedusaConfiguration %s", configKey))
232-
return result.Error(err)
238+
return result.RequeueSoon(r.DefaultDelay)
233239
}
234240
// check if the StorageProperties from the cluster have the prefix field set
235241
// it is required to be present because that's the single thing that differentiates backups of two different clusters
@@ -243,7 +249,65 @@ func mergeStorageProperties(
243249
logger.Error(err, "failed to merge MedusaConfiguration StorageProperties")
244250
return result.Error(err)
245251
}
252+
// medusaapi.MedusaConfiguration comes with a storage corev1.Secret containing the credentials to access the storage
253+
// we make a copy of that secret for each cluster/dc, and then point to it with a corev1.LocalObjectReference
254+
// when we do the copy, we name the secret as <cluster-name>-<original-secret-name>
255+
// here we need to update the reference to point to that copied secret
256+
mergedProperties.StorageSecretRef.Name = fmt.Sprintf("%s-%s", desiredKc.Name, mergedProperties.StorageSecretRef.Name)
257+
246258
// copy the merged properties back into the cluster
247259
mergedProperties.DeepCopyInto(&desiredKc.Spec.Medusa.StorageProperties)
248260
return result.Continue()
249261
}
262+
263+
func (r *K8ssandraClusterReconciler) reconcileBucketSecrets(
264+
ctx context.Context,
265+
c client.Client,
266+
kc *api.K8ssandraCluster,
267+
logger logr.Logger,
268+
) error {
269+
270+
logger.Info("Reconciling Medusa bucket secrets")
271+
medusaSpec := kc.Spec.Medusa
272+
273+
// there is nothing to reconcile if we're not using Medusa configuration reference
274+
if medusaSpec == nil || medusaSpec.MedusaConfigurationRef.Name == "" {
275+
logger.Info("MedusaConfigurationRef is not set, skipping bucket secret reconciliation")
276+
return nil
277+
}
278+
279+
// fetch the referenced configuration
280+
medusaConfigName := medusaSpec.MedusaConfigurationRef.Name
281+
medusaConfigNamespace := medusaSpec.MedusaConfigurationRef.Namespace
282+
medusaConfigKey := types.NamespacedName{Namespace: medusaConfigNamespace, Name: medusaConfigName}
283+
medusaConfig := &medusaapi.MedusaConfiguration{}
284+
if err := c.Get(ctx, medusaConfigKey, medusaConfig); err != nil {
285+
logger.Error(err, "could not get MedusaConfiguration")
286+
return err
287+
}
288+
289+
// fetch the referenced medusa configuration's bucket secret
290+
bucketSecretName := medusaConfig.Spec.StorageProperties.StorageSecretRef.Name
291+
bucketSecret := &corev1.Secret{}
292+
bucketSecretKey := types.NamespacedName{Namespace: medusaConfigNamespace, Name: bucketSecretName}
293+
if err := c.Get(ctx, bucketSecretKey, bucketSecret); err != nil {
294+
logger.Error(err, "could not get bucket Secret")
295+
return err
296+
}
297+
298+
// write the secret into the namespace of the K8ssandraCluster
299+
clusterBucketSecret := bucketSecret.DeepCopy()
300+
clusterBucketSecret.ResourceVersion = ""
301+
clusterBucketSecret.Name = fmt.Sprintf("%s-%s", kc.Name, bucketSecret.Name)
302+
clusterBucketSecret.Namespace = kc.Namespace
303+
labels.SetReplicatedBy(clusterBucketSecret, utils.GetKey(kc))
304+
if err := c.Create(ctx, clusterBucketSecret); err != nil {
305+
if !errors.IsAlreadyExists(err) {
306+
logger.Error(err, fmt.Sprintf("failed to create cluster bucket secret %s", clusterBucketSecret))
307+
return err
308+
}
309+
// we already have the bucket secret, so continue to updating the cluster (it might have failed before)
310+
}
311+
312+
return nil
313+
}

controllers/k8ssandra/medusa_reconciler_test.go

+128-4
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ func dcTemplate(dcName string, dataPlaneContext string) api.CassandraDatacenterT
5353
}
5454
}
5555

56-
func MedusaConfig(namespace string) *medusaapi.MedusaConfiguration {
56+
func MedusaConfig(name, namespace string) *medusaapi.MedusaConfiguration {
5757
return &medusaapi.MedusaConfiguration{
5858
ObjectMeta: metav1.ObjectMeta{
59-
Name: medusaConfigName,
59+
Name: name,
6060
Namespace: namespace,
6161
},
6262
Spec: medusaapi.MedusaConfigurationSpec{
@@ -442,7 +442,7 @@ func createSingleDcClusterWithMedusaConfigRef(t *testing.T, ctx context.Context,
442442

443443
t.Log("Creating Medusa Configuration object")
444444
medusaConfigKey := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusaConfigName}, K8sContext: f.DataPlaneContexts[0]}
445-
err := f.Create(ctx, medusaConfigKey, MedusaConfig(namespace))
445+
err := f.Create(ctx, medusaConfigKey, MedusaConfig(medusaConfigName, namespace))
446446
require.NoError(err, "failed to create Medusa Configuration")
447447
require.Eventually(f.MedusaConfigExists(ctx, f.DataPlaneContexts[0], medusaConfigKey), timeout, interval)
448448

@@ -520,7 +520,7 @@ func creatingSingleDcClusterWithoutPrefixInClusterSpecFails(t *testing.T, ctx co
520520
// create the MedusaConfiguration object
521521
t.Log("Creating Medusa Configuration object")
522522
medusaConfigKey := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusaConfigName}, K8sContext: f.DataPlaneContexts[0]}
523-
err = f.Create(ctx, medusaConfigKey, MedusaConfig(namespace))
523+
err = f.Create(ctx, medusaConfigKey, MedusaConfig(medusaConfigName, namespace))
524524
require.NoError(err, "failed to create Medusa Configuration")
525525
require.Eventually(f.MedusaConfigExists(ctx, f.DataPlaneContexts[0], medusaConfigKey), timeout, interval)
526526

@@ -538,3 +538,127 @@ func creatingSingleDcClusterWithoutPrefixInClusterSpecFails(t *testing.T, ctx co
538538
// verify the cluster still doesn't get created
539539
require.Never(f.DatacenterExists(ctx, dc1Key), timeout, interval)
540540
}
541+
542+
func controlPlaneContextKey(f *framework.Framework, object metav1.Object, contextName string) framework.ClusterKey {
543+
return framework.ClusterKey{NamespacedName: utils.GetKey(object), K8sContext: contextName}
544+
}
545+
546+
func dataPlaneContextKey(f *framework.Framework, object metav1.Object, dataPlaneContextIndex int) framework.ClusterKey {
547+
return framework.ClusterKey{NamespacedName: utils.GetKey(object), K8sContext: f.DataPlaneContexts[dataPlaneContextIndex]}
548+
}
549+
550+
func createMultiDcClusterWithReplicatedSecrets(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) {
551+
require := require.New(t)
552+
553+
clusterName := "test-cluster"
554+
originalConfigName := "test-config"
555+
originalSecretName := fmt.Sprintf("%s-bucket-key", originalConfigName)
556+
clusterSecretName := fmt.Sprintf("%s-%s", clusterName, originalSecretName)
557+
558+
// create a storage secret, then a MedusaConfiguration that points to it
559+
// the ReplicatedSecrets controller is not loaded in env tests, so we "mock" it by replicating the secrets manually
560+
medusaSecret := &corev1.Secret{
561+
ObjectMeta: metav1.ObjectMeta{
562+
Name: originalSecretName,
563+
Namespace: namespace,
564+
},
565+
StringData: map[string]string{
566+
"credentials": "some-credentials",
567+
},
568+
}
569+
// create the secret in the control plane
570+
cpMedusaSecret := medusaSecret.DeepCopy()
571+
err := f.Create(ctx, controlPlaneContextKey(f, cpMedusaSecret, f.ControlPlaneContext), cpMedusaSecret)
572+
require.NoError(err, fmt.Sprintf("failed to create secret in control plane %s", f.ControlPlaneContext))
573+
//create the secret in the data planes
574+
for i, n := range f.DataPlaneContexts {
575+
dpMedusaSecret := medusaSecret.DeepCopy()
576+
dpMedusaSecret.Name = clusterSecretName
577+
err := f.Create(ctx, dataPlaneContextKey(f, dpMedusaSecret, i), dpMedusaSecret)
578+
require.NoError(err, fmt.Sprintf("failed to create secret in context %d (%s)", i, n))
579+
}
580+
581+
// create medusa config in the control plane only
582+
medusaConfig := MedusaConfig(originalConfigName, namespace)
583+
medusaConfig.Spec.StorageProperties.StorageSecretRef = corev1.LocalObjectReference{
584+
Name: originalSecretName,
585+
}
586+
cpMedusaConfig := medusaConfig.DeepCopy()
587+
err = f.Create(ctx, controlPlaneContextKey(f, cpMedusaConfig, f.ControlPlaneContext), cpMedusaConfig)
588+
require.NoError(err, fmt.Sprintf("failed to create MedusaConfiguration in control plane %s", f.ControlPlaneContext))
589+
590+
// create a 2-dc K8ssandraCluster with Medusa featuring the reference to the above MedusaConfiguration
591+
kc := &api.K8ssandraCluster{
592+
ObjectMeta: metav1.ObjectMeta{
593+
Name: clusterName,
594+
Namespace: namespace,
595+
},
596+
Spec: api.K8ssandraClusterSpec{
597+
Cassandra: &api.CassandraClusterTemplate{
598+
Datacenters: []api.CassandraDatacenterTemplate{
599+
dcTemplate("dc1", f.DataPlaneContexts[1]),
600+
dcTemplate("dc2", f.DataPlaneContexts[2]),
601+
},
602+
},
603+
Medusa: &medusaapi.MedusaClusterTemplate{
604+
MedusaConfigurationRef: corev1.ObjectReference{
605+
Namespace: namespace,
606+
Name: originalConfigName,
607+
},
608+
StorageProperties: medusaapi.Storage{
609+
Prefix: "some-prefix",
610+
},
611+
},
612+
},
613+
}
614+
err = f.Client.Create(ctx, kc)
615+
require.NoError(err, "failed to create K8ssandraCluster")
616+
617+
verifySuperuserSecretCreated(ctx, t, f, kc)
618+
verifyReplicatedSecretReconciled(ctx, t, f, kc)
619+
620+
reconcileMedusaStandaloneDeployment(ctx, t, f, kc, "dc1", f.DataPlaneContexts[1])
621+
622+
// crate the first DC
623+
dc1Key := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: "dc1"}, K8sContext: f.DataPlaneContexts[1]}
624+
require.Eventually(f.DatacenterExists(ctx, dc1Key), timeout, interval)
625+
626+
// mark the first DC as ready
627+
t.Log("update dc1 status to ready")
628+
err = f.SetDatacenterStatusReady(ctx, dc1Key)
629+
require.NoError(err, "failed to update dc1 status to ready")
630+
631+
// create the second DC
632+
reconcileMedusaStandaloneDeployment(ctx, t, f, kc, "dc2", f.DataPlaneContexts[2])
633+
dc2Key := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: "dc2"}, K8sContext: f.DataPlaneContexts[2]}
634+
require.Eventually(f.DatacenterExists(ctx, dc2Key), timeout, interval)
635+
636+
// verify the copied secret is mounted in the pods
637+
verifyBucketSecretMounted(ctx, t, f, dc1Key, clusterSecretName)
638+
verifyBucketSecretMounted(ctx, t, f, dc2Key, clusterSecretName)
639+
640+
// verify the cluster's spec still contains the correct value
641+
// which is empty because we used MedusaConfigRef
642+
// merged it at runtime but never persisted to the k8ssandraCluster object
643+
kc = &api.K8ssandraCluster{}
644+
err = f.Client.Get(ctx, types.NamespacedName{Namespace: namespace, Name: clusterName}, kc)
645+
require.NoError(err, "failed to get K8ssandraCluster")
646+
require.Equal("", kc.Spec.Medusa.StorageProperties.StorageSecretRef.Name)
647+
}
648+
649+
func verifyBucketSecretMounted(ctx context.Context, t *testing.T, f *framework.Framework, dcKey framework.ClusterKey, clusterSecretName string) {
650+
require := require.New(t)
651+
652+
// fetch the DC spec
653+
dc := &cassdcapi.CassandraDatacenter{}
654+
err := f.Get(ctx, dcKey, dc)
655+
require.NoError(err, fmt.Sprintf("failed to get %s", dcKey.Name))
656+
657+
// fetch medusa container
658+
containerIndex, found := cassandra.FindContainer(dc.Spec.PodTemplateSpec, "medusa")
659+
require.True(found, fmt.Sprintf("%s doesn't have medusa container", dc.Name))
660+
medusaContainer := dc.Spec.PodTemplateSpec.Spec.Containers[containerIndex]
661+
662+
// check its mount
663+
assert.True(t, f.ContainerHasVolumeMount(medusaContainer, clusterSecretName, "/etc/medusa-secrets"), "Missing Volume Mount for Medusa bucket key")
664+
}

docs/content/en/tasks/backup-restore/_index.md

+5-3
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ spec:
8888
# size: 100Mi
8989
```
9090

91-
The definition above requires a secret named `medusa-bucket-key` to be created in the target namespace before the `K8ssandraCluster` object gets created. Use the following format for this secret:
91+
The definition above requires a secret named `medusa-bucket-key` to be present in the target namespace before the `K8ssandraCluster` object gets created. Use the following format for this secret:
9292

9393
```yaml
9494
apiVersion: v1
@@ -106,9 +106,11 @@ stringData:
106106
107107
The file should always specify `credentials` as shown in the example above; in that section, provide the expected format and credential values that are expected by Medusa for the chosen storage backend. For more, refer to the [Medusa documentation](https://github.com/thelastpickle/cassandra-medusa/blob/master/docs/Installation.md) to know which file format should used for each supported storage backend.
108108

109-
A successful deployment should inject a new init container named `medusa-restore` and a new container named `medusa` in the Cassandra StatefulSet pods.
109+
If using a shared Medusa configuration (see below), this secret can be created in the same namespace as the `MedusaConfiguration` object. The K8ssandra operator will then make sure the secret is replicated to the namespaces hosting the Cassandra clusters.
110110

111-
## Using shared medusa configuration properties
111+
A successful deployment should inject a new init container named `medusa-restore` and a new container named `medusa` in the Cassandra StatefulSet pods.
112+
113+
## Using shared Medusa configuration properties
112114

113115
Medusa configuration properties can be shared across multiple K8ssandraClusters by creating a `MedusaConfiguration` custom resource in the Control Plane K8ssandra cluster.
114116
Example:

test/e2e/cluster_scope_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func multiDcMultiCluster(t *testing.T, ctx context.Context, klusterNamespace str
2323

2424
dc1Key := framework.ClusterKey{K8sContext: f.DataPlaneContexts[0], NamespacedName: types.NamespacedName{Namespace: dc1Namespace, Name: "dc1"}}
2525
checkDatacenterReady(t, ctx, dc1Key, f)
26+
checkBucketKeyPresent(t, f, ctx, dc1Namespace, dc1Key.K8sContext, k8ssandra)
2627

2728
t.Log("check k8ssandra cluster status")
2829
require.Eventually(func() bool {
@@ -41,6 +42,7 @@ func multiDcMultiCluster(t *testing.T, ctx context.Context, klusterNamespace str
4142

4243
dc2Key := framework.ClusterKey{K8sContext: f.DataPlaneContexts[1], NamespacedName: types.NamespacedName{Namespace: dc2Namespace, Name: "dc2"}}
4344
checkDatacenterReady(t, ctx, dc2Key, f)
45+
checkBucketKeyPresent(t, f, ctx, dc2Namespace, dc2Key.K8sContext, k8ssandra)
4446

4547
t.Log("check k8ssandra cluster status")
4648
require.Eventually(func() bool {
@@ -65,6 +67,10 @@ func multiDcMultiCluster(t *testing.T, ctx context.Context, klusterNamespace str
6567
return cassandraDatacenterReady(cassandraStatus)
6668
}, polling.k8ssandraClusterStatus.timeout, polling.k8ssandraClusterStatus.interval, "timed out waiting for K8ssandraCluster status to get updated")
6769

70+
t.Log("check replicated secret mounted")
71+
checkReplicatedSecretMounted(t, ctx, f, dc1Key, dc1Namespace, k8ssandra)
72+
checkReplicatedSecretMounted(t, ctx, f, dc2Key, dc2Namespace, k8ssandra)
73+
6874
t.Log("retrieve database credentials")
6975
username, password, err := f.RetrieveDatabaseCredentials(ctx, f.DataPlaneContexts[0], dc1Namespace, k8ssandra.SanitizedName())
7076
require.NoError(err, "failed to retrieve database credentials")

0 commit comments

Comments
 (0)