Merge pull request #5865 from allenxu404/issue4816

Add a json output to velero backup describe
This commit is contained in:
Xun Jiang/Bruce Jiang
2023-02-21 18:19:06 +08:00
committed by GitHub
4 changed files with 539 additions and 4 deletions

View File

@@ -0,0 +1 @@
Add a json output to cmd velero backup describe

View File

@@ -41,6 +41,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
listOptions metav1.ListOptions
details bool
insecureSkipTLSVerify bool
outputFormat = "plaintext"
)
config, err := client.LoadConfig()
@@ -59,6 +60,10 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
kbClient, err := f.KubebuilderClient()
cmd.CheckError(err)
if outputFormat != "plaintext" && outputFormat != "json" {
cmd.CheckError(fmt.Errorf("Invalid output format '%s'. Valid value are 'plaintext, json'", outputFormat))
}
var backups *velerov1api.BackupList
if len(args) > 0 {
backups = new(velerov1api.BackupList)
@@ -102,13 +107,21 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
}
}
s := output.DescribeBackup(context.Background(), kbClient, &backups.Items[i], deleteRequestList.Items, podVolumeBackupList.Items, vscList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile)
if first {
first = false
// structured output only applies to a single backup in case of OOM
// To describe the list of backups in structured format, users could iterate over the list and describe backup one after another.
if len(backups.Items) == 1 && outputFormat != "plaintext" {
s := output.DescribeBackupInSF(context.Background(), kbClient, &backups.Items[i], deleteRequestList.Items, podVolumeBackupList.Items, vscList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile, outputFormat)
fmt.Print(s)
} else {
fmt.Printf("\n\n%s", s)
s := output.DescribeBackup(context.Background(), kbClient, &backups.Items[i], deleteRequestList.Items, podVolumeBackupList.Items, vscList.Items, details, veleroClient, insecureSkipTLSVerify, caCertFile)
if first {
first = false
fmt.Print(s)
} else {
fmt.Printf("\n\n%s", s)
}
}
}
cmd.CheckError(err)
},
@@ -118,5 +131,7 @@ func NewDescribeCommand(f client.Factory, use string) *cobra.Command {
c.Flags().BoolVar(&details, "details", details, "Display additional detail in the command output.")
c.Flags().BoolVar(&insecureSkipTLSVerify, "insecure-skip-tls-verify", insecureSkipTLSVerify, "If true, the object store's TLS certificate will not be checked for validity. This is insecure and susceptible to man-in-the-middle attacks. Not recommended for production.")
c.Flags().StringVar(&caCertFile, "cacert", caCertFile, "Path to a certificate bundle to use when verifying TLS connections.")
c.Flags().StringVarP(&outputFormat, "output", "o", outputFormat, "Output display format. Valid formats are 'plaintext, json'. 'json' only applies to a single backup")
return c
}

View File

