From f014cab1fe9cfe413fc1f642dc57ef9f187125f9 Mon Sep 17 00:00:00 2001 From: Steve Kriss Date: Mon, 22 Oct 2018 10:37:30 -0600 Subject: [PATCH] backup describer: show snapshot summary by default, details optionally Signed-off-by: Steve Kriss --- pkg/apis/ark/v1/backup.go | 8 +++ pkg/apis/ark/v1/download_request.go | 9 +-- pkg/cmd/cli/backup/describe.go | 8 +-- pkg/cmd/cli/restore/describe.go | 8 +-- pkg/cmd/util/output/backup_describer.go | 79 +++++++++++++++++++----- pkg/cmd/util/output/output.go | 3 + pkg/cmd/util/output/restore_describer.go | 9 ++- pkg/controller/backup_controller.go | 8 +++ pkg/persistence/object_store.go | 2 + 9 files changed, 100 insertions(+), 34 deletions(-) diff --git a/pkg/apis/ark/v1/backup.go b/pkg/apis/ark/v1/backup.go index 1f0e19653..a59967383 100644 --- a/pkg/apis/ark/v1/backup.go +++ b/pkg/apis/ark/v1/backup.go @@ -190,6 +190,14 @@ type BackupStatus struct { // Completion time is recorded before uploading the backup object. // The server's time is used for CompletionTimestamps CompletionTimestamp metav1.Time `json:"completionTimestamp"` + + // VolumeSnapshotsAttempted is the total number of attempted + // volume snapshots for this backup. + VolumeSnapshotsAttempted int `json:"volumeSnapshotsAttempted"` + + // VolumeSnapshotsCompleted is the total number of successfully + // completed volume snapshots for this backup. + VolumeSnapshotsCompleted int `json:"volumeSnapshotsCompleted"` } // VolumeBackupInfo captures the required information about diff --git a/pkg/apis/ark/v1/download_request.go b/pkg/apis/ark/v1/download_request.go index 8fe333b96..593e801e8 100644 --- a/pkg/apis/ark/v1/download_request.go +++ b/pkg/apis/ark/v1/download_request.go @@ -28,10 +28,11 @@ type DownloadRequestSpec struct { type DownloadTargetKind string const ( - DownloadTargetKindBackupLog DownloadTargetKind = "BackupLog" - DownloadTargetKindBackupContents DownloadTargetKind = "BackupContents" - DownloadTargetKindRestoreLog DownloadTargetKind = "RestoreLog" - DownloadTargetKindRestoreResults DownloadTargetKind = "RestoreResults" + DownloadTargetKindBackupLog DownloadTargetKind = "BackupLog" + DownloadTargetKindBackupContents DownloadTargetKind = "BackupContents" + DownloadTargetKindBackupVolumeSnapshots DownloadTargetKind = "BackupVolumeSnapshots" + DownloadTargetKindRestoreLog DownloadTargetKind = "RestoreLog" + DownloadTargetKindRestoreResults DownloadTargetKind = "RestoreResults" ) // DownloadTarget is the specification for what kind of file to download, and the name of the diff --git a/pkg/cmd/cli/backup/describe.go b/pkg/cmd/cli/backup/describe.go index 8f4630827..c1e3ea56b 100644 --- a/pkg/cmd/cli/backup/describe.go +++ b/pkg/cmd/cli/backup/describe.go @@ -33,8 +33,8 @@ import ( func NewDescribeCommand(f client.Factory, use string) *cobra.Command { var ( - listOptions metav1.ListOptions - volumeDetails bool + listOptions metav1.ListOptions + details bool ) c := &cobra.Command{ @@ -71,7 +71,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { fmt.Fprintf(os.Stderr, "error getting PodVolumeBackups for backup %s: %v\n", backup.Name, err) } - s := output.DescribeBackup(&backup, deleteRequestList.Items, podVolumeBackupList.Items, volumeDetails) + s := output.DescribeBackup(&backup, deleteRequestList.Items, podVolumeBackupList.Items, details, arkClient) if first { first = false fmt.Print(s) @@ -84,7 +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") + c.Flags().BoolVar(&details, "details", details, "display additional detail in the command output") return c } diff --git a/pkg/cmd/cli/restore/describe.go b/pkg/cmd/cli/restore/describe.go index e8f0e953f..8b5974f60 100644 --- a/pkg/cmd/cli/restore/describe.go +++ b/pkg/cmd/cli/restore/describe.go @@ -32,8 +32,8 @@ import ( func NewDescribeCommand(f client.Factory, use string) *cobra.Command { var ( - listOptions metav1.ListOptions - volumeDetails bool + listOptions metav1.ListOptions + details bool ) c := &cobra.Command{ @@ -64,7 +64,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command { fmt.Fprintf(os.Stderr, "error getting PodVolumeRestores for restore %s: %v\n", restore.Name, err) } - s := output.DescribeRestore(&restore, podvolumeRestoreList.Items, volumeDetails, arkClient) + s := output.DescribeRestore(&restore, podvolumeRestoreList.Items, details, arkClient) if first { first = false fmt.Print(s) @@ -77,7 +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") + c.Flags().BoolVar(&details, "details", details, "display additional detail in the command output") return c } diff --git a/pkg/cmd/util/output/backup_describer.go b/pkg/cmd/util/output/backup_describer.go index b34e49d5d..38f58ef02 100644 --- a/pkg/cmd/util/output/backup_describer.go +++ b/pkg/cmd/util/output/backup_describer.go @@ -17,16 +17,27 @@ limitations under the License. package output import ( + "bytes" + "encoding/json" "fmt" "sort" "strings" arkv1api "github.com/heptio/ark/pkg/apis/ark/v1" + "github.com/heptio/ark/pkg/cmd/util/downloadrequest" + clientset "github.com/heptio/ark/pkg/generated/clientset/versioned" + "github.com/heptio/ark/pkg/volume" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // DescribeBackup describes a backup in human-readable format. -func DescribeBackup(backup *arkv1api.Backup, deleteRequests []arkv1api.DeleteBackupRequest, podVolumeBackups []arkv1api.PodVolumeBackup, volumeDetails bool) string { +func DescribeBackup( + backup *arkv1api.Backup, + deleteRequests []arkv1api.DeleteBackupRequest, + podVolumeBackups []arkv1api.PodVolumeBackup, + details bool, + arkClient clientset.Interface, +) string { return Describe(func(d *Describer) { d.DescribeMetadata(backup.ObjectMeta) @@ -41,7 +52,7 @@ func DescribeBackup(backup *arkv1api.Backup, deleteRequests []arkv1api.DeleteBac DescribeBackupSpec(d, backup.Spec) d.Println() - DescribeBackupStatus(d, backup.Status) + DescribeBackupStatus(d, backup, details, arkClient) if len(deleteRequests) > 0 { d.Println() @@ -50,7 +61,7 @@ func DescribeBackup(backup *arkv1api.Backup, deleteRequests []arkv1api.DeleteBac if len(podVolumeBackups) > 0 { d.Println() - DescribePodVolumeBackups(d, podVolumeBackups, volumeDetails) + DescribePodVolumeBackups(d, podVolumeBackups, details) } }) } @@ -167,7 +178,9 @@ func DescribeBackupSpec(d *Describer, spec arkv1api.BackupSpec) { } // DescribeBackupStatus describes a backup status in human-readable format. -func DescribeBackupStatus(d *Describer, status arkv1api.BackupStatus) { +func DescribeBackupStatus(d *Describer, backup *arkv1api.Backup, details bool, arkClient clientset.Interface) { + status := backup.Status + d.Printf("Backup Format Version:\t%d\n", status.Version) d.Println() @@ -197,22 +210,54 @@ func DescribeBackupStatus(d *Describer, status arkv1api.BackupStatus) { } d.Println() - if len(status.VolumeBackups) == 0 { - d.Printf("Persistent Volumes: \n") - } else { + if len(status.VolumeBackups) > 0 { + // pre-v0.10 backup d.Printf("Persistent Volumes:\n") for pvName, info := range status.VolumeBackups { - d.Printf("\t%s:\n", pvName) - d.Printf("\t\tSnapshot ID:\t%s\n", info.SnapshotID) - d.Printf("\t\tType:\t%s\n", info.Type) - d.Printf("\t\tAvailability Zone:\t%s\n", info.AvailabilityZone) - iops := "" - if info.Iops != nil { - iops = fmt.Sprintf("%d", *info.Iops) - } - d.Printf("\t\tIOPS:\t%s\n", iops) + printSnapshot(d, pvName, info.SnapshotID, info.Type, info.AvailabilityZone, info.Iops) } + return } + + if status.VolumeSnapshotsAttempted > 0 { + // v0.10+ backup + if !details { + d.Printf("Persistent Volumes:\t%d of %d snapshots completed successfully (specify --details for more information)\n", status.VolumeSnapshotsCompleted, status.VolumeSnapshotsAttempted) + return + } + + buf := new(bytes.Buffer) + if err := downloadrequest.Stream(arkClient.ArkV1(), backup.Namespace, backup.Name, arkv1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout); err != nil { + d.Printf("Persistent Volumes:\t\n", err) + return + } + + var snapshots []*volume.Snapshot + if err := json.NewDecoder(buf).Decode(&snapshots); err != nil { + d.Printf("Persistent Volumes:\t\n", err) + return + } + + d.Printf("Persistent Volumes:\n") + for _, snap := range snapshots { + printSnapshot(d, snap.Spec.PersistentVolumeName, snap.Status.ProviderSnapshotID, snap.Spec.VolumeType, snap.Spec.VolumeAZ, snap.Spec.VolumeIOPS) + } + return + } + + d.Printf("Persistent Volumes: \n") +} + +func printSnapshot(d *Describer, pvName, snapshotID, volumeType, volumeAZ string, iops *int64) { + d.Printf("\t%s:\n", pvName) + d.Printf("\t\tSnapshot ID:\t%s\n", snapshotID) + d.Printf("\t\tType:\t%s\n", volumeType) + d.Printf("\t\tAvailability Zone:\t%s\n", volumeAZ) + iopsString := "" + if iops != nil { + iopsString = fmt.Sprintf("%d", *iops) + } + d.Printf("\t\tIOPS:\t%s\n", iopsString) } // DescribeDeleteBackupRequests describes delete backup requests in human-readable format. @@ -256,7 +301,7 @@ func DescribePodVolumeBackups(d *Describer, backups []arkv1api.PodVolumeBackup, if details { d.Printf("Restic Backups:\n") } else { - d.Printf("Restic Backups (specify --volume-details for more information):\n") + d.Printf("Restic Backups (specify --details for more information):\n") } // separate backups by phase (combining and New into a single group) diff --git a/pkg/cmd/util/output/output.go b/pkg/cmd/util/output/output.go index 3d6802e84..d1b377997 100644 --- a/pkg/cmd/util/output/output.go +++ b/pkg/cmd/util/output/output.go @@ -19,6 +19,7 @@ package output import ( "fmt" "os" + "time" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -32,6 +33,8 @@ import ( "github.com/heptio/ark/pkg/util/encode" ) +const downloadRequestTimeout = 30 * time.Second + // BindFlags defines a set of output-specific flags within the provided // FlagSet. func BindFlags(flags *pflag.FlagSet) { diff --git a/pkg/cmd/util/output/restore_describer.go b/pkg/cmd/util/output/restore_describer.go index 7f907fd5e..876edbae5 100644 --- a/pkg/cmd/util/output/restore_describer.go +++ b/pkg/cmd/util/output/restore_describer.go @@ -21,7 +21,6 @@ import ( "encoding/json" "sort" "strings" - "time" "github.com/heptio/ark/pkg/apis/ark/v1" "github.com/heptio/ark/pkg/cmd/util/downloadrequest" @@ -29,7 +28,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestore, volumeDetails bool, arkClient clientset.Interface) string { +func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestore, details bool, arkClient clientset.Interface) string { return Describe(func(d *Describer) { d.DescribeMetadata(restore.ObjectMeta) @@ -100,7 +99,7 @@ func DescribeRestore(restore *v1.Restore, podVolumeRestores []v1.PodVolumeRestor if len(podVolumeRestores) > 0 { d.Println() - describePodVolumeRestores(d, podVolumeRestores, volumeDetails) + describePodVolumeRestores(d, podVolumeRestores, details) } }) } @@ -114,7 +113,7 @@ func describeRestoreResults(d *Describer, restore *v1.Restore, arkClient clients var buf bytes.Buffer var resultMap map[string]v1.RestoreResult - if err := downloadrequest.Stream(arkClient.ArkV1(), restore.Namespace, restore.Name, v1.DownloadTargetKindRestoreResults, &buf, 30*time.Second); err != nil { + if err := downloadrequest.Stream(arkClient.ArkV1(), restore.Namespace, restore.Name, v1.DownloadTargetKindRestoreResults, &buf, downloadRequestTimeout); err != nil { d.Printf("Warnings:\t\n\nErrors:\t\n", err, err) return } @@ -148,7 +147,7 @@ func describePodVolumeRestores(d *Describer, restores []v1.PodVolumeRestore, det if details { d.Printf("Restic Restores:\n") } else { - d.Printf("Restic Restores (specify --volume-details for more information):\n") + d.Printf("Restic Restores (specify --details for more information):\n") } // separate restores by phase (combining and New into a single group) diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index e70f1b537..a0409d2d1 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -48,6 +48,7 @@ import ( "github.com/heptio/ark/pkg/util/encode" kubeutil "github.com/heptio/ark/pkg/util/kube" "github.com/heptio/ark/pkg/util/logging" + "github.com/heptio/ark/pkg/volume" ) const backupVersion = 1 @@ -398,6 +399,13 @@ func (c *backupController) runBackup(backup *pkgbackup.Request) error { // Otherwise, the JSON file in object storage has a CompletionTimestamp of 'null'. backup.Status.CompletionTimestamp.Time = c.clock.Now() + backup.Status.VolumeSnapshotsAttempted = len(backup.VolumeSnapshots) + for _, snap := range backup.VolumeSnapshots { + if snap.Status.Phase == volume.SnapshotPhaseCompleted { + backup.Status.VolumeSnapshotsCompleted++ + } + } + errs = append(errs, persistBackup(backup, backupFile, logFile, backupStore, c.logger)...) errs = append(errs, recordBackupMetrics(backup.Backup, backupFile, c.metrics)) diff --git a/pkg/persistence/object_store.go b/pkg/persistence/object_store.go index 885c5e808..20ea657da 100644 --- a/pkg/persistence/object_store.go +++ b/pkg/persistence/object_store.go @@ -349,6 +349,8 @@ func (s *objectBackupStore) GetDownloadURL(target arkv1api.DownloadTarget) (stri return s.objectStore.CreateSignedURL(s.bucket, s.layout.getBackupContentsKey(target.Name), DownloadURLTTL) case arkv1api.DownloadTargetKindBackupLog: return s.objectStore.CreateSignedURL(s.bucket, s.layout.getBackupLogKey(target.Name), DownloadURLTTL) + case arkv1api.DownloadTargetKindBackupVolumeSnapshots: + return s.objectStore.CreateSignedURL(s.bucket, s.layout.getBackupVolumeSnapshotsKey(target.Name), DownloadURLTTL) case arkv1api.DownloadTargetKindRestoreLog: return s.objectStore.CreateSignedURL(s.bucket, s.layout.getRestoreLogKey(target.Name), DownloadURLTTL) case arkv1api.DownloadTargetKindRestoreResults: