Merge branch 'main' into block-data-mover-design

This commit is contained in:
lyndon-li
2026-04-14 15:37:45 +08:00
committed by GitHub
186 changed files with 5769 additions and 2831 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v6
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
with:
image-ref: 'docker.io/velero/${{ matrix.images }}:${{ matrix.versions }}'
severity: 'CRITICAL,HIGH,MEDIUM'
+93
View File
@@ -0,0 +1,93 @@
name: Pull Request File Path Check
on: [pull_request]
jobs:
filepath-check:
name: Check for invalid characters in file paths
runs-on: ubuntu-latest
steps:
- name: Check out the code
uses: actions/checkout@v6
- name: Validate file paths for Go module compatibility
run: |
# Go's module zip rejects filenames containing certain characters.
# See golang.org/x/mod/module fileNameOK() for the full specification.
#
# Allowed ASCII: letters, digits, and: !#$%&()+,-.=@[]^_{}~ and space
# Allowed non-ASCII: unicode letters only
# Rejected: " ' * < > ? ` | / \ : and any non-letter unicode (control
# chars, format chars like U+200E LEFT-TO-RIGHT MARK, etc.)
#
# This check catches issues like the U+200E incident in PR #9552.
EXIT_STATUS=0
git ls-files -z | python3 -c "
import sys, unicodedata
data = sys.stdin.buffer.read()
files = data.split(b'\x00')
# Characters explicitly rejected by Go's fileNameOK
# (path separators / and \ are inherent to paths so we check per-element)
bad_ascii = set('\"' + \"'\" + '*<>?\`|:')
allowed_ascii = set('!#$%&()+,-.=@[]^_{}~ ')
def is_ok(ch):
if ch.isascii():
return ch.isalnum() or ch in allowed_ascii
return ch.isalpha()
bad_files = [] # list of (original_path, clean_path, char_desc)
for f in files:
if not f:
continue
try:
name = f.decode('utf-8')
except UnicodeDecodeError:
print(f'::error::Non-UTF-8 bytes in filename: {f!r}')
bad_files.append((repr(f), None, 'non-UTF-8 bytes'))
continue
# Check each path element (split on /)
for element in name.split('/'):
for ch in element:
if not is_ok(ch):
cp = ord(ch)
char_name = unicodedata.name(ch, f'U+{cp:04X}')
char_desc = f'U+{cp:04X} ({char_name})'
# Build cleaned path by stripping invalid chars
clean = '/'.join(
''.join(c for c in elem if is_ok(c))
for elem in name.split('/')
)
print(f'::error file={name}::File \"{name}\" contains invalid char {char_desc}')
bad_files.append((name, clean, char_desc))
break
if bad_files:
print()
print('The following files have characters that are invalid in Go module zip archives:')
print()
for original, clean, desc in bad_files:
print(f' {original} — {desc}')
print()
print('To fix, rename the files to remove the problematic characters:')
print()
for original, clean, desc in bad_files:
if clean:
print(f' mv \"{original}\" \"{clean}\" && git add \"{clean}\"')
print(f' # or: git mv \"{original}\" \"{clean}\"')
else:
print(f' # {original} — cannot auto-suggest rename (non-UTF-8)')
print()
print('See https://github.com/vmware-tanzu/velero/pull/9552 for context.')
sys.exit(1)
else:
print('All file paths are valid for Go module zip.')
" || EXIT_STATUS=1
exit $EXIT_STATUS
+2 -2
View File
@@ -13,7 +13,7 @@
# limitations under the License.
# Velero binary build section
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS velero-builder
FROM --platform=$BUILDPLATFORM golang:1.25-trixie AS velero-builder
ARG GOPROXY
ARG BIN
@@ -49,7 +49,7 @@ RUN mkdir -p /output/usr/bin && \
go clean -modcache -cache
# Restic binary build section
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS restic-builder
FROM --platform=$BUILDPLATFORM golang:1.25-trixie AS restic-builder
ARG GOPROXY
ARG BIN
+1 -1
View File
@@ -15,7 +15,7 @@
ARG OS_VERSION=1809
# Velero binary build section
FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS velero-builder
FROM --platform=$BUILDPLATFORM golang:1.25-trixie AS velero-builder
ARG GOPROXY
ARG BIN
+2 -2
View File
@@ -16,7 +16,7 @@ https://velero.io/docs/v1.18/upgrade-to-1.18/
#### Concurrent backup
In v1.18, Velero is capable to process multiple backups concurrently. This is a significant usability improvement, especially for multiple tenants or multiple users case, backups submitted from different users could run their backups simultaneously without interfering with each other.
Check design https://github.com/vmware-tanzu/velero/blob/main/design/concurrent-backup-processing.md for more details.
Check design https://github.com/vmware-tanzu/velero/blob/main/design/Implemented/concurrent-backup-processing.md for more details.
#### Cache volume for data movers
In v1.18, Velero allows users to configure cache volumes for data mover pods during restore for CSI snapshot data movement and fs-backup. This brings below benefits:
@@ -24,7 +24,7 @@ In v1.18, Velero allows users to configure cache volumes for data mover pods dur
- Solve the problem that multiple data mover pods fail to run concurrently in one node when the node's ephemeral disk is limited
- Working together with backup repository's cache limit configuration, cache volume with appropriate size helps to improve the restore throughput
Check design https://github.com/vmware-tanzu/velero/blob/main/design/backup-repo-cache-volume.md for more details.
Check design https://github.com/vmware-tanzu/velero/blob/main/design/Implemented/backup-repo-cache-volume.md for more details.
#### Incremental size for data movers
In v1.18, Velero allows users to observe the incremental size of data movers backups for CSI snapshot data movement and fs-backup, so that users could visually see the data reduction due to incremental backup.
+1
View File
@@ -0,0 +1 @@
Include InitContainer configured as Sidecars when validating the existence of the target containers configured for the Backup Hooks
+1
View File
@@ -0,0 +1 @@
Support all glob wildcard characters in namespace validation
+1
View File
@@ -0,0 +1 @@
Fix VolumePolicy PVC phase condition filter for unbound PVCs (#9507)
@@ -0,0 +1 @@
Fix VolumeGroupSnapshot restore failure with Ceph RBD CSI driver by creating stub VolumeGroupSnapshotContent during restore and looking up VolumeSnapshotClass by driver for credential support
+1
View File
@@ -0,0 +1 @@
Fix issue #9343, include PV topology to data mover pod affinities
+1
View File
@@ -0,0 +1 @@
Fix issue #9496, support customized host os
+1
View File
@@ -0,0 +1 @@
Add custom action type to volume policies
+1
View File
@@ -0,0 +1 @@
If BIA return updateObj with SkipFromBackupAnnotation, treat it as skip the resource from backup.
+1
View File
@@ -0,0 +1 @@
Issue #9544: Add test coverage for S3 bucket name in MRAP ARN notation and fix bucket validation to accept ARN format
+1
View File
@@ -0,0 +1 @@
Fix issue #9475, use node-selector instead of nodName for generic restore
+1
View File
@@ -0,0 +1 @@
Fix issue #9460, flush buffer before data mover completes
+1
View File
@@ -0,0 +1 @@
Add schedule_expected_interval_seconds metric for dynamic backup alerting thresholds (#9559)
+1
View File
@@ -0,0 +1 @@
Add ephemeral storage limit and request support for data mover and maintenance job
@@ -0,0 +1 @@
Fix DBR stuck when CSI snapshot no longer exists in cloud provider
+1
View File
@@ -0,0 +1 @@
Add check for file extraction from tarball.
+1
View File
@@ -0,0 +1 @@
Implement original VolumeSnapshotContent deletion for legacy backups
+1
View File
@@ -0,0 +1 @@
Fix issue #9626, let go for uninitialized repo under readonly mode
@@ -0,0 +1 @@
Fix issue #9636, fix configmap lookup in non-default namespaces
+1
View File
@@ -0,0 +1 @@
Fix issue #9641, Remove redundant ReadyToUse polling in CSI VolumeSnapshotContent delete plugin
+1
View File
@@ -0,0 +1 @@
Fix service restore with null healthCheckNodePort in last-applied-configuration label
+1
View File
@@ -0,0 +1 @@
Fix issue #9659, in the case that PVB/PVR/DU/DD is cancelled before the data path is really started, call EndEvent to prevent data mover pod from crashing because of delay event distribution
@@ -0,0 +1 @@
Fix issue #9666, fix node-agent node detection in multiple instances scenario
+1
View File
@@ -0,0 +1 @@
Fix issue #9470, remove restic from repository
+1
View File
@@ -0,0 +1 @@
Fix issue #9469, remove restic for uploader
@@ -0,0 +1 @@
Fix issue #9681, fix restores and podvolumerestores list options to only list in installed namespace
+1
View File
@@ -0,0 +1 @@
Fix issue #9428, increase repo maintenance history queue length from 3 to 25
@@ -0,0 +1 @@
Bump external-snapshotter to v8.4.0 and migrate VolumeGroupSnapshot API from v1beta1 to v1beta2 for Kubernetes 1.34+ compatibility
+1
View File
@@ -0,0 +1 @@
Update Debian base image from bookworm to trixie
@@ -0,0 +1 @@
Fix issue #9703, fix CSI PVC Backup Plugin list options to only list in installed namespace
@@ -69,9 +69,7 @@ spec:
- ""
type: string
resticIdentifier:
description: |-
ResticIdentifier is the full restic-compatible string for identifying
this repository. This field is only used when RepositoryType is "restic".
description: Deprecated
type: string
volumeNamespace:
description: |-
File diff suppressed because one or more lines are too long
+93
View File
@@ -0,0 +1,93 @@
# Add custom volume policy action
## Abstract
Currently, velero supports 3 different volume policy actions:
snapshot, fs-backup, and skip, which tell Velero how to handle backing
up PVC contents. Any other policy action is not allowed. This prevents
third party BackupItemAction (BIA) plugins which might want to perform
different actions on PVC via defined volume policies.
## Background
An external BIA plugin that wants to back up volumes via some custom
means (i.e. not CSI snapshots or fs-backup with kopia) is not able to
make use of the existing volume policy API. While the plugin could use
something like PVC annotations instead, this won't integrate with
existing volume policies, which is desirable in case the user wants to
specify some PVCs to use the custom plugin while leaving others using
CSI snapshots or fs-backup.
## Goals
- Add a fourth valid volume policy action "custom"
- Make use of the existing action parameters field to distinguish between multiple custom actions.
## Non Goals
- Implementing custom action logic in velero repo
## High-Level Design
A new VolumeActionType with the value "custom" will be added to
`internal/resourcepolicies`. When the action is "custom", velero will
not perform a snapshot or use fs-backup on the PVC. If there is no
registered plugin which implements the desired custom action, then it
will be equivalent to the "skip" action. Since there could be
different plugins that implement custom actions, when making the API
call (defined below) the plugin should also pass in a partial action
parameters map containing the portion of the map that identifies the
custom plugin as belonging to a particular external
implementation. For example, there might be a custom BIA that's
looking for a `custom` volume policy action with the parameter
`myCustomAction=true`. The volume policy action would be defined like
this:
```yaml
action:
type: custom
parameters:
myCustomAction: true
```
In `internal/volumehelper/volume_policy_helper.go` a new interface
method will be added, similar to `ShouldPerformSnapshot` but it takes
a partial parameter map as an additional input, since for custom
actions to match, the action type must be `custom`, but also there may
be some parameter that needs to match (to distinguish between
different custom actions). We also want a way for the plugin to get
the parameter map for the action. This should probably just return the
map rather than the Action struct is under `internal`.
```go
type VolumeHelper interface {
ShouldPerformSnapshot(obj runtime.Unstructured, groupResource schema.GroupResource) (bool, error)
ShouldPerformFSBackup(volume corev1api.Volume, pod corev1api.Pod) (bool, error)
ShouldPerformCustomAction(obj runtime.Unstructured, groupResource schema.GroupResource, map[string]any) (bool, error)
GetActionParameters(obj runtime.Unstructured, groupResource schema.GroupResource) (map[string]any, error)
}
```
In addition, since the VolumeHelper interface is expected to be called by external plugins, the interface (but not the implementation) should be moved from `internal/volumehelper` to `pkg/util/volumehelper`.
In `pkg/plugin/utils/volumehelper/volume_policy_helper.go`, a new helper func will be added which delegates to the internal volumehelper.NewVolumeHelperImplWithNamespaces
```go
func NewVolumeHelper(
volumePolicy *resourcepolicies.Policies,
snapshotVolumes *bool,
logger logrus.FieldLogger,
client crclient.Client,
defaultVolumesToFSBackup bool,
backupExcludePVC bool,
namespaces []string,
) (VolumeHelper, error) {
```
## Alternative Considered
An alternate approach was to create a new server arg to allow
user-defined parameters. That was rejected in favor of this approach,
as the explicitly-supported "custom" option integrates more easily
into a supportable plugin-callable API.
+22 -22
View File
@@ -26,7 +26,7 @@ require (
github.com/hashicorp/go-plugin v1.6.0
github.com/joho/godotenv v1.3.0
github.com/kopia/kopia v0.16.0
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0
github.com/onsi/ginkgo/v2 v2.22.0
github.com/onsi/gomega v1.36.1
github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9
@@ -42,10 +42,11 @@ require (
github.com/vmware-tanzu/crash-diagnostics v0.3.7
go.uber.org/zap v1.27.1
golang.org/x/mod v0.30.0
golang.org/x/oauth2 v0.33.0
golang.org/x/text v0.31.0
golang.org/x/oauth2 v0.34.0
golang.org/x/sys v0.40.0
golang.org/x/text v0.32.0
google.golang.org/api v0.256.0
google.golang.org/grpc v1.77.0
google.golang.org/grpc v1.79.3
google.golang.org/protobuf v1.36.10
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.3
@@ -63,7 +64,7 @@ require (
)
require (
cel.dev/expr v0.24.0 // indirect
cel.dev/expr v0.25.1 // indirect
cloud.google.com/go v0.121.6 // indirect
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
@@ -93,13 +94,13 @@ require (
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/chmduquesne/rollinghash v4.0.0+incompatible // indirect
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/edsrzf/mmap-go v1.2.0 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
@@ -168,29 +169,28 @@ require (
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.38.0 // indirect
go.opentelemetry.io/otel/metric v1.38.0 // indirect
go.opentelemetry.io/otel/sdk v1.38.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.38.0 // indirect
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.38.0 // indirect
golang.org/x/tools v0.39.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
+46 -46
View File
@@ -1,7 +1,7 @@
al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho=
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
cel.dev/expr v0.25.1 h1:1KrZg61W6TWSxuNZ37Xy49ps13NUovb66QLprthtwi4=
cel.dev/expr v0.25.1/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
@@ -189,8 +189,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0=
github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
@@ -227,15 +227,15 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM=
github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs=
github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo=
github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs=
github.com/envoyproxy/go-control-plane v0.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
github.com/envoyproxy/go-control-plane v0.14.0/go.mod h1:NcS5X47pLl/hfqxU70yPwL9ZMkUlwlKxtAohpi2wBEU=
github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI=
github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8=
github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU=
github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U=
github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
@@ -507,8 +507,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 h1:Q3jQ1NkFqv5o+F8dMmHd8SfEmlcwNeo1immFApntEwE=
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y=
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0 h1:bMqrb3UHgHbP+PW9VwiejfDJU1R0PpXVZNMdeH8WYKI=
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0=
@@ -742,24 +742,24 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs=
go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 h1:kWRNZMsfBHZ+uHjiH4y7Etn2FK26LAGkNFw7RHv1DhE=
go.opentelemetry.io/contrib/detectors/gcp v1.39.0/go.mod h1:t/OGqzHBa5v6RHZwrDBJ2OirWc+4q/w2fTbLZwAKjTk=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0 h1:rixTyDGXFxRy1xzhKrotaHy3/KXdPhlWARrCgK+eqUY=
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.36.0/go.mod h1:dowW6UsM9MKbJq5JTz2AMVp3/5iW5I/TStsk8S+CfHw=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o=
go.starlark.net v0.0.0-20201006213952-227f4aabceb5/go.mod h1:f0znQkUKRrkk36XxWbGjMqQM8wGv/xHBVE2qc3B5oFU=
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY=
@@ -790,8 +790,8 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -876,8 +876,8 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -891,8 +891,8 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -904,8 +904,8 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -969,14 +969,14 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20220526004731-065cf7ba2467/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -986,8 +986,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -1047,8 +1047,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -1134,10 +1134,10 @@ google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaE
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822 h1:rHWScKit0gvAPuOnu87KpaYtjK5zBMLcULh7gxkCXu4=
google.golang.org/genproto v0.0.0-20250603155806-513f23925822/go.mod h1:HubltRL7rMh0LfnQPkMH4NPDFEWp0jw3vixw7jEM53s=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8 h1:mepRgnBZa07I4TRuomDE4sTIYieg/osKmzIf4USdWS4=
google.golang.org/genproto/googleapis/api v0.0.0-20251022142026-3a174f9686a8/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -1159,8 +1159,8 @@ google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAG
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+6 -4
View File
@@ -12,7 +12,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
FROM --platform=$TARGETPLATFORM golang:1.25-bookworm
FROM --platform=$TARGETPLATFORM golang:1.25-trixie
ARG GOPROXY
@@ -21,9 +21,11 @@ ENV GO111MODULE=on
ENV GOPROXY=${GOPROXY}
# kubebuilder test bundle is separated from kubebuilder. Need to setup it for CI test.
RUN curl -sSLo envtest-bins.tar.gz https://go.kubebuilder.io/test-tools/1.22.1/linux/$(go env GOARCH) && \
mkdir /usr/local/kubebuilder && \
tar -C /usr/local/kubebuilder --strip-components=1 -zvxf envtest-bins.tar.gz
# Using setup-envtest to download envtest binaries
RUN go install sigs.k8s.io/controller-runtime/tools/setup-envtest@v0.0.0-20260305094418-8122a6266696 && \
mkdir -p /usr/local/kubebuilder/bin && \
ENVTEST_ASSETS_DIR=$(setup-envtest use 1.33.0 --bin-dir /usr/local/kubebuilder/bin -p path) && \
cp -r ${ENVTEST_ASSETS_DIR}/* /usr/local/kubebuilder/bin/
RUN wget --quiet https://github.com/kubernetes-sigs/kubebuilder/releases/download/v3.2.0/kubebuilder_linux_$(go env GOARCH) && \
mv kubebuilder_linux_$(go env GOARCH) /usr/local/kubebuilder/bin/kubebuilder && \
@@ -18,7 +18,6 @@ package csi
import (
"context"
"time"
"github.com/google/uuid"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
@@ -27,14 +26,11 @@ import (
corev1api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/client"
plugincommon "github.com/vmware-tanzu/velero/pkg/plugin/framework/common"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
)
@@ -71,7 +67,7 @@ func (p *volumeSnapshotContentDeleteItemAction) Execute(
// So skip deleting VolumeSnapshotContent not have the backup name
// in its labels.
if !kubeutil.HasBackupLabel(&snapCont.ObjectMeta, input.Backup.Name) {
p.log.Info(
p.log.Infof(
"VolumeSnapshotContent %s was not taken by backup %s, skipping deletion",
snapCont.Name,
input.Backup.Name,
@@ -81,6 +77,17 @@ func (p *volumeSnapshotContentDeleteItemAction) Execute(
p.log.Infof("Deleting VolumeSnapshotContent %s", snapCont.Name)
// Try to delete the original VSC from the cluster first.
// This handles legacy (pre-1.15) backups where the original VSC
// with DeletionPolicy=Retain still exists in the cluster.
originalVSCName := snapCont.Name
if cleaned := p.tryDeleteOriginalVSC(context.TODO(), originalVSCName); cleaned {
p.log.Infof("Successfully deleted original VolumeSnapshotContent %s from cluster, skipping temp VSC creation", originalVSCName)
return nil
}
// create a temp VSC to trigger cloud snapshot deletion
// (for backups where the original VSC no longer exists in cluster)
uuid, err := uuid.NewRandom()
if err != nil {
p.log.WithError(err).Errorf("Fail to generate the UUID to create VSC %s", snapCont.Name)
@@ -114,60 +121,62 @@ func (p *volumeSnapshotContentDeleteItemAction) Execute(
if err := p.crClient.Create(context.TODO(), &snapCont); err != nil {
return errors.Wrapf(err, "fail to create VolumeSnapshotContent %s", snapCont.Name)
}
p.log.Infof("Created temp VolumeSnapshotContent %s with DeletionPolicy=Delete to trigger cloud snapshot cleanup", snapCont.Name)
// Read resource timeout from backup annotation, if not set, use default value.
timeout, err := time.ParseDuration(
input.Backup.Annotations[velerov1api.ResourceTimeoutAnnotation])
if err != nil {
p.log.Warnf("fail to parse resource timeout annotation %s: %s",
input.Backup.Annotations[velerov1api.ResourceTimeoutAnnotation], err.Error())
timeout = 10 * time.Minute
}
p.log.Debugf("resource timeout is set to %s", timeout.String())
interval := 5 * time.Second
// Wait until VSC created and ReadyToUse is true.
if err := wait.PollUntilContextTimeout(
context.Background(),
interval,
timeout,
true,
func(ctx context.Context) (bool, error) {
return checkVSCReadiness(ctx, &snapCont, p.crClient)
},
); err != nil {
return errors.Wrapf(err, "fail to wait VolumeSnapshotContent %s becomes ready.", snapCont.Name)
}
// Delete the temp VSC immediately to trigger cloud snapshot removal.
// The CSI driver will handle the actual cloud snapshot deletion.
if err := p.crClient.Delete(
context.TODO(),
&snapCont,
); err != nil && !apierrors.IsNotFound(err) {
p.log.Infof("VolumeSnapshotContent %s not found", snapCont.Name)
p.log.WithError(err).Errorf("Failed to delete temp VolumeSnapshotContent %s", snapCont.Name)
return err
}
p.log.Infof("Successfully triggered deletion of VolumeSnapshotContent %s and its cloud snapshot", snapCont.Name)
return nil
}
var checkVSCReadiness = func(
// tryDeleteOriginalVSC attempts to find and delete the original VSC from
// the cluster (legacy pre-1.15 backups). It patches the DeletionPolicy to
// Delete so the CSI driver also removes the cloud snapshot, then deletes
// the VSC object itself.
// Returns true if the original VSC was found and deletion was initiated.
func (p *volumeSnapshotContentDeleteItemAction) tryDeleteOriginalVSC(
ctx context.Context,
vsc *snapshotv1api.VolumeSnapshotContent,
client crclient.Client,
) (bool, error) {
tmpVSC := new(snapshotv1api.VolumeSnapshotContent)
if err := client.Get(ctx, crclient.ObjectKeyFromObject(vsc), tmpVSC); err != nil {
return false, errors.Wrapf(
err, "failed to get VolumeSnapshotContent %s", vsc.Name,
)
vscName string,
) bool {
existing := new(snapshotv1api.VolumeSnapshotContent)
if err := p.crClient.Get(ctx, crclient.ObjectKey{Name: vscName}, existing); err != nil {
if apierrors.IsNotFound(err) {
p.log.Debugf("Original VolumeSnapshotContent %s not found in cluster, will use temp VSC flow", vscName)
} else {
p.log.WithError(err).Warnf("Error looking up original VolumeSnapshotContent %s, will use temp VSC flow", vscName)
}
return false
}
if tmpVSC.Status != nil && boolptr.IsSetToTrue(tmpVSC.Status.ReadyToUse) {
return true, nil
p.log.Debugf("Found original VolumeSnapshotContent %s in cluster (legacy backup), cleaning up directly", vscName)
// Patch DeletionPolicy to Delete so the CSI driver removes the cloud snapshot
if existing.Spec.DeletionPolicy != snapshotv1api.VolumeSnapshotContentDelete {
original := existing.DeepCopy()
existing.Spec.DeletionPolicy = snapshotv1api.VolumeSnapshotContentDelete
if err := p.crClient.Patch(ctx, existing, crclient.MergeFrom(original)); err != nil {
p.log.WithError(err).Warnf("Failed to patch DeletionPolicy on original VSC %s, will use temp VSC flow", vscName)
return false
}
p.log.Debugf("Patched DeletionPolicy to Delete on original VolumeSnapshotContent %s", vscName)
}
return false, nil
// Delete the original VSC — the CSI driver will clean up the cloud snapshot
if err := p.crClient.Delete(ctx, existing); err != nil && !apierrors.IsNotFound(err) {
p.log.WithError(err).Warnf("Failed to delete original VolumeSnapshotContent %s, will use temp VSC flow", vscName)
return false
}
p.log.Infof("Deleted original VolumeSnapshotContent %s with DeletionPolicy=Delete, CSI driver will remove cloud snapshot", vscName)
return true
}
func NewVolumeSnapshotContentDeleteItemAction(
@@ -22,9 +22,10 @@ import (
"testing"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@@ -37,19 +38,44 @@ import (
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
// fakeClientWithErrors wraps a real client and injects errors for specific operations.
type fakeClientWithErrors struct {
crclient.Client
getError error
patchError error
deleteError error
}
func (c *fakeClientWithErrors) Get(ctx context.Context, key crclient.ObjectKey, obj crclient.Object, opts ...crclient.GetOption) error {
if c.getError != nil {
return c.getError
}
return c.Client.Get(ctx, key, obj, opts...)
}
func (c *fakeClientWithErrors) Patch(ctx context.Context, obj crclient.Object, patch crclient.Patch, opts ...crclient.PatchOption) error {
if c.patchError != nil {
return c.patchError
}
return c.Client.Patch(ctx, obj, patch, opts...)
}
func (c *fakeClientWithErrors) Delete(ctx context.Context, obj crclient.Object, opts ...crclient.DeleteOption) error {
if c.deleteError != nil {
return c.deleteError
}
return c.Client.Delete(ctx, obj, opts...)
}
func TestVSCExecute(t *testing.T) {
snapshotHandleStr := "test"
tests := []struct {
name string
item runtime.Unstructured
vsc *snapshotv1api.VolumeSnapshotContent
backup *velerov1api.Backup
function func(
ctx context.Context,
vsc *snapshotv1api.VolumeSnapshotContent,
client crclient.Client,
) (bool, error)
expectErr bool
name string
item runtime.Unstructured
vsc *snapshotv1api.VolumeSnapshotContent
backup *velerov1api.Backup
preExistingVSC *snapshotv1api.VolumeSnapshotContent
expectErr bool
}{
{
name: "VolumeSnapshotContent doesn't have backup label",
@@ -71,27 +97,22 @@ func TestVSCExecute(t *testing.T) {
{
name: "Normal case, VolumeSnapshot should be deleted",
vsc: builder.ForVolumeSnapshotContent("bar").ObjectMeta(builder.WithLabelsMap(map[string]string{velerov1api.BackupNameLabel: "backup"})).VolumeSnapshotClassName("volumesnapshotclass").Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: &snapshotHandleStr}).Result(),
backup: builder.ForBackup("velero", "backup").ObjectMeta(builder.WithAnnotationsMap(map[string]string{velerov1api.ResourceTimeoutAnnotation: "5s"})).Result(),
backup: builder.ForBackup("velero", "backup").Result(),
expectErr: false,
function: func(
ctx context.Context,
vsc *snapshotv1api.VolumeSnapshotContent,
client crclient.Client,
) (bool, error) {
return true, nil
},
},
{
name: "Error case, deletion fails",
name: "Original VSC exists in cluster, cleaned up directly",
vsc: builder.ForVolumeSnapshotContent("bar").ObjectMeta(builder.WithLabelsMap(map[string]string{velerov1api.BackupNameLabel: "backup"})).Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: &snapshotHandleStr}).Result(),
backup: builder.ForBackup("velero", "backup").ObjectMeta(builder.WithAnnotationsMap(map[string]string{velerov1api.ResourceTimeoutAnnotation: "5s"})).Result(),
expectErr: true,
function: func(
ctx context.Context,
vsc *snapshotv1api.VolumeSnapshotContent,
client crclient.Client,
) (bool, error) {
return false, errors.Errorf("test error case")
backup: builder.ForBackup("velero", "backup").Result(),
expectErr: false,
preExistingVSC: &snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{Name: "bar"},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain,
Driver: "disk.csi.azure.com",
Source: snapshotv1api.VolumeSnapshotContentSource{SnapshotHandle: stringPtr("snap-123")},
VolumeSnapshotRef: corev1api.ObjectReference{Name: "vs-1", Namespace: "default"},
},
},
},
}
@@ -100,7 +121,10 @@ func TestVSCExecute(t *testing.T) {
t.Run(test.name, func(t *testing.T) {
crClient := velerotest.NewFakeControllerRuntimeClient(t)
logger := logrus.StandardLogger()
checkVSCReadiness = test.function
if test.preExistingVSC != nil {
require.NoError(t, crClient.Create(t.Context(), test.preExistingVSC))
}
p := volumeSnapshotContentDeleteItemAction{log: logger, crClient: crClient}
@@ -158,52 +182,153 @@ func TestNewVolumeSnapshotContentDeleteItemAction(t *testing.T) {
require.NoError(t, err1)
}
func TestCheckVSCReadiness(t *testing.T) {
func TestTryDeleteOriginalVSC(t *testing.T) {
tests := []struct {
name string
vsc *snapshotv1api.VolumeSnapshotContent
createVSC bool
expectErr bool
ready bool
vscName string
existing *snapshotv1api.VolumeSnapshotContent
createIt bool
expectRet bool
}{
{
name: "VSC not exist",
vsc: &snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{
Name: "vsc-1",
Namespace: "velero",
},
},
createVSC: false,
expectErr: true,
ready: false,
name: "VSC not found in cluster, returns false",
vscName: "not-found",
expectRet: false,
},
{
name: "VSC not ready",
vsc: &snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{
Name: "vsc-1",
Namespace: "velero",
name: "VSC found with Retain policy, patches and deletes",
vscName: "legacy-vsc",
existing: &snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{Name: "legacy-vsc"},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain,
Driver: "disk.csi.azure.com",
Source: snapshotv1api.VolumeSnapshotContentSource{
SnapshotHandle: stringPtr("snap-123"),
},
VolumeSnapshotRef: corev1api.ObjectReference{
Name: "vs-1",
Namespace: "default",
},
},
},
createVSC: true,
expectErr: false,
ready: false,
createIt: true,
expectRet: true,
},
{
name: "VSC found with Delete policy already, just deletes",
vscName: "already-delete-vsc",
existing: &snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{Name: "already-delete-vsc"},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
DeletionPolicy: snapshotv1api.VolumeSnapshotContentDelete,
Driver: "disk.csi.azure.com",
Source: snapshotv1api.VolumeSnapshotContentSource{
SnapshotHandle: stringPtr("snap-456"),
},
VolumeSnapshotRef: corev1api.ObjectReference{
Name: "vs-2",
Namespace: "default",
},
},
},
createIt: true,
expectRet: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
crClient := velerotest.NewFakeControllerRuntimeClient(t)
if test.createVSC {
require.NoError(t, crClient.Create(t.Context(), test.vsc))
logger := logrus.StandardLogger()
p := &volumeSnapshotContentDeleteItemAction{
log: logger,
crClient: crClient,
}
ready, err := checkVSCReadiness(t.Context(), test.vsc, crClient)
require.Equal(t, test.ready, ready)
if test.expectErr {
require.Error(t, err)
if test.createIt && test.existing != nil {
require.NoError(t, crClient.Create(t.Context(), test.existing))
}
result := p.tryDeleteOriginalVSC(t.Context(), test.vscName)
require.Equal(t, test.expectRet, result)
// If cleanup succeeded, verify the VSC is gone
if test.expectRet {
err := crClient.Get(t.Context(), crclient.ObjectKey{Name: test.vscName},
&snapshotv1api.VolumeSnapshotContent{})
require.True(t, apierrors.IsNotFound(err),
"VSC should have been deleted from cluster")
}
})
}
// Error injection tests for tryDeleteOriginalVSC
t.Run("Get returns non-NotFound error, returns false", func(t *testing.T) {
errClient := &fakeClientWithErrors{
Client: velerotest.NewFakeControllerRuntimeClient(t),
getError: fmt.Errorf("connection refused"),
}
p := &volumeSnapshotContentDeleteItemAction{
log: logrus.StandardLogger(),
crClient: errClient,
}
require.False(t, p.tryDeleteOriginalVSC(t.Context(), "some-vsc"))
})
t.Run("Patch fails, returns false", func(t *testing.T) {
realClient := velerotest.NewFakeControllerRuntimeClient(t)
vsc := &snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{Name: "patch-fail-vsc"},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain,
Driver: "disk.csi.azure.com",
Source: snapshotv1api.VolumeSnapshotContentSource{SnapshotHandle: stringPtr("snap-789")},
VolumeSnapshotRef: corev1api.ObjectReference{Name: "vs-3", Namespace: "default"},
},
}
require.NoError(t, realClient.Create(t.Context(), vsc))
errClient := &fakeClientWithErrors{
Client: realClient,
patchError: fmt.Errorf("patch forbidden"),
}
p := &volumeSnapshotContentDeleteItemAction{
log: logrus.StandardLogger(),
crClient: errClient,
}
require.False(t, p.tryDeleteOriginalVSC(t.Context(), "patch-fail-vsc"))
})
t.Run("Delete fails, returns false", func(t *testing.T) {
realClient := velerotest.NewFakeControllerRuntimeClient(t)
vsc := &snapshotv1api.VolumeSnapshotContent{
ObjectMeta: metav1.ObjectMeta{Name: "delete-fail-vsc"},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
DeletionPolicy: snapshotv1api.VolumeSnapshotContentDelete,
Driver: "disk.csi.azure.com",
Source: snapshotv1api.VolumeSnapshotContentSource{SnapshotHandle: stringPtr("snap-999")},
VolumeSnapshotRef: corev1api.ObjectReference{Name: "vs-4", Namespace: "default"},
},
}
require.NoError(t, realClient.Create(t.Context(), vsc))
errClient := &fakeClientWithErrors{
Client: realClient,
deleteError: fmt.Errorf("delete forbidden"),
}
p := &volumeSnapshotContentDeleteItemAction{
log: logrus.StandardLogger(),
crClient: errClient,
}
require.False(t, p.tryDeleteOriginalVSC(t.Context(), "delete-fail-vsc"))
})
}
func boolPtr(b bool) *bool {
return &b
}
func stringPtr(s string) *string {
return &s
}
@@ -42,6 +42,8 @@ const (
FSBackup VolumeActionType = "fs-backup"
// snapshot action can have 3 different meaning based on velero configuration and backup spec - cloud provider based snapshots, local csi snapshots and datamover snapshots
Snapshot VolumeActionType = "snapshot"
// custom action is used to identify a volume that will be handled by an external plugin. Velero will not snapshot or use fs-backup if action=="custom"
Custom VolumeActionType = "custom"
)
// Action defined as one action for a specific way of backup
@@ -90,7 +90,7 @@ func decodeStruct(r io.Reader, s any) error {
func (a *Action) validate() error {
// validate Type
valid := false
if a.Type == Skip || a.Type == Snapshot || a.Type == FSBackup {
if a.Type == Skip || a.Type == Snapshot || a.Type == FSBackup || a.Type == Custom {
valid = true
}
if !valid {
+15 -6
View File
@@ -146,6 +146,10 @@ type CSISnapshotInfo struct {
// The VolumeSnapshot's Status.ReadyToUse value
ReadyToUse *bool
// The VolumeGroupSnapshotHandle from VSC status, used to create stub VGSC during restore
// for CSI drivers that populate this field (e.g., Ceph RBD).
VolumeGroupSnapshotHandle string `json:"volumeGroupSnapshotHandle,omitempty"`
}
// SnapshotDataMovementInfo is used for displaying the snapshot data mover status.
@@ -456,6 +460,10 @@ func (v *BackupVolumesInformation) generateVolumeInfoForCSIVolumeSnapshot() {
if volumeSnapshotContent.Status.SnapshotHandle != nil {
snapshotHandle = *volumeSnapshotContent.Status.SnapshotHandle
}
volumeGroupSnapshotHandle := ""
if volumeSnapshotContent.Status != nil && volumeSnapshotContent.Status.VolumeGroupSnapshotHandle != nil {
volumeGroupSnapshotHandle = *volumeSnapshotContent.Status.VolumeGroupSnapshotHandle
}
if pvcPVInfo := v.pvMap.retrieve("", *volumeSnapshot.Spec.Source.PersistentVolumeClaimName, volumeSnapshot.Namespace); pvcPVInfo != nil {
volumeInfo := &BackupVolumeInfo{
BackupMethod: CSISnapshot,
@@ -466,12 +474,13 @@ func (v *BackupVolumesInformation) generateVolumeInfoForCSIVolumeSnapshot() {
SnapshotDataMoved: false,
PreserveLocalSnapshot: true,
CSISnapshotInfo: &CSISnapshotInfo{
VSCName: *volumeSnapshot.Status.BoundVolumeSnapshotContentName,
Size: size,
Driver: volumeSnapshotContent.Spec.Driver,
SnapshotHandle: snapshotHandle,
OperationID: operation.Spec.OperationID,
ReadyToUse: volumeSnapshot.Status.ReadyToUse,
VSCName: *volumeSnapshot.Status.BoundVolumeSnapshotContentName,
Size: size,
Driver: volumeSnapshotContent.Spec.Driver,
SnapshotHandle: snapshotHandle,
OperationID: operation.Spec.OperationID,
ReadyToUse: volumeSnapshot.Status.ReadyToUse,
VolumeGroupSnapshotHandle: volumeGroupSnapshotHandle,
},
PVInfo: &PVInfo{
ReclaimPolicy: string(pvcPVInfo.PV.Spec.PersistentVolumeReclaimPolicy),
+147 -17
View File
@@ -18,13 +18,9 @@ import (
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
podvolumeutil "github.com/vmware-tanzu/velero/pkg/util/podvolume"
vhutil "github.com/vmware-tanzu/velero/pkg/util/volumehelper"
)
type VolumeHelper interface {
ShouldPerformSnapshot(obj runtime.Unstructured, groupResource schema.GroupResource) (bool, error)
ShouldPerformFSBackup(volume corev1api.Volume, pod corev1api.Pod) (bool, error)
}
type volumeHelperImpl struct {
volumePolicy *resourcepolicies.Policies
snapshotVolumes *bool
@@ -53,7 +49,7 @@ func NewVolumeHelperImpl(
client crclient.Client,
defaultVolumesToFSBackup bool,
backupExcludePVC bool,
) VolumeHelper {
) vhutil.VolumeHelper {
// Pass nil namespaces - no cache will be built, so this never fails.
// This is used by plugins that don't need the cache optimization.
vh, _ := NewVolumeHelperImplWithNamespaces(
@@ -81,7 +77,7 @@ func NewVolumeHelperImplWithNamespaces(
defaultVolumesToFSBackup bool,
backupExcludePVC bool,
namespaces []string,
) (VolumeHelper, error) {
) (vhutil.VolumeHelper, error) {
var pvcPodCache *podvolumeutil.PVCPodCache
if len(namespaces) > 0 {
pvcPodCache = podvolumeutil.NewPVCPodCache()
@@ -110,7 +106,7 @@ func NewVolumeHelperImplWithCache(
client crclient.Client,
logger logrus.FieldLogger,
pvcPodCache *podvolumeutil.PVCPodCache,
) (VolumeHelper, error) {
) (vhutil.VolumeHelper, error) {
resourcePolicies, err := resourcepolicies.GetResourcePoliciesFromBackup(backup, client, logger)
if err != nil {
return nil, errors.Wrap(err, "failed to get volume policies from backup")
@@ -134,6 +130,7 @@ func (v *volumeHelperImpl) ShouldPerformSnapshot(obj runtime.Unstructured, group
pv := new(corev1api.PersistentVolume)
var err error
var pvNotFoundErr error
if groupResource == kuberesource.PersistentVolumeClaims {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pvc); err != nil {
v.logger.WithError(err).Error("fail to convert unstructured into PVC")
@@ -142,8 +139,10 @@ func (v *volumeHelperImpl) ShouldPerformSnapshot(obj runtime.Unstructured, group
pv, err = kubeutil.GetPVForPVC(pvc, v.client)
if err != nil {
v.logger.WithError(err).Errorf("fail to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
return false, err
// Any error means PV not available - save to return later if no policy matches
v.logger.Debugf("PV not found for PVC %s: %v", pvc.Namespace+"/"+pvc.Name, err)
pvNotFoundErr = err
pv = nil
}
}
@@ -158,7 +157,7 @@ func (v *volumeHelperImpl) ShouldPerformSnapshot(obj runtime.Unstructured, group
vfd := resourcepolicies.NewVolumeFilterData(pv, nil, pvc)
action, err := v.volumePolicy.GetMatchAction(vfd)
if err != nil {
v.logger.WithError(err).Errorf("fail to get VolumePolicy match action for PV %s", pv.Name)
v.logger.WithError(err).Errorf("fail to get VolumePolicy match action for %+v", vfd)
return false, err
}
@@ -167,15 +166,21 @@ func (v *volumeHelperImpl) ShouldPerformSnapshot(obj runtime.Unstructured, group
// If there is no match action, go on to the next check.
if action != nil {
if action.Type == resourcepolicies.Snapshot {
v.logger.Infof(fmt.Sprintf("performing snapshot action for pv %s", pv.Name))
v.logger.Infof("performing snapshot action for %+v", vfd)
return true, nil
} else {
v.logger.Infof("Skip snapshot action for pv %s as the action type is %s", pv.Name, action.Type)
v.logger.Infof("Skip snapshot action for %+v as the action type is %s", vfd, action.Type)
return false, nil
}
}
}
// If resource is PVC, and PV is nil (e.g., Pending/Lost PVC with no matching policy), return the original error
if groupResource == kuberesource.PersistentVolumeClaims && pv == nil && pvNotFoundErr != nil {
v.logger.WithError(pvNotFoundErr).Errorf("fail to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
return false, pvNotFoundErr
}
// If this PV is claimed, see if we've already taken a (pod volume backup)
// snapshot of the contents of this PV. If so, don't take a snapshot.
if pv.Spec.ClaimRef != nil {
@@ -209,7 +214,7 @@ func (v *volumeHelperImpl) ShouldPerformSnapshot(obj runtime.Unstructured, group
return true, nil
}
v.logger.Infof(fmt.Sprintf("skipping snapshot action for pv %s possibly due to no volume policy setting or snapshotVolumes is false", pv.Name))
v.logger.Infof("skipping snapshot action for pv %s possibly due to no volume policy setting or snapshotVolumes is false", pv.Name)
return false, nil
}
@@ -219,6 +224,7 @@ func (v volumeHelperImpl) ShouldPerformFSBackup(volume corev1api.Volume, pod cor
return false, nil
}
var pvNotFoundErr error
if v.volumePolicy != nil {
var resource any
var err error
@@ -230,10 +236,13 @@ func (v volumeHelperImpl) ShouldPerformFSBackup(volume corev1api.Volume, pod cor
v.logger.WithError(err).Errorf("fail to get PVC for pod %s", pod.Namespace+"/"+pod.Name)
return false, err
}
resource, err = kubeutil.GetPVForPVC(pvc, v.client)
pvResource, err := kubeutil.GetPVForPVC(pvc, v.client)
if err != nil {
v.logger.WithError(err).Errorf("fail to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
return false, err
// Any error means PV not available - save to return later if no policy matches
v.logger.Debugf("PV not found for PVC %s: %v", pvc.Namespace+"/"+pvc.Name, err)
pvNotFoundErr = err
} else {
resource = pvResource
}
}
@@ -260,6 +269,12 @@ func (v volumeHelperImpl) ShouldPerformFSBackup(volume corev1api.Volume, pod cor
return false, nil
}
}
// If no policy matched and PV was not found, return the original error
if pvNotFoundErr != nil {
v.logger.WithError(pvNotFoundErr).Errorf("fail to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
return false, pvNotFoundErr
}
}
if v.shouldPerformFSBackupLegacy(volume, pod) {
@@ -300,6 +315,121 @@ func (v volumeHelperImpl) shouldPerformFSBackupLegacy(
}
}
func (v *volumeHelperImpl) ShouldPerformCustomAction(obj runtime.Unstructured, groupResource schema.GroupResource, matchParams map[string]any) (bool, error) {
// check if volume policy exists and also check if the object(pv/pvc) fits a volume policy criteria and see if the associated action is custom with the provided param values
pvc := new(corev1api.PersistentVolumeClaim)
pv := new(corev1api.PersistentVolume)
var err error
var pvNotFoundErr error
if groupResource == kuberesource.PersistentVolumeClaims {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pvc); err != nil {
v.logger.WithError(err).Error("fail to convert unstructured into PVC")
return false, err
}
pv, err = kubeutil.GetPVForPVC(pvc, v.client)
if err != nil {
// Any error means PV not available - save to return later if no policy matches
v.logger.Debugf("PV not found for PVC %s: %v", pvc.Namespace+"/"+pvc.Name, err)
pvNotFoundErr = err
pv = nil
}
}
if groupResource == kuberesource.PersistentVolumes {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pv); err != nil {
v.logger.WithError(err).Error("fail to convert unstructured into PV")
return false, err
}
}
if v.volumePolicy != nil {
vfd := resourcepolicies.NewVolumeFilterData(pv, nil, pvc)
action, err := v.volumePolicy.GetMatchAction(vfd)
if err != nil {
v.logger.WithError(err).Errorf("fail to get VolumePolicy match action for %+v", vfd)
return false, err
}
// If there is a match action, and the action type is custom, return true
// if the provided parameters match as well, else return false.
// If there is no match action, also return false
if action != nil {
if action.Type == resourcepolicies.Custom {
for k, requiredValue := range matchParams {
if actionValue, ok := action.Parameters[k]; !ok || actionValue != requiredValue {
v.logger.Infof("Skipping custom action for %+v as value for parameter %s is %s rather than the required %s", vfd, k, actionValue, requiredValue)
return false, nil
}
}
v.logger.Infof("performing custom action for %+v", vfd)
return true, nil
} else {
v.logger.Infof("Skipping custom action for %+v as the action type is %s", vfd, action.Type)
return false, nil
}
}
}
// If resource is PVC, and PV is nil (e.g., Pending/Lost PVC with no matching policy), return the original error
// Don't error out on no PV, just return false
if groupResource == kuberesource.PersistentVolumeClaims && pv == nil && pvNotFoundErr != nil {
v.logger.WithError(pvNotFoundErr).Warnf("fail to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
return false, nil
}
v.logger.Infof("skipping custom action for pv %s due to no matching volume policy", pv.Name)
return false, nil
}
// returns false if no matching action found. Returns true with the action name and Parameters map if there is a matching policy
func (v *volumeHelperImpl) GetActionParameters(obj runtime.Unstructured, groupResource schema.GroupResource) (bool, string, map[string]any, error) {
// if volume policy exists, return action parameters.
pvc := new(corev1api.PersistentVolumeClaim)
pv := new(corev1api.PersistentVolume)
var err error
if groupResource == kuberesource.PersistentVolumeClaims {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pvc); err != nil {
v.logger.WithError(err).Error("fail to convert unstructured into PVC")
return false, "", nil, err
}
pv, err = kubeutil.GetPVForPVC(pvc, v.client)
if err != nil {
v.logger.WithError(err).Warnf("failed to get PV for PVC %s", pvc.Namespace+"/"+pvc.Name)
return false, "", nil, nil
}
}
if groupResource == kuberesource.PersistentVolumes {
if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), &pv); err != nil {
v.logger.WithError(err).Error("fail to convert unstructured into PV")
return false, "", nil, err
}
}
if v.volumePolicy != nil {
vfd := resourcepolicies.NewVolumeFilterData(pv, nil, pvc)
action, err := v.volumePolicy.GetMatchAction(vfd)
if err != nil {
v.logger.WithError(err).Errorf("fail to get VolumePolicy match action for PV %s", pv.Name)
return false, "", nil, err
}
// If there is a match action, and the action type is custom, return true
// if the provided parameters match as well, else return false.
// If there is no match action, also return false
if action != nil {
v.logger.Infof("found matching action for pv %s, returning parameters", pv.Name)
return true, string(action.Type), action.Parameters, nil
}
}
v.logger.Infof("no matching volume policy found for pv %s, no parameters to return", pv.Name)
return false, "", nil, nil
}
func (v *volumeHelperImpl) shouldIncludeVolumeInBackup(vol corev1api.Volume) bool {
includeVolumeInBackup := true
// cannot backup hostpath volumes as they are not mounted into /var/lib/kubelet/pods
@@ -286,7 +286,7 @@ func TestVolumeHelperImpl_ShouldPerformSnapshot(t *testing.T) {
expectedErr: false,
},
{
name: "PVC not having PV, return false and error case PV not found",
name: "PVC not having PV, return false and error when no matching policy",
inputObj: builder.ForPersistentVolumeClaim("default", "example-pvc").StorageClass("gp2-csi").Result(),
groupResource: kuberesource.PersistentVolumeClaims,
resourcePolicies: &resourcepolicies.ResourcePolicies{
@@ -1234,3 +1234,312 @@ func TestNewVolumeHelperImplWithCache_UsesCache(t *testing.T) {
require.NoError(t, err)
require.False(t, shouldSnapshot, "Expected snapshot to be skipped due to fs-backup selection via cache")
}
// TestVolumeHelperImpl_ShouldPerformSnapshot_UnboundPVC tests that Pending and Lost PVCs with
// phase-based skip policies don't cause errors when GetPVForPVC would fail.
func TestVolumeHelperImpl_ShouldPerformSnapshot_UnboundPVC(t *testing.T) {
testCases := []struct {
name string
inputPVC *corev1api.PersistentVolumeClaim
resourcePolicies *resourcepolicies.ResourcePolicies
shouldSnapshot bool
expectedErr bool
}{
{
name: "Pending PVC with phase-based skip policy should not error and return false",
inputPVC: builder.ForPersistentVolumeClaim("ns", "pvc-pending").
StorageClass("non-existent-class").
Phase(corev1api.ClaimPending).
Result(),
resourcePolicies: &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"pvcPhase": []string{"Pending"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
},
shouldSnapshot: false,
expectedErr: false,
},
{
name: "Pending PVC without matching skip policy should error (no PV)",
inputPVC: builder.ForPersistentVolumeClaim("ns", "pvc-pending-no-policy").
StorageClass("non-existent-class").
Phase(corev1api.ClaimPending).
Result(),
resourcePolicies: &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"storageClass": []string{"gp2-csi"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
},
shouldSnapshot: false,
expectedErr: true,
},
{
name: "Lost PVC with phase-based skip policy should not error and return false",
inputPVC: builder.ForPersistentVolumeClaim("ns", "pvc-lost").
StorageClass("some-class").
Phase(corev1api.ClaimLost).
Result(),
resourcePolicies: &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"pvcPhase": []string{"Lost"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
},
shouldSnapshot: false,
expectedErr: false,
},
{
name: "Lost PVC with policy for Pending and Lost should not error and return false",
inputPVC: builder.ForPersistentVolumeClaim("ns", "pvc-lost").
StorageClass("some-class").
Phase(corev1api.ClaimLost).
Result(),
resourcePolicies: &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"pvcPhase": []string{"Pending", "Lost"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
},
shouldSnapshot: false,
expectedErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fakeClient := velerotest.NewFakeControllerRuntimeClient(t)
var p *resourcepolicies.Policies
if tc.resourcePolicies != nil {
p = &resourcepolicies.Policies{}
err := p.BuildPolicy(tc.resourcePolicies)
require.NoError(t, err)
}
vh := NewVolumeHelperImpl(
p,
ptr.To(true),
logrus.StandardLogger(),
fakeClient,
false,
false,
)
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.inputPVC)
require.NoError(t, err)
actualShouldSnapshot, actualError := vh.ShouldPerformSnapshot(&unstructured.Unstructured{Object: obj}, kuberesource.PersistentVolumeClaims)
if tc.expectedErr {
require.Error(t, actualError, "Want error; Got nil error")
return
}
require.NoError(t, actualError)
require.Equalf(t, tc.shouldSnapshot, actualShouldSnapshot, "Want shouldSnapshot as %t; Got shouldSnapshot as %t", tc.shouldSnapshot, actualShouldSnapshot)
})
}
}
// TestVolumeHelperImpl_ShouldPerformFSBackup_UnboundPVC tests that Pending and Lost PVCs with
// phase-based skip policies don't cause errors when GetPVForPVC would fail.
func TestVolumeHelperImpl_ShouldPerformFSBackup_UnboundPVC(t *testing.T) {
testCases := []struct {
name string
pod *corev1api.Pod
pvc *corev1api.PersistentVolumeClaim
resourcePolicies *resourcepolicies.ResourcePolicies
shouldFSBackup bool
expectedErr bool
}{
{
name: "Pending PVC with phase-based skip policy should not error and return false",
pod: builder.ForPod("ns", "pod-1").
Volumes(
&corev1api.Volume{
Name: "vol-pending",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc-pending",
},
},
}).Result(),
pvc: builder.ForPersistentVolumeClaim("ns", "pvc-pending").
StorageClass("non-existent-class").
Phase(corev1api.ClaimPending).
Result(),
resourcePolicies: &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"pvcPhase": []string{"Pending"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
},
shouldFSBackup: false,
expectedErr: false,
},
{
name: "Pending PVC without matching skip policy should error (no PV)",
pod: builder.ForPod("ns", "pod-1").
Volumes(
&corev1api.Volume{
Name: "vol-pending",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc-pending-no-policy",
},
},
}).Result(),
pvc: builder.ForPersistentVolumeClaim("ns", "pvc-pending-no-policy").
StorageClass("non-existent-class").
Phase(corev1api.ClaimPending).
Result(),
resourcePolicies: &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"storageClass": []string{"gp2-csi"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
},
shouldFSBackup: false,
expectedErr: true,
},
{
name: "Lost PVC with phase-based skip policy should not error and return false",
pod: builder.ForPod("ns", "pod-1").
Volumes(
&corev1api.Volume{
Name: "vol-lost",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc-lost",
},
},
}).Result(),
pvc: builder.ForPersistentVolumeClaim("ns", "pvc-lost").
StorageClass("some-class").
Phase(corev1api.ClaimLost).
Result(),
resourcePolicies: &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"pvcPhase": []string{"Lost"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
},
shouldFSBackup: false,
expectedErr: false,
},
{
name: "Lost PVC with policy for Pending and Lost should not error and return false",
pod: builder.ForPod("ns", "pod-1").
Volumes(
&corev1api.Volume{
Name: "vol-lost",
VolumeSource: corev1api.VolumeSource{
PersistentVolumeClaim: &corev1api.PersistentVolumeClaimVolumeSource{
ClaimName: "pvc-lost",
},
},
}).Result(),
pvc: builder.ForPersistentVolumeClaim("ns", "pvc-lost").
StorageClass("some-class").
Phase(corev1api.ClaimLost).
Result(),
resourcePolicies: &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"pvcPhase": []string{"Pending", "Lost"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
},
shouldFSBackup: false,
expectedErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
fakeClient := velerotest.NewFakeControllerRuntimeClient(t, tc.pvc)
require.NoError(t, fakeClient.Create(t.Context(), tc.pod))
var p *resourcepolicies.Policies
if tc.resourcePolicies != nil {
p = &resourcepolicies.Policies{}
err := p.BuildPolicy(tc.resourcePolicies)
require.NoError(t, err)
}
vh := NewVolumeHelperImpl(
p,
ptr.To(true),
logrus.StandardLogger(),
fakeClient,
false,
false,
)
actualShouldFSBackup, actualError := vh.ShouldPerformFSBackup(tc.pod.Spec.Volumes[0], *tc.pod)
if tc.expectedErr {
require.Error(t, actualError, "Want error; Got nil error")
return
}
require.NoError(t, actualError)
require.Equalf(t, tc.shouldFSBackup, actualShouldFSBackup, "Want shouldFSBackup as %t; Got shouldFSBackup as %t", tc.shouldFSBackup, actualShouldFSBackup)
})
}
}
@@ -35,8 +35,7 @@ type BackupRepositorySpec struct {
// +optional
RepositoryType string `json:"repositoryType"`
// ResticIdentifier is the full restic-compatible string for identifying
// this repository. This field is only used when RepositoryType is "restic".
// Deprecated
// +optional
ResticIdentifier string `json:"resticIdentifier,omitempty"`
@@ -58,8 +57,7 @@ const (
BackupRepositoryPhaseReady BackupRepositoryPhase = "Ready"
BackupRepositoryPhaseNotReady BackupRepositoryPhase = "NotReady"
BackupRepositoryTypeRestic string = "restic"
BackupRepositoryTypeKopia string = "kopia"
BackupRepositoryTypeKopia string = "kopia"
)
// BackupRepositoryStatus is the current status of a BackupRepository.
+10
View File
@@ -102,6 +102,15 @@ const (
// even if the resource contains a matching selector label.
ExcludeFromBackupLabel = "velero.io/exclude-from-backup"
// SkipFromBackupAnnotation is the annotation used by internal BackupItemActions
// to indicate that a resource should be skipped from backup,
// even if it doesn't have the ExcludeFromBackupLabel.
// This is used in cases where we want to skip backup of a resource based on some logic in a plugin.
//
// Notice: SkipFromBackupAnnotation's priority is higher than MustIncludeAdditionalItemAnnotation.
// If SkipFromBackupAnnotation is set, the resource will be skipped even if MustIncludeAdditionalItemAnnotation is set.
SkipFromBackupAnnotation = "velero.io/skip-from-backup"
// defaultVGSLabelKey is the default label key used to group PVCs under a VolumeGroupSnapshot
DefaultVGSLabelKey = "velero.io/volume-group"
@@ -132,6 +141,7 @@ const (
VolumeSnapshotRestoreSize = "velero.io/csi-volumesnapshot-restore-size"
DriverNameAnnotation = "velero.io/csi-driver-name"
VSCDeletionPolicyAnnotation = "velero.io/csi-vsc-deletion-policy"
VolumeGroupSnapshotHandleAnnotation = "velero.io/csi-volumegroupsnapshot-handle"
VolumeSnapshotClassSelectorLabel = "velero.io/csi-volumesnapshot-class"
VolumeSnapshotClassDriverBackupAnnotationPrefix = "velero.io/csi-volumesnapshot-class"
VolumeSnapshotClassDriverPVCAnnotation = "velero.io/csi-volumesnapshot-class"
+17 -1
View File
@@ -19,8 +19,10 @@ package archive
import (
"archive/tar"
"compress/gzip"
"fmt"
"io"
"path/filepath"
"strings"
"github.com/sirupsen/logrus"
@@ -66,6 +68,16 @@ func (e *Extractor) writeFile(target string, tarRdr *tar.Reader) error {
return nil
}
// sanitizeArchivePath sanitizes archive file path from "G305: Zip Slip vulnerability"
func sanitizeArchivePath(destDir, sourcePath string) (targetPath string, err error) {
targetPath = filepath.Join(destDir, sourcePath)
if strings.HasPrefix(targetPath, filepath.Clean(destDir)) {
return targetPath, nil
}
return "", fmt.Errorf("invalid archive path %q: escapes target directory", sourcePath)
}
func (e *Extractor) readBackup(tarRdr *tar.Reader) (string, error) {
dir, err := e.fs.TempDir("", "")
if err != nil {
@@ -84,7 +96,11 @@ func (e *Extractor) readBackup(tarRdr *tar.Reader) (string, error) {
return "", err
}
target := filepath.Join(dir, header.Name) //nolint:gosec // Internal usage. No need to check.
target, err := sanitizeArchivePath(dir, header.Name)
if err != nil {
e.log.Infof("error sanitizing archive path: %s", err.Error())
return "", err
}
switch header.Typeflag {
case tar.TypeDir:
+26
View File
@@ -18,6 +18,7 @@ package archive
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"os"
@@ -87,6 +88,31 @@ func TestUnzipAndExtractBackup(t *testing.T) {
}
}
func TestUnzipAndExtractBackupRejectsPathTraversal(t *testing.T) {
ext := NewExtractor(test.NewLogger(), test.NewFakeFileSystem())
var buf bytes.Buffer
gzw := gzip.NewWriter(&buf)
tw := tar.NewWriter(gzw)
err := tw.WriteHeader(&tar.Header{
Name: "../escape.txt",
Mode: 0600,
Typeflag: tar.TypeReg,
Size: int64(len("data")),
})
require.NoError(t, err)
_, err = tw.Write([]byte("data"))
require.NoError(t, err)
require.NoError(t, tw.Close())
require.NoError(t, gzw.Close())
_, err = ext.UnzipAndExtractBackup(&buf)
require.Error(t, err)
require.Contains(t, err.Error(), "invalid archive path")
}
func createArchive(files []string, fs filesystem.Interface) (string, error) {
outName := "output.tar.gz"
out, err := fs.Create(outName)
+31 -38
View File
@@ -24,7 +24,7 @@ import (
"k8s.io/client-go/util/retry"
volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1"
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
@@ -44,7 +44,6 @@ import (
"k8s.io/apimachinery/pkg/api/resource"
internalvolumehelper "github.com/vmware-tanzu/velero/internal/volumehelper"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerov2alpha1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1"
veleroclient "github.com/vmware-tanzu/velero/pkg/client"
@@ -59,6 +58,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/util/csi"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
podvolumeutil "github.com/vmware-tanzu/velero/pkg/util/podvolume"
vhutil "github.com/vmware-tanzu/velero/pkg/util/volumehelper"
)
// TODO: Replace hardcoded VolumeSnapshot finalizer strings with constants from
@@ -128,9 +128,9 @@ func (p *pvcBackupItemAction) ensurePVCPodCacheForNamespace(ctx context.Context,
// getVolumeHelperWithCache creates a VolumeHelper using the pre-built PVC-to-Pod cache.
// The cache should be ensured for the relevant namespace(s) before calling this.
func (p *pvcBackupItemAction) getVolumeHelperWithCache(backup *velerov1api.Backup) (internalvolumehelper.VolumeHelper, error) {
func (p *pvcBackupItemAction) getVolumeHelperWithCache(backup *velerov1api.Backup) (vhutil.VolumeHelper, error) {
// Create VolumeHelper with our lazy-built cache
vh, err := internalvolumehelper.NewVolumeHelperImplWithCache(
vh, err := volumehelper.NewVolumeHelperWithCache(
*backup,
p.crClient,
p.log,
@@ -149,7 +149,7 @@ func (p *pvcBackupItemAction) getVolumeHelperWithCache(backup *velerov1api.Backu
// Since plugin instances are unique per backup (created via newPluginManager and
// cleaned up via CleanupClients at backup completion), we can safely cache this.
// See issue #9179 and PR #9226 for details.
func (p *pvcBackupItemAction) getOrCreateVolumeHelper(backup *velerov1api.Backup) (internalvolumehelper.VolumeHelper, error) {
func (p *pvcBackupItemAction) getOrCreateVolumeHelper(backup *velerov1api.Backup) (vhutil.VolumeHelper, error) {
// Initialize the PVC-to-Pod cache if needed
if p.pvcPodCache == nil {
p.pvcPodCache = podvolumeutil.NewPVCPodCache()
@@ -322,13 +322,9 @@ func (p *pvcBackupItemAction) Execute(
return nil, nil, "", nil, err
}
shouldSnapshot, err := volumehelper.ShouldPerformSnapshotWithVolumeHelper(
shouldSnapshot, err := vh.ShouldPerformSnapshot(
item,
kuberesource.PersistentVolumeClaims,
*backup,
p.crClient,
p.log,
vh,
)
if err != nil {
return nil, nil, "", nil, err
@@ -471,7 +467,7 @@ func (p *pvcBackupItemAction) Progress(
return progress, biav2.InvalidOperationIDError(operationID)
}
dataUpload, err := getDataUpload(context.Background(), p.crClient, operationID)
dataUpload, err := getDataUpload(context.Background(), p.crClient, backup.Namespace, operationID)
if err != nil {
p.log.Errorf(
"fail to get DataUpload for backup %s/%s by operation ID %s: %s",
@@ -516,7 +512,7 @@ func (p *pvcBackupItemAction) Cancel(operationID string, backup *velerov1api.Bac
return biav2.InvalidOperationIDError(operationID)
}
dataUpload, err := getDataUpload(context.Background(), p.crClient, operationID)
dataUpload, err := getDataUpload(context.Background(), p.crClient, backup.Namespace, operationID)
if err != nil {
p.log.Errorf(
"fail to get DataUpload for backup %s/%s: %s",
@@ -609,10 +605,12 @@ func createDataUpload(
func getDataUpload(
ctx context.Context,
crClient crclient.Client,
namespace string,
operationID string,
) (*velerov2alpha1.DataUpload, error) {
dataUploadList := new(velerov2alpha1.DataUploadList)
err := crClient.List(ctx, dataUploadList, &crclient.ListOptions{
Namespace: namespace,
LabelSelector: labels.SelectorFromSet(
map[string]string{velerov1api.AsyncOperationIDLabel: operationID},
),
@@ -708,7 +706,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference(
}
// Filter PVCs by volume policy
filteredPVCs, err := p.filterPVCsByVolumePolicy(groupedPVCs, backup, vh)
filteredPVCs, err := p.filterPVCsByVolumePolicy(groupedPVCs, vh)
if err != nil {
return nil, errors.Wrapf(err, "failed to filter PVCs by volume policy for VolumeGroupSnapshot group %q", group)
}
@@ -769,7 +767,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference(
}
// Re-fetch latest VGS to ensure status is populated after VGSC binding
latestVGS := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{}
latestVGS := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{}
if err := p.crClient.Get(ctx, crclient.ObjectKeyFromObject(newVGS), latestVGS); err != nil {
return nil, errors.Wrapf(err, "failed to re-fetch VolumeGroupSnapshot %s after VGSC binding wait", newVGS.Name)
}
@@ -844,8 +842,7 @@ func (p *pvcBackupItemAction) listGroupedPVCs(ctx context.Context, namespace, la
func (p *pvcBackupItemAction) filterPVCsByVolumePolicy(
pvcs []corev1api.PersistentVolumeClaim,
backup *velerov1api.Backup,
vh internalvolumehelper.VolumeHelper,
vh vhutil.VolumeHelper,
) ([]corev1api.PersistentVolumeClaim, error) {
var filteredPVCs []corev1api.PersistentVolumeClaim
@@ -859,13 +856,9 @@ func (p *pvcBackupItemAction) filterPVCsByVolumePolicy(
// Check if this PVC should be snapshotted according to volume policies
// Uses the cached VolumeHelper for better performance with many PVCs/pods
shouldSnapshot, err := volumehelper.ShouldPerformSnapshotWithVolumeHelper(
shouldSnapshot, err := vh.ShouldPerformSnapshot(
unstructuredPVC,
kuberesource.PersistentVolumeClaims,
*backup,
p.crClient,
p.log,
vh,
)
if err != nil {
return nil, errors.Wrapf(err, "failed to check volume policy for PVC %s/%s", pvc.Namespace, pvc.Name)
@@ -922,7 +915,7 @@ func (p *pvcBackupItemAction) determineVGSClass(
}
// 3. Fallback to label-based default
vgsClassList := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotClassList{}
vgsClassList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotClassList{}
if err := p.crClient.List(ctx, vgsClassList); err != nil {
return "", errors.Wrap(err, "failed to list VolumeGroupSnapshotClasses")
}
@@ -951,22 +944,22 @@ func (p *pvcBackupItemAction) createVolumeGroupSnapshot(
backup *velerov1api.Backup,
pvc corev1api.PersistentVolumeClaim,
vgsLabelKey, vgsLabelValue, vgsClassName string,
) (*volumegroupsnapshotv1beta1.VolumeGroupSnapshot, error) {
) (*volumegroupsnapshotv1beta2.VolumeGroupSnapshot, error) {
vgsLabels := map[string]string{
velerov1api.BackupNameLabel: label.GetValidName(backup.Name),
velerov1api.BackupUIDLabel: string(backup.UID),
vgsLabelKey: vgsLabelValue,
}
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("velero-%s-", vgsLabelValue),
Namespace: pvc.Namespace,
Labels: vgsLabels,
},
Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotSpec{
Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotSpec{
VolumeGroupSnapshotClassName: &vgsClassName,
Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotSource{
Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotSource{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
vgsLabelKey: vgsLabelValue,
@@ -994,7 +987,7 @@ func (p *pvcBackupItemAction) createVolumeGroupSnapshot(
func (p *pvcBackupItemAction) waitForVGSAssociatedVS(
ctx context.Context,
groupedPVCs []corev1api.PersistentVolumeClaim,
vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot,
vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot,
timeout time.Duration,
) (map[string]*snapshotv1api.VolumeSnapshot, error) {
expected := len(groupedPVCs)
@@ -1037,10 +1030,10 @@ func (p *pvcBackupItemAction) waitForVGSAssociatedVS(
return vsMap, nil
}
func hasOwnerReference(obj metav1.Object, vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot) bool {
func hasOwnerReference(obj metav1.Object, vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot) bool {
for _, ref := range obj.GetOwnerReferences() {
if ref.Kind == kuberesource.VGSKind &&
ref.APIVersion == volumegroupsnapshotv1beta1.GroupName+"/"+volumegroupsnapshotv1beta1.SchemeGroupVersion.Version &&
ref.APIVersion == volumegroupsnapshotv1beta2.GroupName+"/"+volumegroupsnapshotv1beta2.SchemeGroupVersion.Version &&
ref.UID == vgs.UID {
return true
}
@@ -1051,7 +1044,7 @@ func hasOwnerReference(obj metav1.Object, vgs *volumegroupsnapshotv1beta1.Volume
func (p *pvcBackupItemAction) updateVGSCreatedVS(
ctx context.Context,
vsMap map[string]*snapshotv1api.VolumeSnapshot,
vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot,
vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot,
backup *velerov1api.Backup,
) error {
for pvcName, vs := range vsMap {
@@ -1094,7 +1087,7 @@ func (p *pvcBackupItemAction) updateVGSCreatedVS(
return nil
}
func (p *pvcBackupItemAction) patchVGSCDeletionPolicy(ctx context.Context, vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot) error {
func (p *pvcBackupItemAction) patchVGSCDeletionPolicy(ctx context.Context, vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot) error {
if vgs == nil || vgs.Status == nil || vgs.Status.BoundVolumeGroupSnapshotContentName == nil {
return errors.New("VolumeGroupSnapshotContent name not found in VGS status")
}
@@ -1102,7 +1095,7 @@ func (p *pvcBackupItemAction) patchVGSCDeletionPolicy(ctx context.Context, vgs *
vgscName := vgs.Status.BoundVolumeGroupSnapshotContentName
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{}
vgsc := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{}
if err := p.crClient.Get(ctx, crclient.ObjectKey{Name: *vgscName}, vgsc); err != nil {
return errors.Wrapf(err, "failed to get VolumeGroupSnapshotContent %s for VolumeGroupSnapshot %s/%s", *vgscName, vgs.Namespace, vgs.Name)
}
@@ -1121,9 +1114,9 @@ func (p *pvcBackupItemAction) patchVGSCDeletionPolicy(ctx context.Context, vgs *
})
}
func (p *pvcBackupItemAction) deleteVGSAndVGSC(ctx context.Context, vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot) error {
func (p *pvcBackupItemAction) deleteVGSAndVGSC(ctx context.Context, vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot) error {
if vgs.Status != nil && vgs.Status.BoundVolumeGroupSnapshotContentName != nil {
vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{
vgsc := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{
ObjectMeta: metav1.ObjectMeta{
Name: *vgs.Status.BoundVolumeGroupSnapshotContentName,
},
@@ -1148,11 +1141,11 @@ func (p *pvcBackupItemAction) deleteVGSAndVGSC(ctx context.Context, vgs *volumeg
func (p *pvcBackupItemAction) waitForVGSCBinding(
ctx context.Context,
vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot,
vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot,
timeout time.Duration,
) error {
return wait.PollUntilContextTimeout(ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) {
vgsRef := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{}
vgsRef := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{}
if err := p.crClient.Get(ctx, crclient.ObjectKeyFromObject(vgs), vgsRef); err != nil {
return false, err
}
@@ -1165,8 +1158,8 @@ func (p *pvcBackupItemAction) waitForVGSCBinding(
})
}
func (p *pvcBackupItemAction) getVGSByLabels(ctx context.Context, namespace string, labels map[string]string) (*volumegroupsnapshotv1beta1.VolumeGroupSnapshot, error) {
vgsList := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotList{}
func (p *pvcBackupItemAction) getVGSByLabels(ctx context.Context, namespace string, labels map[string]string) (*volumegroupsnapshotv1beta2.VolumeGroupSnapshot, error) {
vgsList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotList{}
if err := p.crClient.List(ctx, vgsList,
crclient.InNamespace(namespace),
crclient.MatchingLabels(labels),
+106 -49
View File
@@ -25,7 +25,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/kuberesource"
volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1"
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
"github.com/stretchr/testify/assert"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
@@ -307,6 +307,28 @@ func TestProgress(t *testing.T) {
operationID: "testing",
expectedErr: "not found DataUpload for operationID testing",
},
{
name: "DataUpload in different namespace is not found",
backup: builder.ForBackup("velero", "test").Result(),
dataUpload: &velerov2alpha1.DataUpload{
TypeMeta: metav1.TypeMeta{
Kind: "DataUpload",
APIVersion: "v2alpha1",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "other-namespace",
Name: "testing",
Labels: map[string]string{
velerov1api.AsyncOperationIDLabel: "testing",
},
},
Status: velerov2alpha1.DataUploadStatus{
Phase: velerov2alpha1.DataUploadPhaseFailed,
},
},
operationID: "testing",
expectedErr: "not found DataUpload for operationID testing",
},
{
name: "DataUpload is found",
backup: builder.ForBackup("velero", "test").Result(),
@@ -375,15 +397,15 @@ func TestCancel(t *testing.T) {
tests := []struct {
name string
backup *velerov1api.Backup
dataUpload velerov2alpha1.DataUpload
dataUpload *velerov2alpha1.DataUpload
operationID string
expectedErr error
expectedErr string
expectedDataUpload velerov2alpha1.DataUpload
}{
{
name: "Cancel DataUpload",
backup: builder.ForBackup("velero", "test").Result(),
dataUpload: velerov2alpha1.DataUpload{
dataUpload: &velerov2alpha1.DataUpload{
TypeMeta: metav1.TypeMeta{
Kind: "DataUpload",
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
@@ -414,6 +436,31 @@ func TestCancel(t *testing.T) {
},
},
},
{
name: "DataUpload cannot be found",
backup: builder.ForBackup("velero", "test").Result(),
operationID: "testing",
expectedErr: "not found DataUpload for operationID testing",
},
{
name: "DataUpload in different namespace is not found",
backup: builder.ForBackup("velero", "test").Result(),
dataUpload: &velerov2alpha1.DataUpload{
TypeMeta: metav1.TypeMeta{
Kind: "DataUpload",
APIVersion: velerov2alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Namespace: "other-namespace",
Name: "testing",
Labels: map[string]string{
velerov1api.AsyncOperationIDLabel: "testing",
},
},
},
operationID: "testing",
expectedErr: "not found DataUpload for operationID testing",
},
}
for _, tc := range tests {
@@ -426,17 +473,23 @@ func TestCancel(t *testing.T) {
crClient: crClient,
}
err := crClient.Create(t.Context(), &tc.dataUpload)
require.NoError(t, err)
if tc.dataUpload != nil {
err := crClient.Create(t.Context(), tc.dataUpload)
require.NoError(t, err)
}
err = pvcBIA.Cancel(tc.operationID, tc.backup)
require.NoError(t, err)
err := pvcBIA.Cancel(tc.operationID, tc.backup)
if tc.expectedErr != "" {
require.EqualError(t, err, tc.expectedErr)
} else {
require.NoError(t, err)
du := new(velerov2alpha1.DataUpload)
err = crClient.Get(t.Context(), crclient.ObjectKey{Namespace: tc.dataUpload.Namespace, Name: tc.dataUpload.Name}, du)
require.NoError(t, err)
du := new(velerov2alpha1.DataUpload)
err = crClient.Get(t.Context(), crclient.ObjectKey{Namespace: tc.dataUpload.Namespace, Name: tc.dataUpload.Name}, du)
require.NoError(t, err)
require.True(t, cmp.Equal(tc.expectedDataUpload, *du, cmpopts.IgnoreFields(velerov2alpha1.DataUpload{}, "ResourceVersion")))
require.True(t, cmp.Equal(tc.expectedDataUpload, *du, cmpopts.IgnoreFields(velerov2alpha1.DataUpload{}, "ResourceVersion")))
}
})
}
}
@@ -842,9 +895,13 @@ volumePolicies:
crClient: client,
}
// Pass nil for VolumeHelper in tests - it will fall back to creating a new one per call
// This is the expected behavior for testing and third-party plugins
result, err := action.filterPVCsByVolumePolicy(tt.pvcs, backup, nil)
// Create a VolumeHelper using the same method the plugin would use
vh, err := action.getOrCreateVolumeHelper(backup)
require.NoError(t, err)
require.NotNil(t, vh)
// Test with the pre-created VolumeHelper
result, err := action.filterPVCsByVolumePolicy(tt.pvcs, vh)
if tt.expectError {
require.Error(t, err)
} else {
@@ -959,7 +1016,7 @@ volumePolicies:
require.NotNil(t, vh)
// Test with the pre-created VolumeHelper (non-nil path)
result, err := action.filterPVCsByVolumePolicy(pvcs, backup, vh)
result, err := action.filterPVCsByVolumePolicy(pvcs, vh)
require.NoError(t, err)
// Should filter out the NFS PVC, leaving only the CSI PVC
@@ -1117,7 +1174,7 @@ func TestDetermineVGSClass(t *testing.T) {
name string
backup *velerov1api.Backup
pvc *corev1api.PersistentVolumeClaim
existingVGSClass []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass
existingVGSClass []volumegroupsnapshotv1beta2.VolumeGroupSnapshotClass
expectError bool
expectResult string
}{
@@ -1149,7 +1206,7 @@ func TestDetermineVGSClass(t *testing.T) {
name: "Default label-based match",
pvc: &corev1api.PersistentVolumeClaim{},
backup: &velerov1api.Backup{},
existingVGSClass: []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass{
existingVGSClass: []volumegroupsnapshotv1beta2.VolumeGroupSnapshotClass{
{
ObjectMeta: metav1.ObjectMeta{
Name: "default-class",
@@ -1170,7 +1227,7 @@ func TestDetermineVGSClass(t *testing.T) {
name: "Multiple matching VGS classes",
pvc: &corev1api.PersistentVolumeClaim{},
backup: &velerov1api.Backup{},
existingVGSClass: []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass{
existingVGSClass: []volumegroupsnapshotv1beta2.VolumeGroupSnapshotClass{
{
ObjectMeta: metav1.ObjectMeta{
Name: "class1",
@@ -1200,7 +1257,7 @@ func TestDetermineVGSClass(t *testing.T) {
client := velerotest.NewFakeControllerRuntimeClient(t, initObjs...)
logger := logrus.New()
require.NoError(t, volumegroupsnapshotv1beta1.AddToScheme(client.Scheme()))
require.NoError(t, volumegroupsnapshotv1beta2.AddToScheme(client.Scheme()))
action := &pvcBackupItemAction{crClient: client, log: logger}
@@ -1259,13 +1316,13 @@ func TestCreateVolumeGroupSnapshot(t *testing.T) {
assert.Equal(t, string(testBackup.UID), vgs.Labels[velerov1api.BackupUIDLabel])
// Check that it exists in fake client
retrieved := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{}
retrieved := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{}
err = crClient.Get(t.Context(), crclient.ObjectKey{Name: vgs.Name, Namespace: vgs.Namespace}, retrieved)
require.NoError(t, err)
}
func TestWaitForVGSAssociatedVS(t *testing.T) {
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vgs",
Namespace: "test-ns",
@@ -1278,7 +1335,7 @@ func TestWaitForVGSAssociatedVS(t *testing.T) {
if owned {
refs = []metav1.OwnerReference{
{
APIVersion: "groupsnapshot.storage.k8s.io/v1beta1",
APIVersion: "groupsnapshot.storage.k8s.io/v1beta2",
Kind: "VolumeGroupSnapshot",
Name: vgs.Name,
UID: vgs.UID,
@@ -1425,7 +1482,7 @@ func TestUpdateVGSCreatedVS(t *testing.T) {
},
}
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vgs",
Namespace: "ns",
@@ -1438,7 +1495,7 @@ func TestUpdateVGSCreatedVS(t *testing.T) {
if withVGSOwner {
refs = []metav1.OwnerReference{
{
APIVersion: "groupsnapshot.storage.k8s.io/v1beta1",
APIVersion: "groupsnapshot.storage.k8s.io/v1beta2",
Kind: "VolumeGroupSnapshot",
Name: vgs.Name,
UID: vgs.UID,
@@ -1557,18 +1614,18 @@ func TestPatchVGSCDeletionPolicy(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{
vgsc := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{
ObjectMeta: metav1.ObjectMeta{Name: "test-vgsc"},
Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{
Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{
DeletionPolicy: tt.initialPolicy,
},
}
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vgs",
Namespace: "ns",
},
Status: &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{
Status: &volumegroupsnapshotv1beta2.VolumeGroupSnapshotStatus{
BoundVolumeGroupSnapshotContentName: pointer.String("test-vgsc"),
},
}
@@ -1586,7 +1643,7 @@ func TestPatchVGSCDeletionPolicy(t *testing.T) {
}
require.NoError(t, err)
updated := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{}
updated := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{}
err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgsc"}, updated)
require.NoError(t, err)
require.Equal(t, tt.expectedPolicy, updated.Spec.DeletionPolicy)
@@ -1595,20 +1652,20 @@ func TestPatchVGSCDeletionPolicy(t *testing.T) {
}
func TestDeleteVGSAndVGSC(t *testing.T) {
makeVGS := func(name, namespace string, boundVGSCName *string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot {
return &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
makeVGS := func(name, namespace string, boundVGSCName *string) *volumegroupsnapshotv1beta2.VolumeGroupSnapshot {
return &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Status: &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{
Status: &volumegroupsnapshotv1beta2.VolumeGroupSnapshotStatus{
BoundVolumeGroupSnapshotContentName: boundVGSCName,
},
}
}
makeVGSC := func(name string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent {
return &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{
makeVGSC := func(name string) *volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent {
return &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
@@ -1617,8 +1674,8 @@ func TestDeleteVGSAndVGSC(t *testing.T) {
tests := []struct {
name string
vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot
existingVGSC *volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent
vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot
existingVGSC *volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent
expectVGSCDelete bool
expectVGSDelete bool
}{
@@ -1664,13 +1721,13 @@ func TestDeleteVGSAndVGSC(t *testing.T) {
// Check VGSC is deleted
if tt.expectVGSCDelete {
got := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{}
got := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{}
err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgsc"}, got)
assert.True(t, apierrors.IsNotFound(err), "expected VGSC to be deleted")
}
// Check VGS is deleted
gotVGS := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{}
gotVGS := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{}
err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgs", Namespace: "ns"}, gotVGS)
assert.True(t, apierrors.IsNotFound(err), "expected VGS to be deleted")
})
@@ -1765,8 +1822,8 @@ func TestFindExistingVSForBackup(t *testing.T) {
}
func TestWaitForVGSCBinding(t *testing.T) {
makeVGS := func(name string, withStatus bool) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot {
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
makeVGS := func(name string, withStatus bool) *volumegroupsnapshotv1beta2.VolumeGroupSnapshot {
vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "ns",
@@ -1774,7 +1831,7 @@ func TestWaitForVGSCBinding(t *testing.T) {
}
if withStatus {
contentName := "vgsc-123"
vgs.Status = &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{
vgs.Status = &volumegroupsnapshotv1beta2.VolumeGroupSnapshotStatus{
BoundVolumeGroupSnapshotContentName: &contentName,
}
}
@@ -1783,7 +1840,7 @@ func TestWaitForVGSCBinding(t *testing.T) {
tests := []struct {
name string
vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot
vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot
expectErr bool
}{
{
@@ -1826,8 +1883,8 @@ func TestGetVGSByLabels(t *testing.T) {
labelVal := "backup-123"
testLabels := map[string]string{labelKey: labelVal}
makeVGS := func(name string, labels map[string]string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot {
return &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
makeVGS := func(name string, labels map[string]string) *volumegroupsnapshotv1beta2.VolumeGroupSnapshot {
return &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: "test-ns",
@@ -1912,7 +1969,7 @@ func (f *failingClient) List(ctx context.Context, list crclient.ObjectList, opts
}
func TestHasOwnerReference(t *testing.T) {
vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{
vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{
ObjectMeta: metav1.ObjectMeta{
Name: "test-vgs",
Namespace: "test-ns",
@@ -1929,7 +1986,7 @@ func TestHasOwnerReference(t *testing.T) {
name: "match kind, apiversion, uid",
ownerRef: metav1.OwnerReference{
Kind: kuberesource.VGSKind,
APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version,
APIVersion: volumegroupsnapshotv1beta2.GroupName + "/" + volumegroupsnapshotv1beta2.SchemeGroupVersion.Version,
UID: vgs.UID,
},
expect: true,
@@ -1938,7 +1995,7 @@ func TestHasOwnerReference(t *testing.T) {
name: "mismatch kind",
ownerRef: metav1.OwnerReference{
Kind: "other-kind",
APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version,
APIVersion: volumegroupsnapshotv1beta2.GroupName + "/" + volumegroupsnapshotv1beta2.SchemeGroupVersion.Version,
UID: vgs.UID,
},
expect: false,
@@ -1956,7 +2013,7 @@ func TestHasOwnerReference(t *testing.T) {
name: "mismatch uid",
ownerRef: metav1.OwnerReference{
Kind: kuberesource.VGSKind,
APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version,
APIVersion: volumegroupsnapshotv1beta2.GroupName + "/" + volumegroupsnapshotv1beta2.SchemeGroupVersion.Version,
UID: "wrong-uid",
},
expect: false,
@@ -151,6 +151,12 @@ func (p *volumeSnapshotBackupItemAction) Execute(
annotations[velerov1api.VolumeSnapshotRestoreSize] = resource.NewQuantity(
*vsc.Status.RestoreSize, resource.BinarySI).String()
}
// Capture VolumeGroupSnapshotHandle to create stub VGSC during restore
// for CSI drivers that populate this field (e.g., Ceph RBD).
if vsc.Status.VolumeGroupSnapshotHandle != nil {
annotations[velerov1api.VolumeGroupSnapshotHandleAnnotation] = *vsc.Status.VolumeGroupSnapshotHandle
}
}
p.log.Infof("Patching VolumeSnapshotContent %s with velero BackupNameLabel",
+8
View File
@@ -98,6 +98,14 @@ func (m *backedUpItemsMap) AddItem(key itemKey) {
m.totalItems[key] = struct{}{}
}
func (m *backedUpItemsMap) DeleteItem(key itemKey) {
m.Lock()
defer m.Unlock()
delete(m.backedUpItems, key)
delete(m.totalItems, key)
}
func (m *backedUpItemsMap) AddItemToTotal(key itemKey) {
m.Lock()
defer m.Unlock()
+38 -10
View File
@@ -40,7 +40,6 @@ import (
"github.com/vmware-tanzu/velero/internal/hook"
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
"github.com/vmware-tanzu/velero/internal/volume"
"github.com/vmware-tanzu/velero/internal/volumehelper"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/archive"
"github.com/vmware-tanzu/velero/pkg/client"
@@ -54,6 +53,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/podvolume"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
csiutil "github.com/vmware-tanzu/velero/pkg/util/csi"
"github.com/vmware-tanzu/velero/pkg/util/volumehelper"
)
const (
@@ -244,6 +244,14 @@ func (ib *itemBackupper) backupItemInternal(logger logrus.FieldLogger, obj runti
return false, itemFiles, kubeerrs.NewAggregate(backupErrs)
}
// If err is nil and updatedObj is nil, it means the item is skipped by plugin action,
// we should return here to avoid backing up the item, and avoid potential NPE in the following code.
if updatedObj == nil {
log.Infof("Remove item from the backup's backupItems list and totalItems list because it's skipped by plugin action.")
ib.backupRequest.BackedUpItems.DeleteItem(key)
return false, itemFiles, nil
}
itemFiles = append(itemFiles, additionalItemFiles...)
obj = updatedObj
if metadata, err = meta.Accessor(obj); err != nil {
@@ -398,6 +406,13 @@ func (ib *itemBackupper) executeActions(
}
u := &unstructured.Unstructured{Object: updatedItem.UnstructuredContent()}
if _, ok := u.GetAnnotations()[velerov1api.SkipFromBackupAnnotation]; ok {
log.Infof("Resource (groupResource=%s, namespace=%s, name=%s) is skipped from backup by action %s.",
groupResource.String(), namespace, name, actionName)
return nil, itemFiles, nil
}
if actionName == csiBIAPluginName {
if additionalItemIdentifiers == nil && u.GetAnnotations()[velerov1api.SkippedNoCSIPVAnnotation] == "true" {
// snapshot was skipped by CSI plugin
@@ -687,15 +702,14 @@ func (ib *itemBackupper) getMatchAction(obj runtime.Unstructured, groupResource
return nil, errors.WithStack(err)
}
pvName := pvc.Spec.VolumeName
if pvName == "" {
return nil, errors.Errorf("PVC has no volume backing this claim")
}
pv := &corev1api.PersistentVolume{}
if err := ib.kbClient.Get(context.Background(), kbClient.ObjectKey{Name: pvName}, pv); err != nil {
return nil, errors.WithStack(err)
var pv *corev1api.PersistentVolume
if pvName := pvc.Spec.VolumeName; pvName != "" {
pv = &corev1api.PersistentVolume{}
if err := ib.kbClient.Get(context.Background(), kbClient.ObjectKey{Name: pvName}, pv); err != nil {
return nil, errors.WithStack(err)
}
}
// If pv is nil for unbound PVCs - policy matching will use PVC-only conditions
vfd := resourcepolicies.NewVolumeFilterData(pv, nil, pvc)
return ib.backupRequest.ResPolicies.GetMatchAction(vfd)
}
@@ -709,7 +723,10 @@ func (ib *itemBackupper) trackSkippedPV(obj runtime.Unstructured, groupResource
if name, err := getPVName(obj, groupResource); len(name) > 0 && err == nil {
ib.backupRequest.SkippedPVTracker.Track(name, approach, reason)
} else if err != nil {
log.WithError(err).Warnf("unable to get PV name, skip tracking.")
// Log at info level for tracking purposes. This is not an error because
// it's expected for some resources (e.g., PVCs in Pending or Lost phase)
// to not have a PV name. This occurs when volume policy skips unbound PVCs.
log.WithError(err).Infof("unable to get PV name, skip tracking.")
}
}
@@ -719,6 +736,17 @@ func (ib *itemBackupper) unTrackSkippedPV(obj runtime.Unstructured, groupResourc
if name, err := getPVName(obj, groupResource); len(name) > 0 && err == nil {
ib.backupRequest.SkippedPVTracker.Untrack(name)
} else if err != nil {
// For PVCs in Pending or Lost phase, it's expected that there's no PV name.
// Log at debug level instead of warning to reduce noise.
if groupResource == kuberesource.PersistentVolumeClaims {
pvc := new(corev1api.PersistentVolumeClaim)
if convErr := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.UnstructuredContent(), pvc); convErr == nil {
if pvc.Status.Phase == corev1api.ClaimPending || pvc.Status.Phase == corev1api.ClaimLost {
log.WithError(err).Debugf("unable to get PV name for %s PVC, skip untracking.", pvc.Status.Phase)
return
}
}
}
log.WithError(err).Warnf("unable to get PV name, skip untracking.")
}
}
+225
View File
@@ -17,12 +17,15 @@ limitations under the License.
package backup
import (
"bytes"
"testing"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime/schema"
ctrlfake "sigs.k8s.io/controller-runtime/pkg/client/fake"
"github.com/vmware-tanzu/velero/internal/resourcepolicies"
"github.com/vmware-tanzu/velero/pkg/kuberesource"
"github.com/stretchr/testify/assert"
@@ -269,3 +272,225 @@ func TestAddVolumeInfo(t *testing.T) {
})
}
}
func TestGetMatchAction_PendingLostPVC(t *testing.T) {
scheme := runtime.NewScheme()
require.NoError(t, corev1api.AddToScheme(scheme))
// Create resource policies that skip Pending/Lost PVCs
resPolicies := &resourcepolicies.ResourcePolicies{
Version: "v1",
VolumePolicies: []resourcepolicies.VolumePolicy{
{
Conditions: map[string]any{
"pvcPhase": []string{"Pending", "Lost"},
},
Action: resourcepolicies.Action{
Type: resourcepolicies.Skip,
},
},
},
}
policies := &resourcepolicies.Policies{}
err := policies.BuildPolicy(resPolicies)
require.NoError(t, err)
testCases := []struct {
name string
pvc *corev1api.PersistentVolumeClaim
pv *corev1api.PersistentVolume
expectedAction *resourcepolicies.Action
expectError bool
}{
{
name: "Pending PVC with no VolumeName should match pvcPhase policy",
pvc: builder.ForPersistentVolumeClaim("ns", "pending-pvc").
StorageClass("test-sc").
Phase(corev1api.ClaimPending).
Result(),
pv: nil,
expectedAction: &resourcepolicies.Action{Type: resourcepolicies.Skip},
expectError: false,
},
{
name: "Lost PVC with no VolumeName should match pvcPhase policy",
pvc: builder.ForPersistentVolumeClaim("ns", "lost-pvc").
StorageClass("test-sc").
Phase(corev1api.ClaimLost).
Result(),
pv: nil,
expectedAction: &resourcepolicies.Action{Type: resourcepolicies.Skip},
expectError: false,
},
{
name: "Bound PVC with VolumeName and matching PV should not match pvcPhase policy",
pvc: builder.ForPersistentVolumeClaim("ns", "bound-pvc").
StorageClass("test-sc").
VolumeName("test-pv").
Phase(corev1api.ClaimBound).
Result(),
pv: builder.ForPersistentVolume("test-pv").StorageClass("test-sc").Result(),
expectedAction: nil,
expectError: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Build fake client with PV if present
clientBuilder := ctrlfake.NewClientBuilder().WithScheme(scheme)
if tc.pv != nil {
clientBuilder = clientBuilder.WithObjects(tc.pv)
}
fakeClient := clientBuilder.Build()
ib := &itemBackupper{
kbClient: fakeClient,
backupRequest: &Request{
ResPolicies: policies,
},
}
// Convert PVC to unstructured
pvcData, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pvc)
require.NoError(t, err)
obj := &unstructured.Unstructured{Object: pvcData}
action, err := ib.getMatchAction(obj, kuberesource.PersistentVolumeClaims, csiBIAPluginName)
if tc.expectError {
require.Error(t, err)
} else {
require.NoError(t, err)
}
if tc.expectedAction == nil {
assert.Nil(t, action)
} else {
require.NotNil(t, action)
assert.Equal(t, tc.expectedAction.Type, action.Type)
}
})
}
}
func TestTrackSkippedPV_PendingLostPVC(t *testing.T) {
testCases := []struct {
name string
pvc *corev1api.PersistentVolumeClaim
}{
{
name: "Pending PVC should log at info level",
pvc: builder.ForPersistentVolumeClaim("ns", "pending-pvc").
Phase(corev1api.ClaimPending).
Result(),
},
{
name: "Lost PVC should log at info level",
pvc: builder.ForPersistentVolumeClaim("ns", "lost-pvc").
Phase(corev1api.ClaimLost).
Result(),
},
{
name: "Bound PVC without VolumeName should log at info level",
pvc: builder.ForPersistentVolumeClaim("ns", "bound-pvc").
Phase(corev1api.ClaimBound).
Result(),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ib := &itemBackupper{
backupRequest: &Request{
SkippedPVTracker: NewSkipPVTracker(),
},
}
// Set up log capture
logOutput := &bytes.Buffer{}
logger := logrus.New()
logger.SetOutput(logOutput)
logger.SetLevel(logrus.DebugLevel)
// Convert PVC to unstructured
pvcData, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pvc)
require.NoError(t, err)
obj := &unstructured.Unstructured{Object: pvcData}
ib.trackSkippedPV(obj, kuberesource.PersistentVolumeClaims, "", "test reason", logger)
logStr := logOutput.String()
assert.Contains(t, logStr, "level=info")
assert.Contains(t, logStr, "unable to get PV name, skip tracking.")
})
}
}
func TestUnTrackSkippedPV_PendingLostPVC(t *testing.T) {
testCases := []struct {
name string
pvc *corev1api.PersistentVolumeClaim
expectWarningLog bool
expectDebugMessage string
}{
{
name: "Pending PVC should log at debug level, not warning",
pvc: builder.ForPersistentVolumeClaim("ns", "pending-pvc").
Phase(corev1api.ClaimPending).
Result(),
expectWarningLog: false,
expectDebugMessage: "unable to get PV name for Pending PVC, skip untracking.",
},
{
name: "Lost PVC should log at debug level, not warning",
pvc: builder.ForPersistentVolumeClaim("ns", "lost-pvc").
Phase(corev1api.ClaimLost).
Result(),
expectWarningLog: false,
expectDebugMessage: "unable to get PV name for Lost PVC, skip untracking.",
},
{
name: "Bound PVC without VolumeName should log warning",
pvc: builder.ForPersistentVolumeClaim("ns", "bound-pvc").
Phase(corev1api.ClaimBound).
Result(),
expectWarningLog: true,
expectDebugMessage: "",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ib := &itemBackupper{
backupRequest: &Request{
SkippedPVTracker: NewSkipPVTracker(),
},
}
// Set up log capture
logOutput := &bytes.Buffer{}
logger := logrus.New()
logger.SetOutput(logOutput)
logger.SetLevel(logrus.DebugLevel)
// Convert PVC to unstructured
pvcData, err := runtime.DefaultUnstructuredConverter.ToUnstructured(tc.pvc)
require.NoError(t, err)
obj := &unstructured.Unstructured{Object: pvcData}
ib.unTrackSkippedPV(obj, kuberesource.PersistentVolumeClaims, logger)
logStr := logOutput.String()
if tc.expectWarningLog {
assert.Contains(t, logStr, "level=warning")
assert.Contains(t, logStr, "unable to get PV name, skip untracking.")
} else {
assert.NotContains(t, logStr, "level=warning")
if tc.expectDebugMessage != "" {
assert.Contains(t, logStr, "level=debug")
assert.Contains(t, logStr, tc.expectDebugMessage)
}
}
})
}
}
+3 -3
View File
@@ -19,7 +19,7 @@ package client
import (
"os"
volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1"
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
@@ -168,7 +168,7 @@ func (f *factory) KubebuilderClient() (kbclient.Client, error) {
if err := snapshotv1api.AddToScheme(scheme); err != nil {
return nil, err
}
if err := volumegroupsnapshotv1beta1.AddToScheme(scheme); err != nil {
if err := volumegroupsnapshotv1beta2.AddToScheme(scheme); err != nil {
return nil, err
}
kubebuilderClient, err := kbclient.New(clientConfig, kbclient.Options{
@@ -207,7 +207,7 @@ func (f *factory) KubebuilderWatchClient() (kbclient.WithWatch, error) {
if err := snapshotv1api.AddToScheme(scheme); err != nil {
return nil, err
}
if err := volumegroupsnapshotv1beta1.AddToScheme(scheme); err != nil {
if err := volumegroupsnapshotv1beta2.AddToScheme(scheme); err != nil {
return nil, err
}
kubebuilderWatchClient, err := kbclient.NewWithWatch(clientConfig, kbclient.Options{
+13 -3
View File
@@ -275,11 +275,21 @@ func (o *Options) AsVeleroOptions() (*install.VeleroOptions, error) {
return nil, err
}
}
veleroPodResources, err := kubeutil.ParseResourceRequirements(o.VeleroPodCPURequest, o.VeleroPodMemRequest, o.VeleroPodCPULimit, o.VeleroPodMemLimit)
veleroPodResources, err := kubeutil.ParseCPUAndMemoryResources(
o.VeleroPodCPURequest,
o.VeleroPodMemRequest,
o.VeleroPodCPULimit,
o.VeleroPodMemLimit,
)
if err != nil {
return nil, err
}
nodeAgentPodResources, err := kubeutil.ParseResourceRequirements(o.NodeAgentPodCPURequest, o.NodeAgentPodMemRequest, o.NodeAgentPodCPULimit, o.NodeAgentPodMemLimit)
nodeAgentPodResources, err := kubeutil.ParseCPUAndMemoryResources(
o.NodeAgentPodCPURequest,
o.NodeAgentPodMemRequest,
o.NodeAgentPodCPULimit,
o.NodeAgentPodMemLimit,
)
if err != nil {
return nil, err
}
@@ -371,8 +381,8 @@ This is useful as a starting point for more customized installations.
# velero install --provider azure --plugins velero/velero-plugin-for-microsoft-azure:v1.0.0 --bucket $BLOB_CONTAINER --secret-file ./credentials-velero --backup-location-config resourceGroup=$AZURE_BACKUP_RESOURCE_GROUP,storageAccount=$AZURE_STORAGE_ACCOUNT_ID[,subscriptionId=$AZURE_BACKUP_SUBSCRIPTION_ID] --snapshot-location-config apiTimeout=<YOUR_TIMEOUT>[,resourceGroup=$AZURE_BACKUP_RESOURCE_GROUP,subscriptionId=$AZURE_BACKUP_SUBSCRIPTION_ID]`,
Run: func(c *cobra.Command, args []string) {
cmd.CheckError(o.Validate(c, args, f))
cmd.CheckError(o.Complete(args, f))
cmd.CheckError(o.Validate(c, args, f))
cmd.CheckError(o.Run(c, f))
},
}
+172
View File
@@ -17,11 +17,18 @@ limitations under the License.
package install
import (
"context"
"testing"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
factorymocks "github.com/vmware-tanzu/velero/pkg/client/mocks"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
)
func TestPriorityClassNameFlag(t *testing.T) {
@@ -91,3 +98,168 @@ func TestPriorityClassNameFlag(t *testing.T) {
})
}
}
// makeValidateCmd returns a minimal *cobra.Command that satisfies output.ValidateFlags.
func makeValidateCmd() *cobra.Command {
c := &cobra.Command{}
// output.ValidateFlags only inspects the "output" flag; add it so validation passes.
c.Flags().StringP("output", "o", "", "output format")
return c
}
// configMapInNamespace builds a ConfigMap with a single JSON data entry in the given namespace.
func configMapInNamespace(namespace, name, jsonValue string) *corev1api.ConfigMap {
return &corev1api.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Namespace: namespace,
Name: name,
},
Data: map[string]string{
"config": jsonValue,
},
}
}
// TestValidateConfigMapsUseFactoryNamespace verifies that Validate resolves the target
// namespace correctly for all three ConfigMap flags.
//
// The fix (Option B) calls Complete before Validate in NewCommand so that o.Namespace is
// populated from f.Namespace() before VerifyJSONConfigs runs. Tests mirror that order by
// calling Complete before Validate.
func TestValidateConfigMapsUseFactoryNamespace(t *testing.T) {
const targetNS = "tenant-b"
const defaultNS = "default"
// Shared options that satisfy every other validation gate:
// - NoDefaultBackupLocation=true + UseVolumeSnapshots=false skips provider/bucket/plugins checks
// - NoSecret=true satisfies the secret-file check
baseOptions := func() *Options {
o := NewInstallOptions()
o.NoDefaultBackupLocation = true
o.UseVolumeSnapshots = false
o.NoSecret = true
return o
}
tests := []struct {
name string
setupOpts func(o *Options, cmName string)
cmJSON string
wantErrMsg string // substring expected in error; empty means success
}{
{
name: "NodeAgentConfigMap found in factory namespace",
setupOpts: func(o *Options, cmName string) {
o.NodeAgentConfigMap = cmName
},
cmJSON: `{}`,
},
{
name: "NodeAgentConfigMap not found when only in default namespace",
setupOpts: func(o *Options, cmName string) {
o.NodeAgentConfigMap = cmName
},
cmJSON: `{}`,
wantErrMsg: "--node-agent-configmap specified ConfigMap",
},
{
name: "RepoMaintenanceJobConfigMap found in factory namespace",
setupOpts: func(o *Options, cmName string) {
o.RepoMaintenanceJobConfigMap = cmName
},
cmJSON: `{}`,
},
{
name: "RepoMaintenanceJobConfigMap not found when only in default namespace",
setupOpts: func(o *Options, cmName string) {
o.RepoMaintenanceJobConfigMap = cmName
},
cmJSON: `{}`,
wantErrMsg: "--repo-maintenance-job-configmap specified ConfigMap",
},
{
name: "BackupRepoConfigMap found in factory namespace",
setupOpts: func(o *Options, cmName string) {
o.BackupRepoConfigMap = cmName
},
cmJSON: `{}`,
},
{
name: "BackupRepoConfigMap not found when only in default namespace",
setupOpts: func(o *Options, cmName string) {
o.BackupRepoConfigMap = cmName
},
cmJSON: `{}`,
wantErrMsg: "--backup-repository-configmap specified ConfigMap",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
const cmName = "my-config"
// Decide where to place the ConfigMap:
// "not found" cases put it in "default", so the factory namespace lookup misses it.
cmNamespace := targetNS
if tc.wantErrMsg != "" {
cmNamespace = defaultNS
}
cm := configMapInNamespace(cmNamespace, cmName, tc.cmJSON)
kbClient := velerotest.NewFakeControllerRuntimeClient(t, cm)
f := &factorymocks.Factory{}
f.On("Namespace").Return(targetNS)
f.On("KubebuilderClient").Return(kbClient, nil)
o := baseOptions()
tc.setupOpts(o, cmName)
// Mirror the NewCommand call order: Complete populates o.Namespace before Validate runs.
require.NoError(t, o.Complete([]string{}, f))
c := makeValidateCmd()
c.SetContext(context.Background())
err := o.Validate(c, []string{}, f)
if tc.wantErrMsg == "" {
require.NoError(t, err)
} else {
require.Error(t, err)
assert.Contains(t, err.Error(), tc.wantErrMsg)
}
})
}
}
// TestNewCommandRunClosureOrder covers the Run closure in NewCommand (the lines that were
// reordered by the fix: Complete → Validate → Run).
//
// The closure uses CheckError which calls os.Exit on any error, so the only safe path is one
// where all three steps return nil. DryRun=true causes o.Run to return after PrintWithFormat
// (which is a no-op when no --output flag is set) without touching any cluster clients.
func TestNewCommandRunClosureOrder(t *testing.T) {
const targetNS = "tenant-b"
const cmName = "my-config"
cm := configMapInNamespace(targetNS, cmName, `{}`)
kbClient := velerotest.NewFakeControllerRuntimeClient(t, cm)
f := &factorymocks.Factory{}
f.On("Namespace").Return(targetNS)
f.On("KubebuilderClient").Return(kbClient, nil)
c := NewCommand(f)
c.SetArgs([]string{
"--no-default-backup-location",
"--use-volume-snapshots=false",
"--no-secret",
"--dry-run",
"--node-agent-configmap", cmName,
})
// Execute drives the full Run closure: Complete populates o.Namespace, Validate
// looks up the ConfigMap in targetNS (succeeds), Run returns early via DryRun.
require.NoError(t, c.Execute())
}
+19 -1
View File
@@ -323,7 +323,25 @@ func (s *nodeAgentServer) run() {
podResources := corev1api.ResourceRequirements{}
if s.dataPathConfigs != nil && s.dataPathConfigs.PodResources != nil {
if res, err := kube.ParseResourceRequirements(s.dataPathConfigs.PodResources.CPURequest, s.dataPathConfigs.PodResources.MemoryRequest, s.dataPathConfigs.PodResources.CPULimit, s.dataPathConfigs.PodResources.MemoryLimit); err != nil {
// To make the PodResources ConfigMap without ephemeral storage request/limit backward compatible,
// need to avoid set value as empty, because empty string will cause parsing error.
ephemeralStorageRequest := constant.DefaultEphemeralStorageRequest
if s.dataPathConfigs.PodResources.EphemeralStorageRequest != "" {
ephemeralStorageRequest = s.dataPathConfigs.PodResources.EphemeralStorageRequest
}
ephemeralStorageLimit := constant.DefaultEphemeralStorageLimit
if s.dataPathConfigs.PodResources.EphemeralStorageLimit != "" {
ephemeralStorageLimit = s.dataPathConfigs.PodResources.EphemeralStorageLimit
}
if res, err := kube.ParseResourceRequirements(
s.dataPathConfigs.PodResources.CPURequest,
s.dataPathConfigs.PodResources.MemoryRequest,
ephemeralStorageRequest,
s.dataPathConfigs.PodResources.CPULimit,
s.dataPathConfigs.PodResources.MemoryLimit,
ephemeralStorageLimit,
); err != nil {
s.logger.WithError(err).Warn("Pod resource requirements are invalid, ignore")
} else {
podResources = res
+2 -2
View File
@@ -27,7 +27,7 @@ import (
"time"
logrusr "github.com/bombsimon/logrusr/v3"
volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1"
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/prometheus/client_golang/prometheus/promhttp"
@@ -247,7 +247,7 @@ func newServer(f client.Factory, config *config.Config, logger *logrus.Logger) (
cancelFunc()
return nil, err
}
if err := volumegroupsnapshotv1beta1.AddToScheme(scheme); err != nil {
if err := volumegroupsnapshotv1beta2.AddToScheme(scheme); err != nil {
cancelFunc()
return nil, err
}
+2 -2
View File
@@ -204,9 +204,9 @@ func Test_newServer(t *testing.T) {
}, logger)
require.Error(t, err)
// invalid clientQPS Restic uploader
// invalid clientQPS Kopia uploader
_, err = newServer(factory, &config.Config{
UploaderType: uploader.ResticType,
UploaderType: uploader.KopiaType,
ClientQPS: -1,
}, logger)
require.Error(t, err)
+3
View File
@@ -23,4 +23,7 @@ const (
PluginCSIPVCRestoreRIA = "velero.io/csi-pvc-restorer"
PluginCsiVolumeSnapshotRestoreRIA = "velero.io/csi-volumesnapshot-restorer"
DefaultEphemeralStorageRequest = "0"
DefaultEphemeralStorageLimit = "0"
)
@@ -821,12 +821,12 @@ func TestGetSnapshotsInBackup(t *testing.T) {
{
VolumeNamespace: "ns-1",
SnapshotID: "snap-3",
RepositoryType: "restic",
RepositoryType: "kopia",
},
{
VolumeNamespace: "ns-1",
SnapshotID: "snap-4",
RepositoryType: "restic",
RepositoryType: "kopia",
},
},
},
@@ -876,7 +876,7 @@ func TestGetSnapshotsInBackup(t *testing.T) {
{
VolumeNamespace: "ns-1",
SnapshotID: "snap-3",
RepositoryType: "restic",
RepositoryType: "kopia",
},
},
},
+15 -57
View File
@@ -43,7 +43,6 @@ import (
"github.com/vmware-tanzu/velero/pkg/constant"
"github.com/vmware-tanzu/velero/pkg/label"
"github.com/vmware-tanzu/velero/pkg/metrics"
repoconfig "github.com/vmware-tanzu/velero/pkg/repository/config"
"github.com/vmware-tanzu/velero/pkg/repository/maintenance"
repomanager "github.com/vmware-tanzu/velero/pkg/repository/manager"
"github.com/vmware-tanzu/velero/pkg/util/kube"
@@ -53,9 +52,11 @@ import (
const (
repoSyncPeriod = 5 * time.Minute
defaultMaintainFrequency = 7 * 24 * time.Hour
defaultMaintenanceStatusQueueLength = 3
defaultMaintenanceStatusQueueLength = 25
)
var maintenanceStatusQueueLength = defaultMaintenanceStatusQueueLength
type BackupRepoReconciler struct {
client.Client
namespace string
@@ -238,6 +239,10 @@ func (r *BackupRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
return ctrl.Result{}, err
}
if backupRepo.Spec.RepositoryType != velerov1api.BackupRepositoryTypeKopia {
return ctrl.Result{}, nil
}
bsl, bslErr := r.getBSL(ctx, backupRepo)
if bslErr != nil {
log.WithError(bslErr).Error("Fail to get BSL for BackupRepository. Skip reconciling.")
@@ -245,7 +250,7 @@ func (r *BackupRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
}
if backupRepo.Status.Phase == "" || backupRepo.Status.Phase == velerov1api.BackupRepositoryPhaseNew {
if err := r.initializeRepo(ctx, backupRepo, bsl, log); err != nil {
if err := r.initializeRepo(ctx, backupRepo, log); err != nil {
log.WithError(err).Error("error initialize repository")
return ctrl.Result{}, errors.WithStack(err)
}
@@ -263,7 +268,7 @@ func (r *BackupRepoReconciler) Reconcile(ctx context.Context, req ctrl.Request)
switch backupRepo.Status.Phase {
case velerov1api.BackupRepositoryPhaseNotReady:
ready, err := r.checkNotReadyRepo(ctx, backupRepo, bsl, log)
ready, err := r.checkNotReadyRepo(ctx, backupRepo, log)
if err != nil {
return ctrl.Result{}, err
} else if !ready {
@@ -311,35 +316,9 @@ func (r *BackupRepoReconciler) getBSL(ctx context.Context, req *velerov1api.Back
return loc, nil
}
func (r *BackupRepoReconciler) getIdentifierByBSL(bsl *velerov1api.BackupStorageLocation, req *velerov1api.BackupRepository) (string, error) {
repoIdentifier, err := repoconfig.GetRepoIdentifier(bsl, req.Spec.VolumeNamespace)
if err != nil {
return "", errors.Wrapf(err, "error to get identifier for repo %s", req.Name)
}
return repoIdentifier, nil
}
func (r *BackupRepoReconciler) initializeRepo(ctx context.Context, req *velerov1api.BackupRepository, bsl *velerov1api.BackupStorageLocation, log logrus.FieldLogger) error {
func (r *BackupRepoReconciler) initializeRepo(ctx context.Context, req *velerov1api.BackupRepository, log logrus.FieldLogger) error {
log.WithField("repoConfig", r.backupRepoConfig).Info("Initializing backup repository")
var repoIdentifier string
// Only get restic identifier for restic repositories
if req.Spec.RepositoryType == "" || req.Spec.RepositoryType == velerov1api.BackupRepositoryTypeRestic {
var err error
repoIdentifier, err = r.getIdentifierByBSL(bsl, req)
if err != nil {
return r.patchBackupRepository(ctx, req, func(rr *velerov1api.BackupRepository) {
rr.Status.Message = err.Error()
rr.Status.Phase = velerov1api.BackupRepositoryPhaseNotReady
if rr.Spec.MaintenanceFrequency.Duration <= 0 {
rr.Spec.MaintenanceFrequency = metav1.Duration{Duration: r.getRepositoryMaintenanceFrequency(req)}
}
})
}
}
config, err := getBackupRepositoryConfig(ctx, r, r.backupRepoConfig, r.namespace, req.Name, req.Spec.RepositoryType, log)
if err != nil {
log.WithError(err).Warn("Failed to get repo config, repo config is ignored")
@@ -349,11 +328,6 @@ func (r *BackupRepoReconciler) initializeRepo(ctx context.Context, req *velerov1
// defaulting - if the patch fails, return an error so the item is returned to the queue
if err := r.patchBackupRepository(ctx, req, func(rr *velerov1api.BackupRepository) {
// Only set ResticIdentifier for restic repositories
if rr.Spec.RepositoryType == "" || rr.Spec.RepositoryType == velerov1api.BackupRepositoryTypeRestic {
rr.Spec.ResticIdentifier = repoIdentifier
}
if rr.Spec.MaintenanceFrequency.Duration <= 0 {
rr.Spec.MaintenanceFrequency = metav1.Duration{Duration: r.getRepositoryMaintenanceFrequency(req)}
}
@@ -397,7 +371,7 @@ func ensureRepo(repo *velerov1api.BackupRepository, repoManager repomanager.Mana
}
func (r *BackupRepoReconciler) recallMaintenance(ctx context.Context, req *velerov1api.BackupRepository, log logrus.FieldLogger) error {
history, err := maintenance.WaitAllJobsComplete(ctx, r.Client, req, defaultMaintenanceStatusQueueLength, log)
history, err := maintenance.WaitAllJobsComplete(ctx, r.Client, req, maintenanceStatusQueueLength, log)
if err != nil {
return errors.Wrapf(err, "error waiting incomplete repo maintenance job for repo %s", req.Name)
}
@@ -455,7 +429,7 @@ func consolidateHistory(coming, cur []velerov1api.BackupRepositoryMaintenanceSta
truncated := []velerov1api.BackupRepositoryMaintenanceStatus{}
for consolidator.Len() > 0 {
if len(truncated) == defaultMaintenanceStatusQueueLength {
if len(truncated) == maintenanceStatusQueueLength {
break
}
@@ -565,8 +539,8 @@ func updateRepoMaintenanceHistory(repo *velerov1api.BackupRepository, result vel
}
startingPos := 0
if len(repo.Status.RecentMaintenance) >= defaultMaintenanceStatusQueueLength {
startingPos = len(repo.Status.RecentMaintenance) - defaultMaintenanceStatusQueueLength + 1
if len(repo.Status.RecentMaintenance) >= maintenanceStatusQueueLength {
startingPos = len(repo.Status.RecentMaintenance) - maintenanceStatusQueueLength + 1
}
repo.Status.RecentMaintenance = append(repo.Status.RecentMaintenance[startingPos:], latest)
@@ -576,25 +550,9 @@ func dueForMaintenance(req *velerov1api.BackupRepository, now time.Time) bool {
return req.Status.LastMaintenanceTime == nil || req.Status.LastMaintenanceTime.Add(req.Spec.MaintenanceFrequency.Duration).Before(now)
}
func (r *BackupRepoReconciler) checkNotReadyRepo(ctx context.Context, req *velerov1api.BackupRepository, bsl *velerov1api.BackupStorageLocation, log logrus.FieldLogger) (bool, error) {
func (r *BackupRepoReconciler) checkNotReadyRepo(ctx context.Context, req *velerov1api.BackupRepository, log logrus.FieldLogger) (bool, error) {
log.Info("Checking backup repository for readiness")
// Only check and update restic identifier for restic repositories
if req.Spec.RepositoryType == "" || req.Spec.RepositoryType == velerov1api.BackupRepositoryTypeRestic {
repoIdentifier, err := r.getIdentifierByBSL(bsl, req)
if err != nil {
return false, r.patchBackupRepository(ctx, req, repoNotReady(err.Error()))
}
if repoIdentifier != req.Spec.ResticIdentifier {
if err := r.patchBackupRepository(ctx, req, func(rr *velerov1api.BackupRepository) {
rr.Spec.ResticIdentifier = repoIdentifier
}); err != nil {
return false, err
}
}
}
// we need to ensure it (first check, if check fails, attempt to init)
// because we don't know if it's been successfully initialized yet.
if err := ensureRepo(req, r.repositoryManager); err != nil {
@@ -98,32 +98,6 @@ func TestPatchBackupRepository(t *testing.T) {
}
func TestCheckNotReadyRepo(t *testing.T) {
// Test for restic repository
t.Run("restic repository", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.ResticIdentifier = "fake-identifier"
rr.Spec.VolumeNamespace = "volume-ns-1"
rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeRestic
reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
err := reconciler.Client.Create(t.Context(), rr)
require.NoError(t, err)
location := velerov1api.BackupStorageLocation{
Spec: velerov1api.BackupStorageLocationSpec{
Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"},
},
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: rr.Spec.BackupStorageLocation,
},
}
_, err = reconciler.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
require.NoError(t, err)
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
assert.Equal(t, "s3:test.amazonaws.com/bucket/restic/volume-ns-1", rr.Spec.ResticIdentifier)
})
// Test for kopia repository
t.Run("kopia repository", func(t *testing.T) {
rr := mockBackupRepositoryCR()
@@ -133,48 +107,13 @@ func TestCheckNotReadyRepo(t *testing.T) {
reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
err := reconciler.Client.Create(t.Context(), rr)
require.NoError(t, err)
location := velerov1api.BackupStorageLocation{
Spec: velerov1api.BackupStorageLocationSpec{
Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"},
},
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: rr.Spec.BackupStorageLocation,
},
}
_, err = reconciler.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
_, err = reconciler.checkNotReadyRepo(t.Context(), rr, reconciler.logger)
require.NoError(t, err)
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
// ResticIdentifier should remain empty for kopia
assert.Empty(t, rr.Spec.ResticIdentifier)
})
// Test for empty repository type (defaults to restic)
t.Run("empty repository type", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.ResticIdentifier = "fake-identifier"
rr.Spec.VolumeNamespace = "volume-ns-1"
// Deliberately leave RepositoryType empty
reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
err := reconciler.Client.Create(t.Context(), rr)
require.NoError(t, err)
location := velerov1api.BackupStorageLocation{
Spec: velerov1api.BackupStorageLocationSpec{
Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"},
},
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: rr.Spec.BackupStorageLocation,
},
}
_, err = reconciler.checkNotReadyRepo(t.Context(), rr, &location, reconciler.logger)
require.NoError(t, err)
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
assert.Equal(t, "s3:test.amazonaws.com/bucket/restic/volume-ns-1", rr.Spec.ResticIdentifier)
})
}
func startMaintenanceJobFail(client.Client, context.Context, *velerov1api.BackupRepository, string, logrus.Level, *logging.FormatFlag, logrus.FieldLogger) (string, error) {
@@ -463,17 +402,8 @@ func TestInitializeRepo(t *testing.T) {
reconciler := mockBackupRepoReconciler(t, "PrepareRepo", rr, nil)
err := reconciler.Client.Create(t.Context(), rr)
require.NoError(t, err)
location := velerov1api.BackupStorageLocation{
Spec: velerov1api.BackupStorageLocationSpec{
Config: map[string]string{"resticRepoPrefix": "s3:test.amazonaws.com/bucket/restic"},
},
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: rr.Spec.BackupStorageLocation,
},
}
err = reconciler.initializeRepo(t.Context(), rr, &location, reconciler.logger)
err = reconciler.initializeRepo(t.Context(), rr, reconciler.logger)
require.NoError(t, err)
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
}
@@ -999,6 +929,8 @@ func TestUpdateRepoMaintenanceHistory(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
maintenanceStatusQueueLength = 3
updateRepoMaintenanceHistory(test.backupRepo, test.result, &metav1.Time{Time: standardTime}, &metav1.Time{Time: standardTime.Add(time.Hour)}, "fake-message-0")
for at := range test.backupRepo.Status.RecentMaintenance {
@@ -1494,7 +1426,7 @@ func TestDeleteOldMaintenanceJobWithConfigMap(t *testing.T) {
MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency},
BackupStorageLocation: "default",
VolumeNamespace: "test-ns",
RepositoryType: "restic",
RepositoryType: "kopia",
},
Status: velerov1api.BackupRepositoryStatus{
Phase: velerov1api.BackupRepositoryPhaseReady,
@@ -1531,7 +1463,7 @@ func TestDeleteOldMaintenanceJobWithConfigMap(t *testing.T) {
MaintenanceFrequency: metav1.Duration{Duration: testMaintenanceFrequency},
BackupStorageLocation: "default",
VolumeNamespace: "test-ns",
RepositoryType: "restic",
RepositoryType: "kopia",
},
Status: velerov1api.BackupRepositoryStatus{
Phase: velerov1api.BackupRepositoryPhaseReady,
@@ -1550,8 +1482,8 @@ func TestDeleteOldMaintenanceJobWithConfigMap(t *testing.T) {
Name: "repo-maintenance-job-config",
},
Data: map[string]string{
"global": `{"keepLatestMaintenanceJobs": 5}`,
"test-ns-default-restic": `{"keepLatestMaintenanceJobs": 2}`,
"global": `{"keepLatestMaintenanceJobs": 5}`,
"test-ns-default-kopia": `{"keepLatestMaintenanceJobs": 2}`,
},
},
},
@@ -1605,58 +1537,6 @@ func TestInitializeRepoWithRepositoryTypes(t *testing.T) {
corev1api.AddToScheme(scheme)
velerov1api.AddToScheme(scheme)
// Test for restic repository
t.Run("restic repository", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.VolumeNamespace = "volume-ns-1"
rr.Spec.RepositoryType = velerov1api.BackupRepositoryTypeRestic
location := &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "default",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
Prefix: "test-prefix",
},
},
Config: map[string]string{
"region": "us-east-1",
},
},
}
fakeClient := clientFake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(rr, location).Build()
mgr := &repomokes.Manager{}
mgr.On("PrepareRepo", rr).Return(nil)
reconciler := NewBackupRepoReconciler(
velerov1api.DefaultNamespace,
velerotest.NewLogger(),
fakeClient,
mgr,
testMaintenanceFrequency,
"",
"",
logrus.InfoLevel,
nil,
nil,
)
err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
require.NoError(t, err)
// Verify ResticIdentifier is set for restic
assert.NotEmpty(t, rr.Spec.ResticIdentifier)
assert.Contains(t, rr.Spec.ResticIdentifier, "volume-ns-1")
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
})
// Test for kopia repository
t.Run("kopia repository", func(t *testing.T) {
rr := mockBackupRepositoryCR()
@@ -1700,65 +1580,13 @@ func TestInitializeRepoWithRepositoryTypes(t *testing.T) {
nil,
)
err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
err := reconciler.initializeRepo(t.Context(), rr, reconciler.logger)
require.NoError(t, err)
// Verify ResticIdentifier is NOT set for kopia
assert.Empty(t, rr.Spec.ResticIdentifier)
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
})
// Test for empty repository type (defaults to restic)
t.Run("empty repository type", func(t *testing.T) {
rr := mockBackupRepositoryCR()
rr.Spec.BackupStorageLocation = "default"
rr.Spec.VolumeNamespace = "volume-ns-1"
// Leave RepositoryType empty
location := &velerov1api.BackupStorageLocation{
ObjectMeta: metav1.ObjectMeta{
Namespace: velerov1api.DefaultNamespace,
Name: "default",
},
Spec: velerov1api.BackupStorageLocationSpec{
Provider: "aws",
StorageType: velerov1api.StorageType{
ObjectStorage: &velerov1api.ObjectStorageLocation{
Bucket: "test-bucket",
Prefix: "test-prefix",
},
},
Config: map[string]string{
"region": "us-east-1",
},
},
}
fakeClient := clientFake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(rr, location).Build()
mgr := &repomokes.Manager{}
mgr.On("PrepareRepo", rr).Return(nil)
reconciler := NewBackupRepoReconciler(
velerov1api.DefaultNamespace,
velerotest.NewLogger(),
fakeClient,
mgr,
testMaintenanceFrequency,
"",
"",
logrus.InfoLevel,
nil,
nil,
)
err := reconciler.initializeRepo(t.Context(), rr, location, reconciler.logger)
require.NoError(t, err)
// Verify ResticIdentifier is set when type is empty (defaults to restic)
assert.NotEmpty(t, rr.Spec.ResticIdentifier)
assert.Contains(t, rr.Spec.ResticIdentifier, "volume-ns-1")
assert.Equal(t, velerov1api.BackupRepositoryPhaseReady, rr.Status.Phase)
})
}
func TestRepoMaintenanceMetricsRecording(t *testing.T) {
@@ -360,5 +360,5 @@ func (c *PodVolumeRestoreReconcilerLegacy) closeDataPath(ctx context.Context, pv
}
func IsLegacyPVR(pvr *velerov1api.PodVolumeRestore) bool {
return pvr.Spec.UploaderType == uploader.ResticType
return pvr.Spec.UploaderType == "restic"
}
+1
View File
@@ -529,6 +529,7 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu
LabelSelector: labels.Set(map[string]string{
api.BackupNameLabel: label.GetValidName(restore.Spec.BackupName),
}).AsSelector(),
Namespace: restore.Namespace,
}
podVolumeBackupList := &api.PodVolumeBackupList{}
+33 -2
View File
@@ -238,6 +238,8 @@ func TestRestoreReconcile(t *testing.T) {
expectedFinalPhase string
addValidFinalizer bool
emptyVolumeInfo bool
podVolumeBackups []*velerov1api.PodVolumeBackup
expectedPVBCount int
}{
{
name: "restore with both namespace in both includedNamespaces and excludedNamespaces fails validation",
@@ -357,6 +359,22 @@ func TestRestoreReconcile(t *testing.T) {
expectedCompletedTime: &timestamp,
expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(),
},
{
name: "valid restore gets executed and only includes pod volume backups from restore namespace",
location: defaultStorageLocation,
restore: NewRestore("foo", "bar2", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(),
backup: defaultBackup().StorageLocation("default").Result(),
podVolumeBackups: []*velerov1api.PodVolumeBackup{
builder.ForPodVolumeBackup("foo", "pvb-1").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(),
builder.ForPodVolumeBackup("other-ns", "pvb-2").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(),
},
expectedPVBCount: 1,
expectedErr: false,
expectedPhase: string(velerov1api.RestorePhaseInProgress),
expectedStartTime: &timestamp,
expectedCompletedTime: &timestamp,
expectedRestorerCall: NewRestore("foo", "bar2", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(),
},
{
name: "restoration of nodes is not supported",
location: defaultStorageLocation,
@@ -501,6 +519,13 @@ func TestRestoreReconcile(t *testing.T) {
defaultStorageLocation.ObjectMeta.ResourceVersion = ""
}()
if test.podVolumeBackups != nil {
for _, pvb := range test.podVolumeBackups {
err := fakeClient.Create(t.Context(), pvb)
require.NoError(t, err)
}
}
r := NewRestoreReconciler(
t.Context(),
velerov1api.DefaultNamespace,
@@ -670,6 +695,10 @@ func TestRestoreReconcile(t *testing.T) {
// the mock stores the pointer, which gets modified after
assert.Equal(t, test.expectedRestorerCall.Spec, restorer.calledWithArg.Spec)
assert.Equal(t, test.expectedRestorerCall.Status.Phase, restorer.calledWithArg.Status.Phase)
if test.podVolumeBackups != nil {
assert.Len(t, restorer.calledWithPVBs, test.expectedPVBCount)
}
})
}
}
@@ -1021,8 +1050,9 @@ func NewRestore(ns, name, backup, includeNS, includeResource string, phase veler
type fakeRestorer struct {
mock.Mock
calledWithArg velerov1api.Restore
kbClient client.Client
calledWithArg velerov1api.Restore
calledWithPVBs []*velerov1api.PodVolumeBackup
kbClient client.Client
}
func (r *fakeRestorer) Restore(
@@ -1045,6 +1075,7 @@ func (r *fakeRestorer) RestoreWithResolvers(req *pkgrestore.Request,
r.kbClient, volumeSnapshotterGetter)
r.calledWithArg = *req.Restore
r.calledWithPVBs = req.PodVolumeBackups
return res.Get(0).(results.Result), res.Get(1).(results.Result)
}
+94 -1
View File
@@ -22,6 +22,8 @@ import (
"sync"
"time"
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
@@ -43,6 +45,7 @@ import (
"github.com/vmware-tanzu/velero/pkg/persistence"
"github.com/vmware-tanzu/velero/pkg/plugin/clientmgmt"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
kubeutil "github.com/vmware-tanzu/velero/pkg/util/kube"
"github.com/vmware-tanzu/velero/pkg/util/results"
)
@@ -291,13 +294,16 @@ type finalizerContext struct {
resourceTimeout time.Duration
}
func (ctx *finalizerContext) execute() (results.Result, results.Result) { //nolint:unparam //temporarily ignore the lint report: result 0 is always nil (unparam)
func (ctx *finalizerContext) execute() (results.Result, results.Result) {
warnings, errs := results.Result{}, results.Result{}
// implement finalization tasks
pdpErrs := ctx.patchDynamicPVWithVolumeInfo()
errs.Merge(&pdpErrs)
vgscWarnings := ctx.cleanupStubVGSC()
warnings.Merge(&vgscWarnings)
rehErrs := ctx.WaitRestoreExecHook()
errs.Merge(&rehErrs)
@@ -443,6 +449,93 @@ func (ctx *finalizerContext) patchDynamicPVWithVolumeInfo() (errs results.Result
return errs
}
// cleanupStubVGSC deletes stub VolumeGroupSnapshotContent objects that were
// created during restore to satisfy CSI controller validation. These stubs are
// labeled with velero.io/restore-name for identification.
// Before deleting each VGSC, it waits for all related VolumeSnapshotContents
// to become ReadyToUse, since the CSI controller needs the VGSC during VSC reconciliation.
func (ctx *finalizerContext) cleanupStubVGSC() (warnings results.Result) {
ctx.logger.Info("cleaning up stub VolumeGroupSnapshotContents")
vgscList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentList{}
err := ctx.crClient.List(
context.Background(),
vgscList,
client.MatchingLabels{velerov1api.RestoreNameLabel: ctx.restore.Name},
)
if err != nil {
// If the CRD is not installed, listing will fail. This is expected
// on clusters without VolumeGroupSnapshot support, so treat as warning.
ctx.logger.WithError(err).Warn("failed to list stub VolumeGroupSnapshotContents, skipping cleanup")
warnings.Add("cluster", errors.Wrap(err, "failed to list stub VolumeGroupSnapshotContents"))
return warnings
}
if len(vgscList.Items) == 0 {
ctx.logger.Info("no stub VolumeGroupSnapshotContents to clean up")
return warnings
}
for i := range vgscList.Items {
vgsc := &vgscList.Items[i]
log := ctx.logger.WithField("vgsc", vgsc.Name)
// Collect the snapshot handles associated with this VGSC
snapshotHandles := map[string]bool{}
if vgsc.Spec.Source.GroupSnapshotHandles != nil {
for _, h := range vgsc.Spec.Source.GroupSnapshotHandles.VolumeSnapshotHandles {
snapshotHandles[h] = true
}
}
if len(snapshotHandles) > 0 {
// Wait for related VSCs to become ReadyToUse before deleting the VGSC
log.Infof("waiting for %d related VolumeSnapshotContents to become ReadyToUse", len(snapshotHandles))
err := wait.PollUntilContextTimeout(context.Background(), 10*time.Second, ctx.resourceTimeout, true, func(context.Context) (bool, error) {
vscList := &snapshotv1api.VolumeSnapshotContentList{}
if err := ctx.crClient.List(context.Background(), vscList, client.MatchingLabels{velerov1api.RestoreNameLabel: ctx.restore.Name}); err != nil {
log.WithError(err).Warn("failed to list VolumeSnapshotContents")
return false, nil
}
for j := range vscList.Items {
vsc := &vscList.Items[j]
if vsc.Spec.Source.SnapshotHandle == nil {
continue
}
if !snapshotHandles[*vsc.Spec.Source.SnapshotHandle] {
continue
}
// This VSC is related to our VGSC
if vsc.Status == nil || !boolptr.IsSetToTrue(vsc.Status.ReadyToUse) {
log.Debugf("VolumeSnapshotContent %s not yet ReadyToUse", vsc.Name)
return false, nil
}
}
return true, nil
})
if err != nil {
log.WithError(err).Warn("timed out waiting for related VolumeSnapshotContents to become ReadyToUse, proceeding with VGSC deletion")
warnings.Add("cluster", errors.Wrapf(err, "timed out waiting for VSCs related to VGSC %s", vgsc.Name))
}
}
log.Info("deleting stub VolumeGroupSnapshotContent")
if err := ctx.crClient.Delete(context.Background(), vgsc); err != nil {
if apierrors.IsNotFound(err) {
log.Info("stub VolumeGroupSnapshotContent already deleted")
continue
}
log.WithError(err).Warn("failed to delete stub VolumeGroupSnapshotContent")
warnings.Add("cluster", errors.Wrapf(err, "failed to delete stub VolumeGroupSnapshotContent %s", vgsc.Name))
} else {
log.Info("deleted stub VolumeGroupSnapshotContent")
}
}
return warnings
}
func needPatch(newPV *corev1api.PersistentVolume, pvInfo *volume.PVInfo) bool {
if newPV.Spec.PersistentVolumeReclaimPolicy != corev1api.PersistentVolumeReclaimPolicy(pvInfo.ReclaimPolicy) {
return true
@@ -22,6 +22,8 @@ import (
"testing"
"time"
volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2"
snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
@@ -45,6 +47,7 @@ import (
pluginmocks "github.com/vmware-tanzu/velero/pkg/plugin/mocks"
"github.com/vmware-tanzu/velero/pkg/plugin/velero"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
pkgUtilKubeMocks "github.com/vmware-tanzu/velero/pkg/util/kube/mocks"
"github.com/vmware-tanzu/velero/pkg/util/results"
)
@@ -739,3 +742,253 @@ func TestRestoreOperationList(t *testing.T) {
})
}
}
func TestCleanupStubVGSC(t *testing.T) {
snapshotHandle1 := "snap-handle-1"
snapshotHandle2 := "snap-handle-2"
tests := []struct {
name string
restore *velerov1api.Restore
existingVGSCs []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent
existingVSCs []*snapshotv1api.VolumeSnapshotContent
expectedRemaining int
expectedWarnings bool
}{
{
name: "no stub VGSCs to clean up",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(),
existingVGSCs: nil,
expectedRemaining: 0,
expectedWarnings: false,
},
{
name: "single stub VGSC deleted after VSCs are ready",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(),
existingVGSCs: []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{
{
ObjectMeta: metav1.ObjectMeta{
Name: "vgsc-stub-1",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-1",
},
},
Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{
GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{
VolumeGroupSnapshotHandle: "vgs-handle-1",
VolumeSnapshotHandles: []string{snapshotHandle1},
},
},
},
},
},
existingVSCs: []*snapshotv1api.VolumeSnapshotContent{
{
ObjectMeta: metav1.ObjectMeta{
Name: "vsc-1",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-1",
},
},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain,
Source: snapshotv1api.VolumeSnapshotContentSource{
SnapshotHandle: &snapshotHandle1,
},
VolumeSnapshotRef: corev1api.ObjectReference{
Name: "vs-1",
Namespace: "ns-1",
},
},
Status: &snapshotv1api.VolumeSnapshotContentStatus{
ReadyToUse: boolptr.True(),
},
},
},
expectedRemaining: 0,
expectedWarnings: false,
},
{
name: "multiple stub VGSCs deleted",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(),
existingVGSCs: []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{
{
ObjectMeta: metav1.ObjectMeta{
Name: "vgsc-stub-1",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-1",
},
},
Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{
GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{
VolumeGroupSnapshotHandle: "vgs-handle-1",
VolumeSnapshotHandles: []string{snapshotHandle1},
},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "vgsc-stub-2",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-1",
},
},
Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{
GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{
VolumeGroupSnapshotHandle: "vgs-handle-2",
VolumeSnapshotHandles: []string{snapshotHandle2},
},
},
},
},
},
existingVSCs: []*snapshotv1api.VolumeSnapshotContent{
{
ObjectMeta: metav1.ObjectMeta{
Name: "vsc-1",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-1",
},
},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain,
Source: snapshotv1api.VolumeSnapshotContentSource{
SnapshotHandle: &snapshotHandle1,
},
VolumeSnapshotRef: corev1api.ObjectReference{
Name: "vs-1",
Namespace: "ns-1",
},
},
Status: &snapshotv1api.VolumeSnapshotContentStatus{
ReadyToUse: boolptr.True(),
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "vsc-2",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-1",
},
},
Spec: snapshotv1api.VolumeSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain,
Source: snapshotv1api.VolumeSnapshotContentSource{
SnapshotHandle: &snapshotHandle2,
},
VolumeSnapshotRef: corev1api.ObjectReference{
Name: "vs-2",
Namespace: "ns-1",
},
},
Status: &snapshotv1api.VolumeSnapshotContentStatus{
ReadyToUse: boolptr.True(),
},
},
},
expectedRemaining: 0,
expectedWarnings: false,
},
{
name: "VGSCs from different restore are not deleted",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(),
existingVGSCs: []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{
{
ObjectMeta: metav1.ObjectMeta{
Name: "vgsc-stub-mine",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-1",
},
},
Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "vgsc-stub-other",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-2",
},
},
Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{},
},
},
},
expectedRemaining: 1,
expectedWarnings: false,
},
{
name: "VGSC deleted even when no snapshot handles in spec",
restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(),
existingVGSCs: []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{
{
ObjectMeta: metav1.ObjectMeta{
Name: "vgsc-stub-empty",
Labels: map[string]string{
velerov1api.RestoreNameLabel: "restore-1",
},
},
Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{
Driver: "rbd.csi.ceph.com",
Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{},
},
},
},
expectedRemaining: 0,
expectedWarnings: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fakeClient := velerotest.NewFakeControllerRuntimeClientBuilder(t).Build()
logger := velerotest.NewLogger()
ctx := &finalizerContext{
logger: logger,
crClient: fakeClient,
restore: tc.restore,
resourceTimeout: 10 * time.Second,
}
for _, vgsc := range tc.existingVGSCs {
require.NoError(t, fakeClient.Create(t.Context(), vgsc))
}
for _, vsc := range tc.existingVSCs {
require.NoError(t, fakeClient.Create(t.Context(), vsc))
}
warnings := ctx.cleanupStubVGSC()
if tc.expectedWarnings {
assert.False(t, warnings.IsEmpty())
} else {
assert.True(t, warnings.IsEmpty(), "expected no warnings")
}
remainingList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentList{}
require.NoError(t, fakeClient.List(t.Context(), remainingList))
assert.Len(t, remainingList.Items, tc.expectedRemaining)
// Verify remaining VGSCs don't belong to this restore
for _, remaining := range remainingList.Items {
assert.NotEqual(t, tc.restore.Name, remaining.Labels[velerov1api.RestoreNameLabel],
"VGSC %s should have been deleted", remaining.Name)
}
})
}
}
+7
View File
@@ -129,6 +129,13 @@ func (c *scheduleReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c
} else {
schedule.Status.Phase = velerov1.SchedulePhaseEnabled
schedule.Status.ValidationErrors = nil
// Compute expected interval between consecutive scheduled backup runs.
// Only meaningful when the cron expression is valid.
now := c.clock.Now()
nextRun := cronSchedule.Next(now)
nextNextRun := cronSchedule.Next(nextRun)
c.metrics.SetScheduleExpectedIntervalSeconds(schedule.Name, nextNextRun.Sub(nextRun).Seconds())
}
scheduleNeedsPatch := false
+1
View File
@@ -310,6 +310,7 @@ func (r *BackupMicroService) cancelDataUpload(du *velerov2alpha1api.DataUpload)
fsBackup := r.dataPathMgr.GetAsyncBR(du.Name)
if fsBackup == nil {
r.OnDataUploadCancelled(r.ctx, du.GetNamespace(), du.GetName())
r.eventRecorder.EndingEvent(du, false, datapath.EventReasonStopped, "Data path for %s exited without start", du.Name)
} else {
fsBackup.Cancel()
}
+2 -2
View File
@@ -259,8 +259,8 @@ func TestCancelDataUpload(t *testing.T) {
}{
{
name: "no fs backup",
expectedEventReason: datapath.EventReasonCancelled,
expectedEventMsg: "Data path for data upload fake-data-upload canceled",
expectedEventReason: datapath.EventReasonStopped,
expectedEventMsg: "Data path for fake-data-upload exited without start",
expectedErr: datapath.ErrCancelled,
},
}
+1
View File
@@ -288,6 +288,7 @@ func (r *RestoreMicroService) cancelDataDownload(dd *velerov2alpha1api.DataDownl
fsBackup := r.dataPathMgr.GetAsyncBR(dd.Name)
if fsBackup == nil {
r.OnDataDownloadCancelled(r.ctx, dd.GetNamespace(), dd.GetName())
r.eventRecorder.EndingEvent(dd, false, datapath.EventReasonStopped, "Data path for %s exited without start", dd.Name)
} else {
fsBackup.Cancel()
}
+2 -2
View File
@@ -203,8 +203,8 @@ func TestCancelDataDownload(t *testing.T) {
}{
{
name: "no fs restore",
expectedEventReason: datapath.EventReasonCancelled,
expectedEventMsg: "Data path for data download fake-data-download canceled",
expectedEventReason: datapath.EventReasonStopped,
expectedEventMsg: "Data path for fake-data-download exited without start",
expectedErr: datapath.ErrCancelled,
},
}
+3 -5
View File
@@ -251,11 +251,9 @@ func (fs *fileSystemBR) boostRepoConnect(ctx context.Context, repositoryType str
if err := repoProvider.NewUnifiedRepoProvider(*credentialGetter, repositoryType, fs.log).BoostRepoConnect(ctx, repoProvider.RepoParam{BackupLocation: fs.backupLocation, BackupRepo: fs.backupRepo, CacheDir: cacheDir}); err != nil {
return err
}
} else {
if err := repoProvider.NewResticRepositoryProvider(*credentialGetter, filesystem.NewFileSystem(), fs.log).BoostRepoConnect(ctx, repoProvider.RepoParam{BackupLocation: fs.backupLocation, BackupRepo: fs.backupRepo}); err != nil {
return err
}
return nil
}
return nil
return errors.Errorf("error getting provider for repo %s", repositoryType)
}
+30 -7
View File
@@ -124,6 +124,15 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
"owner": ownerObject.Name,
})
volumeTopology, err := kube.GetVolumeTopology(ctx, e.kubeClient.CoreV1(), e.kubeClient.StorageV1(), csiExposeParam.SourcePVName, csiExposeParam.StorageClass)
if err != nil {
return errors.Wrapf(err, "error getting volume topology for PV %s, storage class %s", csiExposeParam.SourcePVName, csiExposeParam.StorageClass)
}
if volumeTopology != nil {
curLog.Infof("Using volume topology %v", volumeTopology)
}
curLog.Info("Exposing CSI snapshot")
volumeSnapshot, err := csi.WaitVolumeSnapshotReady(ctx, e.csiSnapshotClient, csiExposeParam.SnapshotName, csiExposeParam.SourceNamespace, csiExposeParam.ExposeTimeout, curLog)
@@ -254,6 +263,7 @@ func (e *csiSnapshotExposer) Expose(ctx context.Context, ownerObject corev1api.O
csiExposeParam.NodeOS,
csiExposeParam.PriorityClassName,
intoleratableNodes,
volumeTopology,
)
if err != nil {
return errors.Wrap(err, "error to create backup pod")
@@ -320,7 +330,8 @@ func (e *csiSnapshotExposer) GetExposed(ctx context.Context, ownerObject corev1a
curLog.WithField("pod", pod.Name).Infof("Backup volume is found in pod at index %v", i)
var nodeOS *string
if os, found := pod.Spec.NodeSelector[kube.NodeOSLabel]; found {
if pod.Spec.OS != nil {
os := string(pod.Spec.OS.Name)
nodeOS = &os
}
@@ -588,6 +599,7 @@ func (e *csiSnapshotExposer) createBackupPod(
nodeOS string,
priorityClassName string,
intoleratableNodes []string,
volumeTopology *corev1api.NodeSelector,
) (*corev1api.Pod, error) {
podName := ownerObject.Name
@@ -643,6 +655,10 @@ func (e *csiSnapshotExposer) createBackupPod(
args = append(args, podInfo.logFormatArgs...)
args = append(args, podInfo.logLevelArgs...)
if affinity == nil {
affinity = &kube.LoadAffinity{}
}
var securityCtx *corev1api.PodSecurityContext
nodeSelector := map[string]string{}
podOS := corev1api.PodOS{}
@@ -654,9 +670,14 @@ func (e *csiSnapshotExposer) createBackupPod(
},
}
nodeSelector[kube.NodeOSLabel] = kube.NodeOSWindows
podOS.Name = kube.NodeOSWindows
affinity.NodeSelector.MatchExpressions = append(affinity.NodeSelector.MatchExpressions, metav1.LabelSelectorRequirement{
Key: kube.NodeOSLabel,
Values: []string{kube.NodeOSWindows},
Operator: metav1.LabelSelectorOpIn,
})
toleration = append(toleration, []corev1api.Toleration{
{
Key: "os",
@@ -683,11 +704,15 @@ func (e *csiSnapshotExposer) createBackupPod(
}
}
nodeSelector[kube.NodeOSLabel] = kube.NodeOSLinux
podOS.Name = kube.NodeOSLinux
affinity.NodeSelector.MatchExpressions = append(affinity.NodeSelector.MatchExpressions, metav1.LabelSelectorRequirement{
Key: kube.NodeOSLabel,
Values: []string{kube.NodeOSWindows},
Operator: metav1.LabelSelectorOpNotIn,
})
}
var podAffinity *corev1api.Affinity
if len(intoleratableNodes) > 0 {
if affinity == nil {
affinity = &kube.LoadAffinity{}
@@ -700,9 +725,7 @@ func (e *csiSnapshotExposer) createBackupPod(
})
}
if affinity != nil {
podAffinity = kube.ToSystemAffinity([]*kube.LoadAffinity{affinity})
}
podAffinity := kube.ToSystemAffinity(affinity, volumeTopology)
pod := &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
@@ -154,6 +154,7 @@ func TestCreateBackupPodWithPriorityClass(t *testing.T) {
kube.NodeOSLinux,
tc.expectedPriorityClass,
nil,
nil,
)
require.NoError(t, err, tc.description)
@@ -239,6 +240,7 @@ func TestCreateBackupPodWithMissingConfigMap(t *testing.T) {
kube.NodeOSLinux,
"", // empty priority class since config map is missing
nil,
nil,
)
// Should succeed even when config map is missing
+255 -3
View File
@@ -68,6 +68,12 @@ func TestExpose(t *testing.T) {
var restoreSize int64 = 123456
scObj := &storagev1api.StorageClass{
ObjectMeta: metav1.ObjectMeta{
Name: "fake-sc",
},
}
snapshotClass := "fake-snapshot-class"
vsObject := &snapshotv1api.VolumeSnapshot{
ObjectMeta: metav1.ObjectMeta{
@@ -199,6 +205,18 @@ func TestExpose(t *testing.T) {
expectedAffinity *corev1api.Affinity
expectedPVCAnnotation map[string]string
}{
{
name: "get volume topology fail",
ownerBackup: backup,
exposeParam: CSISnapshotExposeParam{
SnapshotName: "fake-vs",
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
err: "error getting volume topology for PV fake-pv, storage class fake-sc: error getting storage class fake-sc: storageclasses.storage.k8s.io \"fake-sc\" not found",
},
{
name: "wait vs ready fail",
ownerBackup: backup,
@@ -206,6 +224,11 @@ func TestExpose(t *testing.T) {
SnapshotName: "fake-vs",
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
kubeClientObj: []runtime.Object{
scObj,
},
err: "error wait volume snapshot ready: error to get VolumeSnapshot /fake-vs: volumesnapshots.snapshot.storage.k8s.io \"fake-vs\" not found",
},
@@ -217,10 +240,15 @@ func TestExpose(t *testing.T) {
SourceNamespace: "fake-ns",
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
},
kubeClientObj: []runtime.Object{
scObj,
},
err: "error to get volume snapshot content: error getting volume snapshot content from API: volumesnapshotcontents.snapshot.storage.k8s.io \"fake-vsc\" not found",
},
{
@@ -231,6 +259,8 @@ func TestExpose(t *testing.T) {
SourceNamespace: "fake-ns",
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
@@ -245,6 +275,9 @@ func TestExpose(t *testing.T) {
},
},
},
kubeClientObj: []runtime.Object{
scObj,
},
err: "error to delete volume snapshot: error to delete volume snapshot: fake-delete-error",
},
{
@@ -255,6 +288,8 @@ func TestExpose(t *testing.T) {
SourceNamespace: "fake-ns",
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
@@ -269,6 +304,9 @@ func TestExpose(t *testing.T) {
},
},
},
kubeClientObj: []runtime.Object{
scObj,
},
err: "error to delete volume snapshot content: error to delete volume snapshot content: fake-delete-error",
},
{
@@ -279,6 +317,8 @@ func TestExpose(t *testing.T) {
SourceNamespace: "fake-ns",
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
@@ -293,6 +333,9 @@ func TestExpose(t *testing.T) {
},
},
},
kubeClientObj: []runtime.Object{
scObj,
},
err: "error to create backup volume snapshot: fake-create-error",
},
{
@@ -303,6 +346,8 @@ func TestExpose(t *testing.T) {
SourceNamespace: "fake-ns",
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
@@ -317,6 +362,9 @@ func TestExpose(t *testing.T) {
},
},
},
kubeClientObj: []runtime.Object{
scObj,
},
err: "error to create backup volume snapshot content: fake-create-error",
},
{
@@ -326,11 +374,16 @@ func TestExpose(t *testing.T) {
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
AccessMode: "fake-mode",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
vscObj,
},
kubeClientObj: []runtime.Object{
scObj,
},
err: "error to create backup pvc: unsupported access mode fake-mode",
},
{
@@ -342,6 +395,8 @@ func TestExpose(t *testing.T) {
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
AccessMode: AccessModeFileSystem,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
@@ -356,6 +411,9 @@ func TestExpose(t *testing.T) {
},
},
},
kubeClientObj: []runtime.Object{
scObj,
},
err: "error to create backup pvc: error to create pvc: fake-create-error",
},
{
@@ -367,6 +425,8 @@ func TestExpose(t *testing.T) {
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
@@ -374,6 +434,7 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
kubeReactors: []reactor{
{
@@ -395,6 +456,8 @@ func TestExpose(t *testing.T) {
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
@@ -402,6 +465,24 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
@@ -413,6 +494,8 @@ func TestExpose(t *testing.T) {
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObject,
@@ -420,6 +503,24 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
@@ -432,6 +533,8 @@ func TestExpose(t *testing.T) {
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
VolumeSize: *resource.NewQuantity(567890, ""),
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
},
snapshotClientObj: []runtime.Object{
vsObjectWithoutRestoreSize,
@@ -439,8 +542,26 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedVolumeSize: resource.NewQuantity(567890, ""),
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
name: "backupPod mounts read only backupPVC",
@@ -449,6 +570,7 @@ func TestExpose(t *testing.T) {
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
@@ -465,8 +587,26 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedReadOnlyPVC: true,
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
name: "backupPod mounts read only backupPVC and storageClass specified in backupPVC config",
@@ -475,6 +615,7 @@ func TestExpose(t *testing.T) {
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
@@ -491,9 +632,27 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedReadOnlyPVC: true,
expectedBackupPVCStorageClass: "fake-sc-read-only",
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
name: "backupPod mounts backupPVC with storageClass specified in backupPVC config",
@@ -502,6 +661,7 @@ func TestExpose(t *testing.T) {
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
@@ -517,8 +677,26 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedBackupPVCStorageClass: "fake-sc-read-only",
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
name: "Affinity per StorageClass",
@@ -527,6 +705,7 @@ func TestExpose(t *testing.T) {
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
@@ -551,6 +730,7 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
@@ -563,6 +743,11 @@ func TestExpose(t *testing.T) {
Operator: corev1api.NodeSelectorOpIn,
Values: []string{"Linux"},
},
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
@@ -577,6 +762,7 @@ func TestExpose(t *testing.T) {
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
@@ -606,6 +792,7 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedBackupPVCStorageClass: "fake-sc-read-only",
expectedAffinity: &corev1api.Affinity{
@@ -619,6 +806,11 @@ func TestExpose(t *testing.T) {
Operator: corev1api.NodeSelectorOpIn,
Values: []string{"amd64"},
},
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
@@ -633,6 +825,7 @@ func TestExpose(t *testing.T) {
SnapshotName: "fake-vs",
SourceNamespace: "fake-ns",
StorageClass: "fake-sc",
SourcePVName: "fake-pv",
AccessMode: AccessModeFileSystem,
OperationTimeout: time.Millisecond,
ExposeTimeout: time.Millisecond,
@@ -649,9 +842,26 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedBackupPVCStorageClass: "fake-sc-read-only",
expectedAffinity: nil,
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
},
{
name: "IntolerateSourceNode, get source node fail",
@@ -677,6 +887,7 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
kubeReactors: []reactor{
{
@@ -687,7 +898,23 @@ func TestExpose(t *testing.T) {
},
},
},
expectedAffinity: nil,
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
expectedPVCAnnotation: nil,
},
{
@@ -714,8 +941,25 @@ func TestExpose(t *testing.T) {
},
kubeClientObj: []runtime.Object{
daemonSet,
scObj,
},
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
},
},
},
},
},
},
expectedAffinity: nil,
expectedPVCAnnotation: map[string]string{util.VSphereCNSFastCloneAnno: "true"},
},
{
@@ -744,6 +988,7 @@ func TestExpose(t *testing.T) {
daemonSet,
volumeAttachement1,
volumeAttachement2,
scObj,
},
expectedAffinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
@@ -751,6 +996,11 @@ func TestExpose(t *testing.T) {
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Operator: corev1api.NodeSelectorOpNotIn,
Values: []string{"windows"},
},
{
Key: "kubernetes.io/hostname",
Operator: corev1api.NodeSelectorOpNotIn,
@@ -844,6 +1094,8 @@ func TestExpose(t *testing.T) {
if test.expectedAffinity != nil {
assert.Equal(t, test.expectedAffinity, backupPod.Spec.Affinity)
} else {
assert.Nil(t, backupPod.Spec.Affinity)
}
if test.expectedPVCAnnotation != nil {
+22 -10
View File
@@ -493,13 +493,15 @@ func (e *genericRestoreExposer) createRestorePod(
containerName := string(ownerObject.UID)
volumeName := string(ownerObject.UID)
var podAffinity *corev1api.Affinity
if selectedNode == "" {
e.log.Infof("No selected node for restore pod. Try to get affinity from the node-agent config.")
nodeSelector := map[string]string{}
if selectedNode != "" {
affinity = nil
nodeSelector["kubernetes.io/hostname"] = selectedNode
e.log.Infof("Selected node for restore pod. Ignore affinity from the node-agent config.")
}
if affinity != nil {
podAffinity = kube.ToSystemAffinity([]*kube.LoadAffinity{affinity})
}
if affinity == nil {
affinity = &kube.LoadAffinity{}
}
podInfo, err := getInheritedPodInfo(ctx, e.kubeClient, ownerObject.Namespace, nodeOS)
@@ -566,7 +568,6 @@ func (e *genericRestoreExposer) createRestorePod(
args = append(args, podInfo.logLevelArgs...)
var securityCtx *corev1api.PodSecurityContext
nodeSelector := map[string]string{}
podOS := corev1api.PodOS{}
if nodeOS == kube.NodeOSWindows {
userID := "ContainerAdministrator"
@@ -576,9 +577,14 @@ func (e *genericRestoreExposer) createRestorePod(
},
}
nodeSelector[kube.NodeOSLabel] = kube.NodeOSWindows
podOS.Name = kube.NodeOSWindows
affinity.NodeSelector.MatchExpressions = append(affinity.NodeSelector.MatchExpressions, metav1.LabelSelectorRequirement{
Key: kube.NodeOSLabel,
Values: []string{kube.NodeOSWindows},
Operator: metav1.LabelSelectorOpIn,
})
toleration = append(toleration, []corev1api.Toleration{
{
Key: "os",
@@ -599,10 +605,17 @@ func (e *genericRestoreExposer) createRestorePod(
RunAsUser: &userID,
}
nodeSelector[kube.NodeOSLabel] = kube.NodeOSLinux
podOS.Name = kube.NodeOSLinux
affinity.NodeSelector.MatchExpressions = append(affinity.NodeSelector.MatchExpressions, metav1.LabelSelectorRequirement{
Key: kube.NodeOSLabel,
Values: []string{kube.NodeOSWindows},
Operator: metav1.LabelSelectorOpNotIn,
})
}
podAffinity := kube.ToSystemAffinity(affinity, nil)
pod := &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: restorePodName,
@@ -656,7 +669,6 @@ func (e *genericRestoreExposer) createRestorePod(
ServiceAccountName: podInfo.serviceAccount,
TerminationGracePeriodSeconds: &gracePeriod,
Volumes: volumes,
NodeName: selectedNode,
RestartPolicy: corev1api.RestartPolicyNever,
SecurityContext: securityCtx,
Tolerations: toleration,
+17 -2
View File
@@ -434,6 +434,8 @@ func (e *podVolumeExposer) createHostingPod(
args = append(args, podInfo.logFormatArgs...)
args = append(args, podInfo.logLevelArgs...)
affinity := &kube.LoadAffinity{}
var securityCtx *corev1api.PodSecurityContext
var containerSecurityCtx *corev1api.SecurityContext
nodeSelector := map[string]string{}
@@ -446,9 +448,14 @@ func (e *podVolumeExposer) createHostingPod(
},
}
nodeSelector[kube.NodeOSLabel] = kube.NodeOSWindows
podOS.Name = kube.NodeOSWindows
affinity.NodeSelector.MatchExpressions = append(affinity.NodeSelector.MatchExpressions, metav1.LabelSelectorRequirement{
Key: kube.NodeOSLabel,
Values: []string{kube.NodeOSWindows},
Operator: metav1.LabelSelectorOpIn,
})
toleration = append(toleration, []corev1api.Toleration{
{
Key: "os",
@@ -472,10 +479,17 @@ func (e *podVolumeExposer) createHostingPod(
Privileged: &privileged,
}
nodeSelector[kube.NodeOSLabel] = kube.NodeOSLinux
podOS.Name = kube.NodeOSLinux
affinity.NodeSelector.MatchExpressions = append(affinity.NodeSelector.MatchExpressions, metav1.LabelSelectorRequirement{
Key: kube.NodeOSLabel,
Values: []string{kube.NodeOSWindows},
Operator: metav1.LabelSelectorOpNotIn,
})
}
podAffinity := kube.ToSystemAffinity(affinity, nil)
pod := &corev1api.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: hostingPodName,
@@ -495,6 +509,7 @@ func (e *podVolumeExposer) createHostingPod(
Spec: corev1api.PodSpec{
NodeSelector: nodeSelector,
OS: &podOS,
Affinity: podAffinity,
Containers: []corev1api.Container{
{
Name: containerName,
+35 -8
View File
@@ -235,12 +235,28 @@ func DaemonSet(namespace string, opts ...podTemplateOption) *appsv1api.DaemonSet
if c.forWindows {
daemonSet.Spec.Template.Spec.SecurityContext = nil
daemonSet.Spec.Template.Spec.Containers[0].SecurityContext = nil
daemonSet.Spec.Template.Spec.NodeSelector = map[string]string{
"kubernetes.io/os": "windows",
}
daemonSet.Spec.Template.Spec.OS = &corev1api.PodOS{
Name: "windows",
}
daemonSet.Spec.Template.Spec.Affinity = &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Values: []string{"windows"},
Operator: corev1api.NodeSelectorOpIn,
},
},
},
},
},
},
}
daemonSet.Spec.Template.Spec.Tolerations = []corev1api.Toleration{
{
Key: "os",
@@ -256,11 +272,22 @@ func DaemonSet(namespace string, opts ...podTemplateOption) *appsv1api.DaemonSet
},
}
} else {
daemonSet.Spec.Template.Spec.NodeSelector = map[string]string{
"kubernetes.io/os": "linux",
}
daemonSet.Spec.Template.Spec.OS = &corev1api.PodOS{
Name: "linux",
daemonSet.Spec.Template.Spec.Affinity = &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Values: []string{"windows"},
Operator: corev1api.NodeSelectorOpNotIn,
},
},
},
},
},
},
}
}
+34 -3
View File
@@ -34,8 +34,23 @@ func TestDaemonSet(t *testing.T) {
assert.Equal(t, "velero", ds.ObjectMeta.Namespace)
assert.Equal(t, "node-agent", ds.Spec.Template.ObjectMeta.Labels["name"])
assert.Equal(t, "node-agent", ds.Spec.Template.ObjectMeta.Labels["role"])
assert.Equal(t, "linux", ds.Spec.Template.Spec.NodeSelector["kubernetes.io/os"])
assert.Equal(t, "linux", string(ds.Spec.Template.Spec.OS.Name))
assert.Equal(t, &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Values: []string{"windows"},
Operator: corev1api.NodeSelectorOpNotIn,
},
},
},
},
},
},
}, ds.Spec.Template.Spec.Affinity)
assert.Equal(t, corev1api.PodSecurityContext{RunAsUser: &userID}, *ds.Spec.Template.Spec.SecurityContext)
assert.Equal(t, corev1api.SecurityContext{Privileged: &boolFalse}, *ds.Spec.Template.Spec.Containers[0].SecurityContext)
assert.Len(t, ds.Spec.Template.Spec.Volumes, 3)
@@ -80,8 +95,24 @@ func TestDaemonSet(t *testing.T) {
assert.Equal(t, "velero", ds.ObjectMeta.Namespace)
assert.Equal(t, "node-agent-windows", ds.Spec.Template.ObjectMeta.Labels["name"])
assert.Equal(t, "node-agent", ds.Spec.Template.ObjectMeta.Labels["role"])
assert.Equal(t, "windows", ds.Spec.Template.Spec.NodeSelector["kubernetes.io/os"])
assert.Equal(t, "windows", string(ds.Spec.Template.Spec.OS.Name))
assert.Equal(t, &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Values: []string{"windows"},
Operator: corev1api.NodeSelectorOpIn,
},
},
},
},
},
},
}, ds.Spec.Template.Spec.Affinity)
assert.Equal(t, (*corev1api.PodSecurityContext)(nil), ds.Spec.Template.Spec.SecurityContext)
assert.Equal(t, (*corev1api.SecurityContext)(nil), ds.Spec.Template.Spec.Containers[0].SecurityContext)
}
+17 -3
View File
@@ -364,12 +364,26 @@ func Deployment(namespace string, opts ...podTemplateOption) *appsv1api.Deployme
Spec: corev1api.PodSpec{
RestartPolicy: corev1api.RestartPolicyAlways,
ServiceAccountName: c.serviceAccountName,
NodeSelector: map[string]string{
"kubernetes.io/os": "linux",
},
OS: &corev1api.PodOS{
Name: "linux",
},
Affinity: &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Values: []string{"windows"},
Operator: corev1api.NodeSelectorOpNotIn,
},
},
},
},
},
},
},
Containers: []corev1api.Container{
{
Name: "velero",
+17 -2
View File
@@ -100,8 +100,23 @@ func TestDeployment(t *testing.T) {
assert.Len(t, deploy.Spec.Template.Spec.Containers[0].Args, 2)
assert.Equal(t, "--repo-maintenance-job-configmap=test-repo-maintenance-config", deploy.Spec.Template.Spec.Containers[0].Args[1])
assert.Equal(t, "linux", deploy.Spec.Template.Spec.NodeSelector["kubernetes.io/os"])
assert.Equal(t, "linux", string(deploy.Spec.Template.Spec.OS.Name))
assert.Equal(t, &corev1api.Affinity{
NodeAffinity: &corev1api.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1api.NodeSelector{
NodeSelectorTerms: []corev1api.NodeSelectorTerm{
{
MatchExpressions: []corev1api.NodeSelectorRequirement{
{
Key: "kubernetes.io/os",
Values: []string{"windows"},
Operator: corev1api.NodeSelectorOpNotIn,
},
},
},
},
},
},
}, deploy.Spec.Template.Spec.Affinity)
}
func TestDeploymentWithPriorityClassName(t *testing.T) {
+22
View File
@@ -80,6 +80,9 @@ const (
DataDownloadFailureTotal = "data_download_failure_total"
DataDownloadCancelTotal = "data_download_cancel_total"
// schedule metrics
scheduleExpectedIntervalSeconds = "schedule_expected_interval_seconds"
// repo maintenance metrics
repoMaintenanceSuccessTotal = "repo_maintenance_success_total"
repoMaintenanceFailureTotal = "repo_maintenance_failure_total"
@@ -347,6 +350,14 @@ func NewServerMetrics() *ServerMetrics {
},
[]string{scheduleLabel, backupNameLabel},
),
scheduleExpectedIntervalSeconds: prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Namespace: metricNamespace,
Name: scheduleExpectedIntervalSeconds,
Help: "Expected interval between consecutive scheduled backups, in seconds",
},
[]string{scheduleLabel},
),
repoMaintenanceSuccessTotal: prometheus.NewCounterVec(
prometheus.CounterOpts{
Namespace: metricNamespace,
@@ -644,6 +655,9 @@ func (m *ServerMetrics) RemoveSchedule(scheduleName string) {
if c, ok := m.metrics[csiSnapshotFailureTotal].(*prometheus.CounterVec); ok {
c.DeleteLabelValues(scheduleName, "")
}
if g, ok := m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec); ok {
g.DeleteLabelValues(scheduleName)
}
}
// InitMetricsForNode initializes counter metrics for a node.
@@ -758,6 +772,14 @@ func (m *ServerMetrics) SetBackupLastSuccessfulTimestamp(backupSchedule string,
}
}
// SetScheduleExpectedIntervalSeconds records the expected interval in seconds,
// between consecutive backups for a schedule.
func (m *ServerMetrics) SetScheduleExpectedIntervalSeconds(scheduleName string, seconds float64) {
if g, ok := m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec); ok {
g.WithLabelValues(scheduleName).Set(seconds)
}
}
// SetBackupTotal records the current number of existent backups.
func (m *ServerMetrics) SetBackupTotal(numberOfBackups int64) {
if g, ok := m.metrics[backupTotal].(prometheus.Gauge); ok {
+84
View File
@@ -259,6 +259,90 @@ func TestMultipleAdhocBackupsShareMetrics(t *testing.T) {
assert.Equal(t, float64(1), validationFailureMetric, "All adhoc validation failures should be counted together")
}
// TestSetScheduleExpectedIntervalSeconds verifies that the expected interval metric
// is properly recorded for schedules.
func TestSetScheduleExpectedIntervalSeconds(t *testing.T) {
tests := []struct {
name string
scheduleName string
intervalSeconds float64
description string
}{
{
name: "every 5 minutes schedule",
scheduleName: "frequent-backup",
intervalSeconds: 300,
description: "Expected interval should be 5m in seconds",
},
{
name: "daily schedule",
scheduleName: "daily-backup",
intervalSeconds: 86400,
description: "Expected interval should be 24h in seconds",
},
{
name: "monthly schedule",
scheduleName: "monthly-backup",
intervalSeconds: 2678400, // 31 days in seconds
description: "Expected interval should be 31 days in seconds",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
m := NewServerMetrics()
m.SetScheduleExpectedIntervalSeconds(tc.scheduleName, tc.intervalSeconds)
metric := getMetricValue(t, m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec), tc.scheduleName)
assert.Equal(t, tc.intervalSeconds, metric, tc.description)
})
}
}
// TestScheduleExpectedIntervalNotInitializedByDefault verifies that the expected
// interval metric is not initialized by InitSchedule, so it only appears for
// schedules with a valid cron expression.
func TestScheduleExpectedIntervalNotInitializedByDefault(t *testing.T) {
m := NewServerMetrics()
m.InitSchedule("test-schedule")
// The metric should not have any values after InitSchedule
ch := make(chan prometheus.Metric, 1)
m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec).Collect(ch)
close(ch)
count := 0
for range ch {
count++
}
assert.Equal(t, 0, count, "scheduleExpectedIntervalSeconds should not be initialized by InitSchedule")
}
// TestRemoveScheduleCleansUpExpectedInterval verifies that RemoveSchedule
// cleans up the expected interval metric.
func TestRemoveScheduleCleansUpExpectedInterval(t *testing.T) {
m := NewServerMetrics()
m.InitSchedule("test-schedule")
m.SetScheduleExpectedIntervalSeconds("test-schedule", 3600)
// Verify metric exists
metric := getMetricValue(t, m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec), "test-schedule")
assert.Equal(t, float64(3600), metric)
// Remove schedule and verify metric is cleaned up
m.RemoveSchedule("test-schedule")
ch := make(chan prometheus.Metric, 1)
m.metrics[scheduleExpectedIntervalSeconds].(*prometheus.GaugeVec).Collect(ch)
close(ch)
count := 0
for range ch {
count++
}
assert.Equal(t, 0, count, "scheduleExpectedIntervalSeconds should be removed after RemoveSchedule")
}
// TestInitScheduleWithEmptyName verifies that InitSchedule works correctly
// with an empty schedule name (for adhoc backups).
func TestInitScheduleWithEmptyName(t *testing.T) {
+4 -1
View File
@@ -97,7 +97,10 @@ func isRunningInNode(ctx context.Context, namespace string, nodeName string, crC
}
if crClient != nil {
err = crClient.List(ctx, pods, &ctrlclient.ListOptions{LabelSelector: parsedSelector})
err = crClient.List(ctx, pods, &ctrlclient.ListOptions{
LabelSelector: parsedSelector,
Namespace: namespace,
})
} else {
pods, err = kubeClient.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{LabelSelector: parsedSelector.String()})
}
+37 -9
View File
@@ -118,28 +118,37 @@ func TestIsRunningInNode(t *testing.T) {
Phase(corev1api.PodRunning).
NodeName("fake-node").
Result()
nodeAgentPodOtherNs := builder.ForPod("other-ns", "fake-pod-other").
Labels(map[string]string{"role": "node-agent"}).
Phase(corev1api.PodRunning).
NodeName("fake-node").
Result()
tests := []struct {
name string
kubeClientObj []runtime.Object
namespace string
nodeName string
expectErr string
}{
{
name: "node name is empty",
namespace: "fake-ns",
expectErr: "node name is empty",
},
{
name: "ds pod not found",
nodeName: "fake-node",
name: "ds pod not found",
namespace: "fake-ns",
nodeName: "fake-node",
kubeClientObj: []runtime.Object{
nonNodeAgentPod,
},
expectErr: "daemonset pod not found in running state in node fake-node",
},
{
name: "ds po are not all running",
nodeName: "fake-node",
name: "ds po are not all running",
namespace: "fake-ns",
nodeName: "fake-node",
kubeClientObj: []runtime.Object{
nodeAgentPodNotRunning,
nodeAgentPodRunning1,
@@ -147,8 +156,9 @@ func TestIsRunningInNode(t *testing.T) {
expectErr: "daemonset pod not found in running state in node fake-node",
},
{
name: "ds pods wrong node name",
nodeName: "fake-node",
name: "ds pods wrong node name",
namespace: "fake-ns",
nodeName: "fake-node",
kubeClientObj: []runtime.Object{
nodeAgentPodNotRunning,
nodeAgentPodRunning1,
@@ -157,8 +167,9 @@ func TestIsRunningInNode(t *testing.T) {
expectErr: "daemonset pod not found in running state in node fake-node",
},
{
name: "succeed",
nodeName: "fake-node",
name: "succeed",
namespace: "fake-ns",
nodeName: "fake-node",
kubeClientObj: []runtime.Object{
nodeAgentPodNotRunning,
nodeAgentPodRunning1,
@@ -166,6 +177,23 @@ func TestIsRunningInNode(t *testing.T) {
nodeAgentPodRunning3,
},
},
{
name: "cross-namespace isolation - pod in wrong namespace on same node",
namespace: "fake-ns",
nodeName: "fake-node",
kubeClientObj: []runtime.Object{
nodeAgentPodOtherNs,
},
expectErr: "daemonset pod not found in running state in node fake-node",
},
{
name: "cross-namespace isolation - pod in correct namespace on same node",
namespace: "other-ns",
nodeName: "fake-node",
kubeClientObj: []runtime.Object{
nodeAgentPodOtherNs,
},
},
}
for _, test := range tests {
@@ -175,7 +203,7 @@ func TestIsRunningInNode(t *testing.T) {
fakeClient := fakeClientBuilder.WithRuntimeObjects(test.kubeClientObj...).Build()
err := IsRunningInNode(t.Context(), "", test.nodeName, fakeClient)
err := IsRunningInNode(t.Context(), test.namespace, test.nodeName, fakeClient)
if test.expectErr == "" {
assert.NoError(t, err)
} else {
+2 -1
View File
@@ -149,7 +149,8 @@ func (b *objectBackupStoreGetter) Get(location *velerov1api.BackupStorageLocatio
// if there are any slashes in the middle of 'bucket', the user
// probably put <bucket>/<prefix> in the bucket field, which we
// don't support.
if strings.Contains(bucket, "/") {
// Exception: MRAP ARNs (arn:aws:s3::...) legitimately contain slashes.
if strings.Contains(bucket, "/") && !strings.HasPrefix(bucket, "arn:aws:s3:") {
return nil, errors.Errorf("backup storage location's bucket name %q must not contain a '/' (if using a prefix, put it in the 'Prefix' field instead)", location.Spec.ObjectStorage.Bucket)
}
+18
View File
@@ -943,6 +943,24 @@ func TestNewObjectBackupStoreGetter(t *testing.T) {
wantBucket: "bucket",
wantPrefix: "prefix/",
},
{
name: "when the Bucket field is an MRAP ARN, it should be valid",
location: builder.ForBackupStorageLocation("", "").Provider("provider-1").Bucket("arn:aws:s3::123456789012:accesspoint/abcdef0123456.mrap").Result(),
objectStoreGetter: objectStoreGetter{
"provider-1": newInMemoryObjectStore("arn:aws:s3::123456789012:accesspoint/abcdef0123456.mrap"),
},
credFileStore: velerotest.NewFakeCredentialsFileStore("", nil),
wantBucket: "arn:aws:s3::123456789012:accesspoint/abcdef0123456.mrap",
},
{
name: "when the Bucket field is an MRAP ARN with trailing slash, it should be valid and trimmed",
location: builder.ForBackupStorageLocation("", "").Provider("provider-1").Bucket("arn:aws:s3::123456789012:accesspoint/abcdef0123456.mrap/").Result(),
objectStoreGetter: objectStoreGetter{
"provider-1": newInMemoryObjectStore("arn:aws:s3::123456789012:accesspoint/abcdef0123456.mrap"),
},
credFileStore: velerotest.NewFakeCredentialsFileStore("", nil),
wantBucket: "arn:aws:s3::123456789012:accesspoint/abcdef0123456.mrap",
},
}
for _, tc := range tests {
@@ -26,6 +26,8 @@ import (
"github.com/vmware-tanzu/velero/internal/volumehelper"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
"github.com/vmware-tanzu/velero/pkg/util/boolptr"
podvolumeutil "github.com/vmware-tanzu/velero/pkg/util/podvolume"
vhutil "github.com/vmware-tanzu/velero/pkg/util/volumehelper"
)
// ShouldPerformSnapshotWithBackup is used for third-party plugins.
@@ -66,7 +68,7 @@ func ShouldPerformSnapshotWithVolumeHelper(
backup velerov1api.Backup,
crClient crclient.Client,
logger logrus.FieldLogger,
vh volumehelper.VolumeHelper,
vh vhutil.VolumeHelper,
) (bool, error) {
// If a VolumeHelper is provided, use it directly
if vh != nil {
@@ -95,3 +97,45 @@ func ShouldPerformSnapshotWithVolumeHelper(
return volumeHelperImpl.ShouldPerformSnapshot(unstructured, groupResource)
}
// NewVolumeHelperWithNamespaces creates a VolumeHelper with a PVC-to-Pod cache for improved performance.
// The cache is built internally from the provided namespaces list.
// This avoids O(N*M) complexity when there are many PVCs and pods.
// See issue #9179 for details.
// Returns an error if cache building fails - callers should not proceed with backup in this case.
func NewVolumeHelperWithNamespaces(
volumePolicy *resourcepolicies.Policies,
snapshotVolumes *bool,
logger logrus.FieldLogger,
client crclient.Client,
defaultVolumesToFSBackup bool,
backupExcludePVC bool,
namespaces []string,
) (vhutil.VolumeHelper, error) {
return volumehelper.NewVolumeHelperImplWithNamespaces(
volumePolicy,
snapshotVolumes,
logger,
client,
defaultVolumesToFSBackup,
backupExcludePVC,
namespaces,
)
}
// NewVolumeHelperWithCache creates a VolumeHelper using an externally managed PVC-to-Pod cache.
// This is used by plugins that build the cache lazily per-namespace (following the pattern from PR #9226).
// The cache can be nil, in which case PVC-to-Pod lookups will fall back to direct API calls.
func NewVolumeHelperWithCache(
backup velerov1api.Backup,
client crclient.Client,
logger logrus.FieldLogger,
pvcPodCache *podvolumeutil.PVCPodCache,
) (vhutil.VolumeHelper, error) {
return volumehelper.NewVolumeHelperImplWithCache(
backup,
client,
logger,
pvcPodCache,
)
}
+17 -4
View File
@@ -20,6 +20,7 @@ import (
"bytes"
"context"
"net/url"
"slices"
"time"
"github.com/pkg/errors"
@@ -184,10 +185,22 @@ func (e *defaultPodCommandExecutor) ExecutePodCommand(log logrus.FieldLogger, it
}
func ensureContainerExists(pod *corev1api.Pod, container string) error {
for _, c := range pod.Spec.Containers {
if c.Name == container {
return nil
}
existsAsMainContainer := slices.ContainsFunc(pod.Spec.Containers, func(c corev1api.Container) bool {
return c.Name == container
})
if existsAsMainContainer {
return nil
}
existsAsSidecar := slices.ContainsFunc(pod.Spec.InitContainers, func(c corev1api.Container) bool {
return c.RestartPolicy != nil &&
*c.RestartPolicy == corev1api.ContainerRestartPolicyAlways &&
c.Name == container
})
if existsAsSidecar {
return nil
}
return errors.Errorf("no such container: %q", container)
+17 -1
View File
@@ -35,6 +35,7 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
"k8s.io/utils/ptr"
v1 "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
velerotest "github.com/vmware-tanzu/velero/pkg/test"
@@ -253,6 +254,15 @@ func TestEnsureContainerExists(t *testing.T) {
Name: "foo",
},
},
InitContainers: []corev1api.Container{
{
Name: "baz",
},
{
Name: "qux",
RestartPolicy: ptr.To(corev1api.ContainerRestartPolicyAlways),
},
},
},
}
@@ -260,7 +270,13 @@ func TestEnsureContainerExists(t *testing.T) {
require.EqualError(t, err, `no such container: "bar"`)
err = ensureContainerExists(pod, "foo")
assert.NoError(t, err)
require.NoError(t, err)
err = ensureContainerExists(pod, "baz")
require.EqualError(t, err, `no such container: "baz"`)
err = ensureContainerExists(pod, "qux")
require.NoError(t, err)
}
func TestPodCompeted(t *testing.T) {

Some files were not shown because too many files have changed in this diff Show More