mirror of
https://github.com/vmware-tanzu/velero.git
synced 2026-07-19 06:22:25 +00:00
Merge branch 'main' into data-mover-support-fs-type
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
name: "Run the E2E test on kind"
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
@@ -58,7 +60,7 @@ jobs:
|
||||
- name: Check Bitnami MinIO Dockerfile version
|
||||
id: minio-version
|
||||
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 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 +135,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
|
||||
|
||||
@@ -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 \
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Fix restore finalization overwriting dynamically provisioned PV labels with stale backup values
|
||||
@@ -0,0 +1 @@
|
||||
Add CONTAINER_TOOL variable to Makefile for podman support
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
Refactor generic restore exposer to clone the PV during rebind, so as to support block data mover
|
||||
@@ -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.
|
||||
@@ -13,11 +13,11 @@ 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`.
|
||||
Kubernetes matching semantics for access modes require all requested modes to be satisfied by the PV/PVC relationship, so this proposal uses an all-of match for `pvcAccessModes`.
|
||||
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 contains all configured `spec.accessModes` values.
|
||||
- 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
|
||||
@@ -53,7 +53,7 @@ volumePolicies:
|
||||
```
|
||||
|
||||
### Match PVCs by access mode
|
||||
A user wants to apply a policy to PVCs that include `ReadWriteOnce` in `spec.accessModes`.
|
||||
A user wants to apply a policy only to PVCs whose `spec.accessModes` is exactly `ReadWriteOnce`.
|
||||
|
||||
```yaml
|
||||
version: v1
|
||||
@@ -65,9 +65,9 @@ volumePolicies:
|
||||
type: skip
|
||||
```
|
||||
|
||||
### Match all configured access modes
|
||||
A user wants to match volumes whose associated PVC includes both `ReadOnlyMany` and `ReadWriteMany`.
|
||||
A PVC that includes only one of these modes does not match.
|
||||
### 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
|
||||
@@ -81,7 +81,7 @@ volumePolicies:
|
||||
```
|
||||
|
||||
### Combine PVC spec criteria
|
||||
A user wants to select block-mode PVCs that also include `ReadWriteOnce`.
|
||||
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
|
||||
@@ -130,13 +130,13 @@ 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 every configured access mode is present in the PVC's `spec.accessModes`.
|
||||
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 values simply do not match unless the PVC has the same string value.
|
||||
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.
|
||||
@@ -221,11 +221,10 @@ func (c *pvcVolumeModeCondition) match(v *structuredVolume) bool {
|
||||
```
|
||||
|
||||
### PVC access modes condition
|
||||
`pvcAccessModesCondition` matches when all configured access modes are present in the associated PVC's access modes.
|
||||
This all-of match aligns with Kubernetes access mode matching semantics.
|
||||
`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.
|
||||
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 {
|
||||
@@ -236,15 +235,11 @@ func (c *pvcAccessModesCondition) match(v *structuredVolume) bool {
|
||||
if len(c.accessModes) == 0 {
|
||||
return true
|
||||
}
|
||||
if len(v.pvcAccessModes) == 0 {
|
||||
if len(v.pvcAccessModes) == 0 || len(v.pvcAccessModes) != len(c.accessModes) {
|
||||
return false
|
||||
}
|
||||
for _, conditionAccessMode := range c.accessModes {
|
||||
if !slices.Contains(v.pvcAccessModes, conditionAccessMode) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
return sets.New(c.accessModes...).Equal(sets.New(v.pvcAccessModes...))
|
||||
}
|
||||
```
|
||||
|
||||
@@ -266,7 +261,7 @@ 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 PVC contains the same value.
|
||||
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.
|
||||
@@ -300,7 +295,7 @@ If `pvcVolumeMode` is omitted from a policy, Velero does not add a volume mode c
|
||||
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 `ReadWriteOnce` in their access modes.
|
||||
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
|
||||
@@ -325,10 +320,10 @@ 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.
|
||||
|
||||
### Any-of access mode matching
|
||||
Another alternative is to make `pvcAccessModes` match when any configured access mode is present on the PVC.
|
||||
This was not chosen because Kubernetes access mode matching is based on satisfying all requested access modes.
|
||||
Using all-of matching avoids selecting PVCs that satisfy only part of the requested access mode set.
|
||||
### 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.
|
||||
@@ -349,7 +344,7 @@ Existing VolumePolicy behavior remains unchanged when `pvcVolumeMode` and `pvcAc
|
||||
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 PVC contains the same string value.
|
||||
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.
|
||||
|
||||
@@ -3,12 +3,12 @@ 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.41.12
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.17
|
||||
@@ -43,9 +43,9 @@ require (
|
||||
github.com/vmware-tanzu/crash-diagnostics v0.4.3
|
||||
go.uber.org/zap v1.28.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/mod v0.35.0
|
||||
golang.org/x/mod v0.36.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.45.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
|
||||
@@ -74,11 +74,11 @@ 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/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
|
||||
@@ -109,7 +109,6 @@ require (
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
|
||||
github.com/getsentry/sentry-go v0.46.0 // indirect
|
||||
github.com/go-ini/ini v1.67.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
|
||||
@@ -158,11 +157,11 @@ require (
|
||||
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
|
||||
@@ -177,7 +176,7 @@ require (
|
||||
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
|
||||
@@ -192,27 +191,28 @@ require (
|
||||
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.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
|
||||
golang.org/x/crypto v0.51.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.55.0 // indirect
|
||||
golang.org/x/sync v0.20.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-20260523011958-0a33c5d7ca68 // 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
|
||||
@@ -220,4 +220,4 @@ require (
|
||||
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
|
||||
|
||||
@@ -16,12 +16,12 @@ cloud.google.com/go/longrunning v0.9.0 h1:0EzbDEGsAvOZNbqXopgniY0w0a1phvu5IdUFq8
|
||||
cloud.google.com/go/longrunning v0.9.0/go.mod h1:pkTz846W7bF4o2SzdWJ40Hu0Re+UoNT6Q5t+igIcb8E=
|
||||
cloud.google.com/go/monitoring v1.24.3 h1:dde+gMNc0UhPZD1Azu6at2e79bfdztVDS5lvhOdsgaE=
|
||||
cloud.google.com/go/monitoring v1.24.3/go.mod h1:nYP6W0tm3N9H/bOw8am7t62YTzZY+zUeQ+Bi6+2eonI=
|
||||
cloud.google.com/go/storage v1.62.1 h1:Os0G3XbUbjZumkpDUf2Y0rLoXJTCF1kU2kWUujKYXD8=
|
||||
cloud.google.com/go/storage v1.62.1/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
|
||||
cloud.google.com/go/storage v1.62.3 h1:SZq1t23NCI+e96dH77Dg3PEfsNNEjqO8zE5AnD8gVD0=
|
||||
cloud.google.com/go/storage v1.62.3/go.mod h1:cpYz/kRVZ+UQAF1uHeea10/9ewcRbxGoGNKsS9daSXA=
|
||||
cloud.google.com/go/trace v1.11.7 h1:kDNDX8JkaAG3R2nq1lIdkb7FCSi1rCmsEtKVsty7p+U=
|
||||
cloud.google.com/go/trace v1.11.7/go.mod h1:TNn9d5V3fQVf6s4SCveVMIBS2LJUqo73GACmq/Tky0s=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1 h1:jHb/wfvRikGdxMXYV3QG/SzUOPYN9KEUUuC0Yd0/vC0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.1/go.mod h1:pzBXCYn05zvYIrwLgtK8Ap8QcjRg+0i76tMQdWN6wOk=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0 h1:aokoqcHvaGjiM3VpjKDfMMnF/8epJ+Q1HLJ7CudztqE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.22.0/go.mod h1:/WYEx9pcM9Y+Dd/APJaNlSvVSvzl54rrMdZT5+Oi2LM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
@@ -38,14 +38,14 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4 h1:jWQK1GI+LeGGUKBADtcH2rRqPxYB1Ljwms5gFA2LqrM=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.4/go.mod h1:8mwH4klAm9DUgR2EEHyEEAQlRDvLPyg5fQry3y+cDew=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0 h1:BM85pSYlVYQHdq00nxyPoOkyLF5NArJG3bOsrmbwr4k=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.7.0/go.mod h1:QYjP2cB7ZYtS/8jAbE0VSBZde/tjExqGjp+8JY6/+ts=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 h1:RHK7bS+HQMslb1sZpAokUt+zTVmue0hKSs2C791hhzU=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5 h1:IEjq88XO4PuBDcvmjQJcQGg+w+UaafSy8G5Kcb5tBhI=
|
||||
github.com/GehirnInc/crypt v0.0.0-20230320061759-8cc1b52080c5/go.mod h1:exZ0C/1emQJAw5tHOaUDyY1ycttqBAPcxuzf7QbY6ec=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.31.0 h1:DHa2U07rk8syqvCge0QIGMCE1WxGj9njT44GH7zNJLQ=
|
||||
@@ -56,8 +56,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.55.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0 h1:0s6TxfCu2KHkkZPnBfsQ2y5qia0jl3MMrmBhu3nCOYk=
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.55.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc=
|
||||
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
|
||||
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE=
|
||||
github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
|
||||
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
|
||||
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
|
||||
@@ -175,8 +175,6 @@ github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01
|
||||
github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
@@ -310,8 +308,8 @@ github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU
|
||||
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/klauspost/reedsolomon v1.14.0 h1:5YSZeclzSYg5nl349+GDG/agDtQ6MZiwUYXvVKN1Jx0=
|
||||
github.com/klauspost/reedsolomon v1.14.0/go.mod h1:yjqqjgMTQkBUHSG97/rm4zipffCNbCiZcB3kTqr++sQ=
|
||||
github.com/kopia/htmluibuild v0.0.1-0.20260502040510-a4505d4145ae h1:igSzPZDDs3icBsXWC/2zRFBRlzelXcBSODpxpORf6s8=
|
||||
github.com/kopia/htmluibuild v0.0.1-0.20260502040510-a4505d4145ae/go.mod h1:h53A5JM3t2qiwxqxusBe+PFgGcgZdS+DWCQvG5PTlto=
|
||||
github.com/kopia/htmluibuild v0.0.1-0.20260608231842-7bf9fcc0831d h1:2nHZuoDenhCDeIdDnQXLaaMziiHfIjf+INorIIUVNk8=
|
||||
github.com/kopia/htmluibuild v0.0.1-0.20260608231842-7bf9fcc0831d/go.mod h1:h53A5JM3t2qiwxqxusBe+PFgGcgZdS+DWCQvG5PTlto=
|
||||
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=
|
||||
@@ -334,8 +332,8 @@ github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo=
|
||||
github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
|
||||
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||
@@ -346,8 +344,8 @@ github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.1.0 h1:QEt5IStDpxgGjEdtOgpiZ5QhmSl3ax7qy61vi2SwHO8=
|
||||
github.com/minio/minio-go/v7 v7.1.0/go.mod h1:Dm7WS1AgLmBa0NcQD6SeJnJf+K/EUW3GR7Ks6olB3OA=
|
||||
github.com/minio/minio-go/v7 v7.2.0 h1:RCJM0R1XOsRs+A3x3UCaf3ZYbByDaLjFeAi+YCQEPhs=
|
||||
github.com/minio/minio-go/v7 v7.2.0/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
|
||||
github.com/moby/spdystream v0.5.1 h1:9sNYeYZUcci9R6/w7KDaFWEWeV4LStVG78Mpyq/Zm/Y=
|
||||
github.com/moby/spdystream v0.5.1/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
@@ -394,14 +392,14 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1
|
||||
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/project-velero/kopia v0.0.0-20260512025144-908c5c098101 h1:bTzkHkWqMM2Zp942BqDQ3TMrfAKZ6tRTG6vcRBlObps=
|
||||
github.com/project-velero/kopia v0.0.0-20260512025144-908c5c098101/go.mod h1:VxeLQ3AfxPMOraxoEqzbsOrHPSohTc6CWGs5PgMvtDs=
|
||||
github.com/project-velero/kopia v0.0.0-20260616052725-d83462d382c9 h1:sz/aL2lfvYvnbEmfbmVgjoT5NeGHSdDXQspuUnwScEU=
|
||||
github.com/project-velero/kopia v0.0.0-20260616052725-d83462d382c9/go.mod h1:iXcxxMES+wBh3cEDMli2gIO10Rkj3bMRrJGK4bQ7hsg=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
|
||||
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
|
||||
github.com/prometheus/common v0.68.1 h1:omjRRl4QP4komogpXuhfeOiisQg7xdy8VM1UY+pStaY=
|
||||
github.com/prometheus/common v0.68.1/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y=
|
||||
github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
|
||||
github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
@@ -424,11 +422,16 @@ github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xI
|
||||
github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
|
||||
github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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/tg123/go-htpasswd v1.2.4 h1:HgH8KKCjdmo7jjXWN9k1nefPBd7Be3tFCTjc2jPraPU=
|
||||
@@ -469,18 +472,20 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.6
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
|
||||
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0 h1:TC+BewnDpeiAmcscXbGMfxkO+mwYUwE/VySwvw88PfA=
|
||||
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.43.0/go.mod h1:J/ZyF4vfPwsSr9xJSPyQ4LqtcTPULFR64KwTikGLe+A=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
|
||||
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0 h1:YkCrx1zLOChi9ZcZ6euupOcsgzbVlec7D/xoEU1+cTA=
|
||||
go.opentelemetry.io/otel/metric/x v0.66.0/go.mod h1:d1+BDj9t96do0/1LoU1ayfCv79ZgNE41qbhBvnMOBZk=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
|
||||
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
|
||||
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
|
||||
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
|
||||
go.starlark.net v0.0.0-20241226192728-8dfa5b98479f h1:Zs/py28HDFATSDzPcfIzrBFjVsV7HzDEGNNVZIGsjm0=
|
||||
go.starlark.net v0.0.0-20241226192728-8dfa5b98479f/go.mod h1:YKMCv9b1WrfWmeqdV5MAuEHWsu5iC+fe6kYl2sQjdI8=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
@@ -496,14 +501,14 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@@ -515,8 +520,8 @@ golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
|
||||
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
@@ -527,8 +532,8 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -555,10 +560,10 @@ google.golang.org/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik=
|
||||
google.golang.org/api v0.283.0/go.mod h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
|
||||
google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
|
||||
google.golang.org/grpc v1.81.1/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
|
||||
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
|
||||
@@ -570,6 +575,8 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf
|
||||
gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
|
||||
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
||||
@@ -155,10 +155,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 +207,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)
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,10 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func pvcVolumeMode(mode corev1api.PersistentVolumeMode) *corev1api.PersistentVolumeMode {
|
||||
return &mode
|
||||
}
|
||||
|
||||
func TestLoadResourcePolicies(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
@@ -158,6 +162,52 @@ volumePolicies:
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "supported format pvcVolumeMode",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: Block
|
||||
action:
|
||||
type: skip
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "error format of pvcVolumeMode (not a string)",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode:
|
||||
- Block
|
||||
action:
|
||||
type: skip
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "supported format pvcAccessModes",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes:
|
||||
- ReadWriteOnce
|
||||
action:
|
||||
type: skip
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "error format of pvcAccessModes (not a list)",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes: ReadWriteOnce
|
||||
action:
|
||||
type: skip
|
||||
`,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1046,6 +1096,271 @@ volumePolicies:
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "PVC volume mode matching - Block volume mode should skip",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: Block
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-block",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeBlock),
|
||||
},
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "PVC volume mode matching - Filesystem volume mode should not skip",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: Block
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-filesystem",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeFilesystem),
|
||||
},
|
||||
},
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "PVC volume mode matching - nil volume mode should not match Filesystem",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: Filesystem
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-without-volume-mode",
|
||||
},
|
||||
},
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "PVC volume mode matching - unknown condition value should not match empty volume mode",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: foo
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-without-volume-mode",
|
||||
},
|
||||
},
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "PVC volume mode matching - omitted condition should not restrict volume mode",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes: ["ReadWriteOnce"]
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-block-rwo",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeBlock),
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce},
|
||||
},
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "PVC volume mode matching - non-PVC volume should not match",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: Filesystem
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: &corev1api.Volume{
|
||||
Name: "empty-dir-volume",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
EmptyDir: &corev1api.EmptyDirVolumeSource{},
|
||||
},
|
||||
},
|
||||
pvc: nil,
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "PVC access modes matching - non-PVC volume should not match",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes: ["ReadWriteOnce"]
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: &corev1api.Volume{
|
||||
Name: "configmap-volume",
|
||||
VolumeSource: corev1api.VolumeSource{
|
||||
ConfigMap: &corev1api.ConfigMapVolumeSource{},
|
||||
},
|
||||
},
|
||||
pvc: nil,
|
||||
skip: false,
|
||||
},
|
||||
|
||||
{
|
||||
name: "PVC access modes matching - ReadWriteOnce should skip",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes: ["ReadWriteOnce"]
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-rwo",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce},
|
||||
},
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "PVC access modes matching - extra PVC access mode should not skip",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes: ["ReadWriteOnce"]
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-rwo-rom",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce, corev1api.ReadOnlyMany},
|
||||
},
|
||||
},
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "PVC access modes matching - ReadWriteMany should not skip",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes: ["ReadWriteOnce"]
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-rwx",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteMany},
|
||||
},
|
||||
},
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "PVC access modes matching - exact access mode set should match regardless of order",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes: ["ReadWriteMany", "ReadOnlyMany"]
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-rom-rwx",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadOnlyMany, corev1api.ReadWriteMany},
|
||||
},
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
{
|
||||
name: "PVC access modes matching - missing one configured access mode should not skip",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes: ["ReadOnlyMany", "ReadWriteMany"]
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-rwx",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteMany},
|
||||
},
|
||||
},
|
||||
skip: false,
|
||||
},
|
||||
{
|
||||
name: "PVC access modes matching - Combined with volume mode",
|
||||
yamlData: `version: v1
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: Block
|
||||
pvcAccessModes: ["ReadWriteOnce"]
|
||||
action:
|
||||
type: skip`,
|
||||
vol: nil,
|
||||
podVol: nil,
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "default",
|
||||
Name: "pvc-block-rwo",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeBlock),
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce},
|
||||
},
|
||||
},
|
||||
skip: true,
|
||||
},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1119,28 +1434,36 @@ func TestGetMatchAction_Errors(t *testing.T) {
|
||||
|
||||
func TestParsePVC(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
pvc *corev1api.PersistentVolumeClaim
|
||||
expectedLabels map[string]string
|
||||
expectedPhase string
|
||||
expectErr bool
|
||||
name string
|
||||
pvc *corev1api.PersistentVolumeClaim
|
||||
expectedLabels map[string]string
|
||||
expectedPhase string
|
||||
expectedVolumeMode string
|
||||
expectedAccessModes []string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid PVC with labels and Pending phase",
|
||||
name: "valid PVC with labels, Pending phase, Block volume mode, and access modes",
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{"env": "prod"},
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeBlock),
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce, corev1api.ReadOnlyMany},
|
||||
},
|
||||
Status: corev1api.PersistentVolumeClaimStatus{
|
||||
Phase: corev1api.ClaimPending,
|
||||
},
|
||||
},
|
||||
expectedLabels: map[string]string{"env": "prod"},
|
||||
expectedPhase: "Pending",
|
||||
expectErr: false,
|
||||
expectedLabels: map[string]string{"env": "prod"},
|
||||
expectedPhase: "Pending",
|
||||
expectedVolumeMode: "Block",
|
||||
expectedAccessModes: []string{"ReadWriteOnce", "ReadOnlyMany"},
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid PVC with Bound phase",
|
||||
name: "valid PVC with Bound phase and nil volume mode",
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Labels: map[string]string{},
|
||||
@@ -1149,27 +1472,52 @@ func TestParsePVC(t *testing.T) {
|
||||
Phase: corev1api.ClaimBound,
|
||||
},
|
||||
},
|
||||
expectedLabels: nil,
|
||||
expectedPhase: "Bound",
|
||||
expectErr: false,
|
||||
expectedLabels: nil,
|
||||
expectedPhase: "Bound",
|
||||
expectedVolumeMode: "",
|
||||
expectedAccessModes: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid PVC with Lost phase",
|
||||
name: "valid PVC with Lost phase and Filesystem volume mode",
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeFilesystem),
|
||||
},
|
||||
Status: corev1api.PersistentVolumeClaimStatus{
|
||||
Phase: corev1api.ClaimLost,
|
||||
},
|
||||
},
|
||||
expectedLabels: nil,
|
||||
expectedPhase: "Lost",
|
||||
expectErr: false,
|
||||
expectedLabels: nil,
|
||||
expectedPhase: "Lost",
|
||||
expectedVolumeMode: "Filesystem",
|
||||
expectedAccessModes: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil PVC pointer",
|
||||
pvc: (*corev1api.PersistentVolumeClaim)(nil),
|
||||
expectedLabels: nil,
|
||||
expectedPhase: "",
|
||||
expectErr: false,
|
||||
name: "valid PVC with unknown non-nil volume mode",
|
||||
pvc: &corev1api.PersistentVolumeClaim{
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
VolumeMode: pvcVolumeMode(corev1api.PersistentVolumeMode("foo")),
|
||||
},
|
||||
Status: corev1api.PersistentVolumeClaimStatus{
|
||||
Phase: corev1api.ClaimBound,
|
||||
},
|
||||
},
|
||||
expectedLabels: nil,
|
||||
expectedPhase: "Bound",
|
||||
expectedVolumeMode: "foo",
|
||||
expectedAccessModes: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "nil PVC pointer",
|
||||
pvc: (*corev1api.PersistentVolumeClaim)(nil),
|
||||
expectedLabels: nil,
|
||||
expectedPhase: "",
|
||||
expectedVolumeMode: "",
|
||||
expectedAccessModes: nil,
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1180,6 +1528,8 @@ func TestParsePVC(t *testing.T) {
|
||||
|
||||
assert.Equal(t, tc.expectedLabels, s.pvcLabels)
|
||||
assert.Equal(t, tc.expectedPhase, s.pvcPhase)
|
||||
assert.Equal(t, tc.expectedVolumeMode, s.pvcVolumeMode)
|
||||
assert.Equal(t, tc.expectedAccessModes, s.pvcAccessModes)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1509,7 +1859,7 @@ namespacedFilterPolicies:
|
||||
- namespaces: ["team-frontend-*", "specific-ns"]
|
||||
resourceFilters:
|
||||
- kinds: ["Pod", "ConfigMap", "Secret"]
|
||||
- namespaces: ["team-*", "another-pattern"]
|
||||
- namespaces: ["team-*", "another-pattern"]
|
||||
resourceFilters:
|
||||
- kinds: ["Deployment", "Service"]`
|
||||
|
||||
@@ -1680,3 +2030,145 @@ clusterScopedFilterPolicy:
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPVCVolumeModeMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
condition *pvcVolumeModeCondition
|
||||
volume *structuredVolume
|
||||
expectedMatch bool
|
||||
}{
|
||||
{
|
||||
name: "match Block volume mode",
|
||||
condition: &pvcVolumeModeCondition{volumeMode: "Block"},
|
||||
volume: &structuredVolume{pvcVolumeMode: "Block"},
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "match Filesystem volume mode",
|
||||
condition: &pvcVolumeModeCondition{volumeMode: "Filesystem"},
|
||||
volume: &structuredVolume{pvcVolumeMode: "Filesystem"},
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "no match for different volume mode",
|
||||
condition: &pvcVolumeModeCondition{volumeMode: "Block"},
|
||||
volume: &structuredVolume{pvcVolumeMode: "Filesystem"},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "case-sensitive no match for lowercase volume mode",
|
||||
condition: &pvcVolumeModeCondition{volumeMode: "block"},
|
||||
volume: &structuredVolume{pvcVolumeMode: "Block"},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "no match for unknown condition value against Filesystem",
|
||||
condition: &pvcVolumeModeCondition{volumeMode: "foo"},
|
||||
volume: &structuredVolume{pvcVolumeMode: "Filesystem"},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "match unknown condition value only when volume has same value",
|
||||
condition: &pvcVolumeModeCondition{volumeMode: "foo"},
|
||||
volume: &structuredVolume{pvcVolumeMode: "foo"},
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "no match for empty volume mode",
|
||||
condition: &pvcVolumeModeCondition{volumeMode: "Block"},
|
||||
volume: &structuredVolume{pvcVolumeMode: ""},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "match with empty volume mode condition (always match)",
|
||||
condition: &pvcVolumeModeCondition{volumeMode: ""},
|
||||
volume: &structuredVolume{pvcVolumeMode: "Block"},
|
||||
expectedMatch: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := tc.condition.match(tc.volume)
|
||||
assert.Equal(t, tc.expectedMatch, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPVCAccessModesMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
condition *pvcAccessModesCondition
|
||||
volume *structuredVolume
|
||||
expectedMatch bool
|
||||
}{
|
||||
{
|
||||
name: "match ReadWriteOnce access mode",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce"}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce"}},
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "match exact multiple access modes",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce", "ReadOnlyMany"}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce", "ReadOnlyMany"}},
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "match exact multiple access modes regardless of order",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{"ReadOnlyMany", "ReadWriteOnce"}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce", "ReadOnlyMany"}},
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "no match when one of multiple access modes is missing",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce", "ReadOnlyMany"}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadOnlyMany"}},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "no match when PVC has extra access modes",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteMany"}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce", "ReadWriteMany"}},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "no match for different access mode",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce"}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteMany"}},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "case-sensitive no match for lowercase access mode",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{"readwriteonce"}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce"}},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "no match for empty PVC access modes",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{"ReadWriteOnce"}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{}},
|
||||
expectedMatch: false,
|
||||
},
|
||||
{
|
||||
name: "match with empty access modes list (always match)",
|
||||
condition: &pvcAccessModesCondition{accessModes: []string{}},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce"}},
|
||||
expectedMatch: true,
|
||||
},
|
||||
{
|
||||
name: "match with nil access modes list (always match)",
|
||||
condition: &pvcAccessModesCondition{accessModes: nil},
|
||||
volume: &structuredVolume{pvcAccessModes: []string{"ReadWriteOnce"}},
|
||||
expectedMatch: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := tc.condition.match(tc.volume)
|
||||
assert.Equal(t, tc.expectedMatch, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,11 @@ package resourcepolicies
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/labels"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"go.yaml.in/yaml/v3"
|
||||
@@ -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 {
|
||||
|
||||
@@ -40,13 +40,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 {
|
||||
|
||||
@@ -6107,15 +6107,43 @@ func TestGetNamespaceFilter(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// First call (populates cache)
|
||||
result := req.GetNamespaceFilter(tt.namespace)
|
||||
|
||||
if tt.expectNil {
|
||||
assert.Nil(t, result)
|
||||
|
||||
// Verify negative cache
|
||||
val, ok := req.NamespaceFilterCache.Load(tt.namespace)
|
||||
assert.True(t, ok)
|
||||
assert.Nil(t, val)
|
||||
} else {
|
||||
assert.NotNil(t, result)
|
||||
// Ensure the returned filter points to the correct reference in our map
|
||||
assert.Same(t, filterMap[tt.expectMatched], result)
|
||||
|
||||
// Verify positive cache
|
||||
val, ok := req.NamespaceFilterCache.Load(tt.namespace)
|
||||
assert.True(t, ok)
|
||||
assert.Same(t, filterMap[tt.expectMatched], val)
|
||||
}
|
||||
|
||||
// Second call (hits cache)
|
||||
result2 := req.GetNamespaceFilter(tt.namespace)
|
||||
assert.Same(t, result, result2)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNamespaceFilter_CacheBypass(t *testing.T) {
|
||||
req := &Request{
|
||||
NamespacedFilterMap: make(map[string]*ResolvedNamespaceFilter),
|
||||
}
|
||||
|
||||
cachedFilter := &ResolvedNamespaceFilter{}
|
||||
req.NamespaceFilterCache.Store("cached-ns", cachedFilter)
|
||||
|
||||
// Since NamespacedFilterMap is empty, this would normally return nil,
|
||||
// but the cache should return our cachedFilter.
|
||||
assert.Same(t, cachedFilter, req.GetNamespaceFilter("cached-ns"))
|
||||
}
|
||||
|
||||
+23
-4
@@ -100,6 +100,10 @@ type Request struct {
|
||||
// NamespacedFilterPatterns preserves the order of patterns for first-match semantics
|
||||
// and caches pre-compiled globs to avoid repeated compilation in the hot path.
|
||||
NamespacedFilterPatterns []NamespacedFilterPattern
|
||||
|
||||
// NamespaceFilterCache memoizes the resolved filter for a given namespace.
|
||||
// sync.Map is used because item backuppers access this concurrently.
|
||||
NamespaceFilterCache sync.Map
|
||||
}
|
||||
|
||||
// NamespacedFilterPattern pairs a namespace pattern string with its pre-compiled
|
||||
@@ -149,22 +153,37 @@ func (r *Request) StopWorkerPool() {
|
||||
|
||||
// GetNamespaceFilter returns the resolved filter for a namespace, or nil
|
||||
// if the namespace should use global filters. Uses first-match semantics
|
||||
// when multiple patterns could match the same namespace.
|
||||
// when multiple patterns could match the same namespace, but exact matches
|
||||
// always take precedence over glob patterns regardless of definition order.
|
||||
func (r *Request) GetNamespaceFilter(namespace string) *ResolvedNamespaceFilter {
|
||||
if r.NamespacedFilterMap == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// First check for exact match
|
||||
// 1. Check the concurrent cache first
|
||||
if val, ok := r.NamespaceFilterCache.Load(namespace); ok {
|
||||
if val == nil {
|
||||
return nil
|
||||
}
|
||||
return val.(*ResolvedNamespaceFilter)
|
||||
}
|
||||
|
||||
// 2. Check for exact match first
|
||||
if f, ok := r.NamespacedFilterMap[namespace]; ok {
|
||||
r.NamespaceFilterCache.Store(namespace, f)
|
||||
return f
|
||||
}
|
||||
|
||||
// Walk patterns in definition order using pre-compiled globs (no allocation per call)
|
||||
// 3. Walk patterns in definition order using pre-compiled globs
|
||||
for _, p := range r.NamespacedFilterPatterns {
|
||||
if p.Compiled != nil && p.Compiled.Match(namespace) {
|
||||
return r.NamespacedFilterMap[p.Pattern]
|
||||
filter := r.NamespacedFilterMap[p.Pattern]
|
||||
r.NamespaceFilterCache.Store(namespace, filter)
|
||||
return filter
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Cache the miss
|
||||
r.NamespaceFilterCache.Store(namespace, nil)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -421,7 +421,16 @@ func (ctx *finalizerContext) patchDynamicPVWithVolumeInfo() (errs results.Result
|
||||
// patch PV's reclaim policy and label using the corresponding data stored in volume info
|
||||
if needPatch(pv, volInfo.PVInfo) {
|
||||
updatedPV := pv.DeepCopy()
|
||||
updatedPV.Labels = volInfo.PVInfo.Labels
|
||||
|
||||
if updatedPV.Labels == nil {
|
||||
updatedPV.Labels = make(map[string]string)
|
||||
}
|
||||
for k, v := range volInfo.PVInfo.Labels {
|
||||
if _, exists := updatedPV.Labels[k]; !exists {
|
||||
updatedPV.Labels[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
updatedPV.Spec.PersistentVolumeReclaimPolicy = corev1api.PersistentVolumeReclaimPolicy(volInfo.PVInfo.ReclaimPolicy)
|
||||
if err := kubeutil.PatchResource(pv, updatedPV, ctx.crClient); err != nil {
|
||||
return false, err
|
||||
@@ -553,13 +562,10 @@ func needPatch(newPV *corev1api.PersistentVolume, pvInfo *volume.PVInfo) bool {
|
||||
}
|
||||
|
||||
newPVLabels, pvLabels := newPV.Labels, pvInfo.Labels
|
||||
for k, v := range pvLabels {
|
||||
for k := range pvLabels {
|
||||
if _, ok := newPVLabels[k]; !ok {
|
||||
return true
|
||||
}
|
||||
if newPVLabels[k] != v {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
|
||||
@@ -634,6 +634,87 @@ func Test_restoreFinalizerReconciler_finishProcessing(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedPatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
newPV *corev1api.PersistentVolume
|
||||
pvInfo *volume.PVInfo
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "reclaim policy differs",
|
||||
newPV: builder.ForPersistentVolume("pv1").
|
||||
ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).Result(),
|
||||
pvInfo: &volume.PVInfo{
|
||||
ReclaimPolicy: string(corev1api.PersistentVolumeReclaimRetain),
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "backup has label new PV does not",
|
||||
newPV: builder.ForPersistentVolume("pv1").
|
||||
ObjectMeta(builder.WithLabels("existing", "val")).
|
||||
ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).Result(),
|
||||
pvInfo: &volume.PVInfo{
|
||||
ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete),
|
||||
Labels: map[string]string{"existing": "val", "missing": "val"},
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "same labels same values",
|
||||
newPV: builder.ForPersistentVolume("pv1").
|
||||
ObjectMeta(builder.WithLabels("key", "val")).
|
||||
ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).Result(),
|
||||
pvInfo: &volume.PVInfo{
|
||||
ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete),
|
||||
Labels: map[string]string{"key": "val"},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "same label key different values",
|
||||
newPV: builder.ForPersistentVolume("pv1").
|
||||
ObjectMeta(builder.WithLabels("topology.kubernetes.io/zone", "us-west-2a")).
|
||||
ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).Result(),
|
||||
pvInfo: &volume.PVInfo{
|
||||
ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete),
|
||||
Labels: map[string]string{"topology.kubernetes.io/zone": "us-east-1a"},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "new PV has labels backup does not",
|
||||
newPV: builder.ForPersistentVolume("pv1").
|
||||
ObjectMeta(builder.WithLabels("provisioner-label", "val")).
|
||||
ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).Result(),
|
||||
pvInfo: &volume.PVInfo{
|
||||
ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete),
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "both labels nil",
|
||||
newPV: builder.ForPersistentVolume("pv1").
|
||||
ReclaimPolicy(corev1api.PersistentVolumeReclaimDelete).Result(),
|
||||
pvInfo: &volume.PVInfo{
|
||||
ReclaimPolicy: string(corev1api.PersistentVolumeReclaimDelete),
|
||||
Labels: nil,
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := needPatch(tc.newPV, tc.pvInfo)
|
||||
assert.Equal(t, tc.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRestoreOperationList(t *testing.T) {
|
||||
var empty []*itemoperation.RestoreOperation
|
||||
tests := []struct {
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
corev1api "k8s.io/api/core/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
@@ -425,16 +426,19 @@ func (e *genericRestoreExposer) RebindVolume(ctx context.Context, ownerObject co
|
||||
|
||||
curLog.WithField("restore PV", restorePV.Name).WithField("retained", (retained != nil)).Info("Restore PV is retained")
|
||||
|
||||
var rebindPV *corev1api.PersistentVolume
|
||||
|
||||
defer func() {
|
||||
if retained != nil {
|
||||
curLog.WithField("retained PV", retained.Name).Info("Deleting retained PV on error")
|
||||
kube.DeletePVIfAny(ctx, e.kubeClient.CoreV1(), retained.Name, curLog)
|
||||
}
|
||||
}()
|
||||
|
||||
if retained != nil {
|
||||
restorePV = retained
|
||||
}
|
||||
if rebindPV != nil {
|
||||
curLog.WithField("rebind PV", rebindPV.Name).Info("Deleting rebind PV on error")
|
||||
kube.DeletePVIfAny(ctx, e.kubeClient.CoreV1(), rebindPV.Name, curLog)
|
||||
}
|
||||
}()
|
||||
|
||||
err = kube.EnsureDeletePod(ctx, e.kubeClient.CoreV1(), restorePodName, ownerObject.Namespace, param.OperationTimeout)
|
||||
if err != nil {
|
||||
@@ -448,42 +452,38 @@ func (e *genericRestoreExposer) RebindVolume(ctx context.Context, ownerObject co
|
||||
|
||||
curLog.WithField("restore PVC", restorePVCName).Info("Restore PVC is deleted")
|
||||
|
||||
_, err = kube.RebindPVC(ctx, e.kubeClient.CoreV1(), targetPVC, restorePV.Name)
|
||||
rebindPV, err = kube.RebindPV(ctx, e.kubeClient.CoreV1(), uuid.NewString(), retained, targetPVC, orgReclaim, param.TargetFSType)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error to rebind target PVC %s/%s to %s", targetPVC.Namespace, targetPVC.Name, restorePV.Name)
|
||||
return errors.Wrapf(err, "error rebinding PV for target PVC %s", param.TargetPVCName)
|
||||
}
|
||||
|
||||
curLog.WithField("tartet PVC", fmt.Sprintf("%s/%s", targetPVC.Namespace, targetPVC.Name)).WithField("restore PV", restorePV.Name).Info("Target PVC is rebound to restore PV")
|
||||
curLog.WithField("rebind PV", rebindPV.Name).Info("Rebind PV is created")
|
||||
|
||||
var matchLabel map[string]string
|
||||
if targetPVC.Spec.Selector != nil {
|
||||
matchLabel = targetPVC.Spec.Selector.MatchLabels
|
||||
}
|
||||
|
||||
restorePVName := restorePV.Name
|
||||
restorePV, err = kube.ResetPVBinding(ctx, e.kubeClient.CoreV1(), restorePV, matchLabel, targetPVC)
|
||||
err = kube.EnsureDeletePV(ctx, e.kubeClient.CoreV1(), retained.Name, param.OperationTimeout)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error to reset binding info for restore PV %s", restorePVName)
|
||||
return errors.Wrapf(err, "error deleting PV %s", retained.Name)
|
||||
}
|
||||
|
||||
curLog.WithField("restore PV", restorePV.Name).Info("Restore PV is rebound")
|
||||
|
||||
restorePV, err = kube.WaitPVBound(ctx, e.kubeClient.CoreV1(), restorePV.Name, targetPVC.Name, targetPVC.Namespace, param.OperationTimeout)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error to wait restore PV bound, restore PV %s", restorePVName)
|
||||
}
|
||||
|
||||
curLog.WithField("restore PV", restorePV.Name).Info("Restore PV is ready")
|
||||
curLog.WithField("retained PV", retained.Name).Info("Retained PV is deleted")
|
||||
|
||||
retained = nil
|
||||
|
||||
_, err = kube.SetPVReclaimPolicy(ctx, e.kubeClient.CoreV1(), restorePV, orgReclaim)
|
||||
_, err = kube.RebindPVC(ctx, e.kubeClient.CoreV1(), targetPVC, rebindPV.Name)
|
||||
if err != nil {
|
||||
curLog.WithField("restore PV", restorePV.Name).WithError(err).Warn("Restore PV's reclaim policy is not restored")
|
||||
} else {
|
||||
curLog.WithField("restore PV", restorePV.Name).Info("Restore PV's reclaim policy is restored")
|
||||
return errors.Wrapf(err, "error to rebind target PVC %s/%s to %s", targetPVC.Namespace, targetPVC.Name, rebindPV.Name)
|
||||
}
|
||||
|
||||
curLog.WithField("rebind PV", rebindPV.Name).Info("Target PVC is rebound to rebind PV")
|
||||
|
||||
_, err = kube.WaitPVBound(ctx, e.kubeClient.CoreV1(), rebindPV.Name, targetPVC.Name, targetPVC.Namespace, param.OperationTimeout)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error to wait rebind PV ready, rebind PV %s", rebindPV.Name)
|
||||
}
|
||||
|
||||
curLog.WithField("rebind PV", rebindPV.Name).Info("Rebind PV is ready")
|
||||
|
||||
rebindPV = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -352,8 +352,6 @@ func TestRebindVolume(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
hookCount := 0
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
kubeClientObj []runtime.Object
|
||||
@@ -445,6 +443,50 @@ func TestRebindVolume(t *testing.T) {
|
||||
},
|
||||
err: "error to delete restore PVC fake-restore: error to delete pvc fake-restore: fake-delete-error",
|
||||
},
|
||||
{
|
||||
name: "rebind pv fail",
|
||||
targetPVCName: "fake-target-pvc",
|
||||
targetNamespace: "fake-ns",
|
||||
ownerRestore: restore,
|
||||
kubeClientObj: []runtime.Object{
|
||||
targetPVCObj,
|
||||
restorePVCObj,
|
||||
restorePVObj,
|
||||
restorePod,
|
||||
},
|
||||
kubeReactors: []reactor{
|
||||
{
|
||||
verb: "create",
|
||||
resource: "persistentvolumes",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, errors.New("fake-create-error")
|
||||
},
|
||||
},
|
||||
},
|
||||
err: "error rebinding PV for target PVC fake-target-pvc: fake-create-error",
|
||||
},
|
||||
{
|
||||
name: "delete retained pv fail",
|
||||
targetPVCName: "fake-target-pvc",
|
||||
targetNamespace: "fake-ns",
|
||||
ownerRestore: restore,
|
||||
kubeClientObj: []runtime.Object{
|
||||
targetPVCObj,
|
||||
restorePVCObj,
|
||||
restorePVObj,
|
||||
restorePod,
|
||||
},
|
||||
kubeReactors: []reactor{
|
||||
{
|
||||
verb: "delete",
|
||||
resource: "persistentvolumes",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, errors.New("fake-delete-error")
|
||||
},
|
||||
},
|
||||
},
|
||||
err: "error deleting PV fake-restore-pv: error to delete pv fake-restore-pv: fake-delete-error",
|
||||
},
|
||||
{
|
||||
name: "rebind target pvc fail",
|
||||
targetPVCName: "fake-target-pvc",
|
||||
@@ -465,10 +507,10 @@ func TestRebindVolume(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
err: "error to rebind target PVC fake-ns/fake-target-pvc to fake-restore-pv: error patching PVC: fake-patch-error",
|
||||
err: "error to rebind target PVC fake-ns/fake-target-pvc to",
|
||||
},
|
||||
{
|
||||
name: "reset pv binding fail",
|
||||
name: "wait rebind PV ready fail",
|
||||
targetPVCName: "fake-target-pvc",
|
||||
targetNamespace: "fake-ns",
|
||||
ownerRestore: restore,
|
||||
@@ -478,34 +520,7 @@ func TestRebindVolume(t *testing.T) {
|
||||
restorePVObj,
|
||||
restorePod,
|
||||
},
|
||||
kubeReactors: []reactor{
|
||||
{
|
||||
verb: "patch",
|
||||
resource: "persistentvolumes",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
if hookCount == 0 {
|
||||
hookCount++
|
||||
return false, nil, nil
|
||||
} else {
|
||||
return true, nil, errors.New("fake-patch-error")
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
err: "error to reset binding info for restore PV fake-restore-pv: error patching PV: fake-patch-error",
|
||||
},
|
||||
{
|
||||
name: "wait restore PV bound fail",
|
||||
targetPVCName: "fake-target-pvc",
|
||||
targetNamespace: "fake-ns",
|
||||
ownerRestore: restore,
|
||||
kubeClientObj: []runtime.Object{
|
||||
targetPVCObj,
|
||||
restorePVCObj,
|
||||
restorePVObj,
|
||||
restorePod,
|
||||
},
|
||||
err: "error to wait restore PV bound, restore PV fake-restore-pv: error to wait for bound of PV: context deadline exceeded",
|
||||
err: "error to wait rebind PV ready, rebind PV",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -533,14 +548,16 @@ func TestRebindVolume(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
hookCount = 0
|
||||
|
||||
err := exposer.RebindVolume(t.Context(), ownerObject, GenericRestoreRebindVolumeParam{
|
||||
TargetPVCName: test.targetPVCName,
|
||||
TargetNamespace: test.targetNamespace,
|
||||
OperationTimeout: time.Millisecond,
|
||||
})
|
||||
assert.EqualError(t, err, test.err)
|
||||
if test.err != "" {
|
||||
assert.ErrorContains(t, err, test.err)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -161,6 +162,42 @@ func EnsureDeletePVC(ctx context.Context, pvcGetter corev1client.CoreV1Interface
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsureDeletePV(ctx context.Context, pvGetter corev1client.CoreV1Interface, pvName string, timeout time.Duration) error {
|
||||
err := pvGetter.PersistentVolumes().Delete(ctx, pvName, metav1.DeleteOptions{})
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error to delete pv %s", pvName)
|
||||
}
|
||||
|
||||
if timeout == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var updated *corev1api.PersistentVolume
|
||||
err = wait.PollUntilContextTimeout(ctx, waitInternal, timeout, true, func(ctx context.Context) (bool, error) {
|
||||
pv, err := pvGetter.PersistentVolumes().Get(ctx, pvName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if apierrors.IsNotFound(err) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, errors.Wrapf(err, "error to get pv %s", pvName)
|
||||
}
|
||||
|
||||
updated = pv
|
||||
return false, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, context.DeadlineExceeded) {
|
||||
return errors.Errorf("timeout to assure pv %s is deleted, finalizers in pv %v", pvName, updated.Finalizers)
|
||||
} else {
|
||||
return errors.Wrapf(err, "error to ensure pv deleted for %s", pvName)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// EnsurePVDeleted ensures a PV has been deleted. This function is supposed to be called after EnsureDeletePVC
|
||||
// If timeout is 0, it doesn't wait and return nil
|
||||
func EnsurePVDeleted(ctx context.Context, pvGetter corev1client.CoreV1Interface, pvName string, timeout time.Duration) error {
|
||||
@@ -269,6 +306,87 @@ func ResetPVBinding(ctx context.Context, pvGetter corev1client.CoreV1Interface,
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func RebindPV(ctx context.Context, pvGetter corev1client.CoreV1Interface, pvName string, source *corev1api.PersistentVolume,
|
||||
pvc *corev1api.PersistentVolumeClaim, policy corev1api.PersistentVolumeReclaimPolicy, fsType string) (*corev1api.PersistentVolume, error) {
|
||||
if source == nil {
|
||||
return nil, errors.New("source PV is required to rebind PV")
|
||||
}
|
||||
|
||||
if pvc == nil {
|
||||
return nil, errors.New("target PVC is required to rebind PV")
|
||||
}
|
||||
|
||||
pvLabel := make(map[string]string)
|
||||
|
||||
maps.Copy(pvLabel, source.Labels)
|
||||
|
||||
if pvc.Spec.Selector != nil {
|
||||
maps.Copy(pvLabel, pvc.Spec.Selector.MatchLabels)
|
||||
}
|
||||
|
||||
pvAnnotations := make(map[string]string)
|
||||
maps.Copy(pvAnnotations, source.Annotations)
|
||||
delete(pvAnnotations, KubeAnnBoundByController)
|
||||
|
||||
pv := &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: pvName,
|
||||
Labels: pvLabel,
|
||||
Annotations: pvAnnotations,
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeSpec{
|
||||
Capacity: source.Spec.Capacity,
|
||||
PersistentVolumeSource: clonePVSource(&source.Spec.PersistentVolumeSource, fsType),
|
||||
AccessModes: source.Spec.AccessModes,
|
||||
PersistentVolumeReclaimPolicy: policy,
|
||||
StorageClassName: source.Spec.StorageClassName,
|
||||
VolumeMode: pvc.Spec.VolumeMode,
|
||||
NodeAffinity: source.Spec.NodeAffinity,
|
||||
VolumeAttributesClassName: source.Spec.VolumeAttributesClassName,
|
||||
MountOptions: source.Spec.MountOptions,
|
||||
ClaimRef: &corev1api.ObjectReference{
|
||||
Kind: pvc.Kind,
|
||||
Namespace: pvc.Namespace,
|
||||
Name: pvc.Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return pvGetter.PersistentVolumes().Create(ctx, pv, metav1.CreateOptions{})
|
||||
}
|
||||
|
||||
func clonePVSource(source *corev1api.PersistentVolumeSource, newFSType string) corev1api.PersistentVolumeSource {
|
||||
newSource := source.DeepCopy()
|
||||
|
||||
if newFSType != "" {
|
||||
if newSource.CSI != nil {
|
||||
newSource.CSI.FSType = newFSType
|
||||
} else if newSource.AWSElasticBlockStore != nil {
|
||||
newSource.AWSElasticBlockStore.FSType = newFSType
|
||||
} else if newSource.AzureDisk != nil {
|
||||
newSource.AzureDisk.FSType = &newFSType
|
||||
} else if newSource.VsphereVolume != nil {
|
||||
newSource.VsphereVolume.FSType = newFSType
|
||||
} else if newSource.GCEPersistentDisk != nil {
|
||||
newSource.GCEPersistentDisk.FSType = newFSType
|
||||
} else if newSource.Cinder != nil {
|
||||
newSource.Cinder.FSType = newFSType
|
||||
} else if newSource.ISCSI != nil {
|
||||
newSource.ISCSI.FSType = newFSType
|
||||
} else if newSource.RBD != nil {
|
||||
newSource.RBD.FSType = newFSType
|
||||
} else if newSource.FC != nil {
|
||||
newSource.FC.FSType = newFSType
|
||||
} else if newSource.Local != nil {
|
||||
newSource.Local.FSType = &newFSType
|
||||
} else if newSource.FlexVolume != nil {
|
||||
newSource.FlexVolume.FSType = newFSType
|
||||
}
|
||||
}
|
||||
|
||||
return *newSource
|
||||
}
|
||||
|
||||
// SetPVReclaimPolicy sets the specified reclaim policy to a PV
|
||||
func SetPVReclaimPolicy(ctx context.Context, pvGetter corev1client.CoreV1Interface, pv *corev1api.PersistentVolume,
|
||||
policy corev1api.PersistentVolumeReclaimPolicy) (*corev1api.PersistentVolume, error) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/cockroachdb/errors"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
@@ -707,7 +708,7 @@ func TestEnsureDeletePVC(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDeletePV(t *testing.T) {
|
||||
func TestEnsurePVDeleted(t *testing.T) {
|
||||
pvObject := &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "fake-pv",
|
||||
@@ -2049,3 +2050,317 @@ func TestGetVolumeTopology(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDeletePV(t *testing.T) {
|
||||
pvObj := &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "fake-pv",
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pvName string
|
||||
timeout time.Duration
|
||||
kubeClientObj []runtime.Object
|
||||
kubeReactors []reactor
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "delete error",
|
||||
pvName: "fake-pv",
|
||||
kubeReactors: []reactor{
|
||||
{
|
||||
verb: "delete",
|
||||
resource: "persistentvolumes",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, errors.New("delete error")
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "error to delete pv fake-pv: delete error",
|
||||
},
|
||||
{
|
||||
name: "success without wait",
|
||||
pvName: "fake-pv",
|
||||
timeout: 0,
|
||||
kubeClientObj: []runtime.Object{pvObj},
|
||||
},
|
||||
{
|
||||
name: "success with wait",
|
||||
pvName: "fake-pv",
|
||||
timeout: time.Second,
|
||||
kubeReactors: []reactor{
|
||||
{
|
||||
verb: "get",
|
||||
resource: "persistentvolumes",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, apierrors.NewNotFound(corev1api.Resource("persistentvolumes"), "fake-pv")
|
||||
},
|
||||
},
|
||||
},
|
||||
kubeClientObj: []runtime.Object{pvObj},
|
||||
},
|
||||
{
|
||||
name: "get error during wait",
|
||||
pvName: "fake-pv",
|
||||
timeout: time.Millisecond,
|
||||
kubeClientObj: []runtime.Object{pvObj},
|
||||
kubeReactors: []reactor{
|
||||
{
|
||||
verb: "get",
|
||||
resource: "persistentvolumes",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, errors.New("get error")
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "error to ensure pv deleted for fake-pv: error to get pv fake-pv: get error",
|
||||
},
|
||||
{
|
||||
name: "wait timeout",
|
||||
pvName: "fake-pv",
|
||||
timeout: time.Millisecond,
|
||||
kubeClientObj: []runtime.Object{pvObj},
|
||||
kubeReactors: []reactor{
|
||||
{
|
||||
verb: "delete",
|
||||
resource: "persistentvolumes",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, nil // fake delete, pv will still be in tracker
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "timeout to assure pv fake-pv is deleted, finalizers in pv []",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...)
|
||||
for _, reactor := range test.kubeReactors {
|
||||
fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc)
|
||||
}
|
||||
|
||||
err := EnsureDeletePV(t.Context(), fakeKubeClient.CoreV1(), test.pvName, test.timeout)
|
||||
if test.expectedErr != "" {
|
||||
assert.EqualError(t, err, test.expectedErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRebindPV(t *testing.T) {
|
||||
sourcePV := &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "source-pv",
|
||||
Labels: map[string]string{
|
||||
"key1": "val1",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"anno1": "val1",
|
||||
KubeAnnBoundByController: "true",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeSpec{
|
||||
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
Driver: "fake-driver",
|
||||
VolumeHandle: "fake-handle",
|
||||
},
|
||||
},
|
||||
AccessModes: []corev1api.PersistentVolumeAccessMode{corev1api.ReadWriteOnce},
|
||||
PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimRetain,
|
||||
StorageClassName: "fake-sc",
|
||||
},
|
||||
}
|
||||
|
||||
targetPVC := &corev1api.PersistentVolumeClaim{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Namespace: "fake-ns",
|
||||
Name: "target-pvc",
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeClaimSpec{
|
||||
Selector: &metav1.LabelSelector{
|
||||
MatchLabels: map[string]string{
|
||||
"key1": "val3",
|
||||
"key2": "val2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
pvName string
|
||||
sourcePV *corev1api.PersistentVolume
|
||||
targetPVC *corev1api.PersistentVolumeClaim
|
||||
kubeClientObj []runtime.Object
|
||||
kubeReactors []reactor
|
||||
expectedErr string
|
||||
expected *corev1api.PersistentVolume
|
||||
}{
|
||||
{
|
||||
name: "source is nil",
|
||||
expectedErr: "source PV is required to rebind PV",
|
||||
},
|
||||
{
|
||||
name: "target pvc is nil",
|
||||
sourcePV: sourcePV,
|
||||
expectedErr: "target PVC is required to rebind PV",
|
||||
},
|
||||
{
|
||||
name: "create error",
|
||||
pvName: "rebind-pv",
|
||||
sourcePV: sourcePV,
|
||||
targetPVC: targetPVC,
|
||||
kubeReactors: []reactor{
|
||||
{
|
||||
verb: "create",
|
||||
resource: "persistentvolumes",
|
||||
reactorFunc: func(action clientTesting.Action) (handled bool, ret runtime.Object, err error) {
|
||||
return true, nil, errors.New("create error")
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "create error",
|
||||
},
|
||||
{
|
||||
name: "success",
|
||||
pvName: "rebind-pv",
|
||||
sourcePV: sourcePV,
|
||||
targetPVC: targetPVC,
|
||||
expected: &corev1api.PersistentVolume{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "rebind-pv",
|
||||
Labels: map[string]string{
|
||||
"key1": "val3",
|
||||
"key2": "val2",
|
||||
},
|
||||
Annotations: map[string]string{
|
||||
"anno1": "val1",
|
||||
},
|
||||
},
|
||||
Spec: corev1api.PersistentVolumeSpec{
|
||||
Capacity: sourcePV.Spec.Capacity,
|
||||
PersistentVolumeSource: corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
Driver: "fake-driver",
|
||||
VolumeHandle: "fake-handle",
|
||||
FSType: "ext4",
|
||||
},
|
||||
},
|
||||
AccessModes: sourcePV.Spec.AccessModes,
|
||||
PersistentVolumeReclaimPolicy: corev1api.PersistentVolumeReclaimDelete,
|
||||
StorageClassName: sourcePV.Spec.StorageClassName,
|
||||
VolumeMode: targetPVC.Spec.VolumeMode,
|
||||
NodeAffinity: sourcePV.Spec.NodeAffinity,
|
||||
ClaimRef: &corev1api.ObjectReference{
|
||||
Kind: targetPVC.Kind,
|
||||
Namespace: "fake-ns",
|
||||
Name: "target-pvc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
fakeKubeClient := fake.NewSimpleClientset(test.kubeClientObj...)
|
||||
for _, reactor := range test.kubeReactors {
|
||||
fakeKubeClient.Fake.PrependReactor(reactor.verb, reactor.resource, reactor.reactorFunc)
|
||||
}
|
||||
|
||||
pv, err := RebindPV(t.Context(), fakeKubeClient.CoreV1(), test.pvName, test.sourcePV, test.targetPVC, corev1api.PersistentVolumeReclaimDelete, "ext4")
|
||||
|
||||
if test.expectedErr != "" {
|
||||
assert.EqualError(t, err, test.expectedErr)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, test.expected, pv)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestClonePVSource(t *testing.T) {
|
||||
fsTypeExt4 := "ext4"
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
source *corev1api.PersistentVolumeSource
|
||||
newFSType string
|
||||
expected corev1api.PersistentVolumeSource
|
||||
}{
|
||||
{
|
||||
name: "no new fsType",
|
||||
source: &corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
Driver: "fake-driver",
|
||||
},
|
||||
},
|
||||
newFSType: "",
|
||||
expected: corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
Driver: "fake-driver",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "csi source with new fsType",
|
||||
source: &corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
Driver: "fake-driver",
|
||||
FSType: "ext3",
|
||||
},
|
||||
},
|
||||
newFSType: "ext4",
|
||||
expected: corev1api.PersistentVolumeSource{
|
||||
CSI: &corev1api.CSIPersistentVolumeSource{
|
||||
Driver: "fake-driver",
|
||||
FSType: "ext4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "awsEBS source with new fsType",
|
||||
source: &corev1api.PersistentVolumeSource{
|
||||
AWSElasticBlockStore: &corev1api.AWSElasticBlockStoreVolumeSource{
|
||||
VolumeID: "fake-id",
|
||||
},
|
||||
},
|
||||
newFSType: "ext4",
|
||||
expected: corev1api.PersistentVolumeSource{
|
||||
AWSElasticBlockStore: &corev1api.AWSElasticBlockStoreVolumeSource{
|
||||
VolumeID: "fake-id",
|
||||
FSType: "ext4",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "azureDisk source with new fsType",
|
||||
source: &corev1api.PersistentVolumeSource{
|
||||
AzureDisk: &corev1api.AzureDiskVolumeSource{
|
||||
DiskName: "fake-disk",
|
||||
},
|
||||
},
|
||||
newFSType: "ext4",
|
||||
expected: corev1api.PersistentVolumeSource{
|
||||
AzureDisk: &corev1api.AzureDiskVolumeSource{
|
||||
DiskName: "fake-disk",
|
||||
FSType: &fsTypeExt4,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
actual := clonePVSource(test.source, test.newFSType)
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +287,11 @@ The policies YAML config file would look like this:
|
||||
# pvc matches specific phase(s)
|
||||
pvcPhase:
|
||||
- Pending
|
||||
# pvc matches specific volume mode
|
||||
pvcVolumeMode: Block
|
||||
# pvc matches specific access mode(s)
|
||||
pvcAccessModes:
|
||||
- ReadWriteOnce
|
||||
action:
|
||||
type: skip
|
||||
- conditions:
|
||||
@@ -380,6 +385,8 @@ Currently, Velero supports the volume attributes listed below:
|
||||
- storageClass: matching volumes those with specified `storageClass`, such as `gp2`, `ebs-sc` in eks
|
||||
- volume sources: matching volumes that used specified volume sources. Currently we support nfs or csi backend volume source
|
||||
- pvcPhase: matching volumes based on the phase of their associated PVCs (Pending, Bound, Lost)
|
||||
- pvcVolumeMode: matching volumes based on the volume mode of their associated PVCs (Filesystem, Block)
|
||||
- pvcAccessModes: matching volumes based on the access modes of their associated PVCs (ReadWriteOnce, ReadOnlyMany, ReadWriteMany, ReadWriteOncePod). All configured access modes must be present on the PVC.
|
||||
|
||||
Velero supported conditions and format listed below:
|
||||
- capacity
|
||||
@@ -521,6 +528,72 @@ Velero supported conditions and format listed below:
|
||||
type: skip
|
||||
```
|
||||
|
||||
- pvc VolumeMode
|
||||
|
||||
This condition filters PVC-backed volumes based on the volume mode of their associated PVCs. The condition is specified as a single volume mode to match. The volume matches this condition if the PVC's volume mode exactly matches the configured value. Matching is case-sensitive, so `block` does not match `Block`. Supported volume modes are: `Filesystem` and `Block`. If `pvcVolumeMode` is omitted from a policy, volume mode is not restricted. Non-PVC volumes, such as `emptyDir`, `configMap`, or inline volumes without an associated PVC, do not match policies that require this condition.
|
||||
```yaml
|
||||
pvcVolumeMode: Block
|
||||
```
|
||||
|
||||
Some examples:
|
||||
- Skip Block PVCs: Skip backup of volumes whose associated PVC uses `Block` volume mode.
|
||||
```yaml
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: Block
|
||||
action:
|
||||
type: skip
|
||||
```
|
||||
- Combine with other conditions: You can combine PVC volume mode conditions with other conditions like PVC phase, storage class, or labels.
|
||||
```yaml
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcVolumeMode: Block
|
||||
pvcPhase:
|
||||
- Bound
|
||||
action:
|
||||
type: snapshot
|
||||
```
|
||||
|
||||
- pvc AccessModes
|
||||
|
||||
This condition filters PVC-backed volumes based on the access modes of their associated PVCs. The condition is specified as a list of access modes to match. The volume matches this condition only if the PVC has all of the access modes in the list. Matching is case-sensitive, so `readwriteonce` does not match `ReadWriteOnce`. Supported access modes are: `ReadWriteOnce`, `ReadOnlyMany`, `ReadWriteMany`, and `ReadWriteOncePod`. Non-PVC volumes, such as `emptyDir`, `configMap`, or inline volumes without an associated PVC, do not match policies that require this condition.
|
||||
```yaml
|
||||
pvcAccessModes:
|
||||
- ReadWriteOnce
|
||||
```
|
||||
|
||||
Some examples:
|
||||
- Skip ReadWriteOnce PVCs: Skip backup of volumes whose associated PVC includes the `ReadWriteOnce` access mode.
|
||||
```yaml
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes:
|
||||
- ReadWriteOnce
|
||||
action:
|
||||
type: skip
|
||||
```
|
||||
- Match multiple access modes: Apply an action to volumes whose associated PVC includes both `ReadOnlyMany` and `ReadWriteMany`.
|
||||
```yaml
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes:
|
||||
- ReadOnlyMany
|
||||
- ReadWriteMany
|
||||
action:
|
||||
type: snapshot
|
||||
```
|
||||
- Combine with other conditions: You can combine PVC access mode conditions with other conditions like PVC volume mode, PVC phase, storage class, or labels.
|
||||
```yaml
|
||||
volumePolicies:
|
||||
- conditions:
|
||||
pvcAccessModes:
|
||||
- ReadWriteOnce
|
||||
pvcVolumeMode: Block
|
||||
action:
|
||||
type: snapshot
|
||||
```
|
||||
|
||||
|
||||
|
||||
### Resource policies rules
|
||||
|
||||
Reference in New Issue
Block a user