diff --git a/pkg/kotsadmsnapshot/backup.go b/pkg/kotsadmsnapshot/backup.go index b0809c05e4..3a157ef4b2 100644 --- a/pkg/kotsadmsnapshot/backup.go +++ b/pkg/kotsadmsnapshot/backup.go @@ -1,6 +1,7 @@ package snapshot import ( + "bytes" "context" "crypto/sha256" "encoding/json" @@ -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" ) @@ -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) @@ -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) { @@ -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{ @@ -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{ @@ -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 @@ -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-" @@ -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") @@ -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" diff --git a/pkg/kotsadmsnapshot/backup_test.go b/pkg/kotsadmsnapshot/backup_test.go index 142d5e8b39..d58c807a7e 100644 --- a/pkg/kotsadmsnapshot/backup_test.go +++ b/pkg/kotsadmsnapshot/backup_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "reflect" "testing" "time" @@ -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{ @@ -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]) } }, }, @@ -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]) } }, }, @@ -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) }, }, { @@ -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]) } }, }, @@ -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) + } + }) + } +} diff --git a/pkg/kotsadmsnapshot/types/types.go b/pkg/kotsadmsnapshot/types/types.go index 45c1eac11c..1c5b1a112e 100644 --- a/pkg/kotsadmsnapshot/types/types.go +++ b/pkg/kotsadmsnapshot/types/types.go @@ -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"