diff --git a/pkg/handlers/backup.go b/pkg/handlers/backup.go index 26ca52925d..9b94381567 100644 --- a/pkg/handlers/backup.go +++ b/pkg/handlers/backup.go @@ -110,8 +110,8 @@ func (h *Handler) ListBackups(w http.ResponseWriter, r *http.Request) { } type ListInstanceBackupsResponse struct { - Error string `json:"error,omitempty"` - Backups []*snapshottypes.ReplicatedBackup `json:"backups"` + Error string `json:"error,omitempty"` + Backups []*snapshottypes.Backup `json:"backups"` } func (h *Handler) ListInstanceBackups(w http.ResponseWriter, r *http.Request) { diff --git a/pkg/kotsadmsnapshot/backup.go b/pkg/kotsadmsnapshot/backup.go index de7497a757..45298eb74c 100644 --- a/pkg/kotsadmsnapshot/backup.go +++ b/pkg/kotsadmsnapshot/backup.go @@ -747,7 +747,7 @@ func ListBackupsForApp(ctx context.Context, kotsadmNamespace string, appID strin backup := types.Backup{ Name: veleroBackup.Name, - Status: string(veleroBackup.Status.Phase), + Status: types.GetStatusFromBackupPhase(veleroBackup.Status.Phase), AppID: appID, } @@ -770,7 +770,7 @@ func ListBackupsForApp(ctx context.Context, kotsadmNamespace string, appID strin backup.Sequence = s } if backup.Status == "" { - backup.Status = "New" + backup.Status = types.BackupStatusInProgress } trigger, ok := veleroBackup.Annotations[types.BackupTriggerAnnotation] @@ -783,7 +783,7 @@ func ListBackupsForApp(ctx context.Context, kotsadmNamespace string, appID strin backup.SupportBundleID = supportBundleID } - if backup.Status != "New" && backup.Status != "InProgress" { + if backup.Status != types.BackupStatusInProgress { volumeSummary, err := getSnapshotVolumeSummary(ctx, &veleroBackup) if err != nil { return nil, errors.Wrap(err, "failed to get volume summary") @@ -801,7 +801,7 @@ func ListBackupsForApp(ctx context.Context, kotsadmNamespace string, appID strin return backups, nil } -func ListInstanceBackups(ctx context.Context, kotsadmNamespace string) ([]*types.ReplicatedBackup, error) { +func ListInstanceBackups(ctx context.Context, kotsadmNamespace string) ([]*types.Backup, error) { cfg, err := k8sutil.GetClusterConfig() if err != nil { return nil, errors.Wrap(err, "failed to get cluster config") @@ -831,94 +831,113 @@ func ListInstanceBackups(ctx context.Context, kotsadmNamespace string) ([]*types return nil, errors.Wrap(err, "failed to list velero backups") } - replicatedBackupsMap := map[string]*types.ReplicatedBackup{} + return getBackupsFromVeleroBackups(ctx, veleroBackups.Items) +} - for _, veleroBackup := range veleroBackups.Items { - // TODO: Enforce version? +// getBackupsFromVeleroBackups returns an array of `Backup` structs, consisting of Replicated's representation of a backup +// from an array of Velero backups `VolumeSummary`'s and . +func getBackupsFromVeleroBackups(ctx context.Context, veleroBackups []velerov1.Backup) ([]*types.Backup, error) { + result := make(map[string]*types.Backup, 0) + + for _, veleroBackup := range veleroBackups { + // filter out non instance backups if !types.IsInstanceBackup(veleroBackup) { continue } - - backup := types.Backup{ - Name: veleroBackup.Name, - Status: string(veleroBackup.Status.Phase), - IncludedApps: make([]types.App, 0), - } - - if veleroBackup.Status.StartTimestamp != nil { - backup.StartedAt = &veleroBackup.Status.StartTimestamp.Time - } - if veleroBackup.Status.CompletionTimestamp != nil { - backup.FinishedAt = &veleroBackup.Status.CompletionTimestamp.Time - } - if veleroBackup.Status.Expiration != nil { - backup.ExpiresAt = &veleroBackup.Status.Expiration.Time - } - if backup.Status == "" { - backup.Status = "New" + veleroStatus := veleroBackup.Status + backupName := types.GetBackupName(veleroBackup) + if _, ok := result[backupName]; !ok { + result[backupName] = &types.Backup{ + Name: backupName, + Status: types.GetStatusFromBackupPhase(veleroStatus.Phase), + Trigger: types.GetBackupTrigger(veleroBackup), + ExpectedBackupCount: types.GetInstanceBackupCount(veleroBackup), + IncludedApps: []types.App{}, + } } - - trigger, ok := veleroBackup.Annotations[types.BackupTriggerAnnotation] - if ok { - backup.Trigger = trigger + backup := result[backupName] + backup.BackupCount++ + // backup uses the oldest velero backup start time as its start time + if veleroStatus.StartTimestamp != nil { + if backup.StartedAt == nil || veleroStatus.StartTimestamp.Time.Before(*backup.StartedAt) { + backup.StartedAt = &veleroStatus.StartTimestamp.Time + } } - appAnnotationStr, _ := veleroBackup.Annotations[types.BackupAppsSequencesAnnotation] - if len(appAnnotationStr) > 0 { - var apps map[string]int64 - if err := json.Unmarshal([]byte(appAnnotationStr), &apps); err != nil { - return nil, errors.Wrap(err, "failed to unmarshal apps sequences") + // backup uses the first expiration date as its expiration timestamp + if veleroStatus.Expiration != nil { + if backup.ExpiresAt == nil || veleroStatus.Expiration.Time.Before(*backup.ExpiresAt) { + backup.ExpiresAt = &veleroStatus.Expiration.Time } - for slug, sequence := range apps { - a, err := store.GetStore().GetAppFromSlug(slug) - if err != nil { - if store.GetStore().IsNotFound(err) { - // app might not exist in current installation - continue - } - return nil, errors.Wrap(err, "failed to get app from slug") - } + } - backup.IncludedApps = append(backup.IncludedApps, types.App{ - Slug: slug, - Sequence: sequence, - Name: a.Name, - AppIconURI: a.IconURI, - }) + // backup uses the most recent completion date as its completion timestamp + if veleroStatus.CompletionTimestamp != nil { + if backup.FinishedAt == nil || veleroStatus.CompletionTimestamp.Time.After(*backup.FinishedAt) { + backup.FinishedAt = &veleroStatus.CompletionTimestamp.Time } } - // get volume information - if backup.Status != "New" && backup.Status != "InProgress" { - volumeSummary, err := getSnapshotVolumeSummary(ctx, &veleroBackup) - if err != nil { - return nil, errors.Wrap(err, "failed to get volume summary") - } + backup.Status = types.RollupStatus([]types.BackupStatus{backup.Status, types.GetStatusFromBackupPhase(veleroStatus.Phase)}) - backup.VolumeCount = volumeSummary.VolumeCount - backup.VolumeSuccessCount = volumeSummary.VolumeSuccessCount - backup.VolumeBytes = volumeSummary.VolumeBytes - backup.VolumeSizeHuman = volumeSummary.VolumeSizeHuman + // get volume information + volumeSummary, err := getSnapshotVolumeSummary(ctx, &veleroBackup) + if err != nil { + return nil, fmt.Errorf("failed to get volume summary for backup %s: %w", backupName, err) } - // group the velero backups by the name we present to the user - backupName := types.GetBackupName(veleroBackup) - if _, ok := replicatedBackupsMap[backupName]; !ok { - replicatedBackupsMap[backupName] = &types.ReplicatedBackup{ - Name: backupName, - Backups: []types.Backup{}, - ExpectedBackupCount: types.GetInstanceBackupCount(veleroBackup), - } + backup.VolumeCount += volumeSummary.VolumeCount + backup.VolumeSuccessCount += volumeSummary.VolumeSuccessCount + backup.VolumeBytes += volumeSummary.VolumeBytes + backup.VolumeSizeHuman = units.HumanSize(float64(backup.VolumeBytes)) + + apps, err := getAppsFromAppSequences(veleroBackup) + if err != nil { + return nil, fmt.Errorf("failed to get apps from app sequences for backup %s: %w", backupName, err) } - replicatedBackupsMap[backupName].Backups = append(replicatedBackupsMap[backupName].Backups, backup) + backup.IncludedApps = append(backup.IncludedApps, apps...) } - replicatedBackups := []*types.ReplicatedBackup{} - for _, rb := range replicatedBackupsMap { - replicatedBackups = append(replicatedBackups, rb) + backups := []*types.Backup{} + for _, backup := range result { + // we consider a backup to have failed if the number of backups that actually exist is less than the expected number + if backup.ExpectedBackupCount != backup.BackupCount { + backup.Status = types.BackupStatusFailed + } + backups = append(backups, backup) } - return replicatedBackups, nil + return backups, nil +} + +// getAppsFromAppSequences returns a list of `App` structs from the backup sequence annotation. +func getAppsFromAppSequences(veleroBackup velerov1.Backup) ([]types.App, error) { + apps := []types.App{} + appAnnotationStr, _ := veleroBackup.Annotations[types.BackupAppsSequencesAnnotation] + if len(appAnnotationStr) > 0 { + var appsSequences map[string]int64 + if err := json.Unmarshal([]byte(appAnnotationStr), &appsSequences); err != nil { + return nil, fmt.Errorf("failed to unmarshal apps sequences: %w", err) + } + for slug, sequence := range appsSequences { + a, err := store.GetStore().GetAppFromSlug(slug) + if err != nil { + if store.GetStore().IsNotFound(err) { + // app might not exist in current installation + continue + } + return nil, fmt.Errorf("failed to get app from slug: %w", err) + } + + apps = append(apps, types.App{ + Slug: slug, + Sequence: sequence, + Name: a.Name, + AppIconURI: a.IconURI, + }) + } + } + return apps, nil } func getSnapshotVolumeSummary(ctx context.Context, veleroBackup *velerov1.Backup) (*types.VolumeSummary, error) { @@ -1077,7 +1096,7 @@ func HasUnfinishedApplicationBackup(ctx context.Context, kotsadmNamespace string } for _, backup := range backups { - if backup.Status == "New" || backup.Status == "InProgress" { + if backup.Status == types.BackupStatusInProgress { return true, nil } } @@ -1086,16 +1105,14 @@ func HasUnfinishedApplicationBackup(ctx context.Context, kotsadmNamespace string } func HasUnfinishedInstanceBackup(ctx context.Context, kotsadmNamespace string) (bool, error) { - replicatedBackups, err := ListInstanceBackups(ctx, kotsadmNamespace) + backups, err := ListInstanceBackups(ctx, kotsadmNamespace) if err != nil { return false, errors.Wrap(err, "failed to list backups") } - for _, replicatedBackup := range replicatedBackups { - for _, backup := range replicatedBackup.Backups { - if backup.Status == "New" || backup.Status == "InProgress" { - return true, nil - } + for _, backup := range backups { + if backup.Status == types.BackupStatusInProgress { + return true, nil } } diff --git a/pkg/kotsadmsnapshot/backup_test.go b/pkg/kotsadmsnapshot/backup_test.go index 3475f9e892..d0d1c67fc8 100644 --- a/pkg/kotsadmsnapshot/backup_test.go +++ b/pkg/kotsadmsnapshot/backup_test.go @@ -3163,10 +3163,12 @@ func TestListBackupsForApp(t *testing.T) { }, expectedBackups: []*types.Backup{ { - AppID: "app-1", - Name: "app-backup-app-1", - Status: "Completed", - VolumeSizeHuman: "0B", + AppID: "app-1", + Name: "app-backup-app-1", + Status: "Completed", + VolumeSummary: types.VolumeSummary{ + VolumeSizeHuman: "0B", + }, }, }, }, @@ -3201,13 +3203,15 @@ func TestListBackupsForApp(t *testing.T) { }, expectedBackups: []*types.Backup{ { - AppID: "app-1", - Name: "app-backup-app-1", - Status: "Completed", - StartedAt: &startTs, - FinishedAt: &completionTs, - ExpiresAt: &expirationTs, - VolumeSizeHuman: "0B", + AppID: "app-1", + Name: "app-backup-app-1", + Status: "Completed", + StartedAt: &startTs, + FinishedAt: &completionTs, + ExpiresAt: &expirationTs, + VolumeSummary: types.VolumeSummary{ + VolumeSizeHuman: "0B", + }, }, }, }, @@ -3255,14 +3259,16 @@ func TestListBackupsForApp(t *testing.T) { }, expectedBackups: []*types.Backup{ { - AppID: "app-1", - Name: "app-backup-app-1", - Status: "Completed", - Trigger: "schedule", - VolumeSizeHuman: "2kB", - VolumeBytes: 2000, - VolumeSuccessCount: 1, - VolumeCount: 1, + AppID: "app-1", + Name: "app-backup-app-1", + Status: "Completed", + Trigger: "schedule", + VolumeSummary: types.VolumeSummary{ + VolumeSizeHuman: "2kB", + VolumeBytes: 2000, + VolumeSuccessCount: 1, + VolumeCount: 1, + }, }, }, }, @@ -3331,7 +3337,7 @@ func TestListInstanceBackups(t *testing.T) { setup func(mockStore *mock_store.MockStore) veleroClientBuilder veleroclient.VeleroClientBuilder k8sClientBuilder k8sclient.K8sClientsetBuilder - expectedBackups []*types.ReplicatedBackup + expectedBackups []*types.Backup wantErr string }{ { @@ -3379,7 +3385,7 @@ func TestListInstanceBackups(t *testing.T) { testBsl, ).VeleroV1(), }, - expectedBackups: []*types.ReplicatedBackup{}, + expectedBackups: []*types.Backup{}, }, { name: "non instance backups are excluded", @@ -3415,17 +3421,15 @@ func TestListInstanceBackups(t *testing.T) { }, ).VeleroV1(), }, - expectedBackups: []*types.ReplicatedBackup{ + expectedBackups: []*types.Backup{ { Name: "instance-backup", ExpectedBackupCount: 1, - Backups: []types.Backup{ - { - Name: "instance-backup", - Status: "Completed", - IncludedApps: []types.App{}, - VolumeSizeHuman: "0B", - }, + BackupCount: 1, + Status: "Completed", + IncludedApps: []types.App{}, + VolumeSummary: types.VolumeSummary{ + VolumeSizeHuman: "0B", }, }, }, @@ -3479,23 +3483,15 @@ func TestListInstanceBackups(t *testing.T) { }, ).VeleroV1(), }, - expectedBackups: []*types.ReplicatedBackup{ + expectedBackups: []*types.Backup{ { 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", - }, + BackupCount: 2, + Status: "Completed", + IncludedApps: []types.App{}, + VolumeSummary: types.VolumeSummary{ + VolumeSizeHuman: "0B", }, }, }, @@ -3534,25 +3530,23 @@ func TestListInstanceBackups(t *testing.T) { }, ).VeleroV1(), }, - expectedBackups: []*types.ReplicatedBackup{ + expectedBackups: []*types.Backup{ { Name: "some-backup", + Status: "Completed", ExpectedBackupCount: 1, - Backups: []types.Backup{ + BackupCount: 1, + IncludedApps: []types.App{ { - 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", + Slug: "app-1", + Sequence: 1, + Name: "App 1", + AppIconURI: "https://some-url.com/icon.png", }, }, + VolumeSummary: types.VolumeSummary{ + VolumeSizeHuman: "0B", + }, }, }, }, @@ -3584,20 +3578,18 @@ func TestListInstanceBackups(t *testing.T) { }, ).VeleroV1(), }, - expectedBackups: []*types.ReplicatedBackup{ + expectedBackups: []*types.Backup{ { Name: "some-backup", ExpectedBackupCount: 1, - Backups: []types.Backup{ - { - Name: "some-backup", - Status: "Completed", - StartedAt: &startTs, - FinishedAt: &completionTs, - ExpiresAt: &expirationTs, - IncludedApps: []types.App{}, - VolumeSizeHuman: "0B", - }, + BackupCount: 1, + Status: "Completed", + StartedAt: &startTs, + FinishedAt: &completionTs, + ExpiresAt: &expirationTs, + IncludedApps: []types.App{}, + VolumeSummary: types.VolumeSummary{ + VolumeSizeHuman: "0B", }, }, }, @@ -3643,22 +3635,20 @@ func TestListInstanceBackups(t *testing.T) { }, ).VeleroV1(), }, - expectedBackups: []*types.ReplicatedBackup{ + expectedBackups: []*types.Backup{ { Name: "some-backup-with-volumes", ExpectedBackupCount: 1, - Backups: []types.Backup{ - { - Name: "some-backup-with-volumes", - Status: "Completed", - Trigger: "manual", - VolumeSizeHuman: "2kB", - VolumeBytes: 2000, - VolumeSuccessCount: 1, - VolumeCount: 1, - IncludedApps: []types.App{}, - }, - }, + BackupCount: 1, + Status: "Completed", + Trigger: "manual", + VolumeSummary: types.VolumeSummary{ + VolumeSizeHuman: "2kB", + VolumeBytes: 2000, + VolumeSuccessCount: 1, + VolumeCount: 1, + }, + IncludedApps: []types.App{}, }, }, }, diff --git a/pkg/kotsadmsnapshot/types/types.go b/pkg/kotsadmsnapshot/types/types.go index 23b339e5e6..0a6a9fe30c 100644 --- a/pkg/kotsadmsnapshot/types/types.go +++ b/pkg/kotsadmsnapshot/types/types.go @@ -2,6 +2,7 @@ package types import ( "strconv" + "strings" "time" velerov1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" @@ -56,29 +57,38 @@ 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"` -} +// BackupStatus represents the status of a backup +type BackupStatus string +const ( + // BackupStatusInProgress indicates that the backup is currently in progress + BackupStatusInProgress BackupStatus = "InProgress" + // BackupStatusCompleted indicates that the backup has been completed successfully + BackupStatusCompleted BackupStatus = "Completed" + // BackupStatusFailed indicates that the backup has failed + BackupStatusFailed BackupStatus = "Failed" + // BackupStatusDeleting indicates that the backup is being deleted + BackupStatusDeleting BackupStatus = "Deleting" +) + +// Backup represnts a replicated backup working as an abstraction layer between Replicated and +// Velero backups. These can be either infrastructure/instance, app backups or both. type Backup struct { - Name string `json:"name"` - Status string `json:"status"` - Trigger string `json:"trigger"` - AppID string `json:"appID"` // TODO: remove with app backups - Sequence int64 `json:"sequence"` // TODO: remove with app backups - StartedAt *time.Time `json:"startedAt,omitempty"` - FinishedAt *time.Time `json:"finishedAt,omitempty"` - ExpiresAt *time.Time `json:"expiresAt,omitempty"` - VolumeCount int `json:"volumeCount"` - VolumeSuccessCount int `json:"volumeSuccessCount"` - VolumeBytes int64 `json:"volumeBytes"` - VolumeSizeHuman string `json:"volumeSizeHuman"` - SupportBundleID string `json:"supportBundleId,omitempty"` - IncludedApps []App `json:"includedApps,omitempty"` + Name string `json:"name"` + Status BackupStatus `json:"status"` + Trigger string `json:"trigger"` + AppID string `json:"appID"` // TODO: remove with app backups + Sequence int64 `json:"sequence"` // TODO: remove with app backups + StartedAt *time.Time `json:"startedAt,omitempty"` + FinishedAt *time.Time `json:"finishedAt,omitempty"` + ExpiresAt *time.Time `json:"expiresAt,omitempty"` + SupportBundleID string `json:"supportBundleId,omitempty"` + IncludedApps []App `json:"includedApps,omitempty"` + // number of velero backups expected to exist for the Backup to be considered done + ExpectedBackupCount int `json:"expectedBackupCount"` + // number of velero backups that actually exist + BackupCount int `json:"backupCount"` + VolumeSummary } type BackupDetail struct { @@ -234,3 +244,45 @@ func GetInstanceBackupCount(veleroBackup velerov1.Backup) int { } return 1 } + +// GetBackupTrigger returns the trigger of the backup from the velero backup object annotation. +func GetBackupTrigger(veleroBackup velerov1.Backup) string { + if val, ok := veleroBackup.GetAnnotations()[BackupTriggerAnnotation]; ok { + return val + } + return "" +} + +// GetStatusFromBackupPhase returns our backup status from the velero backup phase. +func GetStatusFromBackupPhase(phase velerov1.BackupPhase) BackupStatus { + switch { + case phase == velerov1.BackupPhaseNew || phase == velerov1.BackupPhaseInProgress: + return BackupStatusInProgress + case phase == velerov1.BackupPhaseCompleted: + return BackupStatusCompleted + case strings.Contains(strings.ToLower(string(phase)), "fail"): + return BackupStatusFailed + case phase == velerov1.BackupPhaseDeleting: + return BackupStatusDeleting + default: + return BackupStatusInProgress + } +} + +// RollupStatus returns the overall status of a list of backup statuses. This is particularly useful when we have multiple +// velero backups for a single Replicated backup. +func RollupStatus(backupStatuses []BackupStatus) BackupStatus { + result := BackupStatusCompleted + + for _, backupStatus := range backupStatuses { + switch { + case backupStatus == BackupStatusInProgress: + return BackupStatusInProgress + case backupStatus == BackupStatusDeleting: + result = BackupStatusDeleting + case backupStatus == BackupStatusFailed: + result = BackupStatusFailed + } + } + return result +}