diff --git a/.github/workflows/auto_assign_prs.yml b/.github/workflows/auto_assign_prs.yml index d827b697ad..9b915533c4 100644 --- a/.github/workflows/auto_assign_prs.yml +++ b/.github/workflows/auto_assign_prs.yml @@ -7,6 +7,10 @@ on: pull_request_target: types: [opened, reopened, ready_for_review] +permissions: + contents: read + pull-requests: write + jobs: # Automatically assigns reviewers and owner add-reviews: @@ -16,4 +20,3 @@ jobs: uses: kentaro-m/auto-assign-action@v2.0.0 with: configuration-path: ".github/auto-assignees.yml" - repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/auto_label_prs.yml b/.github/workflows/auto_label_prs.yml index a5d912af8d..042cc7e95d 100644 --- a/.github/workflows/auto_label_prs.yml +++ b/.github/workflows/auto_label_prs.yml @@ -8,6 +8,10 @@ on: pull_request_target: types: [opened, reopened, synchronize, ready_for_review] +permissions: + contents: read + pull-requests: write + jobs: # Automatically labels PRs based on file globs in the change. triage: @@ -15,5 +19,4 @@ jobs: steps: - uses: actions/labeler@v5 with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" configuration-path: .github/labeler.yml diff --git a/.github/workflows/auto_request_review.yml b/.github/workflows/auto_request_review.yml index 83e7032ab2..47844bc6c1 100644 --- a/.github/workflows/auto_request_review.yml +++ b/.github/workflows/auto_request_review.yml @@ -5,6 +5,10 @@ on: pull_request_target: types: [opened, ready_for_review, reopened] +permissions: + contents: read + pull-requests: write + jobs: auto-request-review: name: Auto Request Review @@ -13,5 +17,5 @@ jobs: - name: Request a PR review based on files types/paths, and/or groups the author belongs to uses: necojackarc/auto-request-review@v0.13.0 with: - token: ${{ secrets.GITHUB_TOKEN }} config: .github/auto-assignees.yml + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/e2e-test-kind.yaml b/.github/workflows/e2e-test-kind.yaml index 033e93370d..37bf720926 100644 --- a/.github/workflows/e2e-test-kind.yaml +++ b/.github/workflows/e2e-test-kind.yaml @@ -71,7 +71,7 @@ jobs: run: | echo "Building MinIO image from Bitnami Dockerfile..." git clone --depth 1 https://github.com/bitnami/containers.git /tmp/bitnami-containers - cd /tmp/bitnami-containers/bitnami/minio/2025/debian-12 + cd /tmp/bitnami-containers/bitnami/minio/2026/debian-12 docker build -t bitnami/minio:local . docker save bitnami/minio:local > ${{ github.workspace }}/minio-image.tar # Create json of k8s versions to test diff --git a/.github/workflows/pr-containers.yml b/.github/workflows/pr-containers.yml index 6f839a1661..c2fea13863 100644 --- a/.github/workflows/pr-containers.yml +++ b/.github/workflows/pr-containers.yml @@ -7,6 +7,7 @@ on: - 'release-**' paths: - 'Dockerfile' + - 'Dockerfile-Windows' jobs: build: @@ -32,6 +33,6 @@ jobs: # by push, so BRANCH and TAG are empty by default. docker-push.sh will # only build Velero image without pushing. - name: Make Velero container without pushing to registry. - if: github.repository == 'vmware-tanzu/velero' + if: github.repository == 'velero-io/velero' run: | ./hack/docker-push.sh \ No newline at end of file diff --git a/.github/workflows/pr-filepath-check.yml b/.github/workflows/pr-filepath-check.yml new file mode 100644 index 0000000000..260a09dc4a --- /dev/null +++ b/.github/workflows/pr-filepath-check.yml @@ -0,0 +1,93 @@ +name: Pull Request File Path Check +on: [pull_request] +jobs: + + filepath-check: + name: Check for invalid characters in file paths + runs-on: ubuntu-latest + steps: + + - name: Check out the code + uses: actions/checkout@v6 + + - name: Validate file paths for Go module compatibility + run: | + # Go's module zip rejects filenames containing certain characters. + # See golang.org/x/mod/module fileNameOK() for the full specification. + # + # Allowed ASCII: letters, digits, and: !#$%&()+,-.=@[]^_{}~ and space + # Allowed non-ASCII: unicode letters only + # Rejected: " ' * < > ? ` | / \ : and any non-letter unicode (control + # chars, format chars like U+200E LEFT-TO-RIGHT MARK, etc.) + # + # This check catches issues like the U+200E incident in PR #9552. + + EXIT_STATUS=0 + + git ls-files -z | python3 -c " + import sys, unicodedata + + data = sys.stdin.buffer.read() + files = data.split(b'\x00') + + # Characters explicitly rejected by Go's fileNameOK + # (path separators / and \ are inherent to paths so we check per-element) + bad_ascii = set('\"' + \"'\" + '*<>?\`|:') + + allowed_ascii = set('!#$%&()+,-.=@[]^_{}~ ') + + def is_ok(ch): + if ch.isascii(): + return ch.isalnum() or ch in allowed_ascii + return ch.isalpha() + + bad_files = [] # list of (original_path, clean_path, char_desc) + for f in files: + if not f: + continue + try: + name = f.decode('utf-8') + except UnicodeDecodeError: + print(f'::error::Non-UTF-8 bytes in filename: {f!r}') + bad_files.append((repr(f), None, 'non-UTF-8 bytes')) + continue + + # Check each path element (split on /) + for element in name.split('/'): + for ch in element: + if not is_ok(ch): + cp = ord(ch) + char_name = unicodedata.name(ch, f'U+{cp:04X}') + char_desc = f'U+{cp:04X} ({char_name})' + # Build cleaned path by stripping invalid chars + clean = '/'.join( + ''.join(c for c in elem if is_ok(c)) + for elem in name.split('/') + ) + print(f'::error file={name}::File \"{name}\" contains invalid char {char_desc}') + bad_files.append((name, clean, char_desc)) + break + + if bad_files: + print() + print('The following files have characters that are invalid in Go module zip archives:') + print() + for original, clean, desc in bad_files: + print(f' {original} — {desc}') + print() + print('To fix, rename the files to remove the problematic characters:') + print() + for original, clean, desc in bad_files: + if clean: + print(f' mv \"{original}\" \"{clean}\" && git add \"{clean}\"') + print(f' # or: git mv \"{original}\" \"{clean}\"') + else: + print(f' # {original} — cannot auto-suggest rename (non-UTF-8)') + print() + print('See https://github.com/velero-io/velero/pull/9552 for context.') + sys.exit(1) + else: + print('All file paths are valid for Go module zip.') + " || EXIT_STATUS=1 + + exit $EXIT_STATUS diff --git a/.github/workflows/pr-goreleaser.yml b/.github/workflows/pr-goreleaser.yml index 5215a5bfbc..802080cb57 100644 --- a/.github/workflows/pr-goreleaser.yml +++ b/.github/workflows/pr-goreleaser.yml @@ -18,7 +18,7 @@ jobs: name: Checkout - name: Verify .goreleaser.yml and try a dryrun release. - if: github.repository == 'vmware-tanzu/velero' + if: github.repository == 'velero-io/velero' run: | CHANGELOG=$(ls changelogs | sort -V -r | head -n 1) GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ diff --git a/.github/workflows/push-builder.yml b/.github/workflows/push-builder.yml index 2272ee3f1d..8e3e59c15c 100644 --- a/.github/workflows/push-builder.yml +++ b/.github/workflows/push-builder.yml @@ -28,7 +28,7 @@ jobs: # Only try to publish the container image from the root repo; forks don't have permission to do so and will always get failures. - name: Publish container image - if: github.repository == 'vmware-tanzu/velero' + if: github.repository == 'velero-io/velero' run: | docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index e0c32e1890..10b191630c 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -52,7 +52,7 @@ jobs: verbose: true # Only try to publish the container image from the root repo; forks don't have permission to do so and will always get failures. - name: Publish container image - if: github.repository == 'vmware-tanzu/velero' + if: github.repository == 'velero-io/velero' run: | sudo swapoff -a sudo rm -f /mnt/swapfile diff --git a/.goreleaser.yml b/.goreleaser.yml index 0ff87e36a9..f206d3f9c3 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -55,7 +55,7 @@ checksum: name_template: 'CHECKSUM' release: github: - owner: vmware-tanzu + owner: velero-io name: velero draft: true prerelease: auto diff --git a/Dockerfile b/Dockerfile index 6ce46ca3b4..9b1e04a201 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,7 +13,7 @@ # limitations under the License. # Velero binary build section -FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS velero-builder +FROM --platform=$BUILDPLATFORM golang:1.25-trixie AS velero-builder ARG GOPROXY ARG BIN @@ -48,30 +48,6 @@ RUN mkdir -p /output/usr/bin && \ -ldflags "${LDFLAGS}" ${PKG}/cmd/velero-helper && \ go clean -modcache -cache -# Restic binary build section -FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS restic-builder - -ARG GOPROXY -ARG BIN -ARG TARGETOS -ARG TARGETARCH -ARG TARGETVARIANT -ARG RESTIC_VERSION - -ENV CGO_ENABLED=0 \ - GO111MODULE=on \ - GOPROXY=${GOPROXY} \ - GOOS=${TARGETOS} \ - GOARCH=${TARGETARCH} \ - GOARM=${TARGETVARIANT} - -COPY . /go/src/github.com/vmware-tanzu/velero - -RUN mkdir -p /output/usr/bin && \ - export GOARM=$(echo "${GOARM}" | cut -c2-) && \ - /go/src/github.com/vmware-tanzu/velero/hack/build-restic.sh && \ - go clean -modcache -cache - # Velero image packing section FROM paketobuildpacks/run-jammy-tiny:latest @@ -79,7 +55,4 @@ LABEL maintainer="Xun Jiang " COPY --from=velero-builder /output / -COPY --from=restic-builder /output / - USER cnb:cnb - diff --git a/Dockerfile-Windows b/Dockerfile-Windows index ac22531dc1..757da8f80a 100644 --- a/Dockerfile-Windows +++ b/Dockerfile-Windows @@ -15,7 +15,7 @@ ARG OS_VERSION=1809 # Velero binary build section -FROM --platform=$BUILDPLATFORM golang:1.25-bookworm AS velero-builder +FROM --platform=$BUILDPLATFORM golang:1.25-trixie AS velero-builder ARG GOPROXY ARG BIN diff --git a/Makefile b/Makefile index 82129623d4..80db72e3a2 100644 --- a/Makefile +++ b/Makefile @@ -105,8 +105,6 @@ see: https://velero.io/docs/main/build-from-source/#making-images-and-updating-v endef # comma cannot be escaped and can only be used in Make function arguments by putting into variable comma=, -# The version of restic binary to be downloaded -RESTIC_VERSION ?= 0.15.0 CLI_PLATFORMS ?= linux-amd64 linux-arm linux-arm64 darwin-amd64 darwin-arm64 windows-amd64 linux-ppc64le linux-s390x BUILD_OUTPUT_TYPE ?= docker @@ -213,6 +211,7 @@ shell: build-dirs build-env -v "$$(pwd)/.go/std/$(GOOS)/$(GOARCH):/usr/local/go/pkg/$(GOOS)_$(GOARCH)_static:delegated" \ -v "$$(pwd)/.go/go-build:/.cache/go-build:delegated" \ -v "$$(pwd)/.go/golangci-lint:/.cache/golangci-lint:delegated" \ + -v "$$(pwd)/.go/goimports:/.cache/goimports:delegated" \ -w /github.com/vmware-tanzu/velero \ $(BUILDER_IMAGE) \ /bin/sh $(CMD) @@ -260,7 +259,6 @@ container-linux: --build-arg=GIT_SHA=$(GIT_SHA) \ --build-arg=GIT_TREE_STATE=$(GIT_TREE_STATE) \ --build-arg=REGISTRY=$(REGISTRY) \ - --build-arg=RESTIC_VERSION=$(RESTIC_VERSION) \ --provenance=false \ --sbom=false \ -f $(VELERO_DOCKERFILE) . @@ -345,7 +343,7 @@ update-crd: build-dirs: @mkdir -p _output/bin/$(GOOS)/$(GOARCH) - @mkdir -p .go/src/$(PKG) .go/pkg .go/bin .go/std/$(GOOS)/$(GOARCH) .go/go-build .go/golangci-lint + @mkdir -p .go/src/$(PKG) .go/pkg .go/bin .go/std/$(GOOS)/$(GOARCH) .go/go-build .go/golangci-lint .go/goimports build-env: @# if we have overridden the value for the build-image Dockerfile, diff --git a/Tiltfile b/Tiltfile index 7f2029f6d2..8bfa2ac2f6 100644 --- a/Tiltfile +++ b/Tiltfile @@ -103,11 +103,6 @@ local_resource( deps = ["internal", "pkg/cmd"], ) -local_resource( - "restic_binary", - cmd = 'cd ' + '.' + ';mkdir -p _tiltbuild/restic; BIN=velero GOOS=linux GOARCH=amd64 GOARM="" RESTIC_VERSION=0.13.1 OUTPUT_DIR=_tiltbuild/restic ./hack/build-restic.sh', -) - # Note: we need a distro with a bash shell to exec into the Velero container tilt_dockerfile_header = """ FROM ubuntu:22.04 as tilt @@ -118,7 +113,6 @@ WORKDIR / COPY --from=tilt-helper /start.sh . COPY --from=tilt-helper /restart.sh . COPY velero . -COPY restic/restic /usr/bin/restic """ dockerfile_contents = "\n".join([ diff --git a/changelogs/unreleased/9528-Lyndon-Li b/changelogs/unreleased/9528-Lyndon-Li new file mode 100644 index 0000000000..52bb4a8f33 --- /dev/null +++ b/changelogs/unreleased/9528-Lyndon-Li @@ -0,0 +1 @@ +Add block data mover design for block level incremental backup by integrating with Kubernetes CBT \ No newline at end of file diff --git a/changelogs/unreleased/9558-Joeavaikath b/changelogs/unreleased/9558-Joeavaikath new file mode 100644 index 0000000000..c9be4839e9 --- /dev/null +++ b/changelogs/unreleased/9558-Joeavaikath @@ -0,0 +1 @@ +Wildcard namespaces: Log warning on empty resolution diff --git a/changelogs/unreleased/9613-pjjw b/changelogs/unreleased/9613-pjjw new file mode 100644 index 0000000000..3f8c6faaee --- /dev/null +++ b/changelogs/unreleased/9613-pjjw @@ -0,0 +1 @@ +update go-hclog to current version diff --git a/changelogs/unreleased/9654-pierluigilenoci b/changelogs/unreleased/9654-pierluigilenoci new file mode 100644 index 0000000000..d31a764272 --- /dev/null +++ b/changelogs/unreleased/9654-pierluigilenoci @@ -0,0 +1 @@ +Fix issue #9658, Honor --stderrthreshold when --logtostderr is enabled diff --git a/changelogs/unreleased/9682-adam-jian-zhang b/changelogs/unreleased/9682-adam-jian-zhang new file mode 100644 index 0000000000..0cb724f5b9 --- /dev/null +++ b/changelogs/unreleased/9682-adam-jian-zhang @@ -0,0 +1 @@ +Fix issue #9681, fix restores and podvolumerestores list options to only list in installed namespace diff --git a/changelogs/unreleased/9683-Lyndon-Li b/changelogs/unreleased/9683-Lyndon-Li new file mode 100644 index 0000000000..25b247bc19 --- /dev/null +++ b/changelogs/unreleased/9683-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #9428, increase repo maintenance history queue length from 3 to 25 \ No newline at end of file diff --git a/changelogs/unreleased/9684-Joeavaikath b/changelogs/unreleased/9684-Joeavaikath new file mode 100644 index 0000000000..d5f5d6e762 --- /dev/null +++ b/changelogs/unreleased/9684-Joeavaikath @@ -0,0 +1 @@ +Fix wildcard expansion when includes is empty and excludes has wildcards diff --git a/changelogs/unreleased/9693-priyansh17 b/changelogs/unreleased/9693-priyansh17 new file mode 100644 index 0000000000..0023a3c18b --- /dev/null +++ b/changelogs/unreleased/9693-priyansh17 @@ -0,0 +1 @@ +Enhance backup deletion logic to handle tarball download failures \ No newline at end of file diff --git a/changelogs/unreleased/9695-shubham-pampattiwar b/changelogs/unreleased/9695-shubham-pampattiwar new file mode 100644 index 0000000000..53deadee32 --- /dev/null +++ b/changelogs/unreleased/9695-shubham-pampattiwar @@ -0,0 +1 @@ +Bump external-snapshotter to v8.4.0 and migrate VolumeGroupSnapshot API from v1beta1 to v1beta2 for Kubernetes 1.34+ compatibility diff --git a/changelogs/unreleased/9700-priyansh17 b/changelogs/unreleased/9700-priyansh17 new file mode 100644 index 0000000000..b3cb5af680 --- /dev/null +++ b/changelogs/unreleased/9700-priyansh17 @@ -0,0 +1 @@ +Fix issue #9699, add a 2-second gap between temporary CSI VolumeSnapshotContent create and delete operations \ No newline at end of file diff --git a/changelogs/unreleased/9701-emirot b/changelogs/unreleased/9701-emirot new file mode 100644 index 0000000000..585c1c42a4 --- /dev/null +++ b/changelogs/unreleased/9701-emirot @@ -0,0 +1 @@ +Update Debian base image from bookworm to trixie \ No newline at end of file diff --git a/changelogs/unreleased/9704-adam-jian-zhang b/changelogs/unreleased/9704-adam-jian-zhang new file mode 100644 index 0000000000..59ae81e9a4 --- /dev/null +++ b/changelogs/unreleased/9704-adam-jian-zhang @@ -0,0 +1 @@ +Fix issue #9703, fix CSI PVC Backup Plugin list options to only list in installed namespace diff --git a/changelogs/unreleased/9705-emirot b/changelogs/unreleased/9705-emirot new file mode 100644 index 0000000000..3bd8117bcf --- /dev/null +++ b/changelogs/unreleased/9705-emirot @@ -0,0 +1 @@ +perf: better string concatenation diff --git a/changelogs/unreleased/9716-Lyndon-Li b/changelogs/unreleased/9716-Lyndon-Li new file mode 100644 index 0000000000..5e51e7aaf8 --- /dev/null +++ b/changelogs/unreleased/9716-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #9709, add interfaces for CBT service and CBT bitmap \ No newline at end of file diff --git a/changelogs/unreleased/9724-Lyndon-Li b/changelogs/unreleased/9724-Lyndon-Li new file mode 100644 index 0000000000..5eea82187e --- /dev/null +++ b/changelogs/unreleased/9724-Lyndon-Li @@ -0,0 +1 @@ +Fix issue #9723, extend Unified Repo Interface to support block uploader \ No newline at end of file diff --git a/changelogs/unreleased/9728-blackpiglet b/changelogs/unreleased/9728-blackpiglet new file mode 100644 index 0000000000..a0a9c897f4 --- /dev/null +++ b/changelogs/unreleased/9728-blackpiglet @@ -0,0 +1 @@ +Remove Restic build from Dockerfile, Makefile and Tiltfile. \ No newline at end of file diff --git a/changelogs/unreleased/9732-blackpiglet b/changelogs/unreleased/9732-blackpiglet new file mode 100644 index 0000000000..d110fa2d6f --- /dev/null +++ b/changelogs/unreleased/9732-blackpiglet @@ -0,0 +1 @@ +Remove Restic code path from PodVolumeRestore. \ No newline at end of file diff --git a/changelogs/unreleased/9740-emirot b/changelogs/unreleased/9740-emirot new file mode 100644 index 0000000000..dee2611c1d --- /dev/null +++ b/changelogs/unreleased/9740-emirot @@ -0,0 +1 @@ +fix: lint permission issue diff --git a/cmd/velero-restore-helper/velero-restore-helper.go b/cmd/velero-restore-helper/velero-restore-helper.go index d310f807e9..40a9421371 100644 --- a/cmd/velero-restore-helper/velero-restore-helper.go +++ b/cmd/velero-restore-helper/velero-restore-helper.go @@ -35,7 +35,7 @@ func main() { for { <-ticker.C if done() { - fmt.Println("All restic restores are done") + fmt.Println("All PodVolumeRestores are done") err := removeFolder() if err != nil { fmt.Println(err) diff --git a/design/block-data-mover/backup-architecture.png b/design/block-data-mover/backup-architecture.png new file mode 100644 index 0000000000..a5a50fe82a Binary files /dev/null and b/design/block-data-mover/backup-architecture.png differ diff --git a/design/block-data-mover/block-data-mover.md b/design/block-data-mover/block-data-mover.md new file mode 100644 index 0000000000..bc386a3a2b --- /dev/null +++ b/design/block-data-mover/block-data-mover.md @@ -0,0 +1,551 @@ +# Block Data Mover Design + +## Glossary & Abbreviation + +**Backup Storage**: The storage to store the backup data. Check [Unified Repository design][1] for details. +**Backup Repository**: Backup repository is layered between BR data movers and Backup Storage to provide BR related features that is introduced in [Unified Repository design][1]. +**Velero Generic Data Path (VGDP)**: VGDP is the collective of modules that is introduced in [Unified Repository design][1]. Velero uses these modules to finish data transfer for various purposes (i.e., PodVolume backup/restore, Volume Snapshot Data Movement). VGDP modules include uploaders and the backup repository. +**Velero Built-in Data Mover (VBDM)**: VBDM, which is introduced in [Volume Snapshot Data Movement design][2] and [Unified Repository design][1], is the built-in data mover shipped along with Velero, it includes Velero data mover controllers and VGDP. +**Data Mover Pods**: Intermediate pods which hold VGDP and complete the data transfer. See [VGDP Micro Service for Volume Snapshot Data Movement][3] for details. +**Change Block Tracking (CBT)**: CBT is the mechanism to track changed blocks, so that backups could back up the changed data only. CBT usually provides by the computing/storage platform. +**TCO**: Total Cost of Ownership. This is a general criteria for products/solutions, but also means a lot for BR solutions. For example, this means what kind of backup storage (and its cost) it requires, the retention policy of backup copies, the ways to remove backup data redundancy, etc. +**PodVolume Backup**: This is the Velero backup method which accesses the data from live file system, see [Kopia Integration design][1] for how it works. +**CAOS and CABS**: Content-Addressable Object Storage and Content-Addressable Block Storage, they are the parts from Kopia repository, see [Kopia Architecture][5]. + +## Background +Kubernetes supports two kinds of volume mode, `FileSystem` and `Block`, for persistent volumes. Underlyingly, the storage could use a block storage to provision either `FileSystem` mode or `Block` mode volumes; and the storage could use a file storage to provision `FileSystem` mode volumes. +For volumes provisioned by block storage, they could be backed up/restored from the block level, regardless the volume mode of the persistent volume. +On the other hand, as long as the data could be accessed from the file system, a backup/restore could be conducted from the file system level. That is to say `FileSystem` mode volumes could be backed up/restored from the file system level, regardless of the backend storage type. +Then if a `FileSystem` mode volume is provisioned by a block storage, the volume could be backed up/restored either from the file system level or block level. + +For Velero, [CSI Snapshot Data Movement][2] which is implemented by VBDM, ships a file system uploader, so the backup/restore is done from file system only. + +Once possible, block level backup/restore is better than file system level backup/restore: +- Block level backup could leverage CBT to process minimal size of data, so it significantly reduces the overhead to network, backup repository and backup storage. As a result, TCO is significantly reduced. +- Block level backup/restore is performant in throughput and resource consumption, because it doesn't need to handle the complexity of the file system, especially for the case that huge number of small files in the file system. +- Block level backup/restore is less OS dependent because the uploader doesn't need the OS to be aware of the file system in the volume. + +At present, [Kubernetes CBT API][4] is mature and close to Beta stage. Many platform/storage has supported/is going to support it. + +Therefore, it is very important for Velero to deliver the block level backup/restore and recommend users to use it over the file system data mover as long as: +- The volume is backed by block storage so block level access is possible +- The platform supports CBT + +Meanwhile, file system level backup/restore is still valuable for below scenarios: +- The volume is backed by file storage, e.g., AWS EFS, Azure File, CephFS, VKS File Volume, etc. +- The volume is backed by block storage but CBT is not available +- The volume doesn't support CSI snapshot, so Velero PodVolume Backup method is used + +There are rich features delivered with VGDP, VBDM and [VGDP micro service][3], to reuse these features, block data mover should be built based on these modules. + +Velero VBDM supports linux and Windows nodes, however, Windows container doesn't support block mode volumes, so backing up/restoring from Windows nodes is not supported until Windows container removes this limitation. As a result, if there are both linux and Windows nodes in the cluster, block data mover can only run in linux nodes. + +Both the Kubernetes CBT service and Velero work in the boundary of the cluster, even though the backend storage may be shared by multiple clusters, Velero can only protection workloads in the same cluster where it is running. + +## Goals + +Add a block data mover to VBDM and support block level backup/restore for [CSI Snapshot Data Movement][2], which includes: +- Support block level full backup for both `FileSystem` and `Block` mode volumes +- Support block level incremental backup for both `FileSystem` and `Block` mode volumes +- Support block level restore from full/incremental backup for both `FileSystem` and `Block` mode volumes +- Support block level backup/restore for both linux and Windows workloads from linux cluster nodes +- Support all existing features, i.e., load concurrency, node selection, cache volume, deduplication, compression, encryption, etc. for the block data mover +- Support volumes processed from file system level and block level in the same backup/restore + +## Non-Goals + +- PodVolume Backup does the backup/restore from file system level only, so block level backup/restore is not supported +- Volumes that are backed by file system storages, can only be backed up/restored from file system level, so block level backup/restore is not supported +- Backing up/restoring from Windows nodes is not supported +- Block level incremental backup requires special capabilities of the backup repository, and Velero [Unified Repository][1] supports multiple kinds of backup repositories. The current design focus on Kopia repository only, block level incremental backup support of other repositories will be considered when the specific backup repository is integrated to [Velero Unified Repository][1] + +## Architecture + +### Data Path + +Below shows the architecture of VGDP when integrating to Unified Repository (implemented by Kopia repository). +A new block data mover will be added besides the existing file system data mover, the both data movers read/write data from/to the same backup repository through Unified Repo interface. +Unified Repo interface and the backup repository needs to be enhanced to support incremental backups. + +![Data path overview](data-path-overview.png) + +For more details of VGDP architecture, see [Unified Repository design][1], [Volume Snapshot Data Movement design][2] and [VGDP Micro Service for Volume Snapshot Data Movement][3]. + +### Backup + +Below is the architecture for block data mover backup which is developed based on the existing VBDM: + +![Backup architecture](backup-architecture.png) + +The existing VBDM is reused, below are the major changes based on the existing VBDM: +**Exposer**: Exposer needs to create block mode backupPVC all the time regardless of the sourcePVC mode. +**CBT**: This is a new layer to retrieve, transform and store the changed blocks, it interacts with CSI SnapshotMetadataService through gRPC. +**Uploader**: A new block uploader is added. It interacts with CBT layer, holds special logics to make performant data read from block devices and holds special logics to write incremental data to Unified Repository. +**Extended Kopia repo**: A new Incremental Aware Object Extension is added to Kopia's CAOS, so as to support incremental data write. Other parts of Kopia repository, including the existing CAOS and CABS, are not changed. + +### Restore + +Below is architecture for block data mover restore which is developed based on the existing VBDM: + +![Restore architecture](restore-architecture.png) + +The existing VBDM is reused, below are the major changes based on the existing VBDM: +**Exposer**: While the restorePV is in block mode, exposer needs to rebind the restorePV to a targetPVC in either file system mode or block mode. +**Uploader**: The same block uploader holds special logics to make performant data write to block devices and holds special logics to read data from the backup chain in Unified repository. + +For more details of VBDM, see [Volume Snapshot Data Movement design][2]. + +## Detailed Design + +### Selectable Data Mover Type + +#### Per Backup Selection +At present, the backup accepts a `DataMover` parameter and when its value is empty or `velero`, VBDM is used. +After block data mover is introduced, VBDM will have two types of data movers, Velero file system data mover and Velero block data mover. +A new type string `velero-block` is introduced for Velero block data mover, that is, when `DataMover` is set as `velero-block`, Velero block data mover is used. +Another new value `velero-fs` is introduced for Velero file system data mover, that is, when `DataMover` is set as `velero-fs`, Velero file system data mover is used. +For backwards compatibility consideration, `velero` is preserved a valid value, it refers to the default data mover, and the default data mover may change among releases. At present, Velero file system data mover is the default data mover; we can change the default one to Velero block data mover in future releases. + +#### Volume Policy +It is a valid case that users have multiple volumes in a single backup, while they want to use Velero file system data mover for some of the volumes and use Velero block data mover for some others. +To meet this requirement, a combined solution of Per Backup Selection and Volume Policy is used. + +Here are the data structs for VolumePolicy: +```go +type volPolicy struct { + action Action + conditions []volumeCondition +} + +type volumeCondition interface { + match(v *structuredVolume) bool + validate() error +} + +type structuredVolume struct { + capacity resource.Quantity + storageClass string + nfs *nFSVolumeSource + csi *csiVolumeSource + volumeType SupportedVolume + pvcLabels map[string]string + pvcPhase string +} + +type Action struct { + Type VolumeActionType `yaml:"type"` + Parameters map[string]any `yaml:"parameters,omitempty"` +} + +const ( + ConfigmapRefType string = "configmap" + Skip VolumeActionType = "skip" + FSBackup VolumeActionType = "fs-backup" + Snapshot VolumeActionType = "snapshot" +) +``` + +`action.parameters` is used to provide extra information of the action. This is an ideal place to differentiate Velero file system data mover and Velero block data mover. +Therefore, Velero built-in data mover will support `dataMover` key in `parameters`, with the value either `velero-fs` or `velero-block`. While `velero-fs` and `velero-block` are with the same meaning with Per Backup Selection. + +As an example, here is how a user might use both `velero-block` and `velero-fs` in a single backup: +- Users set `DataMover` parameter for the backup as `velero-block` +- Users add a record into Volume Policy, make `conditions` to filter the volumes they want to backup through Velero file system data mover, make `action.type` as `snapshot` and insert a record into `action.parameter` as `dataMover:velero-fs` + +In this way, all volumes matched by `conditions` will be backed up with Velero file system data mover; while the others will fallback to the per backup method Velero block data mover. + +Vice versa, users could set the per backup method as file system data mover and select volumes for Velero block data mover. + +The selected data mover for each volume should be recorded to `volumeInfo.json`. + +### Controllers +Backup controller and Restore controller are kept as is, async operations are still used to interact with VBDM with block data mover. +DataUpload controller and DataDownload controller are almost kept as is, with some minor changes to handle the data mover type and backup type appropriately and convey it to the exposers. With [VGDP Micro Service][3], the controllers are almost isolated from VGDP, so no major changes are required. + +### Exposer + +#### CSI Snapshot Exposer +The existing CSI Snapshot Exposer is reused with some changes to decide the backupPVC volume mode by access mode. Specifically, for Velero block data mover, access mode is always `Block`, so the backupPVC volume mode is always `Block`. +Once the backupPVC is created with correct volume mode, the existing code could create the backupPod and mount the backupPVC appropriately. + +#### Generic Restore Exposer +The existing Generic Restore Exposer is reused, but the workflow needs some changes. +For block data mover, the restorePV is in Block mode all the time, whereas, the targetPVC may be in either file system mode or block mode. +However, Kubernetes doesn't allow to bound a PV to a PVC with mismatch volume mode. + +Therefore, the workflow of ***Finish Volume Readiness*** as introduced in [Volume Snapshot Data Movement design][2] is changed as below: +- When restore completes and restorePV is created, set restorePV's `deletionPolicy` to `Retain` +- Create another rebindPV and copy restorePV's `volumeHandle` but the `volumeMode` matches to the targetPVC +- Delete restorePV +- Set the rebindPV's claim reference (the ```claimRef``` filed) to targetPVC +- Add the ```velero.io/dynamic-pv-restore``` label to the rebindPV + +In this way, the targetPVC will be bound immediately by Kubernetes to rebindPV. + +These changes work for file system data mover as well, so the old workflow will be replaced, only the new workflow is kept. + +### VGDP + +Below is the VGDP workflow during backup: + +![VGDP Backup](vgdp-backup.png) + +Below is the VGDP workflow during restore: + +![VGDP Restore](vgdp-restore.png) + +#### Unified Repo +For block data mover, one Unified Repo Object is created for each volume, and some metadata is also saved into Unified Repo to describe the volume. +During the backup, the write conducts a skippable-write manner: +- For the data range that the write does not skip, object is written with the real data +- For the data range that is skipped, the data is either filled as ZERO or cloned from the parent object. Specifically, for a full backup, data is filled as ZERO; for an incremental backup, data is cloned from the parent object + +To support incremental backup, `ObjectWriter` interface needs to extend to support `io.WriterAt`, so that uploader could conduct a skippable-write manner: +```go +type ObjectWriter interface { + io.WriteCloser + io.WriterAt + + // Seeker is used in the cases that the object is not written sequentially + io.Seeker + + // Checkpoint is periodically called to preserve the state of data written to the repo so far. + // Checkpoint returns a unified identifier that represent the current state. + // An empty ID could be returned on success if the backup repository doesn't support this. + Checkpoint() (ID, error) + + // Result waits for the completion of the object write. + // Result returns the object's unified identifier after the write completes. + Result() (ID, error) +} +``` + +To clone data from parent object, the caller needs to specify the parent object. To support this, `ObjectWriteOptions` is extended with `ParentObject`. +The existing `AccessMode` could be used to indicate the data access type, either file system or block: + +```go +// ObjectWriteOptions defines the options when creating an object for write +type ObjectWriteOptions struct { + FullPath string // Full logical path of the object + DataType int // OBJECT_DATA_TYPE_* + Description string // A description of the object, could be empty + Prefix ID // A prefix of the name used to save the object + AccessMode int // OBJECT_DATA_ACCESS_* + BackupMode int // OBJECT_DATA_BACKUP_* + AsyncWrites int // Num of async writes for the object, 0 means no async write + ParentObject ID // the parent object based on which incremental write will be done +} +``` + +To support non-Kopia uploader to save snapshots to Unified Repo, snapshot related methods will be added to `BackupRepo` interface: +```go + // SaveSnapshot saves a repo snapshot + SaveSnapshot(ctx context.Context, snapshot Snapshot) (ID, error) + + // GetSnapshot returns a repo snapshot from snapshot ID + GetSnapshot(ctx context.Context, id ID) (Snapshot, error) + + // DeleteSnapshot deletes a repo snapshot + DeleteSnapshot(ctx context.Context, id ID) error + + // ListSnapshot lists all snapshots in repo for the given source (if specified) + ListSnapshot(ctx context.Context, source string) ([]Snapshot, error) +``` + +To support non-Kopia uploader to save metadata, which is used to describe the backed up objects, some metadata related methods will be added to `BackupRepo` interface: +```go + // WriteMetadata writes metadata to the repo, metadata is used to describe data, e.g., file system + // dirs are saved as metadata + WriteMetadata(ctx context.Context, meta *Metadata, opt ObjectWriteOptions) (ID, error) + + // ReadMetadata reads a metadata from repo by the metadata's object ID + ReadMetadata(ctx context.Context, id ID) (*Metadata, error) +``` + +kopia-lib for Unified Repo will implement these interfaces by calling the corresponding Kopia repository functions. + +### Kopia Repository +CAOS of Kopia repository implements Unified Repo's Objects. However, CAOS supports full and sequential write only. +To make it support skippable write, a new Incremental Aware Object Extension is created based on the existing CAOS. + +#### Block Address Table +Kopia CAOS uses Block Address Table (BAT) to track objects. It will be reused for both full backups and incremental backups. + +![Incremental Aware Object Extension](caos-extension.png) + +For Incremental Aware Object Extension, one object represents one volume. +For full backup, the skipped areas will be written as all ZERO by Incremental Aware Object Extension, since Kopia repository's interface doesn't support skippable write. But it is fine, the ZERO data will be deduplicated by Kopia repository so nothing is actually written to the backup storage. +For incremental backup, Incremental Aware Object Extension clones the table entries from the parent object for the skipped areas; for the written area, Incremental Aware Object Extension writes the data to Kopia repository and generate new entries. Finally, Incremental Aware Object Extension generates a new block address table for the incremental object which covers its entire logical space. + +Incremental Aware Object Extension is automatically activated for block mode data access as set by `AccessMode` of `ObjectWriteOptions`. + +#### Deduplication +The Incremental Aware Object Extension uses fix-sized splitter for deduplication, this is good enough for block level backup, reasons: +- Not like a file, a disk write never inserts data to the middle of the disk, it only does in-place update or append. So the data never shifts between two disks or the same disk of two different backups +- File system IO to disk general aligned to a specific size, e.g., 4KB for NTFS and ext4, as long as the chunk size is a multiply of this size, it effectively reduces the case that one IO kills two deduplication chunks +- For the usage cases that the disk is used as raw block device without a file system, the IO is still conducted by aligning to a specific boundary + +The chunk size is intentionally chosen as 1MB, reasons: +- 1MB is a multiply of 4KB for file systems or common block sizes for raw block device usages +- 1MB is the start boundary of partitions for modern operating systems, for both MBR and GPT, so partition metadata could be isolated to a separate chunk +- The more chunks are there, the more indexes in the repository, 1MB is a moderate value regarding to the overhead of indexes for Kopia repository + +#### Benefits +Since the existing block address table(BAT) of CAOS is reused and kept as is, it brings below benefits: +- All the entries are still managed by Kopia CAOS, so Velero doesn't need to keep an extra data +- The objects written by Velero block uploader is still recognizable by Kopia, for both full backup and incremental backup +- The existing data management in Kopia repository still works for objects generated by Velero block uploader, e.g., snapshot GC, repository maintenance, etc. + +Most importantly, this solution is super performant: +- During incremental write, it doesn't copy any data from the parent object, instead, it only clones object block address entries +- During backup deletion, it doesn't need to move any data, it only deletes the BAT for the object + +#### Uploader behavior +The block uploader's skippable write must also be aligned to this 1MB boundary, because Incremental Aware Object Extension needs to clone the entries that have been skipped from the parent object. +File system uploader is still using variable-sized deduplication, it is fine to keep data from the two uploaders into the same Kopia repository, though normally they won't be mutually deduplicated. +Volume could be resized; and volume size may not be aligned to 1MB boundary. The uploader need to handle the resize appropriately since Incremental Aware Object Extension cannot copy a BAT entry partially. + +#### CBT Layer +CBT provides below functionalities: +1. For a full backup, it provides the allocated data ranges. E.g., for a 1TB volume, there may be only 1MB of files, with this functionality, the uploader could skip the ranges without real data +2. For an incremental backup, it provides the changed data ranges based on the provided parent snapshot. In this way, the uploader could skip the unchanged data and achieves an incremental backup + +For case 1, the uploader calls Unified Repo Object's `WriteAt` method with the offset for the allocated data, ranges ahead of the offset will be filled as ZERO by unified repository. +For case 2, the uploader calls Unified Repo Object's `WriteAt` method with the offset for the changed data, ranges ahead of the offset will be cloned from the parent object unified repository. + +A changeId is stored with each backup, the next backup will retrieve the parent snapshot's changeId and use it to retrieve the CBT. + +The CBT retrieved from Kubernetes API are a list of `BlockMetadata`, each of range could be with fixed size or variable size. +Block uploader needs to maintain its own granularity that is friendly to its backup repository and uploader, as mentioned above. + +From Kubernetes API, `GetMetadataAllocated` or `GetMetadataDelta` are called looply until all `BlockMetadata` are retrieved. +On the other hand, considering the complexity in uploader, e.g., multiple stream between read and write, the workflow should be driven by the uploader instead of the CBT iterator, therefore, in practice, all the allocated/changed blocks should be retrieved and preserved before passing it to the uploader. + +As another fact, directly saving `BlockMetadata` list will be memory consuming. + +With all the above considerations, the `Bitmap` data structure is used to save the allocated/changed blocks, calling CBT Bitmap. +CBT Bitmap chunk size could be set as 1MB or a multiply of it, but a larger chunk size would amplify the backup size, so 1MB size will be use. + +Finally, interactions among CSI Snapshot Metadata Service, CBT Layer and Uploader is like below: + +![CBT Layer](cbt.png) + +In this way, CBT layer and uploader are decoupled and CBT bitmap plays as a north bound parameter of the uploader. + +#### Block Uploader +Block uploader consists of the reader and writer which are running asynchronously. +During backup, reader reads data from the block device and also refers to CBT Bitmap for allocated/changed blocks; writer writes data to the Unified Repo. +During restore, reader reads data from the Unified Repo; writer writes data to the block device. + +Reader and writer connects by a ring buffer, that is, reader pushes the block data to the ring buffer and writer gets data from the ring buffer and write to the target. + +To improve performance, block device is opened with direct IO, so that no data is going through the system cache unnecessarily. + +During restore, to optimize the write throughput and storage usage, zero blocks should be either skipped (for restoring to a new volume) or unmapped (for restoring to an existing volume). To cover the both cases in a unified way, the SCSI command `WRITE_SAME` is used. Logics are as below: +- Detect if a block read from the backup is with all zero data +- If true, the uploader sends `WRITE_SAME` SCSI command by calling `BLKZEROOUT` ioctl +- If the call fails, the uploader fallbaks to use the conservative way to write all zero bytes to the disk + +Uploader implementation is OS dependent, but since Windows container doesn't support block volumes, the current implementation is for linux only. + +#### ChangeId +ChangeId identifies the base that CBT is generated from, it must strictly map to the parent snapshot in the repository. Otherwise, there will be data corruption in the incremental backup. +Therefore, ChangeId is saved together with the repository snapshot. +The data mover always queries parent snapshot from Unified Repo together with the ChangeId. In this way, no mismatch would happen. +Inside the uploader, the upper layer (DataUpload controller) could also provide the ChangeId as a mechanism of double confirmation. The received ChangeId would be re-evaluated against the one in the provided snapshot. + +For Kubernetes API, changeId is represented by `BaseSnapshotId`. +changeId retrieval is storage specific, generally, it is retrieved from the `SnapshotHandle` of the VolumeSnapshotContent object; however, storages may also refer to other places to retrieve the changeId. +That is, `SnapshotHandle` and changeId may be two different values, in this case, the both values need to be preserved. + +#### Volume Snapshot Retention +Storages/CSI drivers may support the changeId differently based on the storage's capabilities: +1. In order to calculate the changes, some storages require the parent snapshot mapping to the changeId always exists at the time of `GetMetadataDelta` is called, then the parent snapshot can NOT be deleted as long as there are incremental backups based on it. +2. Some storages don't require the parent snapshot itself at the time of calculating changes, then parent snapshot could be deleted immediately after the parent backup completes. + +The existing exposer works perfectly with Case 1, that is, the snapshot is always deleted when the backup completes. +However, for Case 2, since the snapshot must be retained, the exposer needs changes as below: +- At the end of each backup, keep the current VolumeSnapshot's `deletionPolicy` as `Retain`, then when the VolumeSnapshot is deleted at the end of the backup, the current snapshot is retained in the storage +- `GetMetadataDelta` is called with `BaseSnapshotId` set as the preserved changeId +- When deleting a backup, a VolumeSnapshot-VolumeSnapshotContent pair is rebuilt with `deletionPolicy` as `delete` and `snapshotHandle` as the preserved one +- Then the rebuilt VolumeSnapshot is deleted so that the volume snapshot is deleted from the storage + +There is no way to automatically detect which way a specific volume support, so an interface is exposed to users to set the volume snapshot retention method. +The interface could be added to the `Action.Parameters` of Volume Policy. By default, Velero block data mover takes Way 1, so volume snapshot is never retained; if users specify `RetainSnapshot` parameter, Way 2 will be taken. +```go +type Action struct { + Type VolumeActionType `yaml:"type"` + Parameters map[string]any `yaml:"parameters,omitempty"` +} +``` +In this way, users could specify --- for storage class "xxx" or CSI driver "yyy", backup through CSI snapshot with Velero block data mover and retain the snapshot. + +#### Incremental Size +By the end of the backup, incremental size is also returned by the uploader, as same as Velero file system uploader. The size indicates how much data are unique so processed by the uploader, based on the provided CBT. + +### Fallback to Full Backup +There are some occasions that the incremental backup won't continue, so the data mover fallbacks to full backup: +- `GetMetadataAllocated` or `GetMetadataDelta` returns error +- ChangeId is missing +- Parent snapshot is missing + +When the fallback happens, the volume will be fully backed up from block level, but since because of the data deduplication from the backup repository, the unallocated/unchanged data would be probably deduplicated. +During restore, the volume will also be fully restored. The zero blocks handling as mentioned above is still working, so that write IO for unallocated data would be probably eliminated. + +Fallback is to handle the exceptional cases, for most of the backups/restores, fallback is never expected. + +### Irregular Volume Size +As mentioned above, during incremental backup, block uploader IO should be restricted to be aligned to the deduplication chunk size (1MB); on the other hand, there is no hard limit for users' volume size to be aligned. +To support volumes with irregular size, below measures are taken: +- Volume objects in the repository is always aligned to 1MB +- If the volume size is irregular, zero bytes will be padded to the tail of the volume object +- A real size is recorded in the repository snapshot +- During restore, the real size of data is restored + +The padding must be always with zero bytes. + +### Volume Size Change +Incremental backup could continue when volume is resized. +Block uploader supports to write disk with arbitrary size. +The volume resize cases don't need to be handled case by case. + +Instead, when volume resize happens, block uploader needs to handle it appropriately in below ways: +- Loop with CBT +- Read data between RoundDownTo1M(newSize) and newSize to get the tail data +- If there is no tail data, which means the volume size is aligned to 1MB, then call `WriteAt(newSize, nil)` +- Otherwise, call `WriteAt(RoundDownTo1M(newSize), taildata)`, `taildata` is also padded to 1MB + +That is to say: +- If CBT covers the tail of the volume, loop with CBT is enough for both shrink and expand case +- Otherwise, if volume is expanded, `WriteAt` guarantees to clone appropriate objects entries from the parent object and append zero data for the expanded areas. Particularly, if the parent volume is not in regular size, the zero padding bytes is also reused. Therefore, the parent object's padding bytes must be zero +- In the case the volume is shrunk, writing the tail data makes sure zero bytes are padding to the new volume object instead of inheriting non-zero data from the parent object + +### Cancellation +The existing Cancellation mechanism is reused, so there is no change outside of the block uploader. +Inside the uploader, cancellation checkpoints are embedded to the uploader reader and writer, so that the execution could quit in a reasonable time once cancellation happens. + +### Parallelism +Parallelism among data movers will reuse the existing mechanism --- load concurrency. +Inside the data mover, uploader reader and writer are always running in parallel. The number of reader and writer is always 1. +Sequential read/write of the volume is always optimized, there is no prove that multiple readers/writers are beneficial. + +### Progress Report +Progress report outside of the data mover will reuse the existing mechanism. +Inside the data mover, progress update is embedded to the uploader writer. +The progress struct is kept as is, Velero block data mover still supports `TotalBytes` and `BytesDone`: +```go +type Progress struct { + TotalBytes int64 `json:"totalBytes,omitempty"` + BytesDone int64 `json:"doneBytes,omitempty"` +} +``` +By the end of the backup, the progress for block data mover provides the same `GetIncrementalSize` which reports the incremental size of the backup, so that the incremental size is reported to users in the same way as the file system data mover. + +### Selectable Backup Type +For many reasons, a periodical full backup is required: +- From user experience, a periodical full is required to make sure the data integrity among the incremental backups, e.g., every 1 week or 1 month + +Therefore, backup type (full/incremental) should be supported in Velero's manual backup and backup schedule. +Backup type will also be added to `volumeInfo.json` to support observability purposes. + +Backup TTL is still used for users to specify a backup's retention time. By default, both full and incremental backups are with 30 days retention, even though this is not so reasonable for the full backups. This could be enhanced when Velero supports sophisticated retention policy. +As a workaround, users could create two schedules for the same scope of backup, one is for full backups, with less frequency and longer backup TTL; the other one is for incremental backups, with normal frequency and shorter backup TTL. + +#### File System Data Mover +At present, Velero file system data mover doesn't support selectable backup type, instead, incremental backups are always conducted once possible. +From user experience this is not reasonable. + +Therefore, to solve this problem and to make it align with Velero block data mover, Velero file system data mover will support backup type as well. + +At present, the data path for Velero file system data mover has already supported it, we only need to expose this functionality to users. + +### Backup Describe +Backup type should be added to backup description, there are two appearances: +- The `backupType` in the Backup CR. This is the selected backup type by users +- The backup type recorded in `volumeInfo.json`, which is the actual type taken by the backup +With these two values, users are able to know the actual backup type and also whether a fallback happens. + +The `DataMover` item in the existing backup description should be updated to reflect the actual data mover completing the backup, this information could be retrieved from `volumeInfo.json`. + +### Backup Sync +No more data is required for sync, so Backup Sync is kept as is. + +### Backup Deletion +As mentioned above, no data is moved when deleting a repo snapshot for Velero block data mover, so Backup Deletion is kept as is regarding to repo snapshot; and for volume snapshot retention case, backup deletion logics will be modified accordingly to delete the retained snapshots. + +### Restarts +Restarts mechanism is reused without any change. + +### Logging +Logging mechanism is not changed. + +### Backup CRD +A `backupType` field is added to Backup CRD, two values are supported `full` or `incremental`. +`full` indicates the data mover to take a full backup. +`incremental` which is the default value, indicates the data mover to take an incremental backup. + +```yaml + spec: + description: BackupSpec defines the specification for a Velero backup. + properties: + backupType: + description: BackupType indicates the type of the backup + enum: + - full + - incremental + type: string +``` + +### DataUpload CRD +A `parentSnapshot` field is added to the DataUpload CRD, below values are supported: +- `""`: it fallbacks to `auto` +- `auto`: it means the data mover finds the recent snapshot of the same volume from Unified Repository and use it as the parent +- `none`: it means the data mover is not assigned with a parent snapshot, so it runs a full backup +- a specific snapshotID: it means the data mover use the specific snapshotID to find the parent snapshot. If it cannot be found, the data mover fallbacks to a full backup + +The last option is for a backup plan, it will not be used for now and may be useful when Velero supports sophisticated retention policy. This means, Velero always finds the recent backup as the parent. + +When `backupType` of the Backup is `full`, the data mover controller sets `none` to `parentSnapshot` of DataUpload. +When `backupType` of the Backup is `incremental`, the data mover controller sets `auto` to `parentSnapshot` of DataUpload. And `""` is just kept for backwards compatibility consideration. + +```yaml + spec: + description: DataUploadSpec is the specification for a DataUpload. + properties: + parentSnapshot: + description: |- + ParentSnapshot specifies the parent snapshot that current backup is based on. + If its value is "" or "auto", the data mover finds the recent backup of the same volume as parent. + If its value is "none", the data mover will do a full backup + If its value is a specific snapshotID, the data mover finds the specific snapshot as parent. + type: string +``` + +### DataDownload CRD +No change is required to DataDownload CRD. + +## Plugin Data Movers +The current design doesn't break anything for plugin data movers. +The enhancement in VolumePolicy could also be used for plugin data movers. That is, users could select a plugin data mover through VolumePolicy as same as Velero built-in data movers. + +## Installation +No change to Installation. + +## Upgrade +No impacts to Upgrade. The new fields in the CRDs are all optional fields and have backwards compatible values. + +## CLI +Backup type parameter is added to Velero CLI as below: +``` +velero backup create --full +velero schedule create --full +``` +When the parameter is not specified, by default, Velero goes with incremental backups. + + + +[1]: ../Implemented/unified-repo-and-kopia-integration/unified-repo-and-kopia-integration.md +[2]: ../Implemented/volume-snapshot-data-movement/volume-snapshot-data-movement.md +[3]: ../Implemented/vgdp-micro-service/vgdp-micro-service.md +[4]: https://kubernetes.io/blog/2025/09/25/csi-changed-block-tracking/ +[5]: https://kopia.io/docs/advanced/architecture/ \ No newline at end of file diff --git a/design/block-data-mover/caos-extension.png b/design/block-data-mover/caos-extension.png new file mode 100644 index 0000000000..928c1e3522 Binary files /dev/null and b/design/block-data-mover/caos-extension.png differ diff --git a/design/block-data-mover/cbt.png b/design/block-data-mover/cbt.png new file mode 100644 index 0000000000..f20d287c2d Binary files /dev/null and b/design/block-data-mover/cbt.png differ diff --git a/design/block-data-mover/data-path-overview.png b/design/block-data-mover/data-path-overview.png new file mode 100644 index 0000000000..a688804850 Binary files /dev/null and b/design/block-data-mover/data-path-overview.png differ diff --git a/design/block-data-mover/restore-architecture.png b/design/block-data-mover/restore-architecture.png new file mode 100644 index 0000000000..c0b2590283 Binary files /dev/null and b/design/block-data-mover/restore-architecture.png differ diff --git a/design/block-data-mover/vgdp-backup.png b/design/block-data-mover/vgdp-backup.png new file mode 100644 index 0000000000..b9ebb77647 Binary files /dev/null and b/design/block-data-mover/vgdp-backup.png differ diff --git a/design/block-data-mover/vgdp-restore.png b/design/block-data-mover/vgdp-restore.png new file mode 100644 index 0000000000..fefb40dbe4 Binary files /dev/null and b/design/block-data-mover/vgdp-restore.png differ diff --git a/go.mod b/go.mod index 04931a66f7..058ce94f1e 100644 --- a/go.mod +++ b/go.mod @@ -22,11 +22,11 @@ require ( github.com/gobwas/glob v0.2.3 github.com/google/go-cmp v0.7.0 github.com/google/uuid v1.6.0 - github.com/hashicorp/go-hclog v0.14.1 - github.com/hashicorp/go-plugin v1.6.0 + github.com/hashicorp/go-hclog v1.6.3 + github.com/hashicorp/go-plugin v1.7.0 github.com/joho/godotenv v1.3.0 github.com/kopia/kopia v0.16.0 - github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 + github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0 github.com/onsi/ginkgo/v2 v2.22.0 github.com/onsi/gomega v1.36.1 github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 @@ -54,7 +54,7 @@ require ( k8s.io/apimachinery v0.33.3 k8s.io/cli-runtime v0.33.3 k8s.io/client-go v0.33.3 - k8s.io/klog/v2 v2.130.1 + k8s.io/klog/v2 v2.140.0 k8s.io/kube-aggregator v0.33.3 k8s.io/metrics v0.33.3 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 @@ -125,7 +125,7 @@ require ( github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/cronexpr v1.1.3 // indirect - github.com/hashicorp/yamux v0.1.1 // indirect + github.com/hashicorp/yamux v0.1.2 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/josharian/intern v1.0.0 // indirect @@ -143,8 +143,7 @@ require ( github.com/minio/crc64nvme v1.1.0 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/minio/minio-go/v7 v7.0.97 // indirect - github.com/mitchellh/go-testing-interface v1.0.0 // indirect - github.com/moby/spdystream v0.5.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 github.com/modern-go/reflect2 v1.0.2 // indirect @@ -153,7 +152,7 @@ require ( github.com/mxk/go-vss v1.2.0 // indirect github.com/natefinch/atomic v1.0.1 // indirect github.com/nxadm/tail v1.4.8 // indirect - github.com/oklog/run v1.0.0 // indirect + github.com/oklog/run v1.1.0 // indirect github.com/philhofer/fwd v1.2.0 // indirect github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect diff --git a/go.sum b/go.sum index a1c1576c7e..f5b5de932a 100644 --- a/go.sum +++ b/go.sum @@ -174,8 +174,8 @@ github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bombsimon/logrusr/v3 v3.0.0 h1:tcAoLfuAhKP9npBxWzSdpsvKPQt1XV02nSf2lZA82TQ= github.com/bombsimon/logrusr/v3 v3.0.0/go.mod h1:PksPPgSFEL2I52pla2glgCyyd2OqOHAnFF5E+g8Ixco= -github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= -github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= @@ -242,6 +242,7 @@ github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLi github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= @@ -428,13 +429,13 @@ github.com/hashicorp/cronexpr v1.1.3 h1:rl5IkxXN2m681EfivTlccqIryzYJSXRGRNa0xeG7 github.com/hashicorp/cronexpr v1.1.3/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-hclog v0.14.1 h1:nQcJDQwIAGnmoUWp8ubocEX40cCml/17YkF6csQLReU= -github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= -github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= -github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= +github.com/hashicorp/go-plugin v1.7.0 h1:YghfQH/0QmPNc/AZMTFE3ac8fipZyZECHdDPshfk+mA= +github.com/hashicorp/go-plugin v1.7.0/go.mod h1:BExt6KEaIYx804z8k4gRzRLEvxKVb+kn0NMcihqOqb8= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -448,8 +449,8 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= -github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= -github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8= +github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -458,8 +459,8 @@ github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= -github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/jhump/protoreflect v1.17.0 h1:qOEr613fac2lOuTgWN4tPAtLL7fUSbuJL5X5XumQh94= +github.com/jhump/protoreflect v1.17.0/go.mod h1:h9+vUUL38jiBzck8ck+6G/aeMX8Z4QUY/NiJPwPNi+8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= @@ -507,8 +508,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0 h1:Q3jQ1NkFqv5o+F8dMmHd8SfEmlcwNeo1immFApntEwE= -github.com/kubernetes-csi/external-snapshotter/client/v8 v8.2.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y= +github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0 h1:bMqrb3UHgHbP+PW9VwiejfDJU1R0PpXVZNMdeH8WYKI= +github.com/kubernetes-csi/external-snapshotter/client/v8 v8.4.0/go.mod h1:E3vdYxHj2C2q6qo8/Da4g7P+IcwqRZyy3gJBzYybV9Y= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= @@ -523,12 +524,13 @@ github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +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-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= @@ -544,7 +546,6 @@ github.com/minio/minio-go/v7 v7.0.97/go.mod h1:re5VXuo0pwEtoNLsNuSr0RrLfT/MBtohw github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= @@ -552,8 +553,8 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= -github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +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= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -578,8 +579,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= -github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= +github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -696,6 +697,7 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= @@ -914,7 +916,6 @@ golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -926,12 +927,12 @@ golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191002063906-3421d5a6bb1c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -965,7 +966,9 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1232,8 +1235,8 @@ k8s.io/client-go v0.33.3/go.mod h1:luqKBQggEf3shbxHY4uVENAxrDISLOarxpTKMiUuujg= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= -k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= k8s.io/kube-aggregator v0.33.3 h1:Pa6hQpKJMX0p0D2wwcxXJgu02++gYcGWXoW1z1ZJDfo= k8s.io/kube-aggregator v0.33.3/go.mod h1:hwvkUoQ8q6gv0+SgNnlmQ3eUue1zHhJKTHsX7BwxwSE= k8s.io/kube-openapi v0.0.0-20210421082810-95288971da7e/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= diff --git a/hack/build-image/Dockerfile b/hack/build-image/Dockerfile index 0a60e6a165..25a162a826 100644 --- a/hack/build-image/Dockerfile +++ b/hack/build-image/Dockerfile @@ -12,7 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -FROM --platform=$TARGETPLATFORM golang:1.25-bookworm +FROM --platform=$TARGETPLATFORM golang:1.25-trixie ARG GOPROXY diff --git a/hack/build-restic.sh b/hack/build-restic.sh deleted file mode 100755 index d6a233f4a5..0000000000 --- a/hack/build-restic.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -# Copyright 2020 the Velero contributors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -set -o errexit -set -o nounset -set -o pipefail - -# Use /output/usr/bin/ as the default output directory as this -# is the path expected by the Velero Dockerfile. -output_dir=${OUTPUT_DIR:-/output/usr/bin} -restic_bin=${output_dir}/restic -build_path=$(dirname "$PWD") - -if [[ -z "${BIN}" ]]; then - echo "BIN must be set" - exit 1 -fi - -if [[ "${BIN}" != "velero" ]]; then - echo "${BIN} does not need the restic binary" - exit 0 -fi - -if [[ -z "${GOOS}" ]]; then - echo "GOOS must be set" - exit 1 -fi -if [[ -z "${GOARCH}" ]]; then - echo "GOARCH must be set" - exit 1 -fi -if [[ -z "${RESTIC_VERSION}" ]]; then - echo "RESTIC_VERSION must be set" - exit 1 -fi - -mkdir ${build_path}/restic -git clone -b v${RESTIC_VERSION} https://github.com/restic/restic.git ${build_path}/restic -pushd ${build_path}/restic -git apply /go/src/github.com/vmware-tanzu/velero/hack/fix_restic_cve.txt -go run build.go --goos "${GOOS}" --goarch "${GOARCH}" --goarm "${GOARM}" -o ${restic_bin} -chmod +x ${restic_bin} -popd diff --git a/hack/fix_restic_cve.txt b/hack/fix_restic_cve.txt deleted file mode 100644 index eeee2ecc61..0000000000 --- a/hack/fix_restic_cve.txt +++ /dev/null @@ -1,274 +0,0 @@ -diff --git a/go.mod b/go.mod -index 5f939c481..f6205aa3c 100644 ---- a/go.mod -+++ b/go.mod -@@ -24,32 +24,31 @@ require ( - github.com/restic/chunker v0.4.0 - github.com/spf13/cobra v1.6.1 - github.com/spf13/pflag v1.0.5 -- golang.org/x/crypto v0.5.0 -- golang.org/x/net v0.5.0 -- golang.org/x/oauth2 v0.4.0 -- golang.org/x/sync v0.1.0 -- golang.org/x/sys v0.4.0 -- golang.org/x/term v0.4.0 -- golang.org/x/text v0.6.0 -- google.golang.org/api v0.106.0 -+ golang.org/x/crypto v0.45.0 -+ golang.org/x/net v0.47.0 -+ golang.org/x/oauth2 v0.28.0 -+ golang.org/x/sync v0.18.0 -+ golang.org/x/sys v0.38.0 -+ golang.org/x/term v0.37.0 -+ golang.org/x/text v0.31.0 -+ google.golang.org/api v0.114.0 - ) - - require ( -- cloud.google.com/go v0.108.0 // indirect -- cloud.google.com/go/compute v1.15.1 // indirect -- cloud.google.com/go/compute/metadata v0.2.3 // indirect -- cloud.google.com/go/iam v0.10.0 // indirect -+ cloud.google.com/go v0.110.0 // indirect -+ cloud.google.com/go/compute/metadata v0.3.0 // indirect -+ cloud.google.com/go/iam v0.13.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/dnaeon/go-vcr v1.2.0 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect - github.com/felixge/fgprof v0.9.3 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect -- github.com/golang/protobuf v1.5.2 // indirect -+ github.com/golang/protobuf v1.5.3 // indirect - github.com/google/pprof v0.0.0-20230111200839-76d1ae5aea2b // indirect - github.com/google/uuid v1.3.0 // indirect -- github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect -- github.com/googleapis/gax-go/v2 v2.7.0 // indirect -+ github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect -+ github.com/googleapis/gax-go/v2 v2.7.1 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.3 // indirect -@@ -63,11 +62,13 @@ require ( - go.opencensus.io v0.24.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/appengine v1.6.7 // indirect -- google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f // indirect -- google.golang.org/grpc v1.52.0 // indirect -- google.golang.org/protobuf v1.28.1 // indirect -+ google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect -+ google.golang.org/grpc v1.56.3 // indirect -+ google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - ) - --go 1.18 -+go 1.24.0 -+ -+toolchain go1.24.11 -diff --git a/go.sum b/go.sum -index 026e1d2fa..4a37e7ac7 100644 ---- a/go.sum -+++ b/go.sum -@@ -1,23 +1,24 @@ - cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= --cloud.google.com/go v0.108.0 h1:xntQwnfn8oHGX0crLVinvHM+AhXvi3QHQIEcX/2hiWk= --cloud.google.com/go v0.108.0/go.mod h1:lNUfQqusBJp0bgAg6qrHgYFYbTB+dOiob1itwnlD33Q= --cloud.google.com/go/compute v1.15.1 h1:7UGq3QknM33pw5xATlpzeoomNxsacIVvTqTTvbfajmE= --cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= --cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= --cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= --cloud.google.com/go/iam v0.10.0 h1:fpP/gByFs6US1ma53v7VxhvbJpO2Aapng6wabJ99MuI= --cloud.google.com/go/iam v0.10.0/go.mod h1:nXAECrMt2qHpF6RZUZseteD6QyanL68reN4OXPw0UWM= --cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= -+cloud.google.com/go v0.110.0 h1:Zc8gqp3+a9/Eyph2KDmcGaPtbKRIoqq4YTlL4NMD0Ys= -+cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -+cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -+cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -+cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= -+cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -+cloud.google.com/go/longrunning v0.4.1 h1:v+yFJOfKC3yZdY6ZUI933pIYdhyhV8S3NpWrXWmg7jM= -+cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= - cloud.google.com/go/storage v1.28.1 h1:F5QDG5ChchaAVQhINh24U99OWHURqrW8OmQcGKXcbgI= - cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0 h1:VuHAcMq8pU1IWNT/m5yRaGqbK0BiQKHT8X4DTp9CHdI= - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.3.0/go.mod h1:tZoQYdDZNOiIjdSn0dVWVfl0NEPGOJqVLzSrcFk4Is0= - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4SathZPhDhF4mVwpBMFlYjyAqy8= -+github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0= - github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2 h1:+5VZ72z0Qan5Bog5C+ZkgSqUbeVUd9wgtHOrIKuc5b8= - github.com/Azure/azure-sdk-for-go/sdk/internal v1.1.2/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w= - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1 h1:BMTdr+ib5ljLa9MxTJK8x/Ds0MbBb4MfuW5BL0zMJnI= - github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v0.5.1/go.mod h1:c6WvOhtmjNUWbLfOG1qxM/q0SPvQNSVJvolm+C52dIU= - github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 h1:BWe8a+f/t+7KY7zH2mqygeUD0t8hNFXe08p1Pb3/jKE= -+github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1/go.mod h1:Vt9sXTKwMyGcOxSmLDMnGPgqsUg7m8pe215qMLrDXw4= - github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= - github.com/Julusian/godocdown v0.0.0-20170816220326-6d19f8ff2df8/go.mod h1:INZr5t32rG59/5xeltqoCJoNY7e5x/3xoY9WSWVWg74= - github.com/anacrolix/fuse v0.2.0 h1:pc+To78kI2d/WUjIyrsdqeJQAesuwpGxlI3h1nAv3Do= -@@ -54,6 +55,7 @@ github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNu - github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= - github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= - github.com/golang-jwt/jwt v3.2.1+incompatible h1:73Z+4BJcrTC+KczS6WvTPvRGOp1WmfEP4Q1lOd9Z/+c= -+github.com/golang-jwt/jwt v3.2.1+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= - github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -@@ -70,8 +72,8 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq - github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= - github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= - github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= --github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= --github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= - github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= - github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= - github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -@@ -82,17 +84,18 @@ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ - github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= - github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= - github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= --github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= -+github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -+github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= - github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= - github.com/google/pprof v0.0.0-20230111200839-76d1ae5aea2b h1:8htHrh2bw9c7Idkb7YNac+ZpTqLMjRpI+FWu51ltaQc= - github.com/google/pprof v0.0.0-20230111200839-76d1ae5aea2b/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= - github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= - github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= - github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= --github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= --github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= --github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= --github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -+github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -+github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -+github.com/googleapis/gax-go/v2 v2.7.1 h1:gF4c0zjUP2H/s/hEGyLA3I0fA2ZWjzYiONAD6cvPr8A= -+github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= - github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= - github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= - github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= -@@ -114,6 +117,7 @@ github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= - github.com/kurin/blazer v0.5.4-0.20211030221322-ba894c124ac6 h1:nz7i1au+nDzgExfqW5Zl6q85XNTvYoGnM5DHiQC0yYs= - github.com/kurin/blazer v0.5.4-0.20211030221322-ba894c124ac6/go.mod h1:4FCXMUWo9DllR2Do4TtBd377ezyAJ51vB5uTBjt0pGU= - github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= -+github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= - github.com/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.0.46 h1:Vo3tNmNXuj7ME5qrvN4iadO7b4mzu/RSFdUkUhaPldk= -@@ -129,6 +133,7 @@ github.com/modocache/gover v0.0.0-20171022184752-b58185e213c5/go.mod h1:caMODM3P - github.com/ncw/swift/v2 v2.0.1 h1:q1IN8hNViXEv8Zvg3Xdis4a3c4IlIGezkYz09zQL5J0= - github.com/ncw/swift/v2 v2.0.1/go.mod h1:z0A9RVdYPjNjXVo2pDOPxZ4eu3oarO1P91fTItcb+Kg= - github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 h1:Qj1ukM4GlMWXNdMBuXcXfz/Kw9s1qm0CLY32QxuSImI= -+github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4/go.mod h1:N6UoU20jOqggOuDwUaBQpluzLNDqif3kq9z2wpdYEfQ= - github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= - github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= - github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= -@@ -172,8 +177,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk - 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.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= --golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= --golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= -+golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -+golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= - golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= - golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= - golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -@@ -189,17 +194,17 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL - golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= - golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= - golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= --golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= --golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -+golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -+golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= - golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= --golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= --golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -+golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= -+golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= - golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= - golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= - golang.org/x/sync v0.0.0-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.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= --golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -+golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -+golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= - golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= - 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= -@@ -214,17 +219,17 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc - golang.org/x/sys v0.0.0-20220408201424-a24fb2fb8a0f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= - golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= - golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= --golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= --golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -+golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -+golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= - golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= --golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= --golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -+golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= -+golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= - golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= - golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= - golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= - golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= --golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= --golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= - golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= - golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= - golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -@@ -237,8 +242,8 @@ golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8T - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= --google.golang.org/api v0.106.0 h1:ffmW0faWCwKkpbbtvlY/K/8fUl+JKvNS5CVzRoyfCv8= --google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -+google.golang.org/api v0.114.0 h1:1xQPji6cO2E2vLiI+C/XiFAnsn1WV3mjaEwGLhi3grE= -+google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= - google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= - google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= - google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -@@ -246,15 +251,15 @@ google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCID - google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= - google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= - google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= --google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f h1:BWUVssLB0HVOSY78gIdvk1dTVYtT1y8SBWtPYuTJ/6w= --google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -+google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= - google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= - google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= - google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= - google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= - google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= --google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= --google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -+google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -+google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= - google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= - google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= - google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -@@ -266,14 +271,15 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD - google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= - google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= - google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= --google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= --google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= - gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= - gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= - gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= - gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= - gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= - gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= - gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= - gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= - gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/release-tools/goreleaser.sh b/hack/release-tools/goreleaser.sh index 79f9a4fc52..b8a1807540 100755 --- a/hack/release-tools/goreleaser.sh +++ b/hack/release-tools/goreleaser.sh @@ -51,11 +51,13 @@ if [[ "${PUBLISH:-}" != "TRUE" ]]; then echo "Not set to publish" goreleaser release \ --clean \ + --parallelism 2 \ --release-notes="${RELEASE_NOTES_FILE}" \ --snapshot # Generate an unversioned snapshot release, skipping all validations and without publishing any artifacts (implies --skip-publish, --skip-announce and --skip-validate) else echo "Getting ready to publish" goreleaser release \ --clean \ + --parallelism 2 \ --release-notes="${RELEASE_NOTES_FILE}" fi diff --git a/hack/release-tools/tag-release.sh b/hack/release-tools/tag-release.sh index 4fbfa933e7..7a993c03b0 100755 --- a/hack/release-tools/tag-release.sh +++ b/hack/release-tools/tag-release.sh @@ -24,7 +24,7 @@ # The following variables are needed: -# - $VELERO_VERSION: defines the tag of Velero that any https://github.com/vmware-tanzu/velero/... +# - $VELERO_VERSION: defines the tag of Velero that any https://github.com/velero-io/velero/... # links in the docs should redirect to. # - $REMOTE: defines the remote that should be used when pushing tags and branches. Defaults to "upstream" # - $publish: TRUE/FALSE value where FALSE (or not including it) will indicate a dry-run, and TRUE, or simply adding 'publish', diff --git a/internal/delete/actions/csi/volumesnapshotcontent_action.go b/internal/delete/actions/csi/volumesnapshotcontent_action.go index 98e0fc03bc..9473686e05 100644 --- a/internal/delete/actions/csi/volumesnapshotcontent_action.go +++ b/internal/delete/actions/csi/volumesnapshotcontent_action.go @@ -18,6 +18,7 @@ package csi import ( "context" + "time" "github.com/google/uuid" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" @@ -40,6 +41,10 @@ type volumeSnapshotContentDeleteItemAction struct { crClient crclient.Client } +const tempVSCCreateDeleteGap = 2 * time.Second + +var sleepBetweenTempVSCCreateAndDelete = time.Sleep + // AppliesTo returns information indicating // VolumeSnapshotContentRestoreItemAction action should be invoked // while restoring VolumeSnapshotContent.snapshot.storage.k8s.io resources @@ -123,6 +128,9 @@ func (p *volumeSnapshotContentDeleteItemAction) Execute( } p.log.Infof("Created temp VolumeSnapshotContent %s with DeletionPolicy=Delete to trigger cloud snapshot cleanup", snapCont.Name) + // Add a small delay before delete to avoid create/delete race conditions in CSI controllers. + sleepBetweenTempVSCCreateAndDelete(tempVSCCreateDeleteGap) + // Delete the temp VSC immediately to trigger cloud snapshot removal. // The CSI driver will handle the actual cloud snapshot deletion. if err := p.crClient.Delete( diff --git a/internal/delete/actions/csi/volumesnapshotcontent_action_test.go b/internal/delete/actions/csi/volumesnapshotcontent_action_test.go index 114bd752f3..e8a0b58654 100644 --- a/internal/delete/actions/csi/volumesnapshotcontent_action_test.go +++ b/internal/delete/actions/csi/volumesnapshotcontent_action_test.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "testing" + "time" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/sirupsen/logrus" @@ -46,6 +47,21 @@ type fakeClientWithErrors struct { deleteError error } +type fakeClientWithCallTracking struct { + crclient.Client + events *[]string +} + +func (c *fakeClientWithCallTracking) Create(ctx context.Context, obj crclient.Object, opts ...crclient.CreateOption) error { + *c.events = append(*c.events, "create") + return c.Client.Create(ctx, obj, opts...) +} + +func (c *fakeClientWithCallTracking) Delete(ctx context.Context, obj crclient.Object, opts ...crclient.DeleteOption) error { + *c.events = append(*c.events, "delete") + return c.Client.Delete(ctx, obj, opts...) +} + func (c *fakeClientWithErrors) Get(ctx context.Context, key crclient.ObjectKey, obj crclient.Object, opts ...crclient.GetOption) error { if c.getError != nil { return c.getError @@ -325,6 +341,39 @@ func TestTryDeleteOriginalVSC(t *testing.T) { }) } +func TestVSCExecute_CreateSleepDeleteOrder(t *testing.T) { + snapshotHandleStr := "test" + vsc := builder.ForVolumeSnapshotContent("bar"). + ObjectMeta(builder.WithLabelsMap(map[string]string{velerov1api.BackupNameLabel: "backup"})). + Status(&snapshotv1api.VolumeSnapshotContentStatus{SnapshotHandle: &snapshotHandleStr}). + Result() + + vscMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(vsc) + require.NoError(t, err) + + events := make([]string, 0, 3) + realClient := velerotest.NewFakeControllerRuntimeClient(t) + trackingClient := &fakeClientWithCallTracking{Client: realClient, events: &events} + + originalSleep := sleepBetweenTempVSCCreateAndDelete + t.Cleanup(func() { + sleepBetweenTempVSCCreateAndDelete = originalSleep + }) + + sleepBetweenTempVSCCreateAndDelete = func(d time.Duration) { + require.Equal(t, tempVSCCreateDeleteGap, d) + events = append(events, "sleep") + } + + p := volumeSnapshotContentDeleteItemAction{log: logrus.StandardLogger(), crClient: trackingClient} + err = p.Execute(&velero.DeleteItemActionExecuteInput{ + Item: &unstructured.Unstructured{Object: vscMap}, + Backup: builder.ForBackup("velero", "backup").Result(), + }) + require.NoError(t, err) + require.Equal(t, []string{"create", "sleep", "delete"}, events) +} + func boolPtr(b bool) *bool { return &b } diff --git a/internal/resourcepolicies/resource_policies.go b/internal/resourcepolicies/resource_policies.go index 6b5046e57b..3a173fce1e 100644 --- a/internal/resourcepolicies/resource_policies.go +++ b/internal/resourcepolicies/resource_policies.go @@ -38,7 +38,7 @@ const ( ConfigmapRefType string = "configmap" // skip action implies the volume would be skipped from the backup operation Skip VolumeActionType = "skip" - // fs-backup action implies that the volume would be backed up via file system copy method using the uploader(kopia/restic) configured by the user + // fs-backup action implies that the volume would be backed up via file system copy method using the uploader(kopia) configured by the user FSBackup VolumeActionType = "fs-backup" // snapshot action can have 3 different meaning based on velero configuration and backup spec - cloud provider based snapshots, local csi snapshots and datamover snapshots Snapshot VolumeActionType = "snapshot" diff --git a/pkg/backup/actions/csi/pvc_action.go b/pkg/backup/actions/csi/pvc_action.go index ac5f71a98e..7f5fd2afa9 100644 --- a/pkg/backup/actions/csi/pvc_action.go +++ b/pkg/backup/actions/csi/pvc_action.go @@ -24,7 +24,7 @@ import ( "k8s.io/client-go/util/retry" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -467,7 +467,7 @@ func (p *pvcBackupItemAction) Progress( return progress, biav2.InvalidOperationIDError(operationID) } - dataUpload, err := getDataUpload(context.Background(), p.crClient, operationID) + dataUpload, err := getDataUpload(context.Background(), p.crClient, backup.Namespace, operationID) if err != nil { p.log.Errorf( "fail to get DataUpload for backup %s/%s by operation ID %s: %s", @@ -512,7 +512,7 @@ func (p *pvcBackupItemAction) Cancel(operationID string, backup *velerov1api.Bac return biav2.InvalidOperationIDError(operationID) } - dataUpload, err := getDataUpload(context.Background(), p.crClient, operationID) + dataUpload, err := getDataUpload(context.Background(), p.crClient, backup.Namespace, operationID) if err != nil { p.log.Errorf( "fail to get DataUpload for backup %s/%s: %s", @@ -605,10 +605,12 @@ func createDataUpload( func getDataUpload( ctx context.Context, crClient crclient.Client, + namespace string, operationID string, ) (*velerov2alpha1.DataUpload, error) { dataUploadList := new(velerov2alpha1.DataUploadList) err := crClient.List(ctx, dataUploadList, &crclient.ListOptions{ + Namespace: namespace, LabelSelector: labels.SelectorFromSet( map[string]string{velerov1api.AsyncOperationIDLabel: operationID}, ), @@ -765,7 +767,7 @@ func (p *pvcBackupItemAction) getVolumeSnapshotReference( } // Re-fetch latest VGS to ensure status is populated after VGSC binding - latestVGS := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{} + latestVGS := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{} if err := p.crClient.Get(ctx, crclient.ObjectKeyFromObject(newVGS), latestVGS); err != nil { return nil, errors.Wrapf(err, "failed to re-fetch VolumeGroupSnapshot %s after VGSC binding wait", newVGS.Name) } @@ -913,7 +915,7 @@ func (p *pvcBackupItemAction) determineVGSClass( } // 3. Fallback to label-based default - vgsClassList := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotClassList{} + vgsClassList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotClassList{} if err := p.crClient.List(ctx, vgsClassList); err != nil { return "", errors.Wrap(err, "failed to list VolumeGroupSnapshotClasses") } @@ -942,22 +944,22 @@ func (p *pvcBackupItemAction) createVolumeGroupSnapshot( backup *velerov1api.Backup, pvc corev1api.PersistentVolumeClaim, vgsLabelKey, vgsLabelValue, vgsClassName string, -) (*volumegroupsnapshotv1beta1.VolumeGroupSnapshot, error) { +) (*volumegroupsnapshotv1beta2.VolumeGroupSnapshot, error) { vgsLabels := map[string]string{ velerov1api.BackupNameLabel: label.GetValidName(backup.Name), velerov1api.BackupUIDLabel: string(backup.UID), vgsLabelKey: vgsLabelValue, } - vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{ + vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{ ObjectMeta: metav1.ObjectMeta{ GenerateName: fmt.Sprintf("velero-%s-", vgsLabelValue), Namespace: pvc.Namespace, Labels: vgsLabels, }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotSpec{ VolumeGroupSnapshotClassName: &vgsClassName, - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotSource{ + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotSource{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ vgsLabelKey: vgsLabelValue, @@ -985,7 +987,7 @@ func (p *pvcBackupItemAction) createVolumeGroupSnapshot( func (p *pvcBackupItemAction) waitForVGSAssociatedVS( ctx context.Context, groupedPVCs []corev1api.PersistentVolumeClaim, - vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot, + vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot, timeout time.Duration, ) (map[string]*snapshotv1api.VolumeSnapshot, error) { expected := len(groupedPVCs) @@ -1028,10 +1030,10 @@ func (p *pvcBackupItemAction) waitForVGSAssociatedVS( return vsMap, nil } -func hasOwnerReference(obj metav1.Object, vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot) bool { +func hasOwnerReference(obj metav1.Object, vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot) bool { for _, ref := range obj.GetOwnerReferences() { if ref.Kind == kuberesource.VGSKind && - ref.APIVersion == volumegroupsnapshotv1beta1.GroupName+"/"+volumegroupsnapshotv1beta1.SchemeGroupVersion.Version && + ref.APIVersion == volumegroupsnapshotv1beta2.GroupName+"/"+volumegroupsnapshotv1beta2.SchemeGroupVersion.Version && ref.UID == vgs.UID { return true } @@ -1042,7 +1044,7 @@ func hasOwnerReference(obj metav1.Object, vgs *volumegroupsnapshotv1beta1.Volume func (p *pvcBackupItemAction) updateVGSCreatedVS( ctx context.Context, vsMap map[string]*snapshotv1api.VolumeSnapshot, - vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot, + vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot, backup *velerov1api.Backup, ) error { for pvcName, vs := range vsMap { @@ -1085,7 +1087,7 @@ func (p *pvcBackupItemAction) updateVGSCreatedVS( return nil } -func (p *pvcBackupItemAction) patchVGSCDeletionPolicy(ctx context.Context, vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot) error { +func (p *pvcBackupItemAction) patchVGSCDeletionPolicy(ctx context.Context, vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot) error { if vgs == nil || vgs.Status == nil || vgs.Status.BoundVolumeGroupSnapshotContentName == nil { return errors.New("VolumeGroupSnapshotContent name not found in VGS status") } @@ -1093,7 +1095,7 @@ func (p *pvcBackupItemAction) patchVGSCDeletionPolicy(ctx context.Context, vgs * vgscName := vgs.Status.BoundVolumeGroupSnapshotContentName return retry.RetryOnConflict(retry.DefaultBackoff, func() error { - vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + vgsc := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} if err := p.crClient.Get(ctx, crclient.ObjectKey{Name: *vgscName}, vgsc); err != nil { return errors.Wrapf(err, "failed to get VolumeGroupSnapshotContent %s for VolumeGroupSnapshot %s/%s", *vgscName, vgs.Namespace, vgs.Name) } @@ -1112,9 +1114,9 @@ func (p *pvcBackupItemAction) patchVGSCDeletionPolicy(ctx context.Context, vgs * }) } -func (p *pvcBackupItemAction) deleteVGSAndVGSC(ctx context.Context, vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot) error { +func (p *pvcBackupItemAction) deleteVGSAndVGSC(ctx context.Context, vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot) error { if vgs.Status != nil && vgs.Status.BoundVolumeGroupSnapshotContentName != nil { - vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + vgsc := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ ObjectMeta: metav1.ObjectMeta{ Name: *vgs.Status.BoundVolumeGroupSnapshotContentName, }, @@ -1139,11 +1141,11 @@ func (p *pvcBackupItemAction) deleteVGSAndVGSC(ctx context.Context, vgs *volumeg func (p *pvcBackupItemAction) waitForVGSCBinding( ctx context.Context, - vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot, + vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot, timeout time.Duration, ) error { return wait.PollUntilContextTimeout(ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { - vgsRef := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{} + vgsRef := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{} if err := p.crClient.Get(ctx, crclient.ObjectKeyFromObject(vgs), vgsRef); err != nil { return false, err } @@ -1156,8 +1158,8 @@ func (p *pvcBackupItemAction) waitForVGSCBinding( }) } -func (p *pvcBackupItemAction) getVGSByLabels(ctx context.Context, namespace string, labels map[string]string) (*volumegroupsnapshotv1beta1.VolumeGroupSnapshot, error) { - vgsList := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotList{} +func (p *pvcBackupItemAction) getVGSByLabels(ctx context.Context, namespace string, labels map[string]string) (*volumegroupsnapshotv1beta2.VolumeGroupSnapshot, error) { + vgsList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotList{} if err := p.crClient.List(ctx, vgsList, crclient.InNamespace(namespace), crclient.MatchingLabels(labels), diff --git a/pkg/backup/actions/csi/pvc_action_test.go b/pkg/backup/actions/csi/pvc_action_test.go index efcb0b0abf..9ffe20be51 100644 --- a/pkg/backup/actions/csi/pvc_action_test.go +++ b/pkg/backup/actions/csi/pvc_action_test.go @@ -25,7 +25,7 @@ import ( "github.com/vmware-tanzu/velero/pkg/kuberesource" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" "github.com/stretchr/testify/assert" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" @@ -307,6 +307,28 @@ func TestProgress(t *testing.T) { operationID: "testing", expectedErr: "not found DataUpload for operationID testing", }, + { + name: "DataUpload in different namespace is not found", + backup: builder.ForBackup("velero", "test").Result(), + dataUpload: &velerov2alpha1.DataUpload{ + TypeMeta: metav1.TypeMeta{ + Kind: "DataUpload", + APIVersion: "v2alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "other-namespace", + Name: "testing", + Labels: map[string]string{ + velerov1api.AsyncOperationIDLabel: "testing", + }, + }, + Status: velerov2alpha1.DataUploadStatus{ + Phase: velerov2alpha1.DataUploadPhaseFailed, + }, + }, + operationID: "testing", + expectedErr: "not found DataUpload for operationID testing", + }, { name: "DataUpload is found", backup: builder.ForBackup("velero", "test").Result(), @@ -375,15 +397,15 @@ func TestCancel(t *testing.T) { tests := []struct { name string backup *velerov1api.Backup - dataUpload velerov2alpha1.DataUpload + dataUpload *velerov2alpha1.DataUpload operationID string - expectedErr error + expectedErr string expectedDataUpload velerov2alpha1.DataUpload }{ { name: "Cancel DataUpload", backup: builder.ForBackup("velero", "test").Result(), - dataUpload: velerov2alpha1.DataUpload{ + dataUpload: &velerov2alpha1.DataUpload{ TypeMeta: metav1.TypeMeta{ Kind: "DataUpload", APIVersion: velerov2alpha1.SchemeGroupVersion.String(), @@ -414,6 +436,31 @@ func TestCancel(t *testing.T) { }, }, }, + { + name: "DataUpload cannot be found", + backup: builder.ForBackup("velero", "test").Result(), + operationID: "testing", + expectedErr: "not found DataUpload for operationID testing", + }, + { + name: "DataUpload in different namespace is not found", + backup: builder.ForBackup("velero", "test").Result(), + dataUpload: &velerov2alpha1.DataUpload{ + TypeMeta: metav1.TypeMeta{ + Kind: "DataUpload", + APIVersion: velerov2alpha1.SchemeGroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "other-namespace", + Name: "testing", + Labels: map[string]string{ + velerov1api.AsyncOperationIDLabel: "testing", + }, + }, + }, + operationID: "testing", + expectedErr: "not found DataUpload for operationID testing", + }, } for _, tc := range tests { @@ -426,17 +473,23 @@ func TestCancel(t *testing.T) { crClient: crClient, } - err := crClient.Create(t.Context(), &tc.dataUpload) - require.NoError(t, err) + if tc.dataUpload != nil { + err := crClient.Create(t.Context(), tc.dataUpload) + require.NoError(t, err) + } - err = pvcBIA.Cancel(tc.operationID, tc.backup) - require.NoError(t, err) + err := pvcBIA.Cancel(tc.operationID, tc.backup) + if tc.expectedErr != "" { + require.EqualError(t, err, tc.expectedErr) + } else { + require.NoError(t, err) - du := new(velerov2alpha1.DataUpload) - err = crClient.Get(t.Context(), crclient.ObjectKey{Namespace: tc.dataUpload.Namespace, Name: tc.dataUpload.Name}, du) - require.NoError(t, err) + du := new(velerov2alpha1.DataUpload) + err = crClient.Get(t.Context(), crclient.ObjectKey{Namespace: tc.dataUpload.Namespace, Name: tc.dataUpload.Name}, du) + require.NoError(t, err) - require.True(t, cmp.Equal(tc.expectedDataUpload, *du, cmpopts.IgnoreFields(velerov2alpha1.DataUpload{}, "ResourceVersion"))) + require.True(t, cmp.Equal(tc.expectedDataUpload, *du, cmpopts.IgnoreFields(velerov2alpha1.DataUpload{}, "ResourceVersion"))) + } }) } } @@ -1121,7 +1174,7 @@ func TestDetermineVGSClass(t *testing.T) { name string backup *velerov1api.Backup pvc *corev1api.PersistentVolumeClaim - existingVGSClass []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass + existingVGSClass []volumegroupsnapshotv1beta2.VolumeGroupSnapshotClass expectError bool expectResult string }{ @@ -1153,7 +1206,7 @@ func TestDetermineVGSClass(t *testing.T) { name: "Default label-based match", pvc: &corev1api.PersistentVolumeClaim{}, backup: &velerov1api.Backup{}, - existingVGSClass: []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass{ + existingVGSClass: []volumegroupsnapshotv1beta2.VolumeGroupSnapshotClass{ { ObjectMeta: metav1.ObjectMeta{ Name: "default-class", @@ -1174,7 +1227,7 @@ func TestDetermineVGSClass(t *testing.T) { name: "Multiple matching VGS classes", pvc: &corev1api.PersistentVolumeClaim{}, backup: &velerov1api.Backup{}, - existingVGSClass: []volumegroupsnapshotv1beta1.VolumeGroupSnapshotClass{ + existingVGSClass: []volumegroupsnapshotv1beta2.VolumeGroupSnapshotClass{ { ObjectMeta: metav1.ObjectMeta{ Name: "class1", @@ -1204,7 +1257,7 @@ func TestDetermineVGSClass(t *testing.T) { client := velerotest.NewFakeControllerRuntimeClient(t, initObjs...) logger := logrus.New() - require.NoError(t, volumegroupsnapshotv1beta1.AddToScheme(client.Scheme())) + require.NoError(t, volumegroupsnapshotv1beta2.AddToScheme(client.Scheme())) action := &pvcBackupItemAction{crClient: client, log: logger} @@ -1263,13 +1316,13 @@ func TestCreateVolumeGroupSnapshot(t *testing.T) { assert.Equal(t, string(testBackup.UID), vgs.Labels[velerov1api.BackupUIDLabel]) // Check that it exists in fake client - retrieved := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{} + retrieved := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{} err = crClient.Get(t.Context(), crclient.ObjectKey{Name: vgs.Name, Namespace: vgs.Namespace}, retrieved) require.NoError(t, err) } func TestWaitForVGSAssociatedVS(t *testing.T) { - vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{ + vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: "test-vgs", Namespace: "test-ns", @@ -1282,7 +1335,7 @@ func TestWaitForVGSAssociatedVS(t *testing.T) { if owned { refs = []metav1.OwnerReference{ { - APIVersion: "groupsnapshot.storage.k8s.io/v1beta1", + APIVersion: "groupsnapshot.storage.k8s.io/v1beta2", Kind: "VolumeGroupSnapshot", Name: vgs.Name, UID: vgs.UID, @@ -1429,7 +1482,7 @@ func TestUpdateVGSCreatedVS(t *testing.T) { }, } - vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{ + vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: "test-vgs", Namespace: "ns", @@ -1442,7 +1495,7 @@ func TestUpdateVGSCreatedVS(t *testing.T) { if withVGSOwner { refs = []metav1.OwnerReference{ { - APIVersion: "groupsnapshot.storage.k8s.io/v1beta1", + APIVersion: "groupsnapshot.storage.k8s.io/v1beta2", Kind: "VolumeGroupSnapshot", Name: vgs.Name, UID: vgs.UID, @@ -1561,18 +1614,18 @@ func TestPatchVGSCDeletionPolicy(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + vgsc := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ ObjectMeta: metav1.ObjectMeta{Name: "test-vgsc"}, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ DeletionPolicy: tt.initialPolicy, }, } - vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{ + vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: "test-vgs", Namespace: "ns", }, - Status: &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{ + Status: &volumegroupsnapshotv1beta2.VolumeGroupSnapshotStatus{ BoundVolumeGroupSnapshotContentName: pointer.String("test-vgsc"), }, } @@ -1590,7 +1643,7 @@ func TestPatchVGSCDeletionPolicy(t *testing.T) { } require.NoError(t, err) - updated := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + updated := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgsc"}, updated) require.NoError(t, err) require.Equal(t, tt.expectedPolicy, updated.Spec.DeletionPolicy) @@ -1599,20 +1652,20 @@ func TestPatchVGSCDeletionPolicy(t *testing.T) { } func TestDeleteVGSAndVGSC(t *testing.T) { - makeVGS := func(name, namespace string, boundVGSCName *string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot { - return &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{ + makeVGS := func(name, namespace string, boundVGSCName *string) *volumegroupsnapshotv1beta2.VolumeGroupSnapshot { + return &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, - Status: &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{ + Status: &volumegroupsnapshotv1beta2.VolumeGroupSnapshotStatus{ BoundVolumeGroupSnapshotContentName: boundVGSCName, }, } } - makeVGSC := func(name string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent { - return &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + makeVGSC := func(name string) *volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent { + return &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ ObjectMeta: metav1.ObjectMeta{ Name: name, }, @@ -1621,8 +1674,8 @@ func TestDeleteVGSAndVGSC(t *testing.T) { tests := []struct { name string - vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot - existingVGSC *volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent + vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot + existingVGSC *volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent expectVGSCDelete bool expectVGSDelete bool }{ @@ -1668,13 +1721,13 @@ func TestDeleteVGSAndVGSC(t *testing.T) { // Check VGSC is deleted if tt.expectVGSCDelete { - got := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + got := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgsc"}, got) assert.True(t, apierrors.IsNotFound(err), "expected VGSC to be deleted") } // Check VGS is deleted - gotVGS := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{} + gotVGS := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{} err = client.Get(t.Context(), crclient.ObjectKey{Name: "test-vgs", Namespace: "ns"}, gotVGS) assert.True(t, apierrors.IsNotFound(err), "expected VGS to be deleted") }) @@ -1769,8 +1822,8 @@ func TestFindExistingVSForBackup(t *testing.T) { } func TestWaitForVGSCBinding(t *testing.T) { - makeVGS := func(name string, withStatus bool) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot { - vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{ + makeVGS := func(name string, withStatus bool) *volumegroupsnapshotv1beta2.VolumeGroupSnapshot { + vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "ns", @@ -1778,7 +1831,7 @@ func TestWaitForVGSCBinding(t *testing.T) { } if withStatus { contentName := "vgsc-123" - vgs.Status = &volumegroupsnapshotv1beta1.VolumeGroupSnapshotStatus{ + vgs.Status = &volumegroupsnapshotv1beta2.VolumeGroupSnapshotStatus{ BoundVolumeGroupSnapshotContentName: &contentName, } } @@ -1787,7 +1840,7 @@ func TestWaitForVGSCBinding(t *testing.T) { tests := []struct { name string - vgs *volumegroupsnapshotv1beta1.VolumeGroupSnapshot + vgs *volumegroupsnapshotv1beta2.VolumeGroupSnapshot expectErr bool }{ { @@ -1830,8 +1883,8 @@ func TestGetVGSByLabels(t *testing.T) { labelVal := "backup-123" testLabels := map[string]string{labelKey: labelVal} - makeVGS := func(name string, labels map[string]string) *volumegroupsnapshotv1beta1.VolumeGroupSnapshot { - return &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{ + makeVGS := func(name string, labels map[string]string) *volumegroupsnapshotv1beta2.VolumeGroupSnapshot { + return &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "test-ns", @@ -1916,7 +1969,7 @@ func (f *failingClient) List(ctx context.Context, list crclient.ObjectList, opts } func TestHasOwnerReference(t *testing.T) { - vgs := &volumegroupsnapshotv1beta1.VolumeGroupSnapshot{ + vgs := &volumegroupsnapshotv1beta2.VolumeGroupSnapshot{ ObjectMeta: metav1.ObjectMeta{ Name: "test-vgs", Namespace: "test-ns", @@ -1933,7 +1986,7 @@ func TestHasOwnerReference(t *testing.T) { name: "match kind, apiversion, uid", ownerRef: metav1.OwnerReference{ Kind: kuberesource.VGSKind, - APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version, + APIVersion: volumegroupsnapshotv1beta2.GroupName + "/" + volumegroupsnapshotv1beta2.SchemeGroupVersion.Version, UID: vgs.UID, }, expect: true, @@ -1942,7 +1995,7 @@ func TestHasOwnerReference(t *testing.T) { name: "mismatch kind", ownerRef: metav1.OwnerReference{ Kind: "other-kind", - APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version, + APIVersion: volumegroupsnapshotv1beta2.GroupName + "/" + volumegroupsnapshotv1beta2.SchemeGroupVersion.Version, UID: vgs.UID, }, expect: false, @@ -1960,7 +2013,7 @@ func TestHasOwnerReference(t *testing.T) { name: "mismatch uid", ownerRef: metav1.OwnerReference{ Kind: kuberesource.VGSKind, - APIVersion: volumegroupsnapshotv1beta1.GroupName + "/" + volumegroupsnapshotv1beta1.SchemeGroupVersion.Version, + APIVersion: volumegroupsnapshotv1beta2.GroupName + "/" + volumegroupsnapshotv1beta2.SchemeGroupVersion.Version, UID: "wrong-uid", }, expect: false, diff --git a/pkg/backup/backup.go b/pkg/backup/backup.go index 1a1c54247f..9edaf6a855 100644 --- a/pkg/backup/backup.go +++ b/pkg/backup/backup.go @@ -167,15 +167,15 @@ func NewKubernetesBackupper( }, nil } -// getNamespaceIncludesExcludesAndArgoCDNamespaces returns an IncludesExcludes list containing which namespaces to -// include and exclude from the backup and a list of namespaces managed by ArgoCD. -func getNamespaceIncludesExcludesAndArgoCDNamespaces(backup *velerov1api.Backup, kbClient kbclient.Client) (*collections.NamespaceIncludesExcludes, []string, error) { +// getNamespaceIncludesExcludes returns an IncludesExcludes list containing which namespaces to +// include and exclude from the backup. +func getNamespaceIncludesExcludes(backup *velerov1api.Backup, kbClient kbclient.Client) (*collections.NamespaceIncludesExcludes, error) { nsList := corev1api.NamespaceList{} - activeNamespaces := []string{} - nsManagedByArgoCD := []string{} if err := kbClient.List(context.Background(), &nsList); err != nil { - return nil, nsManagedByArgoCD, err + return nil, err } + + activeNamespaces := []string{} for _, ns := range nsList.Items { activeNamespaces = append(activeNamespaces, ns.Name) } @@ -188,10 +188,20 @@ func getNamespaceIncludesExcludesAndArgoCDNamespaces(backup *velerov1api.Backup, // Expand wildcards if needed if err := includesExcludes.ExpandIncludesExcludes(); err != nil { - return nil, []string{}, err + return nil, err } - // Check for ArgoCD managed namespaces in the namespaces that will be included + return includesExcludes, nil +} + +// getArgoCDManagedNamespaces returns a list of namespaces managed by ArgoCD that should be included in the backup. +func getArgoCDManagedNamespaces(kbClient kbclient.Client, includesExcludes *collections.NamespaceIncludesExcludes) ([]string, error) { + nsList := corev1api.NamespaceList{} + if err := kbClient.List(context.Background(), &nsList); err != nil { + return nil, err + } + + nsManagedByArgoCD := []string{} for _, ns := range nsList.Items { nsLabels := ns.GetLabels() if len(nsLabels[ArgoCDManagedByNamespaceLabel]) > 0 && includesExcludes.ShouldInclude(ns.Name) { @@ -199,7 +209,7 @@ func getNamespaceIncludesExcludesAndArgoCDNamespaces(backup *velerov1api.Backup, } } - return includesExcludes, nsManagedByArgoCD, nil + return nsManagedByArgoCD, nil } func getResourceHooks(hookSpecs []velerov1api.BackupResourceHookSpec, discoveryHelper discovery.Helper) ([]hook.ResourceHook, error) { @@ -274,13 +284,18 @@ func (kb *kubernetesBackupper) BackupWithResolvers( return errors.WithStack(err) } var err error - var nsManagedByArgoCD []string - backupRequest.NamespaceIncludesExcludes, nsManagedByArgoCD, err = getNamespaceIncludesExcludesAndArgoCDNamespaces(backupRequest.Backup, kb.kbClient) + backupRequest.NamespaceIncludesExcludes, err = getNamespaceIncludesExcludes(backupRequest.Backup, kb.kbClient) if err != nil { log.WithError(err).Errorf("error getting namespace includes/excludes") return err } + nsManagedByArgoCD, err := getArgoCDManagedNamespaces(kb.kbClient, backupRequest.NamespaceIncludesExcludes) + if err != nil { + log.WithError(err).Errorf("error getting ArgoCD managed namespaces") + return err + } + if backupRequest.NamespaceIncludesExcludes.IsWildcardExpanded() { expandedIncludes := backupRequest.NamespaceIncludesExcludes.GetIncludes() expandedExcludes := backupRequest.NamespaceIncludesExcludes.GetExcludes() @@ -292,6 +307,10 @@ func (kb *kubernetesBackupper) BackupWithResolvers( return err } + if len(wildcardResult) == 0 { + log.Warnf("no namespaces matched the resolution of wildcard patterns ") + } + log.WithFields(logrus.Fields{ "expandedIncludes": expandedIncludes, "expandedExcludes": expandedExcludes, diff --git a/pkg/cbtservice/service.go b/pkg/cbtservice/service.go new file mode 100644 index 0000000000..96e43b2c47 --- /dev/null +++ b/pkg/cbtservice/service.go @@ -0,0 +1,46 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cbtservice + +import "context" + +// Range defines the range of a change +type Range struct { + Offset int64 + Length int64 +} + +// SourceInfo is the information provided to the uploader, the uploader calls CBT service with this information +type SourceInfo struct { + // Snapshot is the identifier of the current snapshot + Snapshot string + + // ChangeID is the identifier associated to the current snapshot that is used as changeID for following backups + ChangeID string + + // VolumeID is the identifier uniquely identifier a volume in the storage to which the CBT is associated + VolumeID string +} + +// Service defines the methods for CBT service which could be implemented by Kubernetes SnapshotMetadataService or other customized services +type Service interface { + // GetAllocatedBlocks enumerates the allocated blocks of the snapshot and call the record callback + GetAllocatedBlocks(ctx context.Context, snapshot string, record func([]Range) error) error + + // GetChangedBlocks enumerates the changed blocks of the snapshot since PIT of changeID and call the record callback + GetChangedBlocks(ctx context.Context, snapshot string, changeID string, record func([]Range) error) error +} diff --git a/pkg/client/factory.go b/pkg/client/factory.go index 4b3c8941e8..51dfb62c3c 100644 --- a/pkg/client/factory.go +++ b/pkg/client/factory.go @@ -19,7 +19,7 @@ package client import ( "os" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1" @@ -168,7 +168,7 @@ func (f *factory) KubebuilderClient() (kbclient.Client, error) { if err := snapshotv1api.AddToScheme(scheme); err != nil { return nil, err } - if err := volumegroupsnapshotv1beta1.AddToScheme(scheme); err != nil { + if err := volumegroupsnapshotv1beta2.AddToScheme(scheme); err != nil { return nil, err } kubebuilderClient, err := kbclient.New(clientConfig, kbclient.Options{ @@ -207,7 +207,7 @@ func (f *factory) KubebuilderWatchClient() (kbclient.WithWatch, error) { if err := snapshotv1api.AddToScheme(scheme); err != nil { return nil, err } - if err := volumegroupsnapshotv1beta1.AddToScheme(scheme); err != nil { + if err := volumegroupsnapshotv1beta2.AddToScheme(scheme); err != nil { return nil, err } kubebuilderWatchClient, err := kbclient.NewWithWatch(clientConfig, kbclient.Options{ diff --git a/pkg/cmd/cli/nodeagent/server.go b/pkg/cmd/cli/nodeagent/server.go index 7e7c86e6c0..bb1764cb32 100644 --- a/pkg/cmd/cli/nodeagent/server.go +++ b/pkg/cmd/cli/nodeagent/server.go @@ -37,7 +37,6 @@ import ( "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/kubernetes" cacheutil "k8s.io/client-go/tools/cache" @@ -430,10 +429,6 @@ func (s *nodeAgentServer) run() { s.logger.WithError(err).Fatal("Unable to create the pod volume restore controller") } - if err := controller.InitLegacyPodVolumeRestoreReconciler(s.mgr.GetClient(), s.mgr, s.kubeClient, s.dataPathMgr, s.namespace, s.config.resourceTimeout, s.logger); err != nil { - s.logger.WithError(err).Fatal("Unable to create the legacy pod volume restore controller") - } - dataUploadReconciler := controller.NewDataUploadReconciler( s.mgr.GetClient(), s.mgr, @@ -509,8 +504,6 @@ func (s *nodeAgentServer) run() { if err := pvrReconciler.AttemptPVRResume(s.ctx, s.logger.WithField("node", s.nodeName), s.namespace); err != nil { s.logger.WithError(errors.WithStack(err)).Error("Failed to attempt PVR resume") } - - s.markLegacyPVRsFailed(s.mgr.GetClient()) }() s.logger.Info("Controllers starting...") @@ -604,47 +597,6 @@ func (s *nodeAgentServer) validatePodVolumesHostPath(client kubernetes.Interface return nil } -func (s *nodeAgentServer) markLegacyPVRsFailed(client ctrlclient.Client) { - pvrs := &velerov1api.PodVolumeRestoreList{} - if err := client.List(s.ctx, pvrs, &ctrlclient.ListOptions{Namespace: s.namespace}); err != nil { - s.logger.WithError(errors.WithStack(err)).Error("failed to list podvolumerestores") - return - } - - for i, pvr := range pvrs.Items { - if !controller.IsLegacyPVR(&pvr) { - continue - } - - if pvr.Status.Phase != velerov1api.PodVolumeRestorePhaseInProgress { - s.logger.Debugf("the status of podvolumerestore %q is %q, skip", pvr.GetName(), pvr.Status.Phase) - continue - } - - pod := &corev1api.Pod{} - if err := client.Get(s.ctx, types.NamespacedName{ - Namespace: pvr.Spec.Pod.Namespace, - Name: pvr.Spec.Pod.Name, - }, pod); err != nil { - s.logger.WithError(errors.WithStack(err)).Errorf("failed to get pod \"%s/%s\" of podvolumerestore %q", - pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name, pvr.GetName()) - continue - } - if pod.Spec.NodeName != s.nodeName { - s.logger.Debugf("the node of pod referenced by podvolumerestore %q is %q, not %q, skip", pvr.GetName(), pod.Spec.NodeName, s.nodeName) - continue - } - - if err := controller.UpdatePVRStatusToFailed(s.ctx, client, &pvrs.Items[i], errors.New("cannot survive from node-agent restart"), - fmt.Sprintf("get a legacy podvolumerestore with status %q during the server starting, mark it as %q", velerov1api.PodVolumeRestorePhaseInProgress, velerov1api.PodVolumeRestorePhaseFailed), - time.Now(), s.logger); err != nil { - s.logger.WithError(errors.WithStack(err)).Errorf("failed to patch podvolumerestore %q", pvr.GetName()) - continue - } - s.logger.WithField("podvolumerestore", pvr.GetName()).Warn(pvr.Status.Message) - } -} - var getConfigsFunc = nodeagent.GetConfigs func (s *nodeAgentServer) getDataPathConfigs() error { diff --git a/pkg/cmd/server/server.go b/pkg/cmd/server/server.go index e744013e85..44c88b9800 100644 --- a/pkg/cmd/server/server.go +++ b/pkg/cmd/server/server.go @@ -27,7 +27,7 @@ import ( "time" logrusr "github.com/bombsimon/logrusr/v3" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/pkg/errors" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -247,7 +247,7 @@ func newServer(f client.Factory, config *config.Config, logger *logrus.Logger) ( cancelFunc() return nil, err } - if err := volumegroupsnapshotv1beta1.AddToScheme(scheme); err != nil { + if err := volumegroupsnapshotv1beta2.AddToScheme(scheme); err != nil { cancelFunc() return nil, err } @@ -1164,8 +1164,8 @@ func markPodVolumeRestoresCancel(ctx context.Context, client ctrlclient.Client, for i := range pvrs.Items { pvr := pvrs.Items[i] - if controller.IsLegacyPVR(&pvr) { - log.WithField("PVR", pvr.GetName()).Warn("Found a legacy PVR during velero server restart, cannot stop it") + if _, err := uploader.ValidateUploaderType(pvr.Spec.UploaderType); err != nil { + log.WithField("PVR", pvr.Name).Warnf("invalid uploader type %s, skip marking cancel for this PVR", pvr.Spec.UploaderType) continue } diff --git a/pkg/cmd/velero/velero.go b/pkg/cmd/velero/velero.go index a74c68fbcb..42f07b03a3 100644 --- a/pkg/cmd/velero/velero.go +++ b/pkg/cmd/velero/velero.go @@ -132,6 +132,11 @@ operations can also be performed as 'velero backup get' and 'velero schedule cre // init and add the klog flags klog.InitFlags(flag.CommandLine) + // Opt into the new klog behavior so that -stderrthreshold is honored even + // when -logtostderr=true (the default). + // Ref: kubernetes/klog#212, kubernetes/klog#432 + flag.CommandLine.Set("legacy_stderr_threshold_behavior", "false") //nolint:errcheck // flag is registered by klog.InitFlags above + flag.CommandLine.Set("stderrthreshold", "INFO") //nolint:errcheck // flag is registered by klog.InitFlags above c.PersistentFlags().AddGoFlagSet(flag.CommandLine) return c diff --git a/pkg/controller/backup_controller.go b/pkg/controller/backup_controller.go index 496308a6e0..496875bbfe 100644 --- a/pkg/controller/backup_controller.go +++ b/pkg/controller/backup_controller.go @@ -570,6 +570,13 @@ func (b *backupReconciler) prepareBackupRequest(ctx context.Context, backup *vel } } + // Empty IncludedNamespaces means "include all namespaces". Normalize + // to ["*"] so that downstream wildcard expansion does not collapse + // an empty-includes + wildcard-excludes combination into "back up nothing". + if len(request.Spec.IncludedNamespaces) == 0 { + request.Spec.IncludedNamespaces = []string{"*"} + } + // validate the included/excluded namespaces for _, err := range collections.ValidateNamespaceIncludesExcludes(request.Spec.IncludedNamespaces, request.Spec.ExcludedNamespaces) { request.Status.ValidationErrors = append(request.Status.ValidationErrors, fmt.Sprintf("Invalid included/excluded namespace lists: %v", err)) diff --git a/pkg/controller/backup_controller_test.go b/pkg/controller/backup_controller_test.go index 3864989002..c65f1d15d2 100644 --- a/pkg/controller/backup_controller_test.go +++ b/pkg/controller/backup_controller_test.go @@ -320,6 +320,34 @@ func TestBackupLocationLabel(t *testing.T) { } } +func TestPrepareBackupRequest_EmptyIncludedNamespacesNormalizedToWildcard(t *testing.T) { + formatFlag := logging.FormatText + logger := logging.DefaultLogger(logrus.DebugLevel, formatFlag) + + apiServer := velerotest.NewAPIServer(t) + discoveryHelper, err := discovery.NewHelper(apiServer.DiscoveryClient, logger) + require.NoError(t, err) + + backupLocation := builder.ForBackupStorageLocation("velero", "loc-1").Result() + fakeClient := velerotest.NewFakeControllerRuntimeClient(t, backupLocation) + + c := &backupReconciler{ + discoveryHelper: discoveryHelper, + kbClient: fakeClient, + defaultBackupLocation: backupLocation.Name, + clock: &clock.RealClock{}, + formatFlag: formatFlag, + } + + backup := defaultBackup().Result() + backup.Spec.IncludedNamespaces = nil + + res := c.prepareBackupRequest(ctx, backup, logger) + defer res.WorkerPool.Stop() + + assert.Equal(t, []string{"*"}, res.Spec.IncludedNamespaces) +} + func Test_prepareBackupRequest_BackupStorageLocation(t *testing.T) { var ( defaultBackupTTL = metav1.Duration{Duration: 24 * 30 * time.Hour} @@ -709,6 +737,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.True(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -748,6 +777,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: "alt-loc", + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -791,6 +821,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: "read-write", + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.True(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -831,6 +862,7 @@ func TestProcessBackupCompletions(t *testing.T) { Spec: velerov1api.BackupSpec{ TTL: metav1.Duration{Duration: 10 * time.Minute}, StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -871,6 +903,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.True(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -912,6 +945,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -953,6 +987,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.True(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -994,6 +1029,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.True(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1035,6 +1071,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1077,6 +1114,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.True(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1119,6 +1157,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.True(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1161,6 +1200,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.True(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1204,6 +1244,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1247,6 +1288,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1290,6 +1332,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.True(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1334,6 +1377,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.False(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1377,6 +1421,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.True(), ExcludedClusterScopedResources: autoExcludeClusterScopedResources, @@ -1424,6 +1469,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.True(), IncludedClusterScopedResources: []string{"storageclasses"}, @@ -1473,6 +1519,7 @@ func TestProcessBackupCompletions(t *testing.T) { }, Spec: velerov1api.BackupSpec{ StorageLocation: defaultBackupLocation.Name, + IncludedNamespaces: []string{"*"}, DefaultVolumesToFsBackup: boolptr.False(), SnapshotMoveData: boolptr.True(), IncludedClusterScopedResources: []string{"storageclasses"}, diff --git a/pkg/controller/backup_deletion_controller.go b/pkg/controller/backup_deletion_controller.go index 5a791999c8..5fe29c5f11 100644 --- a/pkg/controller/backup_deletion_controller.go +++ b/pkg/controller/backup_deletion_controller.go @@ -20,6 +20,7 @@ import ( "context" "encoding/json" "fmt" + "strings" "time" jsonpatch "github.com/evanphx/json-patch/v5" @@ -267,8 +268,17 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque if err != nil { log.WithError(err).Errorf("Unable to download tarball for backup %s, skipping associated DeleteItemAction plugins", backup.Name) + // for backups which failed before tarball object could be uploaded we do offline cleanup log.Info("Cleaning up CSI volumesnapshots") r.deleteCSIVolumeSnapshotsIfAny(ctx, backup, log) + + // If the tarball simply does not exist (HTTP 404 / not found), the download + // failure is permanent and not retryable, so we let deletion proceed. + // For transient errors (throttling, auth failures, network issues), record + // the error to fail the deletion so it can be retried later. + if !isTarballNotFoundError(err) { + errs = append(errs, errors.Wrapf(err, "error downloading backup tarball, CSI snapshot cleanup was skipped").Error()) + } } else { defer closeAndRemoveFile(backupFile, r.logger) deleteCtx := &delete.Context{ @@ -351,11 +361,13 @@ func (r *backupDeletionReconciler) Reconcile(ctx context.Context, req ctrl.Reque } } - if backupStore != nil { + if backupStore != nil && len(errs) == 0 { log.Info("Removing backup from backup storage") if err := backupStore.DeleteBackup(backup.Name); err != nil { errs = append(errs, err.Error()) } + } else if len(errs) > 0 { + log.Info("Skipping removal of backup from backup storage due to previous errors") } log.Info("Removing restores") @@ -691,3 +703,28 @@ func batchDeleteSnapshots(ctx context.Context, repoEnsurer *repository.Ensurer, return errs } + +// isTarballNotFoundError reports whether err indicates that the backup tarball +// does not exist in object storage (e.g. HTTP 404 / not-found). Such errors are +// permanent and not retryable, so callers should let deletion proceed (skipping +// DeleteItemAction plugins) rather than failing the entire deletion. +// +// Transient errors (throttling, auth failures, network timeouts) return false so +// the deletion is failed and can be retried once the storage is reachable again. +func isTarballNotFoundError(err error) bool { + if err == nil { + return false + } + // Lower-case once for all comparisons. + msg := strings.ToLower(err.Error()) + // Common "not found" indicators across cloud providers: + // - "not found" / "does not exist": generic, in-memory object store + // - "nosuchkey": AWS S3 + // - "blobnotfound": Azure Blob Storage + // - "objectnotexist": GCS + return strings.Contains(msg, "not found") || + strings.Contains(msg, "does not exist") || + strings.Contains(msg, "nosuchkey") || + strings.Contains(msg, "blobnotfound") || + strings.Contains(msg, "objectnotexist") +} diff --git a/pkg/controller/backup_deletion_controller_test.go b/pkg/controller/backup_deletion_controller_test.go index 58d9b04207..24cc658464 100644 --- a/pkg/controller/backup_deletion_controller_test.go +++ b/pkg/controller/backup_deletion_controller_test.go @@ -25,8 +25,6 @@ import ( "reflect" "time" - snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" - "context" "github.com/sirupsen/logrus" @@ -606,7 +604,7 @@ func TestBackupDeletionControllerReconcile(t *testing.T) { // Make sure snapshot was deleted assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) }) - t.Run("backup is still deleted if downloading tarball fails for DeleteItemAction plugins", func(t *testing.T) { + t.Run("backup deletion fails with error when downloading tarball fails for DeleteItemAction plugins", func(t *testing.T) { backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").Result() backup.UID = "uid" backup.Spec.StorageLocation = "primary" @@ -672,38 +670,108 @@ func TestBackupDeletionControllerReconcile(t *testing.T) { td.backupStore.On("GetBackupVolumeSnapshots", input.Spec.BackupName).Return(snapshots, nil) td.backupStore.On("GetBackupContents", input.Spec.BackupName).Return(nil, fmt.Errorf("error downloading tarball")) - td.backupStore.On("DeleteBackup", input.Spec.BackupName).Return(nil) _, err := td.controller.Reconcile(t.Context(), td.req) require.NoError(t, err) td.backupStore.AssertCalled(t, "GetBackupContents", input.Spec.BackupName) - td.backupStore.AssertCalled(t, "DeleteBackup", input.Spec.BackupName) + // DeleteBackup (removing backup data from object storage) must NOT be called + // when there are errors, so that the deletion can be retried later. + td.backupStore.AssertNotCalled(t, "DeleteBackup", input.Spec.BackupName) - // the dbr should be deleted + // the dbr should still exist and be marked Processed with errors res := &velerov1api.DeleteBackupRequest{} err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) - assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) - if err == nil { - t.Logf("status of the dbr: %s, errors in dbr: %v", res.Status.Phase, res.Status.Errors) - } + require.NoError(t, err, "Expected DBR to still exist after tarball download failure") + assert.Equal(t, velerov1api.DeleteBackupRequestPhaseProcessed, res.Status.Phase) + require.Len(t, res.Status.Errors, 1) + assert.Contains(t, res.Status.Errors[0], "error downloading backup tarball, CSI snapshot cleanup was skipped") - // backup CR should be deleted + // backup CR should NOT be deleted err = td.fakeClient.Get(t.Context(), types.NamespacedName{ Namespace: velerov1api.DefaultNamespace, Name: backup.Name, }, &velerov1api.Backup{}) - assert.True(t, apierrors.IsNotFound(err), "Expected not found error, but actual value of error: %v", err) + require.NoError(t, err, "Expected backup CR to still exist after tarball download failure") + }) + t.Run("backup is still deleted if downloading tarball returns a not-found error", func(t *testing.T) { + backup := builder.ForBackup(velerov1api.DefaultNamespace, "foo").Result() + backup.UID = "uid" + backup.Spec.StorageLocation = "primary" - // leaked CSI snapshot should be deleted - err = td.fakeClient.Get(t.Context(), types.NamespacedName{ - Namespace: "user-ns", - Name: "vs-1", - }, &snapshotv1api.VolumeSnapshot{}) - assert.True(t, apierrors.IsNotFound(err), "Expected not found error for the leaked CSI snapshot, but actual value of error: %v", err) + input := defaultTestDbr() + input.Labels = nil - // Make sure snapshot was deleted - assert.Equal(t, 0, td.volumeSnapshotter.SnapshotsTaken.Len()) + location := &velerov1api.BackupStorageLocation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: backup.Namespace, + Name: backup.Spec.StorageLocation, + }, + Spec: velerov1api.BackupStorageLocationSpec{ + Provider: "objStoreProvider", + StorageType: velerov1api.StorageType{ + ObjectStorage: &velerov1api.ObjectStorageLocation{ + Bucket: "bucket", + }, + }, + }, + Status: velerov1api.BackupStorageLocationStatus{ + Phase: velerov1api.BackupStorageLocationPhaseAvailable, + }, + } + + snapshotLocation := &velerov1api.VolumeSnapshotLocation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: backup.Namespace, + Name: "vsl-1", + }, + Spec: velerov1api.VolumeSnapshotLocationSpec{ + Provider: "provider-1", + }, + } + + td := setupBackupDeletionControllerTest(t, defaultTestDbr(), backup, location, snapshotLocation) + td.volumeSnapshotter.SnapshotsTaken.Insert("snap-1") + + snapshots := []*volume.Snapshot{ + { + Spec: volume.SnapshotSpec{ + Location: "vsl-1", + }, + Status: volume.SnapshotStatus{ + ProviderSnapshotID: "snap-1", + }, + }, + } + + pluginManager := &pluginmocks.Manager{} + pluginManager.On("GetVolumeSnapshotter", "provider-1").Return(td.volumeSnapshotter, nil) + pluginManager.On("GetDeleteItemActions").Return([]velero.DeleteItemAction{new(mocks.DeleteItemAction)}, nil) + pluginManager.On("CleanupClients") + td.controller.newPluginManager = func(logrus.FieldLogger) clientmgmt.Manager { return pluginManager } + + td.backupStore.On("GetBackupVolumeSnapshots", input.Spec.BackupName).Return(snapshots, nil) + // Simulate a 404/not-found error (tarball has already been removed from storage) + td.backupStore.On("GetBackupContents", input.Spec.BackupName).Return(nil, fmt.Errorf("key not found")) + td.backupStore.On("DeleteBackup", input.Spec.BackupName).Return(nil) + + _, err := td.controller.Reconcile(t.Context(), td.req) + require.NoError(t, err) + + td.backupStore.AssertCalled(t, "GetBackupContents", input.Spec.BackupName) + td.backupStore.AssertCalled(t, "DeleteBackup", input.Spec.BackupName) + + // the dbr should be deleted (not-found is treated as permanent, deletion proceeds) + res := &velerov1api.DeleteBackupRequest{} + err = td.fakeClient.Get(ctx, td.req.NamespacedName, res) + assert.True(t, apierrors.IsNotFound(err), "Expected DBR to be deleted after not-found tarball error, but actual error: %v", err) + + // backup CR should be deleted because there are no errors in errs + err = td.fakeClient.Get(t.Context(), types.NamespacedName{ + Namespace: velerov1api.DefaultNamespace, + Name: backup.Name, + }, &velerov1api.Backup{}) + assert.True(t, apierrors.IsNotFound(err), "Expected backup CR to be deleted after not-found tarball error, but actual error: %v", err) }) t.Run("Expired request will be deleted if the status is processed", func(t *testing.T) { expired := time.Date(2018, 4, 3, 12, 0, 0, 0, time.UTC) diff --git a/pkg/controller/backup_repository_controller.go b/pkg/controller/backup_repository_controller.go index eb90660f49..11ebb5aec2 100644 --- a/pkg/controller/backup_repository_controller.go +++ b/pkg/controller/backup_repository_controller.go @@ -52,9 +52,11 @@ import ( const ( repoSyncPeriod = 5 * time.Minute defaultMaintainFrequency = 7 * 24 * time.Hour - defaultMaintenanceStatusQueueLength = 3 + defaultMaintenanceStatusQueueLength = 25 ) +var maintenanceStatusQueueLength = defaultMaintenanceStatusQueueLength + type BackupRepoReconciler struct { client.Client namespace string @@ -369,7 +371,7 @@ func ensureRepo(repo *velerov1api.BackupRepository, repoManager repomanager.Mana } func (r *BackupRepoReconciler) recallMaintenance(ctx context.Context, req *velerov1api.BackupRepository, log logrus.FieldLogger) error { - history, err := maintenance.WaitAllJobsComplete(ctx, r.Client, req, defaultMaintenanceStatusQueueLength, log) + history, err := maintenance.WaitAllJobsComplete(ctx, r.Client, req, maintenanceStatusQueueLength, log) if err != nil { return errors.Wrapf(err, "error waiting incomplete repo maintenance job for repo %s", req.Name) } @@ -427,7 +429,7 @@ func consolidateHistory(coming, cur []velerov1api.BackupRepositoryMaintenanceSta truncated := []velerov1api.BackupRepositoryMaintenanceStatus{} for consolidator.Len() > 0 { - if len(truncated) == defaultMaintenanceStatusQueueLength { + if len(truncated) == maintenanceStatusQueueLength { break } @@ -537,8 +539,8 @@ func updateRepoMaintenanceHistory(repo *velerov1api.BackupRepository, result vel } startingPos := 0 - if len(repo.Status.RecentMaintenance) >= defaultMaintenanceStatusQueueLength { - startingPos = len(repo.Status.RecentMaintenance) - defaultMaintenanceStatusQueueLength + 1 + if len(repo.Status.RecentMaintenance) >= maintenanceStatusQueueLength { + startingPos = len(repo.Status.RecentMaintenance) - maintenanceStatusQueueLength + 1 } repo.Status.RecentMaintenance = append(repo.Status.RecentMaintenance[startingPos:], latest) diff --git a/pkg/controller/backup_repository_controller_test.go b/pkg/controller/backup_repository_controller_test.go index 8a458033f4..ed952f4aee 100644 --- a/pkg/controller/backup_repository_controller_test.go +++ b/pkg/controller/backup_repository_controller_test.go @@ -929,6 +929,8 @@ func TestUpdateRepoMaintenanceHistory(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { + maintenanceStatusQueueLength = 3 + updateRepoMaintenanceHistory(test.backupRepo, test.result, &metav1.Time{Time: standardTime}, &metav1.Time{Time: standardTime.Add(time.Hour)}, "fake-message-0") for at := range test.backupRepo.Status.RecentMaintenance { diff --git a/pkg/controller/pod_volume_restore_controller.go b/pkg/controller/pod_volume_restore_controller.go index 0af0d8c868..f40de528b2 100644 --- a/pkg/controller/pod_volume_restore_controller.go +++ b/pkg/controller/pod_volume_restore_controller.go @@ -603,7 +603,7 @@ func (r *PodVolumeRestoreReconciler) closeDataPath(ctx context.Context, pvrName func (r *PodVolumeRestoreReconciler) SetupWithManager(mgr ctrl.Manager) error { gp := kube.NewGenericEventPredicate(func(object client.Object) bool { pvr := object.(*velerov1api.PodVolumeRestore) - if IsLegacyPVR(pvr) { + if _, err := uploader.ValidateUploaderType(pvr.Spec.UploaderType); err != nil { return false } @@ -628,7 +628,8 @@ func (r *PodVolumeRestoreReconciler) SetupWithManager(mgr ctrl.Manager) error { pred := kube.NewAllEventPredicate(func(obj client.Object) bool { pvr := obj.(*velerov1api.PodVolumeRestore) - return !IsLegacyPVR(pvr) + _, err := uploader.ValidateUploaderType(pvr.Spec.UploaderType) + return err == nil }) return ctrl.NewControllerManagedBy(mgr). @@ -678,7 +679,7 @@ func (r *PodVolumeRestoreReconciler) findPVRForTargetPod(ctx context.Context, po requests := []reconcile.Request{} for _, item := range list.Items { - if IsLegacyPVR(&item) { + if _, err := uploader.ValidateUploaderType(item.Spec.UploaderType); err != nil { continue } @@ -708,6 +709,11 @@ func (r *PodVolumeRestoreReconciler) findPVRForRestorePod(ctx context.Context, p "PVR": pvr.Name, }) + if _, err := uploader.ValidateUploaderType(pvr.Spec.UploaderType); err != nil { + log.WithField("uploaderType", pvr.Spec.UploaderType).Debug("skip PVR with invalid uploader type") + return []reconcile.Request{} + } + if pvr.Status.Phase != velerov1api.PodVolumeRestorePhaseAccepted { return []reconcile.Request{} } @@ -1029,7 +1035,7 @@ func (r *PodVolumeRestoreReconciler) AttemptPVRResume(ctx context.Context, logge for i := range pvrs.Items { pvr := &pvrs.Items[i] - if IsLegacyPVR(pvr) { + if _, err := uploader.ValidateUploaderType(pvr.Spec.UploaderType); err != nil { continue } diff --git a/pkg/controller/pod_volume_restore_controller_legacy.go b/pkg/controller/pod_volume_restore_controller_legacy.go deleted file mode 100644 index 9ddececf58..0000000000 --- a/pkg/controller/pod_volume_restore_controller_legacy.go +++ /dev/null @@ -1,364 +0,0 @@ -/* -Copyright The Velero Contributors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "context" - "fmt" - "os" - "path/filepath" - "time" - - "github.com/pkg/errors" - "github.com/sirupsen/logrus" - corev1api "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/kubernetes" - clocks "k8s.io/utils/clock" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/manager" - "sigs.k8s.io/controller-runtime/pkg/reconcile" - - "github.com/vmware-tanzu/velero/internal/credentials" - veleroapishared "github.com/vmware-tanzu/velero/pkg/apis/velero/shared" - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" - "github.com/vmware-tanzu/velero/pkg/datapath" - "github.com/vmware-tanzu/velero/pkg/exposer" - "github.com/vmware-tanzu/velero/pkg/podvolume" - "github.com/vmware-tanzu/velero/pkg/repository" - "github.com/vmware-tanzu/velero/pkg/restorehelper" - "github.com/vmware-tanzu/velero/pkg/uploader" - "github.com/vmware-tanzu/velero/pkg/util/boolptr" - "github.com/vmware-tanzu/velero/pkg/util/filesystem" - "github.com/vmware-tanzu/velero/pkg/util/kube" -) - -func InitLegacyPodVolumeRestoreReconciler(client client.Client, mgr manager.Manager, kubeClient kubernetes.Interface, dataPathMgr *datapath.Manager, namespace string, - resourceTimeout time.Duration, logger logrus.FieldLogger) error { - log := logger.WithField("controller", "PodVolumeRestoreLegacy") - - credentialFileStore, err := credentials.NewNamespacedFileStore(client, namespace, credentials.DefaultStoreDirectory(), filesystem.NewFileSystem()) - if err != nil { - return errors.Wrapf(err, "error creating credentials file store") - } - - credSecretStore, err := credentials.NewNamespacedSecretStore(client, namespace) - if err != nil { - return errors.Wrapf(err, "error creating secret file store") - } - - credentialGetter := &credentials.CredentialGetter{FromFile: credentialFileStore, FromSecret: credSecretStore} - ensurer := repository.NewEnsurer(client, log, resourceTimeout) - - reconciler := &PodVolumeRestoreReconcilerLegacy{ - Client: client, - kubeClient: kubeClient, - logger: log, - repositoryEnsurer: ensurer, - credentialGetter: credentialGetter, - fileSystem: filesystem.NewFileSystem(), - clock: &clocks.RealClock{}, - dataPathMgr: dataPathMgr, - } - - if err = reconciler.SetupWithManager(mgr); err != nil { - return errors.Wrapf(err, "error setup controller manager") - } - - return nil -} - -type PodVolumeRestoreReconcilerLegacy struct { - client.Client - kubeClient kubernetes.Interface - logger logrus.FieldLogger - repositoryEnsurer *repository.Ensurer - credentialGetter *credentials.CredentialGetter - fileSystem filesystem.Interface - clock clocks.WithTickerAndDelayedExecution - dataPathMgr *datapath.Manager -} - -// +kubebuilder:rbac:groups=velero.io,resources=podvolumerestores,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=velero.io,resources=podvolumerestores/status,verbs=get;update;patch -// +kubebuilder:rbac:groups="",resources=pods,verbs=get -// +kubebuilder:rbac:groups="",resources=persistentvolumes,verbs=get -// +kubebuilder:rbac:groups="",resources=persistentvolumerclaims,verbs=get - -func (c *PodVolumeRestoreReconcilerLegacy) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - log := c.logger.WithField("PodVolumeRestore", req.NamespacedName.String()) - log.Info("Reconciling PVR by legacy controller") - - pvr := &velerov1api.PodVolumeRestore{} - if err := c.Get(ctx, types.NamespacedName{Namespace: req.Namespace, Name: req.Name}, pvr); err != nil { - if apierrors.IsNotFound(err) { - log.Warn("PodVolumeRestore not found, skip") - return ctrl.Result{}, nil - } - log.WithError(err).Error("Unable to get the PodVolumeRestore") - return ctrl.Result{}, err - } - - log = log.WithField("pod", fmt.Sprintf("%s/%s", pvr.Spec.Pod.Namespace, pvr.Spec.Pod.Name)) - if len(pvr.OwnerReferences) == 1 { - log = log.WithField("restore", fmt.Sprintf("%s/%s", pvr.Namespace, pvr.OwnerReferences[0].Name)) - } - - shouldProcess, pod, err := shouldProcess(ctx, c.Client, log, pvr) - if err != nil { - return ctrl.Result{}, err - } - if !shouldProcess { - return ctrl.Result{}, nil - } - - initContainerIndex := getInitContainerIndex(pod) - if initContainerIndex > 0 { - log.Warnf(`Init containers before the %s container may cause issues - if they interfere with volumes being restored: %s index %d`, restorehelper.WaitInitContainer, restorehelper.WaitInitContainer, initContainerIndex) - } - - log.Info("Restore starting") - - callbacks := datapath.Callbacks{ - OnCompleted: c.OnDataPathCompleted, - OnFailed: c.OnDataPathFailed, - OnCancelled: c.OnDataPathCancelled, - OnProgress: c.OnDataPathProgress, - } - - fsRestore, err := c.dataPathMgr.CreateFileSystemBR(pvr.Name, pVBRRequestor, ctx, c.Client, pvr.Namespace, callbacks, log) - if err != nil { - if err == datapath.ConcurrentLimitExceed { - return ctrl.Result{Requeue: true, RequeueAfter: time.Second * 5}, nil - } else { - return c.errorOut(ctx, pvr, err, "error to create data path", log) - } - } - - original := pvr.DeepCopy() - pvr.Status.Phase = velerov1api.PodVolumeRestorePhaseInProgress - pvr.Status.StartTimestamp = &metav1.Time{Time: c.clock.Now()} - if err = c.Patch(ctx, pvr, client.MergeFrom(original)); err != nil { - c.closeDataPath(ctx, pvr.Name) - return c.errorOut(ctx, pvr, err, "error to update status to in progress", log) - } - - volumePath, err := exposer.GetPodVolumeHostPath(ctx, pod, pvr.Spec.Volume, c.kubeClient, c.fileSystem, log) - if err != nil { - c.closeDataPath(ctx, pvr.Name) - return c.errorOut(ctx, pvr, err, "error exposing host path for pod volume", log) - } - - log.WithField("path", volumePath.ByPath).Debugf("Found host path") - - if err := fsRestore.Init(ctx, &datapath.FSBRInitParam{ - BSLName: pvr.Spec.BackupStorageLocation, - SourceNamespace: pvr.Spec.SourceNamespace, - UploaderType: pvr.Spec.UploaderType, - RepositoryType: podvolume.GetPvrRepositoryType(pvr), - RepoIdentifier: pvr.Spec.RepoIdentifier, - RepositoryEnsurer: c.repositoryEnsurer, - CredentialGetter: c.credentialGetter, - }); err != nil { - c.closeDataPath(ctx, pvr.Name) - return c.errorOut(ctx, pvr, err, "error to initialize data path", log) - } - - if err := fsRestore.StartRestore(pvr.Spec.SnapshotID, volumePath, pvr.Spec.UploaderSettings); err != nil { - c.closeDataPath(ctx, pvr.Name) - return c.errorOut(ctx, pvr, err, "error starting data path restore", log) - } - - log.WithField("path", volumePath.ByPath).Info("Async fs restore data path started") - - return ctrl.Result{}, nil -} - -func (c *PodVolumeRestoreReconcilerLegacy) errorOut(ctx context.Context, pvr *velerov1api.PodVolumeRestore, err error, msg string, log logrus.FieldLogger) (ctrl.Result, error) { - _ = UpdatePVRStatusToFailed(ctx, c.Client, pvr, err, msg, c.clock.Now(), log) - return ctrl.Result{}, err -} - -func (c *PodVolumeRestoreReconcilerLegacy) SetupWithManager(mgr ctrl.Manager) error { - // The pod may not being scheduled at the point when its PVRs are initially reconciled. - // By watching the pods, we can trigger the PVR reconciliation again once the pod is finally scheduled on the node. - pred := kube.NewAllEventPredicate(func(obj client.Object) bool { - pvr := obj.(*velerov1api.PodVolumeRestore) - return IsLegacyPVR(pvr) - }) - - return ctrl.NewControllerManagedBy(mgr).Named("podvolumerestorelegacy"). - For(&velerov1api.PodVolumeRestore{}, builder.WithPredicates(pred)). - Watches(&corev1api.Pod{}, handler.EnqueueRequestsFromMapFunc(c.findVolumeRestoresForPod)). - Complete(c) -} - -func (c *PodVolumeRestoreReconcilerLegacy) findVolumeRestoresForPod(ctx context.Context, pod client.Object) []reconcile.Request { - list := &velerov1api.PodVolumeRestoreList{} - options := &client.ListOptions{ - LabelSelector: labels.Set(map[string]string{ - velerov1api.PodUIDLabel: string(pod.GetUID()), - }).AsSelector(), - } - if err := c.Client.List(context.TODO(), list, options); err != nil { - c.logger.WithField("pod", fmt.Sprintf("%s/%s", pod.GetNamespace(), pod.GetName())).WithError(err). - Error("unable to list PodVolumeRestores") - return []reconcile.Request{} - } - - requests := []reconcile.Request{} - for _, item := range list.Items { - if !IsLegacyPVR(&item) { - continue - } - - requests = append(requests, reconcile.Request{ - NamespacedName: types.NamespacedName{ - Namespace: item.GetNamespace(), - Name: item.GetName(), - }, - }) - } - return requests -} - -func (c *PodVolumeRestoreReconcilerLegacy) OnDataPathCompleted(ctx context.Context, namespace string, pvrName string, result datapath.Result) { - defer c.dataPathMgr.RemoveAsyncBR(pvrName) - - log := c.logger.WithField("pvr", pvrName) - - log.WithField("PVR", pvrName).Info("Async fs restore data path completed") - - var pvr velerov1api.PodVolumeRestore - if err := c.Client.Get(ctx, types.NamespacedName{Name: pvrName, Namespace: namespace}, &pvr); err != nil { - log.WithError(err).Warn("Failed to get PVR on completion") - return - } - - volumePath := result.Restore.Target.ByPath - if volumePath == "" { - _, _ = c.errorOut(ctx, &pvr, errors.New("path is empty"), "invalid restore target", log) - return - } - - // Remove the .velero directory from the restored volume (it may contain done files from previous restores - // of this volume, which we don't want to carry over). If this fails for any reason, log and continue, since - // this is non-essential cleanup (the done files are named based on restore UID and the init container looks - // for the one specific to the restore being executed). - if err := os.RemoveAll(filepath.Join(volumePath, ".velero")); err != nil { - log.WithError(err).Warnf("error removing .velero directory from directory %s", volumePath) - } - - var restoreUID types.UID - for _, owner := range pvr.OwnerReferences { - if boolptr.IsSetToTrue(owner.Controller) { - restoreUID = owner.UID - break - } - } - - // Create the .velero directory within the volume dir so we can write a done file - // for this restore. - if err := os.MkdirAll(filepath.Join(volumePath, ".velero"), 0755); err != nil { - _, _ = c.errorOut(ctx, &pvr, err, "error creating .velero directory for done file", log) - return - } - - // Write a done file with name= into the just-created .velero dir - // within the volume. The velero init container on the pod is waiting - // for this file to exist in each restored volume before completing. - if err := os.WriteFile(filepath.Join(volumePath, ".velero", string(restoreUID)), nil, 0644); err != nil { //nolint:gosec // Internal usage. No need to check. - _, _ = c.errorOut(ctx, &pvr, err, "error writing done file", log) - return - } - - original := pvr.DeepCopy() - pvr.Status.Phase = velerov1api.PodVolumeRestorePhaseCompleted - pvr.Status.CompletionTimestamp = &metav1.Time{Time: c.clock.Now()} - if err := c.Patch(ctx, &pvr, client.MergeFrom(original)); err != nil { - log.WithError(err).Error("error updating PodVolumeRestore status") - } - - log.Info("Restore completed") -} - -func (c *PodVolumeRestoreReconcilerLegacy) OnDataPathFailed(ctx context.Context, namespace string, pvrName string, err error) { - defer c.dataPathMgr.RemoveAsyncBR(pvrName) - - log := c.logger.WithField("pvr", pvrName) - - log.WithError(err).Error("Async fs restore data path failed") - - var pvr velerov1api.PodVolumeRestore - if getErr := c.Client.Get(ctx, types.NamespacedName{Name: pvrName, Namespace: namespace}, &pvr); getErr != nil { - log.WithError(getErr).Warn("Failed to get PVR on failure") - } else { - _, _ = c.errorOut(ctx, &pvr, err, "data path restore failed", log) - } -} - -func (c *PodVolumeRestoreReconcilerLegacy) OnDataPathCancelled(ctx context.Context, namespace string, pvrName string) { - defer c.dataPathMgr.RemoveAsyncBR(pvrName) - - log := c.logger.WithField("pvr", pvrName) - - log.Warn("Async fs restore data path canceled") - - var pvr velerov1api.PodVolumeRestore - if getErr := c.Client.Get(ctx, types.NamespacedName{Name: pvrName, Namespace: namespace}, &pvr); getErr != nil { - log.WithError(getErr).Warn("Failed to get PVR on cancel") - } else { - _, _ = c.errorOut(ctx, &pvr, errors.New("PVR is canceled"), "data path restore canceled", log) - } -} - -func (c *PodVolumeRestoreReconcilerLegacy) OnDataPathProgress(ctx context.Context, namespace string, pvrName string, progress *uploader.Progress) { - log := c.logger.WithField("pvr", pvrName) - - var pvr velerov1api.PodVolumeRestore - if err := c.Client.Get(ctx, types.NamespacedName{Name: pvrName, Namespace: namespace}, &pvr); err != nil { - log.WithError(err).Warn("Failed to get PVB on progress") - return - } - - original := pvr.DeepCopy() - pvr.Status.Progress = veleroapishared.DataMoveOperationProgress{TotalBytes: progress.TotalBytes, BytesDone: progress.BytesDone} - - if err := c.Client.Patch(ctx, &pvr, client.MergeFrom(original)); err != nil { - log.WithError(err).Error("Failed to update progress") - } -} - -func (c *PodVolumeRestoreReconcilerLegacy) closeDataPath(ctx context.Context, pvbName string) { - fsRestore := c.dataPathMgr.GetAsyncBR(pvbName) - if fsRestore != nil { - fsRestore.Close(ctx) - } - - c.dataPathMgr.RemoveAsyncBR(pvbName) -} - -func IsLegacyPVR(pvr *velerov1api.PodVolumeRestore) bool { - return pvr.Spec.UploaderType == "restic" -} diff --git a/pkg/controller/pod_volume_restore_controller_legacy_test.go b/pkg/controller/pod_volume_restore_controller_legacy_test.go deleted file mode 100644 index a107603e0e..0000000000 --- a/pkg/controller/pod_volume_restore_controller_legacy_test.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright The Velero Contributors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package controller - -import ( - "testing" - - "github.com/sirupsen/logrus" - "github.com/stretchr/testify/assert" - corev1api "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "sigs.k8s.io/controller-runtime/pkg/client/fake" - - velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" -) - -func TestFindVolumeRestoresForPodLegacy(t *testing.T) { - pod := &corev1api.Pod{} - pod.UID = "uid" - - scheme := runtime.NewScheme() - scheme.AddKnownTypes(velerov1api.SchemeGroupVersion, &velerov1api.PodVolumeRestore{}, &velerov1api.PodVolumeRestoreList{}) - clientBuilder := fake.NewClientBuilder().WithScheme(scheme) - - // no matching PVR - reconciler := &PodVolumeRestoreReconcilerLegacy{ - Client: clientBuilder.Build(), - logger: logrus.New(), - } - requests := reconciler.findVolumeRestoresForPod(t.Context(), pod) - assert.Empty(t, requests) - - // contain one matching PVR - reconciler.Client = clientBuilder.WithLists(&velerov1api.PodVolumeRestoreList{ - Items: []velerov1api.PodVolumeRestore{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "pvr1", - Labels: map[string]string{ - velerov1api.PodUIDLabel: string(pod.GetUID()), - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "pvr2", - Labels: map[string]string{ - velerov1api.PodUIDLabel: "non-matching-uid", - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "pvr3", - Labels: map[string]string{ - velerov1api.PodUIDLabel: string(pod.GetUID()), - }, - }, - Spec: velerov1api.PodVolumeRestoreSpec{ - UploaderType: "kopia", - }, - }, - { - ObjectMeta: metav1.ObjectMeta{ - Name: "pvr4", - Labels: map[string]string{ - velerov1api.PodUIDLabel: string(pod.GetUID()), - }, - }, - Spec: velerov1api.PodVolumeRestoreSpec{ - UploaderType: "restic", - }, - }, - }, - }).Build() - requests = reconciler.findVolumeRestoresForPod(t.Context(), pod) - assert.Len(t, requests, 1) -} diff --git a/pkg/controller/pod_volume_restore_controller_test.go b/pkg/controller/pod_volume_restore_controller_test.go index 9f2fe7a7f7..8999543d7f 100644 --- a/pkg/controller/pod_volume_restore_controller_test.go +++ b/pkg/controller/pod_volume_restore_controller_test.go @@ -526,6 +526,7 @@ func TestFindPVRForTargetPod(t *testing.T) { velerov1api.PodUIDLabel: string(pod.GetUID()), }, }, + Spec: velerov1api.PodVolumeRestoreSpec{UploaderType: uploader.KopiaType}, }, { ObjectMeta: metav1.ObjectMeta{ @@ -688,6 +689,7 @@ func TestPodVolumeRestoreReconcile(t *testing.T) { mockClose bool needExclusiveUpdateError error constrained bool + preserveEmptyUploader bool expected *velerov1api.PodVolumeRestore expectDeleted bool expectCancelRecord bool @@ -939,6 +941,13 @@ func TestPodVolumeRestoreReconcile(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { + if !test.preserveEmptyUploader && test.pvr != nil && test.pvr.Spec.UploaderType == "" { + test.pvr.Spec.UploaderType = uploader.KopiaType + } + if !test.preserveEmptyUploader && test.expected != nil && test.expected.Spec.UploaderType == "" { + test.expected.Spec.UploaderType = uploader.KopiaType + } + objs := []runtime.Object{daemonSet, node} ctlObj := []client.Object{} @@ -1396,7 +1405,7 @@ func TestFindPVBForRestorePod(t *testing.T) { }{ { name: "find pvr for pod", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), pod: builder.ForPod(velerov1api.DefaultNamespace, pvrName).Labels(map[string]string{velerov1api.PVRLabel: pvrName}).Status(corev1api.PodStatus{Phase: corev1api.PodRunning}).Result(), checkFunc: func(pvr *velerov1api.PodVolumeRestore, requests []reconcile.Request) { // Assert that the function returns a single request @@ -1407,7 +1416,7 @@ func TestFindPVBForRestorePod(t *testing.T) { }, }, { name: "no selected label found for pod", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), pod: builder.ForPod(velerov1api.DefaultNamespace, pvrName).Result(), checkFunc: func(pvr *velerov1api.PodVolumeRestore, requests []reconcile.Request) { // Assert that the function returns a single request @@ -1415,7 +1424,7 @@ func TestFindPVBForRestorePod(t *testing.T) { }, }, { name: "no matched pod", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), pod: builder.ForPod(velerov1api.DefaultNamespace, pvrName).Labels(map[string]string{velerov1api.PVRLabel: "non-existing-pvr"}).Result(), checkFunc: func(pvr *velerov1api.PodVolumeRestore, requests []reconcile.Request) { assert.Empty(t, requests) @@ -1423,12 +1432,20 @@ func TestFindPVBForRestorePod(t *testing.T) { }, { name: "pvr not accept", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseInProgress).Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseInProgress).Result(), pod: builder.ForPod(velerov1api.DefaultNamespace, pvrName).Labels(map[string]string{velerov1api.PVRLabel: pvrName}).Result(), checkFunc: func(pvr *velerov1api.PodVolumeRestore, requests []reconcile.Request) { assert.Empty(t, requests) }, }, + { + name: "invalid uploader type", + pvr: pvrBuilder().UploaderType("restic").Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), + pod: builder.ForPod(velerov1api.DefaultNamespace, pvrName).Labels(map[string]string{velerov1api.PVRLabel: pvrName}).Status(corev1api.PodStatus{Phase: corev1api.PodRunning}).Result(), + checkFunc: func(pvr *velerov1api.PodVolumeRestore, requests []reconcile.Request) { + assert.Empty(t, requests) + }, + }, } for _, test := range tests { ctx := t.Context() @@ -1613,32 +1630,32 @@ func TestAttemptPVRResume(t *testing.T) { }{ { name: "Other pvr", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhasePrepared).Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhasePrepared).Result(), }, { name: "Other pvr", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseAccepted).Result(), }, { name: "InProgress pvr, not the current node", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseInProgress).Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseInProgress).Result(), inProgressPvrs: []string{pvrName}, }, { name: "InProgress pvr, no resume error", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseInProgress).Node("node-1").Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseInProgress).Node("node-1").Result(), inProgressPvrs: []string{pvrName}, }, { name: "InProgress pvr, resume error, cancel error", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseInProgress).Node("node-1").Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseInProgress).Node("node-1").Result(), resumeErr: errors.New("fake-resume-error"), needErrs: []bool{false, false, true, false, false, false}, inProgressPvrs: []string{pvrName}, }, { name: "InProgress pvr, resume error, cancel succeed", - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhaseInProgress).Node("node-1").Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhaseInProgress).Node("node-1").Result(), resumeErr: errors.New("fake-resume-error"), cancelledPvrs: []string{pvrName}, inProgressPvrs: []string{pvrName}, @@ -1646,7 +1663,7 @@ func TestAttemptPVRResume(t *testing.T) { { name: "Error", needErrs: []bool{false, false, false, false, false, true}, - pvr: pvrBuilder().Phase(velerov1api.PodVolumeRestorePhasePrepared).Result(), + pvr: pvrBuilder().UploaderType(uploader.KopiaType).Phase(velerov1api.PodVolumeRestorePhasePrepared).Result(), expectedError: "error to list PVRs: List error", }, } diff --git a/pkg/controller/restore_controller.go b/pkg/controller/restore_controller.go index 208a3ddca8..469951f273 100644 --- a/pkg/controller/restore_controller.go +++ b/pkg/controller/restore_controller.go @@ -529,6 +529,7 @@ func (r *restoreReconciler) runValidatedRestore(restore *api.Restore, info backu LabelSelector: labels.Set(map[string]string{ api.BackupNameLabel: label.GetValidName(restore.Spec.BackupName), }).AsSelector(), + Namespace: restore.Namespace, } podVolumeBackupList := &api.PodVolumeBackupList{} diff --git a/pkg/controller/restore_controller_test.go b/pkg/controller/restore_controller_test.go index 3acc03d2a8..b013ee64db 100644 --- a/pkg/controller/restore_controller_test.go +++ b/pkg/controller/restore_controller_test.go @@ -238,6 +238,8 @@ func TestRestoreReconcile(t *testing.T) { expectedFinalPhase string addValidFinalizer bool emptyVolumeInfo bool + podVolumeBackups []*velerov1api.PodVolumeBackup + expectedPVBCount int }{ { name: "restore with both namespace in both includedNamespaces and excludedNamespaces fails validation", @@ -357,6 +359,22 @@ func TestRestoreReconcile(t *testing.T) { expectedCompletedTime: ×tamp, expectedRestorerCall: NewRestore("foo", "bar", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(), }, + { + name: "valid restore gets executed and only includes pod volume backups from restore namespace", + location: defaultStorageLocation, + restore: NewRestore("foo", "bar2", "backup-1", "ns-1", "", velerov1api.RestorePhaseNew).Result(), + backup: defaultBackup().StorageLocation("default").Result(), + podVolumeBackups: []*velerov1api.PodVolumeBackup{ + builder.ForPodVolumeBackup("foo", "pvb-1").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), + builder.ForPodVolumeBackup("other-ns", "pvb-2").ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, "backup-1")).Result(), + }, + expectedPVBCount: 1, + expectedErr: false, + expectedPhase: string(velerov1api.RestorePhaseInProgress), + expectedStartTime: ×tamp, + expectedCompletedTime: ×tamp, + expectedRestorerCall: NewRestore("foo", "bar2", "backup-1", "ns-1", "", velerov1api.RestorePhaseInProgress).Result(), + }, { name: "restoration of nodes is not supported", location: defaultStorageLocation, @@ -501,6 +519,13 @@ func TestRestoreReconcile(t *testing.T) { defaultStorageLocation.ObjectMeta.ResourceVersion = "" }() + if test.podVolumeBackups != nil { + for _, pvb := range test.podVolumeBackups { + err := fakeClient.Create(t.Context(), pvb) + require.NoError(t, err) + } + } + r := NewRestoreReconciler( t.Context(), velerov1api.DefaultNamespace, @@ -670,6 +695,10 @@ func TestRestoreReconcile(t *testing.T) { // the mock stores the pointer, which gets modified after assert.Equal(t, test.expectedRestorerCall.Spec, restorer.calledWithArg.Spec) assert.Equal(t, test.expectedRestorerCall.Status.Phase, restorer.calledWithArg.Status.Phase) + + if test.podVolumeBackups != nil { + assert.Len(t, restorer.calledWithPVBs, test.expectedPVBCount) + } }) } } @@ -1021,8 +1050,9 @@ func NewRestore(ns, name, backup, includeNS, includeResource string, phase veler type fakeRestorer struct { mock.Mock - calledWithArg velerov1api.Restore - kbClient client.Client + calledWithArg velerov1api.Restore + calledWithPVBs []*velerov1api.PodVolumeBackup + kbClient client.Client } func (r *fakeRestorer) Restore( @@ -1045,6 +1075,7 @@ func (r *fakeRestorer) RestoreWithResolvers(req *pkgrestore.Request, r.kbClient, volumeSnapshotterGetter) r.calledWithArg = *req.Restore + r.calledWithPVBs = req.PodVolumeBackups return res.Get(0).(results.Result), res.Get(1).(results.Result) } diff --git a/pkg/controller/restore_finalizer_controller.go b/pkg/controller/restore_finalizer_controller.go index 6ff7f8cb0d..f82216bc3b 100644 --- a/pkg/controller/restore_finalizer_controller.go +++ b/pkg/controller/restore_finalizer_controller.go @@ -22,7 +22,7 @@ import ( "sync" "time" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -457,7 +457,7 @@ func (ctx *finalizerContext) patchDynamicPVWithVolumeInfo() (errs results.Result func (ctx *finalizerContext) cleanupStubVGSC() (warnings results.Result) { ctx.logger.Info("cleaning up stub VolumeGroupSnapshotContents") - vgscList := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentList{} + vgscList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentList{} err := ctx.crClient.List( context.Background(), vgscList, diff --git a/pkg/controller/restore_finalizer_controller_test.go b/pkg/controller/restore_finalizer_controller_test.go index 0f1cc340dd..6fb5ba303b 100644 --- a/pkg/controller/restore_finalizer_controller_test.go +++ b/pkg/controller/restore_finalizer_controller_test.go @@ -22,7 +22,7 @@ import ( "testing" "time" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -750,7 +750,7 @@ func TestCleanupStubVGSC(t *testing.T) { tests := []struct { name string restore *velerov1api.Restore - existingVGSCs []*volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent + existingVGSCs []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent existingVSCs []*snapshotv1api.VolumeSnapshotContent expectedRemaining int expectedWarnings bool @@ -765,7 +765,7 @@ func TestCleanupStubVGSC(t *testing.T) { { name: "single stub VGSC deleted after VSCs are ready", restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(), - existingVGSCs: []*volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + existingVGSCs: []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ { ObjectMeta: metav1.ObjectMeta{ Name: "vgsc-stub-1", @@ -773,10 +773,10 @@ func TestCleanupStubVGSC(t *testing.T) { velerov1api.RestoreNameLabel: "restore-1", }, }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ Driver: "rbd.csi.ceph.com", - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{ - GroupSnapshotHandles: &volumegroupsnapshotv1beta1.GroupSnapshotHandles{ + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{ + GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{ VolumeGroupSnapshotHandle: "vgs-handle-1", VolumeSnapshotHandles: []string{snapshotHandle1}, }, @@ -814,7 +814,7 @@ func TestCleanupStubVGSC(t *testing.T) { { name: "multiple stub VGSCs deleted", restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(), - existingVGSCs: []*volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + existingVGSCs: []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ { ObjectMeta: metav1.ObjectMeta{ Name: "vgsc-stub-1", @@ -822,10 +822,10 @@ func TestCleanupStubVGSC(t *testing.T) { velerov1api.RestoreNameLabel: "restore-1", }, }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ Driver: "rbd.csi.ceph.com", - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{ - GroupSnapshotHandles: &volumegroupsnapshotv1beta1.GroupSnapshotHandles{ + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{ + GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{ VolumeGroupSnapshotHandle: "vgs-handle-1", VolumeSnapshotHandles: []string{snapshotHandle1}, }, @@ -839,10 +839,10 @@ func TestCleanupStubVGSC(t *testing.T) { velerov1api.RestoreNameLabel: "restore-1", }, }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ Driver: "rbd.csi.ceph.com", - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{ - GroupSnapshotHandles: &volumegroupsnapshotv1beta1.GroupSnapshotHandles{ + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{ + GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{ VolumeGroupSnapshotHandle: "vgs-handle-2", VolumeSnapshotHandles: []string{snapshotHandle2}, }, @@ -902,7 +902,7 @@ func TestCleanupStubVGSC(t *testing.T) { { name: "VGSCs from different restore are not deleted", restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(), - existingVGSCs: []*volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + existingVGSCs: []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ { ObjectMeta: metav1.ObjectMeta{ Name: "vgsc-stub-mine", @@ -910,9 +910,9 @@ func TestCleanupStubVGSC(t *testing.T) { velerov1api.RestoreNameLabel: "restore-1", }, }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ Driver: "rbd.csi.ceph.com", - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{}, + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{}, }, }, { @@ -922,9 +922,9 @@ func TestCleanupStubVGSC(t *testing.T) { velerov1api.RestoreNameLabel: "restore-2", }, }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ Driver: "rbd.csi.ceph.com", - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{}, + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{}, }, }, }, @@ -934,7 +934,7 @@ func TestCleanupStubVGSC(t *testing.T) { { name: "VGSC deleted even when no snapshot handles in spec", restore: builder.ForRestore(velerov1api.DefaultNamespace, "restore-1").Result(), - existingVGSCs: []*volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + existingVGSCs: []*volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ { ObjectMeta: metav1.ObjectMeta{ Name: "vgsc-stub-empty", @@ -942,9 +942,9 @@ func TestCleanupStubVGSC(t *testing.T) { velerov1api.RestoreNameLabel: "restore-1", }, }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ Driver: "rbd.csi.ceph.com", - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{}, + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{}, }, }, }, @@ -980,7 +980,7 @@ func TestCleanupStubVGSC(t *testing.T) { assert.True(t, warnings.IsEmpty(), "expected no warnings") } - remainingList := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentList{} + remainingList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentList{} require.NoError(t, fakeClient.List(t.Context(), remainingList)) assert.Len(t, remainingList.Items, tc.expectedRemaining) diff --git a/pkg/plugin/clientmgmt/process/logrus_adapter.go b/pkg/plugin/clientmgmt/process/logrus_adapter.go index da573284a7..8a790aacec 100644 --- a/pkg/plugin/clientmgmt/process/logrus_adapter.go +++ b/pkg/plugin/clientmgmt/process/logrus_adapter.go @@ -196,3 +196,21 @@ func (l *logrusAdapter) Name() string { func (l *logrusAdapter) StandardWriter(opts *hclog.StandardLoggerOptions) io.Writer { panic("not implemented") } + +// GetLevel returns the current level +func (l *logrusAdapter) GetLevel() hclog.Level { + switch l.level { + case logrus.TraceLevel: + return hclog.Trace + case logrus.DebugLevel: + return hclog.Debug + case logrus.InfoLevel: + return hclog.Info + case logrus.WarnLevel: + return hclog.Warn + case logrus.ErrorLevel: + return hclog.Error + default: + return hclog.NoLevel + } +} diff --git a/pkg/repository/udmrepo/kopialib/lib_repo.go b/pkg/repository/udmrepo/kopialib/lib_repo.go index e6c46ae66f..34559baf7e 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo.go @@ -388,9 +388,9 @@ func (kr *kopiaRepository) Close(ctx context.Context) error { return nil } -func (kr *kopiaRepository) NewObjectWriter(ctx context.Context, opt udmrepo.ObjectWriteOptions) udmrepo.ObjectWriter { +func (kr *kopiaRepository) NewObjectWriter(ctx context.Context, opt udmrepo.ObjectWriteOptions) (udmrepo.ObjectWriter, error) { if kr.rawWriter == nil { - return nil + return nil, errors.New("repo writer is closed or not open") } writer := kr.rawWriter.NewObjectWriter(kopia.SetupKopiaLog(ctx, kr.logger), object.WriterOptions{ @@ -402,12 +402,22 @@ func (kr *kopiaRepository) NewObjectWriter(ctx context.Context, opt udmrepo.Obje }) if writer == nil { - return nil + return nil, errors.Errorf("error creating writer for object %s", opt.Description) } return &kopiaObjectWriter{ rawWriter: writer, - } + }, nil +} + +// TODO add implementation in following PRs +func (kr *kopiaRepository) WriteMetadata(ctx context.Context, meta *udmrepo.Metadata, opt udmrepo.ObjectWriteOptions) (udmrepo.ID, error) { + return "", errors.New("not supported") +} + +// TODO add implementation in following PRs +func (kr *kopiaRepository) ReadMetadata(ctx context.Context, id udmrepo.ID) (*udmrepo.Metadata, error) { + return nil, errors.New("not supported") } func (kr *kopiaRepository) PutManifest(ctx context.Context, manifest udmrepo.RepoManifest) (udmrepo.ID, error) { @@ -436,6 +446,21 @@ func (kr *kopiaRepository) DeleteManifest(ctx context.Context, id udmrepo.ID) er return nil } +// TODO add implementation in following PRs +func (kr *kopiaRepository) SaveSnapshot(ctx context.Context, snap udmrepo.Snapshot) (udmrepo.ID, error) { + return "", errors.New("not supported") +} + +// TODO add implementation in following PRs +func (kr *kopiaRepository) GetSnapshot(ctx context.Context, id udmrepo.ID) (udmrepo.Snapshot, error) { + return udmrepo.Snapshot{}, errors.New("not supported") +} + +// TODO add implementation in following PRs +func (kr *kopiaRepository) DeleteSnapshot(ctx context.Context, id udmrepo.ID) error { + return errors.New("not supported") +} + func (kr *kopiaRepository) Flush(ctx context.Context) error { if kr.rawWriter == nil { return errors.New("repo writer is closed or not open") @@ -546,8 +571,9 @@ func (kow *kopiaObjectWriter) Write(p []byte) (int, error) { return kow.rawWriter.Write(p) } -func (kow *kopiaObjectWriter) Seek(offset int64, whence int) (int64, error) { - return -1, errors.New("not supported") +// TODO add implementation in following PRs +func (kow *kopiaObjectWriter) WriteAt(p []byte, offset int64) (int, error) { + return 0, errors.New("not supported") } func (kow *kopiaObjectWriter) Checkpoint() (udmrepo.ID, error) { diff --git a/pkg/repository/udmrepo/kopialib/lib_repo_test.go b/pkg/repository/udmrepo/kopialib/lib_repo_test.go index 2feabaeca3..36e331bef7 100644 --- a/pkg/repository/udmrepo/kopialib/lib_repo_test.go +++ b/pkg/repository/udmrepo/kopialib/lib_repo_test.go @@ -663,13 +663,16 @@ func TestNewObjectWriter(t *testing.T) { rawWriter *repomocks.MockRepositoryWriter rawWriterRet object.Writer expectedRet udmrepo.ObjectWriter + expectedErr string }{ { - name: "raw writer is nil", + name: "raw writer is nil", + expectedErr: "repo writer is closed or not open", }, { - name: "new object writer fail", - rawWriter: repomocks.NewMockRepositoryWriter(t), + name: "new object writer fail", + rawWriter: repomocks.NewMockRepositoryWriter(t), + expectedErr: "error creating writer for object ", }, { name: "succeed", @@ -688,9 +691,14 @@ func TestNewObjectWriter(t *testing.T) { kr.rawWriter = tc.rawWriter } - ret := kr.NewObjectWriter(t.Context(), udmrepo.ObjectWriteOptions{}) + ret, err := kr.NewObjectWriter(t.Context(), udmrepo.ObjectWriteOptions{}) - assert.Equal(t, tc.expectedRet, ret) + if tc.expectedErr == "" { + require.NoError(t, err) + require.Equal(t, tc.expectedRet, ret) + } else { + require.EqualError(t, err, tc.expectedErr) + } }) } } diff --git a/pkg/repository/udmrepo/mocks/BackupRepo.go b/pkg/repository/udmrepo/mocks/BackupRepo.go index 7d044356d5..e495b6b00f 100644 --- a/pkg/repository/udmrepo/mocks/BackupRepo.go +++ b/pkg/repository/udmrepo/mocks/BackupRepo.go @@ -1,42 +1,98 @@ -// Code generated by mockery v2.39.1. DO NOT EDIT. +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify package mocks import ( - context "context" - time "time" + "context" + "time" mock "github.com/stretchr/testify/mock" - - udmrepo "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" ) +// NewBackupRepo creates a new instance of BackupRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBackupRepo(t interface { + mock.TestingT + Cleanup(func()) +}) *BackupRepo { + mock := &BackupRepo{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + // BackupRepo is an autogenerated mock type for the BackupRepo type type BackupRepo struct { mock.Mock } -// Close provides a mock function with given fields: ctx -func (_m *BackupRepo) Close(ctx context.Context) error { - ret := _m.Called(ctx) +type BackupRepo_Expecter struct { + mock *mock.Mock +} + +func (_m *BackupRepo) EXPECT() *BackupRepo_Expecter { + return &BackupRepo_Expecter{mock: &_m.Mock} +} + +// Close provides a mock function for the type BackupRepo +func (_mock *BackupRepo) Close(ctx context.Context) error { + ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for Close") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) + if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = returnFunc(ctx) } else { r0 = ret.Error(0) } - return r0 } -// ConcatenateObjects provides a mock function with given fields: ctx, objectIDs -func (_m *BackupRepo) ConcatenateObjects(ctx context.Context, objectIDs []udmrepo.ID) (udmrepo.ID, error) { - ret := _m.Called(ctx, objectIDs) +// BackupRepo_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type BackupRepo_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +// - ctx context.Context +func (_e *BackupRepo_Expecter) Close(ctx interface{}) *BackupRepo_Close_Call { + return &BackupRepo_Close_Call{Call: _e.mock.On("Close", ctx)} +} + +func (_c *BackupRepo_Close_Call) Run(run func(ctx context.Context)) *BackupRepo_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *BackupRepo_Close_Call) Return(err error) *BackupRepo_Close_Call { + _c.Call.Return(err) + return _c +} + +func (_c *BackupRepo_Close_Call) RunAndReturn(run func(ctx context.Context) error) *BackupRepo_Close_Call { + _c.Call.Return(run) + return _c +} + +// ConcatenateObjects provides a mock function for the type BackupRepo +func (_mock *BackupRepo) ConcatenateObjects(ctx context.Context, objectIDs []udmrepo.ID) (udmrepo.ID, error) { + ret := _mock.Called(ctx, objectIDs) if len(ret) == 0 { panic("no return value specified for ConcatenateObjects") @@ -44,45 +100,179 @@ func (_m *BackupRepo) ConcatenateObjects(ctx context.Context, objectIDs []udmrep var r0 udmrepo.ID var r1 error - if rf, ok := ret.Get(0).(func(context.Context, []udmrepo.ID) (udmrepo.ID, error)); ok { - return rf(ctx, objectIDs) + if returnFunc, ok := ret.Get(0).(func(context.Context, []udmrepo.ID) (udmrepo.ID, error)); ok { + return returnFunc(ctx, objectIDs) } - if rf, ok := ret.Get(0).(func(context.Context, []udmrepo.ID) udmrepo.ID); ok { - r0 = rf(ctx, objectIDs) + if returnFunc, ok := ret.Get(0).(func(context.Context, []udmrepo.ID) udmrepo.ID); ok { + r0 = returnFunc(ctx, objectIDs) } else { r0 = ret.Get(0).(udmrepo.ID) } - - if rf, ok := ret.Get(1).(func(context.Context, []udmrepo.ID) error); ok { - r1 = rf(ctx, objectIDs) + if returnFunc, ok := ret.Get(1).(func(context.Context, []udmrepo.ID) error); ok { + r1 = returnFunc(ctx, objectIDs) } else { r1 = ret.Error(1) } - return r0, r1 } -// DeleteManifest provides a mock function with given fields: ctx, id -func (_m *BackupRepo) DeleteManifest(ctx context.Context, id udmrepo.ID) error { - ret := _m.Called(ctx, id) +// BackupRepo_ConcatenateObjects_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConcatenateObjects' +type BackupRepo_ConcatenateObjects_Call struct { + *mock.Call +} + +// ConcatenateObjects is a helper method to define mock.On call +// - ctx context.Context +// - objectIDs []udmrepo.ID +func (_e *BackupRepo_Expecter) ConcatenateObjects(ctx interface{}, objectIDs interface{}) *BackupRepo_ConcatenateObjects_Call { + return &BackupRepo_ConcatenateObjects_Call{Call: _e.mock.On("ConcatenateObjects", ctx, objectIDs)} +} + +func (_c *BackupRepo_ConcatenateObjects_Call) Run(run func(ctx context.Context, objectIDs []udmrepo.ID)) *BackupRepo_ConcatenateObjects_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []udmrepo.ID + if args[1] != nil { + arg1 = args[1].([]udmrepo.ID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_ConcatenateObjects_Call) Return(iD udmrepo.ID, err error) *BackupRepo_ConcatenateObjects_Call { + _c.Call.Return(iD, err) + return _c +} + +func (_c *BackupRepo_ConcatenateObjects_Call) RunAndReturn(run func(ctx context.Context, objectIDs []udmrepo.ID) (udmrepo.ID, error)) *BackupRepo_ConcatenateObjects_Call { + _c.Call.Return(run) + return _c +} + +// DeleteManifest provides a mock function for the type BackupRepo +func (_mock *BackupRepo) DeleteManifest(ctx context.Context, id udmrepo.ID) error { + ret := _mock.Called(ctx, id) if len(ret) == 0 { panic("no return value specified for DeleteManifest") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.ID) error); ok { - r0 = rf(ctx, id) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) error); ok { + r0 = returnFunc(ctx, id) } else { r0 = ret.Error(0) } + return r0 +} + +// BackupRepo_DeleteManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteManifest' +type BackupRepo_DeleteManifest_Call struct { + *mock.Call +} + +// DeleteManifest is a helper method to define mock.On call +// - ctx context.Context +// - id udmrepo.ID +func (_e *BackupRepo_Expecter) DeleteManifest(ctx interface{}, id interface{}) *BackupRepo_DeleteManifest_Call { + return &BackupRepo_DeleteManifest_Call{Call: _e.mock.On("DeleteManifest", ctx, id)} +} + +func (_c *BackupRepo_DeleteManifest_Call) Run(run func(ctx context.Context, id udmrepo.ID)) *BackupRepo_DeleteManifest_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.ID + if args[1] != nil { + arg1 = args[1].(udmrepo.ID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_DeleteManifest_Call) Return(err error) *BackupRepo_DeleteManifest_Call { + _c.Call.Return(err) + return _c +} +func (_c *BackupRepo_DeleteManifest_Call) RunAndReturn(run func(ctx context.Context, id udmrepo.ID) error) *BackupRepo_DeleteManifest_Call { + _c.Call.Return(run) + return _c +} + +// DeleteSnapshot provides a mock function for the type BackupRepo +func (_mock *BackupRepo) DeleteSnapshot(ctx context.Context, id udmrepo.ID) error { + ret := _mock.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for DeleteSnapshot") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) error); ok { + r0 = returnFunc(ctx, id) + } else { + r0 = ret.Error(0) + } return r0 } -// FindManifests provides a mock function with given fields: ctx, filter -func (_m *BackupRepo) FindManifests(ctx context.Context, filter udmrepo.ManifestFilter) ([]*udmrepo.ManifestEntryMetadata, error) { - ret := _m.Called(ctx, filter) +// BackupRepo_DeleteSnapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteSnapshot' +type BackupRepo_DeleteSnapshot_Call struct { + *mock.Call +} + +// DeleteSnapshot is a helper method to define mock.On call +// - ctx context.Context +// - id udmrepo.ID +func (_e *BackupRepo_Expecter) DeleteSnapshot(ctx interface{}, id interface{}) *BackupRepo_DeleteSnapshot_Call { + return &BackupRepo_DeleteSnapshot_Call{Call: _e.mock.On("DeleteSnapshot", ctx, id)} +} + +func (_c *BackupRepo_DeleteSnapshot_Call) Run(run func(ctx context.Context, id udmrepo.ID)) *BackupRepo_DeleteSnapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.ID + if args[1] != nil { + arg1 = args[1].(udmrepo.ID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_DeleteSnapshot_Call) Return(err error) *BackupRepo_DeleteSnapshot_Call { + _c.Call.Return(err) + return _c +} + +func (_c *BackupRepo_DeleteSnapshot_Call) RunAndReturn(run func(ctx context.Context, id udmrepo.ID) error) *BackupRepo_DeleteSnapshot_Call { + _c.Call.Return(run) + return _c +} + +// FindManifests provides a mock function for the type BackupRepo +func (_mock *BackupRepo) FindManifests(ctx context.Context, filter udmrepo.ManifestFilter) ([]*udmrepo.ManifestEntryMetadata, error) { + ret := _mock.Called(ctx, filter) if len(ret) == 0 { panic("no return value specified for FindManifests") @@ -90,103 +280,359 @@ func (_m *BackupRepo) FindManifests(ctx context.Context, filter udmrepo.Manifest var r0 []*udmrepo.ManifestEntryMetadata var r1 error - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.ManifestFilter) ([]*udmrepo.ManifestEntryMetadata, error)); ok { - return rf(ctx, filter) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ManifestFilter) ([]*udmrepo.ManifestEntryMetadata, error)); ok { + return returnFunc(ctx, filter) } - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.ManifestFilter) []*udmrepo.ManifestEntryMetadata); ok { - r0 = rf(ctx, filter) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ManifestFilter) []*udmrepo.ManifestEntryMetadata); ok { + r0 = returnFunc(ctx, filter) } else { if ret.Get(0) != nil { r0 = ret.Get(0).([]*udmrepo.ManifestEntryMetadata) } } - - if rf, ok := ret.Get(1).(func(context.Context, udmrepo.ManifestFilter) error); ok { - r1 = rf(ctx, filter) + if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.ManifestFilter) error); ok { + r1 = returnFunc(ctx, filter) } else { r1 = ret.Error(1) } - return r0, r1 } -// Flush provides a mock function with given fields: ctx -func (_m *BackupRepo) Flush(ctx context.Context) error { - ret := _m.Called(ctx) +// BackupRepo_FindManifests_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'FindManifests' +type BackupRepo_FindManifests_Call struct { + *mock.Call +} + +// FindManifests is a helper method to define mock.On call +// - ctx context.Context +// - filter udmrepo.ManifestFilter +func (_e *BackupRepo_Expecter) FindManifests(ctx interface{}, filter interface{}) *BackupRepo_FindManifests_Call { + return &BackupRepo_FindManifests_Call{Call: _e.mock.On("FindManifests", ctx, filter)} +} + +func (_c *BackupRepo_FindManifests_Call) Run(run func(ctx context.Context, filter udmrepo.ManifestFilter)) *BackupRepo_FindManifests_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.ManifestFilter + if args[1] != nil { + arg1 = args[1].(udmrepo.ManifestFilter) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_FindManifests_Call) Return(manifestEntryMetadatas []*udmrepo.ManifestEntryMetadata, err error) *BackupRepo_FindManifests_Call { + _c.Call.Return(manifestEntryMetadatas, err) + return _c +} + +func (_c *BackupRepo_FindManifests_Call) RunAndReturn(run func(ctx context.Context, filter udmrepo.ManifestFilter) ([]*udmrepo.ManifestEntryMetadata, error)) *BackupRepo_FindManifests_Call { + _c.Call.Return(run) + return _c +} + +// Flush provides a mock function for the type BackupRepo +func (_mock *BackupRepo) Flush(ctx context.Context) error { + ret := _mock.Called(ctx) if len(ret) == 0 { panic("no return value specified for Flush") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context) error); ok { - r0 = rf(ctx) + if returnFunc, ok := ret.Get(0).(func(context.Context) error); ok { + r0 = returnFunc(ctx) } else { r0 = ret.Error(0) } - return r0 } -// GetAdvancedFeatures provides a mock function with given fields: -func (_m *BackupRepo) GetAdvancedFeatures() udmrepo.AdvancedFeatureInfo { - ret := _m.Called() +// BackupRepo_Flush_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Flush' +type BackupRepo_Flush_Call struct { + *mock.Call +} + +// Flush is a helper method to define mock.On call +// - ctx context.Context +func (_e *BackupRepo_Expecter) Flush(ctx interface{}) *BackupRepo_Flush_Call { + return &BackupRepo_Flush_Call{Call: _e.mock.On("Flush", ctx)} +} + +func (_c *BackupRepo_Flush_Call) Run(run func(ctx context.Context)) *BackupRepo_Flush_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *BackupRepo_Flush_Call) Return(err error) *BackupRepo_Flush_Call { + _c.Call.Return(err) + return _c +} + +func (_c *BackupRepo_Flush_Call) RunAndReturn(run func(ctx context.Context) error) *BackupRepo_Flush_Call { + _c.Call.Return(run) + return _c +} + +// GetAdvancedFeatures provides a mock function for the type BackupRepo +func (_mock *BackupRepo) GetAdvancedFeatures() udmrepo.AdvancedFeatureInfo { + ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for GetAdvancedFeatures") } var r0 udmrepo.AdvancedFeatureInfo - if rf, ok := ret.Get(0).(func() udmrepo.AdvancedFeatureInfo); ok { - r0 = rf() + if returnFunc, ok := ret.Get(0).(func() udmrepo.AdvancedFeatureInfo); ok { + r0 = returnFunc() } else { r0 = ret.Get(0).(udmrepo.AdvancedFeatureInfo) } - return r0 } -// GetManifest provides a mock function with given fields: ctx, id, mani -func (_m *BackupRepo) GetManifest(ctx context.Context, id udmrepo.ID, mani *udmrepo.RepoManifest) error { - ret := _m.Called(ctx, id, mani) +// BackupRepo_GetAdvancedFeatures_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetAdvancedFeatures' +type BackupRepo_GetAdvancedFeatures_Call struct { + *mock.Call +} + +// GetAdvancedFeatures is a helper method to define mock.On call +func (_e *BackupRepo_Expecter) GetAdvancedFeatures() *BackupRepo_GetAdvancedFeatures_Call { + return &BackupRepo_GetAdvancedFeatures_Call{Call: _e.mock.On("GetAdvancedFeatures")} +} + +func (_c *BackupRepo_GetAdvancedFeatures_Call) Run(run func()) *BackupRepo_GetAdvancedFeatures_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *BackupRepo_GetAdvancedFeatures_Call) Return(advancedFeatureInfo udmrepo.AdvancedFeatureInfo) *BackupRepo_GetAdvancedFeatures_Call { + _c.Call.Return(advancedFeatureInfo) + return _c +} + +func (_c *BackupRepo_GetAdvancedFeatures_Call) RunAndReturn(run func() udmrepo.AdvancedFeatureInfo) *BackupRepo_GetAdvancedFeatures_Call { + _c.Call.Return(run) + return _c +} + +// GetManifest provides a mock function for the type BackupRepo +func (_mock *BackupRepo) GetManifest(ctx context.Context, id udmrepo.ID, mani *udmrepo.RepoManifest) error { + ret := _mock.Called(ctx, id, mani) if len(ret) == 0 { panic("no return value specified for GetManifest") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.ID, *udmrepo.RepoManifest) error); ok { - r0 = rf(ctx, id, mani) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID, *udmrepo.RepoManifest) error); ok { + r0 = returnFunc(ctx, id, mani) } else { r0 = ret.Error(0) } - return r0 } -// NewObjectWriter provides a mock function with given fields: ctx, opt -func (_m *BackupRepo) NewObjectWriter(ctx context.Context, opt udmrepo.ObjectWriteOptions) udmrepo.ObjectWriter { - ret := _m.Called(ctx, opt) +// BackupRepo_GetManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetManifest' +type BackupRepo_GetManifest_Call struct { + *mock.Call +} + +// GetManifest is a helper method to define mock.On call +// - ctx context.Context +// - id udmrepo.ID +// - mani *udmrepo.RepoManifest +func (_e *BackupRepo_Expecter) GetManifest(ctx interface{}, id interface{}, mani interface{}) *BackupRepo_GetManifest_Call { + return &BackupRepo_GetManifest_Call{Call: _e.mock.On("GetManifest", ctx, id, mani)} +} + +func (_c *BackupRepo_GetManifest_Call) Run(run func(ctx context.Context, id udmrepo.ID, mani *udmrepo.RepoManifest)) *BackupRepo_GetManifest_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.ID + if args[1] != nil { + arg1 = args[1].(udmrepo.ID) + } + var arg2 *udmrepo.RepoManifest + if args[2] != nil { + arg2 = args[2].(*udmrepo.RepoManifest) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *BackupRepo_GetManifest_Call) Return(err error) *BackupRepo_GetManifest_Call { + _c.Call.Return(err) + return _c +} + +func (_c *BackupRepo_GetManifest_Call) RunAndReturn(run func(ctx context.Context, id udmrepo.ID, mani *udmrepo.RepoManifest) error) *BackupRepo_GetManifest_Call { + _c.Call.Return(run) + return _c +} + +// GetSnapshot provides a mock function for the type BackupRepo +func (_mock *BackupRepo) GetSnapshot(ctx context.Context, id udmrepo.ID) (udmrepo.Snapshot, error) { + ret := _mock.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for GetSnapshot") + } + + var r0 udmrepo.Snapshot + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) (udmrepo.Snapshot, error)); ok { + return returnFunc(ctx, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) udmrepo.Snapshot); ok { + r0 = returnFunc(ctx, id) + } else { + r0 = ret.Get(0).(udmrepo.Snapshot) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.ID) error); ok { + r1 = returnFunc(ctx, id) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// BackupRepo_GetSnapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetSnapshot' +type BackupRepo_GetSnapshot_Call struct { + *mock.Call +} + +// GetSnapshot is a helper method to define mock.On call +// - ctx context.Context +// - id udmrepo.ID +func (_e *BackupRepo_Expecter) GetSnapshot(ctx interface{}, id interface{}) *BackupRepo_GetSnapshot_Call { + return &BackupRepo_GetSnapshot_Call{Call: _e.mock.On("GetSnapshot", ctx, id)} +} + +func (_c *BackupRepo_GetSnapshot_Call) Run(run func(ctx context.Context, id udmrepo.ID)) *BackupRepo_GetSnapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.ID + if args[1] != nil { + arg1 = args[1].(udmrepo.ID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_GetSnapshot_Call) Return(snapshot udmrepo.Snapshot, err error) *BackupRepo_GetSnapshot_Call { + _c.Call.Return(snapshot, err) + return _c +} + +func (_c *BackupRepo_GetSnapshot_Call) RunAndReturn(run func(ctx context.Context, id udmrepo.ID) (udmrepo.Snapshot, error)) *BackupRepo_GetSnapshot_Call { + _c.Call.Return(run) + return _c +} + +// NewObjectWriter provides a mock function for the type BackupRepo +func (_mock *BackupRepo) NewObjectWriter(ctx context.Context, opt udmrepo.ObjectWriteOptions) (udmrepo.ObjectWriter, error) { + ret := _mock.Called(ctx, opt) if len(ret) == 0 { panic("no return value specified for NewObjectWriter") } var r0 udmrepo.ObjectWriter - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.ObjectWriteOptions) udmrepo.ObjectWriter); ok { - r0 = rf(ctx, opt) + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ObjectWriteOptions) (udmrepo.ObjectWriter, error)); ok { + return returnFunc(ctx, opt) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ObjectWriteOptions) udmrepo.ObjectWriter); ok { + r0 = returnFunc(ctx, opt) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(udmrepo.ObjectWriter) } } + if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.ObjectWriteOptions) error); ok { + r1 = returnFunc(ctx, opt) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} - return r0 +// BackupRepo_NewObjectWriter_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'NewObjectWriter' +type BackupRepo_NewObjectWriter_Call struct { + *mock.Call +} + +// NewObjectWriter is a helper method to define mock.On call +// - ctx context.Context +// - opt udmrepo.ObjectWriteOptions +func (_e *BackupRepo_Expecter) NewObjectWriter(ctx interface{}, opt interface{}) *BackupRepo_NewObjectWriter_Call { + return &BackupRepo_NewObjectWriter_Call{Call: _e.mock.On("NewObjectWriter", ctx, opt)} +} + +func (_c *BackupRepo_NewObjectWriter_Call) Run(run func(ctx context.Context, opt udmrepo.ObjectWriteOptions)) *BackupRepo_NewObjectWriter_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.ObjectWriteOptions + if args[1] != nil { + arg1 = args[1].(udmrepo.ObjectWriteOptions) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_NewObjectWriter_Call) Return(objectWriter udmrepo.ObjectWriter, err error) *BackupRepo_NewObjectWriter_Call { + _c.Call.Return(objectWriter, err) + return _c +} + +func (_c *BackupRepo_NewObjectWriter_Call) RunAndReturn(run func(ctx context.Context, opt udmrepo.ObjectWriteOptions) (udmrepo.ObjectWriter, error)) *BackupRepo_NewObjectWriter_Call { + _c.Call.Return(run) + return _c } -// OpenObject provides a mock function with given fields: ctx, id -func (_m *BackupRepo) OpenObject(ctx context.Context, id udmrepo.ID) (udmrepo.ObjectReader, error) { - ret := _m.Called(ctx, id) +// OpenObject provides a mock function for the type BackupRepo +func (_mock *BackupRepo) OpenObject(ctx context.Context, id udmrepo.ID) (udmrepo.ObjectReader, error) { + ret := _mock.Called(ctx, id) if len(ret) == 0 { panic("no return value specified for OpenObject") @@ -194,29 +640,67 @@ func (_m *BackupRepo) OpenObject(ctx context.Context, id udmrepo.ID) (udmrepo.Ob var r0 udmrepo.ObjectReader var r1 error - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.ID) (udmrepo.ObjectReader, error)); ok { - return rf(ctx, id) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) (udmrepo.ObjectReader, error)); ok { + return returnFunc(ctx, id) } - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.ID) udmrepo.ObjectReader); ok { - r0 = rf(ctx, id) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) udmrepo.ObjectReader); ok { + r0 = returnFunc(ctx, id) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(udmrepo.ObjectReader) } } - - if rf, ok := ret.Get(1).(func(context.Context, udmrepo.ID) error); ok { - r1 = rf(ctx, id) + if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.ID) error); ok { + r1 = returnFunc(ctx, id) } else { r1 = ret.Error(1) } - return r0, r1 } -// PutManifest provides a mock function with given fields: ctx, mani -func (_m *BackupRepo) PutManifest(ctx context.Context, mani udmrepo.RepoManifest) (udmrepo.ID, error) { - ret := _m.Called(ctx, mani) +// BackupRepo_OpenObject_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'OpenObject' +type BackupRepo_OpenObject_Call struct { + *mock.Call +} + +// OpenObject is a helper method to define mock.On call +// - ctx context.Context +// - id udmrepo.ID +func (_e *BackupRepo_Expecter) OpenObject(ctx interface{}, id interface{}) *BackupRepo_OpenObject_Call { + return &BackupRepo_OpenObject_Call{Call: _e.mock.On("OpenObject", ctx, id)} +} + +func (_c *BackupRepo_OpenObject_Call) Run(run func(ctx context.Context, id udmrepo.ID)) *BackupRepo_OpenObject_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.ID + if args[1] != nil { + arg1 = args[1].(udmrepo.ID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_OpenObject_Call) Return(objectReader udmrepo.ObjectReader, err error) *BackupRepo_OpenObject_Call { + _c.Call.Return(objectReader, err) + return _c +} + +func (_c *BackupRepo_OpenObject_Call) RunAndReturn(run func(ctx context.Context, id udmrepo.ID) (udmrepo.ObjectReader, error)) *BackupRepo_OpenObject_Call { + _c.Call.Return(run) + return _c +} + +// PutManifest provides a mock function for the type BackupRepo +func (_mock *BackupRepo) PutManifest(ctx context.Context, mani udmrepo.RepoManifest) (udmrepo.ID, error) { + ret := _mock.Called(ctx, mani) if len(ret) == 0 { panic("no return value specified for PutManifest") @@ -224,52 +708,308 @@ func (_m *BackupRepo) PutManifest(ctx context.Context, mani udmrepo.RepoManifest var r0 udmrepo.ID var r1 error - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.RepoManifest) (udmrepo.ID, error)); ok { - return rf(ctx, mani) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.RepoManifest) (udmrepo.ID, error)); ok { + return returnFunc(ctx, mani) } - if rf, ok := ret.Get(0).(func(context.Context, udmrepo.RepoManifest) udmrepo.ID); ok { - r0 = rf(ctx, mani) + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.RepoManifest) udmrepo.ID); ok { + r0 = returnFunc(ctx, mani) } else { r0 = ret.Get(0).(udmrepo.ID) } + if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.RepoManifest) error); ok { + r1 = returnFunc(ctx, mani) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} - if rf, ok := ret.Get(1).(func(context.Context, udmrepo.RepoManifest) error); ok { - r1 = rf(ctx, mani) +// BackupRepo_PutManifest_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutManifest' +type BackupRepo_PutManifest_Call struct { + *mock.Call +} + +// PutManifest is a helper method to define mock.On call +// - ctx context.Context +// - mani udmrepo.RepoManifest +func (_e *BackupRepo_Expecter) PutManifest(ctx interface{}, mani interface{}) *BackupRepo_PutManifest_Call { + return &BackupRepo_PutManifest_Call{Call: _e.mock.On("PutManifest", ctx, mani)} +} + +func (_c *BackupRepo_PutManifest_Call) Run(run func(ctx context.Context, mani udmrepo.RepoManifest)) *BackupRepo_PutManifest_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.RepoManifest + if args[1] != nil { + arg1 = args[1].(udmrepo.RepoManifest) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_PutManifest_Call) Return(iD udmrepo.ID, err error) *BackupRepo_PutManifest_Call { + _c.Call.Return(iD, err) + return _c +} + +func (_c *BackupRepo_PutManifest_Call) RunAndReturn(run func(ctx context.Context, mani udmrepo.RepoManifest) (udmrepo.ID, error)) *BackupRepo_PutManifest_Call { + _c.Call.Return(run) + return _c +} + +// ReadMetadata provides a mock function for the type BackupRepo +func (_mock *BackupRepo) ReadMetadata(ctx context.Context, id udmrepo.ID) (*udmrepo.Metadata, error) { + ret := _mock.Called(ctx, id) + + if len(ret) == 0 { + panic("no return value specified for ReadMetadata") + } + + var r0 *udmrepo.Metadata + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) (*udmrepo.Metadata, error)); ok { + return returnFunc(ctx, id) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.ID) *udmrepo.Metadata); ok { + r0 = returnFunc(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*udmrepo.Metadata) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.ID) error); ok { + r1 = returnFunc(ctx, id) } else { r1 = ret.Error(1) } + return r0, r1 +} + +// BackupRepo_ReadMetadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReadMetadata' +type BackupRepo_ReadMetadata_Call struct { + *mock.Call +} + +// ReadMetadata is a helper method to define mock.On call +// - ctx context.Context +// - id udmrepo.ID +func (_e *BackupRepo_Expecter) ReadMetadata(ctx interface{}, id interface{}) *BackupRepo_ReadMetadata_Call { + return &BackupRepo_ReadMetadata_Call{Call: _e.mock.On("ReadMetadata", ctx, id)} +} + +func (_c *BackupRepo_ReadMetadata_Call) Run(run func(ctx context.Context, id udmrepo.ID)) *BackupRepo_ReadMetadata_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.ID + if args[1] != nil { + arg1 = args[1].(udmrepo.ID) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_ReadMetadata_Call) Return(metadata *udmrepo.Metadata, err error) *BackupRepo_ReadMetadata_Call { + _c.Call.Return(metadata, err) + return _c +} + +func (_c *BackupRepo_ReadMetadata_Call) RunAndReturn(run func(ctx context.Context, id udmrepo.ID) (*udmrepo.Metadata, error)) *BackupRepo_ReadMetadata_Call { + _c.Call.Return(run) + return _c +} + +// SaveSnapshot provides a mock function for the type BackupRepo +func (_mock *BackupRepo) SaveSnapshot(ctx context.Context, snapshot udmrepo.Snapshot) (udmrepo.ID, error) { + ret := _mock.Called(ctx, snapshot) + + if len(ret) == 0 { + panic("no return value specified for SaveSnapshot") + } + var r0 udmrepo.ID + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.Snapshot) (udmrepo.ID, error)); ok { + return returnFunc(ctx, snapshot) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, udmrepo.Snapshot) udmrepo.ID); ok { + r0 = returnFunc(ctx, snapshot) + } else { + r0 = ret.Get(0).(udmrepo.ID) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, udmrepo.Snapshot) error); ok { + r1 = returnFunc(ctx, snapshot) + } else { + r1 = ret.Error(1) + } return r0, r1 } -// Time provides a mock function with given fields: -func (_m *BackupRepo) Time() time.Time { - ret := _m.Called() +// BackupRepo_SaveSnapshot_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveSnapshot' +type BackupRepo_SaveSnapshot_Call struct { + *mock.Call +} + +// SaveSnapshot is a helper method to define mock.On call +// - ctx context.Context +// - snapshot udmrepo.Snapshot +func (_e *BackupRepo_Expecter) SaveSnapshot(ctx interface{}, snapshot interface{}) *BackupRepo_SaveSnapshot_Call { + return &BackupRepo_SaveSnapshot_Call{Call: _e.mock.On("SaveSnapshot", ctx, snapshot)} +} + +func (_c *BackupRepo_SaveSnapshot_Call) Run(run func(ctx context.Context, snapshot udmrepo.Snapshot)) *BackupRepo_SaveSnapshot_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 udmrepo.Snapshot + if args[1] != nil { + arg1 = args[1].(udmrepo.Snapshot) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *BackupRepo_SaveSnapshot_Call) Return(iD udmrepo.ID, err error) *BackupRepo_SaveSnapshot_Call { + _c.Call.Return(iD, err) + return _c +} + +func (_c *BackupRepo_SaveSnapshot_Call) RunAndReturn(run func(ctx context.Context, snapshot udmrepo.Snapshot) (udmrepo.ID, error)) *BackupRepo_SaveSnapshot_Call { + _c.Call.Return(run) + return _c +} + +// Time provides a mock function for the type BackupRepo +func (_mock *BackupRepo) Time() time.Time { + ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Time") } var r0 time.Time - if rf, ok := ret.Get(0).(func() time.Time); ok { - r0 = rf() + if returnFunc, ok := ret.Get(0).(func() time.Time); ok { + r0 = returnFunc() } else { r0 = ret.Get(0).(time.Time) } - return r0 } -// NewBackupRepo creates a new instance of BackupRepo. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewBackupRepo(t interface { - mock.TestingT - Cleanup(func()) -}) *BackupRepo { - mock := &BackupRepo{} - mock.Mock.Test(t) +// BackupRepo_Time_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Time' +type BackupRepo_Time_Call struct { + *mock.Call +} - t.Cleanup(func() { mock.AssertExpectations(t) }) +// Time is a helper method to define mock.On call +func (_e *BackupRepo_Expecter) Time() *BackupRepo_Time_Call { + return &BackupRepo_Time_Call{Call: _e.mock.On("Time")} +} - return mock +func (_c *BackupRepo_Time_Call) Run(run func()) *BackupRepo_Time_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *BackupRepo_Time_Call) Return(time1 time.Time) *BackupRepo_Time_Call { + _c.Call.Return(time1) + return _c +} + +func (_c *BackupRepo_Time_Call) RunAndReturn(run func() time.Time) *BackupRepo_Time_Call { + _c.Call.Return(run) + return _c +} + +// WriteMetadata provides a mock function for the type BackupRepo +func (_mock *BackupRepo) WriteMetadata(ctx context.Context, meta *udmrepo.Metadata, opt udmrepo.ObjectWriteOptions) (udmrepo.ID, error) { + ret := _mock.Called(ctx, meta, opt) + + if len(ret) == 0 { + panic("no return value specified for WriteMetadata") + } + + var r0 udmrepo.ID + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, *udmrepo.Metadata, udmrepo.ObjectWriteOptions) (udmrepo.ID, error)); ok { + return returnFunc(ctx, meta, opt) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, *udmrepo.Metadata, udmrepo.ObjectWriteOptions) udmrepo.ID); ok { + r0 = returnFunc(ctx, meta, opt) + } else { + r0 = ret.Get(0).(udmrepo.ID) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, *udmrepo.Metadata, udmrepo.ObjectWriteOptions) error); ok { + r1 = returnFunc(ctx, meta, opt) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// BackupRepo_WriteMetadata_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteMetadata' +type BackupRepo_WriteMetadata_Call struct { + *mock.Call +} + +// WriteMetadata is a helper method to define mock.On call +// - ctx context.Context +// - meta *udmrepo.Metadata +// - opt udmrepo.ObjectWriteOptions +func (_e *BackupRepo_Expecter) WriteMetadata(ctx interface{}, meta interface{}, opt interface{}) *BackupRepo_WriteMetadata_Call { + return &BackupRepo_WriteMetadata_Call{Call: _e.mock.On("WriteMetadata", ctx, meta, opt)} +} + +func (_c *BackupRepo_WriteMetadata_Call) Run(run func(ctx context.Context, meta *udmrepo.Metadata, opt udmrepo.ObjectWriteOptions)) *BackupRepo_WriteMetadata_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 *udmrepo.Metadata + if args[1] != nil { + arg1 = args[1].(*udmrepo.Metadata) + } + var arg2 udmrepo.ObjectWriteOptions + if args[2] != nil { + arg2 = args[2].(udmrepo.ObjectWriteOptions) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *BackupRepo_WriteMetadata_Call) Return(iD udmrepo.ID, err error) *BackupRepo_WriteMetadata_Call { + _c.Call.Return(iD, err) + return _c +} + +func (_c *BackupRepo_WriteMetadata_Call) RunAndReturn(run func(ctx context.Context, meta *udmrepo.Metadata, opt udmrepo.ObjectWriteOptions) (udmrepo.ID, error)) *BackupRepo_WriteMetadata_Call { + _c.Call.Return(run) + return _c } diff --git a/pkg/repository/udmrepo/mocks/ObjectWriter.go b/pkg/repository/udmrepo/mocks/ObjectWriter.go index 4bc21f8b79..d50a3ce768 100644 --- a/pkg/repository/udmrepo/mocks/ObjectWriter.go +++ b/pkg/repository/udmrepo/mocks/ObjectWriter.go @@ -1,20 +1,44 @@ -// Code generated by mockery v2.39.1. DO NOT EDIT. +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify package mocks import ( mock "github.com/stretchr/testify/mock" - udmrepo "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" + "github.com/vmware-tanzu/velero/pkg/repository/udmrepo" ) +// NewObjectWriter creates a new instance of ObjectWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewObjectWriter(t interface { + mock.TestingT + Cleanup(func()) +}) *ObjectWriter { + mock := &ObjectWriter{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + // ObjectWriter is an autogenerated mock type for the ObjectWriter type type ObjectWriter struct { mock.Mock } -// Checkpoint provides a mock function with given fields: -func (_m *ObjectWriter) Checkpoint() (udmrepo.ID, error) { - ret := _m.Called() +type ObjectWriter_Expecter struct { + mock *mock.Mock +} + +func (_m *ObjectWriter) EXPECT() *ObjectWriter_Expecter { + return &ObjectWriter_Expecter{mock: &_m.Mock} +} + +// Checkpoint provides a mock function for the type ObjectWriter +func (_mock *ObjectWriter) Checkpoint() (udmrepo.ID, error) { + ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Checkpoint") @@ -22,45 +46,96 @@ func (_m *ObjectWriter) Checkpoint() (udmrepo.ID, error) { var r0 udmrepo.ID var r1 error - if rf, ok := ret.Get(0).(func() (udmrepo.ID, error)); ok { - return rf() + if returnFunc, ok := ret.Get(0).(func() (udmrepo.ID, error)); ok { + return returnFunc() } - if rf, ok := ret.Get(0).(func() udmrepo.ID); ok { - r0 = rf() + if returnFunc, ok := ret.Get(0).(func() udmrepo.ID); ok { + r0 = returnFunc() } else { r0 = ret.Get(0).(udmrepo.ID) } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() } else { r1 = ret.Error(1) } - return r0, r1 } -// Close provides a mock function with given fields: -func (_m *ObjectWriter) Close() error { - ret := _m.Called() +// ObjectWriter_Checkpoint_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Checkpoint' +type ObjectWriter_Checkpoint_Call struct { + *mock.Call +} + +// Checkpoint is a helper method to define mock.On call +func (_e *ObjectWriter_Expecter) Checkpoint() *ObjectWriter_Checkpoint_Call { + return &ObjectWriter_Checkpoint_Call{Call: _e.mock.On("Checkpoint")} +} + +func (_c *ObjectWriter_Checkpoint_Call) Run(run func()) *ObjectWriter_Checkpoint_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ObjectWriter_Checkpoint_Call) Return(iD udmrepo.ID, err error) *ObjectWriter_Checkpoint_Call { + _c.Call.Return(iD, err) + return _c +} + +func (_c *ObjectWriter_Checkpoint_Call) RunAndReturn(run func() (udmrepo.ID, error)) *ObjectWriter_Checkpoint_Call { + _c.Call.Return(run) + return _c +} + +// Close provides a mock function for the type ObjectWriter +func (_mock *ObjectWriter) Close() error { + ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Close") } var r0 error - if rf, ok := ret.Get(0).(func() error); ok { - r0 = rf() + if returnFunc, ok := ret.Get(0).(func() error); ok { + r0 = returnFunc() } else { r0 = ret.Error(0) } - return r0 } -// Result provides a mock function with given fields: -func (_m *ObjectWriter) Result() (udmrepo.ID, error) { - ret := _m.Called() +// ObjectWriter_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close' +type ObjectWriter_Close_Call struct { + *mock.Call +} + +// Close is a helper method to define mock.On call +func (_e *ObjectWriter_Expecter) Close() *ObjectWriter_Close_Call { + return &ObjectWriter_Close_Call{Call: _e.mock.On("Close")} +} + +func (_c *ObjectWriter_Close_Call) Run(run func()) *ObjectWriter_Close_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ObjectWriter_Close_Call) Return(err error) *ObjectWriter_Close_Call { + _c.Call.Return(err) + return _c +} + +func (_c *ObjectWriter_Close_Call) RunAndReturn(run func() error) *ObjectWriter_Close_Call { + _c.Call.Return(run) + return _c +} + +// Result provides a mock function for the type ObjectWriter +func (_mock *ObjectWriter) Result() (udmrepo.ID, error) { + ret := _mock.Called() if len(ret) == 0 { panic("no return value specified for Result") @@ -68,90 +143,171 @@ func (_m *ObjectWriter) Result() (udmrepo.ID, error) { var r0 udmrepo.ID var r1 error - if rf, ok := ret.Get(0).(func() (udmrepo.ID, error)); ok { - return rf() + if returnFunc, ok := ret.Get(0).(func() (udmrepo.ID, error)); ok { + return returnFunc() } - if rf, ok := ret.Get(0).(func() udmrepo.ID); ok { - r0 = rf() + if returnFunc, ok := ret.Get(0).(func() udmrepo.ID); ok { + r0 = returnFunc() } else { r0 = ret.Get(0).(udmrepo.ID) } - - if rf, ok := ret.Get(1).(func() error); ok { - r1 = rf() + if returnFunc, ok := ret.Get(1).(func() error); ok { + r1 = returnFunc() } else { r1 = ret.Error(1) } - return r0, r1 } -// Seek provides a mock function with given fields: offset, whence -func (_m *ObjectWriter) Seek(offset int64, whence int) (int64, error) { - ret := _m.Called(offset, whence) +// ObjectWriter_Result_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Result' +type ObjectWriter_Result_Call struct { + *mock.Call +} + +// Result is a helper method to define mock.On call +func (_e *ObjectWriter_Expecter) Result() *ObjectWriter_Result_Call { + return &ObjectWriter_Result_Call{Call: _e.mock.On("Result")} +} + +func (_c *ObjectWriter_Result_Call) Run(run func()) *ObjectWriter_Result_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ObjectWriter_Result_Call) Return(iD udmrepo.ID, err error) *ObjectWriter_Result_Call { + _c.Call.Return(iD, err) + return _c +} + +func (_c *ObjectWriter_Result_Call) RunAndReturn(run func() (udmrepo.ID, error)) *ObjectWriter_Result_Call { + _c.Call.Return(run) + return _c +} + +// Write provides a mock function for the type ObjectWriter +func (_mock *ObjectWriter) Write(p []byte) (int, error) { + ret := _mock.Called(p) if len(ret) == 0 { - panic("no return value specified for Seek") + panic("no return value specified for Write") } - var r0 int64 + var r0 int var r1 error - if rf, ok := ret.Get(0).(func(int64, int) (int64, error)); ok { - return rf(offset, whence) + if returnFunc, ok := ret.Get(0).(func([]byte) (int, error)); ok { + return returnFunc(p) } - if rf, ok := ret.Get(0).(func(int64, int) int64); ok { - r0 = rf(offset, whence) + if returnFunc, ok := ret.Get(0).(func([]byte) int); ok { + r0 = returnFunc(p) } else { - r0 = ret.Get(0).(int64) + r0 = ret.Get(0).(int) } - - if rf, ok := ret.Get(1).(func(int64, int) error); ok { - r1 = rf(offset, whence) + if returnFunc, ok := ret.Get(1).(func([]byte) error); ok { + r1 = returnFunc(p) } else { r1 = ret.Error(1) } - return r0, r1 } -// Write provides a mock function with given fields: p -func (_m *ObjectWriter) Write(p []byte) (int, error) { - ret := _m.Called(p) +// ObjectWriter_Write_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Write' +type ObjectWriter_Write_Call struct { + *mock.Call +} + +// Write is a helper method to define mock.On call +// - p []byte +func (_e *ObjectWriter_Expecter) Write(p interface{}) *ObjectWriter_Write_Call { + return &ObjectWriter_Write_Call{Call: _e.mock.On("Write", p)} +} + +func (_c *ObjectWriter_Write_Call) Run(run func(p []byte)) *ObjectWriter_Write_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []byte + if args[0] != nil { + arg0 = args[0].([]byte) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *ObjectWriter_Write_Call) Return(n int, err error) *ObjectWriter_Write_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *ObjectWriter_Write_Call) RunAndReturn(run func(p []byte) (int, error)) *ObjectWriter_Write_Call { + _c.Call.Return(run) + return _c +} + +// WriteAt provides a mock function for the type ObjectWriter +func (_mock *ObjectWriter) WriteAt(p []byte, off int64) (int, error) { + ret := _mock.Called(p, off) if len(ret) == 0 { - panic("no return value specified for Write") + panic("no return value specified for WriteAt") } var r0 int var r1 error - if rf, ok := ret.Get(0).(func([]byte) (int, error)); ok { - return rf(p) + if returnFunc, ok := ret.Get(0).(func([]byte, int64) (int, error)); ok { + return returnFunc(p, off) } - if rf, ok := ret.Get(0).(func([]byte) int); ok { - r0 = rf(p) + if returnFunc, ok := ret.Get(0).(func([]byte, int64) int); ok { + r0 = returnFunc(p, off) } else { r0 = ret.Get(0).(int) } - - if rf, ok := ret.Get(1).(func([]byte) error); ok { - r1 = rf(p) + if returnFunc, ok := ret.Get(1).(func([]byte, int64) error); ok { + r1 = returnFunc(p, off) } else { r1 = ret.Error(1) } - return r0, r1 } -// NewObjectWriter creates a new instance of ObjectWriter. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -// The first argument is typically a *testing.T value. -func NewObjectWriter(t interface { - mock.TestingT - Cleanup(func()) -}) *ObjectWriter { - mock := &ObjectWriter{} - mock.Mock.Test(t) +// ObjectWriter_WriteAt_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WriteAt' +type ObjectWriter_WriteAt_Call struct { + *mock.Call +} - t.Cleanup(func() { mock.AssertExpectations(t) }) +// WriteAt is a helper method to define mock.On call +// - p []byte +// - off int64 +func (_e *ObjectWriter_Expecter) WriteAt(p interface{}, off interface{}) *ObjectWriter_WriteAt_Call { + return &ObjectWriter_WriteAt_Call{Call: _e.mock.On("WriteAt", p, off)} +} - return mock +func (_c *ObjectWriter_WriteAt_Call) Run(run func(p []byte, off int64)) *ObjectWriter_WriteAt_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 []byte + if args[0] != nil { + arg0 = args[0].([]byte) + } + var arg1 int64 + if args[1] != nil { + arg1 = args[1].(int64) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *ObjectWriter_WriteAt_Call) Return(n int, err error) *ObjectWriter_WriteAt_Call { + _c.Call.Return(n, err) + return _c +} + +func (_c *ObjectWriter_WriteAt_Call) RunAndReturn(run func(p []byte, off int64) (int, error)) *ObjectWriter_WriteAt_Call { + _c.Call.Return(run) + return _c } diff --git a/pkg/repository/udmrepo/repo.go b/pkg/repository/udmrepo/repo.go index 1e7e4d0117..095d18973f 100644 --- a/pkg/repository/udmrepo/repo.go +++ b/pkg/repository/udmrepo/repo.go @@ -62,19 +62,41 @@ const ( // ObjectWriteOptions defines the options when creating an object for write type ObjectWriteOptions struct { - FullPath string // Full logical path of the object - DataType int // OBJECT_DATA_TYPE_* - Description string // A description of the object, could be empty - Prefix ID // A prefix of the name used to save the object - AccessMode int // OBJECT_DATA_ACCESS_* - BackupMode int // OBJECT_DATA_BACKUP_* - AsyncWrites int // Num of async writes for the object, 0 means no async write + FullPath string // Full logical path of the object + DataType int // OBJECT_DATA_TYPE_* + Description string // A description of the object, could be empty + Prefix ID // A prefix of the name used to save the object + AccessMode int // OBJECT_DATA_ACCESS_* + BackupMode int // OBJECT_DATA_BACKUP_* + AsyncWrites int // Num of async writes for the object, 0 means no async write + ParentObject ID // The object in the previous snapshot, for incremental backup } type AdvancedFeatureInfo struct { MultiPartBackup bool // if set to true, it means the repo supports multiple-part backup } +type ObjectMetadata struct { + ID ID + Type int // OBJECT_DATA_TYPE_* + Size int64 +} + +type Metadata struct { + SubObjects []ObjectMetadata // For dir metadata only, the sub objects in this dir. + ExtraDataLen int // Extra data associated to this metadata. + ExtraData []byte +} + +type Snapshot struct { + Source string + Description string + StartTime time.Time + EndTime time.Time + Tags map[string]string + RootObject ID +} + // BackupRepoService is used to initialize, open or maintain a backup repository type BackupRepoService interface { // Create creates a new backup repository. @@ -119,7 +141,14 @@ type BackupRepo interface { // NewObjectWriter creates a new object and return the object's writer interface. // return: A unified identifier of the object on success. - NewObjectWriter(ctx context.Context, opt ObjectWriteOptions) ObjectWriter + NewObjectWriter(ctx context.Context, opt ObjectWriteOptions) (ObjectWriter, error) + + // WriteMetadata writes metadata to the repo, metadata is used to describe data, e.g., file system + // dirs are saved as metadata + WriteMetadata(ctx context.Context, meta *Metadata, opt ObjectWriteOptions) (ID, error) + + // ReadMetadata reads a metadata from repo by the metadata's object ID + ReadMetadata(ctx context.Context, id ID) (*Metadata, error) // PutManifest saves a manifest object into the backup repository. PutManifest(ctx context.Context, mani RepoManifest) (ID, error) @@ -139,6 +168,15 @@ type BackupRepo interface { // Time returns the local time of the backup repository. It may be different from the time of the caller Time() time.Time + // SaveSnapshot saves a repo snapshot + SaveSnapshot(ctx context.Context, snapshot Snapshot) (ID, error) + + // GetSnapshot returns a repo snapshot from snapshot ID + GetSnapshot(ctx context.Context, id ID) (Snapshot, error) + + // DeleteSnapshot deletes a repo snapshot + DeleteSnapshot(ctx context.Context, id ID) error + // Close closes the backup repository Close(ctx context.Context) error } @@ -154,8 +192,8 @@ type ObjectReader interface { type ObjectWriter interface { io.WriteCloser - // Seeker is used in the cases that the object is not written sequentially - io.Seeker + // WriterAt is used in the cases that the object is not written sequentially + io.WriterAt // Checkpoint is periodically called to preserve the state of data written to the repo so far. // Checkpoint returns a unified identifier that represent the current state. diff --git a/pkg/restore/actions/csi/volumesnapshot_action.go b/pkg/restore/actions/csi/volumesnapshot_action.go index 708b406814..dec33d4efe 100644 --- a/pkg/restore/actions/csi/volumesnapshot_action.go +++ b/pkg/restore/actions/csi/volumesnapshot_action.go @@ -20,7 +20,7 @@ import ( "context" "fmt" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -103,7 +103,7 @@ func (p *volumeSnapshotRestoreItemAction) ensureStubVGSCExists( vgscName := util.GenerateSha256FromRestoreUIDAndVsName(string(restore.UID), vgsh) // Check if VGSC already exists - existingVGSC := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + existingVGSC := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} err := p.crClient.Get(ctx, crclient.ObjectKey{Name: vgscName}, existingVGSC) if err == nil { // VGSC already exists, add this snapshot handle if not already present @@ -119,7 +119,7 @@ func (p *volumeSnapshotRestoreItemAction) ensureStubVGSCExists( // Look up VolumeGroupSnapshotClass to get secret annotations vgscAnnotations := map[string]string{} - vgscList := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotClassList{} + vgscList := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotClassList{} if err := p.crClient.List(ctx, vgscList); err == nil { for _, vgsClass := range vgscList.Items { if vgsClass.Driver == driver { @@ -135,7 +135,7 @@ func (p *volumeSnapshotRestoreItemAction) ensureStubVGSCExists( } } - vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + vgsc := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ ObjectMeta: metav1.ObjectMeta{ Name: vgscName, Labels: map[string]string{ @@ -143,11 +143,11 @@ func (p *volumeSnapshotRestoreItemAction) ensureStubVGSCExists( }, Annotations: vgscAnnotations, }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain, Driver: driver, - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{ - GroupSnapshotHandles: &volumegroupsnapshotv1beta1.GroupSnapshotHandles{ + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{ + GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{ VolumeGroupSnapshotHandle: vgsh, VolumeSnapshotHandles: []string{snapshotHandle}, }, @@ -164,7 +164,7 @@ func (p *volumeSnapshotRestoreItemAction) ensureStubVGSCExists( // Another VS restore created the VGSC between our Get and Create. // Re-fetch and add our snapshot handle. p.log.Infof("Stub VGSC %s was created by another VS restore, adding our handle", vgscName) - raceVGSC := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + raceVGSC := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} if getErr := p.crClient.Get(ctx, crclient.ObjectKey{Name: vgscName}, raceVGSC); getErr != nil { return errors.Wrapf(getErr, "failed to get VGSC %s after race", vgscName) } @@ -174,7 +174,7 @@ func (p *volumeSnapshotRestoreItemAction) ensureStubVGSCExists( } // Re-fetch to get server-assigned metadata (resourceVersion) needed for patching - createdVGSC := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + createdVGSC := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} if err := p.crClient.Get(ctx, crclient.ObjectKey{Name: vgscName}, createdVGSC); err != nil { p.log.Warnf("Failed to fetch stub VGSC %s for status patch: %v", vgscName, err) return nil @@ -183,7 +183,7 @@ func (p *volumeSnapshotRestoreItemAction) ensureStubVGSCExists( // Set volumeGroupSnapshotHandle in status using Patch to avoid conflicts with the CSI controller. patchBase := createdVGSC.DeepCopy() if createdVGSC.Status == nil { - createdVGSC.Status = &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentStatus{} + createdVGSC.Status = &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentStatus{} } createdVGSC.Status.VolumeGroupSnapshotHandle = &vgsh if err := p.crClient.Status().Patch(ctx, createdVGSC, crclient.MergeFrom(patchBase)); err != nil { @@ -198,7 +198,7 @@ func (p *volumeSnapshotRestoreItemAction) ensureStubVGSCExists( // This is needed when multiple VolumeSnapshots from the same VolumeGroupSnapshot are restored. func (p *volumeSnapshotRestoreItemAction) addSnapshotHandleToVGSC( ctx context.Context, - vgsc *volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent, + vgsc *volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent, snapshotHandle string, ) error { // Check if handle is already in the list @@ -214,7 +214,7 @@ func (p *volumeSnapshotRestoreItemAction) addSnapshotHandleToVGSC( // Add the snapshot handle to the list patchBase := vgsc.DeepCopy() if vgsc.Spec.Source.GroupSnapshotHandles == nil { - vgsc.Spec.Source.GroupSnapshotHandles = &volumegroupsnapshotv1beta1.GroupSnapshotHandles{} + vgsc.Spec.Source.GroupSnapshotHandles = &volumegroupsnapshotv1beta2.GroupSnapshotHandles{} } vgsc.Spec.Source.GroupSnapshotHandles.VolumeSnapshotHandles = append( vgsc.Spec.Source.GroupSnapshotHandles.VolumeSnapshotHandles, diff --git a/pkg/restore/actions/csi/volumesnapshot_action_test.go b/pkg/restore/actions/csi/volumesnapshot_action_test.go index 9e548b59d0..de3e592c03 100644 --- a/pkg/restore/actions/csi/volumesnapshot_action_test.go +++ b/pkg/restore/actions/csi/volumesnapshot_action_test.go @@ -21,7 +21,7 @@ import ( "fmt" "testing" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -232,7 +232,7 @@ func TestEnsureStubVGSCExists(t *testing.T) { name string vs *snapshotv1api.VolumeSnapshot restore *velerov1api.Restore - existingVGSC *volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent + existingVGSC *volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent expectVGSC bool expectErr bool expectedHandle string @@ -317,15 +317,15 @@ func TestEnsureStubVGSCExists(t *testing.T) { }, }, restore: builder.ForRestore("velero", "restore").ObjectMeta(builder.WithUID("restore-uid")).Result(), - existingVGSC: &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + existingVGSC: &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ ObjectMeta: metav1.ObjectMeta{ Name: util.GenerateSha256FromRestoreUIDAndVsName("restore-uid", testVGSHandle), }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ Driver: testDriver, DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain, - Source: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{ - GroupSnapshotHandles: &volumegroupsnapshotv1beta1.GroupSnapshotHandles{ + Source: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{ + GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{ VolumeGroupSnapshotHandle: testVGSHandle, VolumeSnapshotHandles: []string{testSnapshotHandle}, }, @@ -362,7 +362,7 @@ func TestEnsureStubVGSCExists(t *testing.T) { // Check if VGSC was created/updated vgscName := util.GenerateSha256FromRestoreUIDAndVsName(string(tc.restore.UID), tc.vs.Annotations[velerov1api.VolumeGroupSnapshotHandleAnnotation]) - vgsc := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + vgsc := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} getErr := crClient.Get(context.Background(), crclient.ObjectKey{Name: vgscName}, vgsc) if tc.expectVGSC { @@ -420,23 +420,23 @@ func TestAddSnapshotHandleToVGSC(t *testing.T) { t.Run(tc.name, func(t *testing.T) { crClient := velerotest.NewFakeControllerRuntimeClient(t) - var source volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource + var source volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource if tc.nilGroupSnapshotHandles { - source = volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{} + source = volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{} } else { - source = volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSource{ - GroupSnapshotHandles: &volumegroupsnapshotv1beta1.GroupSnapshotHandles{ + source = volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSource{ + GroupSnapshotHandles: &volumegroupsnapshotv1beta2.GroupSnapshotHandles{ VolumeGroupSnapshotHandle: testVGSHandle, VolumeSnapshotHandles: tc.existingHandles, }, } } - existingVGSC := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{ + existingVGSC := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{ ObjectMeta: metav1.ObjectMeta{ Name: "test-vgsc", }, - Spec: volumegroupsnapshotv1beta1.VolumeGroupSnapshotContentSpec{ + Spec: volumegroupsnapshotv1beta2.VolumeGroupSnapshotContentSpec{ Driver: testDriver, DeletionPolicy: snapshotv1api.VolumeSnapshotContentRetain, Source: source, @@ -445,7 +445,7 @@ func TestAddSnapshotHandleToVGSC(t *testing.T) { require.NoError(t, crClient.Create(context.Background(), existingVGSC)) // Re-fetch to get the created object with proper metadata - fetchedVGSC := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + fetchedVGSC := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} require.NoError(t, crClient.Get(context.Background(), crclient.ObjectKey{Name: "test-vgsc"}, fetchedVGSC)) p := &volumeSnapshotRestoreItemAction{ @@ -457,7 +457,7 @@ func TestAddSnapshotHandleToVGSC(t *testing.T) { require.NoError(t, err) // Verify the VGSC has expected handles - updatedVGSC := &volumegroupsnapshotv1beta1.VolumeGroupSnapshotContent{} + updatedVGSC := &volumegroupsnapshotv1beta2.VolumeGroupSnapshotContent{} require.NoError(t, crClient.Get(context.Background(), crclient.ObjectKey{Name: "test-vgsc"}, updatedVGSC)) require.ElementsMatch(t, tc.expectedHandles, updatedVGSC.Spec.Source.GroupSnapshotHandles.VolumeSnapshotHandles) }) diff --git a/pkg/restore/actions/pod_volume_restore_action.go b/pkg/restore/actions/pod_volume_restore_action.go index 4e3180fef7..e26a530347 100644 --- a/pkg/restore/actions/pod_volume_restore_action.go +++ b/pkg/restore/actions/pod_volume_restore_action.go @@ -101,6 +101,7 @@ func (a *PodVolumeRestoreAction) Execute(input *velero.RestoreItemActionExecuteI opts := &ctrlclient.ListOptions{ LabelSelector: label.NewSelectorForBackup(input.Restore.Spec.BackupName), + Namespace: input.Restore.Namespace, } podVolumeBackupList := new(velerov1api.PodVolumeBackupList) if err := a.crClient.List(context.TODO(), podVolumeBackupList, opts); err != nil { diff --git a/pkg/restore/actions/pod_volume_restore_action_test.go b/pkg/restore/actions/pod_volume_restore_action_test.go index 70911ca377..bc9662ab7d 100644 --- a/pkg/restore/actions/pod_volume_restore_action_test.go +++ b/pkg/restore/actions/pod_volume_restore_action_test.go @@ -350,6 +350,28 @@ func TestPodVolumeRestoreActionExecute(t *testing.T) { VolumeMounts(builder.ForVolumeMount("myvol", "/restores/myvol").Result()). Command([]string{"/velero-restore-helper"}).Result()).Result(), }, + { + name: "pod volume backups in a different namespace are ignored when looking for matches due to namespace scoping", + pod: builder.ForPod("ns-1", "my-pod"). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + Result(), + podVolumeBackups: []runtime.Object{ + builder.ForPodVolumeBackup("other-ns", "pvb-1"). + PodName("my-pod"). + PodNamespace("ns-1"). + Volume("myvol"). + ObjectMeta(builder.WithLabels(velerov1api.BackupNameLabel, backupName)). + SnapshotID("foo"). + Result(), + }, + want: builder.ForPod("ns-1", "my-pod"). + Volumes( + builder.ForVolume("myvol").PersistentVolumeClaimSource("pvc-1").Result(), + ). + Result(), + }, } veleroDeployment := &appsv1api.Deployment{ diff --git a/pkg/test/fake_controller_runtime_client.go b/pkg/test/fake_controller_runtime_client.go index ec22a3dc6a..90ee95d11d 100644 --- a/pkg/test/fake_controller_runtime_client.go +++ b/pkg/test/fake_controller_runtime_client.go @@ -19,7 +19,7 @@ package test import ( "testing" - volumegroupsnapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta1" + volumegroupsnapshotv1beta2 "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumegroupsnapshot/v1beta2" snapshotv1api "github.com/kubernetes-csi/external-snapshotter/client/v8/apis/volumesnapshot/v1" "github.com/stretchr/testify/require" @@ -45,7 +45,7 @@ func NewFakeControllerRuntimeClientBuilder(t *testing.T) *k8sfake.ClientBuilder require.NoError(t, appsv1api.AddToScheme(scheme)) require.NoError(t, snapshotv1api.AddToScheme(scheme)) require.NoError(t, storagev1api.AddToScheme(scheme)) - require.NoError(t, volumegroupsnapshotv1beta1.AddToScheme(scheme)) + require.NoError(t, volumegroupsnapshotv1beta2.AddToScheme(scheme)) return k8sfake.NewClientBuilder().WithScheme(scheme) } @@ -61,7 +61,7 @@ func NewFakeControllerRuntimeClient(t *testing.T, initObjs ...runtime.Object) cl require.NoError(t, snapshotv1api.AddToScheme(scheme)) require.NoError(t, storagev1api.AddToScheme(scheme)) require.NoError(t, batchv1api.AddToScheme(scheme)) - require.NoError(t, volumegroupsnapshotv1beta1.AddToScheme(scheme)) + require.NoError(t, volumegroupsnapshotv1beta2.AddToScheme(scheme)) return k8sfake.NewClientBuilder().WithScheme(scheme).WithRuntimeObjects(initObjs...).Build() } diff --git a/pkg/uploader/cbt/bitmap.go b/pkg/uploader/cbt/bitmap.go new file mode 100644 index 0000000000..6cf6e61e0b --- /dev/null +++ b/pkg/uploader/cbt/bitmap.go @@ -0,0 +1,55 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cbt + +import "github.com/vmware-tanzu/velero/pkg/cbtservice" + +// Bitmap defines the methods to store and iterate the CBT bitmap +type Bitmap interface { + // Set sets bits within the provided range + Set(cbtservice.Range) + + // SetFull sets all bits to the bitmap + SetFull() + + // Snapshot returns snapshot of the bitmap + SourceID() string + + // ChangeID returns the changeID of the bitmap + ChangeID() string + + // Iterator returns the iterator for the CBT Bitmap + Iterator() Iterator +} + +// Iterator defines the methods to iterate the CBT bitmap and query the associated information +type Iterator interface { + // ChangeID returns the changeID of the bitmap + ChangeID() string + + // Snapshot returns snapshot of the bitmap + Snapshot() string + + // BlockSize returns the granularity of the bitmap + BlockSize() int + + // Count returns the toal number of count in the bitmap + Count() uint64 + + // Next returns the offset of the next set block and whether it comes to the end of the iteration + Next() (int64, bool) +} diff --git a/pkg/uploader/cbt/set.go b/pkg/uploader/cbt/set.go new file mode 100644 index 0000000000..40e6e331cf --- /dev/null +++ b/pkg/uploader/cbt/set.go @@ -0,0 +1,49 @@ +/* +Copyright The Velero Contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cbt + +import ( + "context" + + "github.com/vmware-tanzu/velero/pkg/cbtservice" +) + +// SetBitmapOrFull translates the allocated/changed blocks from CBT service to the given bitmap or set the bitmap to full when error happens +func SetBitmapOrFull(ctx context.Context, service cbtservice.Service, bitmap Bitmap) error { + var err error + if bitmap.ChangeID() == "" { + err = setFromAllocatedBlocks(ctx, service, bitmap) + } else { + err = setFromChangedBlocks(ctx, service, bitmap) + } + + if err != nil { + bitmap.SetFull() + } + + return err +} + +// TODO implement in following PRs +func setFromAllocatedBlocks(_ context.Context, _ cbtservice.Service, _ Bitmap) error { + return nil +} + +// TODO implement in following PRs +func setFromChangedBlocks(_ context.Context, _ cbtservice.Service, _ Bitmap) error { + return nil +} diff --git a/pkg/uploader/kopia/shim.go b/pkg/uploader/kopia/shim.go index 1b9812d487..f146348fad 100644 --- a/pkg/uploader/kopia/shim.go +++ b/pkg/uploader/kopia/shim.go @@ -183,8 +183,8 @@ func (sr *shimRepository) NewObjectWriter(ctx context.Context, option object.Wri opt.DataType = udmrepo.ObjectDataTypeData } - writer := sr.udmRepo.NewObjectWriter(ctx, opt) - if writer == nil { + writer, err := sr.udmRepo.NewObjectWriter(ctx, opt) + if err != nil || writer == nil { return nil } diff --git a/pkg/uploader/kopia/shim_test.go b/pkg/uploader/kopia/shim_test.go index 69d1605a6c..7933ec6b8f 100644 --- a/pkg/uploader/kopia/shim_test.go +++ b/pkg/uploader/kopia/shim_test.go @@ -66,7 +66,7 @@ func TestShimRepo(t *testing.T) { backupRepo.On("Flush", mock.Anything).Return(nil) NewShimRepo(backupRepo).Flush(ctx) - backupRepo.On("NewObjectWriter", mock.Anything, mock.Anything).Return(nil) + backupRepo.On("NewObjectWriter", mock.Anything, mock.Anything).Return(nil, nil) NewShimRepo(backupRepo).NewObjectWriter(ctx, object.WriterOptions{}) } diff --git a/pkg/util/collections/includes_excludes.go b/pkg/util/collections/includes_excludes.go index ab63eaa720..f326a4124c 100644 --- a/pkg/util/collections/includes_excludes.go +++ b/pkg/util/collections/includes_excludes.go @@ -173,7 +173,6 @@ func (nie *NamespaceIncludesExcludes) ExpandIncludesExcludes() error { } // ResolveNamespaceList returns a list of all namespaces which will be backed up. -// The second return value indicates whether wildcard expansion was performed. func (nie *NamespaceIncludesExcludes) ResolveNamespaceList() ([]string, error) { // Check if this is being called by non-backup processing e.g. backup queue controller if !nie.wildcardExpanded { diff --git a/pkg/util/csi/volume_snapshot.go b/pkg/util/csi/volume_snapshot.go index 57e6f2e1d2..ed6371f7b5 100644 --- a/pkg/util/csi/volume_snapshot.go +++ b/pkg/util/csi/volume_snapshot.go @@ -708,17 +708,18 @@ func DiagnoseVS(vs *snapshotv1api.VolumeSnapshot, events *corev1api.EventList) s } } - diag := fmt.Sprintf("VS %s/%s, bind to %s, readyToUse %v, errMessage %s\n", vs.Namespace, vs.Name, vscName, readyToUse, errMessage) + var diag strings.Builder + _, _ = fmt.Fprintf(&diag, "VS %s/%s, bind to %s, readyToUse %v, errMessage %s\n", vs.Namespace, vs.Name, vscName, readyToUse, errMessage) if events != nil { for _, e := range events.Items { if e.InvolvedObject.UID == vs.UID && e.Type == corev1api.EventTypeWarning { - diag += fmt.Sprintf("VS event reason %s, message %s\n", e.Reason, e.Message) + _, _ = fmt.Fprintf(&diag, "VS event reason %s, message %s\n", e.Reason, e.Message) } } } - return diag + return diag.String() } func DiagnoseVSC(vsc *snapshotv1api.VolumeSnapshotContent) string { diff --git a/pkg/util/kube/pod.go b/pkg/util/kube/pod.go index 4ff05b43ee..4dc423272a 100644 --- a/pkg/util/kube/pod.go +++ b/pkg/util/kube/pod.go @@ -20,6 +20,7 @@ import ( "fmt" "io" "os" + "strings" "time" "github.com/pkg/errors" @@ -183,16 +184,16 @@ func GetPodContainerTerminateMessage(pod *corev1api.Pod, container string) strin // GetPodTerminateMessage returns the terminate message for all containers of a pod func GetPodTerminateMessage(pod *corev1api.Pod) string { - message := "" + var message strings.Builder for _, containerStatus := range pod.Status.ContainerStatuses { if containerStatus.State.Terminated != nil { if containerStatus.State.Terminated.Message != "" { - message += containerStatus.State.Terminated.Message + "/" + message.WriteString(containerStatus.State.Terminated.Message + "/") } } } - return message + return message.String() } func getPodLogReader(ctx context.Context, podGetter corev1client.CoreV1Interface, pod string, namespace string, logOptions *corev1api.PodLogOptions) (io.ReadCloser, error) { @@ -272,21 +273,22 @@ func ToSystemAffinity(loadAffinity *LoadAffinity, volumeTopology *corev1api.Node } func DiagnosePod(pod *corev1api.Pod, events *corev1api.EventList) string { - diag := fmt.Sprintf("Pod %s/%s, phase %s, node name %s, message %s\n", pod.Namespace, pod.Name, pod.Status.Phase, pod.Spec.NodeName, pod.Status.Message) + var diag strings.Builder + _, _ = fmt.Fprintf(&diag, "Pod %s/%s, phase %s, node name %s, message %s\n", pod.Namespace, pod.Name, pod.Status.Phase, pod.Spec.NodeName, pod.Status.Message) for _, condition := range pod.Status.Conditions { - diag += fmt.Sprintf("Pod condition %s, status %s, reason %s, message %s\n", condition.Type, condition.Status, condition.Reason, condition.Message) + _, _ = fmt.Fprintf(&diag, "Pod condition %s, status %s, reason %s, message %s\n", condition.Type, condition.Status, condition.Reason, condition.Message) } if events != nil { for _, e := range events.Items { if e.InvolvedObject.UID == pod.UID && e.Type == corev1api.EventTypeWarning { - diag += fmt.Sprintf("Pod event reason %s, message %s\n", e.Reason, e.Message) + _, _ = fmt.Fprintf(&diag, "Pod event reason %s, message %s\n", e.Reason, e.Message) } } } - return diag + return diag.String() } var funcExit = os.Exit diff --git a/pkg/util/kube/pvc_pv.go b/pkg/util/kube/pvc_pv.go index d5d2e20416..fa886bf604 100644 --- a/pkg/util/kube/pvc_pv.go +++ b/pkg/util/kube/pvc_pv.go @@ -464,17 +464,18 @@ func GetPVCForPodVolume(vol *corev1api.Volume, pod *corev1api.Pod, crClient crcl } func DiagnosePVC(pvc *corev1api.PersistentVolumeClaim, events *corev1api.EventList) string { - diag := fmt.Sprintf("PVC %s/%s, phase %s, binding to %s\n", pvc.Namespace, pvc.Name, pvc.Status.Phase, pvc.Spec.VolumeName) + var diag strings.Builder + _, _ = fmt.Fprintf(&diag, "PVC %s/%s, phase %s, binding to %s\n", pvc.Namespace, pvc.Name, pvc.Status.Phase, pvc.Spec.VolumeName) if events != nil { for _, e := range events.Items { if e.InvolvedObject.UID == pvc.UID && e.Type == corev1api.EventTypeWarning { - diag += fmt.Sprintf("PVC event reason %s, message %s\n", e.Reason, e.Message) + _, _ = fmt.Fprintf(&diag, "PVC event reason %s, message %s\n", e.Reason, e.Message) } } } - return diag + return diag.String() } func DiagnosePV(pv *corev1api.PersistentVolume) string { diff --git a/pkg/util/wildcard/expand.go b/pkg/util/wildcard/expand.go index 8767c8ed37..4a88f14640 100644 --- a/pkg/util/wildcard/expand.go +++ b/pkg/util/wildcard/expand.go @@ -9,6 +9,11 @@ import ( ) func ShouldExpandWildcards(includes []string, excludes []string) bool { + // Empty includes is equivalent to * (match all) - don't expand + if len(includes) == 0 { + return false + } + wildcardFound := false for _, include := range includes { // Special case: "*" alone means "match all" - don't expand diff --git a/pkg/util/wildcard/expand_test.go b/pkg/util/wildcard/expand_test.go index 3170206489..5730956cf5 100644 --- a/pkg/util/wildcard/expand_test.go +++ b/pkg/util/wildcard/expand_test.go @@ -68,6 +68,12 @@ func TestShouldExpandWildcards(t *testing.T) { excludes: []string{}, expected: false, }, + { + name: "empty includes with wildcard excludes - should not expand", + includes: []string{}, + excludes: []string{"ns*"}, + expected: false, + }, { name: "complex wildcard patterns", includes: []string{"*-prod"}, diff --git a/site/content/community/_index.md b/site/content/community/_index.md index c9eebef314..70e31c8690 100644 --- a/site/content/community/_index.md +++ b/site/content/community/_index.md @@ -13,11 +13,10 @@ You can follow the work we do, see our milestones, and our backlog on our [GitHu * Follow us on Twitter at [@projectvelero](https://twitter.com/projectvelero) * Join our Kubernetes Slack channel and talk to over 800 other community members: [#velero-users](https://kubernetes.slack.com/messages/velero-users) -* Join our [Google Group](https://groups.google.com/forum/#!forum/projectvelero) to get updates on the project and invites to community meetings. -* Join the Velero community meetings - [Zoom link](https://broadcom.zoom.us/j/94416678753?pwd=YkptN1k4M2lrUTdGbitNTmorODcvUT09) +* Join the Velero community meetings Bi-weekly community meeting alternating every week between Beijing Friendly timezone and EST/Europe Friendly Timezone - * Beijing/US friendly - we start at 8am Beijing Time(bound to CST) / 8pm EDT(7pm EST) / 5pm PDT(4pm PST) / 2am CEST(1am CET) - [Convert to your time zone](https://dateful.com/convert/beijing-china?t=8am) - * US/Europe friendly - we start at 10am ET(bound to ET) / 7am PT / 3pm CET / 10pm(11pm) CST - [Convert to your time zone](https://dateful.com/convert/est-edt-eastern-time?t=10) -* Read and comment on the [meeting notes](https://hackmd.io/bxrvgewUQ5ORH10BKUFpxw) + * Beijing/US friendly - we start at 8am Beijing Time(bound to CST) / 8pm EDT(7pm EST) / 5pm PDT(4pm PST) / 2am CEST(1am CET) - [Convert to your time zone](https://dateful.com/convert/beijing-china?t=8am) - [Zoom Link](https://broadcom.zoom.us/j/93945566592?pwd=rovF20vuI73kR6v67QBMpQuJOtM6sr.1&jst=2) + * US/Europe friendly - we start at 10am ET(bound to ET) / 7am PT / 3pm CET / 10pm(11pm) CST - [Convert to your time zone](https://dateful.com/convert/est-edt-eastern-time?t=10) - [Google meet link](https://meet.google.com/dyr-djtj-sko) +* Read and comment on the [meeting notes](https://hackmd.io/fCDVjqGuTG23CoOWQpoEVg) * See previous community meetings on our [YouTube Channel](https://www.youtube.com/playlist?list=PL7bmigfV0EqQRysvqvqOtRNk4L5S7uqwM) * Have a question to discuss in the community meeting? Please add it to our [Q&A Discussion board](https://github.com/vmware-tanzu/velero/discussions/categories/community-support-q-a) diff --git a/site/content/docs/main/customize-installation.md b/site/content/docs/main/customize-installation.md index c73142fdf5..7774565f18 100644 --- a/site/content/docs/main/customize-installation.md +++ b/site/content/docs/main/customize-installation.md @@ -342,6 +342,12 @@ If you are installing Velero in Kubernetes 1.14.x or earlier, you need to use `k If you intend to use Velero with a storage provider that is secured by a self-signed certificate, you may need to instruct Velero to trust that certificate. See [use Velero with a storage provider secured by a self-signed certificate][9] for details. +## Enabling parallel/concurrent backup processing + +By default, only one backup is processed in the `InProgress` phase at a time. The install flag `concurrent-backups`, which takes an integer argument, configures Velero to process multiple backups at the same time, up to a max of `concurrent-backups`. The other restriction on parallel backup processing is that two backups which have any included namespaces in common may not run at the same time. For example, if `concurrent-backups` is set to 2 and two backups for "namespace1" are submitted at the same time, only one of those will be processed at the same time. On the other hand, if a backup for "namespace1" and another for "namespace2" are submitted, then both can be processed in parallel. Note that a whole-cluster backup (one which does not restrict to a set list of namespaces) includes all namespaces, and therefore it will not run in parallel with any other backup. + +Enabling parallel backups can provide a significant performance benefit for backups which contain a large number of Kubernetes resources or ones which contain a large number of smaller volumes. Backups dominated by large volumes will not see as much benefit, since the majority of time for those backups is spent waiting for the async phase to complete. A larger `concurrent-backups` configuration may require additional memory and CPU resources for the velero container. + ## Additional options Run `velero install --help` or see the [Helm chart documentation](https://vmware-tanzu.github.io/helm-charts/) for the full set of installation options. diff --git a/site/content/docs/main/namespace-glob-patterns.md b/site/content/docs/main/namespace-glob-patterns.md index 4695124eae..c4cea9195a 100644 --- a/site/content/docs/main/namespace-glob-patterns.md +++ b/site/content/docs/main/namespace-glob-patterns.md @@ -5,6 +5,8 @@ layout: docs When using `--include-namespaces` and `--exclude-namespaces` flags with backup and restore commands, you can use glob patterns to match multiple namespaces. +Note: If the resolution of namespace patterns results in no namespaces, the backup will succeed with a warning. + ## Supported Patterns Velero supports the following glob pattern characters: diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index 3d650dfbe7..909cc80743 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -685,6 +685,12 @@ var _ = Describe( LoadAffinities, ) +var _ = Describe( + "Test data mover dynamic Cache PVC injection", + Label("NodeAgentConfig", "CachePVC"), + CachePVCTest, +) + func GetKubeConfigContext() error { var err error var tcDefault, tcStandby k8s.TestClient diff --git a/test/e2e/nodeagentconfig/cache_pvc.go b/test/e2e/nodeagentconfig/cache_pvc.go new file mode 100644 index 0000000000..9104946c79 --- /dev/null +++ b/test/e2e/nodeagentconfig/cache_pvc.go @@ -0,0 +1,253 @@ +/* +Copyright the Velero contributors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package nodeagentconfig + +import ( + "context" + "fmt" + "strings" + "time" + + . "github.com/onsi/gomega" + "github.com/pkg/errors" + corev1api "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/wait" + "sigs.k8s.io/controller-runtime/pkg/client" + + velerov1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v1" + velerov2alpha1api "github.com/vmware-tanzu/velero/pkg/apis/velero/v2alpha1" + "github.com/vmware-tanzu/velero/pkg/builder" + "github.com/vmware-tanzu/velero/test" + . "github.com/vmware-tanzu/velero/test/e2e/test" + k8sutil "github.com/vmware-tanzu/velero/test/util/k8s" + veleroutil "github.com/vmware-tanzu/velero/test/util/velero" +) + +type CachePVCTestCase struct { + TestCase + nodeAgentConfigMapName string + repoConfigMapName string +} + +var CachePVCTest func() = TestFunc(&CachePVCTestCase{ + nodeAgentConfigMapName: "node-agent-config-cache", + repoConfigMapName: "backup-repo-config-cache", +}) + +func (c *CachePVCTestCase) Init() error { + c.TestCase.Init() + c.CaseBaseName = "cache-pvc-" + c.UUIDgen + c.BackupName = "backup-" + c.CaseBaseName + c.RestoreName = "restore-" + c.CaseBaseName + c.NamespacesTotal = 1 + c.NSIncluded = &[]string{} + for nsNum := 0; nsNum < c.NamespacesTotal; nsNum++ { + createNSName := fmt.Sprintf("%s-%00000d", c.CaseBaseName, nsNum) + *c.NSIncluded = append(*c.NSIncluded, createNSName) + } + + c.VeleroCfg.UseNodeAgent = true + c.VeleroCfg.UseNodeAgentWindows = true + + // Ensure Data Mover is used to trigger DataUpload/DataDownload pods + c.BackupArgs = []string{ + "create", "--namespace", c.VeleroCfg.VeleroNamespace, "backup", c.BackupName, + "--include-namespaces", strings.Join(*c.NSIncluded, ","), + "--snapshot-volumes=true", "--snapshot-move-data", + } + + c.RestoreArgs = []string{ + "create", "--namespace", c.VeleroCfg.VeleroNamespace, "restore", c.RestoreName, + "--from-backup", c.BackupName, + } + + c.TestMsg = &TestMSG{ + Desc: "Validate dynamically provisioned Cache PVC for data mover pods", + FailedMSG: "Failed to apply and validate cache PVC configuration in data mover pods.", + Text: "Should dynamically provision a Cache PVC for data mover restore pods to avoid node root FS exhaustion.", + } + return nil +} + +func (c *CachePVCTestCase) InstallVelero() error { + fmt.Println("Start to uninstall Velero") + if err := veleroutil.VeleroUninstall(c.Ctx, c.VeleroCfg); err != nil { + fmt.Printf("Fail to uninstall Velero: %s\n", err.Error()) + return err + } + + // 1. Construct node-agent ConfigMap (Define Cache PVC StorageClass and trigger threshold) + // Set residentThresholdInMB to 0 to force Velero to create a PVC even for tiny E2E test data. + nodeAgentConfigJSON := fmt.Sprintf(`{"cachePVC": {"residentThresholdInMB": 0, "storageClass": "%s"}}`, test.StorageClassName) + nodeAgentConfig := builder.ForConfigMap(c.VeleroCfg.VeleroNamespace, c.nodeAgentConfigMapName). + Data("node-agent-config", nodeAgentConfigJSON).Result() + + // 2. Construct backup repository ConfigMap (Define cacheLimitMB) + repoConfigJSON := `{"cacheLimitMB": 2048}` // Set 2GB cache limit + repoConfig := builder.ForConfigMap(c.VeleroCfg.VeleroNamespace, c.repoConfigMapName). + Data(test.UploaderTypeKopia, repoConfigJSON).Result() + + c.VeleroCfg.NodeAgentConfigMap = c.nodeAgentConfigMapName + c.VeleroCfg.BackupRepoConfigMap = c.repoConfigMapName + + // Deploy Velero with the two Cache configuration ConfigMaps + return veleroutil.PrepareVelero(c.Ctx, c.CaseBaseName, c.VeleroCfg, nodeAgentConfig, repoConfig) +} + +func (c *CachePVCTestCase) CreateResources() error { + for _, ns := range *c.NSIncluded { + if err := k8sutil.CreateNamespace(c.Ctx, c.Client, ns); err != nil { + return err + } + + pvc, err := k8sutil.CreatePVC(c.Client, ns, "volume-1", test.StorageClassName, nil) + if err != nil { + return err + } + + vols := k8sutil.CreateVolumes(pvc.Name, []string{"volume-1"}) + + deployment := k8sutil.NewDeployment( + c.CaseBaseName, + ns, + 1, + map[string]string{"app": "test"}, + c.VeleroCfg.ImageRegistryProxy, + c.VeleroCfg.WorkerOS, + ).WithVolume(vols).Result() + + deployment, err = k8sutil.CreateDeployment(c.Client.ClientGo, ns, deployment) + if err != nil { + return errors.Wrap(err, "failed to create deployment") + } + + if err := k8sutil.WaitForReadyDeployment(c.Client.ClientGo, deployment.Namespace, deployment.Name); err != nil { + return err + } + } + return nil +} + +// verifyCacheVolumeInPod correctly verifies that the Pod mounted a dynamic PVC instead of an emptyDir +func (c *CachePVCTestCase) verifyCacheVolumeInPod(pod corev1api.Pod) error { + for _, vol := range pod.Spec.Volumes { + if vol.PersistentVolumeClaim != nil { + // Velero dynamically provisioned cache volumes typically have 'cache' in the name + if strings.Contains(vol.PersistentVolumeClaim.ClaimName, "cache") || strings.Contains(vol.Name, "cache") { + return nil // Success: Found the dynamically provisioned Cache PVC + } + } + } + return fmt.Errorf("Dynamically provisioned Cache PVC not found in pod %s, feature failed!", pod.Name) +} + +func (c *CachePVCTestCase) Backup() error { + if err := veleroutil.VeleroCmdExec(c.Ctx, c.VeleroCfg.VeleroCLI, c.BackupArgs); err != nil { + return err + } + + fmt.Println("Waiting for backup to complete...") + + // Wait for backup completion + wait.PollUntilContextTimeout(c.Ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { + backup := new(velerov1api.Backup) + if err := c.VeleroCfg.ClientToInstallVelero.Kubebuilder.Get( + c.Ctx, + client.ObjectKey{Namespace: c.VeleroCfg.VeleroNamespace, Name: c.BackupName}, + backup, + ); err != nil { + return false, err + } + + if backup.Status.Phase != velerov1api.BackupPhaseCompleted && + backup.Status.Phase != velerov1api.BackupPhaseFailed && + backup.Status.Phase != velerov1api.BackupPhasePartiallyFailed { + return false, nil + } + + return true, nil + }) + + return nil +} + +func (c *CachePVCTestCase) Restore() error { + if err := veleroutil.VeleroCmdExec(c.Ctx, c.VeleroCfg.VeleroCLI, c.RestoreArgs); err != nil { + return err + } + + restorePodList := new(corev1api.PodList) + + wait.PollUntilContextTimeout(c.Ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { + ddList := new(velerov2alpha1api.DataDownloadList) + if err := c.VeleroCfg.ClientToInstallVelero.Kubebuilder.List( + c.Ctx, + ddList, + &client.ListOptions{Namespace: c.VeleroCfg.VeleroNamespace}, + ); err != nil { + return false, err + } else if len(ddList.Items) <= 0 { + return false, nil + } + + if err := c.VeleroCfg.ClientToInstallVelero.Kubebuilder.List( + c.Ctx, + restorePodList, + &client.ListOptions{ + LabelSelector: labels.SelectorFromSet(map[string]string{ + velerov1api.DataDownloadLabel: ddList.Items[0].Name, + }), + }); err != nil { + return false, err + } else if len(restorePodList.Items) <= 0 { + return false, nil + } + + return true, nil + }) + + fmt.Println("Start to verify restore data mover pod content.") + Expect(restorePodList.Items).ToNot(BeEmpty()) + + // Ensure the Data Mover pod is using a true PVC for caching + err := c.verifyCacheVolumeInPod(restorePodList.Items[0]) + Expect(err).To(Succeed(), "Injected Cache PVC should exist in DataDownload Pod") + fmt.Println("Restore data mover pod content verification completed successfully.") + + // Wait for restore completion + wait.PollUntilContextTimeout(c.Ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { + restore := new(velerov1api.Restore) + if err := c.VeleroCfg.ClientToInstallVelero.Kubebuilder.Get( + c.Ctx, + client.ObjectKey{Namespace: c.VeleroCfg.VeleroNamespace, Name: c.RestoreName}, + restore, + ); err != nil { + return false, err + } + + if restore.Status.Phase != velerov1api.RestorePhaseCompleted && + restore.Status.Phase != velerov1api.RestorePhaseFailed && + restore.Status.Phase != velerov1api.RestorePhasePartiallyFailed { + return false, nil + } + + return true, nil + }) + + return nil +} diff --git a/test/e2e/nodeagentconfig/node-agent-config.go b/test/e2e/nodeagentconfig/node-agent-config.go index dc88d98bdb..5266bffc56 100644 --- a/test/e2e/nodeagentconfig/node-agent-config.go +++ b/test/e2e/nodeagentconfig/node-agent-config.go @@ -77,6 +77,14 @@ var LoadAffinities func() = TestFunc(&NodeAgentConfigTestCase{ IgnoreDelayBinding: true, }, PriorityClassName: test.PriorityClassNameForDataMover, + // Explicitly add custom labels and annotations to be tested + PodLabels: map[string]string{ + "spectrocloud.com/connection": "proxy", // Tests appending custom labels (Issue #9435) + "azure.workload.identity/use": "true", // Tests overwriting built-in labels + }, + PodAnnotations: map[string]string{ + "test-data-mover-annotation": "true", + }, }, nodeAgentConfigMapName: "node-agent-config", }) @@ -247,6 +255,16 @@ func (n *NodeAgentConfigTestCase) Backup() error { // but we can verify if the expected affinity is contained in the pod affinity. Expect(backupPodList.Items[0].Spec.Affinity.String()).To(ContainSubstring(expectedLabelKey)) + // Verify PodLabels + for k, v := range n.nodeAgentConfigs.PodLabels { + Expect(backupPodList.Items[0].Labels[k]).To(Equal(v)) + } + + // Verify PodAnnotations + for k, v := range n.nodeAgentConfigs.PodAnnotations { + Expect(backupPodList.Items[0].Annotations[k]).To(Equal(v)) + } + fmt.Println("backupPod content verification completed successfully.") wait.PollUntilContextTimeout(n.Ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) { @@ -328,6 +346,16 @@ func (n *NodeAgentConfigTestCase) Restore() error { // but we can verify if the expected affinity is contained in the pod affinity. Expect(restorePodList.Items[0].Spec.Affinity.String()).To(ContainSubstring(expectedLabelKey)) + // Verify PodLabels + for k, v := range n.nodeAgentConfigs.PodLabels { + Expect(restorePodList.Items[0].Labels[k]).To(Equal(v)) + } + + // Verify PodAnnotations + for k, v := range n.nodeAgentConfigs.PodAnnotations { + Expect(restorePodList.Items[0].Annotations[k]).To(Equal(v)) + } + fmt.Println("restorePod content verification completed successfully.") wait.PollUntilContextTimeout(n.Ctx, 5*time.Second, 5*time.Minute, true, func(ctx context.Context) (bool, error) {