@@ -0,0 +1,462 @@
/*
Copyright the Velero contributors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package output
import (
"bytes"
"context"
"encoding/json"
"fmt"
"strings"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v4/apis/volumesnapshot/v1"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
"github.com/vmware-tanzu/velero/pkg/features"
clientset "github.com/vmware-tanzu/velero/pkg/generated/clientset/versioned"
"github.com/vmware-tanzu/velero/pkg/volume"
)
// DescribeBackupInSF describes a backup in structured format.
func DescribeBackupInSF(
ctx context.Context,
kbClient kbclient.Client,
backup *velerov1api.Backup,
deleteRequests []velerov1api.DeleteBackupRequest,
podVolumeBackups []velerov1api.PodVolumeBackup,
volumeSnapshotContents []snapshotv1api.VolumeSnapshotContent,
details bool,
veleroClient clientset.Interface,
insecureSkipTLSVerify bool,
caCertFile string,
outputFormat string,
) string {
return DescribeInSF(func(d *StructuredDescriber) {
d.DescribeMetadata(backup.ObjectMeta)
d.Describe("phase", backup.Status.Phase)
status := backup.Status
if len(status.ValidationErrors) > 0 {
d.Describe("validationErrors", status.ValidationErrors)
}
d.Describe("errors", status.Errors)
d.Describe("warnings", status.Warnings)
DescribeBackupSpecInSF(d, backup.Spec)
DescribeBackupStatusInSF(ctx, kbClient, d, backup, details, veleroClient, insecureSkipTLSVerify, caCertFile)
if len(deleteRequests) > 0 {
DescribeDeleteBackupRequestsInSF(d, deleteRequests)
}
if features.IsEnabled(velerov1api.CSIFeatureFlag) {
DescribeCSIVolumeSnapshotsInSF(d, details, volumeSnapshotContents)
}
if len(podVolumeBackups) > 0 {
DescribePodVolumeBackupsInSF(d, podVolumeBackups, details)
}
}, outputFormat)
}
// DescribeBackupSpecInSF describes a backup spec in structured format.
func DescribeBackupSpecInSF(d *StructuredDescriber, spec velerov1api.BackupSpec) {
backupSpecInfo := make(map[string]interface{})
var s string
// describe namespaces
namespaceInfo := make(map[string]interface{})
if len(spec.IncludedNamespaces) == 0 {
s = "*"
} else {
s = strings.Join(spec.IncludedNamespaces, ", ")
}
namespaceInfo["included"] = s
if len(spec.ExcludedNamespaces) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.ExcludedNamespaces, ", ")
}
namespaceInfo["excluded"] = s
backupSpecInfo["namespaces"] = namespaceInfo
// describe resources
resourcesInfo := make(map[string]string)
if len(spec.IncludedResources) == 0 {
s = "*"
} else {
s = strings.Join(spec.IncludedResources, ", ")
}
resourcesInfo["included"] = s
if len(spec.ExcludedResources) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.ExcludedResources, ", ")
}
resourcesInfo["excluded"] = s
resourcesInfo["clusterScoped"] = BoolPointerString(spec.IncludeClusterResources, "excluded", "included", "auto")
backupSpecInfo["resources"] = resourcesInfo
// describe label selector
s = emptyDisplay
if spec.LabelSelector != nil {
s = metav1.FormatLabelSelector(spec.LabelSelector)
}
backupSpecInfo["labelSelector"] = s
// describe storage location
backupSpecInfo["storageLocation"] = spec.StorageLocation
// describe snapshot volumes
backupSpecInfo["veleroNativeSnapshotPVs"] = BoolPointerString(spec.SnapshotVolumes, "false", "true", "auto")
// describe TTL
backupSpecInfo["TTL"] = spec.TTL.Duration.String()
// describe CSI snapshot timeout
backupSpecInfo["CSISnapshotTimeout"] = spec.CSISnapshotTimeout.Duration.String()
// describe hooks
hooksInfo := make(map[string]interface{})
hooksResources := make(map[string]interface{})
for _, backupResourceHookSpec := range spec.Hooks.Resources {
ResourceDetails := make(map[string]interface{})
var s string
namespaceInfo := make(map[string]string)
if len(spec.IncludedNamespaces) == 0 {
s = "*"
} else {
s = strings.Join(spec.IncludedNamespaces, ", ")
}
namespaceInfo["included"] = s
if len(spec.ExcludedNamespaces) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.ExcludedNamespaces, ", ")
}
namespaceInfo["excluded"] = s
ResourceDetails["namespaces"] = namespaceInfo
resourcesInfo := make(map[string]string)
if len(spec.IncludedResources) == 0 {
s = "*"
} else {
s = strings.Join(spec.IncludedResources, ", ")
}
resourcesInfo["included"] = s
if len(spec.ExcludedResources) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.ExcludedResources, ", ")
}
resourcesInfo["excluded"] = s
ResourceDetails["resources"] = resourcesInfo
s = emptyDisplay
if backupResourceHookSpec.LabelSelector != nil {
s = metav1.FormatLabelSelector(backupResourceHookSpec.LabelSelector)
}
ResourceDetails["labelSelector"] = s
preHooks := make([]map[string]interface{}, 0)
for _, hook := range backupResourceHookSpec.PreHooks {
if hook.Exec != nil {
preExecHook := make(map[string]interface{})
preExecHook["container"] = hook.Exec.Container
preExecHook["command"] = strings.Join(hook.Exec.Command, " ")
preExecHook["onError:"] = hook.Exec.OnError
preExecHook["timeout"] = hook.Exec.Timeout.Duration.String()
preHooks = append(preHooks, preExecHook)
}
}
ResourceDetails["preExecHook"] = preHooks
postHooks := make([]map[string]interface{}, 0)
for _, hook := range backupResourceHookSpec.PostHooks {
if hook.Exec != nil {
postExecHook := make(map[string]interface{})
postExecHook["container"] = hook.Exec.Container
postExecHook["command"] = strings.Join(hook.Exec.Command, " ")
postExecHook["onError:"] = hook.Exec.OnError
postExecHook["timeout"] = hook.Exec.Timeout.Duration.String()
postHooks = append(postHooks, postExecHook)
}
}
ResourceDetails["postExecHook"] = postHooks
hooksResources[backupResourceHookSpec.Name] = ResourceDetails
}
if len(spec.Hooks.Resources) > 0 {
hooksInfo["resources"] = hooksResources
backupSpecInfo["hooks"] = hooksInfo
}
// desrcibe ordered resources
if spec.OrderedResources != nil {
backupSpecInfo["orderedResources"] = spec.OrderedResources
}
d.Describe("spec", backupSpecInfo)
}
// DescribeBackupStatusInSF describes a backup status in structured format.
func DescribeBackupStatusInSF(ctx context.Context, kbClient kbclient.Client, d *StructuredDescriber, backup *velerov1api.Backup, details bool, veleroClient clientset.Interface, insecureSkipTLSVerify bool, caCertPath string) {
status := backup.Status
backupStatusInfo := make(map[string]interface{})
// Status.Version has been deprecated, use Status.FormatVersion
backupStatusInfo["backupFormatVersion"] = status.FormatVersion
// "<n/a>" output should only be applicable for backups that failed validation
if status.StartTimestamp == nil || status.StartTimestamp.Time.IsZero() {
backupStatusInfo["started"] = "<n/a>"
} else {
backupStatusInfo["started"] = status.StartTimestamp.Time.String()
}
if status.CompletionTimestamp == nil || status.CompletionTimestamp.Time.IsZero() {
backupStatusInfo["completed"] = "<n/a>"
} else {
backupStatusInfo["completed"] = status.CompletionTimestamp.Time.String()
}
// Expiration can't be 0, it is always set to a 30-day default. It can be nil
// if the controller hasn't processed this Backup yet, in which case this will
// just display `<nil>`, though this should be temporary.
backupStatusInfo["expiration"] = status.Expiration.String()
defer d.Describe("status", backupStatusInfo)
if backup.Status.Progress != nil {
if backup.Status.Phase == velerov1api.BackupPhaseInProgress {
backupStatusInfo["estimatedTotalItemsToBeBackedUp"] = backup.Status.Progress.TotalItems
backupStatusInfo["itemsBackedUpSoFar"] = backup.Status.Progress.ItemsBackedUp
} else {
backupStatusInfo["totalItemsToBeBackedUp"] = backup.Status.Progress.TotalItems
backupStatusInfo["itemsBackedUp"] = backup.Status.Progress.ItemsBackedUp
}
}
if details {
describeBackupResourceListInSF(ctx, kbClient, backupStatusInfo, backup, insecureSkipTLSVerify, caCertPath)
}
// In consideration of decoding structured output conveniently, the three separate fields were created here
// the field of "veleroNativeSnapshots" displays the brief snapshots info
// the field of "veleroNativeSnapshotsError" displays the error message if it fails to get snapshot info
// the field of "veleroNativeSnapshotsDetail" displays the detailed snapshots info
if status.VolumeSnapshotsAttempted > 0 {
if !details {
backupStatusInfo["veleroNativeSnapshots"] = fmt.Sprintf("%d of %d snapshots completed successfully", status.VolumeSnapshotsCompleted, status.VolumeSnapshotsAttempted)
return
}
buf := new(bytes.Buffer)
if err := downloadrequest.Stream(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
backupStatusInfo["veleroNativeSnapshotsError"] = fmt.Sprintf("<error getting snapshot info: %v>", err)
return
}
var snapshots []*volume.Snapshot
if err := json.NewDecoder(buf).Decode(&snapshots); err != nil {
backupStatusInfo["veleroNativeSnapshotsError"] = fmt.Sprintf("<error reading snapshot info: %v>", err)
return
}
snapshotDetails := make(map[string]interface{})
for _, snap := range snapshots {
describeSnapshotInSF(snap.Spec.PersistentVolumeName, snap.Status.ProviderSnapshotID, snap.Spec.VolumeType, snap.Spec.VolumeAZ, snap.Spec.VolumeIOPS, snapshotDetails)
}
backupStatusInfo["veleroNativeSnapshotsDetail"] = snapshotDetails
return
}
}
func describeBackupResourceListInSF(ctx context.Context, kbClient kbclient.Client, backupStatusInfo map[string]interface{}, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) {
// In consideration of decoding structured output conveniently, the two separate fields were created here(in func describeBackupResourceList, there is only one field describing either error message or resource list)
// the field of 'resourceListError' gives specific error message when it fails to get resources list
// the field of 'resourceList' lists the rearranged resources
buf := new(bytes.Buffer)
if err := downloadrequest.Stream(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath); err != nil {
if err == downloadrequest.ErrNotFound {
// the backup resource list could be missing if (other reasons may exist as well):
// - the backup was taken prior to v1.1; or
// - the backup hasn't completed yet; or
// - there was an error uploading the file; or
// - the file was manually deleted after upload
backupStatusInfo["resourceListError"] = "<backup resource list not found>"
} else {
backupStatusInfo["resourceListError"] = fmt.Sprintf("<error getting backup resource list: %v>", err)
}
return
}
var resourceList map[string][]string
if err := json.NewDecoder(buf).Decode(&resourceList); err != nil {
backupStatusInfo["resourceListError"] = fmt.Sprintf("<error reading backup resource list: %v>\n", err)
return
}
backupStatusInfo["resourceList"] = resourceList
}
func describeSnapshotInSF(pvName, snapshotID, volumeType, volumeAZ string, iops *int64, snapshotDetails map[string]interface{}) {
snapshotInfo := make(map[string]string)
iopsString := "<N/A>"
if iops != nil {
iopsString = fmt.Sprintf("%d", *iops)
}
snapshotInfo["snapshotID"] = snapshotID
snapshotInfo["type"] = volumeType
snapshotInfo["availabilityZone"] = volumeAZ
snapshotInfo["IOPS"] = iopsString
snapshotDetails[pvName] = snapshotInfo
}
// DescribeDeleteBackupRequestsInSF describes delete backup requests in structured format.
func DescribeDeleteBackupRequestsInSF(d *StructuredDescriber, requests []velerov1api.DeleteBackupRequest) {
deletionAttempts := make(map[string]interface{})
if count := failedDeletionCount(requests); count > 0 {
deletionAttempts["failed"] = count
}
deletionRequests := make([]map[string]interface{}, 0)
for _, req := range requests {
deletionReq := make(map[string]interface{})
deletionReq["creationTimestamp"] = req.CreationTimestamp.String()
deletionReq["phase"] = req.Status.Phase
if len(req.Status.Errors) > 0 {
deletionReq["errors"] = req.Status.Errors
}
deletionRequests = append(deletionRequests, deletionReq)
}
deletionAttempts["deleteBackupRequests"] = deletionRequests
d.Describe("deletionAttempts", deletionAttempts)
}
// DescribePodVolumeBackupsInSF describes pod volume backups in structured format.
func DescribePodVolumeBackupsInSF(d *StructuredDescriber, backups []velerov1api.PodVolumeBackup, details bool) {
PodVolumeBackupsInfo := make(map[string]interface{})
// Get the type of pod volume uploader. Since the uploader only comes from a single source, we can
// take the uploader type from the first element of the array.
var uploaderType string
if len(backups) > 0 {
uploaderType = backups[0].Spec.UploaderType
} else {
return
}
// type display the type of pod volume backups
PodVolumeBackupsInfo["type"] = uploaderType
podVolumeBackupsDetails := make(map[string]interface{})
// separate backups by phase (combining <none> and New into a single group)
backupsByPhase := groupByPhase(backups)
// go through phases in a specific order
for _, phase := range []string{
string(velerov1api.PodVolumeBackupPhaseCompleted),
string(velerov1api.PodVolumeBackupPhaseFailed),
"In Progress",
string(velerov1api.PodVolumeBackupPhaseNew),
} {
if len(backupsByPhase[phase]) == 0 {
continue
}
// if we're not printing details, just report the phase and count
if !details {
podVolumeBackupsDetails[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, phase, backup.Status.Progress)
}
backupsByPods := make([]map[string]string, 0)
for _, backupGroup := range backupsByPod.volumesByPodSlice {
// print volumes backed up for this pod
backupsByPods = append(backupsByPods, map[string]string{backupGroup.label: strings.Join(backupGroup.volumes, ", ")})
}
podVolumeBackupsDetails[phase] = backupsByPods
}
// Pod Volume Backups Details display the detailed pod volume backups info
PodVolumeBackupsInfo["podVolumeBackupsDetails"] = podVolumeBackupsDetails
d.Describe("podVolumeBackups", PodVolumeBackupsInfo)
}
// DescribeCSIVolumeSnapshotsInSF describes CSI volume snapshots in structured format.
func DescribeCSIVolumeSnapshotsInSF(d *StructuredDescriber, details bool, volumeSnapshotContents []snapshotv1api.VolumeSnapshotContent) {
CSIVolumeSnapshotsInfo := make(map[string]interface{})
if !features.IsEnabled(velerov1api.CSIFeatureFlag) {
return
}
if len(volumeSnapshotContents) == 0 {
return
}
// In consideration of decoding structured output conveniently, the two separate fields were created here
// the field of 'CSI Volume Snapshots Count' displays the count of CSI Volume Snapshots
// the field of 'CSI Volume Snapshots Details' displays the content of CSI Volume Snapshots
if !details {
CSIVolumeSnapshotsInfo["CSIVolumeSnapshotsCount"] = len(volumeSnapshotContents)
return
}
vscDetails := make(map[string]interface{})
for _, vsc := range volumeSnapshotContents {
DescribeVSCInSF(details, vsc, vscDetails)
}
CSIVolumeSnapshotsInfo["CSIVolumeSnapshotsDetails"] = vscDetails
d.Describe("CSIVolumeSnapshots", CSIVolumeSnapshotsInfo)
}
// DescribeVSCInSF describes CSI volume snapshot contents in structured format.
func DescribeVSCInSF(details bool, vsc snapshotv1api.VolumeSnapshotContent, vscDetails map[string]interface{}) {
content := make(map[string]interface{})
if vsc.Status == nil {
vscDetails[vsc.Name] = content
return
}
if vsc.Status.SnapshotHandle != nil {
content["storageSnapshotID"] = *vsc.Status.SnapshotHandle
}
if vsc.Status.RestoreSize != nil {
content["snapshotSize(bytes)"] = *vsc.Status.RestoreSize
}
if vsc.Status.ReadyToUse != nil {
content["readyToUse"] = *vsc.Status.ReadyToUse
}
vscDetails[vsc.Name] = content
}

View File

@@ -18,6 +18,7 @@ package output
import (
"bytes"
"encoding/json"
"fmt"
"sort"
"strings"
@@ -46,6 +47,15 @@ func Describe(fn func(d *Describer)) string {
return d.buf.String()
}
func NewDescriber(minwidth, tabwidth, padding int, padchar byte, flags uint) *Describer {
d := &Describer{
out: new(tabwriter.Writer),
buf: new(bytes.Buffer),
}
d.out.Init(d.buf, minwidth, tabwidth, padding, padchar, flags)
return d
}
func (d *Describer) Printf(msg string, args ...interface{}) {
fmt.Fprint(d.out, d.Prefix)
fmt.Fprintf(d.out, msg, args...)
@@ -119,3 +129,50 @@ func BoolPointerString(b *bool, falseString, trueString, nilString string) strin
}
return falseString
}
type StructuredDescriber struct {
output map[string]interface{}
format string
}
// NewStructuredDescriber creates a StructuredDescriber.
func NewStructuredDescriber(format string) *StructuredDescriber {
return &StructuredDescriber{
output: make(map[string]interface{}),
format: format,
}
}
// DescribeInSF returns the structured output based on the func
// that applies StructuredDescriber to collect outputs.
// This function takes arg 'format' for future format extension.
func DescribeInSF(fn func(d *StructuredDescriber), format string) string {
d := NewStructuredDescriber(format)
fn(d)
return d.JsonEncode()
}
// Describe adds all types of argument to d.output.
func (d *StructuredDescriber) Describe(name string, arg interface{}) {
d.output[name] = arg
}
// DescribeMetadata describes standard object metadata.
func (d *StructuredDescriber) DescribeMetadata(metadata metav1.ObjectMeta) {
metadataInfo := make(map[string]interface{})
metadataInfo["name"] = metadata.Name
metadataInfo["namespace"] = metadata.Namespace
metadataInfo["labels"] = metadata.Labels
metadataInfo["annotations"] = metadata.Annotations
d.Describe("metadata", metadataInfo)
}
// JsonEncode encodes d.output to json
func (d *StructuredDescriber) JsonEncode() string {
byteBuffer := &bytes.Buffer{}
encoder := json.NewEncoder(byteBuffer)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
_ = encoder.Encode(d.output)
return byteBuffer.String()
}