Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(backups): change list instance backups response payload #5061

Merged
merged 7 commits into from
Dec 18, 2024
4 changes: 2 additions & 2 deletions pkg/handlers/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
175 changes: 96 additions & 79 deletions pkg/kotsadmsnapshot/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}

Expand All @@ -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]
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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
}
}

Expand Down
Loading
Loading