mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-01-05 21:14:56 +00:00
Add a json output to velero backup describe
Signed-off-by: allenxu404 <qix2@vmware.com>
This commit is contained in:
1
changelogs/unreleased/5865-allenxu404
Normal file
1
changelogs/unreleased/5865-allenxu404
Normal file
@@ -0,0 +1 @@
|
||||
Add a json output to cmd velero backup describe
|
||||
@@ -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
|
||||
}
|
||||
|
||||
462
pkg/cmd/util/output/backup_structured_describer.go
Normal file
462
pkg/cmd/util/output/backup_structured_describer.go
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user