From 5af8075ec9bfeaeb9bfc2124a84012ea8e70fe45 Mon Sep 17 00:00:00 2001 From: Simon Beck Date: Wed, 10 Jan 2024 15:19:21 +0100 Subject: [PATCH] Add MariaDB Backups * The backup is done via K8up and MariaBackup. The backup command is added via annotation to the pods. MariaBackup streams the backup to stdout, where K8up picks it up. * Generalized a lot of logic which injects the annotations and scripts into any given helm values. --- apis/vshn/v1/common_types.go | 5 + apis/vshn/v1/dbaas_vshn_mariadb.go | 10 + apis/vshn/v1/dbaas_vshn_redis.go | 10 + .../functions/common/backup/backup.go | 269 ++++++++++++++++++ .../functions/common/backup/backup_test.go | 112 ++++++++ .../functions/common/interfaces.go | 12 + .../functions/common/password.go | 6 - .../functions/common/release.go | 21 ++ .../functions/vshnmariadb/backup.go | 114 ++++++++ .../functions/vshnmariadb/backup_test.go | 44 +++ .../functions/vshnmariadb/register.go | 4 + .../functions/vshnmariadb/script/backup.sh | 3 + .../functions/vshnredis/backup.go | 207 +------------- .../functions/vshnredis/backup_test.go | 8 - .../functions/vshnredis/pvcresize.go | 3 +- .../functions/vshnredis/release.go | 85 +----- .../vshnmariadb/deploy/01_default.yaml | 26 +- 17 files changed, 641 insertions(+), 298 deletions(-) create mode 100644 pkg/comp-functions/functions/common/backup/backup.go create mode 100644 pkg/comp-functions/functions/common/backup/backup_test.go create mode 100644 pkg/comp-functions/functions/common/interfaces.go create mode 100644 pkg/comp-functions/functions/common/release.go create mode 100644 pkg/comp-functions/functions/vshnmariadb/backup.go create mode 100644 pkg/comp-functions/functions/vshnmariadb/backup_test.go create mode 100644 pkg/comp-functions/functions/vshnmariadb/script/backup.sh diff --git a/apis/vshn/v1/common_types.go b/apis/vshn/v1/common_types.go index e16b86c257..cd7b78237b 100644 --- a/apis/vshn/v1/common_types.go +++ b/apis/vshn/v1/common_types.go @@ -21,6 +21,11 @@ func (k *K8upBackupSpec) SetBackupSchedule(schedule string) { k.Schedule = schedule } +// GetBackupRetention returns the retention definition for this backup. +func (k *K8upBackupSpec) GetBackupRetention() K8upRetentionPolicy { + return k.Retention +} + // K8upRetentionPolicy describes the retention configuration for a K8up backup. type K8upRetentionPolicy struct { KeepLast int `json:"keepLast,omitempty"` diff --git a/apis/vshn/v1/dbaas_vshn_mariadb.go b/apis/vshn/v1/dbaas_vshn_mariadb.go index 32c6df20ac..2d6ba0b4d2 100644 --- a/apis/vshn/v1/dbaas_vshn_mariadb.go +++ b/apis/vshn/v1/dbaas_vshn_mariadb.go @@ -207,6 +207,16 @@ func (v *VSHNMariaDB) SetBackupSchedule(schedule string) { v.Status.Schedules.Backup = schedule } +// GetBackupRetention returns the retention definition for this backup. +func (v *VSHNMariaDB) GetBackupRetention() K8upRetentionPolicy { + return v.Spec.Parameters.Backup.Retention +} + +// GetServiceName returns the name of this service +func (v *VSHNMariaDB) GetServiceName() string { + return "mariadb" +} + // GetFullMaintenanceSchedule returns func (v *VSHNMariaDB) GetFullMaintenanceSchedule() VSHNDBaaSMaintenanceScheduleSpec { schedule := v.Spec.Parameters.Maintenance diff --git a/apis/vshn/v1/dbaas_vshn_redis.go b/apis/vshn/v1/dbaas_vshn_redis.go index 6d690254cf..823ff08809 100644 --- a/apis/vshn/v1/dbaas_vshn_redis.go +++ b/apis/vshn/v1/dbaas_vshn_redis.go @@ -231,6 +231,16 @@ func (v *VSHNRedis) SetBackupSchedule(schedule string) { v.Status.Schedules.Backup = schedule } +// GetBackupRetention returns the retention definition for this backup. +func (v *VSHNRedis) GetBackupRetention() K8upRetentionPolicy { + return v.Spec.Parameters.Backup.Retention +} + +// GetServiceName returns the name of this service +func (v *VSHNRedis) GetServiceName() string { + return "redis" +} + // GetFullMaintenanceSchedule returns func (v *VSHNRedis) GetFullMaintenanceSchedule() VSHNDBaaSMaintenanceScheduleSpec { schedule := v.Spec.Parameters.Maintenance diff --git a/pkg/comp-functions/functions/common/backup/backup.go b/pkg/comp-functions/functions/common/backup/backup.go new file mode 100644 index 0000000000..3214f553f2 --- /dev/null +++ b/pkg/comp-functions/functions/common/backup/backup.go @@ -0,0 +1,269 @@ +package backup + +import ( + "context" + "fmt" + "strings" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + k8upv1 "github.com/k8up-io/k8up/v2/api/v1" + "github.com/sethvargo/go-password/password" + appcatv1 "github.com/vshn/appcat/v4/apis/v1" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/utils/ptr" + controllerruntime "sigs.k8s.io/controller-runtime" +) + +const ( + credentialSecretName = "backup-bucket-credentials" + k8upRepoSecretName = "k8up-repository-password" + k8upRepoSecretKey = "password" + backupScriptCMName = "backup-script" +) + +// AddK8upBackup creates an S3 bucket and a K8up schedule according to the composition spec. +func AddK8upBackup(ctx context.Context, svc *runtime.ServiceRuntime, comp common.InfoGetter) error { + + l := controllerruntime.LoggerFrom(ctx) + + l.Info("Creating backup bucket") + err := createObjectBucket(ctx, comp, svc) + if err != nil { + return fmt.Errorf("cannot create backup bucket: %w", err) + } + + l.Info("Creating repository password") + err = createRepositoryPassword(ctx, comp, svc) + if err != nil { + return fmt.Errorf("cannot create repository password: %w", err) + } + + l.Info("Creating backup schedule") + err = createK8upSchedule(ctx, comp, svc) + if err != nil { + return fmt.Errorf("cannot create backup schedule, %w", err) + } + + return nil +} + +func createObjectBucket(ctx context.Context, comp common.InfoGetter, svc *runtime.ServiceRuntime) error { + + ob := &appcatv1.XObjectBucket{ + ObjectMeta: metav1.ObjectMeta{ + Name: comp.GetName() + "-backup", + }, + Spec: appcatv1.XObjectBucketSpec{ + Parameters: appcatv1.ObjectBucketParameters{ + BucketName: comp.GetName() + "-backup", + Region: svc.Config.Data["bucketRegion"], + }, + ResourceSpec: xpv1.ResourceSpec{ + WriteConnectionSecretToReference: &xpv1.SecretReference{ + Namespace: comp.GetInstanceNamespace(), + Name: credentialSecretName, + }, + }, + }, + } + + return svc.SetDesiredComposedResource(ob) +} + +func createRepositoryPassword(ctx context.Context, comp common.InfoGetter, svc *runtime.ServiceRuntime) error { + + l := controllerruntime.LoggerFrom(ctx) + + secretName := comp.GetName() + "-k8up-repo-pw" + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: k8upRepoSecretName, + Namespace: comp.GetInstanceNamespace(), + }, + } + + err := svc.GetObservedKubeObject(secret, secretName) + if err != nil && err != runtime.ErrNotFound { + return err + } + + if _, ok := secret.Data[k8upRepoSecretKey]; ok { + l.V(1).Info("secret is not empty") + return svc.SetDesiredKubeObject(secret, secretName) + } + + pw, err := password.Generate(64, 5, 5, false, true) + if err != nil { + return err + } + + secret.Data = map[string][]byte{ + k8upRepoSecretKey: []byte(pw), + } + + return svc.SetDesiredKubeObject(secret, secretName) +} + +func createK8upSchedule(ctx context.Context, comp common.InfoGetter, svc *runtime.ServiceRuntime) error { + + l := controllerruntime.LoggerFrom(ctx) + + cd, err := svc.GetObservedComposedResourceConnectionDetails(comp.GetName() + "-backup") + if err != nil && err == runtime.ErrNotFound { + l.V(1).Info("credential secret not found, skipping schedule") + return nil + } else if err != nil { + return err + } + + bucket := string(cd["BUCKET_NAME"]) + endpoint := string(cd["ENDPOINT_URL"]) + retention := comp.GetBackupRetention() + + endpoint, _ = strings.CutSuffix(endpoint, "/") + + schedule := &k8upv1.Schedule{ + ObjectMeta: metav1.ObjectMeta{ + Name: comp.GetServiceName() + "-schedule", + Namespace: comp.GetInstanceNamespace(), + }, + Spec: k8upv1.ScheduleSpec{ + Backend: &k8upv1.Backend{ + RepoPasswordSecretRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: k8upRepoSecretName, + }, + Key: k8upRepoSecretKey, + }, + S3: &k8upv1.S3Spec{ + Endpoint: endpoint, + Bucket: bucket, + AccessKeyIDSecretRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: credentialSecretName, + }, + Key: "AWS_ACCESS_KEY_ID", + }, + SecretAccessKeySecretRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: credentialSecretName, + }, + Key: "AWS_SECRET_ACCESS_KEY", + }, + }, + }, + Backup: &k8upv1.BackupSchedule{ + ScheduleCommon: &k8upv1.ScheduleCommon{ + Schedule: k8upv1.ScheduleDefinition(comp.GetBackupSchedule()), + }, + BackupSpec: k8upv1.BackupSpec{ + KeepJobs: ptr.To(0), + }, + }, + Prune: &k8upv1.PruneSchedule{ + ScheduleCommon: &k8upv1.ScheduleCommon{ + Schedule: "@weekly-random", + }, + PruneSpec: k8upv1.PruneSpec{ + Retention: k8upv1.RetentionPolicy{ + KeepLast: retention.KeepLast, + KeepHourly: retention.KeepHourly, + KeepDaily: retention.KeepDaily, + KeepWeekly: retention.KeepWeekly, + KeepMonthly: retention.KeepMonthly, + KeepYearly: retention.KeepYearly, + }, + }, + }, + }, + } + + return svc.SetDesiredKubeObject(schedule, comp.GetName()+"-backup-schedule") +} + +// AddPVCAnnotationToValues adds the default exclude annotations to the PVCs via the release values. +func AddPVCAnnotationToValues(valueMap map[string]any, path ...string) error { + annotations := map[string]interface{}{ + "k8up.io/backup": "false", + } + err := unstructured.SetNestedMap(valueMap, annotations, path...) + if err != nil { + return fmt.Errorf("cannot set annotations the helm values for key: master.persistence") + } + + return nil +} + +// AddPodAnnotationToValues add the annotations to trigger the pre-backup script via the release values. +func AddPodAnnotationToValues(valueMap map[string]any, scriptName, fileExt string, path ...string) error { + annotations := map[string]interface{}{ + "k8up.io/backupcommand": scriptName, + "k8up.io/file-extension": fileExt, + } + err := unstructured.SetNestedMap(valueMap, annotations, path...) + if err != nil { + return fmt.Errorf("cannot set annotations the helm values for key: master.podAnnotations") + } + + return nil +} + +// AddBackupCMToValues adds the volume mount for the given configMap to the helm values. +// volumePath and mountPath specify the value path within the values map. +func AddBackupCMToValues(values map[string]any, volumePath []string, mountPath []string) error { + volumes := []interface{}{ + corev1.Volume{ + Name: backupScriptCMName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: backupScriptCMName, + }, + DefaultMode: ptr.To(int32(0774)), + }, + }, + }, + } + + err := setNestedObjectValue(values, volumePath, volumes) + if err != nil { + return err + } + + volumeMounts := []interface{}{ + corev1.VolumeMount{ + Name: backupScriptCMName, + MountPath: "/scripts", + }, + } + + err = setNestedObjectValue(values, mountPath, volumeMounts) + if err != nil { + return err + } + + return nil +} + +// setNestedObjectValue is necessary as unstructured can't handle anything except basic values and maps. +// this is a recursive function, it will traverse the map until it reaches the last element of the path. +// If it encounters any non-map values while traversing, it will throw an error. +func setNestedObjectValue(values map[string]interface{}, path []string, val interface{}) error { + + if len(path) == 1 { + values[path[0]] = val + return nil + } + + tmpVals, ok := values[path[0]].(map[string]interface{}) + if !ok { + return fmt.Errorf("cannot traverse map, value at field %s is not a map", path[0]) + } + + return setNestedObjectValue(tmpVals, path[1:], val) +} diff --git a/pkg/comp-functions/functions/common/backup/backup_test.go b/pkg/comp-functions/functions/common/backup/backup_test.go new file mode 100644 index 0000000000..e8d1462902 --- /dev/null +++ b/pkg/comp-functions/functions/common/backup/backup_test.go @@ -0,0 +1,112 @@ +package backup + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + appcatv1 "github.com/vshn/appcat/v4/apis/v1" + vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/commontest" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" + corev1 "k8s.io/api/core/v1" +) + +func TestAddBackupObjectCreation(t *testing.T) { + svc, comp := getRedisBackupComp(t) + + ctx := context.TODO() + + assert.Nil(t, AddK8upBackup(ctx, svc, comp)) + + bucket := &appcatv1.XObjectBucket{} + assert.NoError(t, svc.GetDesiredComposedResourceByName(bucket, comp.Name+"-backup")) + + repoPW := &corev1.Secret{} + assert.NoError(t, svc.GetDesiredKubeObject(repoPW, comp.Name+"-k8up-repo-pw")) + +} + +func getRedisBackupComp(t *testing.T) (*runtime.ServiceRuntime, *vshnv1.VSHNRedis) { + svc := commontest.LoadRuntimeFromFile(t, "vshnredis/backup/01_default.yaml") + + comp := &vshnv1.VSHNRedis{} + err := svc.GetDesiredComposite(comp) + assert.NoError(t, err) + + return svc, comp +} + +func Test_setNestedValue(t *testing.T) { + type args struct { + values map[string]interface{} + path []string + val interface{} + } + tests := []struct { + name string + args args + want map[string]interface{} + wantErr bool + }{ + { + name: "GivenPathOfOneLevel_ThenInsertIt", + args: args{ + values: map[string]interface{}{ + "test": "", + }, + path: []string{"test"}, + val: "hello", + }, + want: map[string]interface{}{ + "test": "hello", + }, + }, + { + name: "GivenPathOfTwoLevels_ThenInsertIt", + args: args{ + values: map[string]interface{}{ + "test": map[string]interface{}{ + "test2": "", + }, + }, + path: []string{"test", "test2"}, + val: "hello", + }, + want: map[string]interface{}{ + "test": map[string]interface{}{ + "test2": "hello", + }, + }, + }, + { + name: "GivenPathOfThreeLevels_ThenInsertIt", + args: args{ + values: map[string]interface{}{ + "test": map[string]interface{}{ + "test2": map[string]interface{}{ + "test3": "", + }, + }, + }, + path: []string{"test", "test2", "test3"}, + val: "hello", + }, + want: map[string]interface{}{ + "test": map[string]interface{}{ + "test2": map[string]interface{}{ + "test3": "hello", + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := setNestedObjectValue(tt.args.values, tt.args.path, tt.args.val); (err != nil) != tt.wantErr { + t.Errorf("setNestedValue() error = %v, wantErr %v", err, tt.wantErr) + } + assert.Equal(t, tt.want, tt.args.values) + }) + } +} diff --git a/pkg/comp-functions/functions/common/interfaces.go b/pkg/comp-functions/functions/common/interfaces.go new file mode 100644 index 0000000000..612c3cb51a --- /dev/null +++ b/pkg/comp-functions/functions/common/interfaces.go @@ -0,0 +1,12 @@ +package common + +import vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" + +// InfoGetter will return various information about the given AppCat composite. +type InfoGetter interface { + GetName() string + GetInstanceNamespace() string + GetBackupSchedule() string + GetBackupRetention() vshnv1.K8upRetentionPolicy + GetServiceName() string +} diff --git a/pkg/comp-functions/functions/common/password.go b/pkg/comp-functions/functions/common/password.go index 5816b1011f..0d5b357aa0 100644 --- a/pkg/comp-functions/functions/common/password.go +++ b/pkg/comp-functions/functions/common/password.go @@ -9,12 +9,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// InfoGetter will return the name of the given object. -type InfoGetter interface { - GetName() string - GetInstanceNamespace() string -} - // AddCredentialsSecret creates secrets and passwords for use with helm based services. // This is to avoid issues with re-generating passwords if helm internal password generators are used. // The function accepts a list of fields that should be populated with passwords. diff --git a/pkg/comp-functions/functions/common/release.go b/pkg/comp-functions/functions/common/release.go new file mode 100644 index 0000000000..75bf72c946 --- /dev/null +++ b/pkg/comp-functions/functions/common/release.go @@ -0,0 +1,21 @@ +package common + +import ( + "encoding/json" + "fmt" + + xhelmv1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" +) + +// GetReleaseValues returns the parsed values from the given release. +func GetReleaseValues(r *xhelmv1.Release) (map[string]interface{}, error) { + values := map[string]interface{}{} + if r.Spec.ForProvider.Values.Raw == nil { + return values, nil + } + err := json.Unmarshal(r.Spec.ForProvider.Values.Raw, &values) + if err != nil { + return nil, fmt.Errorf("cannot unmarshal values from release: %v", err) + } + return values, nil +} diff --git a/pkg/comp-functions/functions/vshnmariadb/backup.go b/pkg/comp-functions/functions/vshnmariadb/backup.go new file mode 100644 index 0000000000..32e754706e --- /dev/null +++ b/pkg/comp-functions/functions/vshnmariadb/backup.go @@ -0,0 +1,114 @@ +package vshnmariadb + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + + xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" + xhelmv1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" + vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common/backup" + "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + controllerruntime "sigs.k8s.io/controller-runtime" +) + +//go:embed script/backup.sh +var mariadbBackupScript string + +// AddBackupMariadb adds k8up backup to a MariaDB deployment. +func AddBackupMariadb(ctx context.Context, svc *runtime.ServiceRuntime) *xfnproto.Result { + l := controllerruntime.LoggerFrom(ctx) + + comp := &vshnv1.VSHNMariaDB{} + err := svc.GetObservedComposite(comp) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("failed to parse composite: %w", err)) + } + + common.SetRandomSchedules(comp, comp) + + err = svc.SetDesiredCompositeStatus(comp) + if err != nil { + return runtime.NewFatalResult(fmt.Errorf("failed to set composite: %w", err)) + } + + err = backup.AddK8upBackup(ctx, svc, comp) + if err != nil { + return runtime.NewWarningResult(fmt.Sprintf("cannot create backup: %s", err.Error())) + } + + l.Info("Adding backup script config map") + err = addBackupScriptCM(svc, comp) + if err != nil { + return runtime.NewWarningResult(fmt.Sprintf("cannot create backup script configMap: %s", err.Error())) + } + + l.Info("Updating the release object") + err = updateRelease(ctx, svc, comp) + if err != nil { + return runtime.NewWarningResult(fmt.Sprintf("cannot update release: %s", err.Error())) + } + + return nil +} + +func addBackupScriptCM(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNMariaDB) error { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "backup-script", + Namespace: comp.GetInstanceNamespace(), + }, + Data: map[string]string{ + "backup.sh": mariadbBackupScript, + }, + } + + return svc.SetDesiredKubeObject(cm, comp.GetName()+"-backup-script") +} + +func updateRelease(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.VSHNMariaDB) error { + l := controllerruntime.LoggerFrom(ctx) + + release := &xhelmv1.Release{} + + err := svc.GetDesiredComposedResourceByName(release, comp.GetName()+"-release") + if err != nil { + return err + } + + values, err := common.GetReleaseValues(release) + if err != nil { + return err + } + + l.Info("Adding the PVC k8up annotations") + err = backup.AddPVCAnnotationToValues(values, "persistence", "annotations") + if err != nil { + return err + } + + l.Info("Adding the Pod k8up annotations") + err = backup.AddPodAnnotationToValues(values, "/scripts/backup.sh", ".xb", "podAnnotations") + if err != nil { + return err + } + + l.Info("Mounting CM into pod") + err = backup.AddBackupCMToValues(values, []string{"extraVolumes"}, []string{"extraVolumeMounts"}) + if err != nil { + return err + } + + byteValues, err := json.Marshal(values) + if err != nil { + return err + } + release.Spec.ForProvider.Values.Raw = byteValues + + return svc.SetDesiredComposedResourceWithName(release, comp.GetName()+"-release") +} diff --git a/pkg/comp-functions/functions/vshnmariadb/backup_test.go b/pkg/comp-functions/functions/vshnmariadb/backup_test.go new file mode 100644 index 0000000000..35bf061fa3 --- /dev/null +++ b/pkg/comp-functions/functions/vshnmariadb/backup_test.go @@ -0,0 +1,44 @@ +package vshnmariadb + +import ( + "context" + _ "embed" + "encoding/json" + "testing" + + "github.com/stretchr/testify/assert" + xhelmv1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func Test_AddBackupMariadb(t *testing.T) { + svc, comp := getMariadbComp(t) + + assert.Nil(t, AddBackupMariadb(context.TODO(), svc)) + + cm := &corev1.ConfigMap{} + assert.NoError(t, svc.GetDesiredKubeObject(cm, comp.GetName()+"-backup-script")) + + release := &xhelmv1.Release{} + assert.NoError(t, svc.GetDesiredComposedResourceByName(release, comp.GetName()+"-release")) + + assert.NotNil(t, release.Spec.ForProvider.Values) + + values := map[string]interface{}{} + + assert.NoError(t, json.Unmarshal(release.Spec.ForProvider.Values.Raw, &values)) + + _, _, err := unstructured.NestedMap(values, "persistence", "annotations") + assert.NoError(t, err) + + _, _, err = unstructured.NestedStringMap(values, "podAnnotations") + assert.NoError(t, err) + + _, _, err = unstructured.NestedFieldNoCopy(values, "extraVolumes") + assert.NoError(t, err) + + _, _, err = unstructured.NestedFieldNoCopy(values, "extraVolumeMounts") + assert.NoError(t, err) + +} diff --git a/pkg/comp-functions/functions/vshnmariadb/register.go b/pkg/comp-functions/functions/vshnmariadb/register.go index 1b87ca7a69..01cc29ed9f 100644 --- a/pkg/comp-functions/functions/vshnmariadb/register.go +++ b/pkg/comp-functions/functions/vshnmariadb/register.go @@ -14,6 +14,10 @@ func init() { Name: "maintenance", Execute: AddMaintenanceJob, }, + { + Name: "backup", + Execute: AddBackupMariadb, + }, }, }) } diff --git a/pkg/comp-functions/functions/vshnmariadb/script/backup.sh b/pkg/comp-functions/functions/vshnmariadb/script/backup.sh new file mode 100644 index 0000000000..bbc1ea4628 --- /dev/null +++ b/pkg/comp-functions/functions/vshnmariadb/script/backup.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mariadb-backup --user=root --password="$MARIADB_ROOT_PASSWORD" --backup --stream=xbstream diff --git a/pkg/comp-functions/functions/vshnredis/backup.go b/pkg/comp-functions/functions/vshnredis/backup.go index ecd3a1c71a..e11ceec252 100644 --- a/pkg/comp-functions/functions/vshnredis/backup.go +++ b/pkg/comp-functions/functions/vshnredis/backup.go @@ -4,29 +4,19 @@ import ( "context" _ "embed" "fmt" - "strings" - xkube "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1" - xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" - k8upv1 "github.com/k8up-io/k8up/v2/api/v1" - "github.com/sethvargo/go-password/password" - appcatv1 "github.com/vshn/appcat/v4/apis/v1" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common/backup" "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - k8sruntime "k8s.io/apimachinery/pkg/runtime" - "k8s.io/utils/ptr" controllerruntime "sigs.k8s.io/controller-runtime" ) const ( - credentialSecretName = "backup-bucket-credentials" - k8upRepoSecretName = "k8up-repository-password" - k8upRepoSecretKey = "password" - backupScriptCMName = "backup-script" + backupScriptCMName = "backup-script" ) //go:embed script/backup.sh @@ -50,28 +40,9 @@ func AddBackup(ctx context.Context, svc *runtime.ServiceRuntime) *xfnproto.Resul return runtime.NewFatalResult(fmt.Errorf("failed to set composite: %w", err)) } - l.Info("Creating backup bucket") - err = createObjectBucket(ctx, comp, svc) + err = backup.AddK8upBackup(ctx, svc, comp) if err != nil { - return runtime.NewFatalResult(fmt.Errorf("cannot create backup bucket: %w", err)) - } - - l.Info("Creating credential observer") - err = createObjectBucketCredentialObserver(ctx, comp, svc) - if err != nil { - return runtime.NewFatalResult(fmt.Errorf("cannot create credential observer: %w", err)) - } - - l.Info("Creating repository password") - err = createRepositoryPassword(ctx, comp, svc) - if err != nil { - return runtime.NewFatalResult(fmt.Errorf("cannot create repository password: %w", err)) - } - - l.Info("Creating backup schedule") - err = createK8upSchedule(ctx, comp, svc) - if err != nil { - return runtime.NewFatalResult(fmt.Errorf("cannot create backup schedule, %w", err)) + return runtime.NewWarningResult(fmt.Sprintf("cannot add k8up backup: %s", err.Error())) } l.Info("Creating backup config map") @@ -83,176 +54,6 @@ func AddBackup(ctx context.Context, svc *runtime.ServiceRuntime) *xfnproto.Resul return nil } -func createObjectBucket(ctx context.Context, comp *vshnv1.VSHNRedis, svc *runtime.ServiceRuntime) error { - - ob := &appcatv1.XObjectBucket{ - ObjectMeta: metav1.ObjectMeta{ - Name: comp.Name + "-backup", - }, - Spec: appcatv1.XObjectBucketSpec{ - Parameters: appcatv1.ObjectBucketParameters{ - BucketName: comp.Name + "-backup", - Region: svc.Config.Data["bucketRegion"], - }, - ResourceSpec: xpv1.ResourceSpec{ - WriteConnectionSecretToReference: &xpv1.SecretReference{ - Namespace: getInstanceNamespace(comp), - Name: credentialSecretName, - }, - }, - }, - } - - return svc.SetDesiredComposedResource(ob) -} - -func createObjectBucketCredentialObserver(ctx context.Context, comp *vshnv1.VSHNRedis, svc *runtime.ServiceRuntime) error { - - secret := &corev1.Secret{ - TypeMeta: metav1.TypeMeta{ - Kind: "Secret", - APIVersion: "v1", - }, - ObjectMeta: metav1.ObjectMeta{ - Name: credentialSecretName, - Namespace: getInstanceNamespace(comp), - }, - } - - xobj := &xkube.Object{ - ObjectMeta: metav1.ObjectMeta{ - Name: comp.Name + "-backup-credential-observer", - }, - Spec: xkube.ObjectSpec{ - ManagementPolicy: xkube.Observe, - ForProvider: xkube.ObjectParameters{ - Manifest: k8sruntime.RawExtension{ - Object: secret, - }, - }, - ResourceSpec: xkube.ResourceSpec{ - ProviderConfigReference: &xpv1.Reference{ - Name: "kubernetes", - }, - }, - }, - } - - return svc.SetDesiredComposedResource(xobj) -} - -func createRepositoryPassword(ctx context.Context, comp *vshnv1.VSHNRedis, svc *runtime.ServiceRuntime) error { - - l := controllerruntime.LoggerFrom(ctx) - - secret := &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: k8upRepoSecretName, - Namespace: getInstanceNamespace(comp), - }, - } - - err := svc.GetObservedKubeObject(secret, comp.Name+"-k8up-repo-pw") - if err != nil && err != runtime.ErrNotFound { - return err - } - - if _, ok := secret.Data[k8upRepoSecretKey]; ok { - l.V(1).Info("secret is not empty") - return svc.SetDesiredKubeObject(secret, comp.Name+"-k8up-repo-pw") - } - - pw, err := password.Generate(64, 5, 5, false, true) - if err != nil { - return err - } - - secret.Data = map[string][]byte{ - k8upRepoSecretKey: []byte(pw), - } - - return svc.SetDesiredKubeObject(secret, comp.Name+"-k8up-repo-pw") -} - -func createK8upSchedule(ctx context.Context, comp *vshnv1.VSHNRedis, svc *runtime.ServiceRuntime) error { - - l := controllerruntime.LoggerFrom(ctx) - - creds := &corev1.Secret{} - - err := svc.GetObservedKubeObject(creds, comp.Name+"-backup-credential-observer") - if err != nil && err == runtime.ErrNotFound { - l.V(1).Info("credential secret not found, skipping schedule") - return nil - } else if err != nil { - return err - } - - bucket := string(creds.Data["BUCKET_NAME"]) - endpoint := string(creds.Data["ENDPOINT_URL"]) - retention := comp.Spec.Parameters.Backup.Retention - - endpoint, _ = strings.CutSuffix(endpoint, "/") - - schedule := &k8upv1.Schedule{ - ObjectMeta: metav1.ObjectMeta{ - Name: "redis-schedule", - Namespace: getInstanceNamespace(comp), - }, - Spec: k8upv1.ScheduleSpec{ - Backend: &k8upv1.Backend{ - RepoPasswordSecretRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: k8upRepoSecretName, - }, - Key: k8upRepoSecretKey, - }, - S3: &k8upv1.S3Spec{ - Endpoint: endpoint, - Bucket: bucket, - AccessKeyIDSecretRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: credentialSecretName, - }, - Key: "AWS_ACCESS_KEY_ID", - }, - SecretAccessKeySecretRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: credentialSecretName, - }, - Key: "AWS_SECRET_ACCESS_KEY", - }, - }, - }, - Backup: &k8upv1.BackupSchedule{ - ScheduleCommon: &k8upv1.ScheduleCommon{ - Schedule: k8upv1.ScheduleDefinition(comp.GetBackupSchedule()), - }, - BackupSpec: k8upv1.BackupSpec{ - KeepJobs: ptr.To(0), - }, - }, - Prune: &k8upv1.PruneSchedule{ - ScheduleCommon: &k8upv1.ScheduleCommon{ - Schedule: "@weekly-random", - }, - PruneSpec: k8upv1.PruneSpec{ - Retention: k8upv1.RetentionPolicy{ - KeepLast: retention.KeepLast, - KeepHourly: retention.KeepHourly, - KeepDaily: retention.KeepDaily, - KeepWeekly: retention.KeepWeekly, - KeepMonthly: retention.KeepMonthly, - KeepYearly: retention.KeepYearly, - }, - }, - }, - }, - } - - return svc.SetDesiredKubeObject(schedule, comp.Name+"-backup-schedule") -} - func createScriptCM(ctx context.Context, comp *vshnv1.VSHNRedis, svc *runtime.ServiceRuntime) error { cm := &corev1.ConfigMap{ diff --git a/pkg/comp-functions/functions/vshnredis/backup_test.go b/pkg/comp-functions/functions/vshnredis/backup_test.go index f5b849b804..44949f1fe3 100644 --- a/pkg/comp-functions/functions/vshnredis/backup_test.go +++ b/pkg/comp-functions/functions/vshnredis/backup_test.go @@ -6,8 +6,6 @@ import ( "github.com/vshn/appcat/v4/pkg/comp-functions/functions/commontest" - xkube "github.com/crossplane-contrib/provider-kubernetes/apis/object/v1alpha1" - k8upv1 "github.com/k8up-io/k8up/v2/api/v1" "github.com/stretchr/testify/assert" appcatv1 "github.com/vshn/appcat/v4/apis/v1" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" @@ -25,15 +23,9 @@ func TestAddBackupObjectCreation(t *testing.T) { bucket := &appcatv1.XObjectBucket{} assert.NoError(t, svc.GetDesiredComposedResourceByName(bucket, comp.Name+"-backup")) - observer := &xkube.Object{} - assert.NoError(t, svc.GetDesiredComposedResourceByName(observer, comp.Name+"-backup-credential-observer")) - repoPW := &corev1.Secret{} assert.NoError(t, svc.GetDesiredKubeObject(repoPW, comp.Name+"-k8up-repo-pw")) - schedule := &k8upv1.Schedule{} - assert.NoError(t, svc.GetDesiredKubeObject(schedule, comp.Name+"-backup-credential-observer")) - cm := &corev1.ConfigMap{} assert.NoError(t, svc.GetDesiredKubeObject(cm, comp.Name+"-backup-cm")) diff --git a/pkg/comp-functions/functions/vshnredis/pvcresize.go b/pkg/comp-functions/functions/vshnredis/pvcresize.go index a129aba553..5dc3aff15c 100644 --- a/pkg/comp-functions/functions/vshnredis/pvcresize.go +++ b/pkg/comp-functions/functions/vshnredis/pvcresize.go @@ -10,6 +10,7 @@ import ( xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1" helmv1beta1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" @@ -40,7 +41,7 @@ func ResizePVCs(ctx context.Context, svc *runtime.ServiceRuntime) *xfnproto.Resu return runtime.NewFatalResult(fmt.Errorf("cannot get release: %w", err)) } - values, err := getReleaseValues(release) + values, err := common.GetReleaseValues(release) if err != nil { return runtime.NewFatalResult(fmt.Errorf("cannot parse release values: %w", err)) } diff --git a/pkg/comp-functions/functions/vshnredis/release.go b/pkg/comp-functions/functions/vshnredis/release.go index 9ce6cae3cd..c891e7c6a6 100644 --- a/pkg/comp-functions/functions/vshnredis/release.go +++ b/pkg/comp-functions/functions/vshnredis/release.go @@ -11,11 +11,11 @@ import ( xhelmv1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1" vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1" "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common" + "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common/backup" "github.com/vshn/appcat/v4/pkg/comp-functions/functions/common/maintenance" "github.com/vshn/appcat/v4/pkg/comp-functions/runtime" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/utils/ptr" controllerruntime "sigs.k8s.io/controller-runtime" ) @@ -91,11 +91,11 @@ func updateRelease(ctx context.Context, comp *vshnv1.VSHNRedis, desired *xhelmv1 l.Info("Getting helm values") releaseName := desired.Name - values, err := getReleaseValues(desired) + values, err := common.GetReleaseValues(desired) if err != nil { return nil, err } - observedValues, err := getReleaseValues(observed) + observedValues, err := common.GetReleaseValues(observed) if err != nil { return nil, err } @@ -107,19 +107,19 @@ func updateRelease(ctx context.Context, comp *vshnv1.VSHNRedis, desired *xhelmv1 } l.V(1).Info("Adding PVC annotations") - err = addPVCAnnotation(values) + err = backup.AddPVCAnnotationToValues(values, "master", "persistence", "annotations") if err != nil { return nil, fmt.Errorf("cannot add pvc annotations for release %s: %v", releaseName, err) } l.V(1).Info("Adding pod annotations") - err = addPodAnnotation(values) + err = backup.AddPodAnnotationToValues(values, "/scripts/backup.sh", ".tar", "master", "podAnnotations") if err != nil { return nil, fmt.Errorf("cannot add pod annotations for release %s: %v", releaseName, err) } l.V(1).Info("Adding backup config map") - err = addBackupCM(values) + err = backup.AddBackupCMToValues(values, []string{"master", "extraVolumes"}, []string{"master", "extraVolumeMounts"}) if err != nil { return nil, fmt.Errorf("cannot add configmap for release %s: %v", releaseName, err) } @@ -144,79 +144,6 @@ func updateRelease(ctx context.Context, comp *vshnv1.VSHNRedis, desired *xhelmv1 return desired, nil } -func addPVCAnnotation(valueMap map[string]any) error { - annotations := map[string]interface{}{ - "k8up.io/backup": "false", - } - err := unstructured.SetNestedMap(valueMap, annotations, "master", "persistence", "annotations") - if err != nil { - return fmt.Errorf("cannot set annotations the helm values for key: master.persistence") - } - - return nil -} - -func addPodAnnotation(valueMap map[string]any) error { - annotations := map[string]interface{}{ - "k8up.io/backupcommand": "/scripts/backup.sh", - "k8up.io/file-extension": ".tar", - } - err := unstructured.SetNestedMap(valueMap, annotations, "master", "podAnnotations") - if err != nil { - return fmt.Errorf("cannot set annotations the helm values for key: master.podAnnotations") - } - - return nil -} - -func addBackupCM(valueMap map[string]any) error { - masterMap, ok := valueMap["master"].(map[string]any) - if !ok { - return fmt.Errorf("cannot parse the helm values for key: master") - } - - volumes := []corev1.Volume{ - { - Name: backupScriptCMName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: backupScriptCMName, - }, - DefaultMode: ptr.To(int32(0774)), - }, - }, - }, - } - - masterMap["extraVolumes"] = volumes - - volumeMounts := []corev1.VolumeMount{ - { - Name: backupScriptCMName, - MountPath: "/scripts", - }, - } - - masterMap["extraVolumeMounts"] = volumeMounts - - valueMap["master"] = masterMap - - return nil -} - -func getReleaseValues(r *xhelmv1.Release) (map[string]interface{}, error) { - values := map[string]interface{}{} - if r.Spec.ForProvider.Values.Raw == nil { - return values, nil - } - err := json.Unmarshal(r.Spec.ForProvider.Values.Raw, &values) - if err != nil { - return nil, fmt.Errorf("cannot unmarshal values from release: %v", err) - } - return values, nil -} - func addPassword(valueMap map[string]interface{}, secretName string) error { err := unstructured.SetNestedField(valueMap, secretName, "auth", "existingSecret") if err != nil { diff --git a/test/functions/vshnmariadb/deploy/01_default.yaml b/test/functions/vshnmariadb/deploy/01_default.yaml index 4f7ac07a51..d9fb629520 100644 --- a/test/functions/vshnmariadb/deploy/01_default.yaml +++ b/test/functions/vshnmariadb/deploy/01_default.yaml @@ -1,4 +1,28 @@ -desired: {} +desired: + resources: + mariadb-gc9x4-release: + connection_details: + MARIADB_USER: cm9vdA== #root + resource: + apiVersion: helm.crossplane.io/v1beta1 + kind: Release + spec: + forProvider: + chart: + name: mariadb-galera + repository: https://charts.bitnami.com/bitnami + values: + fullnameOverride: mariadb + persistence: + size: 50Gi + replicasCount: 1 + resources: + limits: + cpu: 1 + memory: 1Gi + requests: + cpu: 1 + memory: 1Gi input: apiVersion: v1 data: