From a6271b40d79995d353aed8fcdfdaf8e7b803dcdb Mon Sep 17 00:00:00 2001 From: Radovan Date: Mon, 29 Jan 2024 18:30:43 +0200 Subject: [PATCH] Make Medusa's config map be a merge of MedusaConfig object and cluster spec (#1187) --- CHANGELOG/CHANGELOG-1.12.md | 1 + .../v1alpha1/k8ssandracluster_webhook.go | 11 + .../v1alpha1/k8ssandracluster_webhook_test.go | 44 +++ apis/medusa/v1alpha1/medusa_types.go | 6 + apis/medusa/v1alpha1/zz_generated.deepcopy.go | 1 + .../bases/k8ssandra.io_k8ssandraclusters.yaml | 40 +++ .../k8ssandracluster_controller_test.go | 2 + controllers/k8ssandra/medusa_reconciler.go | 46 ++- .../k8ssandra/medusa_reconciler_test.go | 303 +++++++++++++----- .../content/en/tasks/backup-restore/_index.md | 22 ++ test/framework/framework.go | 15 + .../k8ssandra.yaml | 3 + 12 files changed, 417 insertions(+), 77 deletions(-) diff --git a/CHANGELOG/CHANGELOG-1.12.md b/CHANGELOG/CHANGELOG-1.12.md index db805daaf..182557bef 100644 --- a/CHANGELOG/CHANGELOG-1.12.md +++ b/CHANGELOG/CHANGELOG-1.12.md @@ -15,6 +15,7 @@ When cutting a new release, update the `unreleased` heading to the tag being gen ## unreleased +* [CHANGE] [#1158](https://github.com/k8ssandra/k8ssandra-operator/issues/1158) Use the MedusaConfiguration API when creating Medusa configuration * [CHANGE] [#1050](https://github.com/k8ssandra/k8ssandra-operator/issues/1050) Remove unnecessary requeues in the Medusa controllers * [CHANGE] [#1165](https://github.com/k8ssandra/k8ssandra-operator/issues/1165) Upgrade to Medusa v0.17.1 * [FEATURE] [#1157](https://github.com/k8ssandra/k8ssandra-operator/issues/1157) Add the MedusaConfiguration API diff --git a/apis/k8ssandra/v1alpha1/k8ssandracluster_webhook.go b/apis/k8ssandra/v1alpha1/k8ssandracluster_webhook.go index 08850eb3e..97e4b1b30 100644 --- a/apis/k8ssandra/v1alpha1/k8ssandracluster_webhook.go +++ b/apis/k8ssandra/v1alpha1/k8ssandracluster_webhook.go @@ -35,6 +35,7 @@ var ( ErrNoStorageConfig = fmt.Errorf("storageConfig must be defined at cluster level or dc level") ErrNoResourcesSet = fmt.Errorf("softPodAntiAffinity requires Resources to be set") ErrClusterName = fmt.Errorf("cluster name can not be changed") + ErrNoStoragePrefix = fmt.Errorf("medusa storage prefix must be set when a medusaConfigurationRef is used") ) // log is for logging in this package. @@ -88,6 +89,16 @@ func (r *K8ssandraCluster) validateK8ssandraCluster() error { } } + // Verify the Medusa storage prefix is explicitly set + // only relevant if Medusa is enabled and the MedusaConfiguration object is referenced + if r.Spec.Medusa != nil { + if r.Spec.Medusa.MedusaConfigurationRef.Name != "" { + if r.Spec.Medusa.StorageProperties.Prefix == "" { + return ErrNoStoragePrefix + } + } + } + if err := r.validateStatefulsetNameSize(); err != nil { return err } diff --git a/apis/k8ssandra/v1alpha1/k8ssandracluster_webhook_test.go b/apis/k8ssandra/v1alpha1/k8ssandracluster_webhook_test.go index 82071f8f5..1f86cce89 100644 --- a/apis/k8ssandra/v1alpha1/k8ssandracluster_webhook_test.go +++ b/apis/k8ssandra/v1alpha1/k8ssandracluster_webhook_test.go @@ -42,6 +42,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/envtest" logf "sigs.k8s.io/controller-runtime/pkg/log" + medusaapi "github.com/k8ssandra/k8ssandra-operator/apis/medusa/v1alpha1" reaperapi "github.com/k8ssandra/k8ssandra-operator/apis/reaper/v1alpha1" ) @@ -145,6 +146,7 @@ func TestWebhook(t *testing.T) { t.Run("NumTokensValidation", testNumTokens) t.Run("NumTokensValidationInUpdate", testNumTokensInUpdate) t.Run("StsNameTooLong", testStsNameTooLong) + t.Run("MedusaPrefixMissing", testMedusaPrefixMissing) } func testContextValidation(t *testing.T) { @@ -407,3 +409,45 @@ func createMinimalClusterObj(name, namespace string) *K8ssandraCluster { }, } } + +func testMedusaPrefixMissing(t *testing.T) { + required := require.New(t) + createNamespace(required, "short-namespace") + + clusterWithoutMedusa := createMinimalClusterObj("without-medusa", "short-namespace") + err := k8sClient.Create(ctx, clusterWithoutMedusa) + required.NoError(err) + + clusterWithMedusa := createMinimalClusterObj("with-medusa", "short-namespace") + clusterWithMedusa.Spec.Medusa = &medusaapi.MedusaClusterTemplate{ + StorageProperties: medusaapi.Storage{ + Prefix: "", + }, + } + err = k8sClient.Create(ctx, clusterWithMedusa) + required.NoError(err) + + clusterWithoutPrefix := createMinimalClusterObj("without-prefix", "short-namespace") + clusterWithoutPrefix.Spec.Medusa = &medusaapi.MedusaClusterTemplate{ + MedusaConfigurationRef: corev1.ObjectReference{ + Name: "medusa-config", + }, + StorageProperties: medusaapi.Storage{ + Prefix: "", + }, + } + err = k8sClient.Create(ctx, clusterWithoutPrefix) + required.Error(err) + + clusterWithPrefix := createMinimalClusterObj("with-prefix", "short-namespace") + clusterWithPrefix.Spec.Medusa = &medusaapi.MedusaClusterTemplate{ + MedusaConfigurationRef: corev1.ObjectReference{ + Name: "medusa-config", + }, + StorageProperties: medusaapi.Storage{ + Prefix: "some-prefix", + }, + } + err = k8sClient.Create(ctx, clusterWithPrefix) + required.NoError(err) +} diff --git a/apis/medusa/v1alpha1/medusa_types.go b/apis/medusa/v1alpha1/medusa_types.go index 8062218a9..98e1594d7 100644 --- a/apis/medusa/v1alpha1/medusa_types.go +++ b/apis/medusa/v1alpha1/medusa_types.go @@ -129,6 +129,12 @@ type PodStorageSettings struct { } type MedusaClusterTemplate struct { + // MedusaConfigurationRef points to an existing MedusaConfiguration object. + // The purpose is to allow shared default settings across several clusters. + // StorageProperties override the settings from MedusaConfiguration object to allow customization. + // +optional + MedusaConfigurationRef corev1.ObjectReference `json:"medusaConfigurationRef,omitempty"` + // MedusaContainerImage is the image characteristics to use for Medusa containers. Leave nil // to use a default image. // +optional diff --git a/apis/medusa/v1alpha1/zz_generated.deepcopy.go b/apis/medusa/v1alpha1/zz_generated.deepcopy.go index 918071f1a..69e321a1d 100644 --- a/apis/medusa/v1alpha1/zz_generated.deepcopy.go +++ b/apis/medusa/v1alpha1/zz_generated.deepcopy.go @@ -371,6 +371,7 @@ func (in *MedusaBackupStatus) DeepCopy() *MedusaBackupStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *MedusaClusterTemplate) DeepCopyInto(out *MedusaClusterTemplate) { *out = *in + out.MedusaConfigurationRef = in.MedusaConfigurationRef if in.ContainerImage != nil { in, out := &in.ContainerImage, &out.ContainerImage *out = new(images.Image) diff --git a/config/crd/bases/k8ssandra.io_k8ssandraclusters.yaml b/config/crd/bases/k8ssandra.io_k8ssandraclusters.yaml index b1faf249c..2c0f62308 100644 --- a/config/crd/bases/k8ssandra.io_k8ssandraclusters.yaml +++ b/config/crd/bases/k8ssandra.io_k8ssandraclusters.yaml @@ -27021,6 +27021,46 @@ spec: format: int32 type: integer type: object + medusaConfigurationRef: + description: MedusaConfigurationRef points to an existing MedusaConfiguration + object. The purpose is to allow shared default settings across + several clusters. StorageProperties override the settings from + MedusaConfiguration object to allow customization. + properties: + apiVersion: + description: API version of the referent. + type: string + fieldPath: + description: 'If referring to a piece of an object instead + of an entire object, this string should contain a valid + JSON/Go field access statement, such as desiredState.manifest.containers[2]. + For example, if the object reference is to a container within + a pod, this would take on a value like: "spec.containers{name}" + (where "name" refers to the name of the container that triggered + the event) or if no container name is specified "spec.containers[2]" + (container with index 2 in this pod). This syntax is chosen + only to have some well-defined way of referencing a part + of an object. TODO: this design is not final and this field + is subject to change in the future.' + type: string + kind: + description: 'Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + namespace: + description: 'Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/' + type: string + resourceVersion: + description: 'Specific resourceVersion to which this reference + is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency' + type: string + uid: + description: 'UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids' + type: string + type: object + x-kubernetes-map-type: atomic readinessProbe: description: Define the readiness probe settings to use for the Medusa containers. diff --git a/controllers/k8ssandra/k8ssandracluster_controller_test.go b/controllers/k8ssandra/k8ssandracluster_controller_test.go index 629fd43f1..2205fd66d 100644 --- a/controllers/k8ssandra/k8ssandracluster_controller_test.go +++ b/controllers/k8ssandra/k8ssandracluster_controller_test.go @@ -101,6 +101,8 @@ func TestK8ssandraCluster(t *testing.T) { t.Run("CreateMultiDcClusterWithStargate", testEnv.ControllerTest(ctx, createMultiDcClusterWithStargate)) t.Run("CreateMultiDcClusterWithReaper", testEnv.ControllerTest(ctx, createMultiDcClusterWithReaper)) t.Run("CreateMultiDcClusterWithMedusa", testEnv.ControllerTest(ctx, createMultiDcClusterWithMedusa)) + t.Run("CreateSingleDcClusterWithMedusaConfigRef", testEnv.ControllerTest(ctx, createSingleDcClusterWithMedusaConfigRef)) + t.Run("CreatingSingleDcClusterWithoutPrefixInClusterSpecFail", testEnv.ControllerTest(ctx, creatingSingleDcClusterWithoutPrefixInClusterSpecFails)) t.Run("CreateSingleDcClusterNoAuth", testEnv.ControllerTest(ctx, createSingleDcClusterNoAuth)) t.Run("CreateSingleDcClusterAuth", testEnv.ControllerTest(ctx, createSingleDcClusterAuth)) t.Run("CreateSingleDcClusterAuthExternalSecrets", testEnv.ControllerTest(ctx, createSingleDcClusterAuthExternalSecrets)) diff --git a/controllers/k8ssandra/medusa_reconciler.go b/controllers/k8ssandra/medusa_reconciler.go index 730d666a8..ee781a1cb 100644 --- a/controllers/k8ssandra/medusa_reconciler.go +++ b/controllers/k8ssandra/medusa_reconciler.go @@ -3,6 +3,9 @@ package k8ssandra import ( "context" "fmt" + "github.com/adutra/goalesce" + medusaapi "github.com/k8ssandra/k8ssandra-operator/apis/medusa/v1alpha1" + "k8s.io/apimachinery/pkg/types" "github.com/go-logr/logr" api "github.com/k8ssandra/k8ssandra-operator/apis/k8ssandra/v1alpha1" @@ -21,17 +24,23 @@ import ( // Create all things Medusa related in the cassdc podTemplateSpec func (r *K8ssandraClusterReconciler) reconcileMedusa( ctx context.Context, - kc *api.K8ssandraCluster, + desiredKc *api.K8ssandraCluster, dcConfig *cassandra.DatacenterConfig, remoteClient client.Client, logger logr.Logger, ) result.ReconcileResult { + kc := desiredKc.DeepCopy() namespace := utils.FirstNonEmptyString(dcConfig.Meta.Namespace, kc.Namespace) logger.Info("Medusa reconcile for " + dcConfig.CassDcName() + " on namespace " + namespace) medusaSpec := kc.Spec.Medusa if medusaSpec != nil { logger.Info("Medusa is enabled") + mergeResult := mergeStorageProperties(ctx, remoteClient, namespace, medusaSpec, logger, kc) + if mergeResult.IsError() { + return result.Error(mergeResult.GetError()) + } + // Check that certificates are provided if client encryption is enabled if cassandra.ClientEncryptionEnabled(dcConfig) { if kc.Spec.UseExternalSecrets() { @@ -203,3 +212,38 @@ func (r *K8ssandraClusterReconciler) reconcileMedusaConfigMap( logger.Info("Medusa ConfigMap successfully reconciled") return result.Continue() } + +func mergeStorageProperties( + ctx context.Context, + remoteClient client.Client, + namespace string, + medusaSpec *medusaapi.MedusaClusterTemplate, + logger logr.Logger, + desiredKc *api.K8ssandraCluster, +) result.ReconcileResult { + // check if the StorageProperties are defined in the K8ssandraCluster + if medusaSpec.MedusaConfigurationRef.Name == "" { + return result.Continue() + } + storageProperties := &medusaapi.MedusaConfiguration{} + configKey := types.NamespacedName{Namespace: namespace, Name: medusaSpec.MedusaConfigurationRef.Name} + if err := remoteClient.Get(ctx, configKey, storageProperties); err != nil { + logger.Error(err, fmt.Sprintf("failed to get MedusaConfiguration %s", configKey)) + return result.Error(err) + } + // check if the StorageProperties from the cluster have the prefix field set + // it is required to be present because that's the single thing that differentiates backups of two different clusters + if desiredKc.Spec.Medusa.StorageProperties.Prefix == "" { + return result.Error(fmt.Errorf("StorageProperties.Prefix is not set in K8ssandraCluster %s", utils.GetKey(desiredKc))) + } + // try to merge the storage properties. goalesce gives priority to the 2nd argument, + // so stuff in the cluster overrides stuff in the config object + mergedProperties, err := goalesce.DeepMerge(storageProperties.Spec.StorageProperties, desiredKc.Spec.Medusa.StorageProperties) + if err != nil { + logger.Error(err, "failed to merge MedusaConfiguration StorageProperties") + return result.Error(err) + } + // copy the merged properties back into the cluster + mergedProperties.DeepCopyInto(&desiredKc.Spec.Medusa.StorageProperties) + return result.Continue() +} diff --git a/controllers/k8ssandra/medusa_reconciler_test.go b/controllers/k8ssandra/medusa_reconciler_test.go index ac25c1511..a2c7bad28 100644 --- a/controllers/k8ssandra/medusa_reconciler_test.go +++ b/controllers/k8ssandra/medusa_reconciler_test.go @@ -3,6 +3,7 @@ package k8ssandra import ( "context" "fmt" + "strings" "testing" cassdcapi "github.com/k8ssandra/cass-operator/apis/cassandra/v1beta1" @@ -25,93 +26,137 @@ import ( ) const ( - medusaImageRepo = "test" - storageSecret = "storage-secret" - cassandraUserSecret = "medusa-secret" + medusaImageRepo = "test" + storageSecret = "storage-secret" + cassandraUserSecret = "medusa-secret" + k8ssandraClusterName = "test" + medusaConfigName = "medusa-config" + prefixFromMedusaConfig = "prefix-from-medusa-config" + prefixFromClusterSpec = "prefix-from-cluster-spec" ) +func dcTemplate(dcName string, dataPlaneContext string) api.CassandraDatacenterTemplate { + return api.CassandraDatacenterTemplate{ + Meta: api.EmbeddedObjectMeta{ + Name: dcName, + }, + K8sContext: dataPlaneContext, + Size: 3, + DatacenterOptions: api.DatacenterOptions{ + ServerVersion: "3.11.14", + StorageConfig: &cassdcapi.StorageConfig{ + CassandraDataVolumeClaimSpec: &corev1.PersistentVolumeClaimSpec{ + StorageClassName: &defaultStorageClass, + }, + }, + }, + } +} + +func MedusaConfig(namespace string) *medusaapi.MedusaConfiguration { + return &medusaapi.MedusaConfiguration{ + ObjectMeta: metav1.ObjectMeta{ + Name: medusaConfigName, + Namespace: namespace, + }, + Spec: medusaapi.MedusaConfigurationSpec{ + StorageProperties: medusaapi.Storage{ + Prefix: prefixFromMedusaConfig, + }, + }, + } +} + +func medusaTemplateWithoutConfigRef() *medusaapi.MedusaClusterTemplate { + return medusaTemplate(nil) +} + +func medusaTemplateWithConfigRef(configRefName string) *medusaapi.MedusaClusterTemplate { + configRef := &corev1.ObjectReference{ + Name: configRefName, + } + return medusaTemplate(configRef) +} + +func medusaTemplateWithConfigRefWithoutPrefix(configRefName string) *medusaapi.MedusaClusterTemplate { + template := medusaTemplateWithConfigRef(configRefName) + template.StorageProperties.Prefix = "" + return template +} + +func medusaTemplateWithConfigRefWithPrefix(configRefName string, prefix string) *medusaapi.MedusaClusterTemplate { + template := medusaTemplateWithConfigRef(configRefName) + template.StorageProperties.Prefix = prefix + return template +} + +func medusaTemplate(configObjectReference *corev1.ObjectReference) *medusaapi.MedusaClusterTemplate { + template := medusaapi.MedusaClusterTemplate{ + ContainerImage: &images.Image{ + Repository: medusaImageRepo, + }, + StorageProperties: medusaapi.Storage{ + StorageSecretRef: corev1.LocalObjectReference{ + Name: cassandraUserSecret, + }, + }, + CassandraUserSecretRef: corev1.LocalObjectReference{ + Name: cassandraUserSecret, + }, + ReadinessProbe: &corev1.Probe{ + InitialDelaySeconds: 1, + TimeoutSeconds: 2, + PeriodSeconds: 3, + SuccessThreshold: 1, + FailureThreshold: 5, + }, + LivenessProbe: &corev1.Probe{ + InitialDelaySeconds: 6, + TimeoutSeconds: 7, + PeriodSeconds: 8, + SuccessThreshold: 1, + FailureThreshold: 10, + }, + Resources: &corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("150m"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + }, + } + + if configObjectReference != nil { + configObjectReference.DeepCopyInto(&template.MedusaConfigurationRef) + } + + return &template +} + func createMultiDcClusterWithMedusa(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) { require := require.New(t) kc := &api.K8ssandraCluster{ ObjectMeta: metav1.ObjectMeta{ Namespace: namespace, - Name: "test", + Name: k8ssandraClusterName, }, Spec: api.K8ssandraClusterSpec{ Cassandra: &api.CassandraClusterTemplate{ Datacenters: []api.CassandraDatacenterTemplate{ - { - Meta: api.EmbeddedObjectMeta{ - Name: "dc1", - }, - K8sContext: f.DataPlaneContexts[0], - Size: 3, - DatacenterOptions: api.DatacenterOptions{ - ServerVersion: "3.11.14", - StorageConfig: &cassdcapi.StorageConfig{ - CassandraDataVolumeClaimSpec: &corev1.PersistentVolumeClaimSpec{ - StorageClassName: &defaultStorageClass, - }, - }, - }, - }, - { - Meta: api.EmbeddedObjectMeta{ - Name: "dc2", - }, - K8sContext: f.DataPlaneContexts[1], - Size: 3, - DatacenterOptions: api.DatacenterOptions{ - ServerVersion: "3.11.14", - StorageConfig: &cassdcapi.StorageConfig{ - CassandraDataVolumeClaimSpec: &corev1.PersistentVolumeClaimSpec{ - StorageClassName: &defaultStorageClass, - }, - }, - }, - }, - }, - }, - Medusa: &medusaapi.MedusaClusterTemplate{ - ContainerImage: &images.Image{ - Repository: medusaImageRepo, - }, - StorageProperties: medusaapi.Storage{ - StorageSecretRef: corev1.LocalObjectReference{ - Name: cassandraUserSecret, - }, - }, - CassandraUserSecretRef: corev1.LocalObjectReference{ - Name: cassandraUserSecret, - }, - ReadinessProbe: &corev1.Probe{ - InitialDelaySeconds: 1, - TimeoutSeconds: 2, - PeriodSeconds: 3, - SuccessThreshold: 1, - FailureThreshold: 5, - }, - LivenessProbe: &corev1.Probe{ - InitialDelaySeconds: 6, - TimeoutSeconds: 7, - PeriodSeconds: 8, - SuccessThreshold: 1, - FailureThreshold: 10, - }, - Resources: &corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("500m"), - corev1.ResourceMemory: resource.MustParse("1Gi"), - }, - Requests: corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse("150m"), - corev1.ResourceMemory: resource.MustParse("500Mi"), - }, + dcTemplate("dc1", f.DataPlaneContexts[0]), + dcTemplate("dc2", f.DataPlaneContexts[1]), }, }, + Medusa: medusaTemplateWithoutConfigRef(), }, } + require.NotNil(kc.Spec.Medusa.MedusaConfigurationRef) + require.Equal("", kc.Spec.Medusa.MedusaConfigurationRef.Name) + require.Equal("", kc.Spec.Medusa.MedusaConfigurationRef.Namespace) t.Log("Creating k8ssandracluster with Medusa") err := f.Client.Create(ctx, kc) @@ -125,8 +170,12 @@ func createMultiDcClusterWithMedusa(t *testing.T, ctx context.Context, f *framew verifySecretAnnotationAdded(t, f, ctx, dc1Key, cassandraUserSecret) + t.Log("verify the config map exists and has the contents from the MedusaConfiguration object") + defaultPrefix := kc.Spec.Medusa.StorageProperties.Prefix + verifyConfigMap(require, ctx, f, namespace, defaultPrefix) + t.Log("check that the standalone Medusa deployment was created in dc1") - medusaDeploymentKey1 := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusa.MedusaStandaloneDeploymentName("test", "dc1")}, K8sContext: f.DataPlaneContexts[0]} + medusaDeploymentKey1 := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusa.MedusaStandaloneDeploymentName(k8ssandraClusterName, "dc1")}, K8sContext: f.DataPlaneContexts[0]} medusaDeployment1 := &appsv1.Deployment{} require.Eventually(func() bool { if err := f.Get(ctx, medusaDeploymentKey1, medusaDeployment1); err != nil { @@ -138,7 +187,7 @@ func createMultiDcClusterWithMedusa(t *testing.T, ctx context.Context, f *framew require.True(f.ContainerHasEnvVar(medusaDeployment1.Spec.Template.Spec.Containers[0], "MEDUSA_RESOLVE_IP_ADDRESSES", "False")) t.Log("check that the standalone Medusa service was created") - medusaServiceKey1 := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusa.MedusaServiceName("test", "dc1")}, K8sContext: f.DataPlaneContexts[0]} + medusaServiceKey1 := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusa.MedusaServiceName(k8ssandraClusterName, "dc1")}, K8sContext: f.DataPlaneContexts[0]} medusaService1 := &corev1.Service{} require.Eventually(func() bool { if err := f.Get(ctx, medusaServiceKey1, medusaService1); err != nil { @@ -157,7 +206,7 @@ func createMultiDcClusterWithMedusa(t *testing.T, ctx context.Context, f *framew }) require.NoError(err, "failed to patch datacenter status") - kcKey := framework.ClusterKey{K8sContext: f.ControlPlaneContext, NamespacedName: types.NamespacedName{Namespace: namespace, Name: "test"}} + kcKey := framework.ClusterKey{K8sContext: f.ControlPlaneContext, NamespacedName: types.NamespacedName{Namespace: namespace, Name: k8ssandraClusterName}} t.Log("check that the K8ssandraCluster status is updated") require.Eventually(func() bool { @@ -213,7 +262,7 @@ func createMultiDcClusterWithMedusa(t *testing.T, ctx context.Context, f *framew require.Eventually(f.DatacenterExists(ctx, dc2Key), timeout, interval) t.Log("check that the standalone Medusa deployment was created in dc2") - medusaDeploymentKey2 := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusa.MedusaStandaloneDeploymentName("test", "dc2")}, K8sContext: f.DataPlaneContexts[1]} + medusaDeploymentKey2 := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusa.MedusaStandaloneDeploymentName(k8ssandraClusterName, "dc2")}, K8sContext: f.DataPlaneContexts[1]} medusaDeployment2 := &appsv1.Deployment{} require.Eventually(func() bool { if err := f.Get(ctx, medusaDeploymentKey2, medusaDeployment2); err != nil { @@ -223,7 +272,7 @@ func createMultiDcClusterWithMedusa(t *testing.T, ctx context.Context, f *framew }, timeout, interval) t.Log("check that the standalone Medusa service was created in dc2") - medusaServiceKey2 := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusa.MedusaServiceName("test", "dc2")}, K8sContext: f.DataPlaneContexts[1]} + medusaServiceKey2 := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusa.MedusaServiceName(k8ssandraClusterName, "dc2")}, K8sContext: f.DataPlaneContexts[1]} medusaService2 := &corev1.Service{} require.Eventually(func() bool { if err := f.Get(ctx, medusaServiceKey2, medusaService2); err != nil { @@ -387,3 +436,105 @@ func reconcileMedusaStandaloneDeployment(ctx context.Context, t *testing.T, f *f require.NoError(t, err, "Failed to update Medusa Deployment status") } + +func createSingleDcClusterWithMedusaConfigRef(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) { + require := require.New(t) + + t.Log("Creating Medusa Configuration object") + medusaConfigKey := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusaConfigName}, K8sContext: f.DataPlaneContexts[0]} + err := f.Create(ctx, medusaConfigKey, MedusaConfig(namespace)) + require.NoError(err, "failed to create Medusa Configuration") + require.Eventually(f.MedusaConfigExists(ctx, f.DataPlaneContexts[0], medusaConfigKey), timeout, interval) + + kc := &api.K8ssandraCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: k8ssandraClusterName, + }, + Spec: api.K8ssandraClusterSpec{ + Cassandra: &api.CassandraClusterTemplate{ + Datacenters: []api.CassandraDatacenterTemplate{ + dcTemplate("dc1", f.DataPlaneContexts[0]), + }, + }, + Medusa: medusaTemplateWithConfigRefWithPrefix(medusaConfigName, prefixFromClusterSpec), + }, + } + require.NotNil(kc.Spec.Medusa.MedusaConfigurationRef) + require.Equal(medusaConfigName, kc.Spec.Medusa.MedusaConfigurationRef.Name) + + t.Log("Creating k8ssandracluster with Medusa and a config ref") + err = f.Client.Create(ctx, kc) + require.NoError(err, "failed to create K8ssandraCluster") + verifyReplicatedSecretReconciled(ctx, t, f, kc) + + t.Log("verify the config map exists and has the contents from the MedusaConfiguration object") + verifyConfigMap(require, ctx, f, namespace, prefixFromClusterSpec) +} + +func verifyConfigMap(r *require.Assertions, ctx context.Context, f *framework.Framework, namespace string, expectedPrefix string) { + configMapName := fmt.Sprintf("%s-medusa", k8ssandraClusterName) + configMapKey := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: configMapName}, K8sContext: f.DataPlaneContexts[0]} + configMap := &corev1.ConfigMap{} + r.Eventually(func() bool { + if err := f.Get(ctx, configMapKey, configMap); err != nil { + r.NoError(err, "failed to get Medusa ConfigMap") + return false + } + return strings.Contains(configMap.Data["medusa.ini"], fmt.Sprintf("prefix = %s", expectedPrefix)) + }, timeout, interval, "Medusa ConfigMap doesn't have the right content") +} + +func creatingSingleDcClusterWithoutPrefixInClusterSpecFails(t *testing.T, ctx context.Context, f *framework.Framework, namespace string) { + require := require.New(t) + + // make a cluster spec without the prefix + kcFirstAttempt := &api.K8ssandraCluster{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: namespace, + Name: k8ssandraClusterName, + }, + Spec: api.K8ssandraClusterSpec{ + Cassandra: &api.CassandraClusterTemplate{ + Datacenters: []api.CassandraDatacenterTemplate{ + dcTemplate("dc1", f.DataPlaneContexts[0]), + }, + }, + Medusa: medusaTemplateWithConfigRefWithoutPrefix(medusaConfigName), + }, + } + require.NotNil(kcFirstAttempt.Spec.Medusa.MedusaConfigurationRef) + require.Equal(medusaConfigName, kcFirstAttempt.Spec.Medusa.MedusaConfigurationRef.Name) + require.Equal("", kcFirstAttempt.Spec.Medusa.StorageProperties.Prefix) + kcSecondAttempt := kcFirstAttempt.DeepCopy() + + // submit the cluster for creation + t.Log("Creating k8ssandracluster with Medusa but without MedusaConfig and without a prefix in the cluster spec") + err := f.Client.Create(ctx, kcFirstAttempt) + require.Error(err, "creating a cluster without Medusa's storage prefix should not happen") + + // verify the cluster doesn't get created + dc1Key := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: "dc1"}, K8sContext: f.DataPlaneContexts[0]} + require.Never(f.DatacenterExists(ctx, dc1Key), timeout, interval) + + // create the MedusaConfiguration object + t.Log("Creating Medusa Configuration object") + medusaConfigKey := framework.ClusterKey{NamespacedName: types.NamespacedName{Namespace: namespace, Name: medusaConfigName}, K8sContext: f.DataPlaneContexts[0]} + err = f.Create(ctx, medusaConfigKey, MedusaConfig(namespace)) + require.NoError(err, "failed to create Medusa Configuration") + require.Eventually(f.MedusaConfigExists(ctx, f.DataPlaneContexts[0], medusaConfigKey), timeout, interval) + + // add a reference to the MedusaConfiguration object to the cluster spec + kcSecondAttempt.Spec.Medusa.MedusaConfigurationRef.Name = medusaConfigName + + // confirm the prefix in the storage properties is still empty + require.Equal("", kcSecondAttempt.Spec.Medusa.StorageProperties.Prefix) + + // retry creating the cluster + t.Log("Creating k8ssandracluster with Medusa and MedusaConfig but without a prefix in the cluster spec") + err = f.Client.Create(ctx, kcSecondAttempt) + require.Error(err, "creating a cluster without Medusa's storage prefix should not happen if the MedusaConfig object exists") + + // verify the cluster still doesn't get created + require.Never(f.DatacenterExists(ctx, dc1Key), timeout, interval) +} diff --git a/docs/content/en/tasks/backup-restore/_index.md b/docs/content/en/tasks/backup-restore/_index.md index 9dc801bc6..74001d153 100644 --- a/docs/content/en/tasks/backup-restore/_index.md +++ b/docs/content/en/tasks/backup-restore/_index.md @@ -131,6 +131,28 @@ This allows creating bucket configurations that are easy to share across multipl The referenced secret must exist in the same namespace as the `MedusaConfiguration` object, and must contain the credentials file for the storage backend, as described in the previous section. +The storage properties from the `K8ssandraCluster` definition will override the ones from the `MedusaConfiguration` object. + +The storage properties in the `K8ssandraCluster` definition must specify the storage prefix. With the other settings shared, the prefix is what allows multiple clusters place backups in the same bucket without interfering with each other. + +When creating the cluster, the reference to the config can look like this example: + +```yaml +apiVersion: k8ssandra.io/v1alpha1 +kind: K8ssandraCluster +metadata: + name: demo +spec: + cassandra: + ... + medusa: + medusaConfigurationRef: + name: medusaconfiguration-s3 + storageProperties: + prefix: demo + ... +``` + ## Creating a Backup To perform a backup of a Cassandra datacenter, create the following custom resource in the same namespace and Kubernetes cluster as the CassandraDatacenter resource, `cassandradatacenter/dc1` in this case : diff --git a/test/framework/framework.go b/test/framework/framework.go index a795727d3..275788484 100644 --- a/test/framework/framework.go +++ b/test/framework/framework.go @@ -580,6 +580,21 @@ func (f *Framework) DatacenterExists(ctx context.Context, key ClusterKey) func() }) } +func (f *Framework) MedusaConfigExists(ctx context.Context, k8sContext string, medusaConfigKey ClusterKey) func() bool { + remoteClient, found := f.remoteClients[k8sContext] + if !found { + f.logger.Error(f.k8sContextNotFound(k8sContext), "cannot lookup CassandraDatacenter", "context", k8sContext) + return func() bool { return false } + } + medusaConfig := &medusaapi.MedusaConfiguration{} + if err := remoteClient.Get(ctx, medusaConfigKey.NamespacedName, medusaConfig); err != nil { + f.logger.Error(err, "failed to get MedusaConfiguration", "key", medusaConfigKey) + return func() bool { return false } + } else { + return func() bool { return true } + } +} + // NewWithCassTask is a function generator for withCassandraTask that is bound to ctx, and key. func (f *Framework) NewWithCassTask(ctx context.Context, key ClusterKey) func(func(*casstaskapi.CassandraTask) bool) func() bool { return func(condition func(dc *casstaskapi.CassandraTask) bool) func() bool { diff --git a/test/testdata/fixtures/single-dc-encryption-medusa/k8ssandra.yaml b/test/testdata/fixtures/single-dc-encryption-medusa/k8ssandra.yaml index 8e9a52fcd..afe17fa8c 100644 --- a/test/testdata/fixtures/single-dc-encryption-medusa/k8ssandra.yaml +++ b/test/testdata/fixtures/single-dc-encryption-medusa/k8ssandra.yaml @@ -2,6 +2,7 @@ apiVersion: v1 kind: ConfigMap metadata: name: cassandra-config + namespace: k8ssandra-operator data: cassandra.yaml: | concurrent_reads: 32 @@ -12,8 +13,10 @@ apiVersion: k8ssandra.io/v1alpha1 kind: K8ssandraCluster metadata: name: test + namespace: k8ssandra-operator spec: medusa: + storageProperties: storageProvider: s3_compatible bucketName: k8ssandra-medusa