Files
velero/pkg/cmd/util/output/backup_describer.go
Joseph Antony Vaikath 975f647323
Some checks failed
Run the E2E test on kind / get-go-version (push) Failing after 1m5s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Run the E2E test on kind / run-e2e-test (push) Has been skipped
Main CI / get-go-version (push) Successful in 11s
Main CI / Build (push) Failing after 25s
Close stale issues and PRs / stale (push) Successful in 14s
Trivy Nightly Scan / Trivy nightly scan (velero, main) (push) Failing after 1m36s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-aws, main) (push) Failing after 1m2s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-gcp, main) (push) Failing after 1m19s
Trivy Nightly Scan / Trivy nightly scan (velero-plugin-for-microsoft-azure, main) (push) Failing after 1m2s
Wildcard ns implement (#9255)
* Add wildcard status fields

Signed-off-by: Joseph <jvaikath@redhat.com>

* Implement wildcard namespace expansion in item collector

- Introduced methods to get active namespaces and expand wildcard includes/excludes in the item collector.
- Updated getNamespacesToList to handle wildcard patterns and return expanded lists.
- Added utility functions for setting includes and excludes in the IncludesExcludes struct.
- Created a new package for wildcard handling, including functions to determine when to expand wildcards and to perform the expansion.

This enhances the backup process by allowing more flexible namespace selection based on wildcard patterns.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Enhance wildcard expansion logic and logging in item collector

- Improved logging to include original includes and excludes when expanding wildcards.
- Updated the ShouldExpandWildcards function to check for wildcard patterns in excludes.
- Added comments for clarity in the expandWildcards function regarding pattern handling.

These changes enhance the clarity and functionality of the wildcard expansion process in the backup system.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Add wildcard namespace fields to Backup CRD and update deepcopy methods

- Introduced `wildcardIncludedNamespaces` and `wildcardExcludedNamespaces` fields to the Backup CRD to support wildcard patterns for namespace inclusion and exclusion.
- Updated deepcopy methods to handle the new fields, ensuring proper copying of data during object manipulation.

These changes enhance the flexibility of namespace selection in backup operations, aligning with recent improvements in wildcard handling.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Refactor Backup CRD to rename wildcard namespace fields

- Updated `BackupStatus` struct to rename `WildcardIncludedNamespaces` to `WildcardExpandedIncludedNamespaces` and `WildcardExcludedNamespaces` to `WildcardExpandedExcludedNamespaces` for clarity.
- Adjusted associated comments to reflect the new naming and ensure consistency in documentation.
- Modified deepcopy methods to accommodate the renamed fields, ensuring proper data handling during object manipulation.

These changes enhance the clarity and maintainability of the Backup CRD, aligning with recent improvements in wildcard handling.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Fix

Signed-off-by: Joseph <jvaikath@redhat.com>

* Refactor where wildcard expansion happens

Signed-off-by: Joseph <jvaikath@redhat.com>

* Refactor Backup CRD and related components for expanded namespace handling

- Updated `BackupStatus` struct to rename fields for clarity: `WildcardExpandedIncludedNamespaces` and `WildcardExpandedExcludedNamespaces` are now `ExpandedIncludedNamespaces` and `ExpandedExcludedNamespaces`, respectively.
- Adjusted associated comments and deepcopy methods to reflect the new naming conventions.
- Removed the `getActiveNamespaces` function from the item collector, streamlining the namespace handling process.
- Enhanced logging during wildcard expansion to provide clearer insights into the process.

These changes improve the clarity and maintainability of the Backup CRD and enhance the functionality of namespace selection in backup operations.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Refactor wildcard expansion logic in item collector and enhance testing

- Moved the wildcard expansion logic into a dedicated method, `expandNamespaceWildcards`, improving code organization and readability.
- Updated logging to provide detailed insights during the wildcard expansion process.
- Introduced comprehensive unit tests for wildcard handling, covering various scenarios and edge cases.
- Enhanced the `ShouldExpandWildcards` function to better identify wildcard patterns and validate inputs.

These changes improve the maintainability and robustness of the wildcard handling in the backup system.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Enhance Restore CRD with expanded namespace fields and update logic

- Added `ExpandedIncludedNamespaces` and `ExpandedExcludedNamespaces` fields to the `RestoreStatus` struct to support expanded wildcard namespace handling.
- Updated the `DeepCopyInto` method to ensure proper copying of the new fields.
- Implemented logic in the restore process to expand wildcard patterns for included and excluded namespaces, improving flexibility in namespace selection during restores.
- Enhanced logging to provide insights into the expanded namespaces.

These changes improve the functionality and maintainability of the restore process, aligning with recent enhancements in wildcard handling.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Refactor Backup and Restore CRDs to enhance wildcard namespace handling

- Renamed fields in `BackupStatus` and `RestoreStatus` from `ExpandedIncludedNamespaces` and `ExpandedExcludedNamespaces` to `IncludeWildcardMatches` and `ExcludeWildcardMatches` for clarity.
- Introduced a new field `WildcardResult` to record the final namespaces after applying wildcard logic.
- Updated the `DeepCopyInto` methods to accommodate the new field names and ensure proper data handling.
- Enhanced comments to reflect the changes and improve documentation clarity.

These updates improve the maintainability and clarity of the CRDs, aligning with recent enhancements in wildcard handling.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Enhance wildcard namespace handling in Backup and Restore processes

- Updated `BackupRequest` and `Restore` status structures to include a new field `WildcardResult`, which captures the final list of namespaces after applying wildcard logic.
- Renamed existing fields to `IncludeWildcardMatches` and `ExcludeWildcardMatches` for improved clarity.
- Enhanced logging to provide detailed insights into the expanded namespaces and final results during backup and restore operations.
- Introduced a new utility function `GetWildcardResult` to streamline the selection of namespaces based on include/exclude criteria.

These changes improve the clarity and functionality of namespace selection in both backup and restore processes, aligning with recent enhancements in wildcard handling.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Refactor namespace wildcard expansion logic in restore process

- Moved the wildcard expansion logic into a dedicated method, `expandNamespaceWildcards`, improving code organization and readability.
- Enhanced error handling and logging to provide detailed insights into the expanded namespaces during the restore operation.
- Updated the restore context with expanded namespace patterns and final results, ensuring clarity in the restore status.

These changes improve the maintainability and clarity of the restore process, aligning with recent enhancements in wildcard handling.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Add checks for "*" in exclude

Signed-off-by: Joseph <jvaikath@redhat.com>

* Rebase

Signed-off-by: Joseph <jvaikath@redhat.com>

* Create NamespaceIncludesExcludes to get full NS listing for backup w/

Signed-off-by: Scott Seago <sseago@redhat.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Add new NamespaceIncludesExcludes struct

Signed-off-by: Joseph <jvaikath@redhat.com>

* Move namespace expansion logic

Signed-off-by: Joseph <jvaikath@redhat.com>

* Update backup status with expansion

Signed-off-by: Joseph <jvaikath@redhat.com>

* Wildcard status update

Signed-off-by: Joseph <jvaikath@redhat.com>

* Skip ns check if wildcard expansion

Signed-off-by: Joseph <jvaikath@redhat.com>

* Move wildcard expansion to getResourceItems

Signed-off-by: Joseph <jvaikath@redhat.com>

* lint

Signed-off-by: Joseph <jvaikath@redhat.com>

* Changelog

Signed-off-by: Joseph <jvaikath@redhat.com>

* linting issues

Signed-off-by: Joseph <jvaikath@redhat.com>

* Remove wildcard restore to check if tests pass

Signed-off-by: Joseph <jvaikath@redhat.com>

* Fix namespace mapping test bug from lint fix

The previous commit (0a4aabcf4) attempted to fix linting issues by
using strings.Builder, but incorrectly wrote commas to a separate
builder and concatenated them at the end instead of between namespace
mappings.

This caused the namespace mapping string to be malformed:
  Before: ns-1:ns-1-mapped,ns-2:ns-2-mapped
  Bug:    ns-1:ns-1-mappedns-2:ns-2-mapped,,

The malformed string was parsed as a single mapping with an invalid
namespace name containing a colon, causing Kubernetes to reject it:
  "ns-1-mappedns-2:ns-2-mapped" is invalid

Fix by properly using strings.Builder to construct the mapping string
with commas between entries, addressing both the linting concern and
the functional bug.

Fixes the MultiNamespacesMappingResticTest and
MultiNamespacesMappingSnapshotTest failures.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Fix wildcard namespace expansion edge cases

This commit fixes two bugs in the wildcard namespace expansion feature:

1. Empty wildcard results: When a wildcard pattern (e.g., "invalid*")
   matched no namespaces, the backup would incorrectly back up ALL
   namespaces instead of backing up nothing. This was because the empty
   includes list was indistinguishable from "no filter specified".

   Fix: Added wildcardExpanded flag to NamespaceIncludesExcludes to
   track when wildcard expansion has occurred. When true and the
   includes list is empty, ShouldInclude now correctly returns false.

2. Premature namespace filtering: An earlier attempt to fix bug #1
   filtered namespaces too early in collectNamespaces, breaking
   LabelSelector tests where namespaces should be included based on
   resources within them matching the label selector.

   Fix: Removed the premature filtering and rely on the existing
   filterNamespaces call at the end of getAllItems, which correctly
   handles both wildcard expansion and label selector scenarios.

The fixes ensure:
- Wildcard patterns matching nothing result in empty backups
- Label selectors still work correctly (namespace included if any
  resource in it matches the selector)
- State is preserved across multiple ResolveNamespaceList calls

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
Signed-off-by: Joseph <jvaikath@redhat.com>

* Run wildcard expansion during backup processing

Signed-off-by: Joseph <jvaikath@redhat.com>

* Lint fix

Signed-off-by: Joseph <jvaikath@redhat.com>

* Improve coverage

Signed-off-by: Joseph <jvaikath@redhat.com>

* gofmt fix

Signed-off-by: Joseph <jvaikath@redhat.com>

* Add wildcard details to describe backup status

Signed-off-by: Joseph <jvaikath@redhat.com>

* Revert "Remove wildcard restore to check if tests pass"

This reverts commit 4e22c2af855b71447762cb0a9fab7e7049f38a5f.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Add restore describe for wildcard namespaces Revert restore wildcard removal

Signed-off-by: Joseph <jvaikath@redhat.com>

* Add coverage

Signed-off-by: Joseph <jvaikath@redhat.com>

* Lint

Signed-off-by: Joseph <jvaikath@redhat.com>

* Remove unintentional changes

Signed-off-by: Joseph <jvaikath@redhat.com>

* Remove wildcard status fields and mentionsRemove usage of wildcard fields for backup and restore status.

Signed-off-by: Joseph <jvaikath@redhat.com>

* Remove status update changelog line

Signed-off-by: Joseph <jvaikath@redhat.com>

* Rename getNamespaceIncludesExcludes
Signed-off-by: Scott Seago <sseago@redhat.com>

Signed-off-by: Scott Seago <sseago@redhat.com>

* Rewrite brace pattern validation

Signed-off-by: Joseph <jvaikath@redhat.com>

* Different var for internal loop

Signed-off-by: Joseph <jvaikath@redhat.com>

---------

Signed-off-by: Joseph <jvaikath@redhat.com>
Signed-off-by: Scott Seago <sseago@redhat.com>
Signed-off-by: Tiger Kaovilai <tkaovila@redhat.com>
Co-authored-by: Scott Seago <sseago@redhat.com>
Co-authored-by: Tiger Kaovilai <tkaovila@redhat.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-12-02 12:28:03 -05:00

982 lines
34 KiB
Go

/*
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"
"sort"
"strconv"
"strings"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/fatih/color"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
veleroapishared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/cmd/util/cacert"
"github.com/vmware-tanzu/velero/pkg/cmd/util/downloadrequest"
"github.com/vmware-tanzu/velero/pkg/itemoperation"
"github.com/vmware-tanzu/velero/internal/volume"
"github.com/vmware-tanzu/velero/pkg/util/collections"
"github.com/vmware-tanzu/velero/pkg/util/results"
)
// DescribeBackup describes a backup in human-readable format.
func DescribeBackup(
ctx context.Context,
kbClient kbclient.Client,
backup *velerov1api.Backup,
deleteRequests []velerov1api.DeleteBackupRequest,
podVolumeBackups []velerov1api.PodVolumeBackup,
details bool,
insecureSkipTLSVerify bool,
caCertFile string,
) string {
return Describe(func(d *Describer) {
d.DescribeMetadata(backup.ObjectMeta)
d.Println()
phase := backup.Status.Phase
if phase == "" {
phase = velerov1api.BackupPhaseNew
}
phaseString := string(phase)
switch phase {
case velerov1api.BackupPhaseFailedValidation, velerov1api.BackupPhasePartiallyFailed, velerov1api.BackupPhaseFailed:
phaseString = color.RedString(phaseString)
case velerov1api.BackupPhaseCompleted:
phaseString = color.GreenString(phaseString)
case velerov1api.BackupPhaseDeleting:
case velerov1api.BackupPhaseWaitingForPluginOperations, velerov1api.BackupPhaseWaitingForPluginOperationsPartiallyFailed:
case velerov1api.BackupPhaseFinalizing, velerov1api.BackupPhaseFinalizingPartiallyFailed:
case velerov1api.BackupPhaseInProgress:
case velerov1api.BackupPhaseNew:
}
logsNote := ""
if backup.Status.Phase == velerov1api.BackupPhaseFailed || backup.Status.Phase == velerov1api.BackupPhasePartiallyFailed {
logsNote = fmt.Sprintf(" (run `velero backup logs %s` for more information)", backup.Name)
}
d.Printf("Phase:\t%s%s\n", phaseString, logsNote)
if backup.Spec.ResourcePolicy != nil {
d.Println()
DescribeResourcePolicies(d, backup.Spec.ResourcePolicy)
}
if backup.Spec.UploaderConfig != nil && backup.Spec.UploaderConfig.ParallelFilesUpload > 0 {
d.Println()
DescribeUploaderConfigForBackup(d, backup.Spec)
}
status := backup.Status
if len(status.ValidationErrors) > 0 {
d.Println()
d.Printf("Validation errors:")
for _, ve := range status.ValidationErrors {
d.Printf("\t%s\n", color.RedString(ve))
}
}
d.Println()
DescribeBackupResults(ctx, kbClient, d, backup, insecureSkipTLSVerify, caCertFile)
d.Println()
DescribeBackupSpec(d, backup.Spec)
d.Println()
DescribeBackupStatus(ctx, kbClient, d, backup, details, insecureSkipTLSVerify, caCertFile, podVolumeBackups)
if len(deleteRequests) > 0 {
d.Println()
DescribeDeleteBackupRequests(d, deleteRequests)
}
})
}
// DescribeResourcePolicies describes resource policies in human-readable format
func DescribeResourcePolicies(d *Describer, resPolicies *corev1api.TypedLocalObjectReference) {
d.Printf("Resource policies:\n")
d.Printf("\tType:\t%s\n", resPolicies.Kind)
d.Printf("\tName:\t%s\n", resPolicies.Name)
}
// DescribeUploaderConfigForBackup describes uploader config in human-readable format
func DescribeUploaderConfigForBackup(d *Describer, spec velerov1api.BackupSpec) {
d.Printf("Uploader config:\n")
d.Printf("\tParallel files upload:\t%d\n", spec.UploaderConfig.ParallelFilesUpload)
}
// DescribeBackupSpec describes a backup spec in human-readable format.
func DescribeBackupSpec(d *Describer, spec velerov1api.BackupSpec) {
// TODO make a helper for this and use it in all the describers.
d.Printf("Namespaces:\n")
var s string
if len(spec.IncludedNamespaces) == 0 {
s = "*"
} else {
s = strings.Join(spec.IncludedNamespaces, ", ")
}
d.Printf("\tIncluded:\t%s\n", s)
if len(spec.ExcludedNamespaces) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.ExcludedNamespaces, ", ")
}
d.Printf("\tExcluded:\t%s\n", s)
d.Println()
d.Printf("Resources:\n")
if collections.UseOldResourceFilters(spec) {
if len(spec.IncludedResources) == 0 {
s = "*"
} else {
s = strings.Join(spec.IncludedResources, ", ")
}
d.Printf("\tIncluded:\t%s\n", s)
if len(spec.ExcludedResources) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.ExcludedResources, ", ")
}
d.Printf("\tExcluded:\t%s\n", s)
d.Printf("\tCluster-scoped:\t%s\n", BoolPointerString(spec.IncludeClusterResources, "excluded", "included", "auto"))
} else {
if len(spec.IncludedClusterScopedResources) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.IncludedClusterScopedResources, ", ")
}
d.Printf("\tIncluded cluster-scoped:\t%s\n", s)
if len(spec.ExcludedClusterScopedResources) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.ExcludedClusterScopedResources, ", ")
}
d.Printf("\tExcluded cluster-scoped:\t%s\n", s)
if len(spec.IncludedNamespaceScopedResources) == 0 {
s = "*"
} else {
s = strings.Join(spec.IncludedNamespaceScopedResources, ", ")
}
d.Printf("\tIncluded namespace-scoped:\t%s\n", s)
if len(spec.ExcludedNamespaceScopedResources) == 0 {
s = emptyDisplay
} else {
s = strings.Join(spec.ExcludedNamespaceScopedResources, ", ")
}
d.Printf("\tExcluded namespace-scoped:\t%s\n", s)
}
d.Println()
s = emptyDisplay
if spec.LabelSelector != nil {
s = metav1.FormatLabelSelector(spec.LabelSelector)
}
d.Printf("Label selector:\t%s\n", s)
d.Println()
if len(spec.OrLabelSelectors) == 0 {
s = emptyDisplay
} else {
orLabelSelectors := []string{}
for _, v := range spec.OrLabelSelectors {
orLabelSelectors = append(orLabelSelectors, metav1.FormatLabelSelector(v))
}
s = strings.Join(orLabelSelectors, " or ")
}
d.Printf("Or label selector:\t%s\n", s)
d.Println()
d.Printf("Storage Location:\t%s\n", spec.StorageLocation)
d.Println()
d.Printf("Velero-Native Snapshot PVs:\t%s\n", BoolPointerString(spec.SnapshotVolumes, "false", "true", "auto"))
if spec.DefaultVolumesToFsBackup != nil {
d.Printf("File System Backup (Default):\t%s\n", BoolPointerString(spec.DefaultVolumesToFsBackup, "false", "true", ""))
}
d.Printf("Snapshot Move Data:\t%s\n", BoolPointerString(spec.SnapshotMoveData, "false", "true", "auto"))
if len(spec.DataMover) == 0 {
s = defaultDataMover
} else {
s = spec.DataMover
}
d.Printf("Data Mover:\t%s\n", s)
d.Println()
d.Printf("TTL:\t%s\n", spec.TTL.Duration)
d.Println()
d.Printf("CSISnapshotTimeout:\t%s\n", spec.CSISnapshotTimeout.Duration)
d.Printf("ItemOperationTimeout:\t%s\n", spec.ItemOperationTimeout.Duration)
d.Println()
if len(spec.Hooks.Resources) == 0 {
d.Printf("Hooks:\t" + emptyDisplay + "\n")
} else {
d.Printf("Hooks:\n")
d.Printf("\tResources:\n")
for _, backupResourceHookSpec := range spec.Hooks.Resources {
d.Printf("\t\t%s:\n", backupResourceHookSpec.Name)
d.Printf("\t\t\tNamespaces:\n")
var s string
if len(backupResourceHookSpec.IncludedNamespaces) == 0 {
s = "*"
} else {
s = strings.Join(backupResourceHookSpec.IncludedNamespaces, ", ")
}
d.Printf("\t\t\t\tIncluded:\t%s\n", s)
if len(backupResourceHookSpec.ExcludedNamespaces) == 0 {
s = emptyDisplay
} else {
s = strings.Join(backupResourceHookSpec.ExcludedNamespaces, ", ")
}
d.Printf("\t\t\t\tExcluded:\t%s\n", s)
d.Println()
d.Printf("\t\t\tResources:\n")
if len(backupResourceHookSpec.IncludedResources) == 0 {
s = "*"
} else {
s = strings.Join(backupResourceHookSpec.IncludedResources, ", ")
}
d.Printf("\t\t\t\tIncluded:\t%s\n", s)
if len(backupResourceHookSpec.ExcludedResources) == 0 {
s = emptyDisplay
} else {
s = strings.Join(backupResourceHookSpec.ExcludedResources, ", ")
}
d.Printf("\t\t\t\tExcluded:\t%s\n", s)
d.Println()
s = emptyDisplay
if backupResourceHookSpec.LabelSelector != nil {
s = metav1.FormatLabelSelector(backupResourceHookSpec.LabelSelector)
}
d.Printf("\t\t\tLabel selector:\t%s\n", s)
for _, hook := range backupResourceHookSpec.PreHooks {
if hook.Exec != nil {
d.Println()
d.Printf("\t\t\tPre Exec Hook:\n")
d.Printf("\t\t\t\tContainer:\t%s\n", hook.Exec.Container)
d.Printf("\t\t\t\tCommand:\t%s\n", strings.Join(hook.Exec.Command, " "))
d.Printf("\t\t\t\tOn Error:\t%s\n", hook.Exec.OnError)
d.Printf("\t\t\t\tTimeout:\t%s\n", hook.Exec.Timeout.Duration)
}
}
for _, hook := range backupResourceHookSpec.PostHooks {
if hook.Exec != nil {
d.Println()
d.Printf("\t\t\tPost Exec Hook:\n")
d.Printf("\t\t\t\tContainer:\t%s\n", hook.Exec.Container)
d.Printf("\t\t\t\tCommand:\t%s\n", strings.Join(hook.Exec.Command, " "))
d.Printf("\t\t\t\tOn Error:\t%s\n", hook.Exec.OnError)
d.Printf("\t\t\t\tTimeout:\t%s\n", hook.Exec.Timeout.Duration)
}
}
}
}
if spec.OrderedResources != nil {
d.Println()
d.Printf("OrderedResources:\n")
for key, value := range spec.OrderedResources {
d.Printf("\t%s: %s\n", key, value)
}
}
}
// DescribeBackupStatus describes a backup status in human-readable format.
func DescribeBackupStatus(ctx context.Context,
kbClient kbclient.Client,
d *Describer,
backup *velerov1api.Backup,
details bool,
insecureSkipTLSVerify bool,
caCertPath string,
podVolumeBackups []velerov1api.PodVolumeBackup) {
status := backup.Status
// Status.Version has been deprecated, use Status.FormatVersion
d.Printf("Backup Format Version:\t%s\n", status.FormatVersion)
d.Println()
// "<n/a>" output should only be applicable for backups that failed validation
if status.StartTimestamp == nil || status.StartTimestamp.Time.IsZero() {
d.Printf("Started:\t%s\n", "<n/a>")
} else {
d.Printf("Started:\t%s\n", status.StartTimestamp.Time)
}
if status.CompletionTimestamp == nil || status.CompletionTimestamp.Time.IsZero() {
d.Printf("Completed:\t%s\n", "<n/a>")
} else {
d.Printf("Completed:\t%s\n", status.CompletionTimestamp.Time)
}
d.Println()
// 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.
d.Printf("Expiration:\t%s\n", status.Expiration)
d.Println()
if backup.Status.Progress != nil {
if backup.Status.Phase == velerov1api.BackupPhaseInProgress {
d.Printf("Estimated total items to be backed up:\t%d\n", backup.Status.Progress.TotalItems)
d.Printf("Items backed up so far:\t%d\n", backup.Status.Progress.ItemsBackedUp)
} else {
d.Printf("Total items to be backed up:\t%d\n", backup.Status.Progress.TotalItems)
d.Printf("Items backed up:\t%d\n", backup.Status.Progress.ItemsBackedUp)
}
d.Println()
}
describeBackupItemOperations(ctx, kbClient, d, backup, details, insecureSkipTLSVerify, caCertPath)
if details {
describeBackupResourceList(ctx, kbClient, d, backup, insecureSkipTLSVerify, caCertPath)
d.Println()
}
describeBackupVolumes(ctx, kbClient, d, backup, details, insecureSkipTLSVerify, caCertPath, podVolumeBackups)
if status.HookStatus != nil {
d.Println()
d.Printf("HooksAttempted:\t%d\n", status.HookStatus.HooksAttempted)
d.Printf("HooksFailed:\t%d\n", status.HookStatus.HooksFailed)
}
}
func describeBackupItemOperations(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup, details bool, insecureSkipTLSVerify bool, caCertPath string) {
status := backup.Status
if status.BackupItemOperationsAttempted > 0 {
if !details {
d.Printf("Backup Item Operations:\t%d of %d completed successfully, %d failed (specify --details for more information)\n", status.BackupItemOperationsCompleted, status.BackupItemOperationsAttempted, status.BackupItemOperationsFailed)
return
}
// Get BSL cacert if available
bslCACert, err := cacert.GetCACertFromBackup(ctx, kbClient, backup.Namespace, backup)
if err != nil {
// Log the error but don't fail - we can still try to download without the BSL cacert
d.Printf("WARNING: Error getting cacert from BSL: %v\n", err)
bslCACert = ""
}
buf := new(bytes.Buffer)
if err := downloadrequest.StreamWithBSLCACert(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupItemOperations, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath, bslCACert); err != nil {
d.Printf("Backup Item Operations:\t<error getting operation info: %v>\n", err)
return
}
var operations []*itemoperation.BackupOperation
if err := json.NewDecoder(buf).Decode(&operations); err != nil {
d.Printf("Backup Item Operations:\t<error reading operation info: %v>\n", err)
return
}
d.Printf("Backup Item Operations:\n")
for _, operation := range operations {
describeBackupItemOperation(d, operation)
}
}
}
func describeBackupResourceList(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) {
// Get BSL cacert if available
bslCACert, err := cacert.GetCACertFromBackup(ctx, kbClient, backup.Namespace, backup)
if err != nil {
// Log the error but don't fail - we can still try to download without the BSL cacert
d.Printf("WARNING: Error getting cacert from BSL: %v\n", err)
bslCACert = ""
}
buf := new(bytes.Buffer)
if err := downloadrequest.StreamWithBSLCACert(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResourceList, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath, bslCACert); 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
d.Println("Resource List:\t<backup resource list not found>")
} else {
d.Printf("Resource List:\t<error getting backup resource list: %v>\n", err)
}
return
}
var resourceList map[string][]string
if err := json.NewDecoder(buf).Decode(&resourceList); err != nil {
d.Printf("Resource List:\t<error reading backup resource list: %v>\n", err)
return
}
d.Println("Resource List:")
// Sort GVKs in output
gvks := make([]string, 0, len(resourceList))
for gvk := range resourceList {
gvks = append(gvks, gvk)
}
sort.Strings(gvks)
for _, gvk := range gvks {
d.Printf("\t%s:\n\t\t- %s\n", gvk, strings.Join(resourceList[gvk], "\n\t\t- "))
}
}
func describeBackupVolumes(
ctx context.Context,
kbClient kbclient.Client,
d *Describer,
backup *velerov1api.Backup,
details bool,
insecureSkipTLSVerify bool,
caCertPath string,
podVolumeBackupCRs []velerov1api.PodVolumeBackup,
) {
d.Println("Backup Volumes:")
// Get BSL cacert if available
bslCACert, err := cacert.GetCACertFromBackup(ctx, kbClient, backup.Namespace, backup)
if err != nil {
// Log the error but don't fail - we can still try to download without the BSL cacert
d.Printf("WARNING: Error getting cacert from BSL: %v\n", err)
bslCACert = ""
}
nativeSnapshots := []*volume.BackupVolumeInfo{}
csiSnapshots := []*volume.BackupVolumeInfo{}
legacyInfoSource := false
buf := new(bytes.Buffer)
err = downloadrequest.StreamWithBSLCACert(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeInfos, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath, bslCACert)
if err == downloadrequest.ErrNotFound {
nativeSnapshots, err = retrieveNativeSnapshotLegacy(ctx, kbClient, backup, insecureSkipTLSVerify, caCertPath, bslCACert)
if err != nil {
d.Printf("\t<error concluding native snapshot info: %v>\n", err)
return
}
csiSnapshots, err = retrieveCSISnapshotLegacy(ctx, kbClient, backup, insecureSkipTLSVerify, caCertPath, bslCACert)
if err != nil {
d.Printf("\t<error concluding CSI snapshot info: %v>\n", err)
return
}
legacyInfoSource = true
} else if err != nil {
d.Printf("\t<error getting backup volume info: %v>\n", err)
return
} else {
var volumeInfos []volume.BackupVolumeInfo
if err := json.NewDecoder(buf).Decode(&volumeInfos); err != nil {
d.Printf("\t<error reading backup volume info: %v>\n", err)
return
}
for i := range volumeInfos {
switch volumeInfos[i].BackupMethod {
case volume.NativeSnapshot:
nativeSnapshots = append(nativeSnapshots, &volumeInfos[i])
case volume.CSISnapshot:
csiSnapshots = append(csiSnapshots, &volumeInfos[i])
}
}
}
describeNativeSnapshots(d, details, nativeSnapshots)
d.Println()
describeCSISnapshots(d, details, csiSnapshots, legacyInfoSource)
d.Println()
describePodVolumeBackups(d, details, podVolumeBackupCRs)
}
func retrieveNativeSnapshotLegacy(ctx context.Context, kbClient kbclient.Client, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string, bslCACert string) ([]*volume.BackupVolumeInfo, error) {
status := backup.Status
nativeSnapshots := []*volume.BackupVolumeInfo{}
if status.VolumeSnapshotsAttempted == 0 {
return nativeSnapshots, nil
}
buf := new(bytes.Buffer)
if err := downloadrequest.StreamWithBSLCACert(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupVolumeSnapshots, buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath, bslCACert); err != nil {
return nativeSnapshots, errors.Wrapf(err, "error to download native snapshot info")
}
var snapshots []*volume.Snapshot
if err := json.NewDecoder(buf).Decode(&snapshots); err != nil {
return nativeSnapshots, errors.Wrapf(err, "error to decode native snapshot info")
}
for _, snap := range snapshots {
volumeInfo := volume.BackupVolumeInfo{
PVName: snap.Spec.PersistentVolumeName,
NativeSnapshotInfo: &volume.NativeSnapshotInfo{
SnapshotHandle: snap.Status.ProviderSnapshotID,
VolumeType: snap.Spec.VolumeType,
VolumeAZ: snap.Spec.VolumeAZ,
},
}
if snap.Spec.VolumeIOPS != nil {
volumeInfo.NativeSnapshotInfo.IOPS = strconv.FormatInt(*snap.Spec.VolumeIOPS, 10)
}
nativeSnapshots = append(nativeSnapshots, &volumeInfo)
}
return nativeSnapshots, nil
}
func retrieveCSISnapshotLegacy(ctx context.Context, kbClient kbclient.Client, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string, bslCACert string) ([]*volume.BackupVolumeInfo, error) {
status := backup.Status
csiSnapshots := []*volume.BackupVolumeInfo{}
if status.CSIVolumeSnapshotsAttempted == 0 {
return csiSnapshots, nil
}
vsBuf := new(bytes.Buffer)
err := downloadrequest.StreamWithBSLCACert(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindCSIBackupVolumeSnapshots, vsBuf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath, bslCACert)
if err != nil {
return csiSnapshots, errors.Wrapf(err, "error to download vs list")
}
var vsList []snapshotv1api.VolumeSnapshot
if err := json.NewDecoder(vsBuf).Decode(&vsList); err != nil {
return csiSnapshots, errors.Wrapf(err, "error to decode vs list")
}
vscBuf := new(bytes.Buffer)
err = downloadrequest.StreamWithBSLCACert(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindCSIBackupVolumeSnapshotContents, vscBuf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath, bslCACert)
if err != nil {
return csiSnapshots, errors.Wrapf(err, "error to download vsc list")
}
var vscList []snapshotv1api.VolumeSnapshotContent
if err := json.NewDecoder(vscBuf).Decode(&vscList); err != nil {
return csiSnapshots, errors.Wrapf(err, "error to decode vsc list")
}
for _, vsc := range vscList {
volInfo := volume.BackupVolumeInfo{
PreserveLocalSnapshot: true,
CSISnapshotInfo: &volume.CSISnapshotInfo{
VSCName: vsc.Name,
Driver: vsc.Spec.Driver,
},
}
if vsc.Status != nil && vsc.Status.SnapshotHandle != nil {
volInfo.CSISnapshotInfo.SnapshotHandle = *vsc.Status.SnapshotHandle
}
if vsc.Status != nil && vsc.Status.RestoreSize != nil {
volInfo.CSISnapshotInfo.Size = *vsc.Status.RestoreSize
}
for _, vs := range vsList {
if vs.Status.BoundVolumeSnapshotContentName == nil {
continue
}
if vs.Spec.Source.PersistentVolumeClaimName == nil {
continue
}
if *vs.Status.BoundVolumeSnapshotContentName == vsc.Name {
volInfo.PVCName = *vs.Spec.Source.PersistentVolumeClaimName
volInfo.PVCNamespace = vs.Namespace
}
}
if volInfo.PVCName == "" {
volInfo.PVCName = "<PVC name not found>"
}
csiSnapshots = append(csiSnapshots, &volInfo)
}
return csiSnapshots, nil
}
func describeNativeSnapshots(d *Describer, details bool, infos []*volume.BackupVolumeInfo) {
if len(infos) == 0 {
d.Printf("\tVelero-Native Snapshots: <none included>\n")
return
}
d.Println("\tVelero-Native Snapshots:")
for _, info := range infos {
describNativeSnapshot(d, details, info)
}
}
func describNativeSnapshot(d *Describer, details bool, info *volume.BackupVolumeInfo) {
if details {
d.Printf("\t\t%s:\n", info.PVName)
d.Printf("\t\t\tSnapshot ID:\t%s\n", info.NativeSnapshotInfo.SnapshotHandle)
d.Printf("\t\t\tType:\t%s\n", info.NativeSnapshotInfo.VolumeType)
d.Printf("\t\t\tAvailability Zone:\t%s\n", info.NativeSnapshotInfo.VolumeAZ)
d.Printf("\t\t\tIOPS:\t%s\n", info.NativeSnapshotInfo.IOPS)
d.Printf("\t\t\tResult:\t%s\n", info.Result)
} else {
d.Printf("\t\t%s: specify --details for more information\n", info.PVName)
}
}
func describeCSISnapshots(d *Describer, details bool, infos []*volume.BackupVolumeInfo, legacyInfoSource bool) {
if len(infos) == 0 {
if legacyInfoSource {
d.Printf("\tCSI Snapshots: <none included or not detectable>\n")
} else {
d.Printf("\tCSI Snapshots: <none included>\n")
}
return
}
d.Println("\tCSI Snapshots:")
for _, info := range infos {
describeCSISnapshot(d, details, info)
}
}
func describeCSISnapshot(d *Describer, details bool, info *volume.BackupVolumeInfo) {
d.Printf("\t\t%s:\n", fmt.Sprintf("%s/%s", info.PVCNamespace, info.PVCName))
describeLocalSnapshot(d, details, info)
describeDataMovement(d, details, info)
}
func describeLocalSnapshot(d *Describer, details bool, info *volume.BackupVolumeInfo) {
if !info.PreserveLocalSnapshot {
return
}
if details {
d.Printf("\t\t\tSnapshot:\n")
if !info.SnapshotDataMoved && info.CSISnapshotInfo.OperationID != "" {
d.Printf("\t\t\t\tOperation ID: %s\n", info.CSISnapshotInfo.OperationID)
}
d.Printf("\t\t\t\tSnapshot Content Name: %s\n", info.CSISnapshotInfo.VSCName)
d.Printf("\t\t\t\tStorage Snapshot ID: %s\n", info.CSISnapshotInfo.SnapshotHandle)
d.Printf("\t\t\t\tSnapshot Size (bytes): %d\n", info.CSISnapshotInfo.Size)
d.Printf("\t\t\t\tCSI Driver: %s\n", info.CSISnapshotInfo.Driver)
d.Printf("\t\t\t\tResult: %s\n", info.Result)
} else {
d.Printf("\t\t\tSnapshot: %s\n", "included, specify --details for more information")
}
}
func describeDataMovement(d *Describer, details bool, info *volume.BackupVolumeInfo) {
if !info.SnapshotDataMoved {
return
}
if details {
d.Printf("\t\t\tData Movement:\n")
d.Printf("\t\t\t\tOperation ID: %s\n", info.SnapshotDataMovementInfo.OperationID)
dataMover := "velero"
if info.SnapshotDataMovementInfo.DataMover != "" {
dataMover = info.SnapshotDataMovementInfo.DataMover
}
d.Printf("\t\t\t\tData Mover: %s\n", dataMover)
d.Printf("\t\t\t\tUploader Type: %s\n", info.SnapshotDataMovementInfo.UploaderType)
d.Printf("\t\t\t\tMoved data Size (bytes): %d\n", info.SnapshotDataMovementInfo.Size)
if info.SnapshotDataMovementInfo.IncrementalSize > 0 {
d.Printf("\t\t\t\tIncremental data Size (bytes): %d\n", info.SnapshotDataMovementInfo.IncrementalSize)
}
d.Printf("\t\t\t\tResult: %s\n", info.Result)
} else {
d.Printf("\t\t\tData Movement: %s\n", "included, specify --details for more information")
}
}
func describeBackupItemOperation(d *Describer, operation *itemoperation.BackupOperation) {
d.Printf("\tOperation for %s %s/%s:\n", operation.Spec.ResourceIdentifier, operation.Spec.ResourceIdentifier.Namespace, operation.Spec.ResourceIdentifier.Name)
d.Printf("\t\tBackup Item Action Plugin:\t%s\n", operation.Spec.BackupItemAction)
d.Printf("\t\tOperation ID:\t%s\n", operation.Spec.OperationID)
if len(operation.Spec.PostOperationItems) > 0 {
d.Printf("\t\tItems to Update:\n")
}
for _, item := range operation.Spec.PostOperationItems {
d.Printf("\t\t\t%s %s/%s\n", item, item.Namespace, item.Name)
}
d.Printf("\t\tPhase:\t%s\n", operation.Status.Phase)
if operation.Status.Error != "" {
d.Printf("\t\tOperation Error:\t%s\n", operation.Status.Error)
}
if operation.Status.NTotal > 0 || operation.Status.NCompleted > 0 {
d.Printf("\t\tProgress:\t%v of %v complete (%s)\n",
operation.Status.NCompleted,
operation.Status.NTotal,
operation.Status.OperationUnits)
}
if operation.Status.Description != "" {
d.Printf("\t\tProgress description:\t%s\n", operation.Status.Description)
}
if operation.Status.Created != nil {
d.Printf("\t\tCreated:\t%s\n", operation.Status.Created.String())
}
if operation.Status.Started != nil {
d.Printf("\t\tStarted:\t%s\n", operation.Status.Started.String())
}
if operation.Status.Updated != nil {
d.Printf("\t\tUpdated:\t%s\n", operation.Status.Updated.String())
}
}
// DescribeDeleteBackupRequests describes delete backup requests in human-readable format.
func DescribeDeleteBackupRequests(d *Describer, requests []velerov1api.DeleteBackupRequest) {
d.Printf("Deletion Attempts")
if count := failedDeletionCount(requests); count > 0 {
d.Printf(" (%d failed)", count)
}
d.Println(":")
started := false
for _, req := range requests {
if !started {
started = true
} else {
d.Println()
}
d.Printf("\t%s: %s\n", req.CreationTimestamp.String(), req.Status.Phase)
if len(req.Status.Errors) > 0 {
d.Printf("\tErrors:\n")
for _, err := range req.Status.Errors {
d.Printf("\t\t%s\n", err)
}
}
}
}
func failedDeletionCount(requests []velerov1api.DeleteBackupRequest) int {
var count int
for _, req := range requests {
if req.Status.Phase == velerov1api.DeleteBackupRequestPhaseProcessed && len(req.Status.Errors) > 0 {
count++
}
}
return count
}
// describePodVolumeBackups describes pod volume backups in human-readable format.
func describePodVolumeBackups(d *Describer, details bool, podVolumeBackups []velerov1api.PodVolumeBackup) {
// 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(podVolumeBackups) > 0 {
uploaderType = podVolumeBackups[0].Spec.UploaderType
} else {
d.Printf("\tPod Volume Backups: <none included>\n")
return
}
if details {
d.Printf("\tPod Volume Backups - %s:\n", uploaderType)
} else {
d.Printf("\tPod Volume Backups - %s (specify --details for more information):\n", uploaderType)
}
// separate backups by phase (combining <none> and New into a single group)
backupsByPhase := groupByPhase(podVolumeBackups)
// go through phases in a specific order
for _, phase := range []string{
string(velerov1api.PodVolumeBackupPhaseCompleted),
string(velerov1api.PodVolumeBackupPhaseFailed),
string(velerov1api.PodVolumeBackupPhaseCanceled),
"In Progress",
string(velerov1api.PodVolumeBackupPhaseCanceling),
string(velerov1api.PodVolumeBackupPhasePrepared),
string(velerov1api.PodVolumeBackupPhaseAccepted),
string(velerov1api.PodVolumeBackupPhaseNew),
} {
if len(backupsByPhase[phase]) == 0 {
continue
}
// if we're not printing details, just report the phase and count
if !details {
d.Printf("\t\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, phase, backup.Status.Progress, backup.Status.IncrementalBytes)
}
d.Printf("\t\t%s:\n", phase)
for _, backupGroup := range backupsByPod.Sorted() {
sort.Strings(backupGroup.volumes)
// print volumes backed up for this pod
d.Printf("\t\t\t%s: %s\n", backupGroup.label, strings.Join(backupGroup.volumes, ", "))
}
}
}
func groupByPhase(backups []velerov1api.PodVolumeBackup) map[string][]velerov1api.PodVolumeBackup {
backupsByPhase := make(map[string][]velerov1api.PodVolumeBackup)
phaseToGroup := map[velerov1api.PodVolumeBackupPhase]string{
velerov1api.PodVolumeBackupPhaseCompleted: string(velerov1api.PodVolumeBackupPhaseCompleted),
velerov1api.PodVolumeBackupPhaseFailed: string(velerov1api.PodVolumeBackupPhaseFailed),
velerov1api.PodVolumeBackupPhaseCanceled: string(velerov1api.PodVolumeBackupPhaseCanceled),
velerov1api.PodVolumeBackupPhaseInProgress: "In Progress",
velerov1api.PodVolumeBackupPhaseCanceling: string(velerov1api.PodVolumeBackupPhaseCanceling),
velerov1api.PodVolumeBackupPhasePrepared: string(velerov1api.PodVolumeBackupPhasePrepared),
velerov1api.PodVolumeBackupPhaseAccepted: string(velerov1api.PodVolumeBackupPhaseAccepted),
velerov1api.PodVolumeBackupPhaseNew: string(velerov1api.PodVolumeBackupPhaseNew),
"": string(velerov1api.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.
// Used for both backup and restore
func (v *volumesByPod) Add(namespace, name, volume, phase string, progress veleroapishared.DataMoveOperationProgress, incrementalBytes int64) {
if v.volumesByPodMap == nil {
v.volumesByPodMap = make(map[string]*podVolumeGroup)
}
key := fmt.Sprintf("%s/%s", namespace, name)
// append backup progress percentage if backup is in progress
if phase == "In Progress" && progress.TotalBytes != 0 {
volume = fmt.Sprintf("%s (%.2f%%)", volume, float64(progress.BytesDone)/float64(progress.TotalBytes)*100)
} else if phase == string(velerov1api.PodVolumeBackupPhaseCompleted) && incrementalBytes > 0 {
volume = fmt.Sprintf("%s (size: %v, incremental size: %v)", volume, progress.TotalBytes, incrementalBytes)
} else if (phase == string(velerov1api.PodVolumeBackupPhaseCompleted) ||
phase == string(velerov1api.PodVolumeRestorePhaseCompleted)) &&
progress.TotalBytes > 0 {
volume = fmt.Sprintf("%s (size: %v)", volume, progress.TotalBytes)
}
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
}
// DescribeBackupResults describes errors and warnings in human-readable format.
func DescribeBackupResults(ctx context.Context, kbClient kbclient.Client, d *Describer, backup *velerov1api.Backup, insecureSkipTLSVerify bool, caCertPath string) {
if backup.Status.Warnings == 0 && backup.Status.Errors == 0 {
return
}
// Get BSL cacert if available
bslCACert, err := cacert.GetCACertFromBackup(ctx, kbClient, backup.Namespace, backup)
if err != nil {
// Log the error but don't fail - we can still try to download without the BSL cacert
d.Printf("WARNING: Error getting cacert from BSL: %v\n", err)
bslCACert = ""
}
var buf bytes.Buffer
var resultMap map[string]results.Result
// If err 'ErrNotFound' occurs, it means the backup bundle in the bucket has already been there before the backup-result file is introduced.
// We only display the count of errors and warnings in this case.
err = downloadrequest.StreamWithBSLCACert(ctx, kbClient, backup.Namespace, backup.Name, velerov1api.DownloadTargetKindBackupResults, &buf, downloadRequestTimeout, insecureSkipTLSVerify, caCertPath, bslCACert)
if err == downloadrequest.ErrNotFound {
d.Printf("Errors:\t%d\n", backup.Status.Errors)
d.Printf("Warnings:\t%d\n", backup.Status.Warnings)
return
} else if err != nil {
d.Printf("Warnings:\t<error getting warnings: %v>\n\nErrors:\t<error getting errors: %v>\n", err, err)
return
}
if err := json.NewDecoder(&buf).Decode(&resultMap); err != nil {
d.Printf("Warnings:\t<error decoding warnings: %v>\n\nErrors:\t<error decoding errors: %v>\n", err, err)
return
}
if backup.Status.Warnings > 0 {
d.Println()
describeResult(d, "Warnings", resultMap["warnings"])
}
if backup.Status.Errors > 0 {
d.Println()
describeResult(d, "Errors", resultMap["errors"])
}
}