Merge branch 'main' into copilot/remove-restic-from-velero-doc
Run the E2E test on kind / get-go-version (push) Failing after 57s
Run the E2E test on kind / build (push) Has been skipped
Run the E2E test on kind / setup-test-matrix (push) Successful in 3s
Run the E2E test on kind / run-e2e-test (push) Has been skipped

This commit is contained in:
Shubham Pampattiwar
2026-07-16 14:18:24 -07:00
committed by GitHub
456 changed files with 14364 additions and 1945 deletions
+2
View File
@@ -0,0 +1,2 @@
# maintainers are the overall code owners
* @velero-io/Maintainer
+14
View File
@@ -15,6 +15,20 @@ updates:
schedule:
interval: "weekly"
labels:
- "Dependencies"
- "go"
- "kind/changelog-not-required"
ignore:
- dependency-name: "*"
update-types: ["version-update:semver-major", "version-update:semver-minor", "version-update:semver-patch"]
# Dependencies listed in pkg/apis/go.mod
- package-ecosystem: "gomod"
directory: "/pkg/apis" # Location of package manifests
schedule:
interval: "weekly"
labels:
- "Dependencies"
- "go"
- "kind/changelog-not-required"
ignore:
- dependency-name: "*"
+1
View File
@@ -14,6 +14,7 @@ permissions:
jobs:
# Automatically assigns reviewers and owner
add-reviews:
if: github.repository == 'velero-io/velero'
runs-on: ubuntu-latest
steps:
- name: Set the author of a PR as the assignee
+1
View File
@@ -15,6 +15,7 @@ permissions:
jobs:
# Automatically labels PRs based on file globs in the change.
triage:
if: github.repository == 'velero-io/velero'
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5
@@ -11,6 +11,7 @@ permissions:
jobs:
auto-request-review:
if: github.repository == 'velero-io/velero'
name: Auto Request Review
runs-on: ubuntu-latest
steps:
+9 -5
View File
@@ -1,4 +1,6 @@
name: "Run the E2E test on kind"
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
on:
push:
pull_request:
@@ -57,8 +59,10 @@ jobs:
# Check and build MinIO image once for all e2e tests
- name: Check Bitnami MinIO Dockerfile version
id: minio-version
env:
GH_TOKEN: ${{ github.token }}
run: |
DOCKERFILE_SHA=$(curl -s https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2025/debian-12/Dockerfile\&per_page=1 | jq -r '.[0].sha')
DOCKERFILE_SHA=$(curl -s -H "Authorization: Bearer $GH_TOKEN" https://api.github.com/repos/bitnami/containers/commits?path=bitnami/minio/2026/debian-12/Dockerfile\&per_page=1 | jq -r '.[0].sha')
echo "dockerfile_sha=${DOCKERFILE_SHA}" >> $GITHUB_OUTPUT
- name: Cache MinIO Image
uses: actions/cache@v4
@@ -133,11 +137,11 @@ jobs:
- name: Install MinIO
run: |
docker run -d --rm -p 9000:9000 -e "MINIO_ROOT_USER=minio" -e "MINIO_ROOT_PASSWORD=minio123" -e "MINIO_DEFAULT_BUCKETS=bucket,additional-bucket" bitnami/minio:local
- uses: engineerd/setup-kind@v0.6.2
- uses: helm/kind-action@v1
with:
skipClusterLogsExport: true
cluster_name: "kind"
version: "v0.32.0"
image: "kindest/node:v${{ matrix.k8s }}"
node_image: "kindest/node:v${{ matrix.k8s }}"
- name: Fetch built CLI
id: cli-cache
uses: actions/cache@v4
@@ -185,7 +189,7 @@ jobs:
timeout-minutes: 30
- name: Upload debug bundle
if: ${{ failure() }}
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v7
with:
name: DebugBundle-k8s-${{ matrix.k8s }}-job-${{ strategy.job-index }}
path: /home/runner/work/velero/velero/test/e2e/debug-bundle*
+1
View File
@@ -7,6 +7,7 @@ on:
jobs:
build:
if: github.repository == 'velero-io/velero'
name: Run Changelog Check
runs-on: ubuntu-latest
steps:
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
- name: Make ci
run: make ci
- name: Upload test coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.out
+1
View File
@@ -3,6 +3,7 @@ on: [pull_request]
jobs:
codespell:
if: github.repository == 'velero-io/velero'
name: Run Codespell
runs-on: ubuntu-latest
steps:
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: all
+1
View File
@@ -3,6 +3,7 @@ on: [pull_request]
jobs:
filepath-check:
if: github.repository == 'velero-io/velero'
name: Check for invalid characters in file paths
runs-on: ubuntu-latest
steps:
+1
View File
@@ -11,6 +11,7 @@ permissions:
jobs:
execute:
if: github.repository == 'velero-io/velero'
runs-on: ubuntu-latest
steps:
- uses: jpmcb/prow-github-actions@v1.1.3
+2 -2
View File
@@ -29,7 +29,7 @@ jobs:
- name: Set up QEMU
id: qemu
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
with:
platforms: all
- name: Set up Docker Buildx
@@ -45,7 +45,7 @@ jobs:
- name: Test
run: make test
- name: Upload test coverage
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: coverage.out
+1 -1
View File
@@ -5,7 +5,7 @@ name: Automatic Rebase
jobs:
rebase:
name: Rebase
if: github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
if: github.repository == 'velero-io/velero' && github.event.issue.pull_request != '' && contains(github.event.comment.body, '/rebase')
runs-on: ubuntu-latest
steps:
- name: Checkout the latest code
+1
View File
@@ -5,6 +5,7 @@ on:
jobs:
stale:
if: github.repository == 'velero-io/velero'
runs-on: ubuntu-latest
steps:
- uses: actions/stale@v10.1.1
+40 -13
View File
@@ -20,6 +20,10 @@ BIN ?= velero
# This repo's root import path (under GOPATH).
PKG := github.com/vmware-tanzu/velero
# Container tool for local development targets (shell, lint, build-image, etc.)
# Override with CONTAINER_TOOL=podman to use podman instead of docker.
CONTAINER_TOOL ?= docker
# Where to push the docker image.
REGISTRY ?= velero
# In order to push images to an insecure registry, follow the two steps:
@@ -63,7 +67,7 @@ else
endif
BUILDER_IMAGE := $(REGISTRY)/build-image:$(BUILDER_IMAGE_TAG)
BUILDER_IMAGE_CACHED := $(shell docker images -q ${BUILDER_IMAGE} 2>/dev/null )
BUILDER_IMAGE_CACHED := $(shell $(CONTAINER_TOOL) images -q ${BUILDER_IMAGE} 2>/dev/null )
HUGO_IMAGE := ghcr.io/gohugoio/hugo
@@ -103,6 +107,11 @@ define BUILDX_ERROR
buildx not enabled, refusing to run this recipe
see: https://velero.io/docs/main/build-from-source/#making-images-and-updating-velero for more info
endef
define DOCKER_ONLY_ERROR
this target requires docker buildx/manifest and is not supported with CONTAINER_TOOL=$(CONTAINER_TOOL).
use docker for multi-arch image targets, or build single-arch images with podman directly.
endef
# comma cannot be escaped and can only be used in Make function arguments by putting into variable
comma=,
@@ -198,7 +207,7 @@ shell: build-dirs build-env
@# because the Kubernetes code-generator tools require the project to
@# exist in a directory hierarchy ending like this (but *NOT* necessarily
@# under $GOPATH).
@docker run \
@$(CONTAINER_TOOL) run \
-e GOFLAGS \
-e GOPROXY \
-i $(TTY) \
@@ -217,6 +226,9 @@ shell: build-dirs build-env
/bin/sh $(CMD)
container:
ifneq ($(CONTAINER_TOOL),docker)
$(error $(DOCKER_ONLY_ERROR))
endif
ifneq ($(BUILDX_ENABLED), true)
$(error $(BUILDX_ERROR))
endif
@@ -246,6 +258,9 @@ container-linux-%:
@BUILDX_ARCH=$* $(MAKE) container-linux
container-linux:
ifneq ($(CONTAINER_TOOL),docker)
$(error $(DOCKER_ONLY_ERROR))
endif
@echo "building container: $(IMAGE):$(VERSION)-linux-$(BUILDX_ARCH)"
@docker buildx build --pull \
@@ -269,6 +284,9 @@ container-windows-%:
@BUILDX_OSVERSION=$(firstword $(subst -, ,$*)) BUILDX_ARCH=$(lastword $(subst -, ,$*)) $(MAKE) container-windows
container-windows:
ifneq ($(CONTAINER_TOOL),docker)
$(error $(DOCKER_ONLY_ERROR))
endif
@echo "building container: $(IMAGE):$(VERSION)-windows-$(BUILDX_OSVERSION)-$(BUILDX_ARCH)"
@docker buildx build --pull \
@@ -290,6 +308,9 @@ container-windows:
@echo "built container: $(IMAGE):$(VERSION)-windows-$(BUILDX_OSVERSION)-$(BUILDX_ARCH)"
push-manifest:
ifneq ($(CONTAINER_TOOL),docker)
$(error $(DOCKER_ONLY_ERROR))
endif
@echo "building manifest: $(IMAGE_TAG) for $(foreach osarch, $(ALL_OS_ARCH), $(IMAGE_TAG)-${osarch})"
@docker manifest create --amend --insecure=$(INSECURE_REGISTRY) $(IMAGE_TAG) $(foreach osarch, $(ALL_OS_ARCH), $(IMAGE_TAG)-${osarch})
@@ -363,24 +384,30 @@ else ifneq ($(BUILDER_IMAGE_CACHED),)
@echo "Using Cached Image: $(BUILDER_IMAGE)"
else
@echo "Trying to pull build-image: $(BUILDER_IMAGE)"
docker pull -q $(BUILDER_IMAGE) || $(MAKE) build-image
$(CONTAINER_TOOL) pull -q $(BUILDER_IMAGE) || $(MAKE) build-image
endif
build-image:
@# When we build a new image we just untag the old one.
@# This makes sure we don't leave the orphaned image behind.
$(eval old_id=$(shell docker image inspect --format '{{ .ID }}' ${BUILDER_IMAGE} 2>/dev/null))
$(eval old_id=$(shell $(CONTAINER_TOOL) image inspect --format '{{ .ID }}' ${BUILDER_IMAGE} 2>/dev/null))
ifeq ($(BUILDX_ENABLED), true)
@cd hack/build-image && docker buildx build --build-arg=GOPROXY=$(GOPROXY) --output=type=docker --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) .
else
@cd hack/build-image && docker build --build-arg=GOPROXY=$(GOPROXY) --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) .
ifneq ($(CONTAINER_TOOL),docker)
$(error $(DOCKER_ONLY_ERROR))
endif
$(eval new_id=$(shell docker image inspect --format '{{ .ID }}' ${BUILDER_IMAGE} 2>/dev/null))
@cd hack/build-image && $(CONTAINER_TOOL) buildx build --build-arg=GOPROXY=$(GOPROXY) --output=type=docker --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) .
else
@cd hack/build-image && $(CONTAINER_TOOL) build --build-arg=GOPROXY=$(GOPROXY) --pull -t $(BUILDER_IMAGE) -f $(BUILDER_IMAGE_DOCKERFILE_REALPATH) .
endif
$(eval new_id=$(shell $(CONTAINER_TOOL) image inspect --format '{{ .ID }}' ${BUILDER_IMAGE} 2>/dev/null))
@if [ "$(old_id)" != "" ] && [ "$(old_id)" != "$(new_id)" ]; then \
docker rmi -f $$id || true; \
$(CONTAINER_TOOL) rmi -f $$id || true; \
fi
push-build-image:
ifneq ($(CONTAINER_TOOL),docker)
$(error $(DOCKER_ONLY_ERROR))
endif
@# this target will push the build-image it assumes you already have docker
@# credentials needed to accomplish this.
@# Pushing will be skipped if a custom Dockerfile was used to build the image.
@@ -392,17 +419,17 @@ else
endif
build-image-hugo:
cd site && docker build --pull -t $(HUGO_IMAGE) .
cd site && $(CONTAINER_TOOL) build --pull -t $(HUGO_IMAGE) .
clean:
# if we have a cached image then use it to run go clean --modcache
# this test checks if we there is an image id in the BUILDER_IMAGE_CACHED variable.
ifneq ($(strip $(BUILDER_IMAGE_CACHED)),)
$(MAKE) shell CMD="-c 'go clean --modcache'"
docker rmi -f $(BUILDER_IMAGE) || true
$(CONTAINER_TOOL) rmi -f $(BUILDER_IMAGE) || true
endif
rm -rf .go _output
docker rmi $(HUGO_IMAGE)
$(CONTAINER_TOOL) rmi $(HUGO_IMAGE)
.PHONY: modules
@@ -447,7 +474,7 @@ release:
./hack/release-tools/goreleaser.sh'"
serve-docs: build-image-hugo
docker run \
$(CONTAINER_TOOL) run \
--rm \
-v "$$(pwd)/site:/project" \
-it -p 1313:1313 \
+1
View File
@@ -0,0 +1 @@
Support selecting the data mover type (velero-fs or velero-block) through the volume policy snapshot action's dataMover parameter
+1
View File
@@ -0,0 +1 @@
Restores from backups not in a completed or partially failed phase are now rejected.
@@ -0,0 +1 @@
Fix issue #9813, add validations for ClusterScopedFilterPolicy
+1
View File
@@ -0,0 +1 @@
Support change-id and volume-id in backup workflow.
@@ -0,0 +1 @@
Fix issue #9815, implement core logic of backup with ClusterScopedFilterPolicy and NamespacedFilterPolicies
@@ -0,0 +1 @@
Fix issue #9816, add cli support for backup with ClusterScopedFilterPolicy and NamespacedFilterPolicies
+1
View File
@@ -0,0 +1 @@
Add WriteAt implementation for Incremental aware object writer for block data mover
+1
View File
@@ -0,0 +1 @@
Enhance RebindVolume method for generic restore exposer to support block data mover
@@ -0,0 +1 @@
Skip VGS cleanup when backup did not use VolumeGroupSnapshots
+1
View File
@@ -0,0 +1 @@
Refactor object writer interface to align with incremental aware object writer
+1
View File
@@ -0,0 +1 @@
Fix restore finalization overwriting dynamically provisioned PV labels with stale backup values
+1
View File
@@ -0,0 +1 @@
Add CONTAINER_TOOL variable to Makefile for podman support
+1
View File
@@ -0,0 +1 @@
feat(resourcepolicies): support PVC volume mode and access mode matching
@@ -0,0 +1 @@
Fix issue #9907, add cache for the GetNamespaceFilter call
+1
View File
@@ -0,0 +1 @@
Refactor generic restore exposer to clone the PV during rebind, so as to support block data mover
@@ -0,0 +1 @@
Design: Server default restore resource modifier
+1
View File
@@ -0,0 +1 @@
Add `--global-backup-volume-policies-configmap` server flag to configure cluster-wide global backup volume policies that are merged into every backup
+1
View File
@@ -0,0 +1 @@
Support fsType in DUCR and DDCR for data mover
+1
View File
@@ -0,0 +1 @@
Wait restorePV detached before binding the cloned PV to avoid confusing the CSI driver
+1
View File
@@ -0,0 +1 @@
Recall the old rebind volume way for the case that volumeMode is not changed; and use the new way for volumeMode changed case
@@ -0,0 +1 @@
Fix issue #9935, add resource policy on restore CRD
+1
View File
@@ -0,0 +1 @@
Decide restorePVC volumeMode by data mover type for block data mover
+1
View File
@@ -0,0 +1 @@
Extract pkg/apis into its own Go module with a local replace directive in the root go.mod
+1
View File
@@ -0,0 +1 @@
Fix flaky TestWaitExecHandleHooks test for 2-container hook ordering by synchronizing pod state changes with hook execution using channels
+1
View File
@@ -0,0 +1 @@
Add snapshot operations for block uploader
@@ -0,0 +1 @@
Fix issue #9936, restore filters via resource policy implementation
+1
View File
@@ -0,0 +1 @@
Add BackupType in backup.spec
+1
View File
@@ -0,0 +1 @@
Optimize VSC handle readiness polling for VSS backups
@@ -0,0 +1 @@
Fix issue #9937, add CLI support for restore filters via resource policy
+1
View File
@@ -0,0 +1 @@
Use forward slash as the path separator to make sure it works on both Linux and Windows nodes
@@ -0,0 +1 @@
Add image volume type support to volume policies
+1
View File
@@ -0,0 +1 @@
Add the backup implementation for block data mover
@@ -0,0 +1 @@
Validate user-provided labels and annotations in maintenance job
@@ -0,0 +1 @@
Fix globalExcludes lookup, it should be lookup against lower case
+1
View File
@@ -0,0 +1 @@
Surface DeleteItemAction plugin errors from InvokeDeleteActions so backup deletion fails and retries instead of silently orphaning data mover snapshots and other private artifacts
+1
View File
@@ -0,0 +1 @@
Add block device operations for block uploader backup
@@ -0,0 +1 @@
Fix PodVolumeBackup metadata loss on fs-backup timeout, which caused all fs-backup volumes to become unrestorable
@@ -41,6 +41,13 @@ spec:
spec:
description: BackupSpec defines the specification for a Velero backup.
properties:
backupType:
description: BackupType specifies how volume data is backed up, with
possible values including Full and Incremental.
enum:
- Full
- Incremental
type: string
csiSnapshotTimeout:
description: |-
CSISnapshotTimeout specifies the time used to wait for CSI VolumeSnapshot status turns to
@@ -404,6 +404,33 @@ spec:
- name
type: object
x-kubernetes-map-type: atomic
resourcePolicy:
description: |-
ResourcePolicy specifies the reference to a ConfigMap containing resource
filter policies for this restore. The ConfigMap can contain a
namespacedFilterPolicies section that specifies per-namespace resource type
filters, label selectors, and resource name patterns, and a
clusterScopedFilterPolicy section for per-kind filtering of cluster-scoped
resources. The ConfigMap format is the same as for BackupSpec.ResourcePolicy.
nullable: true
properties:
apiGroup:
description: |-
APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in the core API group.
For any other third-party types, APIGroup is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
x-kubernetes-map-type: atomic
restorePVs:
description: |-
RestorePVs specifies whether to restore all included
@@ -80,6 +80,13 @@ spec:
Template is the definition of the Backup to be run
on the provided schedule
properties:
backupType:
description: BackupType specifies how volume data is backed up,
with possible values including Full and Incremental.
enum:
- Full
- Incremental
type: string
csiSnapshotTimeout:
description: |-
CSISnapshotTimeout specifies the time used to wait for CSI VolumeSnapshot status turns to
File diff suppressed because one or more lines are too long
@@ -121,6 +121,9 @@ spec:
description: TargetVolume is the information of the target PVC and
PV.
properties:
fsType:
description: FSType is the file system type of the target volume.
type: string
namespace:
description: Namespace is the target namespace
type: string
@@ -133,6 +133,9 @@ spec:
description: SnapshotType is the type of the snapshot to be backed
up.
type: string
sourceFSType:
description: SourceFSType is the file system type of the source volume.
type: string
sourceNamespace:
description: |-
SourceNamespace is the original namespace where the volume is backed up from.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,122 @@
# Dynamic Resource Autocompletion for Velero CLI
## Abstract
Velero CLI has no dynamic shell completion for resource names ([#9782](https://github.com/vmware-tanzu/velero/issues/9782)).
Tab-completing `velero backup describe <TAB>` produces no suggestions, even when backups exist on the cluster.
This proposal adds dynamic completion for all commands that take Velero resource names as positional arguments or flag values (using cobra's built-in completion callbacks).
## Background
Shell completion is a standard UX feature in Kubernetes CLI tooling.
Tools like `kubectl`, `oc`, and `helm` all provide dynamic completions that query the cluster to suggest resource names.
Velero's `velero completion` command generates completion scripts, but the CLI does not register any completion callbacks, so tab-completing resource names produces no suggestions.
Cobra's completion infrastructure already supports dynamic completion across all shell types (bash, zsh, fish); Velero just needs to register the callbacks.
## Goals
- Add dynamic shell completion for all 20 commands that accept existing Velero resource names as positional arguments.
- Add dynamic flag completion for 9 flags that reference existing Velero resources.
- Fail silently when the cluster is unreachable, matching the behavior of `kubectl`.
## Non Goals
- Completing positional arguments for commands that take new resource names (e.g., `velero backup create <new-name>`).
- Completing flags that take non-resource values (e.g., `--include-namespaces`, `--labels`).
- Adding completion for hidden internal commands (`data-mover`, `pod-volume`, `repo-maintenance`).
- Caching cluster state across tab presses.
## High-Level Design
A centralized set of completion functions is added to `pkg/cmd/cli/completion_functions.go`.
Each function takes a `client.Factory`, returns a closure matching cobra's completion function signature, and lists resources of a specific type from the cluster.
Each command constructor wires the appropriate completion function onto its `cobra.Command` via `ValidArgsFunction` or `RegisterFlagCompletionFunc`.
## Detailed Design
### Completion functions
A new file `pkg/cmd/cli/completion_functions.go` provides six public functions:
| Function | Resource listed |
|---|---|
| `CompleteBackupNames(f client.Factory)` | `BackupList` |
| `CompleteRestoreNames(f client.Factory)` | `RestoreList` |
| `CompleteScheduleNames(f client.Factory)` | `ScheduleList` |
| `CompleteBackupStorageLocationNames(f client.Factory)` | `BackupStorageLocationList` |
| `CompleteVolumeSnapshotLocationNames(f client.Factory)` | `VolumeSnapshotLocationList` |
| `CompleteBackupRepositoryNames(f client.Factory)` | `BackupRepositoryList` |
All six delegate to a single private `completeNames` helper that uses `meta.ExtractList()` and `meta.Accessor()` to extract names from any `ObjectList` type.
The completion closure:
- Lists resources in the configured namespace with a **3-second context timeout**.
- Filters by `strings.HasPrefix(name, toComplete)`.
- Removes names already present in `args` to avoid re-suggesting previously typed arguments.
- Returns `cobra.ShellCompDirectiveNoFileComp` in all cases (success or failure).
- Fails silently on any error (client construction, API call, extraction), returning no suggestions.
### Commands wired with `ValidArgsFunction`
| Package | Commands | Completion function |
|---|---|---|
| `backup` | get, describe, delete, logs, download | `CompleteBackupNames` |
| `restore` | get, describe, delete, logs | `CompleteRestoreNames` |
| `schedule` | get, describe, delete, pause, unpause | `CompleteScheduleNames` |
| `backuplocation` | get, set, delete | `CompleteBackupStorageLocationNames` |
| `snapshotlocation` | get, set | `CompleteVolumeSnapshotLocationNames` |
| `repo` | get | `CompleteBackupRepositoryNames` |
### Flags wired with `RegisterFlagCompletionFunc`
| Command | Flag | Completion function |
|---|---|---|
| `backup create` | `--from-schedule` | `CompleteScheduleNames` |
| `backup create` | `--storage-location` | `CompleteBackupStorageLocationNames` |
| `backup create` | `--volume-snapshot-locations` * | `CompleteVolumeSnapshotLocationNames` |
| `schedule create` | `--storage-location` | `CompleteBackupStorageLocationNames` |
| `schedule create` | `--volume-snapshot-locations` * | `CompleteVolumeSnapshotLocationNames` |
| `restore create` | `--from-backup` | `CompleteBackupNames` |
| `restore create` | `--from-schedule` | `CompleteScheduleNames` |
| `debug` | `--backup` | `CompleteBackupNames` |
| `debug` | `--restore` | `CompleteRestoreNames` |
\* See Open Issues — comma-separated values.
## Alternatives Considered
The approach follows the standard cobra pattern for dynamic completion. No alternative designs were considered.
## Security Considerations
Completion functions issue read-only list requests using the user's existing kubeconfig credentials.
No new permissions are required beyond what the user already has.
Users without list permission receive empty completions, consistent with kubectl's behavior.
## Compatibility
Existing command behavior is unaffected.
`ValidArgsFunction` is only invoked during shell completion; it has no effect on normal command execution.
Completion respects the `--namespace` flag and `VELERO_NAMESPACE` environment variable.
## Testing
Unit tests in `pkg/cmd/cli/completion_functions_test.go` cover:
- **Core logic:** Table-driven tests across all six resource types: empty cluster, full match, prefix filtering, no match.
- **Error resilience:** Factory errors return nil completions without panicking.
- **Wrapper isolation:** Each `Complete*Names` wrapper returns only its own resource type.
## Open Issues
- **Single-argument commands:** Commands like `backup download` and `backup logs` accept exactly one positional argument, but cobra still calls the completion function after one arg is provided.
The completion function should check `len(args)` and return no suggestions when the maximum arg count is reached.
The approach (parameter on the helper vs. per-command wrapper) is TBD.
- **Comma-separated flag values:** `--volume-snapshot-locations` accepts comma-separated values.
Completion only works for the first value because `toComplete` contains the full string including commas.
Completing subsequent values would require comma-aware splitting, similar to how kubectl handles this.
- **Bash v1 to v2 migration:** The current bash completion generator already supports dynamic completion through cobra's `__complete` mechanism, so migration to v2 is not required for this feature.
A separate migration could be considered for other benefits (cleaner generated scripts, ActiveHelp support) but would require users to regenerate their completion scripts.
+417
View File
@@ -0,0 +1,417 @@
# Server Default Restore Resource Modifier
- [Server Default Restore Resource Modifier](#server-default-restore-resource-modifier)
- [Abstract](#abstract)
- [Background](#background)
- [Goals](#goals)
- [Non Goals](#non-goals)
- [High-Level Design](#high-level-design)
- [Detailed Design](#detailed-design)
- [Server Configuration](#server-configuration)
- [Restore API Change](#restore-api-change)
- [Controller Logic](#controller-logic)
- [Restore CLI](#restore-cli)
- [Restore Describe Output](#restore-describe-output)
- [Install Path](#install-path)
- [Curated Default ConfigMap Example](#curated-default-configmap-example)
- [Alternatives Considered](#alternatives-considered)
- [Security Considerations](#security-considerations)
- [Compatibility](#compatibility)
- [Implementation](#implementation)
- [Open Issues](#open-issues)
## Abstract
This proposal introduces a server-level default restore resource modifier for Velero.
A new `--default-resource-modifier-configmap` flag on the Velero server references a ConfigMap containing resource modifier rules that apply automatically to every restore, eliminating the need for per-restore configuration for common transformations like stripping stale CNI annotations.
## Background
When pods are backed up, CNI-managed annotations may be present that carry pod-specific networking state such as IP addresses, MAC addresses, and routes.
Restoring these stale values can cause networking failures because the CNI expects to inject fresh values and the restored annotations may conflict with the new cluster's network state.
The following annotations are commonly affected:
| Annotation | CNI |
|---|---|
| `k8s.ovn.org/pod-networks` | OVN-Kubernetes |
| `k8s.v1.cni.cncf.io/network-status` | Multus |
| `k8s.v1.cni.cncf.io/networks-status` | Multus |
Today, users can strip these annotations using [Resource Modifiers](https://velero.io/docs/main/restore-resource-modifiers/), but this requires authoring a ConfigMap and referencing it on every restore via `--resource-modifier-configmap`.
This is not discoverable for users unfamiliar with the feature and adds friction for a problem that affects most OpenShift and multi-CNI deployments.
Velero already strips certain annotations during restore as built-in behavior (e.g., `volume.kubernetes.io/selected-node` from PVCs).
This proposal extends that concept by allowing administrators to configure a default set of resource modifier rules at the server level.
## Goals
- Allow Velero administrators to configure a default resource modifier ConfigMap that applies to all restores without per-restore configuration.
- Provide a mechanism for individual restores to opt out of the default modifier.
- Ship a documented example ConfigMap that strips well-known CNI annotations.
## Non Goals
- Auto-creating a default ConfigMap during `velero install`. The mechanism is opt-in; administrators create and configure the ConfigMap.
- Merging default and per-restore resource modifier rules. When a per-restore modifier is specified, it takes exclusive precedence over the default.
- Supporting non-ConfigMap sources for default modifiers (e.g., CRDs, inline rules).
- Stripping CNI annotations via a built-in RestoreItemAction plugin. The resource modifier mechanism is the right abstraction for this.
## High-Level Design
A new `--default-resource-modifier-configmap` server flag references a ConfigMap name in the Velero namespace.
During restore, if no per-restore resource modifier is specified, the server loads and applies the default ConfigMap's rules.
When a per-restore modifier is specified via `--resource-modifier-configmap`, it takes exclusive precedence and the default is not applied.
A new `--skip-default-resource-modifier` flag on `velero restore create` allows opting out of the default per-restore.
This follows the existing pattern used by `--backup-repository-configmap` and `--repo-maintenance-job-configmap`.
## Detailed Design
### Server Configuration
Add a new field to the server `Config` struct and bind it as a CLI flag.
In `pkg/cmd/server/config/config.go`:
```go
type Config struct {
// ... existing fields ...
DefaultResourceModifierConfigMap string
}
```
```go
func (c *Config) BindFlags(flags *pflag.FlagSet) {
// ... existing flags ...
flags.StringVar(
&c.DefaultResourceModifierConfigMap,
"default-resource-modifier-configmap",
c.DefaultResourceModifierConfigMap,
"The name of a ConfigMap in the Velero namespace containing default resource modifier rules applied to all restores. "+
"Ignored when a per-restore resource modifier is specified.",
)
}
```
The default value is an empty string, meaning no default modifier is configured.
No change to `GetDefaultConfig()` is needed.
### Restore API Change
Add a new field to `RestoreSpec` for opting out of the default modifier.
In `pkg/apis/velero/v1/restore_types.go`:
```go
type RestoreSpec struct {
// ... existing fields ...
// SkipDefaultResourceModifier controls whether the server-configured default
// resource modifier is applied to this restore.
// When true, the default modifier is skipped even if configured on the server.
// Has no effect when a per-restore ResourceModifier is specified.
// +optional
// +nullable
SkipDefaultResourceModifier *bool `json:"skipDefaultResourceModifier,omitempty"`
}
```
This follows the existing RestoreSpec convention where optional booleans use `*bool` with `+nullable` (e.g., `RestorePVs`, `PreserveNodePorts`, `IncludeClusterResources`).
This preserves the ability to distinguish "unset" from "explicit false" if needed in the future.
### Controller Logic
Thread the new config value through to the restore controller and implement the precedence logic.
In `pkg/controller/restore_controller.go`, add a field to `restoreReconciler`:
```go
type restoreReconciler struct {
// ... existing fields ...
defaultResourceModifierConfigMap string
}
```
Update `NewRestoreReconciler` to accept and store the new parameter.
In `pkg/cmd/server/server.go`, pass `s.config.DefaultResourceModifierConfigMap` to `NewRestoreReconciler`.
Refactor `validateAndComplete` to use a shared helper for ConfigMap loading and implement the precedence logic:
```go
func (r *restoreReconciler) validateAndComplete(restore *api.Restore) (backupInfo, *resourcemodifiers.ResourceModifiers) {
// ... existing validation logic (unchanged) ...
// Resource modifier resolution: per-restore takes exclusive precedence over default.
var resourceModifiers *resourcemodifiers.ResourceModifiers
if restore.Spec.ResourceModifier != nil &&
strings.EqualFold(restore.Spec.ResourceModifier.Kind, resourcemodifiers.ConfigmapRefType) {
// Per-restore modifier specified: use it exclusively, ignore default.
resourceModifiers = r.loadResourceModifierConfigMap(
restore, restore.Spec.ResourceModifier.Name, false,
)
} else if r.defaultResourceModifierConfigMap != "" && !boolptr.IsSetToTrue(restore.Spec.SkipDefaultResourceModifier) {
// No per-restore modifier: apply server default if configured and not skipped.
resourceModifiers = r.loadResourceModifierConfigMap(
restore, r.defaultResourceModifierConfigMap, true,
)
}
return info, resourceModifiers
}
```
Extract the ConfigMap loading into a helper to avoid code duplication:
```go
// loadResourceModifierConfigMap loads and validates a resource modifier ConfigMap.
// When isDefault is true, errors are non-fatal (logged as warnings, returns nil).
// When isDefault is false, errors are added to restore.Status.ValidationErrors.
func (r *restoreReconciler) loadResourceModifierConfigMap(
restore *api.Restore, cmName string, isDefault bool,
) *resourcemodifiers.ResourceModifiers {
cm := &corev1api.ConfigMap{}
if err := r.kbClient.Get(
context.Background(),
client.ObjectKey{Namespace: restore.Namespace, Name: cmName},
cm,
); err != nil {
if isDefault {
r.logger.WithError(err).Warnf(
"Failed to retrieve default resource modifier configmap %s/%s, skipping",
restore.Namespace, cmName,
)
return nil
}
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
fmt.Sprintf("failed to get resource modifiers configmap %s/%s", restore.Namespace, cmName))
return nil
}
modifiers, err := resourcemodifiers.GetResourceModifiersFromConfig(cm)
if err != nil {
if isDefault {
r.logger.WithError(err).Warnf(
"Error parsing default resource modifier configmap %s/%s, skipping",
restore.Namespace, cmName,
)
return nil
}
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
errors.Wrapf(err, "Error in parsing resource modifiers provided in configmap %s/%s",
restore.Namespace, cmName).Error())
return nil
}
if err = modifiers.Validate(); err != nil {
if isDefault {
r.logger.WithError(err).Warnf(
"Validation error in default resource modifier configmap %s/%s, skipping",
restore.Namespace, cmName,
)
return nil
}
restore.Status.ValidationErrors = append(restore.Status.ValidationErrors,
errors.Wrapf(err, "Validation error in resource modifiers provided in configmap %s/%s",
restore.Namespace, cmName).Error())
return nil
}
source := "per-restore"
if isDefault {
source = "default"
}
r.logger.Infof("Retrieved %s resource modifiers from configmap %s/%s", source, restore.Namespace, cmName)
return modifiers
}
```
Key design decisions in this logic:
1. **Exclusive precedence**: When a per-restore modifier is specified, the default is not applied at all.
This is the simplest mental model and avoids complex merge semantics.
Users who want both default and custom rules can copy the default rules into their per-restore ConfigMap.
2. **Non-fatal default errors**: If the default ConfigMap is missing or invalid, log a warning and proceed without it.
A misconfigured default should not break all restores cluster-wide.
Per-restore modifier errors remain fatal (validation errors), preserving current behavior.
3. **SkipDefaultResourceModifier**: Allows opting out per-restore without specifying a per-restore modifier.
Has no effect when a per-restore modifier is specified (it already takes precedence).
### Restore CLI
Add a `--skip-default-resource-modifier` flag to `velero restore create`.
In `pkg/cmd/cli/restore/create.go`:
```go
type CreateOptions struct {
// ... existing fields ...
SkipDefaultResourceModifier bool
}
```
```go
func (o *CreateOptions) BindFlags(flags *pflag.FlagSet) {
// ... existing flags ...
flags.BoolVar(&o.SkipDefaultResourceModifier, "skip-default-resource-modifier", false,
"Skip applying the server-configured default resource modifier for this restore")
}
```
Set the field on the RestoreSpec when building the Restore object.
Only set it when the flag is true (using `boolptr.True()`) to leave it nil otherwise, consistent with how other `*bool` fields are handled:
```go
if o.SkipDefaultResourceModifier {
restore.Spec.SkipDefaultResourceModifier = boolptr.True()
}
```
### Restore Describe Output
Update the restore describer in `pkg/cmd/util/output/restore_describer.go` to show which resource modifier was applied and its source.
The describe output should reflect the resolved state:
- When the default resource modifier was applied, display its ConfigMap name and source:
```
Default Resource Modifier: default-restore-resource-modifiers
```
- When the default was skipped because `SkipDefaultResourceModifier` is true:
```
Default Resource Modifier: skipped (SkipDefaultResourceModifier=true)
```
- When the default was skipped because a per-restore modifier was specified, no extra output is needed since the per-restore modifier is already displayed under the existing `Resource Modifier` field.
- When the default was ignored due to a validation or retrieval error, the warning is already logged to the restore log. The describe output should not surface transient errors.
### Install Path
Add the flag to the install CLI and deployment builder so administrators can configure it during installation.
In `pkg/install/deployment.go`, add a `defaultResourceModifierConfigMap` field to `podTemplateConfig` with an option function:
```go
func WithDefaultResourceModifierConfigMap(name string) podTemplateOption {
return func(c *podTemplateConfig) {
c.defaultResourceModifierConfigMap = name
}
}
```
In the `Deployment()` function, append the CLI arg:
```go
if len(c.defaultResourceModifierConfigMap) > 0 {
args = append(args, fmt.Sprintf("--default-resource-modifier-configmap=%s",
c.defaultResourceModifierConfigMap))
}
```
Wire it through `VeleroOptions` in `pkg/install/resources.go` and the install CLI in `pkg/cmd/cli/install/install.go`.
Add a builder method to `pkg/builder/restore_builder.go`:
```go
func (b *RestoreBuilder) SkipDefaultResourceModifier(val bool) *RestoreBuilder {
b.object.Spec.SkipDefaultResourceModifier = &val
return b
}
```
### Curated Default ConfigMap Example
Provide a ready-to-use ConfigMap in `examples/default-resource-modifier-cni.yaml` that strips well-known CNI annotations:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: default-restore-resource-modifiers
namespace: velero
data:
resource-modifiers.yaml: |
version: v1
resourceModifierRules:
- conditions:
groupResource: pods
mergePatches:
- patchData: |
metadata:
annotations:
k8s.ovn.org/pod-networks: null
k8s.v1.cni.cncf.io/network-status: null
k8s.v1.cni.cncf.io/networks-status: null
```
This uses JSON Merge Patch to remove annotations by setting them to `null`.
Administrators can extend this ConfigMap with additional CNI-specific annotations (Calico, Cilium, etc.) or other stale metadata as needed.
Usage:
```bash
# Create the ConfigMap
kubectl apply -f examples/default-resource-modifier-cni.yaml
# Configure the Velero server to use it
# Option 1: During install
velero install --default-resource-modifier-configmap=default-restore-resource-modifiers ...
# Option 2: Edit existing deployment
kubectl -n velero edit deploy velero
# Add: --default-resource-modifier-configmap=default-restore-resource-modifiers
```
## Alternatives Considered
**Merge default and per-restore rules**: Instead of exclusive precedence, concatenate default and per-restore rules so both apply.
This avoids users having to copy default rules when specifying per-restore modifiers.
However, it introduces complexity around rule ordering and makes it harder to reason about what transformations will be applied.
It also makes it impossible to fully override the default for a specific restore without the `SkipDefaultResourceModifier` flag.
Exclusive precedence was chosen for simplicity.
Merge semantics can be revisited in a future enhancement if user demand warrants it.
**Built-in RestoreItemAction plugin**: Implement CNI annotation stripping as a built-in RIA plugin rather than using the resource modifier mechanism.
This would hard-code the logic and make it less configurable.
The resource modifier mechanism already supports this use case and is more flexible.
**Validate default ConfigMap at server startup**: Validate the ConfigMap when the server starts rather than at restore time.
Rejected because the ConfigMap may be created after the server starts and should not require a server restart to take effect.
**Auto-create default ConfigMap during install**: Have `velero install` automatically create the CNI-stripping ConfigMap.
Rejected for the initial release to minimize the change surface and let administrators opt in.
Can be added later as a default behavior or install flag.
## Security Considerations
No new security surface.
The default ConfigMap resides in the Velero namespace and is subject to the same RBAC controls as existing resource modifier ConfigMaps.
Only users with access to create/edit ConfigMaps in the Velero namespace can modify the default modifier rules.
## Compatibility
Fully backward compatible.
When `--default-resource-modifier-configmap` is not set (the default), behavior is identical to current Velero.
No changes to existing per-restore resource modifier behavior.
The new `SkipDefaultResourceModifier` field in RestoreSpec defaults to `false` and has no effect when no default modifier is configured.
## Implementation
1. Add `DefaultResourceModifierConfigMap` to `Config` struct and bind the CLI flag.
2. Add `SkipDefaultResourceModifier` to `RestoreSpec` and regenerate deepcopy/CRD.
3. Thread the config to `restoreReconciler` via `NewRestoreReconciler`.
4. Refactor `validateAndComplete` with `loadResourceModifierConfigMap` helper.
5. Add `--skip-default-resource-modifier` to the restore CLI.
6. Wire through the install path (deployment builder, install CLI).
7. Add unit tests for all precedence and error scenarios.
8. Create the example ConfigMap.
9. Update user documentation.
10. Add E2E test for default resource modifier.
## Open Issues
- Should additional CNI annotations (Calico, Cilium) be included in the curated example ConfigMap?
Feedback from the community on which annotations are commonly problematic would be helpful.
+162
View File
@@ -0,0 +1,162 @@
# Global Backup Volume Policies for Velero
## Background
Velero supports [resource policies](./Implemented/handle-backup-of-volumes-by-resources-filters.md) (commonly referred to as "volume policies") that let a user control how volumes are handled during a backup — for example, whether a volume is skipped, backed up via file-system backup (`fs-backup`), snapshotted, or handled by a custom plugin.
Today these policies are defined per-backup:
1. A user creates a ConfigMap in the Velero install namespace whose single data key holds a `ResourcePolicies` YAML document (`volumePolicies` and the related include/exclude and fine-grained filter policies).
2. The user opts a specific backup into that ConfigMap with the CLI flag `--resource-policies-configmap`, which sets `Backup.Spec.ResourcePolicy` as a reference to the ConfigMap.
3. When the backup is processed, velero loads the referenced ConfigMap, unmarshals the YAML, builds a `Policies` object, and applies it when performing the backup.
The limitation today is that volume policies are strictly opt-in **per backup**. An administrator, who usually has the best knowledge of the environment, may want a baseline behavior to apply to *every* backup in the cluster (for example, "always skip volumes from the `gp2` storage class", or "always use `fs-backup` for NFS volumes"). However, today they must remember to attach the same ConfigMap to every backup and every schedule. There is no way to express a cluster-wide default volume policy that is enforced regardless of what an individual backup requests.
## Goals
- Introduce "global backup volume policies" that an administrator configures once when the Velero server starts.
- Expose it as a Velero server CLI parameter that points to a ConfigMap in the Velero install namespace.
- When a backup runs, merge the global backup volume policies with the backup's own resource policies ConfigMap (if any) and use the merged result as the effective resource policies for that backup.
- Keep the existing per-backup `--resource-policies-configmap` behavior fully backward compatible when no global policy is configured.
## Non Goals
- Changing the schema of the `ResourcePolicies`/`volumePolicies` YAML itself.
- Defining global defaults for anything other than resource policies (e.g. it does not introduce new global backup spec defaults).
- Supporting per-namespace or per-schedule global policy overrides. The "global policies" is a single, server-wide configuration.
- Hot-reloading the global policies ConfigMap without a server restart is out of scope for the initial implementation.
- Support setting other filters in "resource policies" (e.g. include/exclude or fine-grained filters) in the global policy is out of scope for the initial implementation. Only `volumePolicies` will be supported in the global policy for now.
## Design
A new Velero server flag, `--global-backup-volume-policies-configmap`, accepts the name of a ConfigMap that lives in the Velero install namespace. The ConfigMap has the exact same format as an existing per-backup resource policies ConfigMap (a single data key holding a `ResourcePolicies` YAML document).
The flag value is plumbed from the server `Config` into the `backupReconciler`. During `prepareBackupRequest`, in addition to loading the backup's own resource policy (referenced by `Backup.Spec.ResourcePolicy`), Velero loads the global policy ConfigMap. The two `ResourcePolicies` documents are then **merged** into a single effective `ResourcePolicies`, which is compiled into a `Policies` object, validated, and stored on `request.ResPolicies` exactly as today. The rest of the backup pipeline is unchanged because it only consumes `request.ResPolicies`.
```
server flag --global-backup-volume-policies-configmap
|
v
Backup.Spec.ResourcePolicy global policies ConfigMap (install ns)
| |
v v
backup-level ResourcePolicies global ResourcePolicies
\ /
\ /
v v
merge() -> effective ResourcePolicies
|
v
Policies (compiled + validated)
|
v
request.ResPolicies (unchanged consumers)
```
### Volume Policy only
The resource policies ConfigMap schema includes both volume policies and include/exclude/fine-grained filter policies. The global backup volume policy only applies to the `volumePolicies` section of the schema. If the global ConfigMap includes any include/exclude/fine-grained filter policies, they are ignored and not merged into the effective policy. In this case, a warning message will be printed in the Velero server logs.
This is a design choice because only the volume policies are more tied to the environment where velero runs, and are more likely to be something an administrator would want to enforce globally. The include/exclude/fine-grained filter policies are more tied to the specific backup use case, and it would be less intuitive for an administrator to have those apply globally across all backups.
### Validation
Velero will validate the global backup volume policies ConfigMap at server startup. If the ConfigMap is missing or invalid, the server will fail to start and log an error. This ensures any mistakes in configuration will be caught early.
It should also make sure the validation happens for each backup, because the ConfigMap could be updated or removed after the server starts. If the global policies ConfigMap is missing or invalid at backup time, the backup CR will be put into "FailedValidation" phase, with an appropriate error message in the logs.
### Merge semantics
The merge combines two `ResourcePolicies` documents: the global policy (`G`) and the backup-level policy (`B`). The guiding principle is that the global policy provides a baseline, and the backup-level policy is layered with it.
- **`volumePolicies`**: `volumePolicies` is an ordered list where the *first* matching policy wins (per the existing `Policies.match` logic). The merged list is the concatenation of the backup-level policies followed by the global policies:
```
merged.volumePolicies = B.volumePolicies ++ G.volumePolicies
```
This gives a backup the ability to override the global baseline for a specific volume (because its policy is evaluated first), while still inheriting all global rules that the backup does not override.
When only the global policy is configured (the backup does not reference a resource policy), the effective policy is the global policy alone. When only the backup policy exists (no global policy configured), behavior is identical to today.
#### Example
Global policy ConfigMap (set on the server with `--global-backup-volume-policies-configmap=global-volume-policy`):
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: global-volume-policy
namespace: velero
data:
policies.yaml: |
version: v1
volumePolicies:
- conditions:
storageClass:
- gp2
action:
type: skip
```
Backup-level policy ConfigMap (referenced with `velero backup create --resource-policies-configmap backup01`):
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: backup01
namespace: velero
data:
policies.yaml: |
version: v1
volumePolicies:
- conditions:
nfs: {}
action:
type: fs-backup
```
Effective (merged) volume policies used for the backup — backup rules first, then global:
```yaml
version: v1
volumePolicies:
- conditions:
nfs: {}
action:
type: fs-backup
- conditions:
storageClass:
- gp2
action:
type: skip
```
### Output of `velero backup describe`
Currently, the `velero backup describe` command shows the backup-level resource policy. We should update the CLI to make sure the global volume policies are also shown in the output, so that user will not need to check the parameter of velero server.
## Implementation
- **Server flag and config.** Add a new field (e.g. `GlobalBackupVolumePoliciesConfigMap`) to the server `Config` struct in `pkg/cmd/server/config/config.go`, register the `--global-backup-volume-policies-configmap` flag in `Config.BindFlags`, and leave its default empty in `GetDefaultConfig` so the feature stays opt-in.
- **Plumb the value into the reconciler.** In `pkg/cmd/server/server.go`, pass the configured ConfigMap name (along with the Velero install namespace) into `controller.NewBackupReconciler`. Add a corresponding parameter and store it as a field on the `backupReconciler` struct in `pkg/controller/backup_controller.go`.
- **Load and merge the policies.** In `internal/resourcepolicies/resource_policies.go`, add a new function (e.g. `GetResourcePoliciesFromBackupWithGlobal`) that, in addition to loading the backup-referenced ConfigMap as `GetResourcePoliciesFromBackup` does today, also loads the global ConfigMap from the install namespace via the existing `getResourcePoliciesFromConfig` helper. After that the function merges the two `ResourcePolicies` documents according to the semantics described above.
- **Call site.** Update `prepareBackupRequest` in `pkg/controller/backup_controller.go` (currently calling `GetResourcePoliciesFromBackup`) to apply the merged policies from the new function. The rest of the backup pipeline remains unchanged.
- **CLI describe output.** Update `DescribeResourcePolicies` in `pkg/cmd/util/output/backup_describer.go` and `DescribeResourcePoliciesInSF` in `pkg/cmd/util/output/backup_structured_describer.go` to also surface the global volume policy ConfigMap that contributed to the backup.
## Security Considerations
The Global Backup Volume Policy is read from a ConfigMap in the Velero install namespace, the same trust boundary as existing resource policy ConfigMaps and Velero's own configuration. Setting it requires the ability to pass server flags / edit the Velero deployment, which is already an administrative privilege. No new data is exposed and no new external access patterns are introduced.
## Compatibility
- The feature is fully opt-in. If `--global-backup-volume-policies-configmap` is not set (the default), behavior is byte-for-byte identical to today.
- Existing per-backup `--resource-policies-configmap` usage is unchanged; it is simply merged with the global baseline when one is configured.
- Backups created before this feature, and backups that reference no resource policy, transparently start honoring the global policy once it is configured. This is the intended behavior of a "global" policy, but operators should be aware that introducing a global policy changes the effective behavior of backups that previously had no resource policy.
- The behavior of scheduled backup may change when a global backup volume policy is introduced, because the scheduled backup will start honoring the global volume policies. This is an expected change, but administrators should be aware of this when introducing a global policy to an existing velero instance with scheduled backups.
- The merged policy is computed at backup time and is reflected wherever `request.ResPolicies` is consumed. `velero backup describe` should be updated to indicate when a global policy contributed to a backup.
## Alternatives Considered
- **Global policies applied only when a backup has no policy of its own.** Simpler, but it makes the global policy a fallback default rather than an enforced baseline, and it cannot express "always do X in addition to whatever the backup wants". Merging is more expressive.
- **Global precedence over backup-level policies** (global volume policies evaluated first). Rejected as the default because it would prevent backups from overriding the baseline for specific volumes.
@@ -0,0 +1,833 @@
# Fine Grained Restore Filters via Resource Policies
This is a continuation of the work done for backup filters enhancement introduced by [PR 9783](https://github.com/velero-io/velero/pull/9783), referred to as Phase 1 throughout this design.
## Glossary & Abbreviation
**Restore Filter**: The mechanism in Velero that determines which resources from a backup archive are restored into the target cluster. Restore filters currently operate on four dimensions: namespace, resource type, label, and cluster scope.
**Global Filter**: A filter that applies uniformly across all namespaces in a restore. All existing Velero restore filters are global filters.
**Namespace-Scoped Filter**: A filter that applies only within specific namespaces, overriding the global filter for those namespaces. This is the capability introduced by this design.
**ClusterScopedFilterPolicy**: A global filter for cluster-scoped resources that allows per-kind label selectors and name patterns, functioning similarly to `NamespacedFilterPolicy` but applied to cluster-scoped resources globally. Mirrors the backup-side concept of the same name.
**Resource Filter**: A filter rule that pairs one or more resource kinds with their own label selector and/or name patterns. Multiple resource filters within a namespace-scoped policy allow different filtering criteria for different resource types.
**Resource Name Filter**: A filter that matches individual resource instances by their metadata.name, using glob patterns. This filter dimension was introduced in Phase 1 (backup-side) and is extended to restore in this design.
**Resource Policy**: An existing Velero mechanism where backup behavior rules are defined in a ConfigMap and referenced from `BackupSpec.ResourcePolicy`. Phase 1 extended this with `namespacedFilterPolicies` and `clusterScopedFilterPolicy` for backup. This design adds an analogous `RestoreSpec.ResourcePolicy` for restore, reusing the same ConfigMap format.
## Background
### Why Restore-Side Filters?
Phase 1 enables selective backup — for example, backing up only Deployments and ConfigMaps from `ns-a` while backing up everything from `ns-b`. However, backup-time filtering alone is insufficient for several real-world restore scenarios:
**Scenario 1 — Selective restore from a full backup.** An organization performs full-cluster backups (all namespaces, all resource types) for disaster recovery. When a specific application needs recovery, the administrator wants to restore only the application's resources (specific resource types, specific names) from a single namespace — without restoring monitoring, logging, or infrastructure resources that exist in the same namespace. Today, `RestoreSpec.IncludedResources` applies globally, so filtering out ConfigMaps means filtering them out of *every* namespace being restored.
**Scenario 2 — Cross-environment migration with selective resources.** When migrating workloads between clusters, different namespaces may need different resource types restored. A database namespace needs StatefulSets and PVCs but not Deployments; a frontend namespace needs Deployments and Services but not PVCs. The current global filter cannot express this.
**Scenario 3 — Restore with name-based selection.** A backup contains many ConfigMaps and Secrets in a namespace (e.g., `app-config`, `app-secret`, `monitoring-config`, `monitoring-secret`). The user wants to restore only the `app-*` resources. Without name-based filtering at restore time, this requires either pre-filtering at backup time (which may not have been done) or post-restore manual cleanup.
**Scenario 4 — Restore-time override of backup-time filters.** A backup was produced with `namespacedFilterPolicies` that included specific resources per namespace. At restore time, the operator may want to apply *different* per-namespace filters — for example, restoring only a subset of what was backed up, or applying different label selectors to handle environment differences.
### Existing Restore Filter Mechanisms
The restore pipeline currently supports:
| Filter | Scope | Where Applied |
|---|---|---|
| `RestoreSpec.IncludedNamespaces` / `ExcludedNamespaces` | Global | `getOrderedResourceCollection()` |
| `RestoreSpec.IncludedResources` / `ExcludedResources` | Global | `getOrderedResourceCollection()`, `restoreItem()` |
| `RestoreSpec.LabelSelector` / `OrLabelSelectors` | Global | `getSelectedRestoreableItems()` |
| `RestoreSpec.IncludeClusterResources` | Global | `getOrderedResourceCollection()` |
| `RestoreSpec.NamespaceMapping` | Per-namespace | `getSelectedRestoreableItems()` |
All resource-type, label, and name filters are global. There is no per-namespace override capability.
### Design Approach: New `RestoreSpec.ResourcePolicy` Field
Phase 1 avoided CRD changes for backup by reusing the existing `BackupSpec.ResourcePolicy` ConfigMap reference. For restore, no equivalent field exists — `RestoreSpec` has no `ResourcePolicy` field today.
Two approaches were evaluated:
**Option A — Reuse the backup's ResourcePolicy ConfigMap.** The restore pipeline could read the `namespacedFilterPolicies` from the backup's ConfigMap. This is rejected because:
- Restore should be able to apply *different* filters than backup
- The backup's ConfigMap may no longer exist at restore time
- The backup's ConfigMap is semantically about backup behavior, not restore
- The ConfigMap may have been updated since the backup was taken
- The ConfigMap may not exist on the target cluster, because it's maybe on a different velero instance.
**Option B — Add `RestoreSpec.ResourcePolicy` (minimal CRD change).** Add a single `TypedLocalObjectReference` field to `RestoreSpec`, mirroring the existing `BackupSpec.ResourcePolicy` and `RestoreSpec.ResourceModifier` patterns. This is a small, focused CRD change that follows an established pattern in the codebase.
This design uses **Option B**. The rationale:
| Consideration | Assessment |
|---|---|
| CRD change size | **Minimal** — one `TypedLocalObjectReference` field, identical pattern to `ResourceModifier` |
| Precedent | `RestoreSpec.ResourceModifier` already uses the exact same pattern (ConfigMap ref loaded in `validateAndComplete()`) |
| Independence from backup | Restore filters are decoupled from backup filters — different ConfigMap, different lifecycle |
| Reuse | The `NamespacedFilterPolicy` and `ClusterScopedFilterPolicy` types from Phase 1 (`internal/resourcepolicies/`) are reused unchanged |
### Why Not Just Reuse `BackupSpec.ResourcePolicy` Semantics?
The backup-side `ResourcePolicy` ConfigMap contains multiple policy types (`volumePolicies`, `includeExcludePolicy`, `namespacedFilterPolicies`, `clusterScopedFilterPolicy`). Rather than forcing users to create a ConfigMap with backup-specific sections just to specify restore filters, this design introduces a restore-specific ConfigMap format that contains only `namespacedFilterPolicies` and `clusterScopedFilterPolicy` (and potentially other restore-specific policies in the future).
The restore-side ConfigMap uses the **same YAML structure** for both sections. The `NamespacedFilterPolicy` and `ClusterScopedFilterPolicy` types are reused without modification. This means:
- Users who already understand the backup-side format can immediately use the restore-side one
- The `internal/resourcepolicies/` validation code is reused
- A single ConfigMap can be used for both backup and restore if the user wants (by specifying it in both `BackupSpec.ResourcePolicy` and `RestoreSpec.ResourcePolicy`)
## Goals
- Add a `ResourcePolicy` field to `RestoreSpec` pointing to a ConfigMap with `namespacedFilterPolicies` and/or `clusterScopedFilterPolicy`
- Reuse the `NamespacedFilterPolicy`, `ClusterScopedFilterPolicy`, and `ResourceFilter` types from Phase 1 unchanged
- Apply per-namespace resource type filters, label selectors, and resource name patterns during restore
- Apply per-kind label selectors and name patterns for cluster-scoped resources during restore
- Maintain full backward compatibility — existing restores without `ResourcePolicy` behave exactly as they do today
- Define clear precedence rules for how per-namespace filters interact with global restore filters
- Add corresponding validation in the restore controller
- Update `velero restore describe` output to display per-namespace and cluster-scoped filter information when present
- Ensure restore-side filters work correctly with both filtered and unfiltered backups
## Non-Goals
- Modifying the existing `NamespacedFilterPolicy`, `ClusterScopedFilterPolicy`, or `ResourceFilter` types or the `internal/resourcepolicies/` package structure (reused as-is from Phase 1)
- Adding volume policies or include/exclude policies to the restore-side ResourcePolicy ConfigMap
- Supporting regex patterns for resource names (glob patterns only, consistent with Phase 1)
- Modifying the restore plugin `ResourceSelector` system (`AppliesTo()` / `resolvedAction.ShouldUse()`)
- CLI flags for inline specification of namespace-scoped restore filters (configuration is in ConfigMap YAML)
## Architecture of Restore-Side Filters
### Configuration Model
The restore-side filters are defined in a ConfigMap referenced by a new `RestoreSpec.ResourcePolicy` field. The ConfigMap YAML format reuses the `namespacedFilterPolicies` and `clusterScopedFilterPolicy` sections from Phase 1, with the same `resourceFilters` model:
```yaml
version: v1
clusterScopedFilterPolicy:
# NEW: global overrides for cluster-scoped resources during restore
resourceFilters:
- kinds: [ClusterRole, ClusterRoleBinding]
names: ["my-app-*"]
- kinds: [CustomResourceDefinition]
labelSelector:
app: my-app
namespacedFilterPolicies:
- namespaces:
- ns-a
resourceFilters:
- kinds: [ConfigMap, Secret, Deployment]
labelSelector:
app: my-app
- namespaces:
- ns-b
resourceFilters:
- kinds: [Deployment]
names: [app-1, app-2]
- kinds: [ConfigMap]
labelSelector:
app: my-service
```
The restore-side ConfigMap does **not** require `volumePolicies` or `includeExcludePolicy` sections. Those are backup-specific. The YAML parser will ignore unknown fields gracefully, so a user can technically point to the same ConfigMap used for backup — the restore pipeline will only read `namespacedFilterPolicies` and `clusterScopedFilterPolicy`.
### The `resourceFilters` Model
Each `namespacedFilterPolicies` entry targets one or more namespaces and contains a `resourceFilters` array. Each entry in `resourceFilters` pairs one or more resource kinds with their own label selector and name patterns:
```yaml
namespacedFilterPolicies:
- namespaces: [ns-a]
resourceFilters:
- kinds: [ConfigMap, Secret] # these kinds share a selector
labelSelector: {app: my-app}
names: ["app-*"]
- kinds: [Deployment] # this kind has its own selector
names: [workload-1, workload-2]
- kinds: [StatefulSet] # this kind has no extra filtering
```
Only resource kinds listed in `resourceFilters` entries are restored for the matched namespaces; unlisted kinds are implicitly excluded (globally excluded kinds cannot be re-included — see precedence model).
#### Peek-and-Map Fallback for Unresolved Kinds
The `kinds` field accepts both plural resource names (e.g., `configmaps`, `mycustomkinds.mygroup.io`) and singular `Kind` names (e.g., `ConfigMap`, `MyCustomKind`).
To ensure consistent case-insensitive behavior across all code paths, Velero normalizes all input `kinds` to lowercase *before* attempting discovery or fallback matching.
During a restore, Velero attempts to resolve `Kind` names to fully-qualified plural resource names using the cluster's discovery helper. However, for Custom Resources (CRDs), the CRD might not exist in the cluster yet when the restore begins.
To handle this, Velero implements a **peek-and-map fallback**:
1. If a normalized `Kind` cannot be resolved via the discovery helper at the start of the restore, Velero stores the normalized string as provided in the policy.
2. Later, when iterating through the backup tarball, if Velero encounters a resource type (e.g., `mycustomkinds.mygroup.io`) that doesn't match any resolved filters, it peeks at the `Kind` of the first item in the tarball for that resource type.
3. It then checks if this actual `Kind` (case-insensitively) matches any of the unresolved normalized strings in the user's policy.
4. If a match is found, the filter is applied and cached for subsequent lookups.
This ensures that users can intuitively write `kinds: [MyCustomKind]` and it will work reliably, even if the CRD hasn't been restored yet. This logic applies to both `namespacedFilterPolicies` and `clusterScopedFilterPolicy`.
#### Catch-All Resource Filter (Empty `kinds` or `["*"]`)
A `ResourceFilter` entry with an empty (or omitted) `kinds` field, or a field explicitly set to `["*"]`, acts as a **catch-all**. Its `labelSelector` or `orLabelSelectors` (if provided) is applied to **all resource types in the namespace that are not already matched by a kind-specific filter entry**. If no selectors are provided, all unlisted resources are included. Using `["*"]` is highly recommended as it makes the catch-all intention explicit and self-documenting.
**Rules for catch-all entries:**
- At most **one** catch-all entry is allowed per `NamespacedFilterPolicy`.
- `names` and `excludedNames` are **not** supported on catch-all entries. Name patterns are kind-specific by nature and cannot be applied across arbitrary kinds; use kind-specific entries for name-based filtering.
- The catch-all applies to kinds that are **not listed in any other `resourceFilters` entry** in the same policy. Kind-specific entries take precedence over the catch-all.
- A catch-all entry **does not inherit or fall back to `RestoreSpec.LabelSelector`**. If a catch-all entry has no `labelSelector`/`orLabelSelectors`, all unlisted resource kinds in the namespace are included with **no label filtering** — the global label selector is not applied.
- **Catch-all is a `namespacedFilterPolicies`-only feature**. `clusterScopedFilterPolicy` does **not** support catch-all entries (empty or `["*"]` kinds). This is because `clusterScopedFilterPolicy` is a refinement overlay — unlisted cluster-scoped kinds already fall back to global filters by default. A catch-all would conflict with that fallback semantics. Validation rejects catch-all entries in `clusterScopedFilterPolicy`.
**Evaluation order within a namespace filter policy:**
1. For each resource kind encountered during restore, the system first checks whether a kind-specific `resourceFilters` entry exists for that kind.
2. If a kind-specific entry exists, it is used exclusively (label selectors, name patterns from that entry).
3. If no kind-specific entry exists but a catch-all entry is present, the catch-all's `labelSelector`/`orLabelSelectors` is applied to that kind.
4. If neither a kind-specific entry nor a catch-all entry exists, the kind is excluded from the restore for that namespace.
### Filter Precedence Model
The restore-side namespace-scoped filter system layers on top of the existing global restore filter system. The evaluation order is:
1. **Global namespace filter** (`RestoreSpec.IncludedNamespaces`/`ExcludedNamespaces`) is checked first. A namespace must pass this filter to be considered at all. `namespacedFilterPolicies` cannot override namespace exclusion — if a namespace is excluded globally, no filter policy entry can bring it back.
2. **Global resource type filter** (`RestoreSpec.IncludedResources`/`ExcludedResources`) is checked next. A resource type must pass the global filter to be considered. Per-namespace filters can further narrow the set of resource types within a namespace, but cannot include a resource type that is globally excluded.
3. **Per-namespace filter lookup.** For each namespace that passes the global filters, the system checks whether any `namespacedFilterPolicies` entry matches (by namespace name or glob pattern). If a match is found, the `resourceFilters` array determines what gets restored for that namespace:
- Only resource kinds listed in `resourceFilters[].kinds` are restored (globally excluded kinds cannot be re-included by a per-namespace policy)
- Each kind uses its own `labelSelector`/`orLabelSelectors` from its `ResourceFilter` entry, **replacing** the global label selector for that kind
- Each kind uses its own `names`/`excludedNames` patterns from its `ResourceFilter` entry
4. **Namespaces without a matching filter policy** continue to use the global filters (`RestoreSpec.IncludedResources`, `RestoreSpec.LabelSelector`, etc.) exactly as they do today.
5. **If multiple filter policy entries could match the same namespace** (e.g., `team-*` and `team-frontend-*` both matching `team-frontend-prod`), the **first matching policy in the list** is used. **Important: Place more specific patterns before broader patterns** to achieve the intended filtering behavior.
6. **Namespace mapping** is applied after filter lookup. If `RestoreSpec.NamespaceMapping` maps `ns-a` to `ns-a-restored`, the filter policy lookup uses the *original* namespace name (`ns-a`), since the ConfigMap was authored against the backup's namespace structure.
**For Cluster-Scoped Resources:**
1. If `clusterScopedFilterPolicy` is present, it acts as a **refinement overlay** over the existing global filters for cluster-scoped resources. It is NOT an exclusive allowlist.
- If a cluster-scoped kind is listed in its `resourceFilters`, its specific `labelSelector`/`orLabelSelectors` and `names`/`excludedNames` patterns are applied.
- If a cluster-scoped kind is **not listed**, it falls back to the standard global filters (`RestoreSpec.LabelSelector`, etc.).
2. If `clusterScopedFilterPolicy` is absent, Velero falls back to the existing global filters (`IncludedResources`, `LabelSelector`, etc.) for cluster-scoped resources.
3. **The `velero.io/exclude-from-backup=true` label** always takes precedence over all filters. Although named for backup, this label is set on resources at backup time and remains present on items in the archive. The restore pipeline honors it: any item carrying this label is skipped regardless of whether it matches global or per-namespace restore filters.
```mermaid
flowchart TD
A["RestoreSpec Global<br>IncludedNamespaces / ExcludedNamespaces"]
B{Namespace passes<br>global filter?}
C[Namespace excluded<br>from restore]
D{"Resource type passes<br>IncludedResources / ExcludedResources?"}
E[Resource type excluded<br>from restore]
G{namespacedFilterPolicies<br>lookup by original namespace}
H{"For each resource kind:<br>is kind in resourceFilters?"}
I["Apply namespace kind-specific filters:<br>- labelSelector / orLabelSelectors<br>- names / excludedNames"]
J[Kind skipped for<br>this namespace]
K["Use global filters:<br>- RestoreSpec LabelSelector<br>- RestoreSpec OrLabelSelectors"]
L{"Is resource<br>cluster-scoped?"}
M{"Is clusterScopedFilterPolicy<br>present?"}
N{"Is kind in clusterScopedFilterPolicy<br>resourceFilters?"}
O["Apply cluster kind-specific filters:<br>- labelSelector / orLabelSelectors<br>- names / excludedNames"]
L -- Yes --> M
M -- Yes --> N
N -- Yes --> O
N -- No --> K
M -- No --> K
L -- No --> A
A --> B
B -- No --> C
B -- Yes --> D
D -- No --> E
D -- Yes --> G
G -- Match found --> H
H -- Yes --> I
H -- No --> J
G -- No match found --> K
```
### Key Difference from Backup-Side Precedence
Both sides enforce the same fundamental rule: **a per-namespace filter policy cannot re-include a resource kind that has been globally excluded**. The difference lies in which global gate enforces this constraint and how unlisted kinds are handled for namespaces *without* a matching filter policy:
- **Backup side**: The global exclusion gate is `includeExcludePolicy` (in the ResourcePolicy ConfigMap). It runs first at the resource-type level before any per-namespace lookup occurs. For a namespace that *has* a matching `namespacedFilterPolicies` entry, the per-namespace kind list acts as an exclusive allowlist — only listed kinds are collected, and no fallback to `BackupSpec.IncludedResources` occurs. However, any kind that `includeExcludePolicy` globally excludes remains excluded even if it appears in the per-namespace `resourceFilters`. For a namespace *without* a matching entry, the standard global filters (`BackupSpec.IncludedResources`, `BackupSpec.LabelSelector`, `includeExcludePolicy`) apply as before. See point 6 in the backup design's Filter Precedence Model (`fine-grained-backup-filters-design.md`) for the full treatment, including the warning log emitted when a per-namespace entry lists a globally excluded kind.
- **Restore side**: The global exclusion gate is `RestoreSpec.IncludedResources`/`ExcludedResources` directly on the RestoreSpec. It runs first, globally. For a namespace that *has* a matching `namespacedFilterPolicies` entry, the per-namespace kind list acts as an exclusive allowlist within what the global gate permits — a kind must pass the global filter and be listed in `resourceFilters` to be restored. No fallback to `RestoreSpec.IncludedResources` for additional kinds occurs. For a namespace *without* a matching entry, the standard global filters apply as before. See the "Interaction with Global `IncludedResources`/`ExcludedResources`" entry in the Edge Cases section below for a detailed example.
In both cases, per-namespace policies are an **allowlist that operates within globally established bounds** — the label selector for a matched kind is fully replaced by the per-namespace one on both sides.
For label selectors, **replacement** semantics are used on both sides, because label selectors are typically workload-specific and a per-namespace selector is a complete override of the filtering intent for that namespace.
| | Backup | Restore |
|---|---|---|
| **Data source** | Live cluster — items are listed from Kubernetes API | Backup archive — items are read from tarball |
| **Operator intent** | "What should go into the archive for this namespace?" | "Of what's in the archive, what should I restore for this namespace?" |
| **Global exclusion gate** | `includeExcludePolicy` in ResourcePolicy ConfigMap | `RestoreSpec.IncludedResources` / `ExcludedResources` |
| **Namespaces without a matching policy** | Fall back to `BackupSpec.IncludedResources` + `includeExcludePolicy` | Fall back to `RestoreSpec.IncludedResources` / `ExcludedResources` |
| **Per-namespace label selector** | Replaces global label selector for that kind | Replaces global label selector for that kind |
| **clusterScopedFilterPolicy behavior** | Refinement overlay (unlisted kinds fall back to global) | Refinement overlay (unlisted kinds fall back to global) |
### Data Flow in the Restore Pipeline
The restore pipeline has two phases: resource selection and item restore. Namespace-scoped filters are applied in both:
**Phase A — Resource Selection (`getOrderedResourceCollection()` + `getSelectedRestoreableItems()`)**
Resources are enumerated from the backup archive (not from the live cluster — this is a key difference from backup).
- **Resource type check** in `getOrderedResourceCollection()`: The global resource type check still applies. Within the namespace iteration, a per-namespace resource type check is added. If a filter policy matches the current namespace, only kinds listed in `resourceFilters[].kinds` (or matched by a catch-all) are restored — unlisted kinds are skipped for that namespace. Globally excluded kinds cannot be re-included by a per-namespace policy.
- **Label selector** in `getSelectedRestoreableItems()`: The function looks up the filter policy for the current namespace and retrieves the `ResourceFilter` entry for the current resource kind. If found, it uses that entry's `labelSelector`/`orLabelSelectors` instead of the global ones. If not found, the global selectors are used as before.
- **Name pattern check** in `getSelectedRestoreableItems()`: After the label selector check, the item's name is checked against the `ResourceFilter` entry's `names`/`excludedNames` glob patterns for the current kind.
**Phase B — Item Restore (`restoreItem()`)**
The `restoreItem()` function is called for each selected item and also for "additional items" requested by restore plugins.
**Important:** Like the backup-side Stage 2 which is permissive for unlisted kinds requested by plugins, the restore-side Phase B is permissive for AdditionalItems requested by plugins regarding kind, name, and label selectors. This means if a plugin requests an AdditionalItem, it bypasses the fine-grained `namespacedFilterPolicies` and `clusterScopedFilterPolicy` checks, though it must still pass global resource/namespace exclusions. This is intentional to ensure that semantic dependencies (like a PV needed by a PVC) are successfully restored even if their specific resource kind or name pattern wasn't explicitly allowed in the user's namespace-scoped filter policy.
### Interaction with NamespaceMapping
When `RestoreSpec.NamespaceMapping` remaps namespaces (e.g., `ns-a` -> `ns-a-staging`), the filter policy lookup uses the **original** (backup-side) namespace name. This is because:
- The filter ConfigMap is authored against the backup's namespace structure
- The archive directory structure uses the original namespace names
- The `getSelectedRestoreableItems()` function receives `originalNamespace` and applies mapping afterward
The `getNamespaceFilter()` method on `restoreContext` takes the original namespace name as input.
### Interaction with Existing Restore Features
| Feature | Interaction |
|---|---|
| `RestoreSpec.RestorePVs` | Orthogonal — controls PV snapshot restoration, not resource inclusion |
| `RestoreSpec.ExistingResourcePolicy` | Orthogonal — controls overwrite behavior for resources that pass all filters |
| `RestoreSpec.RestoreStatus` | Orthogonal — controls status field restoration for resources that pass all filters |
| `RestoreSpec.Hooks` | Applied to resources that pass all filters. Hooks run regardless of how the item was selected |
| `RestoreSpec.ResourceModifier` | Applied to resources that pass all filters. Modifiers run on resources after filter selection |
| `RestoreSpec.PreserveNodePorts` | Orthogonal — applies to Services that pass all filters |
| Restore Item Actions (plugins) | Plugins may request "additional items." These go through `restoreItem()` which permits them, bypassing the fine-grained filter checks (similar to backup side Stage 2). |
### Edge Cases and Behavior Documentation
**Plugin Additional Items (Restore-Side):**
Like the backup side — which is permissive at Stage 2 to allow CSI plugin-injected resources through — the restore side is permissive for AdditionalItems in `restoreItem()`. If a restore plugin requests an additional item, it is allowed to bypass the fine-grained `namespacedFilterPolicies` and `clusterScopedFilterPolicy` kind, name, and label selector checks. This allows plugins to successfully restore dependencies (like a PV needed by a PVC, or a specific Secret) without the user having to explicitly authorize every single dependent resource type in their configuration. Note that these additional items must still pass global resource/namespace exclusions.
**Multiple Glob Patterns Matching Same Namespace (Incorrect Order):**
```yaml
namespacedFilterPolicies:
- namespaces: ["team-*"] # Broader pattern listed first
resourceFilters:
- kinds: [Deployment, Service]
- namespaces: ["team-frontend-*"] # More specific pattern listed second
resourceFilters:
- kinds: [ConfigMap, Secret, Deployment, Service]
```
**Behavior:** For namespace `team-frontend-prod`, the broader `team-*` pattern matches first, so only `Deployment` and `Service` are restored. The more specific `team-frontend-*` rule is never reached.
**Multiple Glob Patterns Matching Same Namespace (Correct Order):**
```yaml
namespacedFilterPolicies:
- namespaces: ["team-frontend-*"] # More specific pattern listed first
resourceFilters:
- kinds: [ConfigMap, Secret, Deployment, Service]
- namespaces: ["team-*"] # Broader pattern listed second
resourceFilters:
- kinds: [Deployment, Service]
```
**Behavior:** For namespace `team-frontend-prod`, the specific `team-frontend-*` pattern matches first, restoring all specified resources. For `team-backend-dev`, the broader `team-*` pattern matches, restoring only `Deployment` and `Service`. This achieves the intended behavior.
**Namespace Included Globally But No Matching Filter Policy:**
```yaml
# RestoreSpec includes "production" namespace
# ResourcePolicy has no namespacedFilterPolicies entry for "production"
```
**Behavior:** The namespace uses global filters exactly as it does today. This is the backward compatibility behavior.
**Empty ResourceFilters Array:**
```yaml
namespacedFilterPolicies:
- namespaces: ["test-namespace"]
resourceFilters: [] # empty array
```
**Behavior:** Validation error during restore creation:
```
namespacedFilterPolicies[0]: at least one resourceFilter must be specified
```
**Namespace Pattern with No Matches:**
```yaml
namespacedFilterPolicies:
- namespaces: ["nonexistent-*"]
resourceFilters: [...]
```
**Behavior:** No error. The filter policy is loaded but never applied since no namespaces match the pattern.
**Resource Kind Not Present in Target Namespaces:**
```yaml
resourceFilters:
- kinds: ["StatefulSet"] # namespace has no StatefulSets in the backup archive
names: ["workload-1"]
```
**Behavior:** No error. The filter is applied but finds no matching resources. Empty result set is valid.
**Conflicting Name Patterns:**
```yaml
resourceFilters:
- kinds: ["ConfigMap"]
names: ["app-*"]
excludedNames: ["app-config"] # conflicts with names pattern
```
**Behavior:** The `excludedNames` takes precedence. Resources matching `app-*` are included, then `app-config` is excluded. Net result: includes `app-secret`, `app-data`, etc., but excludes `app-config`.
**Invalid Label Selector Syntax:**
```yaml
resourceFilters:
- kinds: ["Deployment"]
labelSelector:
"invalid label key!": "value" # invalid key syntax
```
**Behavior:** Validation error during restore creation when `labels.ValidatedSelectorFromSet()` fails:
```
namespacedFilterPolicies[0].resourceFilters[0]: invalid label selector: "invalid label key!" is not a valid label key
```
**Out-of-Scope Kinds in Filter Entries:**
A user may accidentally list a cluster-scoped kind (e.g., `ClusterRole`) inside a `namespacedFilterPolicies` entry, or a namespace-scoped kind (e.g., `ConfigMap`) inside `clusterScopedFilterPolicy`. The system silently ignores such entries at the archive traversal level: namespace-scoped items are never in the cluster-scope portion of the archive, and vice versa. A warning is logged at restore start so the user can detect the misconfiguration:
```
WARN kind "ClusterRole" in namespacedFilterPolicies[0].resourceFilters[1] is a cluster-scoped resource; it will never match in a namespace-scoped filter — did you mean clusterScopedFilterPolicy?
```
**Discovery Helper Unavailable:**
If the discovery helper is unavailable during restore initialization, the restore fails with:
```
failed to resolve namespace filter policies: discovery client unavailable
```
**Interaction with Global `IncludedResources`/`ExcludedResources`:**
`namespacedFilterPolicies` operates within the bounds already established by the global resource type filter — it is a refinement, not a replacement. `RestoreSpec.IncludedResources`/`ExcludedResources` is applied first at the resource-type level, before any per-namespace filter policy is consulted. A namespace-scoped filter policy cannot re-include a resource kind that has been globally excluded.
Two separate gates are applied in order:
1. **`RestoreSpec.IncludedResources`/`ExcludedResources` runs first**, globally, across all namespaces. It decides which resource types are eligible at all.
2. **`namespacedFilterPolicies` runs second**, within the bounds established by step 1. It can only further restrict kinds that survived the global gate — it cannot widen it.
```yaml
# RestoreSpec
excludedResources: [secrets] # global — Secrets excluded from all namespaces
# ResourcePolicy ConfigMap
namespacedFilterPolicies:
- namespaces: [ns-a]
resourceFilters:
- kinds: [ConfigMap, Secret] # Secret listed here is ineffective — globally excluded
labelSelector:
app: my-app
- kinds: [Deployment]
```
**What gets restored from `ns-a`:**
- `ConfigMap` with label `app=my-app` — restored (listed in per-namespace policy, not globally excluded)
- `Secret` with label `app=my-app`**not restored** (globally excluded by `ExcludedResources`, even though listed in the per-namespace policy)
- `Deployment` — restored (listed in per-namespace policy, not globally excluded)
The "no fallback to `RestoreSpec.IncludedResources`" rule means that for a namespace *with* a matching policy, only the kinds listed in `resourceFilters` are candidates for restore — `RestoreSpec.IncludedResources` is not consulted to add additional kinds. The global `ExcludedResources` exclusions, however, still apply because they are enforced at an earlier, separate stage.
To restore `Secret` in specific namespaces, users must remove `secrets` from `ExcludedResources` globally, or restructure their policy.
A warning is logged at restore start when a `namespacedFilterPolicies` entry lists a kind that is globally excluded:
```
level=warn msg="namespacedFilterPolicies entry lists a kind that is globally excluded by RestoreSpec.ExcludedResources; the per-namespace filter entry has no effect" kind="secrets" namespacePattern="ns-a"
```
> **See also:** The backup-side design's "Interaction with `includeExcludePolicy`" (point 6 in the Filter Precedence Model of `fine-grained-backup-filters-design.md`) documents the structurally identical behavior for backup. The only difference is the global gate: on the backup side it is `includeExcludePolicy` (in the ResourcePolicy ConfigMap); on the restore side it is `RestoreSpec.IncludedResources`/`ExcludedResources` (on the RestoreSpec directly).
# Detailed Design
## Workflow
### Restore Workflow
The restore workflow is preserved with the following additions. The modules in the existing restore path remain unchanged when `ResourcePolicy` is absent from `RestoreSpec`.
**Step 1 — Load and parse policies (in `restore_controller.go`, `validateAndComplete()`)**
The restore controller loads the ConfigMap, similar to how `ResourceModifier` is loaded today:
The loaded policies are passed through to `runValidatedRestore()` and stored on the `restore.Request`.
**Step 2 — Resolve namespace and cluster-scoped filter maps (in `restore.go`, `RestoreWithResolvers()`)**
After existing filter setup, the filter policies are resolved into the runtime maps:
The `resolveRestoreNamespacedFilterPolicies` function:
- For each `NamespacedFilterPolicy`, iterates its `ResourceFilters` entries
- Resolves kind names to fully-qualified group-resource strings using the discovery helper
- Converts `labelSelector` maps into `labels.Selector` objects using `labels.ValidatedSelectorFromSet()`
- Converts `orLabelSelectors` maps into `[]labels.Selector`
- Creates `IncludesExcludes` instances for `names`/`excludedNames` patterns
- Identifies catch-all entries (empty or `["*"]` kinds) and stores them in `catchAllFilter`
- Builds a `resourceFilterMap` keyed by the resolved group-resource string
- Returns both the map and an ordered `namespacedFilterPatterns` slice for first-match traversal
**Step 3 — Per-namespace resource type check (in `restore.go`, `getOrderedResourceCollection()`)**
Inside the namespace iteration, after the global namespace check and global resource type check, and before calling `getSelectedRestoreableItems()`:
**Step 4 — Label selector and name filter (in `restore.go`, `getSelectedRestoreableItems()`)**
Before the items loop, resolve the effective `ResourceFilter` (hoisted for performance). The function handles three cases in order:
1. **Namespace-scoped item with a matching `namespacedFilterPolicies` entry** — resolve the effective `ResourceFilter` by checking the kind-specific entry first, then falling back to the catch-all
2. **Cluster-scoped item with the kind listed in `clusterScopedFilterPolicy`** — apply that kind's label/name filters (refinement overlay; unlisted cluster-scoped kinds fall through to global)
3. **All other cases** — fall back to the existing global label selector logic
**Note on cluster-scoped resources:** There is no separate kind-level skip step in `getOrderedResourceCollection()` for cluster-scoped resources analogous to Step 3. `clusterScopedFilterPolicy` is a refinement overlay — unlisted cluster-scoped kinds are not skipped; they fall through to existing global filter handling. Behavior changes only when the kind is explicitly listed in `clusterScopedFilterMap`, and only in `getSelectedRestoreableItems()` (above).
### Backup Workflow
No changes. The backup pipeline is unaffected by this design.
### Delete Workflow
No changes. Restore deletion removes the restore metadata. The backup archive is unaffected.
## Validation
The following validation is added in `restore_controller.go`'s `validateAndComplete()`:
1. **ConfigMap existence and format**: Handled by `GetResourcePoliciesFromRestore()`, which returns validation errors if the ConfigMap is missing, malformed, or fails `Policies.Validate()`.
2. **`ResourcePolicy.Kind` must be `"configmap"`** (case-insensitive): Consistent with `BackupSpec.ResourcePolicy` and `RestoreSpec.ResourceModifier`.
3. **Namespace filter policy validation** (delegated to `Policies.Validate()`):
- Each filter policy must specify at least one namespace
- Each filter policy must specify at least one resource filter
- Each resource filter without kinds can only be defined once (at most one catch-all), and cannot specify `names`/`excludedNames`
- No duplicate kinds across resource filter entries within the same namespace filter
- `labelSelector` and `orLabelSelectors` cannot co-exist within each resource filter
- No duplicate exact namespace patterns across filter policies (overlapping glob patterns are allowed — first-match semantics handle them at runtime)
- Name/excludedNames patterns must be valid globs
4. **`clusterScopedFilterPolicy` validation** (delegated to `Policies.Validate()`):
- At least one resourceFilter must be specified
- Each resource filter must specify at least one kind — **catch-all (empty `kinds` or `["*"]`) is NOT permitted in `clusterScopedFilterPolicy`** since it is a refinement overlay rather than an allowlist
- No duplicate kinds across resource filters
- `labelSelector` and `orLabelSelectors` mutual exclusion
- Resource name patterns must be valid globs
5. **Mutual exclusion with global `OrLabelSelectors`/`LabelSelector`**: If `namespacedFilterPolicies` are present and the `RestoreSpec` also has both `LabelSelector` and `OrLabelSelectors`, the existing validation catches this. No additional validation needed for the interaction — per-namespace selectors simply override the global ones for matching namespaces.
## ConfigMap Examples
### Restore-Specific ResourcePolicy ConfigMap
Restore only Deployments and ConfigMaps (labeled `app=my-app`) from `ns-a`, but everything from `ns-b`:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: restore-filter-policy
namespace: velero
data:
policy: |
version: v1
namespacedFilterPolicies:
- namespaces:
- ns-a
resourceFilters:
- kinds: [Deployment, ConfigMap]
labelSelector:
app: my-app
# ns-b has no filter policy entry, so global filters apply (restore everything)
```
Restore CR:
```yaml
apiVersion: velero.io/v1
kind: Restore
metadata:
name: selective-restore
namespace: velero
spec:
backupName: full-backup
includedNamespaces:
- ns-a
- ns-b
resourcePolicy:
kind: configmap
name: restore-filter-policy
```
### Restore with Name Pattern Filtering
Restore only `app-*` ConfigMaps and Secrets from `production`:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: app-restore-filter
namespace: velero
data:
policy: |
version: v1
namespacedFilterPolicies:
- namespaces:
- production
resourceFilters:
- kinds: [ConfigMap, Secret]
names: ["app-*"]
excludedNames: ["*-tmp", "*-debug"]
```
### Catch-All with No Label Selector (Override-Only)
A user may want to use the global configuration for 99% of resources in a namespace, but only apply a specific name filter to a single kind. A catch-all filter without a label selector achieves this:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: override-only-restore-policy
namespace: velero
data:
policy: |
version: v1
namespacedFilterPolicies:
- namespaces:
- ns-a
resourceFilters:
- kinds: [Secret]
names: [my-secret] # Specific override for Secrets
- kinds: ["*"] # Catch-all: NO label selector
# Restores all other kinds unconditionally
```
**Result:**
- `Secret` resources: only `my-secret` is restored.
- All other resource types: restored unconditionally (acting like a global fallback).
### Catch-All with Per-Kind Name Overrides
Use exact names for specific kinds, and fall back to a label selector for all remaining kinds:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: mixed-restore-filter-policy
namespace: velero
data:
policy: |
version: v1
namespacedFilterPolicies:
- namespaces:
- production
resourceFilters:
- kinds: [Deployment]
names: [api-server, worker] # these exact Deployments by name
- kinds: [Secret]
names: [db-credentials, tls-cert] # these exact Secrets by name
- kinds: ["*"] # catch-all for all other kinds
labelSelector:
backup: "true" # restore by label
```
**Result:**
- `Deployment` resources: only `api-server` and `worker` are restored.
- `Secret` resources: only `db-credentials` and `tls-cert` are restored.
- All other resource types: restored only if they carry `backup=true`.
### Cluster-Scoped Filter Policy
Restore only specific ClusterRoles and CRDs matching a label:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: cluster-restore-filter
namespace: velero
data:
policy: |
version: v1
clusterScopedFilterPolicy:
resourceFilters:
- kinds: [ClusterRole, ClusterRoleBinding]
names: ["my-app-*"]
- kinds: [CustomResourceDefinition]
labelSelector:
app: my-app
namespacedFilterPolicies:
- namespaces:
- production
resourceFilters:
- kinds: [Deployment, ConfigMap, Secret, StatefulSet, PersistentVolumeClaim]
```
### Restore with Glob Namespace Patterns
Apply the same filter to all namespaces matching a pattern. **Critical: Order patterns from most specific to least specific:**
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: team-restore-filter
namespace: velero
data:
policy: |
version: v1
namespacedFilterPolicies:
# More specific patterns first
- namespaces:
- "team-frontend-prod" # Most specific (exact match)
resourceFilters:
- kinds: [Deployment, Service, ConfigMap, Secret, PersistentVolumeClaim]
- namespaces:
- "team-frontend-*" # Less specific (pattern match)
resourceFilters:
- kinds: [Deployment, Service, ConfigMap]
- namespaces:
- "team-*" # Least specific (broad pattern)
resourceFilters:
- kinds: [Deployment, Service]
```
**Pattern Matching Results:**
- `team-frontend-prod` → Uses exact match policy (restores 5 resource types)
- `team-frontend-dev` → Uses `team-frontend-*` policy (restores 3 resource types)
- `team-backend-test` → Uses `team-*` policy (restores 2 resource types)
- `app-namespace` → No match, uses global filters
### Same ConfigMap for Backup and Restore
A single ConfigMap can be referenced by both `BackupSpec.ResourcePolicy` and `RestoreSpec.ResourcePolicy`. The backup pipeline uses `volumePolicies`, `includeExcludePolicy`, `namespacedFilterPolicies`, and `clusterScopedFilterPolicy`. The restore pipeline uses only `namespacedFilterPolicies` and `clusterScopedFilterPolicy`:
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: shared-policy
namespace: velero
data:
policy: |
version: v1
volumePolicies:
- conditions:
capacity: "0,10Gi"
action:
type: fs-backup
clusterScopedFilterPolicy:
resourceFilters:
- kinds: [ClusterRole, ClusterRoleBinding]
names: ["my-app-*"]
namespacedFilterPolicies:
- namespaces:
- production
resourceFilters:
- kinds: [Deployment, ConfigMap, Secret, StatefulSet, PersistentVolumeClaim]
```
### Restore CR — No ResourcePolicy (backward compatible)
Existing restores continue to work exactly as before:
```yaml
apiVersion: velero.io/v1
kind: Restore
metadata:
name: full-restore
namespace: velero
spec:
backupName: my-backup
includedNamespaces:
- "*"
```
## CLI
### `velero restore describe`
The output is extended to display resource policy configmap name when present:
```
Name: selective-restore
Namespace: velero
Labels: <none>
Annotations: <none>
Phase: Completed
Errors: 0
Warnings: 0
Backup: full-backup
Namespaces:
Included: ns-a, ns-b
Excluded: <none>
Resources:
Included: *
Excluded: <none>
Cluster-scoped: auto
Namespace Mapping: <none>
Label Selector: <none>
Resource Policy: restore-filter-policy
Restore PVs: auto
...
```
### `velero restore create`
A new `--resource-policies-configmap` flag is added to `velero restore create`, mirroring the existing backup-side flag:
```bash
velero restore create selective-restore \
--from-backup full-backup \
--include-namespaces ns-a,ns-b \
--resource-policies-configmap restore-filter-policy
```
The `--help` output for `velero restore create` is updated to clarify the interaction between global and namespace-scoped filters:
```
Restore Filtering Options:
--include-namespaces stringArray namespaces to include in the restore (use '*' for all namespaces)
--exclude-namespaces stringArray namespaces to exclude from the restore
--include-resources stringArray resources to include in the restore, formatted as resource.group
--exclude-resources stringArray resources to exclude from the restore, formatted as resource.group
--include-cluster-resources optionalBool[=true] include cluster-scoped resources
--selector labelSelector only restore resources matching this label selector
--or-selector labelSelector restore resources matching any of the label selectors (can be repeated)
--resource-policies-configmap string reference to a configmap containing resource policies for namespace-scoped and cluster-scoped filtering
Notes:
- Global filters (--include-resources, --selector, etc.) apply to all included namespaces
- Namespace-scoped filters defined in --resource-policies-configmap refine global filters for matching namespaces (globally excluded kinds cannot be re-included)
- Fine-grained global filter policies defined in --resource-policies-configmap refine global filters for cluster-scoped resources
- Use 'velero restore describe' to view resolved filter policies after restore creation
```
## User Perspective
- **For users not using restore-side filter policies**: Zero changes. All existing restores work identically.
- **For users adopting restore-side filter policies**: Create a ConfigMap with the `namespacedFilterPolicies` and/or `clusterScopedFilterPolicy` sections and reference it via `RestoreSpec.ResourcePolicy` (or `--resource-policies-configmap` CLI flag). The restore will selectively include/exclude resources per namespace.
- **For users already using backup-side filter policies**: Restore-side policies are independent. A backup-side ConfigMap can be reused for restore (both `BackupSpec.ResourcePolicy` and `RestoreSpec.ResourcePolicy` can point to the same ConfigMap), or a different ConfigMap can be used.
- **Interaction with NamespaceMapping**: Filter policies use the original (backup-side) namespace names. If `NamespaceMapping` remaps `ns-a` to `ns-b`, the filter ConfigMap should reference `ns-a`.
- **`velero restore describe`**: Shows per-namespace and cluster-scoped filter details when `ResourcePolicy` is present.
- **Validation errors**: Reported at restore start when the ConfigMap is invalid.
## Alternatives Considered
1. **Reuse Backup's ResourcePolicy ConfigMap**: Automatically apply the backup's `namespacedFilterPolicies` during restore without requiring restore-side configuration. Rejected because restore should be independently configurable from backup, and the backup's ConfigMap may not exist at restore time or may have been modified.
2. **No CRD Change — Annotation-Based Reference**: Use a Velero annotation on the Restore CR to point to the ConfigMap instead of a CRD field. Rejected because annotations are not validated, not documented via `kubectl explain`, and are inconsistent with how the backup side works.
3. **Embed Filter Policies in RestoreSpec (Full CRD Approach)**: Add `NamespacedFilters []NamespaceFilter` directly to `RestoreSpec`. Rejected because it requires complex nested CRD types, doesn't reuse the Phase 1 ConfigMap infrastructure, and is a drift from backup side design.
4. **CLI-Only (No CRD Change)**: Express restore filters entirely via CLI flags that get stored as annotations. Rejected because it doesn't support the declarative Restore CR workflow and is not auditable.
@@ -0,0 +1,359 @@
# Add PVC VolumeMode and AccessModes as Criteria for Volume Policy
## Abstract
This proposal extends Velero VolumePolicy conditions with two PVC-based criteria, `pvcVolumeMode` and `pvcAccessModes`.
These conditions allow users to select volumes according to the `volumeMode` and `accessModes` of the associated PersistentVolumeClaim (PVC), enabling backup behavior such as skipping block-mode PVCs or choosing a specific backup method for volumes with selected access modes.
## Background
Velero VolumePolicy already supports selecting volumes by attributes such as capacity, storage class, volume source, volume type, PVC labels, and PVC phase.
PVC metadata and spec fields are often the most direct way for users to express the intended storage semantics of a workload.
Kubernetes PVCs include a `spec.volumeMode` field that describes whether the volume is exposed as a filesystem or as a raw block device.
The field supports values such as `Filesystem` and `Block`.
Kubernetes PVCs also include a `spec.accessModes` field that describes how the volume can be mounted.
Common values are `ReadWriteOnce`, `ReadOnlyMany`, `ReadWriteMany`, and `ReadWriteOncePod`.
For resource policies, `pvcAccessModes` uses an exact set match against the PVC's `spec.accessModes`, so a policy does not match PVCs that have missing or additional access modes.
## Goals
- Add a `pvcVolumeMode` VolumePolicy condition to match volumes by a single `spec.volumeMode` value of their associated PVC.
- Add a `pvcAccessModes` VolumePolicy condition to match volumes whose associated PVC has exactly the configured `spec.accessModes` values, regardless of order.
- Keep the new conditions consistent with existing VolumePolicy behavior, where all conditions in a policy must match and the first matching policy wins.
## Non-Goals
- This proposal does not add new VolumePolicy actions.
- This proposal does not change how PVCs are discovered or passed into the resource policy matching code.
- This proposal does not add set-based or negative matching operators such as `NotIn`, `Exists`, or `DoesNotExist`.
- This proposal does not change Kubernetes PVC semantics or validate storage provider capabilities.
## Use-cases/Scenarios
### Skip block-mode PVCs
A user wants to skip volumes whose associated PVC is configured with raw block volume mode.
```yaml
version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Block
action:
type: skip
```
### Snapshot filesystem PVCs
A user wants to use snapshots only for volumes whose associated PVC has filesystem mode.
```yaml
version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Filesystem
action:
type: snapshot
```
### Match PVCs by access mode
A user wants to apply a policy only to PVCs whose `spec.accessModes` is exactly `ReadWriteOnce`.
```yaml
version: v1
volumePolicies:
- conditions:
pvcAccessModes:
- ReadWriteOnce
action:
type: skip
```
### Match an exact access mode set
A user wants to match volumes whose associated PVC access modes are exactly `ReadOnlyMany` and `ReadWriteMany`.
A PVC that includes only one of these modes, or includes additional modes, does not match.
```yaml
version: v1
volumePolicies:
- conditions:
pvcAccessModes:
- ReadOnlyMany
- ReadWriteMany
action:
type: snapshot
```
### Combine PVC spec criteria
A user wants to select block-mode PVCs whose access modes are exactly `ReadWriteOnce`.
Because VolumePolicy conditions are conjunctive, the volume must satisfy both conditions.
```yaml
version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Block
pvcAccessModes:
- ReadWriteOnce
action:
type: snapshot
```
## High-Level Design
The VolumePolicy condition schema is extended with two optional fields, `pvcVolumeMode` and `pvcAccessModes`.
`pvcVolumeMode` is represented as a single string value in the resource policy YAML.
`pvcAccessModes` is represented as a string list in the resource policy YAML.
The internal `structuredVolume` representation is extended to store the associated PVC's volume mode and access modes.
The existing PVC parsing path populates these fields when a PVC is available in `VolumeFilterData`.
The policy builder creates a `pvcVolumeModeCondition` when `pvcVolumeMode` is specified and creates a `pvcAccessModesCondition` when `pvcAccessModes` is specified.
The existing matching flow remains unchanged: each condition implements the `volumeCondition` interface, all conditions in a policy must match, and the first matching policy's action is returned.
## Detailed Design
### Resource policy YAML schema
Two new fields are added under `volumePolicies[].conditions`.
```yaml
version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Block
pvcAccessModes:
- ReadWriteOnce
- ReadWriteMany
action:
type: snapshot
```
`pvcVolumeMode` is a string.
The intended values are Kubernetes PVC volume mode values, including `Filesystem` and `Block`.
The condition matches only when the PVC volume mode value observed by Velero exactly equals the configured value.
Matching is case-sensitive, so `block` does not match `Block`.
`pvcAccessModes` is a list of strings.
The intended values are Kubernetes PVC access mode values, including `ReadWriteOnce`, `ReadOnlyMany`, `ReadWriteMany`, and `ReadWriteOncePod`.
The condition matches only when the configured access modes exactly equal the PVC's `spec.accessModes`, ignoring order.
Matching is case-sensitive, so `readwriteonce` does not match `ReadWriteOnce`.
The implementation validates that `pvcVolumeMode`, when present, is a string.
The implementation validates that `pvcAccessModes`, when present, is a list of strings.
The implementation does not strictly reject unknown string values so that the condition format remains tolerant of Kubernetes additions or storage-provider-specific behavior.
Unknown `pvcVolumeMode` values match only when the PVC has the same string value, and unknown `pvcAccessModes` values match only as part of the same exact access-mode set.
### Volume condition struct
The parsed condition struct is extended as follows.
```go
type volumeConditions struct {
Capacity string `yaml:"capacity,omitempty"`
StorageClass []string `yaml:"storageClass,omitempty"`
NFS *nFSVolumeSource `yaml:"nfs,omitempty"`
CSI *csiVolumeSource `yaml:"csi,omitempty"`
VolumeTypes []SupportedVolume `yaml:"volumeTypes,omitempty"`
PVCLabels map[string]string `yaml:"pvcLabels,omitempty"`
PVCPhase []string `yaml:"pvcPhase,omitempty"`
PVCVolumeMode string `yaml:"pvcVolumeMode,omitempty"`
PVCAccessModes []string `yaml:"pvcAccessModes,omitempty"`
}
```
### Structured volume data
The internal `structuredVolume` is extended with `pvcVolumeMode` and `pvcAccessModes`.
```go
type structuredVolume struct {
capacity resource.Quantity
storageClass string
nfs *nFSVolumeSource
csi *csiVolumeSource
volumeType SupportedVolume
pvcLabels map[string]string
pvcPhase string
pvcVolumeMode string
pvcAccessModes []string
}
```
When a PVC is available, `parsePVC` extracts PVC attributes into `structuredVolume` for later condition evaluation.
This parsing step does not create or imply a `pvcVolumeMode` policy condition; `pvcVolumeMode` only constrains matching when the user explicitly configures `conditions.pvcVolumeMode` in the VolumePolicy.
Velero uses `pvc.Spec.VolumeMode` as-is when it is present.
If `pvc.Spec.VolumeMode` is nil, `pvcVolumeMode` remains empty and does not match any non-empty `pvcVolumeMode` condition.
If `pvc.Spec.AccessModes` is empty, `pvcAccessModes` remains empty and does not match any non-empty `pvcAccessModes` condition.
```go
func (s *structuredVolume) parsePVC(pvc *corev1api.PersistentVolumeClaim) {
if pvc != nil {
if len(pvc.GetLabels()) > 0 {
s.pvcLabels = pvc.Labels
}
s.pvcPhase = string(pvc.Status.Phase)
if pvc.Spec.VolumeMode != nil {
s.pvcVolumeMode = string(*pvc.Spec.VolumeMode)
}
if len(pvc.Spec.AccessModes) > 0 {
s.pvcAccessModes = make([]string, 0, len(pvc.Spec.AccessModes))
for _, accessMode := range pvc.Spec.AccessModes {
s.pvcAccessModes = append(s.pvcAccessModes, string(accessMode))
}
}
}
}
```
### PVC volume mode condition
`pvcVolumeModeCondition` matches when the associated PVC's parsed volume mode exactly equals the configured value.
The comparison is case-sensitive and does not normalize values.
An empty configured value is treated as no constraint and always matches, consistent with other VolumePolicy conditions.
A non-empty configured value does not match if no PVC volume mode is available.
```go
type pvcVolumeModeCondition struct {
volumeMode string
}
func (c *pvcVolumeModeCondition) match(v *structuredVolume) bool {
if c.volumeMode == "" {
return true
}
if v.pvcVolumeMode == "" {
return false
}
return v.pvcVolumeMode == c.volumeMode
}
```
### PVC access modes condition
`pvcAccessModesCondition` matches when the configured access modes exactly equal the associated PVC's access modes, ignoring order.
The comparison is case-sensitive and does not normalize values.
An empty configured list is treated as no constraint and always matches.
A non-empty configured list does not match if the structured volume has no PVC access modes, has a different number of access modes, or has a different access-mode set.
```go
type pvcAccessModesCondition struct {
accessModes []string
}
func (c *pvcAccessModesCondition) match(v *structuredVolume) bool {
if len(c.accessModes) == 0 {
return true
}
if len(v.pvcAccessModes) == 0 || len(v.pvcAccessModes) != len(c.accessModes) {
return false
}
return sets.New(c.accessModes...).Equal(sets.New(v.pvcAccessModes...))
}
```
### Condition validation
Both `pvcVolumeModeCondition` and `pvcAccessModesCondition` implement the `validate()` method required by the `volumeCondition` interface.
The `validate()` method returns nil for both conditions.
```go
func (c *pvcVolumeModeCondition) validate() error {
return nil
}
func (c *pvcAccessModesCondition) validate() error {
return nil
}
```
YAML shape validation is handled when resource policy conditions are unmarshaled.
`pvcVolumeMode` must be a string, and `pvcAccessModes` must be a list of strings.
Condition-level validation intentionally does not reject unknown string values.
This keeps the policy format forward-compatible with future Kubernetes values and consistent with other string-based VolumePolicy conditions.
Unknown values simply do not match normal PVCs unless the evaluated PVC has the same exact value or access-mode set.
### Policy builder integration
The policy builder appends the new conditions only when the corresponding YAML fields are present.
```go
func (p *Policies) BuildPolicy(resPolicies *ResourcePolicies) error {
for _, vp := range resPolicies.VolumePolicies {
con, err := unmarshalVolConditions(vp.Conditions)
if err != nil {
return errors.WithStack(err)
}
// Existing conditions are appended here.
if con.PVCVolumeMode != "" {
volP.conditions = append(volP.conditions, &pvcVolumeModeCondition{volumeMode: con.PVCVolumeMode})
}
if len(con.PVCAccessModes) > 0 {
volP.conditions = append(volP.conditions, &pvcAccessModesCondition{accessModes: con.PVCAccessModes})
}
}
return nil
}
```
### Matching behavior with other conditions
The new conditions follow the existing VolumePolicy matching behavior.
Within a single policy, every configured condition must match.
If `pvcVolumeMode` is omitted from a policy, Velero does not add a volume mode condition and the policy does not restrict volume mode.
`pvcVolumeMode` and `pvcAccessModes` are PVC-specific conditions and only match when the volume policy evaluation has associated PVC data.
For non-PVC volumes such as `emptyDir`, `configMap`, or inline volumes without an associated PVC, the parsed PVC fields are empty and policies requiring `pvcVolumeMode` or `pvcAccessModes` do not match.
Across multiple policies, the first matching policy wins.
For example, this policy matches only PVC-backed volumes that are both `Block` mode and have exactly `ReadWriteOnce` as their access modes.
```yaml
version: v1
volumePolicies:
- conditions:
pvcVolumeMode: Block
pvcAccessModes:
- ReadWriteOnce
action:
type: snapshot
```
## Alternatives Considered
### A single `pvcSpec` condition object
One alternative is to add a nested object such as `pvcSpec.volumeMode` and `pvcSpec.accessModes`.
This was not chosen because existing PVC-based VolumePolicy conditions use flat field names such as `pvcLabels` and `pvcPhase`.
Flat names keep the YAML concise and consistent with existing conditions.
### List-based `pvcVolumeMode`
One alternative is to make `pvcVolumeMode` a list, similar to `pvcPhase`.
This was not chosen because Kubernetes PVC `spec.volumeMode` is a single value and the policy condition is intended to describe an exact match against that value.
Using a string avoids implying that multiple volume modes can apply to one PVC.
### Contains-based access mode matching
Another alternative is to make `pvcAccessModes` match when any or all configured access modes are present on the PVC.
This was not chosen because contains-based matching would also select PVCs with additional access modes.
Using exact set matching keeps `pvcAccessModes` consistent with `pvcVolumeMode`'s exact-match behavior and avoids matching PVCs whose access mode set differs from the policy.
### Strict validation of allowed Kubernetes values
Another alternative is to reject `pvcVolumeMode` or `pvcAccessModes` values that are not currently known Kubernetes constants.
This was not chosen because accepting strings is more forward-compatible and keeps behavior consistent with other string-based resource policy conditions.
Invalid or unknown values naturally fail to match unless a PVC has the same value.
## Security Considerations
This proposal does not introduce new privileges or access to additional Kubernetes resources.
It only uses PVC data already available to the volume policy matching path.
The new conditions can cause Velero to skip or choose different backup actions for matched volumes.
Users should review policy configuration carefully because an overly broad policy can exclude data from backup or select an unintended backup method.
## Compatibility
The new fields are optional and do not affect existing resource policy files.
Existing VolumePolicy behavior remains unchanged when `pvcVolumeMode` and `pvcAccessModes` are not configured.
PVCs without a parsed `spec.volumeMode` value do not match non-empty `pvcVolumeMode` conditions.
PVCs without `spec.accessModes` do not match non-empty `pvcAccessModes` conditions.
Unknown `pvcVolumeMode` or `pvcAccessModes` string values in a policy are accepted as strings but will not match normal Kubernetes PVCs unless the evaluated PVC has the same exact value or access-mode set.
## Implementation
Implementation requires changes in the resource policies package and documentation.
- Extend `volumeConditions` with `PVCVolumeMode string` and `PVCAccessModes []string`.
- Extend `structuredVolume` with `pvcVolumeMode string` and `pvcAccessModes []string`.
- Update `parsePVC` to populate the new fields from the PVC spec.
- Add `pvcVolumeModeCondition` and `pvcAccessModesCondition` implementations.
- Update `Policies.BuildPolicy` to append the new conditions.
- Add YAML type validation to ensure `pvcVolumeMode` is a string and `pvcAccessModes` is a string list.
- Add unit tests for parsing, validation, condition matching, and end-to-end `GetMatchAction` behavior.
- Update user documentation in `site/content/docs/main/resource-filtering.md`.
+75 -61
View File
@@ -3,21 +3,22 @@ module github.com/vmware-tanzu/velero
go 1.26.0
require (
cloud.google.com/go/storage v1.62.1
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1
cloud.google.com/go/storage v1.62.3
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/compute/armcompute/v5 v5.6.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0
github.com/RoaringBitmap/roaring v1.9.4
github.com/aws/aws-sdk-go-v2 v1.24.1
github.com/aws/aws-sdk-go-v2/config v1.26.3
github.com/aws/aws-sdk-go-v2/credentials v1.16.14
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.15.11
github.com/aws/aws-sdk-go-v2 v1.41.12
github.com/aws/aws-sdk-go-v2/config v1.32.17
github.com/aws/aws-sdk-go-v2/credentials v1.19.16
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.22.18
github.com/aws/aws-sdk-go-v2/service/ec2 v1.143.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.48.0
github.com/aws/aws-sdk-go-v2/service/sts v1.26.7
github.com/bombsimon/logrusr/v3 v3.0.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.101.0
github.com/aws/aws-sdk-go-v2/service/sts v1.42.1
github.com/bombsimon/logrusr/v3 v3.1.0
github.com/cockroachdb/errors v1.13.0
github.com/evanphx/json-patch/v5 v5.9.11
github.com/fatih/color v1.19.0
github.com/gobwas/glob v0.2.3
@@ -25,32 +26,31 @@ require (
github.com/google/uuid v1.6.0
github.com/hashicorp/go-hclog v1.6.3
github.com/hashicorp/go-plugin v1.7.0
github.com/joho/godotenv v1.3.0
github.com/kopia/kopia v0.16.0
github.com/kubernetes-csi/external-snapshot-metadata v1.0.0
github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0
github.com/netresearch/go-cron v0.15.0
github.com/onsi/ginkgo/v2 v2.28.3
github.com/onsi/gomega v1.40.0
github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9
github.com/pkg/errors v0.9.1
github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/robfig/cron/v3 v3.0.1
github.com/sirupsen/logrus v1.9.3
github.com/spf13/afero v1.10.0
github.com/sirupsen/logrus v1.9.4
github.com/spf13/afero v1.15.0
github.com/spf13/cobra v1.10.2
github.com/spf13/pflag v1.0.10
github.com/stretchr/testify v1.11.1
github.com/vmware-tanzu/crash-diagnostics v0.3.7
github.com/vmware-tanzu/crash-diagnostics v0.4.3
github.com/vmware-tanzu/velero/pkg/apis v0.0.0
go.uber.org/zap v1.28.0
golang.org/x/mod v0.35.0
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/mod v0.36.0
golang.org/x/oauth2 v0.36.0
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
google.golang.org/api v0.277.0
google.golang.org/grpc v1.80.0
golang.org/x/sys v0.46.0
golang.org/x/text v0.37.0
google.golang.org/api v0.283.0
google.golang.org/grpc v1.81.1
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.36.0
k8s.io/apiextensions-apiserver v0.36.0
k8s.io/apimachinery v0.36.0
@@ -75,39 +75,41 @@ require (
cloud.google.com/go/monitoring v1.24.3 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.12.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.55.0 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.14.11 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.5.10 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.7.2 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.10.4 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.10.10 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.10 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.18.6 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.21.6 // indirect
github.com/aws/smithy-go v1.19.0 // indirect
github.com/Masterminds/semver/v3 v3.5.0 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.10 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.23 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.24 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.9 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.15 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.23 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.23 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.11 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.17 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.21 // indirect
github.com/aws/smithy-go v1.27.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bits-and-blooms/bitset v1.12.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-20260202195803-dba9d589def2 // indirect
github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect
github.com/cockroachdb/redact v1.1.5 // 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.13.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.36.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.0 // indirect
github.com/envoyproxy/go-control-plane/envoy v1.37.0 // indirect
github.com/envoyproxy/protoc-gen-validate v1.3.3 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/getsentry/sentry-go v0.46.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -128,12 +130,13 @@ require (
github.com/go-openapi/swag/yamlutils v0.25.5 // indirect
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
github.com/gofrs/flock v0.13.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.7.1 // indirect
github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.16 // indirect
github.com/googleapis/gax-go/v2 v2.22.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/hashicorp/cronexpr v1.1.3 // indirect
@@ -141,19 +144,25 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kcp-dev/apimachinery/v2 v2.0.1-0.20250223115924-431177b024f3 // indirect
github.com/kcp-dev/kcp/cli v0.27.1 // indirect
github.com/kcp-dev/kcp/sdk v0.27.1 // indirect
github.com/kcp-dev/logicalcluster/v3 v3.0.5 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/klauspost/pgzip v1.2.6 // indirect
github.com/klauspost/reedsolomon v1.14.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kubernetes-csi/external-snapshot-metadata/client v1.0.0 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/minio/minio-go/v7 v7.1.0 // indirect
github.com/minio/minio-go/v7 v7.2.0 // indirect
github.com/moby/spdystream v0.5.1 // indirect
github.com/moby/term v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
@@ -162,52 +171,57 @@ require (
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-vss v1.2.1 // indirect
github.com/natefinch/atomic v1.0.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/common v0.68.1 // indirect
github.com/prometheus/procfs v0.20.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
github.com/vladimirvivien/gexe v0.1.1 // indirect
github.com/vladimirvivien/gexe v0.4.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.4 // indirect
github.com/zeebo/xxh3 v1.1.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.39.0 // indirect
go.opentelemetry.io/contrib/detectors/gcp v1.42.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
go.opentelemetry.io/otel v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/sdk v1.44.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.starlark.net v0.0.0-20241226192728-8dfa5b98479f // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.50.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
golang.org/x/net v0.53.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/term v0.42.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.21.0 // indirect
golang.org/x/term v0.43.0 // indirect
golang.org/x/time v0.15.0 // indirect
golang.org/x/tools v0.44.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/streaming v0.36.0 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
)
replace github.com/kopia/kopia => github.com/project-velero/kopia v0.0.0-20260512025144-908c5c098101
replace (
github.com/kopia/kopia => github.com/project-velero/kopia v0.0.0-20260616052725-d83462d382c9
github.com/vmware-tanzu/velero/pkg/apis => ./pkg/apis
)
+154 -854
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -96,7 +96,9 @@ RUN ARCH=$(go env GOARCH) && \
chmod +x /usr/bin/goreleaser
# get golangci-lint
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/HEAD/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0
# Use "go install" so the download goes through GOPROXY instead of the GitHub
# release API/CDN, which has been returning intermittent/persistent HTTP 504s.
RUN go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.5.0
# install kubectl
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/$(go env GOARCH)/kubectl
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"os"
"path/filepath"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
corev1api "k8s.io/api/core/v1"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
package credentials
import (
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
corev1api "k8s.io/api/core/v1"
kbclient "sigs.k8s.io/controller-runtime/pkg/client"
@@ -20,9 +20,9 @@ import (
"context"
"time"
"github.com/cockroachdb/errors"
"github.com/google/uuid"
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"
apierrors "k8s.io/apimachinery/pkg/api/errors"
+17 -3
View File
@@ -21,10 +21,11 @@ import (
"github.com/vmware-tanzu/velero/pkg/plugin/framework"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
kubeerrs "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apimachinery/pkg/util/sets"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
@@ -80,6 +81,15 @@ func InvokeDeleteActions(ctx *Context) error {
}
processdResources := sets.NewString()
// deleteErrs collects errors returned by DeleteItemAction plugins. We keep
// looping over the remaining items even when a plugin fails, but we must not
// swallow these errors: a DIA failure means the private artifacts it manages
// (e.g. data mover repository snapshots) may not have been deleted. If we
// returned nil here, the caller would proceed to delete the backup and its
// metadata, orphaning those artifacts forever. Returning the aggregated error
// makes the caller fail the deletion so it can be retried.
var deleteErrs []error
for resource := range backupResources {
groupResource := schema.ParseGroupResource(resource)
@@ -124,15 +134,19 @@ func InvokeDeleteActions(ctx *Context) error {
Item: obj,
Backup: ctx.Backup,
})
// Since we want to keep looping even on errors, log them instead of just returning.
// Keep looping even on errors so a single failing plugin
// doesn't prevent the remaining items from being cleaned up,
// but record the error so it can be surfaced to the caller.
if err != nil {
itemLog.WithError(err).Error("plugin error")
deleteErrs = append(deleteErrs, errors.Wrapf(err,
"error executing DeleteItemAction for %s %s", groupResource.String(), obj.GetName()))
}
}
}
}
}
return nil
return kubeerrs.NewAggregate(deleteErrs)
}
// getApplicableActions takes resolved DeleteItemActions and filters them for a given group/resource and namespace.
@@ -17,6 +17,7 @@ limitations under the License.
package delete
import (
"errors"
"io"
"sort"
"testing"
@@ -276,3 +277,54 @@ func TestInvokeDeleteItemActionsWithNoPlugins(t *testing.T) {
err := InvokeDeleteActions(c)
require.NoError(t, err)
}
// failingAction is a DeleteItemAction that always returns an error from Execute.
// It is used to verify that InvokeDeleteActions surfaces plugin errors instead
// of swallowing them.
type failingAction struct {
selector velero.ResourceSelector
err error
executed int
}
func (a *failingAction) AppliesTo() (velero.ResourceSelector, error) {
return a.selector, nil
}
func (a *failingAction) Execute(input *velero.DeleteItemActionExecuteInput) error {
a.executed++
return a.err
}
func TestInvokeDeleteActionsReturnsPluginErrors(t *testing.T) {
fs := test.NewFakeFileSystem()
log := logrus.StandardLogger()
tarball := test.NewTarWriter(t).
AddItems("pods", builder.ForPod("ns-1", "pod-1").Result(), builder.ForPod("ns-2", "pod-2").Result()).
Done()
action := &failingAction{err: errors.New("could not delete artifact")}
h := newHarness(t)
h.addResource(t, test.Pods())
c := &Context{
Backup: builder.ForBackup("velero", "velero").Result(),
BackupReader: tarball,
Filesystem: fs,
DiscoveryHelper: h.discoveryHelper,
Actions: []velero.DeleteItemAction{action},
Log: log,
}
err := InvokeDeleteActions(c)
// The plugin error must be surfaced so the caller can fail the deletion
// rather than orphaning the artifacts the plugin failed to delete.
require.Error(t, err)
assert.Contains(t, err.Error(), "could not delete artifact")
// The loop must keep going: the action should run for every matching item,
// not stop at the first failure.
assert.Equal(t, 2, action.executed)
}
+1 -1
View File
@@ -23,8 +23,8 @@ import (
"strings"
"time"
"github.com/cockroachdb/errors"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/meta"
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"testing"
"time"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
+30 -4
View File
@@ -21,7 +21,7 @@ import (
"testing"
"time"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -53,13 +53,25 @@ func TestWaitExecHandleHooks(t *testing.T) {
// delta to wait since last change applied or pod added
wait time.Duration
updated *corev1api.Pod
// waitForSignal, if set, blocks the goroutine after applying this change
// until the channel is closed. Use this to ensure the handler processes
// an intermediate pod state before the next change is applied.
waitForSignal chan struct{}
}
type expectedExecution struct {
hook *velerov1api.ExecHook
name string
error error
pod *corev1api.Pod
// onCalled, if set, is invoked by the mock when ExecutePodCommand is called.
// Use this together with change.waitForSignal to synchronize state transitions.
onCalled func()
}
// hookFired is used by the test case that has two containers with hooks in
// different containers. It ensures the second pod state change is only sent
// after the first hook has fired, preventing the informer from coalescing
// both updates and skipping the intermediate state.
hookFired := make(chan struct{})
tests := []struct {
name string
// Used as argument to HandleHooks and first state added to ListerWatcher
@@ -622,6 +634,8 @@ func TestWaitExecHandleHooks(t *testing.T) {
},
}).
Result(),
// Signal after this hook fires so the goroutine can apply the next change.
onCalled: func() { close(hookFired) },
},
{
name: "my-hook-1",
@@ -678,6 +692,10 @@ func TestWaitExecHandleHooks(t *testing.T) {
},
}).
Result(),
// Block until the hook for container1 has fired before sending the
// next change. Without this, the informer may coalesce both updates
// and deliver only resourceVersion:3, skipping the intermediate state.
waitForSignal: hookFired,
},
// 2nd modification: container2 starts running, resourceVersion 3
{
@@ -838,11 +856,15 @@ func TestWaitExecHandleHooks(t *testing.T) {
go func() {
// This is the state of the pod that will be seen by the AddFunc handler.
source.Add(test.initialPod)
// Changes holds the versions of the pod over time. Each of these states
// will be seen by the UpdateFunc handler.
// Changes holds the versions of the pod over time. The informer may
// coalesce rapid updates, so use waitForSignal when a test requires the
// handler to observe a specific intermediate state before the next change.
for _, change := range test.changes {
time.Sleep(change.wait)
source.Modify(change.updated)
if change.waitForSignal != nil {
<-change.waitForSignal
}
}
}()
@@ -857,7 +879,11 @@ func TestWaitExecHandleHooks(t *testing.T) {
for _, e := range test.expectedExecutions {
obj, err := runtime.DefaultUnstructuredConverter.ToUnstructured(e.pod)
require.NoError(t, err)
podCommandExecutor.On("ExecutePodCommand", mock.Anything, obj, e.pod.Namespace, e.pod.Name, e.name, e.hook).Return(e.error)
call := podCommandExecutor.On("ExecutePodCommand", mock.Anything, obj, e.pod.Namespace, e.pod.Name, e.name, e.hook).Return(e.error)
if e.onCalled != nil {
onCalled := e.onCalled
call.Run(func(mock.Arguments) { onCalled() })
}
}
ctx := t.Context()
@@ -19,9 +19,9 @@ import (
"fmt"
"regexp"
"github.com/cockroachdb/errors"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/gobwas/glob"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+268 -7
View File
@@ -23,13 +23,14 @@ import (
"k8s.io/apimachinery/pkg/util/sets"
"github.com/cockroachdb/errors"
"github.com/gobwas/glob"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
crclient "sigs.k8s.io/controller-runtime/pkg/client"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
datamover "github.com/vmware-tanzu/velero/pkg/util/datamover"
"github.com/vmware-tanzu/velero/pkg/util/wildcard"
)
@@ -48,6 +49,20 @@ const (
Custom VolumeActionType = "custom"
)
const (
// DataMoverParameter is the key of the action parameter that selects the data
// mover to be used for the matched volumes when the action type is snapshot.
DataMoverParameter = "dataMover"
)
// validDataMovers is the set of data mover values accepted in the snapshot
// action's dataMover parameter.
var validDataMovers = map[string]struct{}{
datamover.DataMoverTypeVelero: {},
datamover.DataMoverTypeVeleroFs: {},
datamover.DataMoverTypeVeleroBlock: {},
}
// Action defined as one action for a specific way of backup
type Action struct {
// Type defined specific type of action, currently only support 'skip'
@@ -56,6 +71,35 @@ type Action struct {
Parameters map[string]any `yaml:"parameters,omitempty"`
}
// GetDataMover returns the data mover configured in the snapshot action's
// dataMover parameter. The dataMover parameter is only meaningful for the
// snapshot action, so it returns an error when the action is nil or its type is
// not snapshot. When the parameter is absent, it returns the default built-in
// data mover. The empty string and "velero" both denote the default built-in
// data mover and are returned unchanged; normalizing them to the concrete
// default mover is the consuming workflow's responsibility (issue #9830).
func (a *Action) GetDataMover() (string, error) {
if a == nil || a.Type != Snapshot {
return "", fmt.Errorf("the %q parameter is only supported for the %q action", DataMoverParameter, Snapshot)
}
if len(a.Parameters) == 0 {
return datamover.GetDefaultBuiltInDataMover(), nil
}
raw, ok := a.Parameters[DataMoverParameter]
if !ok {
return datamover.GetDefaultBuiltInDataMover(), nil
}
dataMover, ok := raw.(string)
if !ok {
return "", fmt.Errorf("parameter %q must be a string, got %T", DataMoverParameter, raw)
}
if _, ok := validDataMovers[dataMover]; !ok {
return "", fmt.Errorf("invalid %q value %q, valid values are %q, %q, %q",
DataMoverParameter, dataMover, datamover.DataMoverTypeVelero, datamover.DataMoverTypeVeleroFs, datamover.DataMoverTypeVeleroBlock)
}
return dataMover, nil
}
// ResourceFilter defines a filter for specific resource kinds.
type ResourceFilter struct {
Kinds []string `yaml:"kinds"`
@@ -155,10 +199,35 @@ func unmarshalResourcePolicies(yamlData *string) (*ResourcePolicies, error) {
return nil, fmt.Errorf("pvcLabels must be a map of string to string, got %T", raw)
}
}
if raw, ok := vp.Conditions["pvcVolumeMode"]; ok {
if _, ok := raw.(string); !ok {
return nil, fmt.Errorf("pvcVolumeMode must be a string, got %T", raw)
}
}
if raw, ok := vp.Conditions["pvcAccessModes"]; ok {
if err := validateStringSliceCondition("pvcAccessModes", raw); err != nil {
return nil, err
}
}
}
return resPolicies, nil
}
func validateStringSliceCondition(name string, raw any) error {
switch values := raw.(type) {
case []any:
for _, value := range values {
if _, ok := value.(string); !ok {
return fmt.Errorf("%s must be a list of strings, got element %T", name, value)
}
}
case []string:
default:
return fmt.Errorf("%s must be a list of strings, got %T", name, raw)
}
return nil
}
func (p *Policies) BuildPolicy(resPolicies *ResourcePolicies) error {
for _, vp := range resPolicies.VolumePolicies {
con, err := unmarshalVolConditions(vp.Conditions)
@@ -182,6 +251,12 @@ func (p *Policies) BuildPolicy(resPolicies *ResourcePolicies) error {
if len(con.PVCPhase) > 0 {
volP.conditions = append(volP.conditions, &pvcPhaseCondition{phases: con.PVCPhase})
}
if con.PVCVolumeMode != "" {
volP.conditions = append(volP.conditions, &pvcVolumeModeCondition{volumeMode: con.PVCVolumeMode})
}
if len(con.PVCAccessModes) > 0 {
volP.conditions = append(volP.conditions, &pvcAccessModesCondition{accessModes: con.PVCAccessModes})
}
p.volumePolicies = append(p.volumePolicies, volP)
}
@@ -261,6 +336,34 @@ func (p *Policies) Validate() error {
}
}
if err := p.validateClusterScopedFilterPolicy(); err != nil {
return errors.WithStack(err)
}
if err := p.validateNamespacedFilterPolicies(); err != nil {
return errors.WithStack(err)
}
return nil
}
func (p *Policies) ValidateForRestore() error {
if p.version != currentSupportDataVersion {
return fmt.Errorf("incompatible version number %s with supported version %s", p.version, currentSupportDataVersion)
}
if len(p.volumePolicies) > 0 {
return fmt.Errorf("volumePolicies are not supported for restore")
}
if p.GetIncludeExcludePolicy() != nil {
return fmt.Errorf("includeExcludePolicy is not supported for restore")
}
if err := p.validateClusterScopedFilterPolicy(); err != nil {
return errors.WithStack(err)
}
if err := p.validateNamespacedFilterPolicies(); err != nil {
return errors.WithStack(err)
}
@@ -296,26 +399,143 @@ func GetResourcePoliciesFromBackup(
if err != nil {
logger.Errorf("Fail to get ResourcePolicies %s ConfigMap with error %s.",
backup.Namespace+"/"+backup.Spec.ResourcePolicy.Name, err.Error())
return nil, fmt.Errorf("fail to get ResourcePolicies %s ConfigMap with error %s",
backup.Namespace+"/"+backup.Spec.ResourcePolicy.Name, err.Error())
return nil, fmt.Errorf("fail to get ResourcePolicies %s ConfigMap: %w",
backup.Namespace+"/"+backup.Spec.ResourcePolicy.Name, err)
}
resourcePolicies, err = getResourcePoliciesFromConfig(policiesConfigMap)
if err != nil {
logger.Errorf("Fail to read ResourcePolicies from ConfigMap %s with error %s.",
backup.Namespace+"/"+backup.Name, err.Error())
return nil, fmt.Errorf("fail to read the ResourcePolicies from ConfigMap %s with error %s",
backup.Namespace+"/"+backup.Name, err.Error())
return nil, fmt.Errorf("fail to read the ResourcePolicies from ConfigMap %s: %w",
backup.Namespace+"/"+backup.Name, err)
} else if err = resourcePolicies.Validate(); err != nil {
logger.Errorf("Fail to validate ResourcePolicies in ConfigMap %s with error %s.",
backup.Namespace+"/"+backup.Name, err.Error())
return nil, fmt.Errorf("fail to validate ResourcePolicies in ConfigMap %s with error %s",
backup.Namespace+"/"+backup.Name, err.Error())
return nil, fmt.Errorf("fail to validate ResourcePolicies in ConfigMap %s: %w",
backup.Namespace+"/"+backup.Name, err)
}
}
return resourcePolicies, nil
}
// GetGlobalResourcePolicies loads and validates the cluster-wide global backup volume
// policies from a ConfigMap in the Velero install namespace. Only the volumePolicies
// section is honored globally; any include/exclude or fine-grained filter policies are
// ignored (a warning is logged), as those are tied to a specific backup use case.
func GetGlobalResourcePolicies(
client crclient.Client,
namespace string,
configMapName string,
logger logrus.FieldLogger,
) (*Policies, error) {
cm := &corev1api.ConfigMap{}
if err := client.Get(context.Background(), crclient.ObjectKey{Namespace: namespace, Name: configMapName}, cm); err != nil {
return nil, fmt.Errorf("fail to get global backup volume policies ConfigMap %s/%s: %w", namespace, configMapName, err)
}
policies, err := getResourcePoliciesFromConfig(cm)
if err != nil {
return nil, fmt.Errorf("fail to read global backup volume policies from ConfigMap %s/%s: %w", namespace, configMapName, err)
}
if err := policies.Validate(); err != nil {
return nil, fmt.Errorf("fail to validate global backup volume policies in ConfigMap %s/%s: %w", namespace, configMapName, err)
}
// Only volumePolicies apply globally; warn about any other filter policies that will be ignored.
if policies.includeExcludePolicy != nil ||
policies.clusterScopedFilterPolicy != nil ||
len(policies.namespacedFilterPolicies) > 0 {
logger.Warnf("Global backup volume policies ConfigMap %s/%s contains include/exclude or fine-grained "+
"filter policies; these are ignored, only volumePolicies apply globally.", namespace, configMapName)
}
// Return a fresh Policies carrying only the globally-applicable fields. Using an allowlist here
// (rather than nil-ing out the ignored fields) means any filter field added to Policies in the
// future is excluded from the global policies by default, without needing to update this code.
return &Policies{
version: policies.version,
volumePolicies: policies.volumePolicies,
}, nil
}
// GetResourcePoliciesFromBackupWithGlobal builds the effective resource policies for a backup
// by merging the backup-referenced resource policies with the global backup volume policies
// (when globalConfigMapName is set). The merged volumePolicies list is the backup-level
// policies followed by the global ones, so the first match wins and a backup can override the
// global baseline for a specific volume while still inheriting the rest of the global rules.
func GetResourcePoliciesFromBackupWithGlobal(
backup velerov1api.Backup,
client crclient.Client,
globalConfigMapName string,
installNamespace string,
logger logrus.FieldLogger,
) (*Policies, error) {
backupPolicies, err := GetResourcePoliciesFromBackup(backup, client, logger)
if err != nil {
return nil, err
}
if globalConfigMapName == "" {
return backupPolicies, nil
}
globalPolicies, err := GetGlobalResourcePolicies(client, installNamespace, globalConfigMapName, logger)
if err != nil {
return nil, err
}
if backupPolicies == nil {
return globalPolicies, nil
}
// Backup-level policies first, then global, so backups can override the global baseline.
backupPolicies.volumePolicies = append(backupPolicies.volumePolicies, globalPolicies.volumePolicies...)
return backupPolicies, nil
}
// GetResourcePoliciesFromRestore retrieves the resource policies from the ConfigMap referenced in the Restore spec.
func GetResourcePoliciesFromRestore(
ctx context.Context,
restore *velerov1api.Restore,
client crclient.Client,
logger logrus.FieldLogger,
) (resourcePolicies *Policies, err error) {
if restore.Spec.ResourcePolicy != nil {
if !strings.EqualFold(restore.Spec.ResourcePolicy.Kind, ConfigmapRefType) {
return nil, fmt.Errorf("invalid ResourcePolicy kind %q, only %q is supported",
restore.Spec.ResourcePolicy.Kind, ConfigmapRefType)
}
policiesConfigMap := &corev1api.ConfigMap{}
err = client.Get(
ctx,
crclient.ObjectKey{
Namespace: restore.Namespace,
Name: restore.Spec.ResourcePolicy.Name,
},
policiesConfigMap,
)
if err != nil {
logger.Errorf("Fail to get ResourcePolicies %s ConfigMap with error %s.",
restore.Namespace+"/"+restore.Spec.ResourcePolicy.Name, err.Error())
return nil, fmt.Errorf("fail to get ResourcePolicies %s ConfigMap: %w",
restore.Namespace+"/"+restore.Spec.ResourcePolicy.Name, err)
}
resourcePolicies, err = getResourcePoliciesFromConfig(policiesConfigMap)
if err != nil {
logger.Errorf("Fail to read ResourcePolicies from ConfigMap %s with error %s.",
restore.Namespace+"/"+restore.Spec.ResourcePolicy.Name, err.Error())
return nil, fmt.Errorf("fail to read the ResourcePolicies from ConfigMap %s: %w",
restore.Namespace+"/"+restore.Spec.ResourcePolicy.Name, err)
} else if err = resourcePolicies.ValidateForRestore(); err != nil {
logger.Errorf("Fail to validate ResourcePolicies in ConfigMap %s with error %s.",
restore.Namespace+"/"+restore.Spec.ResourcePolicy.Name, err.Error())
return nil, fmt.Errorf("fail to validate ResourcePolicies in ConfigMap %s: %w",
restore.Namespace+"/"+restore.Spec.ResourcePolicy.Name, err)
}
}
return resourcePolicies, nil
}
func getResourcePoliciesFromConfig(cm *corev1api.ConfigMap) (*Policies, error) {
if cm == nil {
return nil, fmt.Errorf("could not parse config from nil configmap")
@@ -414,3 +634,44 @@ func (p *Policies) validateNamespacedFilterPolicies() error {
return nil
}
func (p *Policies) validateClusterScopedFilterPolicy() error {
if p.clusterScopedFilterPolicy == nil {
return nil
}
if len(p.clusterScopedFilterPolicy.ResourceFilters) == 0 {
return fmt.Errorf("clusterScopedFilterPolicy: resourceFilters cannot be empty; remove the policy block entirely if it is not needed")
}
seenKinds := make(map[string]int)
for j, rf := range p.clusterScopedFilterPolicy.ResourceFilters {
if rf.IsCatchAll() {
return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d]: kinds must be specified (catch-all is not supported)", j)
}
for _, kind := range rf.Kinds {
if prevJ, ok := seenKinds[kind]; ok {
return fmt.Errorf("clusterScopedFilterPolicy: kind %q appears in both resourceFilters[%d] and resourceFilters[%d]", kind, prevJ, j)
}
seenKinds[kind] = j
}
if len(rf.LabelSelector) > 0 && len(rf.OrLabelSelectors) > 0 {
return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d]: labelSelector and orLabelSelectors cannot co-exist", j)
}
for k, pattern := range rf.Names {
if _, err := glob.Compile(pattern); err != nil {
return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d].names[%d]: invalid glob pattern %q: %v", j, k, pattern, err)
}
}
for k, pattern := range rf.ExcludedNames {
if _, err := glob.Compile(pattern); err != nil {
return fmt.Errorf("clusterScopedFilterPolicy.resourceFilters[%d].excludedNames[%d]: invalid glob pattern %q: %v", j, k, pattern, err)
}
}
}
return nil
}
File diff suppressed because it is too large Load Diff
+65 -15
View File
@@ -18,12 +18,14 @@ package resourcepolicies
import (
"bytes"
"fmt"
"slices"
"strings"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/util/sets"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"github.com/cockroachdb/errors"
"go.yaml.in/yaml/v3"
corev1api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)
@@ -45,13 +47,15 @@ type capacity struct {
}
type structuredVolume struct {
capacity resource.Quantity
storageClass string
nfs *nFSVolumeSource
csi *csiVolumeSource
volumeType SupportedVolume
pvcLabels map[string]string
pvcPhase string
capacity resource.Quantity
storageClass string
nfs *nFSVolumeSource
csi *csiVolumeSource
volumeType SupportedVolume
pvcLabels map[string]string
pvcPhase string
pvcVolumeMode string
pvcAccessModes []string
}
func (s *structuredVolume) parsePV(pv *corev1api.PersistentVolume) {
@@ -76,6 +80,15 @@ func (s *structuredVolume) parsePVC(pvc *corev1api.PersistentVolumeClaim) {
s.pvcLabels = pvc.Labels
}
s.pvcPhase = string(pvc.Status.Phase)
if pvc.Spec.VolumeMode != nil {
s.pvcVolumeMode = string(*pvc.Spec.VolumeMode)
}
if len(pvc.Spec.AccessModes) > 0 {
s.pvcAccessModes = make([]string, 0, len(pvc.Spec.AccessModes))
for _, accessMode := range pvc.Spec.AccessModes {
s.pvcAccessModes = append(s.pvcAccessModes, string(accessMode))
}
}
}
}
@@ -127,18 +140,55 @@ func (c *pvcPhaseCondition) match(v *structuredVolume) bool {
if v.pvcPhase == "" {
return false
}
for _, phase := range c.phases {
if v.pvcPhase == phase {
return true
}
}
return false
return slices.Contains(c.phases, v.pvcPhase)
}
func (c *pvcPhaseCondition) validate() error {
return nil
}
// pvcVolumeModeCondition defines a condition that matches if the PVC's volume mode matches the provided volume mode.
type pvcVolumeModeCondition struct {
volumeMode string
}
func (c *pvcVolumeModeCondition) match(v *structuredVolume) bool {
// No volume mode specified: always match.
if c.volumeMode == "" {
return true
}
// Here allows unknown strings for forward compatibility. If Kubernetes adds another volume mode later,
// Velero would not reject the policy just because the string is unfamiliar.
return v.pvcVolumeMode == c.volumeMode
}
func (c *pvcVolumeModeCondition) validate() error {
return nil
}
// pvcAccessModesCondition defines a condition that matches if the PVC has exactly the provided access modes.
type pvcAccessModesCondition struct {
accessModes []string
}
func (c *pvcAccessModesCondition) match(v *structuredVolume) bool {
// No access modes specified: always match.
if len(c.accessModes) == 0 {
return true
}
if len(v.pvcAccessModes) != len(c.accessModes) {
return false
}
return sets.New(c.accessModes...).Equal(sets.New(v.pvcAccessModes...))
}
func (c *pvcAccessModesCondition) validate() error {
return nil
}
type capacityCondition struct {
capacity capacity
}
@@ -430,6 +430,38 @@ func TestUnmarshalVolumeConditions(t *testing.T) {
},
expectedError: "!!str `production` into map[string]string",
},
{
name: "Valid pvcVolumeMode input",
input: map[string]any{
"capacity": "1Gi,10Gi",
"pvcVolumeMode": "Block",
},
expectedError: "",
},
{
name: "Invalid pvcVolumeMode input: not a string",
input: map[string]any{
"capacity": "1Gi,10Gi",
"pvcVolumeMode": []string{"Filesystem", "Block"},
},
expectedError: "cannot unmarshal !!seq",
},
{
name: "Valid pvcAccessModes input",
input: map[string]any{
"capacity": "1Gi,10Gi",
"pvcAccessModes": []string{"ReadWriteOnce", "ReadWriteMany"},
},
expectedError: "",
},
{
name: "Invalid pvcAccessModes input: not a list",
input: map[string]any{
"capacity": "1Gi,10Gi",
"pvcAccessModes": "ReadWriteOnce",
},
expectedError: "cannot unmarshal !!str",
},
}
for _, tc := range testCases {
@@ -19,8 +19,10 @@ import (
"fmt"
"io"
"github.com/pkg/errors"
"gopkg.in/yaml.v3"
"github.com/cockroachdb/errors"
"go.yaml.in/yaml/v3"
datamover "github.com/vmware-tanzu/velero/pkg/util/datamover"
)
const currentSupportDataVersion = "v1"
@@ -40,13 +42,15 @@ type nFSVolumeSource struct {
// volumeConditions defined the current format of conditions we parsed
type volumeConditions struct {
Capacity string `yaml:"capacity,omitempty"`
StorageClass []string `yaml:"storageClass,omitempty"`
NFS *nFSVolumeSource `yaml:"nfs,omitempty"`
CSI *csiVolumeSource `yaml:"csi,omitempty"`
VolumeTypes []SupportedVolume `yaml:"volumeTypes,omitempty"`
PVCLabels map[string]string `yaml:"pvcLabels,omitempty"`
PVCPhase []string `yaml:"pvcPhase,omitempty"`
Capacity string `yaml:"capacity,omitempty"`
StorageClass []string `yaml:"storageClass,omitempty"`
NFS *nFSVolumeSource `yaml:"nfs,omitempty"`
CSI *csiVolumeSource `yaml:"csi,omitempty"`
VolumeTypes []SupportedVolume `yaml:"volumeTypes,omitempty"`
PVCLabels map[string]string `yaml:"pvcLabels,omitempty"`
PVCPhase []string `yaml:"pvcPhase,omitempty"`
PVCVolumeMode string `yaml:"pvcVolumeMode,omitempty"`
PVCAccessModes []string `yaml:"pvcAccessModes,omitempty"`
}
func (c *capacityCondition) validate() error {
@@ -97,6 +101,22 @@ func (a *Action) validate() error {
return fmt.Errorf("invalid action type %s", a.Type)
}
// TODO validate parameters
// validate parameters
if raw, ok := a.Parameters[DataMoverParameter]; ok {
// the dataMover parameter is only meaningful for the snapshot action
if a.Type != Snapshot {
return fmt.Errorf("parameter %q is only supported for the %q action, but the action type is %q",
DataMoverParameter, Snapshot, a.Type)
}
dataMover, ok := raw.(string)
if !ok {
return fmt.Errorf("parameter %q must be a string, got %T", DataMoverParameter, raw)
}
if _, ok := validDataMovers[dataMover]; !ok {
return fmt.Errorf("invalid %q value %q, valid values are %q, %q, %q",
DataMoverParameter, dataMover, datamover.DataMoverTypeVelero, datamover.DataMoverTypeVeleroFs, datamover.DataMoverTypeVeleroBlock)
}
}
return nil
}
@@ -549,6 +549,115 @@ func TestValidate(t *testing.T) {
},
wantErr: false,
},
{
name: "snapshot action with valid dataMover velero-fs",
res: &ResourcePolicies{
Version: "v1",
VolumePolicies: []VolumePolicy{
{
Action: Action{
Type: Snapshot,
Parameters: map[string]any{"dataMover": "velero-fs"},
},
Conditions: map[string]any{"storageClass": []string{"gp2"}},
},
},
},
wantErr: false,
},
{
name: "snapshot action with valid dataMover velero-block",
res: &ResourcePolicies{
Version: "v1",
VolumePolicies: []VolumePolicy{
{
Action: Action{
Type: Snapshot,
Parameters: map[string]any{"dataMover": "velero-block"},
},
Conditions: map[string]any{"storageClass": []string{"gp2"}},
},
},
},
wantErr: false,
},
{
name: "snapshot action with valid dataMover velero",
res: &ResourcePolicies{
Version: "v1",
VolumePolicies: []VolumePolicy{
{
Action: Action{
Type: Snapshot,
Parameters: map[string]any{"dataMover": "velero"},
},
Conditions: map[string]any{"storageClass": []string{"gp2"}},
},
},
},
wantErr: false,
},
{
name: "snapshot action with invalid dataMover value",
res: &ResourcePolicies{
Version: "v1",
VolumePolicies: []VolumePolicy{
{
Action: Action{
Type: Snapshot,
Parameters: map[string]any{"dataMover": "unknown-mover"},
},
Conditions: map[string]any{"storageClass": []string{"gp2"}},
},
},
},
wantErr: true,
},
{
name: "snapshot action with non-string dataMover value",
res: &ResourcePolicies{
Version: "v1",
VolumePolicies: []VolumePolicy{
{
Action: Action{
Type: Snapshot,
Parameters: map[string]any{"dataMover": 123},
},
Conditions: map[string]any{"storageClass": []string{"gp2"}},
},
},
},
wantErr: true,
},
{
name: "dataMover parameter on non-snapshot action is rejected",
res: &ResourcePolicies{
Version: "v1",
VolumePolicies: []VolumePolicy{
{
Action: Action{
Type: FSBackup,
Parameters: map[string]any{"dataMover": "velero-fs"},
},
Conditions: map[string]any{"storageClass": []string{"gp2"}},
},
},
},
wantErr: true,
},
{
name: "snapshot action without parameters still valid",
res: &ResourcePolicies{
Version: "v1",
VolumePolicies: []VolumePolicy{
{
Action: Action{Type: Snapshot},
Conditions: map[string]any{"storageClass": []string{"gp2"}},
},
},
},
wantErr: false,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
@@ -568,3 +677,85 @@ func TestValidate(t *testing.T) {
})
}
}
func TestValidateForRestore(t *testing.T) {
testCases := []struct {
name string
res *ResourcePolicies
wantErr bool
}{
{
name: "valid restore policies",
res: &ResourcePolicies{
Version: "v1",
ClusterScopedFilterPolicy: &ClusterScopedFilterPolicy{
ResourceFilters: []ResourceFilter{
{
Kinds: []string{"ClusterRole"},
},
},
},
NamespacedFilterPolicies: []NamespacedFilterPolicy{
{
Namespaces: []string{"default"},
ResourceFilters: []ResourceFilter{
{
Kinds: []string{"Pod"},
},
},
},
},
},
wantErr: false,
},
{
name: "unsupported volumePolicies for restore",
res: &ResourcePolicies{
Version: "v1",
VolumePolicies: []VolumePolicy{
{
Action: Action{Type: "skip"},
Conditions: map[string]any{
"capacity": "10Gi",
},
},
},
},
wantErr: true,
},
{
name: "unsupported includeExcludePolicy for restore",
res: &ResourcePolicies{
Version: "v1",
IncludeExcludePolicy: &IncludeExcludePolicy{
IncludedClusterScopedResources: []string{"persistentvolumes"},
},
},
wantErr: true,
},
{
name: "wrong version",
res: &ResourcePolicies{
Version: "v2",
},
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
policies := &Policies{}
err1 := policies.BuildPolicy(tc.res)
err2 := policies.ValidateForRestore()
if tc.wantErr {
if err1 == nil && err2 == nil {
t.Fatalf("Expected error %v, but not get error", tc.wantErr)
}
} else {
if err1 != nil || err2 != nil {
t.Fatalf("Expected error %v, but got error %v %v", tc.wantErr, err1, err2)
}
}
})
}
}
@@ -45,6 +45,7 @@ const (
Glusterfs SupportedVolume = "glusterfs"
GCEPersistentDisk SupportedVolume = "gcePersistentDisk"
HostPath SupportedVolume = "hostPath"
Image SupportedVolume = "image"
ISCSI SupportedVolume = "iscsi"
Local SupportedVolume = "local"
NFS SupportedVolume = "nfs"
@@ -243,5 +244,8 @@ func getVolumeTypeFromVolume(vol *corev1api.Volume) SupportedVolume {
if vol.EmptyDir != nil {
return EmptyDir
}
if vol.Image != nil {
return Image
}
return ""
}
@@ -563,6 +563,15 @@ func TestGetVolumeTypeFromVolume(t *testing.T) {
},
expected: Ephemeral,
},
{
name: "Test Image",
inputVol: &corev1api.Volume{
VolumeSource: corev1api.VolumeSource{
Image: &corev1api.ImageVolumeSource{},
},
},
expected: Image,
},
}
for _, tc := range testCases {
@@ -19,7 +19,7 @@ import (
"reflect"
"testing"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
+1 -1
View File
@@ -20,7 +20,7 @@ import (
"context"
"time"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
+1 -1
View File
@@ -17,7 +17,7 @@ limitations under the License.
package volume
import (
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/vmware-tanzu/velero/internal/credentials"
velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1"
+1 -1
View File
@@ -22,8 +22,8 @@ import (
"strings"
"sync"
"github.com/cockroachdb/errors"
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -5,7 +5,7 @@ import (
"fmt"
"strings"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
+28
View File
@@ -0,0 +1,28 @@
module github.com/vmware-tanzu/velero/pkg/apis
go 1.26.0
require (
k8s.io/api v0.36.0
k8s.io/apimachinery v0.36.0
)
require (
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/text v0.37.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/klog/v2 v2.140.0 // indirect
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 // indirect
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect
)
+68
View File
@@ -0,0 +1,68 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80=
k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34=
k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg=
k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU=
k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg=
sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8=
sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
+13
View File
@@ -184,6 +184,10 @@ type BackupSpec struct {
// +optional
// +nullable
UploaderConfig *UploaderConfigForBackup `json:"uploaderConfig,omitempty"`
// BackupType specifies how volume data is backed up, with possible values including Full and Incremental.
// +optional
BackupType BackupType `json:"backupType,omitempty"`
}
// UploaderConfigForBackup defines the configuration for the uploader when doing backup.
@@ -357,6 +361,15 @@ const (
BackupPhaseDeleting BackupPhase = "Deleting"
)
// BackupType specifies how volume data is backed up, with possible values including Full and Incremental.
// +kubebuilder:validation:Enum=Full;Incremental
type BackupType string
const (
BackupTypeFull BackupType = "Full"
BackupTypeIncremental BackupType = "Incremental"
)
// BackupStatus captures the current status of a Velero backup.
type BackupStatus struct {
// Version is the backup format major version.
+5
View File
@@ -80,6 +80,11 @@ const (
// timeout value for backup to plugins.
ResourceTimeoutAnnotation = "velero.io/resource-timeout"
// GlobalBackupVolumePolicyConfigMapAnnotation is the annotation key used to record the
// name of the cluster-wide global backup volume policies ConfigMap that contributed to a
// backup, so that `velero backup describe` can surface it.
GlobalBackupVolumePolicyConfigMapAnnotation = "velero.io/global-backup-volume-policy-configmap"
// AsyncOperationIDLabel is the label key used to identify the async operation ID
AsyncOperationIDLabel = "velero.io/async-operation-id"
+10
View File
@@ -125,6 +125,16 @@ type RestoreSpec struct {
// +nullable
ResourceModifier *corev1api.TypedLocalObjectReference `json:"resourceModifier,omitempty"`
// ResourcePolicy specifies the reference to a ConfigMap containing resource
// filter policies for this restore. The ConfigMap can contain a
// namespacedFilterPolicies section that specifies per-namespace resource type
// filters, label selectors, and resource name patterns, and a
// clusterScopedFilterPolicy section for per-kind filtering of cluster-scoped
// resources. The ConfigMap format is the same as for BackupSpec.ResourcePolicy.
// +optional
// +nullable
ResourcePolicy *corev1api.TypedLocalObjectReference `json:"resourcePolicy,omitempty"`
// UploaderConfig specifies the configuration for the restore.
// +optional
// +nullable
@@ -1415,6 +1415,11 @@ func (in *RestoreSpec) DeepCopyInto(out *RestoreSpec) {
*out = new(corev1.TypedLocalObjectReference)
(*in).DeepCopyInto(*out)
}
if in.ResourcePolicy != nil {
in, out := &in.ResourcePolicy, &out.ResourcePolicy
*out = new(corev1.TypedLocalObjectReference)
(*in).DeepCopyInto(*out)
}
if in.UploaderConfig != nil {
in, out := &in.UploaderConfig, &out.UploaderConfig
*out = new(UploaderConfigForRestore)
@@ -74,6 +74,10 @@ type TargetVolumeSpec struct {
// Namespace is the target namespace
Namespace string `json:"namespace"`
// FSType is the file system type of the target volume.
// +optional
FSType string `json:"fsType,omitempty"`
}
// DataDownloadPhase represents the lifecycle phase of a DataDownload.
@@ -60,6 +60,10 @@ type DataUploadSpec struct {
// OperationTimeout specifies the time used to wait internal operations,
// before returning error as timeout.
OperationTimeout metav1.Duration `json:"operationTimeout"`
// SourceFSType is the file system type of the source volume.
// +optional
SourceFSType string `json:"sourceFSType,omitempty"`
}
type SnapshotType string
@@ -253,4 +257,8 @@ type DataUploadResult struct {
// SnapshotSize is the logical size in Bytes of the snapshot.
// +optional
SnapshotSize int64 `json:"snapshotSize,omitempty"`
// FSType is the file system type of the volume.
// +optional
FSType string `json:"fsType,omitempty"`
}
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"path/filepath"
"strings"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+1 -1
View File
@@ -19,7 +19,7 @@ package actions
import (
"strings"
"github.com/pkg/errors"
"github.com/cockroachdb/errors"
"github.com/sirupsen/logrus"
corev1api "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

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