Skip to content

Commit

Permalink
feat(snapshots): encode restore spec in backup cr for restore (#5042)
Browse files Browse the repository at this point in the history
  • Loading branch information
emosbaugh authored Dec 12, 2024
1 parent d86003b commit acfe1ac
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 25 deletions.
48 changes: 39 additions & 9 deletions pkg/kotsadmsnapshot/backup.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package snapshot

import (
"bytes"
"context"
"crypto/sha256"
"encoding/json"
Expand Down Expand Up @@ -39,8 +40,10 @@ import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
serializer "k8s.io/apimachinery/pkg/runtime/serializer/json"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/utils/ptr"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
)
Expand Down Expand Up @@ -236,7 +239,7 @@ func CreateInstanceBackup(ctx context.Context, cluster *downstreamtypes.Downstre

ctrlClient, err := k8sutil.GetKubeClient(ctx)
if err != nil {
return "", fmt.Errorf("failed to get kubeclient: %w", err)
return "", errors.Wrap(err, "failed to get kubeclient")
}

veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg)
Expand Down Expand Up @@ -324,6 +327,30 @@ func GetInstanceBackupCount(veleroBackup velerov1.Backup) int {
return 1
}

// GetInstanceBackupCount returns the restore CR from the velero backup object annotation.
func GetInstanceBackupRestore(veleroBackup velerov1.Backup) (*velerov1.Restore, error) {
restoreSpec := veleroBackup.GetAnnotations()[types.InstanceBackupRestoreSpecAnnotation]
if restoreSpec == "" {
return nil, nil
}

restore, err := kotsutil.LoadRestoreFromContents([]byte(restoreSpec))
if err != nil {
return nil, errors.Wrap(err, "failed to load restore from contents")
}

return restore, nil
}

func encodeRestoreSpec(restore *velerov1.Restore) (string, error) {
var b bytes.Buffer
s := serializer.NewSerializer(serializer.DefaultMetaFactory, scheme.Scheme, scheme.Scheme, false)
if err := s.Encode(restore, &b); err != nil {
return "", errors.Wrap(err, "failed to encode restore")
}
return strings.TrimSpace(b.String()), nil
}

// getInstanceBackupMetadata returns metadata about the instance backup for use in creating an
// instance backup.
func getInstanceBackupMetadata(ctx context.Context, k8sClient kubernetes.Interface, ctrlClient ctrlclient.Client, veleroClient veleroclientv1.VeleroV1Interface, cluster *downstreamtypes.Downstream, isScheduled bool) (instanceBackupMetadata, error) {
Expand Down Expand Up @@ -391,7 +418,7 @@ func getInstanceBackupMetadata(ctx context.Context, k8sClient kubernetes.Interfa

kotsKinds, err := kotsutil.LoadKotsKinds(archiveDir)
if err != nil {
return metadata, errors.Wrap(err, "failed to load kots kinds from path")
return metadata, errors.Wrapf(err, "failed to load kots kinds from path for app %s", app.Slug)
}

metadata.apps[app.Slug] = appInstanceBackupMetadata{
Expand Down Expand Up @@ -426,12 +453,12 @@ func getECInstanceBackupMetadata(ctx context.Context, ctrlClient ctrlclient.Clie

installation, err := embeddedcluster.GetCurrentInstallation(ctx, ctrlClient)
if err != nil {
return nil, fmt.Errorf("failed to get current installation: %w", err)
return nil, errors.Wrap(err, "failed to get current installation")
}

seaweedFSS3ServiceIP, err := embeddedcluster.GetSeaweedFSS3ServiceIP(ctx, ctrlClient)
if err != nil {
return nil, fmt.Errorf("failed to get seaweedfs s3 service ip: %w", err)
return nil, errors.Wrap(err, "failed to get seaweedfs s3 service ip")
}

return &ecInstanceBackupMetadata{
Expand Down Expand Up @@ -534,6 +561,7 @@ func getAppInstanceBackupSpec(k8sClient kubernetes.Interface, metadata instanceB
}

var appVeleroBackup *velerov1.Backup
var restore *velerov1.Restore

for _, appMeta := range metadata.apps {
// if there is both a backup and a restore spec this is using the new improved DR
Expand All @@ -545,11 +573,8 @@ func getAppInstanceBackupSpec(k8sClient kubernetes.Interface, metadata instanceB
return nil, errors.New("cannot create backup for Embedded Cluster with multiple apps")
}

if appMeta.kotsKinds.Backup == nil {
return nil, errors.New("backup spec is empty, this is unexpected")
}

appVeleroBackup = appMeta.kotsKinds.Backup.DeepCopy()
restore = appMeta.kotsKinds.Restore.DeepCopy()

appVeleroBackup.Name = ""
appVeleroBackup.GenerateName = "application-"
Expand All @@ -561,7 +586,11 @@ func getAppInstanceBackupSpec(k8sClient kubernetes.Interface, metadata instanceB
return nil, nil
}

var err error
restoreSpec, err := encodeRestoreSpec(restore)
if err != nil {
return nil, errors.Wrap(err, "failed to encode restore spec")
}

appVeleroBackup.Annotations, err = appendCommonAnnotations(k8sClient, appVeleroBackup.Annotations, metadata)
if err != nil {
return nil, errors.Wrap(err, "failed to add annotations to application backup")
Expand All @@ -573,6 +602,7 @@ func getAppInstanceBackupSpec(k8sClient kubernetes.Interface, metadata instanceB
appVeleroBackup.Labels[types.InstanceBackupNameLabel] = metadata.backupName
appVeleroBackup.Annotations[types.InstanceBackupTypeAnnotation] = types.InstanceBackupTypeApp
appVeleroBackup.Annotations[types.InstanceBackupCountAnnotation] = strconv.Itoa(2)
appVeleroBackup.Annotations[types.InstanceBackupRestoreSpecAnnotation] = restoreSpec

appVeleroBackup.Spec.StorageLocation = "default"

Expand Down
110 changes: 94 additions & 16 deletions pkg/kotsadmsnapshot/backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"reflect"
"testing"
"time"

Expand Down Expand Up @@ -1626,7 +1627,18 @@ func Test_getAppInstanceBackupSpec(t *testing.T) {
},
},
},
Restore: &velerov1.Restore{},
Restore: &velerov1.Restore{
TypeMeta: metav1.TypeMeta{
APIVersion: "velero.io/v1",
Kind: "Restore",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-restore",
},
Spec: velerov1.RestoreSpec{
BackupName: "test-backup",
},
},
}

ecMeta := &ecInstanceBackupMetadata{
Expand Down Expand Up @@ -1836,8 +1848,8 @@ func Test_getAppInstanceBackupSpec(t *testing.T) {
},
assert: func(t *testing.T, got *velerov1.Backup, err error) {
require.NoError(t, err)
if assert.Contains(t, got.Labels, "replicated.com/backup-name") {
assert.Equal(t, "app-1-17332487841234", got.Labels["replicated.com/backup-name"])
if assert.Contains(t, got.Labels, types.InstanceBackupNameLabel) {
assert.Equal(t, "app-1-17332487841234", got.Labels[types.InstanceBackupNameLabel])
}
},
},
Expand Down Expand Up @@ -1867,11 +1879,14 @@ func Test_getAppInstanceBackupSpec(t *testing.T) {
},
assert: func(t *testing.T, got *velerov1.Backup, err error) {
require.NoError(t, err)
if assert.Contains(t, got.Annotations, "replicated.com/backup-type") {
assert.Equal(t, "app", got.Annotations["replicated.com/backup-type"])
if assert.Contains(t, got.Annotations, types.InstanceBackupTypeAnnotation) {
assert.Equal(t, types.InstanceBackupTypeApp, got.Annotations[types.InstanceBackupTypeAnnotation])
}
if assert.Contains(t, got.Annotations, types.InstanceBackupCountAnnotation) {
assert.Equal(t, "2", got.Annotations[types.InstanceBackupCountAnnotation])
}
if assert.Contains(t, got.Annotations, "replicated.com/backup-count") {
assert.Equal(t, "2", got.Annotations["replicated.com/backup-count"])
if assert.Contains(t, got.Annotations, types.InstanceBackupRestoreSpecAnnotation) {
assert.Equal(t, `{"kind":"Restore","apiVersion":"velero.io/v1","metadata":{"name":"test-restore","creationTimestamp":null},"spec":{"backupName":"test-backup","hooks":{}},"status":{}}`, got.Annotations[types.InstanceBackupRestoreSpecAnnotation])
}
},
},
Expand Down Expand Up @@ -2347,9 +2362,9 @@ func Test_getInfrastructureInstanceBackupSpec(t *testing.T) {
},
assert: func(t *testing.T, got *velerov1.Backup, err error) {
require.NoError(t, err)
assert.NotContains(t, got.Labels, "replicated.com/backup-name")
assert.NotContains(t, got.Annotations, "replicated.com/backup-type")
assert.NotContains(t, got.Annotations, "replicated.com/backup-count")
assert.NotContains(t, got.Labels, types.InstanceBackupNameLabel)
assert.NotContains(t, got.Annotations, types.InstanceBackupTypeAnnotation)
assert.NotContains(t, got.Annotations, types.InstanceBackupCountAnnotation)
},
},
{
Expand Down Expand Up @@ -2379,14 +2394,14 @@ func Test_getInfrastructureInstanceBackupSpec(t *testing.T) {
},
assert: func(t *testing.T, got *velerov1.Backup, err error) {
require.NoError(t, err)
if assert.Contains(t, got.Labels, "replicated.com/backup-name") {
assert.Equal(t, "app-1-17332487841234", got.Labels["replicated.com/backup-name"])
if assert.Contains(t, got.Labels, types.InstanceBackupNameLabel) {
assert.Equal(t, "app-1-17332487841234", got.Labels[types.InstanceBackupNameLabel])
}
if assert.Contains(t, got.Annotations, "replicated.com/backup-type") {
assert.Equal(t, "infra", got.Annotations["replicated.com/backup-type"])
if assert.Contains(t, got.Annotations, types.InstanceBackupTypeAnnotation) {
assert.Equal(t, types.InstanceBackupTypeInfra, got.Annotations[types.InstanceBackupTypeAnnotation])
}
if assert.Contains(t, got.Annotations, "replicated.com/backup-count") {
assert.Equal(t, "2", got.Annotations["replicated.com/backup-count"])
if assert.Contains(t, got.Annotations, types.InstanceBackupCountAnnotation) {
assert.Equal(t, "2", got.Annotations[types.InstanceBackupCountAnnotation])
}
},
},
Expand Down Expand Up @@ -3386,3 +3401,66 @@ func TestListInstanceBackups(t *testing.T) {
})
}
}

func TestGetInstanceBackupRestore(t *testing.T) {
type args struct {
veleroBackup velerov1.Backup
}
tests := []struct {
name string
args args
want *velerov1.Restore
wantErr bool
}{
{
name: "no restore spec",
args: args{
veleroBackup: velerov1.Backup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-backup",
},
},
},
want: nil,
wantErr: false,
},
{
name: "with restore spec",
args: args{
veleroBackup: velerov1.Backup{
ObjectMeta: metav1.ObjectMeta{
Name: "test-backup",
Annotations: map[string]string{
types.InstanceBackupRestoreSpecAnnotation: `{"kind":"Restore","apiVersion":"velero.io/v1","metadata":{"name":"test-restore","creationTimestamp":null},"spec":{"backupName":"test-backup","hooks":{}},"status":{}}`,
},
},
},
},
want: &velerov1.Restore{
TypeMeta: metav1.TypeMeta{
APIVersion: "velero.io/v1",
Kind: "Restore",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-restore",
},
Spec: velerov1.RestoreSpec{
BackupName: "test-backup",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetInstanceBackupRestore(tt.args.veleroBackup)
if (err != nil) != tt.wantErr {
t.Errorf("GetInstanceBackupRestore() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetInstanceBackupRestore() = %v, want %v", got, tt.want)
}
})
}
}
3 changes: 3 additions & 0 deletions pkg/kotsadmsnapshot/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const (
// InstanceBackupCountAnnotation is the annotation used to store the expected number of backups
// for an instance backup.
InstanceBackupCountAnnotation = "replicated.com/backup-count"
// InstanceBackupRestoreSpecAnnotation is the annotation used to store the corresponding restore
// spec for an instance backup.
InstanceBackupRestoreSpecAnnotation = "replicated.com/restore-spec"

// InstanceBackupTypeInfra indicates that the backup is of type infrastructure.
InstanceBackupTypeInfra = "infra"
Expand Down

0 comments on commit acfe1ac

Please sign in to comment.