diff --git a/docs/cli-reference/ark_backup_describe.md b/docs/cli-reference/ark_backup_describe.md index 41691b500..7cd20c23e 100644 --- a/docs/cli-reference/ark_backup_describe.md +++ b/docs/cli-reference/ark_backup_describe.md @@ -16,6 +16,7 @@ ark backup describe [NAME1] [NAME2] [NAME...] [flags] ``` -h, --help help for describe -l, --selector string only show items matching this label selector + --volume-details display details of restic volume backups ``` ### Options inherited from parent commands diff --git a/docs/cli-reference/ark_describe_backups.md b/docs/cli-reference/ark_describe_backups.md index 731355a90..e03ccfcbd 100644 --- a/docs/cli-reference/ark_describe_backups.md +++ b/docs/cli-reference/ark_describe_backups.md @@ -16,6 +16,7 @@ ark describe backups [NAME1] [NAME2] [NAME...] [flags] ``` -h, --help help for backups -l, --selector string only show items matching this label selector + --volume-details display details of restic volume backups ``` ### Options inherited from parent commands diff --git a/docs/cli-reference/ark_describe_restores.md b/docs/cli-reference/ark_describe_restores.md index e0d401ca3..0b5cddd22 100644 --- a/docs/cli-reference/ark_describe_restores.md +++ b/docs/cli-reference/ark_describe_restores.md @@ -16,6 +16,7 @@ ark describe restores [NAME1] [NAME2] [NAME...] [flags] ``` -h, --help help for restores -l, --selector string only show items matching this label selector + --volume-details display details of restic volume restores ``` ### Options inherited from parent commands diff --git a/docs/cli-reference/ark_restore_describe.md b/docs/cli-reference/ark_restore_describe.md index 2c5732380..cb06f1314 100644 --- a/docs/cli-reference/ark_restore_describe.md +++ b/docs/cli-reference/ark_restore_describe.md @@ -16,6 +16,7 @@ ark restore describe [NAME1] [NAME2] [NAME...] [flags] ``` -h, --help help for describe -l, --selector string only show items matching this label selector + --volume-details display details of restic volume restores ``` ### Options inherited from parent commands diff --git a/pkg/cmd/cli/backup/describe.go b/pkg/cmd/cli/backup/describe.go index a06f33a59..8f4630827 100644 --- a/pkg/cmd/cli/backup/describe.go +++ b/pkg/cmd/cli/backup/describe.go @@ -20,18 +20,22 @@ import ( "fmt" "os" - pkgbackup "github.com/heptio/ark/pkg/backup" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "github.com/heptio/ark/pkg/apis/ark/v1" + pkgbackup "github.com/heptio/ark/pkg/backup" "github.com/heptio/ark/pkg/client" "github.com/heptio/ark/pkg/cmd" "github.com/heptio/ark/pkg/cmd/util/output" + "github.com/heptio/ark/pkg/restic" ) func NewDescribeCommand(f client.Factory, use string) *cobra.Command { - var listOptions metav1.ListOptions + var ( + listOptions metav1.ListOptions + volumeDetails bool + ) c := &cobra.Command{ Use: use + " [NAME1] [NAME2] [NAME...]", @@ -61,7 +65,13 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { fmt.Fprintf(os.Stderr, "error getting DeleteBackupRequests for backup %s: %v\n", backup.Name, err) } - s := output.DescribeBackup(&backup, deleteRequestList.Items) + opts := restic.NewPodVolumeBackupListOptions(backup.Name, string(backup.UID)) + podVolumeBackupList, err := arkClient.ArkV1().PodVolumeBackups(f.Namespace()).List(opts) + if err != nil { + fmt.Fprintf(os.Stderr, "error getting PodVolumeBackups for backup %s: %v\n", backup.Name, err) + } + + s := output.DescribeBackup(&backup, deleteRequestList.Items, podVolumeBackupList.Items, volumeDetails) if first { first = false fmt.Print(s) @@ -74,6 +84,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { } c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "only show items matching this label selector") + c.Flags().BoolVar(&volumeDetails, "volume-details", volumeDetails, "display details of restic volume backups") return c } diff --git a/pkg/cmd/cli/restore/describe.go b/pkg/cmd/cli/restore/describe.go index 41572b936..e8f0e953f 100644 --- a/pkg/cmd/cli/restore/describe.go +++ b/pkg/cmd/cli/restore/describe.go @@ -18,6 +18,7 @@ package restore import ( "fmt" + "os" "github.com/spf13/cobra" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -26,10 +27,14 @@ import ( "github.com/heptio/ark/pkg/client" "github.com/heptio/ark/pkg/cmd" "github.com/heptio/ark/pkg/cmd/util/output" + "github.com/heptio/ark/pkg/restic" ) func NewDescribeCommand(f client.Factory, use string) *cobra.Command { - var listOptions metav1.ListOptions + var ( + listOptions metav1.ListOptions + volumeDetails bool + ) c := &cobra.Command{ Use: use + " [NAME1] [NAME2] [NAME...]", @@ -53,7 +58,13 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { first := true for _, restore := range restores.Items { - s := output.DescribeRestore(&restore, arkClient) + opts := restic.NewPodVolumeRestoreListOptions(restore.Name, string(restore.UID)) + podvolumeRestoreList, err := arkClient.ArkV1().PodVolumeRestores(f.Namespace()).List(opts) + if err != nil { + fmt.Fprintf(os.Stderr, "error getting PodVolumeRestores for restore %s: %v\n", restore.Name, err) + } + + s := output.DescribeRestore(&restore, podvolumeRestoreList.Items, volumeDetails, arkClient) if first { first = false fmt.Print(s) @@ -66,6 +77,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { } c.Flags().StringVarP(&listOptions.LabelSelector, "selector", "l", listOptions.LabelSelector, "only show items matching this label selector") + c.Flags().BoolVar(&volumeDetails, "volume-details", volumeDetails, "display details of restic volume restores") return c } diff --git a/pkg/cmd/util/output/backup_describer.go b/pkg/cmd/util/output/backup_describer.go index de3fc56c9..6516a3bc8 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -18,6 +18,7 @@ package output import ( "fmt" + "sort" "strings" "github.com/heptio/ark/pkg/apis/ark/v1" @@ -25,7 +26,7 @@ import ( ) // DescribeBackup describes a backup in human-readable format. -func DescribeBackup(backup *v1.Backup, deleteRequests []v1.DeleteBackupRequest) string { +func DescribeBackup(backup *v1.Backup, deleteRequests []v1.DeleteBackupRequest, podVolumeBackups []v1.PodVolumeBackup, volumeDetails bool) string { return Describe(func(d *Describer) { d.DescribeMetadata(backup.ObjectMeta) @@ -46,6 +47,11 @@ func DescribeBackup(backup *v1.Backup, deleteRequests []v1.DeleteBackupRequest) d.Println() DescribeDeleteBackupRequests(d, deleteRequests) } + + if len(podVolumeBackups) > 0 { + d.Println() + DescribePodVolumeBackups(d, podVolumeBackups, volumeDetails) + } }) } @@ -241,3 +247,111 @@ func failedDeletionCount(requests []v1.DeleteBackupRequest) int { } return count } + +// DescribePodVolumeBackups describes pod volume backups in human-readable format. +func DescribePodVolumeBackups(d *Describer, backups []v1.PodVolumeBackup, details bool) { + if details { + d.Printf("Restic Backups:\n") + } else { + d.Printf("Restic Backups (specify --volume-details for more information):\n") + } + + // separate backups by phase (combining and New into a single group) + backupsByPhase := groupByPhase(backups) + + // go through phases in a specific order + for _, phase := range []string{ + string(v1.PodVolumeBackupPhaseCompleted), + string(v1.PodVolumeBackupPhaseFailed), + "In Progress", + string(v1.PodVolumeBackupPhaseNew), + } { + if len(backupsByPhase[phase]) == 0 { + continue + } + + // if we're not printing details, just report the phase and count + if !details { + d.Printf("\t%s:\t%d\n", phase, len(backupsByPhase[phase])) + continue + } + + // group the backups in the current phase by pod (i.e. "ns/name") + backupsByPod := new(volumesByPod) + + for _, backup := range backupsByPhase[phase] { + backupsByPod.Add(backup.Spec.Pod.Namespace, backup.Spec.Pod.Name, backup.Spec.Volume) + } + + d.Printf("\t%s:\n", phase) + for _, backupGroup := range backupsByPod.Sorted() { + sort.Strings(backupGroup.volumes) + + // print volumes backed up for this pod + d.Printf("\t\t%s: %s\n", backupGroup.label, strings.Join(backupGroup.volumes, ", ")) + } + } +} + +func groupByPhase(backups []v1.PodVolumeBackup) map[string][]v1.PodVolumeBackup { + backupsByPhase := make(map[string][]v1.PodVolumeBackup) + + phaseToGroup := map[v1.PodVolumeBackupPhase]string{ + v1.PodVolumeBackupPhaseCompleted: string(v1.PodVolumeBackupPhaseCompleted), + v1.PodVolumeBackupPhaseFailed: string(v1.PodVolumeBackupPhaseFailed), + v1.PodVolumeBackupPhaseInProgress: "In Progress", + v1.PodVolumeBackupPhaseNew: string(v1.PodVolumeBackupPhaseNew), + "": string(v1.PodVolumeBackupPhaseNew), + } + + for _, backup := range backups { + group := phaseToGroup[backup.Status.Phase] + backupsByPhase[group] = append(backupsByPhase[group], backup) + } + + return backupsByPhase +} + +type podVolumeGroup struct { + label string + volumes []string +} + +// volumesByPod stores podVolumeGroups, where the grouping +// label is "namespace/name". +type volumesByPod struct { + volumesByPodMap map[string]*podVolumeGroup + volumesByPodSlice []*podVolumeGroup +} + +// Add adds a pod volume with the specified pod namespace, name +// and volume to the appropriate group. +func (v *volumesByPod) Add(namespace, name, volume string) { + if v.volumesByPodMap == nil { + v.volumesByPodMap = make(map[string]*podVolumeGroup) + } + + key := fmt.Sprintf("%s/%s", namespace, name) + + if group, ok := v.volumesByPodMap[key]; !ok { + group := &podVolumeGroup{ + label: key, + volumes: []string{volume}, + } + + v.volumesByPodMap[key] = group + v.volumesByPodSlice = append(v.volumesByPodSlice, group) + } else { + group.volumes = append(group.volumes, volume) + } +} + +// Sorted returns a slice of all pod volume groups, ordered by +// label. +func (v *volumesByPod) Sorted() []*podVolumeGroup { + sort.Slice(v.volumesByPodSlice, func(i, j int) bool { + return v.volumesByPodSlice[i].label <= v.volumesByPodSlice[j].label + }) + + return v.volumesByPodSlice +} diff --git a/pkg/cmd/util/output/restore_describer.go b/pkg/cmd/util/output/restore_describer.go index 6251de411..7f907fd5e 100644 --- a/pkg/cmd/util/output/restore_describer.go +++ b/pkg/cmd/util/output/restore_describer.go @@ -19,6 +19,7 @@ package output import ( "bytes" "encoding/json" + "sort" "strings" "time" @@ -28,7 +29,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func DescribeRestore(restore *v1.Restore, arkClient clientset.Interface) string { +func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestore, volumeDetails bool, arkClient clientset.Interface) string { return Describe(func(d *Describer) { d.DescribeMetadata(restore.ObjectMeta) @@ -96,6 +97,11 @@ func DescribeRestore(restore *v1.Restore, arkClient clientset.Interface) string d.Println() describeRestoreResults(d, restore, arkClient) + + if len(podVolumeRestores) > 0 { + d.Println() + describePodVolumeRestores(d, podVolumeRestores, volumeDetails) + } }) } @@ -136,3 +142,67 @@ func describeRestoreResult(d *Describer, name string, result v1.RestoreResult) { } } } + +// describePodVolumeRestores describes pod volume restores in human-readable format. +func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, details bool) { + if details { + d.Printf("Restic Restores:\n") + } else { + d.Printf("Restic Restores (specify --volume-details for more information):\n") + } + + // separate restores by phase (combining and New into a single group) + restoresByPhase := groupRestoresByPhase(restores) + + // go through phases in a specific order + for _, phase := range []string{ + string(v1.PodVolumeRestorePhaseCompleted), + string(v1.PodVolumeRestorePhaseFailed), + "In Progress", + string(v1.PodVolumeRestorePhaseNew), + } { + if len(restoresByPhase[phase]) == 0 { + continue + } + + // if we're not printing details, just report the phase and count + if !details { + d.Printf("\t%s:\t%d\n", phase, len(restoresByPhase[phase])) + continue + } + + // group the restores in the current phase by pod (i.e. "ns/name") + restoresByPod := new(volumesByPod) + + for _, restore := range restoresByPhase[phase] { + restoresByPod.Add(restore.Spec.Pod.Namespace, restore.Spec.Pod.Name, restore.Spec.Volume) + } + + d.Printf("\t%s:\n", phase) + for _, restoreGroup := range restoresByPod.Sorted() { + sort.Strings(restoreGroup.volumes) + + // print volumes restored up for this pod + d.Printf("\t\t%s: %s\n", restoreGroup.label, strings.Join(restoreGroup.volumes, ", ")) + } + } +} + +func groupRestoresByPhase(restores []v1.PodVolumeRestore) map[string][]v1.PodVolumeRestore { + restoresByPhase := make(map[string][]v1.PodVolumeRestore) + + phaseToGroup := map[v1.PodVolumeRestorePhase]string{ + v1.PodVolumeRestorePhaseCompleted: string(v1.PodVolumeRestorePhaseCompleted), + v1.PodVolumeRestorePhaseFailed: string(v1.PodVolumeRestorePhaseFailed), + v1.PodVolumeRestorePhaseInProgress: "In Progress", + v1.PodVolumeRestorePhaseNew: string(v1.PodVolumeRestorePhaseNew), + "": string(v1.PodVolumeRestorePhaseNew), + } + + for _, restore := range restores { + group := phaseToGroup[restore.Status.Phase] + restoresByPhase[group] = append(restoresByPhase[group], restore) + } + + return restoresByPhase +} diff --git a/pkg/restic/common.go b/pkg/restic/common.go index 4c2932c2c..cbaa136af 100644 --- a/pkg/restic/common.go +++ b/pkg/restic/common.go @@ -176,3 +176,19 @@ func TempCredentialsFile(secretLister corev1listers.SecretLister, arkNamespace, return name, nil } + +// NewPodVolumeBackupListOptions creates a ListOptions with a label selector configured to +// find PodVolumeBackups for the backup identified by name and uid. +func NewPodVolumeBackupListOptions(name, uid string) metav1.ListOptions { + return metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s,%s=%s", arkv1api.BackupNameLabel, name, arkv1api.BackupUIDLabel, uid), + } +} + +// NewPodVolumeRestoreListOptions creates a ListOptions with a label selector configured to +// find PodVolumeRestores for the restore identified by name and uid. +func NewPodVolumeRestoreListOptions(name, uid string) metav1.ListOptions { + return metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s,%s=%s", arkv1api.RestoreNameLabel, name, arkv1api.RestoreUIDLabel, uid), + } +}