Skip to content

Commit

Permalink
Merge pull request #122 from vshn/add/mariadb_backup
Browse files Browse the repository at this point in the history
Add MariaDB Backups
  • Loading branch information
Kidswiss authored Jan 15, 2024
2 parents fabffdc + 5af8075 commit ecb5cfc
Show file tree
Hide file tree
Showing 17 changed files with 641 additions and 298 deletions.
5 changes: 5 additions & 0 deletions apis/vshn/v1/common_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
10 changes: 10 additions & 0 deletions apis/vshn/v1/dbaas_vshn_mariadb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions apis/vshn/v1/dbaas_vshn_redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
269 changes: 269 additions & 0 deletions pkg/comp-functions/functions/common/backup/backup.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit ecb5cfc

Please sign in to comment.