diff --git a/pkg/handlers/backup.go b/pkg/handlers/backup.go index ba3713d36a..26ca52925d 100644 --- a/pkg/handlers/backup.go +++ b/pkg/handlers/backup.go @@ -110,21 +110,21 @@ func (h *Handler) ListBackups(w http.ResponseWriter, r *http.Request) { } type ListInstanceBackupsResponse struct { - Error string `json:"error,omitempty"` - Backups []*snapshottypes.Backup `json:"backups"` + Error string `json:"error,omitempty"` + Backups []*snapshottypes.ReplicatedBackup `json:"backups"` } func (h *Handler) ListInstanceBackups(w http.ResponseWriter, r *http.Request) { listBackupsResponse := ListInstanceBackupsResponse{} - backups, err := snapshot.ListInstanceBackups(r.Context(), util.PodNamespace) + replicatedBackups, err := snapshot.ListInstanceBackups(r.Context(), util.PodNamespace) if err != nil { logger.Error(err) listBackupsResponse.Error = "failed to list instance backups" JSON(w, http.StatusInternalServerError, listBackupsResponse) return } - listBackupsResponse.Backups = backups + listBackupsResponse.Backups = replicatedBackups JSON(w, http.StatusOK, listBackupsResponse) } diff --git a/pkg/kotsadmsnapshot/backup.go b/pkg/kotsadmsnapshot/backup.go index dbe8e98e6a..1c3bdfe8ca 100644 --- a/pkg/kotsadmsnapshot/backup.go +++ b/pkg/kotsadmsnapshot/backup.go @@ -21,7 +21,9 @@ import ( "github.com/replicatedhq/kots/pkg/k8sutil" "github.com/replicatedhq/kots/pkg/kotsadm" kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types" + "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/k8sclient" "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/types" + "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/veleroclient" "github.com/replicatedhq/kots/pkg/kotsutil" "github.com/replicatedhq/kots/pkg/kurl" "github.com/replicatedhq/kots/pkg/logger" @@ -81,12 +83,12 @@ func CreateApplicationBackup(ctx context.Context, a *apptypes.App, isScheduled b return nil, errors.Wrap(err, "failed to get cluster config") } - clientset, err := kubernetes.NewForConfig(cfg) + clientset, err := k8sclient.GetBuilder().GetClientset(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create clientset") } - veleroClient, err := veleroclientv1.NewForConfig(cfg) + veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create velero clientset") } @@ -227,7 +229,7 @@ func CreateInstanceBackup(ctx context.Context, cluster *downstreamtypes.Downstre return "", errors.Wrap(err, "failed to get cluster config") } - k8sClient, err := kubernetes.NewForConfig(cfg) + k8sClient, err := k8sclient.GetBuilder().GetClientset(cfg) if err != nil { return "", errors.Wrap(err, "failed to create clientset") } @@ -237,7 +239,7 @@ func CreateInstanceBackup(ctx context.Context, cluster *downstreamtypes.Downstre return "", fmt.Errorf("failed to get kubeclient: %w", err) } - veleroClient, err := veleroclientv1.NewForConfig(cfg) + veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg) if err != nil { return "", errors.Wrap(err, "failed to create velero clientset") } @@ -713,12 +715,12 @@ func ListBackupsForApp(ctx context.Context, kotsadmNamespace string, appID strin return nil, errors.Wrap(err, "failed to get cluster config") } - clientset, err := kubernetes.NewForConfig(cfg) + clientset, err := k8sclient.GetBuilder().GetClientset(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create clientset") } - veleroClient, err := veleroclientv1.NewForConfig(cfg) + veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create velero clientset") } @@ -840,18 +842,18 @@ func ListBackupsForApp(ctx context.Context, kotsadmNamespace string, appID strin return backups, nil } -func ListInstanceBackups(ctx context.Context, kotsadmNamespace string) ([]*types.Backup, error) { +func ListInstanceBackups(ctx context.Context, kotsadmNamespace string) ([]*types.ReplicatedBackup, error) { cfg, err := k8sutil.GetClusterConfig() if err != nil { return nil, errors.Wrap(err, "failed to get cluster config") } - clientset, err := kubernetes.NewForConfig(cfg) + clientset, err := k8sclient.GetBuilder().GetClientset(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create clientset") } - veleroClient, err := veleroclientv1.NewForConfig(cfg) + veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create velero clientset") } @@ -870,7 +872,7 @@ func ListInstanceBackups(ctx context.Context, kotsadmNamespace string) ([]*types return nil, errors.Wrap(err, "failed to list velero backups") } - backups := []*types.Backup{} + replicatedBackupsMap := map[string]*types.ReplicatedBackup{} for _, veleroBackup := range veleroBackups.Items { // TODO: Enforce version? @@ -970,10 +972,24 @@ func ListInstanceBackups(ctx context.Context, kotsadmNamespace string) ([]*types } } - backups = append(backups, &backup) + // group the velero backups by the name we present to the user + backupName := GetBackupName(veleroBackup) + if _, ok := replicatedBackupsMap[backupName]; !ok { + replicatedBackupsMap[backupName] = &types.ReplicatedBackup{ + Name: backupName, + Backups: []types.Backup{}, + ExpectedBackupCount: GetInstanceBackupCount(veleroBackup), + } + } + replicatedBackupsMap[backupName].Backups = append(replicatedBackupsMap[backupName].Backups, backup) } - return backups, nil + replicatedBackups := []*types.ReplicatedBackup{} + for _, rb := range replicatedBackupsMap { + replicatedBackups = append(replicatedBackups, rb) + } + + return replicatedBackups, nil } func getSnapshotVolumeSummary(ctx context.Context, veleroBackup *velerov1.Backup) (*types.VolumeSummary, error) { @@ -982,7 +998,7 @@ func getSnapshotVolumeSummary(ctx context.Context, veleroBackup *velerov1.Backup return nil, errors.Wrap(err, "failed to get cluster config") } - veleroClient, err := veleroclientv1.NewForConfig(cfg) + veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create clientset") } @@ -1023,12 +1039,12 @@ func GetBackup(ctx context.Context, kotsadmNamespace string, backupID string) (* return nil, errors.Wrap(err, "failed to get cluster config") } - clientset, err := kubernetes.NewForConfig(cfg) + clientset, err := k8sclient.GetBuilder().GetClientset(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create clientset") } - veleroClient, err := veleroclientv1.NewForConfig(cfg) + veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create velero clientset") } @@ -1069,12 +1085,12 @@ func DeleteBackup(ctx context.Context, kotsadmNamespace string, backupID string) return errors.Wrap(err, "failed to get cluster config") } - clientset, err := kubernetes.NewForConfig(cfg) + clientset, err := k8sclient.GetBuilder().GetClientset(cfg) if err != nil { return errors.Wrap(err, "failed to create clientset") } - veleroClient, err := veleroclientv1.NewForConfig(cfg) + veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg) if err != nil { return errors.Wrap(err, "failed to create velero clientset") } @@ -1122,14 +1138,16 @@ func HasUnfinishedApplicationBackup(ctx context.Context, kotsadmNamespace string } func HasUnfinishedInstanceBackup(ctx context.Context, kotsadmNamespace string) (bool, error) { - backups, err := ListInstanceBackups(ctx, kotsadmNamespace) + replicatedBackups, err := ListInstanceBackups(ctx, kotsadmNamespace) if err != nil { return false, errors.Wrap(err, "failed to list backups") } - for _, backup := range backups { - if backup.Status == "New" || backup.Status == "InProgress" { - return true, nil + for _, replicatedBackup := range replicatedBackups { + for _, backup := range replicatedBackup.Backups { + if backup.Status == "New" || backup.Status == "InProgress" { + return true, nil + } } } @@ -1142,12 +1160,12 @@ func GetBackupDetail(ctx context.Context, kotsadmNamespace string, backupID stri return nil, errors.Wrap(err, "failed to get cluster config") } - clientset, err := kubernetes.NewForConfig(cfg) + clientset, err := k8sclient.GetBuilder().GetClientset(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create clientset") } - veleroClient, err := veleroclientv1.NewForConfig(cfg) + veleroClient, err := veleroclient.GetBuilder().GetVeleroClient(cfg) if err != nil { return nil, errors.Wrap(err, "failed to create velero clientset") } diff --git a/pkg/kotsadmsnapshot/backup_test.go b/pkg/kotsadmsnapshot/backup_test.go index f289067aa7..319cf61b0e 100644 --- a/pkg/kotsadmsnapshot/backup_test.go +++ b/pkg/kotsadmsnapshot/backup_test.go @@ -2,6 +2,7 @@ package snapshot import ( "context" + "fmt" "os" "path/filepath" "testing" @@ -12,6 +13,9 @@ import ( downstreamtypes "github.com/replicatedhq/kots/pkg/api/downstream/types" apptypes "github.com/replicatedhq/kots/pkg/app/types" kotsadmtypes "github.com/replicatedhq/kots/pkg/kotsadm/types" + "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/k8sclient" + "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/types" + "github.com/replicatedhq/kots/pkg/kotsadmsnapshot/veleroclient" "github.com/replicatedhq/kots/pkg/kotsutil" registrytypes "github.com/replicatedhq/kots/pkg/registry/types" "github.com/replicatedhq/kots/pkg/store" @@ -2965,3 +2969,398 @@ func Test_getBackupNameFromPrefix(t *testing.T) { }) } } + +func TestListInstanceBackups(t *testing.T) { + scheme := runtime.NewScheme() + corev1.AddToScheme(scheme) + embeddedclusterv1beta1.AddToScheme(scheme) + + // setup timestamps + startTs := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC) + completionTs := time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC) + expirationTs := time.Date(2025, 1, 2, 0, 0, 0, 0, time.UTC) + + // setup common mock objects + kotsadmNamespace := "kotsadm-test" + testBsl := &velerov1.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{ + Name: "default", + Namespace: "velero", + }, + Spec: velerov1.BackupStorageLocationSpec{ + Provider: "aws", + Default: true, + }, + } + veleroNamespaceConfigmap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kotsadm-velero-namespace", + Namespace: kotsadmNamespace, + }, + Data: map[string]string{ + "veleroNamespace": "velero", + }, + } + veleroDeployment := &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "velero", + Namespace: "velero", + }, + } + + tests := []struct { + name string + setup func(mockStore *mock_store.MockStore) + veleroClientBuilder veleroclient.VeleroClientBuilder + k8sClientBuilder k8sclient.K8sClientsetBuilder + expectedBackups []*types.ReplicatedBackup + wantErr string + }{ + { + name: "fails to create k8s clientset", + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: nil, + Err: fmt.Errorf("error creating k8s clientset"), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset().VeleroV1(), + }, + wantErr: "failed to create clientset", + }, + { + name: "fails to create velero client", + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset(), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: nil, + Err: fmt.Errorf("error creating velero client"), + }, + wantErr: "failed to create velero clientset", + }, + { + name: "fails to find backup storage location", + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset(), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset().VeleroV1(), + }, + wantErr: "no backup store location found", + }, + { + name: "empty backup list", + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset( + veleroNamespaceConfigmap, + veleroDeployment, + ), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset( + testBsl, + ).VeleroV1(), + }, + expectedBackups: []*types.ReplicatedBackup{}, + }, + { + name: "non instance backups are excluded", + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset( + veleroNamespaceConfigmap, + veleroDeployment, + ), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset( + testBsl, + &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-backup", + Namespace: "velero", + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseCompleted, + }, + }, + &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "instance-backup", + Namespace: "velero", + Annotations: map[string]string{ + types.InstanceBackupAnnotation: "true", + }, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseCompleted, + }, + }, + ).VeleroV1(), + }, + expectedBackups: []*types.ReplicatedBackup{ + { + Name: "instance-backup", + ExpectedBackupCount: 1, + Backups: []types.Backup{ + { + Name: "instance-backup", + Status: "Completed", + IncludedApps: []types.App{}, + VolumeSizeHuman: "0B", + }, + }, + }, + }, + }, + { + name: "new improved dr backups are part of the same replicated backup", + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset( + veleroNamespaceConfigmap, + veleroDeployment, + ), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset( + testBsl, + &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "infra-backup", + Namespace: "velero", + Labels: map[string]string{ + types.InstanceBackupNameLabel: "aggregated-repl-backup", + }, + Annotations: map[string]string{ + types.InstanceBackupAnnotation: "true", + types.InstanceBackupTypeAnnotation: types.InstanceBackupTypeInfra, + types.InstanceBackupCountAnnotation: "2", + }, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseCompleted, + }, + }, + &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-backup", + Namespace: "velero", + Labels: map[string]string{ + types.InstanceBackupNameLabel: "aggregated-repl-backup", + }, + Annotations: map[string]string{ + types.InstanceBackupAnnotation: "true", + types.InstanceBackupTypeAnnotation: types.InstanceBackupTypeApp, + types.InstanceBackupCountAnnotation: "2", + }, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseCompleted, + }, + }, + ).VeleroV1(), + }, + expectedBackups: []*types.ReplicatedBackup{ + { + Name: "aggregated-repl-backup", + ExpectedBackupCount: 2, + Backups: []types.Backup{ + { + Name: "app-backup", + Status: "Completed", + IncludedApps: []types.App{}, + VolumeSizeHuman: "0B", + }, + { + Name: "infra-backup", + Status: "Completed", + IncludedApps: []types.App{}, + VolumeSizeHuman: "0B", + }, + }, + }, + }, + }, + { + name: "included apps are populated ", + setup: func(mockStore *mock_store.MockStore) { + mockStore.EXPECT().GetAppFromSlug("app-1").Times(1).Return(&apptypes.App{ + ID: "1", + Name: "App 1", + Slug: "app-1", + IconURI: "https://some-url.com/icon.png", + }, nil) + }, + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset( + veleroNamespaceConfigmap, + veleroDeployment, + ), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset( + testBsl, + &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-backup", + Namespace: "velero", + Annotations: map[string]string{ + types.InstanceBackupAnnotation: "true", + "kots.io/apps-sequences": "{\"app-1\":1}", + }, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseCompleted, + }, + }, + ).VeleroV1(), + }, + expectedBackups: []*types.ReplicatedBackup{ + { + Name: "some-backup", + ExpectedBackupCount: 1, + Backups: []types.Backup{ + { + Name: "some-backup", + Status: "Completed", + IncludedApps: []types.App{ + { + Slug: "app-1", + Sequence: 1, + Name: "App 1", + AppIconURI: "https://some-url.com/icon.png", + }, + }, + VolumeSizeHuman: "0B", + }, + }, + }, + }, + }, + { + name: "timestamps are populated", + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset( + veleroNamespaceConfigmap, + veleroDeployment, + ), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset( + testBsl, + &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-backup", + Namespace: "velero", + Annotations: map[string]string{ + types.InstanceBackupAnnotation: "true", + }, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseCompleted, + StartTimestamp: &metav1.Time{Time: startTs}, + CompletionTimestamp: &metav1.Time{Time: completionTs}, + Expiration: &metav1.Time{Time: expirationTs}, + }, + }, + ).VeleroV1(), + }, + expectedBackups: []*types.ReplicatedBackup{ + { + Name: "some-backup", + ExpectedBackupCount: 1, + Backups: []types.Backup{ + { + Name: "some-backup", + Status: "Completed", + StartedAt: &startTs, + FinishedAt: &completionTs, + ExpiresAt: &expirationTs, + IncludedApps: []types.App{}, + VolumeSizeHuman: "0B", + }, + }, + }, + }, + }, + { + name: "volume info is populated", + k8sClientBuilder: &k8sclient.MockBuilder{ + Client: fake.NewSimpleClientset( + veleroNamespaceConfigmap, + veleroDeployment, + ), + }, + veleroClientBuilder: &veleroclient.MockBuilder{ + Client: velerofake.NewSimpleClientset( + testBsl, + &velerov1.Backup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "some-backup-with-volumes", + Namespace: "velero", + Annotations: map[string]string{ + types.InstanceBackupAnnotation: "true", + "kots.io/snapshot-trigger": "manual", + "kots.io/snapshot-volume-count": "2", + "kots.io/snapshot-volume-success-count": "1", + "kots.io/snapshot-volume-bytes": "1000", + }, + }, + Status: velerov1.BackupStatus{ + Phase: velerov1.BackupPhaseCompleted, + }, + }, + ).VeleroV1(), + }, + expectedBackups: []*types.ReplicatedBackup{ + { + Name: "some-backup-with-volumes", + ExpectedBackupCount: 1, + Backups: []types.Backup{ + { + Name: "some-backup-with-volumes", + Status: "Completed", + Trigger: "manual", + VolumeSizeHuman: "1kB", + VolumeBytes: 1000, + VolumeSuccessCount: 1, + VolumeCount: 2, + IncludedApps: []types.App{}, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + asrt := assert.New(t) + // setup mock clients + k8sclient.SetBuilder(test.k8sClientBuilder) + veleroclient.SetBuilder(test.veleroClientBuilder) + // setup mock store + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockStore := mock_store.NewMockStore(ctrl) + store.SetStore(mockStore) + + t.Cleanup(func() { + store.SetStore(nil) + }) + + if test.setup != nil { + test.setup(mockStore) + } + + backups, err := ListInstanceBackups(context.Background(), kotsadmNamespace) + + if test.wantErr != "" { + asrt.Error(err) + asrt.Contains(err.Error(), test.wantErr) + } else { + asrt.NoError(err) + } + asrt.Equal(test.expectedBackups, backups) + }) + } +} diff --git a/pkg/kotsadmsnapshot/k8sclient/k8sclient.go b/pkg/kotsadmsnapshot/k8sclient/k8sclient.go new file mode 100644 index 0000000000..12ccdcec42 --- /dev/null +++ b/pkg/kotsadmsnapshot/k8sclient/k8sclient.go @@ -0,0 +1,49 @@ +package k8sclient + +import ( + "k8s.io/client-go/kubernetes" + rest "k8s.io/client-go/rest" +) + +var _ K8sClientsetBuilder = (*Builder)(nil) +var _ K8sClientsetBuilder = (*MockBuilder)(nil) + +// K8sClientsetBuilder interface is used as an abstraction to get a k8s clientset. Useful to mock the client in tests. +type K8sClientsetBuilder interface { + GetClientset(*rest.Config) (kubernetes.Interface, error) +} + +// Builder is the default implementation of K8sClientsetBuilder. It returns a regular k8s clientset. +type Builder struct{} + +// GetClientset returns a regular k8s client. +func (b *Builder) GetClientset(cfg *rest.Config) (kubernetes.Interface, error) { + return kubernetes.NewForConfig(cfg) +} + +// MockBuilder is a mock implementation of K8sClientsetBuilder. It returns the client that was set in the struct allowing +// you to set a fakeClient for example. + +type MockBuilder struct { + Client kubernetes.Interface + Err error +} + +// GetClientset returns the client that was set in the struct. +func (b *MockBuilder) GetClientset(cfg *rest.Config) (kubernetes.Interface, error) { + return b.Client, b.Err +} + +var clientBuilder K8sClientsetBuilder + +func GetBuilder() K8sClientsetBuilder { + return clientBuilder +} + +func SetBuilder(builder K8sClientsetBuilder) { + clientBuilder = builder +} + +func init() { + SetBuilder(&Builder{}) +} diff --git a/pkg/kotsadmsnapshot/types/types.go b/pkg/kotsadmsnapshot/types/types.go index 7e67ed15ae..45c1eac11c 100644 --- a/pkg/kotsadmsnapshot/types/types.go +++ b/pkg/kotsadmsnapshot/types/types.go @@ -36,6 +36,14 @@ type App struct { AppIconURI string `json:"iconUri"` } +// ReplicatedBackup holds both the infrastructure and app backups for an EC cluster +type ReplicatedBackup struct { + Name string `json:"name"` + // number of backups expected to exist for the ReplicatedBackup to be considered complete + ExpectedBackupCount int `json:"expectedBackupCount"` + Backups []Backup `json:"backups"` +} + type Backup struct { Name string `json:"name"` Status string `json:"status"` diff --git a/pkg/kotsadmsnapshot/veleroclient/veleroclient.go b/pkg/kotsadmsnapshot/veleroclient/veleroclient.go new file mode 100644 index 0000000000..f7d9fd1eef --- /dev/null +++ b/pkg/kotsadmsnapshot/veleroclient/veleroclient.go @@ -0,0 +1,49 @@ +package veleroclient + +import ( + veleroclientv1 "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned/typed/velero/v1" + rest "k8s.io/client-go/rest" +) + +var _ VeleroClientBuilder = (*Builder)(nil) +var _ VeleroClientBuilder = (*MockBuilder)(nil) + +// VeleroClientBuilder interface is used as an abstraction to get a velero client. Useful to mock the client in tests. +type VeleroClientBuilder interface { + GetVeleroClient(*rest.Config) (veleroclientv1.VeleroV1Interface, error) +} + +// Builder is the default implementation of VeleroClientBuilder. It returns a regular velero v1 client. +type Builder struct{} + +// GetVeleroClient returns a regular velero client. +func (b *Builder) GetVeleroClient(cfg *rest.Config) (veleroclientv1.VeleroV1Interface, error) { + return veleroclientv1.NewForConfig(cfg) +} + +// MockBuilder is a mock implementation of VeleroClientBuilder. It returns the client that was set in the struct allowing +// you to set a fakeClient for example. + +type MockBuilder struct { + Client veleroclientv1.VeleroV1Interface + Err error +} + +// GetVeleroClient returns the client that was set in the struct. +func (b *MockBuilder) GetVeleroClient(cfg *rest.Config) (veleroclientv1.VeleroV1Interface, error) { + return b.Client, b.Err +} + +var clientBuilder VeleroClientBuilder + +func GetBuilder() VeleroClientBuilder { + return clientBuilder +} + +func SetBuilder(builder VeleroClientBuilder) { + clientBuilder = builder +} + +func init() { + SetBuilder(&Builder{}) +} diff --git a/web/src/components/snapshots/Snapshots.jsx b/web/src/components/snapshots/Snapshots.jsx index a2607c68f3..d21f064fee 100644 --- a/web/src/components/snapshots/Snapshots.jsx +++ b/web/src/components/snapshots/Snapshots.jsx @@ -108,8 +108,12 @@ class Snapshots extends Component { } const response = await res.json(); + const snapshots = response.backups?.flatMap( + (replBackup) => replBackup?.backups + ); + this.setState({ - snapshots: response.backups?.sort((a, b) => + snapshots: snapshots?.sort((a, b) => b.startedAt ? new Date(b.startedAt) - new Date(a.startedAt) : -99999999