diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3f5041b0..b9188009 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -7,4 +7,8 @@ Thanks for trying to improve the csi-cloudscale experience. Pull Request (to get the correct PR number). 2. If it's a breaking change or exceeds 100 lines of code then please open an issue first and describe the changes you want to make. - +3. Before opening a PR, make sure to run: + ``` + make lint-fix + make test + ``` diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c82e6f80..4babc61b 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -5,16 +5,20 @@ updates: directory: "/" schedule: interval: "weekly" - ignore: - - dependency-name: "*" - update-types: - - "version-update:semver-patch" + day: "monday" + groups: + actions: + patterns: + - "*" + cooldown: + default-days: 7 # Check for Go module updates - package-ecosystem: "gomod" directory: "/" schedule: interval: "weekly" + day: "monday" allow: - dependency-type: "direct" groups: @@ -34,23 +38,32 @@ updates: directory: "/charts/csi-cloudscale" schedule: interval: "weekly" + day: "monday" + cooldown: + default-days: 7 # Check docker base image updates - package-ecosystem: "docker" directory: "/cmd/cloudscale-csi-plugin" schedule: interval: "weekly" + day: "monday" ignore: - dependency-name: "*" update-types: - "version-update:semver-patch" + cooldown: + default-days: 7 # Check docker side-car containers in helm updates - package-ecosystem: "docker" directory: "/charts/csi-cloudscale" schedule: interval: "weekly" + day: "monday" ignore: - dependency-name: "*" update-types: - "version-update:semver-patch" + cooldown: + default-days: 7 diff --git a/.github/workflows/govulncheck.yml b/.github/workflows/govulncheck.yml new file mode 100644 index 00000000..7f87b6f7 --- /dev/null +++ b/.github/workflows/govulncheck.yml @@ -0,0 +1,29 @@ +name: Govulncheck + +permissions: {} + +on: + push: + branches: [master] + pull_request: + +jobs: + govulncheck: + permissions: + contents: read + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Run govulncheck + run: make govulncheck diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 00000000..2cac4209 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,29 @@ +name: Lint + +permissions: {} + +on: + push: + branches: [master] + pull_request: + +jobs: + lint: + permissions: + contents: read + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Run linter + run: make lint diff --git a/.github/workflows/release-chart.yml b/.github/workflows/release-chart.yml index 56fa0704..71836251 100644 --- a/.github/workflows/release-chart.yml +++ b/.github/workflows/release-chart.yml @@ -1,5 +1,7 @@ name: Release Charts +permissions: {} + on: push: branches: @@ -9,11 +11,14 @@ on: jobs: release: runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + persist-credentials: false - name: Configure Git run: | @@ -21,7 +26,7 @@ jobs: git config user.email "$GITHUB_ACTOR@users.noreply.github.com" - name: Install Helm - uses: azure/setup-helm@v3.5 + uses: azure/setup-helm@5119fcb9089d432beecbf79bb2c7915207344b78 # v3 with: version: v3.9.3 @@ -32,7 +37,7 @@ jobs: done - name: Run chart-releaser - uses: helm/chart-releaser-action@v1.7.0 + uses: helm/chart-releaser-action@cae68fefc6b5f367a0275617c9f83181ba54714f # v1.7.0 with: config: cr.yaml env: diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml deleted file mode 100644 index 6c4151b3..00000000 --- a/.github/workflows/test.yaml +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2020 DigitalOcean -# -# 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. - -name: test - -on: - push: - branches: - - master - pull_request: - branches: - -jobs: - unit-test: - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 1 - - - name: Go setup - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 - with: - go-version-file: 'go.mod' - - - name: Run unit tests - run: make all diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..799e5b96 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,31 @@ +name: Tests + +permissions: {} + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + permissions: + contents: read + name: Run on Ubuntu + runs-on: ubuntu-latest + steps: + - name: Clone the code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Setup Go + uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 + with: + go-version-file: go.mod + + - name: Check Go modules + run: go mod tidy -diff + - name: Running Tests + run: make test TESTARGS='-race' diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml new file mode 100644 index 00000000..8f52398a --- /dev/null +++ b/.github/workflows/zizmor.yml @@ -0,0 +1,27 @@ +name: Workflow Security Lint + +permissions: {} + +on: + push: + branches: [master] + paths: ['.github/**'] + pull_request: + paths: ['.github/**'] + +jobs: + zizmor: + permissions: + actions: read + contents: read + security-events: write + name: zizmor + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 diff --git a/.gitignore b/.gitignore index faad6d19..ad951bc7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ charts/csi-cloudscale/charts cmd/cloudscale-csi-plugin/cloudscale-csi-plugin k8test/ +bin/ diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..f16299c2 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,91 @@ +version: "2" +run: + allow-parallel-runners: true +linters: + default: none + enable: + - asasalint # warns about passing []any to func(...any) without expanding it + - asciicheck # non ascii symbols + - copyloopvar # copying loop variables + - errcheck # unchecked errors + - exhaustive # exhaustiveness of enum switch statements + - gocritic # bugs, performance, style + - gocyclo # cyclomatic complexity of functions + - godoclint # go documentation linting against best practices + - gosec # potential security problems + - govet # basically 'go vet' + - importas # enforces consistent import aliases. + - ineffassign # ineffectual assignments + - loggercheck # check for even key/value pairs in logger calls + - modernize # suggest simplifications to Go code, using modern language and library features + - misspell # spelling checks + - nakedret # naked returns (named return parameters and an empty return) + # Context cancellation is not safe for state-changing system commands (luksOpen, luksClose, cryptsetup, mkfs, etc.) + # because cancellation creates uncertainty about the final system state. + # These operations must run to completion or fail definitively. + # - noctx # http requests without context.Context + - predeclared # shadowing predeclared identifiers + - revive # better version of golint + - staticcheck # many static checks + - unconvert # unnecessary type conversions + - unparam # unused function parameters + - unused # unused constants, variables,functions, types + - usestdlibvars # using variables/constants from the standard library + - usetesting # reports uses of functions with replacement inside the testing package + - whitespace # unnecessary newlines + settings: + loggercheck: + kitlog: false + slog: false + zap: false + require-string-key: true + no-printf-like: true + modernize: + disable: + - omitzero + gocritic: + disabled-checks: + - ifElseChain # disabled ifElseChain because this is purely stylistic + revive: + rules: + # The following rules are recommended https://github.com/mgechev/revive#recommended-configuration + - name: blank-imports + - name: context-as-argument + - name: context-keys-type + - name: error-return + - name: error-strings + - name: error-naming + - name: if-return + - name: increment-decrement + - name: var-naming + - name: var-declaration + - name: range + - name: receiver-naming + - name: time-naming + - name: unexported-return + - name: indent-error-flow + - name: errorf + - name: empty-block + - name: superfluous-else + - name: unreachable-code + - name: redefines-builtin-id + staticcheck: + dot-import-whitelist: + - fmt + exclusions: + generated: lax + warn-unused: true +formatters: + enable: + - goimports # ensures imports are organized + - gofmt # Check if the code is formatted according to 'gofmt' command. + settings: + goimports: + # A list of prefixes, which, if set, checks import paths + # with the given prefixes are grouped after 3rd-party packages. + # Default: [] + local-prefixes: + - github.com/cloudscale-ch/csi-cloudscale + exclusions: + generated: lax + warn-unused: true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..35e099f5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,94 @@ +# AGENTS.md — CSI-cloudscale-specific guidance for AI agents + +This file is a supplement for AI agents working on csi-cloudscale, the +Container Storage Interface driver for cloudscale.ch. For general project +information, see [`README.md`](README.md). For contribution guidelines, see +[`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md). + +## Related docs + +- [`README.md`](README.md) — project overview, installation, volume parameters +- [`.github/CONTRIBUTING.md`](.github/CONTRIBUTING.md) — PR flow, required local checks +- [`CHANGELOG.md`](CHANGELOG.md) — version history +- [`csi-spec.md`](csi-spec.md) — **authoritative CSI specification — MUST be consulted for all RPC implementations, error handling, idempotency requirements.** + +## CSI Specification Guidance + +The [`csi-spec.md`](csi-spec.md) is the authoritative source for this driver's behavior. **Always consult it when:** + +- Implementing or modifying any RPC method in `driver/controller.go`, `driver/node.go`, or `driver/identity.go`. +- Determining which gRPC error code to return for a failure condition. +- Understanding idempotency requirements for an operation. +- Handling edge cases (missing fields, concurrent operations, timeout recovery). + +**When the spec conflicts with existing code, highlight this very clearly**. + +## What to run after a change + +| You touched | Run | +|-------------------------------------------------|-------------------------------| +| `*.go` | `make lint-fix && make test` | +| `Makefile`, `.golangci.yml` | `make lint` | +| `charts/csi-cloudscale/` | `make helm-template` | + +## Project structure + +| Directory/File | Purpose | +|-----------------------------|---------------------------------------------------| +| `cmd/cloudscale-csi-plugin/` | Binary entry point and Dockerfile | +| `driver/` | CSI driver implementation (Controller, Node, etc.) | +| `charts/csi-cloudscale/` | Helm chart for deployment | +| `deploy/kubernetes/releases/`| Pre-rendered Kubernetes manifests | +| `examples/kubernetes/` | Example StorageClasses and PVCs | +| `test/kubernetes/` | Integration tests | + +## Key components + +- **`driver/controller.go`** — CSI Controller service implementation. + Handles all [CSI-defined API methods](https://github.com/container-storage-interface/spec). All non-read operations are guarded by + `volumeLocks` to prevent concurrent modifications of the same volume. + +- **`driver/node.go`** — CSI Node service implementation. + +- **`driver/luks_util.go`** — LUKS encryption utilities. + Provides functions for creating, opening, closing, and resizing LUKS + encrypted volumes using `cryptsetup`. + +- **`driver/mounter.go`** — Volume mounting logic. + Wraps mount operations, filesystem creation (ext4, xfs), and resize + operations. + +- **`driver/identity.go`** — CSI Identity service. + Reports driver name and capabilities. + +- **`driver/volumelocks.go`** — Per-volume mutual exclusion. + Simple string-keyed mutex map to prevent concurrent operations on the + same volume. + +## Testing conventions + +- Unit tests live next to the code they exercise (`*_test.go` in `driver/`). +- Tests use [testify](https://github.com/stretchr/testify) for assertions. +- The fake cloudscale client is implemented in `driver/driver_test.go`. +- Run `make test` to execute all unit tests. If race detection is wished, add it via TESTARGS="-race". +- Integration tests require a cloudscale.ch account and are run via + `make test-integration`. Do not run integration tests as an agent. Always ask the user to run them for you. + +## Release process + +See [`docs/releasing.md`](docs/releasing.md) for the full release process. + +In brief: +1. Update `VERSION` file and run `make bump-version NEW_VERSION=vX.Y.Z` +2. Optionally bump chart version: `make bump-chart-version NEW_CHART_VERSION=1.X.Y` +3. Commit and push tag (`vX.Y.Z`) +4. GitHub Actions runs `release-chart.yml` to publish the Helm chart + +## Dependency management + +- Go dependencies: `go mod tidy` (run automatically in CI if `go.mod` / `go.sum` + drift is detected). +- Tool binaries (`golangci-lint`, `govulncheck`) are installed on demand to + `./bin/$(GOOS)-$(GOARCH)/`. +- Kubernetes module versions are pinned via `replace` directives in `go.mod`. + Update them with `make update-k8s NEW_KUBERNETES_VERSION='1.34.0'`. diff --git a/Makefile b/Makefile index 6b7ec907..8797c8e5 100644 --- a/Makefile +++ b/Makefile @@ -15,18 +15,51 @@ VERSION ?= $(shell cat VERSION) CHART_VERSION ?= $(shell awk '/^version:/ {print $$2}' charts/csi-cloudscale/Chart.yaml) DOCKER_REPO ?= quay.io/cloudscalech/cloudscale-csi-plugin -all: lint check-unused test - -publish: build push clean +# Setting SHELL to bash allows bash commands to be executed by recipes. +# Options are set to exit when a recipe line exits non-zero or a piped command fails. +SHELL = /usr/bin/env bash -o pipefail +.SHELLFLAGS = -ec + +# Host OS/ARCH used to namespace tool binaries so a shared ./bin (e.g. host tree +# mounted into a Linux sandbox) never mixes incompatible binaries. +HOST_PLATFORM := $(shell go env GOOS)-$(shell go env GOARCH) + +## Location to install dependencies to (per-platform to keep host & sandbox separate) +LOCALBIN := $(shell pwd)/bin/$(HOST_PLATFORM) +$(LOCALBIN): + mkdir -p "$(LOCALBIN)" + +##@ General + +# The help target prints out all targets with their descriptions organized +# beneath their categories. The categories are represented by '##@' and the +# target descriptions by '##'. The awk command is responsible for reading the +# entire set of makefiles included in this invocation, looking for lines of the +# file as xyz: ## something, and then pretty-format the target and help. Then, +# if there's a line with ##@ something, that gets pretty-printed as a category. +# More info on the usage of ANSI control characters for terminal formatting: +# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters +# More info on the awk command: +# http://linuxcommand.org/lc3_adv_awk.php +# Based on Kubebuilders help target: https://github.com/kubernetes-sigs/kubebuilder/blob/f98c0ef7f0df9f83a3044d829effc336b126aaf6/pkg/plugins/golang/v4/scaffolds/internal/templates/makefile.go#L102-L117 + +.PHONY: all +all: lint test ## Run all checks (lint + vet + test) + +.PHONY: help +help: ## Display this help. + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST) + +##@ Development .PHONY: update-k8s -update-k8s: +update-k8s: ## Update Kubernetes version in go.mod scripts/update-k8s.sh $(NEW_KUBERNETES_VERSION) sed -i.sedbak "s/^KUBERNETES_VERSION.*/KUBERNETES_VERSION ?= $(NEW_KUBERNETES_VERSION)/" Makefile rm -f Makefile.sedbak .PHONY: bump-version -bump-version: +bump-version: ## Bump app version (e.g. make NEW_VERSION=v4.1.0 bump-version) @[ "${NEW_VERSION}" ] || ( echo "NEW_VERSION must be set (ex. make NEW_VERSION=v1.x.x bump-version)"; exit 1 ) @(echo ${NEW_VERSION} | grep -E "^v") || ( echo "NEW_VERSION must be a semver ('v' prefix is required)"; exit 1 ) @echo "Bumping VERSION from $(VERSION) to $(NEW_VERSION)" @@ -41,39 +74,57 @@ bump-version: @rm README.md-e CHANGELOG.md-e charts/csi-cloudscale/Chart.yaml-e charts/csi-cloudscale/values.yaml-e .PHONY: bump-chart-version -bump-chart-version: +bump-chart-version: ## Bump Helm chart version (e.g. make NEW_CHART_VERSION=1.6.0 bump-chart-version) @[ "${NEW_CHART_VERSION}" ] || ( echo "NEW_CHART_VERSION must be set (ex. make NEW_CHART_VERSION=v1.x.x bump-version)"; exit 1 ) @(echo ${NEW_CHART_VERSION} | grep -E "^v") || ( echo "NEW_CHART_VERSION must be a semver ('v' prefix is required)"; exit 1 ) @echo "Bumping CHART_VERSION from $(CHART_VERSION) to $(NEW_CHART_VERSION)" @sed -i'' -e 's/${CHART_VERSION:v%=%}/${NEW_CHART_VERSION:v%=%}/g' charts/csi-cloudscale/Chart.yaml @rm charts/csi-cloudscale/Chart.yaml-e -.PHONY: compile -compile: - @echo "==> Building the project" - @docker run --rm -it -e GOOS=${OS} -e GOARCH=amd64 -v ${PWD}/:/app -w /app golang:${GO_VERSION}-alpine sh -c 'apk add git && go build -o cmd/cloudscale-csi-plugin/${NAME} -ldflags "$(LDFLAGS)" ${PKG}' +.PHONY: fmt +fmt: ## Run go fmt against code. + go fmt ./... -.PHONY: check-unused -check-unused: - @git diff --exit-code -- go.sum go.mod || ( echo "there are uncommitted changes to the go.mod/go.sum -- please run 'go mod tidy' and commit the changes first"; exit 1 ) +.PHONY: vet +vet: ## Run go vet against code. + go vet ./... .PHONY: test -test: +test: vet ## Run tests. @echo "==> Testing all packages" - @GO111MODULE=on go test -race -v ./... $(TEST_ARGS) + go test -v ./... $(TESTARGS) .PHONY: test-integration -test-integration: +test-integration: ## Run integration tests @echo "==> Started integration tests" - @env GO111MODULE=on go test -race -count 1 -v $(TESTARGS) -tags integration -parallel 4 -timeout 20m ./test/... + @env go test -race -count 1 -v $(TESTARGS) -tags integration -parallel 4 -timeout 20m ./test/... + +.PHONY: lint +lint: golangci-lint ## Run golangci-lint linter + "$(GOLANGCI_LINT)" run + +.PHONY: lint-fix +lint-fix: golangci-lint ## Run golangci-lint linter and perform fixes + "$(GOLANGCI_LINT)" run --fix + +.PHONY: govulncheck +govulncheck: govulncheck-tool ## Run govulncheck (advisory only) + "$(GOVULNCHECK)" ./... + +##@ Build + +.PHONY: compile +compile: ## Build the project binary + @echo "==> Building the project" + @docker run --rm -it -e GOOS=${OS} -e GOARCH=amd64 -v ${PWD}/:/app -w /app golang:${GO_VERSION}-alpine sh -c 'apk add git && go build -o cmd/cloudscale-csi-plugin/${NAME} -ldflags "$(LDFLAGS)" ${PKG}' .PHONY: build -build: compile +build: compile ## Build the docker image @echo "==> Building the docker image" @docker build --platform linux/amd64 -t $(DOCKER_REPO):$(VERSION) cmd/cloudscale-csi-plugin -f cmd/cloudscale-csi-plugin/Dockerfile .PHONY: push -push: +push: ## Push docker image to registry ifeq ($(DOCKER_REPO),quay.io/cloudscalech/cloudscale-csi-plugin) ifeq ($(filter master,$(BRANCH))$(filter release/%,$(BRANCH)),) ifneq ($(VERSION),dev) @@ -85,23 +136,50 @@ endif @docker push $(DOCKER_REPO):$(VERSION) @echo "==> Your image is now available at $(DOCKER_REPO):$(VERSION)" +.PHONY: publish +publish: build push clean ## Build, push, and clean + .PHONY: clean -clean: +clean: ## Clean build artifacts @echo "==> Cleaning releases" @GOOS=${OS} go clean -i -x ./... .PHONY: helm-template -helm-template: +helm-template: ## Generate helm template @helm template csi-cloudscale -n kube-system --set nameOverride=csi-cloudscale ./charts/csi-cloudscale -## Development tooling - -fmt: ## Run go fmt against code. - go fmt ./... - -vet: ## Run go vet against code. - go vet ./... - -.PHONY: lint -lint: fmt vet ## Combined target to run linters - go tool -modfile tool.mod golangci-lint run --timeout 2m0s ./... \ No newline at end of file +##@ Tooling + +## Tool Versions +GOLANGCI_LINT_VERSION ?= v2.13.1 +GOVULNCHECK_VERSION ?= v1.7.0 + +## Tool Binaries +GOLANGCI_LINT = $(LOCALBIN)/golangci-lint +GOVULNCHECK ?= $(LOCALBIN)/govulncheck + +.PHONY: golangci-lint +golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. +$(GOLANGCI_LINT): $(LOCALBIN) + $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) + +.PHONY: govulncheck-tool +govulncheck-tool: $(GOVULNCHECK) ## Download govulncheck locally if necessary. +$(GOVULNCHECK): $(LOCALBIN) + $(call go-install-tool,$(GOVULNCHECK),golang.org/x/vuln/cmd/govulncheck,$(GOVULNCHECK_VERSION)) + +# go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist +# $1 - target path with name of binary +# $2 - package url which can be installed +# $3 - specific version of package +define go-install-tool +@[ -f "$(1)-$(3)" ] && [ "$$(readlink -- "$(1)" 2>/dev/null)" = "$(1)-$(3)" ] || { \ +set -e; \ +package=$(2)@$(3) ;\ +echo "Downloading $${package}" ;\ +rm -f "$(1)" ;\ +GOBIN="$(LOCALBIN)" go install $${package} ;\ +mv "$(LOCALBIN)/$$(basename "$(1)")" "$(1)-$(3)" ;\ +} ;\ +ln -sf "$$(realpath "$(1)-$(3)")" "$(1)" +endef diff --git a/cmd/cloudscale-csi-plugin/main.go b/cmd/cloudscale-csi-plugin/main.go index b4a40758..3a10ad7c 100644 --- a/cmd/cloudscale-csi-plugin/main.go +++ b/cmd/cloudscale-csi-plugin/main.go @@ -18,13 +18,17 @@ limitations under the License. package main import ( + "context" "flag" "fmt" "log" "os" + "os/signal" + "syscall" - "github.com/cloudscale-ch/csi-cloudscale/driver" "github.com/sirupsen/logrus" + + "github.com/cloudscale-ch/csi-cloudscale/driver" ) func main() { @@ -56,7 +60,10 @@ func main() { log.Fatalln(err) } - if err := drv.Run(); err != nil { + ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) + if err := drv.Run(ctx); err != nil { + cancel() log.Fatalln(err) } + cancel() } diff --git a/csi-spec.md b/csi-spec.md new file mode 100644 index 00000000..879dace6 --- /dev/null +++ b/csi-spec.md @@ -0,0 +1,3956 @@ +# Container Storage Interface (CSI) + +Authors: + +* Jie Yu <> (@jieyu) +* Saad Ali <> (@saad-ali) +* James DeFelice <> (@jdef) +* + +## Notational Conventions + +The keywords "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" are to be interpreted as described in [RFC 2119](http://tools.ietf.org/html/rfc2119) (Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, March 1997). + +The key words "unspecified", "undefined", and "implementation-defined" are to be interpreted as described in the [rationale for the C99 standard](http://www.open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf#page=18). + +An implementation is not compliant if it fails to satisfy one or more of the MUST, REQUIRED, or SHALL requirements for the protocols it implements. +An implementation is compliant if it satisfies all the MUST, REQUIRED, and SHALL requirements for the protocols it implements. + +## Terminology + +| Term | Definition | +|-------------------|--------------------------------------------------| +| Volume | A unit of storage that will be made available inside of a CO-managed container, via the CSI. | +| Block Volume | A volume that will appear as a block device inside the container. | +| Mounted Volume | A volume that will be mounted using the specified file system and appear as a directory inside the container. | +| CO | Container Orchestration system, communicates with Plugins using CSI service RPCs. | +| SP | Storage Provider, the vendor of a CSI plugin implementation. | +| RPC | [Remote Procedure Call](https://en.wikipedia.org/wiki/Remote_procedure_call). | +| Node | A host where the user workload will be running, uniquely identifiable from the perspective of a Plugin by a node ID. | +| Plugin | Aka “plugin implementation”, a gRPC endpoint that implements the CSI Services. | +| Plugin Supervisor | Process that governs the lifecycle of a Plugin, MAY be the CO. | +| Workload | The atomic unit of "work" scheduled by a CO. This MAY be a container or a collection of containers. | + +## Objective + +To define an industry standard “Container Storage Interface” (CSI) that will enable storage vendors (SP) to develop a plugin once and have it work across a number of container orchestration (CO) systems. + +### Goals in MVP + +The Container Storage Interface (CSI) will + +* Enable SP authors to write one CSI compliant Plugin that “just works” across all COs that implement CSI. +* Define API (RPCs) that enable: + * Dynamic provisioning and deprovisioning of a volume. + * Attaching or detaching a volume from a node. + * Mounting/unmounting a volume from a node. + * Consumption of both block and mountable volumes. + * Local storage providers (e.g., device mapper, lvm). + * Creating and deleting a snapshot (source of the snapshot is a volume). + * Provisioning a new volume from a snapshot (reverting snapshot, where data in the original volume is erased and replaced with data in the snapshot, is out of scope). +* Define plugin protocol RECOMMENDATIONS. + * Describe a process by which a Supervisor configures a Plugin. + * Container deployment considerations (`CAP_SYS_ADMIN`, mount namespace, etc.). + +### Non-Goals in MVP + +The Container Storage Interface (CSI) explicitly will not define, provide, or dictate: + +* Specific mechanisms by which a Plugin Supervisor manages the lifecycle of a Plugin, including: + * How to maintain state (e.g. what is attached, mounted, etc.). + * How to deploy, install, upgrade, uninstall, monitor, or respawn (in case of unexpected termination) Plugins. +* A first class message structure/field to represent "grades of storage" (aka "storage class"). +* Protocol-level authentication and authorization. +* Packaging of a Plugin. +* POSIX compliance: CSI provides no guarantee that volumes provided are POSIX compliant filesystems. + Compliance is determined by the Plugin implementation (and any backend storage system(s) upon which it depends). + CSI SHALL NOT obstruct a Plugin Supervisor or CO from interacting with Plugin-managed volumes in a POSIX-compliant manner. + +## Solution Overview + +This specification defines an interface along with the minimum operational and packaging recommendations for a storage provider (SP) to implement a CSI compatible plugin. +The interface declares the RPCs that a plugin MUST expose: this is the **primary focus** of the CSI specification. +Any operational and packaging recommendations offer additional guidance to promote cross-CO compatibility. + +### Architecture + +The primary focus of this specification is on the **protocol** between a CO and a Plugin. +It SHOULD be possible to ship cross-CO compatible Plugins for a variety of deployment architectures. +A CO SHOULD be equipped to handle both centralized and headless plugins, as well as split-component and unified plugins. +Several of these possibilities are illustrated in the following figures. + +``` + CO "Master" Host ++-------------------------------------------+ +| | +| +------------+ +------------+ | +| | CO | gRPC | Controller | | +| | +-----------> Plugin | | +| +------------+ +------------+ | +| | ++-------------------------------------------+ + + CO "Node" Host(s) ++-------------------------------------------+ +| | +| +------------+ +------------+ | +| | CO | gRPC | Node | | +| | +-----------> Plugin | | +| +------------+ +------------+ | +| | ++-------------------------------------------+ + +Figure 1: The Plugin runs on all nodes in the cluster: a centralized +Controller Plugin is available on the CO master host and the Node +Plugin is available on all of the CO Nodes. +``` + +``` + CO "Node" Host(s) ++-------------------------------------------+ +| | +| +------------+ +------------+ | +| | CO | gRPC | Controller | | +| | +--+--------> Plugin | | +| +------------+ | +------------+ | +| | | +| | | +| | +------------+ | +| | | Node | | +| +--------> Plugin | | +| +------------+ | +| | ++-------------------------------------------+ + +Figure 2: Headless Plugin deployment, only the CO Node hosts run +Plugins. Separate, split-component Plugins supply the Controller +Service and the Node Service respectively. +``` + +``` + CO "Node" Host(s) ++-------------------------------------------+ +| | +| +------------+ +------------+ | +| | CO | gRPC | Controller | | +| | +-----------> Node | | +| +------------+ | Plugin | | +| +------------+ | +| | ++-------------------------------------------+ + +Figure 3: Headless Plugin deployment, only the CO Node hosts run +Plugins. A unified Plugin component supplies both the Controller +Service and Node Service. +``` + +``` + CO "Node" Host(s) ++-------------------------------------------+ +| | +| +------------+ +------------+ | +| | CO | gRPC | Node | | +| | +-----------> Plugin | | +| +------------+ +------------+ | +| | ++-------------------------------------------+ + +Figure 4: Headless Plugin deployment, only the CO Node hosts run +Plugins. A Node-only Plugin component supplies only the Node Service. +Its GetPluginCapabilities RPC does not report the CONTROLLER_SERVICE +capability. +``` + +### Volume Lifecycle + +``` + CreateVolume +------------+ DeleteVolume + +------------->| CREATED +--------------+ + | +---+----^---+ | + | Controller | | Controller v ++++ Publish | | Unpublish +++ +|X| Volume | | Volume | | ++-+ +---v----+---+ +-+ + | NODE_READY | + +---+----^---+ + Node | | Node + Publish | | Unpublish + Volume | | Volume + +---v----+---+ + | PUBLISHED | + +------------+ + +Figure 5: The lifecycle of a dynamically provisioned volume, from +creation to destruction. +``` + +``` + CreateVolume +------------+ DeleteVolume + +------------->| CREATED +--------------+ + | +---+----^---+ | + | Controller | | Controller v ++++ Publish | | Unpublish +++ +|X| Volume | | Volume | | ++-+ +---v----+---+ +-+ + | NODE_READY | + +---+----^---+ + Node | | Node + Stage | | Unstage + Volume | | Volume + +---v----+---+ + | VOL_READY | + +---+----^---+ + Node | | Node + Publish | | Unpublish + Volume | | Volume + +---v----+---+ + | PUBLISHED | + +------------+ + +Figure 6: The lifecycle of a dynamically provisioned volume, from +creation to destruction, when the Node Plugin advertises the +STAGE_UNSTAGE_VOLUME capability. +``` + +``` + Controller Controller + Publish Unpublish + Volume +------------+ Volume + +------------->+ NODE_READY +--------------+ + | +---+----^---+ | + | Node | | Node v ++++ Publish | | Unpublish +++ +|X| <-+ Volume | | Volume | | ++++ | +---v----+---+ +-+ + | | | PUBLISHED | + | | +------------+ + +----+ + Validate + Volume + Capabilities + +Figure 7: The lifecycle of a pre-provisioned volume that requires +controller to publish to a node (`ControllerPublishVolume`) prior to +publishing on the node (`NodePublishVolume`). +``` + +``` + +-+ +-+ + |X| | | + +++ +^+ + | | + Node | | Node +Publish | | Unpublish + Volume | | Volume + +---v----+---+ + | PUBLISHED | + +------------+ + +Figure 8: Plugins MAY forego other lifecycle steps by contraindicating +them via the capabilities API. Interactions with the volumes of such +plugins is reduced to `NodePublishVolume` and `NodeUnpublishVolume` +calls. +``` + +The above diagrams illustrate a general expectation with respect to how a CO MAY manage the lifecycle of a volume via the API presented in this specification. +Plugins SHOULD expose all RPCs for an interface: Controller plugins SHOULD implement all RPCs for the `Controller` service. +Unsupported RPCs SHOULD return an appropriate error code that indicates such (e.g. `CALL_NOT_IMPLEMENTED`). +The full list of plugin capabilities is documented in the `ControllerGetCapabilities` and `NodeGetCapabilities` RPCs. + +## Container Storage Interface + +This section describes the interface between COs and Plugins. + +### RPC Interface + +A CO interacts with an Plugin through RPCs. +Each SP MUST provide: + +* **Node Plugin**: A gRPC endpoint serving CSI RPCs that MUST be run on the Node whereupon an SP-provisioned volume will be published. +* **Controller Plugin**: A gRPC endpoint serving CSI RPCs that MAY be run anywhere. +* In some circumstances a single gRPC endpoint MAY serve all CSI RPCs (see Figure 3 in [Architecture](#architecture)). + +```protobuf +syntax = "proto3"; +package csi.v1; + +import "google/protobuf/descriptor.proto"; +import "google/protobuf/timestamp.proto"; +import "google/protobuf/wrappers.proto"; + +option go_package = + "github.com/container-storage-interface/spec/lib/go/csi"; + +extend google.protobuf.EnumOptions { + // Indicates that this enum is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_enum = 1060; +} +extend google.protobuf.EnumValueOptions { + // Indicates that this enum value is OPTIONAL and part of an + // experimental API that may be deprecated and eventually removed + // between minor releases. + bool alpha_enum_value = 1060; +} +extend google.protobuf.FieldOptions { + // Indicates that a field MAY contain information that is sensitive + // and MUST be treated as such (e.g. not logged). + bool csi_secret = 1059; + + // Indicates that this field is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_field = 1060; +} +extend google.protobuf.MessageOptions { + // Indicates that this message is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_message = 1060; +} +extend google.protobuf.MethodOptions { + // Indicates that this method is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_method = 1060; +} +extend google.protobuf.ServiceOptions { + // Indicates that this service is OPTIONAL and part of an experimental + // API that may be deprecated and eventually removed between minor + // releases. + bool alpha_service = 1060; +} +``` + +There are three sets of RPCs: + +* **Identity Service**: Both the Node Plugin and the Controller Plugin MUST implement this sets of RPCs. +* **Controller Service**: The Controller Plugin MUST implement this sets of RPCs. +* **Node Service**: The Node Plugin MUST implement this sets of RPCs. + +```protobuf +service Identity { + rpc GetPluginInfo(GetPluginInfoRequest) + returns (GetPluginInfoResponse) {} + + rpc GetPluginCapabilities(GetPluginCapabilitiesRequest) + returns (GetPluginCapabilitiesResponse) {} + + rpc Probe (ProbeRequest) + returns (ProbeResponse) {} +} + +service Controller { + rpc CreateVolume (CreateVolumeRequest) + returns (CreateVolumeResponse) {} + + rpc DeleteVolume (DeleteVolumeRequest) + returns (DeleteVolumeResponse) {} + + rpc ControllerPublishVolume (ControllerPublishVolumeRequest) + returns (ControllerPublishVolumeResponse) {} + + rpc ControllerUnpublishVolume (ControllerUnpublishVolumeRequest) + returns (ControllerUnpublishVolumeResponse) {} + + rpc ValidateVolumeCapabilities (ValidateVolumeCapabilitiesRequest) + returns (ValidateVolumeCapabilitiesResponse) {} + + rpc ListVolumes (ListVolumesRequest) + returns (ListVolumesResponse) {} + + rpc ControllerListVolumeHealth (ControllerListVolumeHealthRequest) + returns (ControllerListVolumeHealthResponse) { + option (alpha_method) = true; + } + + rpc ControllerGetVolumeHealth (ControllerGetVolumeHealthRequest) + returns (ControllerGetVolumeHealthResponse) { + option (alpha_method) = true; + } + + rpc GetCapacity (GetCapacityRequest) + returns (GetCapacityResponse) {} + + rpc ControllerGetCapabilities (ControllerGetCapabilitiesRequest) + returns (ControllerGetCapabilitiesResponse) {} + + rpc CreateSnapshot (CreateSnapshotRequest) + returns (CreateSnapshotResponse) {} + + rpc DeleteSnapshot (DeleteSnapshotRequest) + returns (DeleteSnapshotResponse) {} + + rpc ListSnapshots (ListSnapshotsRequest) + returns (ListSnapshotsResponse) {} + + rpc GetSnapshot (GetSnapshotRequest) + returns (GetSnapshotResponse) { + option (alpha_method) = true; + } + + rpc ControllerExpandVolume (ControllerExpandVolumeRequest) + returns (ControllerExpandVolumeResponse) {} + + rpc ControllerGetVolume (ControllerGetVolumeRequest) + returns (ControllerGetVolumeResponse) { + option (alpha_method) = true; + } + + rpc ControllerModifyVolume (ControllerModifyVolumeRequest) + returns (ControllerModifyVolumeResponse) { + } +} + +service GroupController { + rpc GroupControllerGetCapabilities ( + GroupControllerGetCapabilitiesRequest) + returns (GroupControllerGetCapabilitiesResponse) {} + + rpc CreateVolumeGroupSnapshot(CreateVolumeGroupSnapshotRequest) + returns (CreateVolumeGroupSnapshotResponse) { + } + + rpc DeleteVolumeGroupSnapshot(DeleteVolumeGroupSnapshotRequest) + returns (DeleteVolumeGroupSnapshotResponse) { + } + + rpc GetVolumeGroupSnapshot( + GetVolumeGroupSnapshotRequest) + returns (GetVolumeGroupSnapshotResponse) { + } +} + +service SnapshotMetadata { + option (alpha_service) = true; + + rpc GetMetadataAllocated(GetMetadataAllocatedRequest) + returns (stream GetMetadataAllocatedResponse) {} + + rpc GetMetadataDelta(GetMetadataDeltaRequest) + returns (stream GetMetadataDeltaResponse) {} +} + +service Node { + rpc NodeStageVolume (NodeStageVolumeRequest) + returns (NodeStageVolumeResponse) {} + + rpc NodeUnstageVolume (NodeUnstageVolumeRequest) + returns (NodeUnstageVolumeResponse) {} + + rpc NodePublishVolume (NodePublishVolumeRequest) + returns (NodePublishVolumeResponse) {} + + rpc NodeUnpublishVolume (NodeUnpublishVolumeRequest) + returns (NodeUnpublishVolumeResponse) {} + + rpc NodeGetVolumeStats (NodeGetVolumeStatsRequest) + returns (NodeGetVolumeStatsResponse) {} + + rpc NodeGetVolumeHealth (NodeGetVolumeHealthRequest) + returns (NodeGetVolumeHealthResponse) { + option (alpha_method) = true; + } + + rpc NodeGetStorageHealth (NodeGetStorageHealthRequest) + returns (NodeGetStorageHealthResponse) { + option (alpha_method) = true; + } + + rpc NodeExpandVolume(NodeExpandVolumeRequest) + returns (NodeExpandVolumeResponse) {} + + + rpc NodeGetCapabilities (NodeGetCapabilitiesRequest) + returns (NodeGetCapabilitiesResponse) {} + + rpc NodeGetInfo (NodeGetInfoRequest) + returns (NodeGetInfoResponse) {} +} +``` + +#### Concurrency + +In general the Cluster Orchestrator (CO) is responsible for ensuring that there is no more than one call “in-flight” per volume at a given time. +However, in some circumstances, the CO MAY lose state (for example when the CO crashes and restarts), and MAY issue multiple calls simultaneously for the same volume. +The plugin SHOULD handle this as gracefully as possible. +The error code `ABORTED` MAY be returned by the plugin in this case (see the [Error Scheme](#error-scheme) section for details). + +#### Field Requirements + +The requirements documented herein apply equally and without exception, unless otherwise noted, for the fields of all protobuf message types defined by this specification. +Violation of these requirements MAY result in RPC message data that is not compatible with all CO, Plugin, and/or CSI middleware implementations. + +##### Size Limits + +CSI defines general size limits for fields of various types (see table below). +The general size limit for a particular field MAY be overridden by specifying a different size limit in said field's description. +Unless otherwise specified, fields SHALL NOT exceed the limits documented here. +These limits apply for messages generated by both COs and plugins. + +| Size | Field Type | +|------------|---------------------| +| 128 bytes | string | +| 4 KiB | map | + +##### `REQUIRED` vs. `OPTIONAL` + +* A field noted as `REQUIRED` MUST be specified, subject to any per-RPC caveats; caveats SHOULD be rare. +* A `repeated` or `map` field listed as `REQUIRED` MUST contain at least 1 element. +* A field noted as `OPTIONAL` MAY be specified and the specification SHALL clearly define expected behavior for the default, zero-value of such fields. + +Scalar fields, even REQUIRED ones, will be defaulted if not specified and any field set to the default value will not be serialized over the wire as per [proto3](https://developers.google.com/protocol-buffers/docs/proto3#default). + +#### Timeouts + +Any of the RPCs defined in this spec MAY timeout and MAY be retried. +The CO MAY choose the maximum time it is willing to wait for a call, how long it waits between retries, and how many time it retries (these values are not negotiated between plugin and CO). + +Idempotency requirements ensure that a retried call with the same fields continues where it left off when retried. +The only way to cancel a call is to issue a "negation" call if one exists. +For example, issue a `ControllerUnpublishVolume` call to cancel a pending `ControllerPublishVolume` operation, etc. +In some cases, a CO MAY NOT be able to cancel a pending operation because it depends on the result of the pending operation in order to execute the "negation" call. +For example, if a `CreateVolume` call never completes then a CO MAY NOT have the `volume_id` to call `DeleteVolume` with. + +### Error Scheme + +All CSI API calls defined in this spec MUST return a [standard gRPC status](https://github.com/grpc/grpc/blob/master/src/proto/grpc/status/status.proto). +Most gRPC libraries provide helper methods to set and read the status fields. + +The status `code` MUST contain a [canonical error code](https://github.com/grpc/grpc-go/blob/master/codes/codes.go). COs MUST handle all valid error codes. Each RPC defines a set of gRPC error codes that MUST be returned by the plugin when specified conditions are encountered. In addition to those, if the conditions defined below are encountered, the plugin MUST return the associated gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Missing required field | 3 INVALID_ARGUMENT | Indicates that a required field is missing from the request. More human-readable information MAY be provided in the `status.message` field. | Caller MUST fix the request by adding the missing required field before retrying. | +| Invalid or unsupported field in the request | 3 INVALID_ARGUMENT | Indicates that the one or more fields in this field is either not allowed by the Plugin or has an invalid value. More human-readable information MAY be provided in the gRPC `status.message` field. | Caller MUST fix the field before retrying. | +| Permission denied | 7 PERMISSION_DENIED | The Plugin is able to derive or otherwise infer an identity from the secrets present within an RPC, but that identity does not have permission to invoke the RPC. | System administrator SHOULD ensure that requisite permissions are granted, after which point the caller MAY retry the attempted RPC. | +| Operation pending for volume | 10 ABORTED | Indicates that there is already an operation pending for the specified volume. In general the Cluster Orchestrator (CO) is responsible for ensuring that there is no more than one call "in-flight" per volume at a given time. However, in some circumstances, the CO MAY lose state (for example when the CO crashes and restarts), and MAY issue multiple calls simultaneously for the same volume. The Plugin, SHOULD handle this as gracefully as possible, and MAY return this error code to reject secondary calls. | Caller SHOULD ensure that there are no other calls pending for the specified volume, and then retry with exponential back off. | +| Call not implemented | 12 UNIMPLEMENTED | The invoked RPC is not implemented by the Plugin or disabled in the Plugin's current mode of operation. | Caller MUST NOT retry. Caller MAY call `GetPluginCapabilities`, `ControllerGetCapabilities`, or `NodeGetCapabilities` to discover Plugin capabilities. | +| Not authenticated | 16 UNAUTHENTICATED | The invoked RPC does not carry secrets that are valid for authentication. | Caller SHALL either fix the secrets provided in the RPC, or otherwise regalvanize said secrets such that they will pass authentication by the Plugin for the attempted RPC, after which point the caller MAY retry the attempted RPC. | + +The status `message` MUST contain a human readable description of error, if the status `code` is not `OK`. +This string MAY be surfaced by CO to end users. + +The status `details` MUST be empty. In the future, this spec MAY require `details` to return a machine-parsable protobuf message if the status `code` is not `OK` to enable CO's to implement smarter error handling and fault resolution. + +### Secrets Requirements + +Secrets MAY be required by plugin to complete a RPC request. +A secret is a string to string map where the key identifies the name of the secret (e.g. "username" or "password"), and the value contains the secret data (e.g. "bob" or "abc123"). +Each key MUST consist of alphanumeric characters, '-', '_' or '.'. +Each value MUST contain a valid string. +An SP MAY choose to accept binary (non-string) data by using a binary-to-text encoding scheme, like base64. +An SP SHALL advertise the requirements for required secret keys and values in documentation. +CO SHALL permit passing through the required secrets. +A CO MAY pass the same secrets to all RPCs, therefore the keys for all unique secrets that an SP expects MUST be unique across all CSI operations. +This information is sensitive and MUST be treated as such (not logged, etc.) by the CO. + +### Identity Service RPC + +Identity service RPCs allow a CO to query a plugin for capabilities, health, and other metadata. +The general flow of the success case MAY be as follows (protos illustrated in YAML for brevity): + +1. CO queries metadata via Identity RPC. + +``` + # CO --(GetPluginInfo)--> Plugin + request: + response: + name: org.foo.whizbang.super-plugin + vendor_version: blue-green + manifest: + baz: qaz +``` + +2. CO queries available capabilities of the plugin. + +``` + # CO --(GetPluginCapabilities)--> Plugin + request: + response: + capabilities: + - service: + type: CONTROLLER_SERVICE +``` + +3. CO queries the readiness of the plugin. + +``` + # CO --(Probe)--> Plugin + request: + response: {} +``` + +#### `GetPluginInfo` + +```protobuf +message GetPluginInfoRequest { + // Intentionally empty. +} + +message GetPluginInfoResponse { + // The name MUST follow domain name notation format + // (https://tools.ietf.org/html/rfc1035#section-2.3.1). It SHOULD + // include the plugin's host company name and the plugin name, + // to minimize the possibility of collisions. It MUST be 63 + // characters or less, beginning and ending with an alphanumeric + // character ([a-z0-9A-Z]) with dashes (-), dots (.), and + // alphanumerics between. This field is REQUIRED. + string name = 1; + + // This field is REQUIRED. Value of this field is opaque to the CO. + string vendor_version = 2; + + // This field is OPTIONAL. Values are opaque to the CO. + map manifest = 3; +} +``` + +##### GetPluginInfo Errors + +If the plugin is unable to complete the GetPluginInfo call successfully, it MUST return a non-ok gRPC code in the gRPC status. + +#### `GetPluginCapabilities` + +This REQUIRED RPC allows the CO to query the supported capabilities of the Plugin "as a whole": it is the grand sum of all capabilities of all instances of the Plugin software, as it is intended to be deployed. +All instances of the same version (see `vendor_version` of `GetPluginInfoResponse`) of the Plugin SHALL return the same set of capabilities, regardless of both: (a) where instances are deployed on the cluster as well as; (b) which RPCs an instance is serving. + +```protobuf +message GetPluginCapabilitiesRequest { + // Intentionally empty. +} + +message GetPluginCapabilitiesResponse { + // All the capabilities that the controller service supports. This + // field is OPTIONAL. + repeated PluginCapability capabilities = 1; +} + +// Specifies a capability of the plugin. +message PluginCapability { + message Service { + enum Type { + UNKNOWN = 0; + // CONTROLLER_SERVICE indicates that the Plugin provides RPCs for + // the ControllerService. Plugins SHOULD provide this capability. + // In rare cases certain plugins MAY wish to omit the + // ControllerService entirely from their implementation, but such + // SHOULD NOT be the common case. + // The presence of this capability determines whether the CO will + // attempt to invoke the REQUIRED ControllerService RPCs, as well + // as specific RPCs as indicated by ControllerGetCapabilities. + CONTROLLER_SERVICE = 1; + + // VOLUME_ACCESSIBILITY_CONSTRAINTS indicates that the volumes for + // this plugin MAY NOT be equally accessible by all nodes in the + // cluster. The CO MUST use the topology information returned by + // CreateVolumeRequest along with the topology information + // returned by NodeGetInfo to ensure that a given volume is + // accessible from a given node when scheduling workloads. + VOLUME_ACCESSIBILITY_CONSTRAINTS = 2; + + // GROUP_CONTROLLER_SERVICE indicates that the Plugin provides + // RPCs for operating on groups of volumes. Plugins MAY provide + // this capability. + // The presence of this capability determines whether the CO will + // attempt to invoke the REQUIRED GroupController service RPCs, as + // well as specific RPCs as indicated by + // GroupControllerGetCapabilities. + GROUP_CONTROLLER_SERVICE = 3; + + // SNAPSHOT_METADATA_SERVICE indicates that the Plugin provides + // RPCs to retrieve metadata on the allocated blocks of a single + // snapshot, or the changed blocks between a pair of snapshots of + // the same block volume. + // The presence of this capability determines whether the CO will + // attempt to invoke the OPTIONAL SnapshotMetadata service RPCs. + SNAPSHOT_METADATA_SERVICE = 4 [(alpha_enum_value) = true]; + + // SNAPSHOT_ACCESSIBILITY_CONSTRAINTS indicates that the snapshots + // for this plugin MAY NOT be equally usable from all + // topologies in the cluster. A snapshot is usable from a location + // if volumes created from that snapshot are guaranteed to be + // accessible from that location. The CO MUST use the topology + // information returned in the CreateSnapshotResponse to ensure + // that a desired volume can be provisioned from a given snapshot + // when scheduling workloads. + SNAPSHOT_ACCESSIBILITY_CONSTRAINTS = 5 + [(alpha_enum_value) = true]; + } + Type type = 1; + } + + message VolumeExpansion { + enum Type { + UNKNOWN = 0; + + // ONLINE indicates that volumes may be expanded when published to + // a node. When a Plugin implements this capability it MUST + // implement either the EXPAND_VOLUME controller capability or the + // EXPAND_VOLUME node capability or both. When a plugin supports + // ONLINE volume expansion and also has the EXPAND_VOLUME + // controller capability then the plugin MUST support expansion of + // volumes currently published and available on a node. When a + // plugin supports ONLINE volume expansion and also has the + // EXPAND_VOLUME node capability then the plugin MAY support + // expansion of node-published volume via NodeExpandVolume. + // + // Example 1: Given a shared filesystem volume (e.g. GlusterFs), + // the Plugin may set the ONLINE volume expansion capability and + // implement ControllerExpandVolume but not NodeExpandVolume. + // + // Example 2: Given a block storage volume type (e.g. EBS), the + // Plugin may set the ONLINE volume expansion capability and + // implement both ControllerExpandVolume and NodeExpandVolume. + // + // Example 3: Given a Plugin that supports volume expansion only + // upon a node, the Plugin may set the ONLINE volume + // expansion capability and implement NodeExpandVolume but not + // ControllerExpandVolume. + ONLINE = 1; + + // OFFLINE indicates that volumes currently published and + // available on a node SHALL NOT be expanded via + // ControllerExpandVolume. When a plugin supports OFFLINE volume + // expansion it MUST implement either the EXPAND_VOLUME controller + // capability or both the EXPAND_VOLUME controller capability and + // the EXPAND_VOLUME node capability. + // + // Example 1: Given a block storage volume type (e.g. Azure Disk) + // that does not support expansion of "node-attached" (i.e. + // controller-published) volumes, the Plugin may indicate + // OFFLINE volume expansion support and implement both + // ControllerExpandVolume and NodeExpandVolume. + OFFLINE = 2; + } + Type type = 1; + } + + oneof type { + // Service that the plugin supports. + Service service = 1; + VolumeExpansion volume_expansion = 2; + } +} +``` + +##### GetPluginCapabilities Errors + +If the plugin is unable to complete the GetPluginCapabilities call successfully, it MUST return a non-ok gRPC code in the gRPC status. + +#### `Probe` + +A Plugin MUST implement this RPC call. +The primary utility of the Probe RPC is to verify that the plugin is in a healthy and ready state. +If an unhealthy state is reported, via a non-success response, a CO MAY take action with the intent to bring the plugin to a healthy state. +Such actions MAY include, but SHALL NOT be limited to, the following: + +* Restarting the plugin container, or +* Notifying the plugin supervisor. + +The Plugin MAY verify that it has the right configurations, devices, dependencies and drivers in order to run and return a success if the validation succeeds. +The CO MAY invoke this RPC at any time. +A CO MAY invoke this call multiple times with the understanding that a plugin's implementation MAY NOT be trivial and there MAY be overhead incurred by such repeated calls. +The SP SHALL document guidance and known limitations regarding a particular Plugin's implementation of this RPC. +For example, the SP MAY document the maximum frequency at which its Probe implementation SHOULD be called. + +```protobuf +message ProbeRequest { + // Intentionally empty. +} + +message ProbeResponse { + // Readiness allows a plugin to report its initialization status back + // to the CO. Initialization for some plugins MAY be time consuming + // and it is important for a CO to distinguish between the following + // cases: + // + // 1) The plugin is in an unhealthy state and MAY need restarting. In + // this case a gRPC error code SHALL be returned. + // 2) The plugin is still initializing, but is otherwise perfectly + // healthy. In this case a successful response SHALL be returned + // with a readiness value of `false`. Calls to the plugin's + // Controller and/or Node services MAY fail due to an incomplete + // initialization state. + // 3) The plugin has finished initializing and is ready to service + // calls to its Controller and/or Node services. A successful + // response is returned with a readiness value of `true`. + // + // This field is OPTIONAL. If not present, the caller SHALL assume + // that the plugin is in a ready state and is accepting calls to its + // Controller and/or Node services (according to the plugin's reported + // capabilities). + .google.protobuf.BoolValue ready = 1; +} +``` + +##### Probe Errors + +If the plugin is unable to complete the Probe call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Plugin not healthy | 9 FAILED_PRECONDITION | Indicates that the plugin is not in a healthy/ready state. | Caller SHOULD assume the plugin is not healthy and that future RPCs MAY fail because of this condition. | +| Missing required dependency | 9 FAILED_PRECONDITION | Indicates that the plugin is missing one or more required dependency. | Caller MUST assume the plugin is not healthy. | + + +### Controller Service RPC + +#### `CreateVolume` + +A Controller Plugin MUST implement this RPC call if it has `CREATE_DELETE_VOLUME` controller capability. +This RPC will be called by the CO to provision a new volume on behalf of a user (to be consumed as either a block device or a mounted filesystem). + +This operation MUST be idempotent. +If a volume corresponding to the specified volume `name` already exists, is accessible from `accessibility_requirements`, and is compatible with the specified `capacity_range`, `volume_capabilities`, `parameters` and `mutable_parameters` in the `CreateVolumeRequest`, the Plugin MUST reply `0 OK` with the corresponding `CreateVolumeResponse`. + +The `parameters` field SHALL contain opaque volume attributes to be specified at creation time. +The `mutable_parameters` field SHALL contain opaque volume attributes that are defined at creation time but MAY also be changed during the lifetime of the volume via a subsequent `ControllerModifyVolume` RPC. +Values specified in `mutable_parameters` MUST take precedence over the values from `parameters`. + +Plugins MAY create 3 types of volumes: + +- Empty volumes. When plugin supports `CREATE_DELETE_VOLUME` OPTIONAL capability. +- From an existing snapshot. When plugin supports `CREATE_DELETE_VOLUME` and `CREATE_DELETE_SNAPSHOT` OPTIONAL capabilities. +- From an existing volume. When plugin supports cloning, and reports the OPTIONAL capabilities `CREATE_DELETE_VOLUME` and `CLONE_VOLUME`. + +If CO requests a volume to be created from existing snapshot or volume and the requested size of the volume is larger than the original snapshotted (or cloned volume), the Plugin can either refuse such a call with `OUT_OF_RANGE` error or MUST provide a volume that, when presented to a workload by `NodePublish` call, has both the requested (larger) size and contains data from the snapshot (or original volume). +Explicitly, it's the responsibility of the Plugin to resize the filesystem of the newly created volume at (or before) the `NodePublish` call, if the volume has `VolumeCapability` access type `MountVolume` and the filesystem resize is required in order to provision the requested capacity. + +NOTE: The requested and reported sizes for a volume refer to the raw volume size that the SP allocated for the volume. Volumes with an `access_type` of `MountVolume` MAY have less available capacity due to the overhead of filesystem metadata (exact amount depends on filesystem). The underlying volume when used in `access_mode` with a value of `BlockVolume` MUST match the specified `capacity_range` request. + +```protobuf +message CreateVolumeRequest { + // The suggested name for the storage space. This field is REQUIRED. + // It serves two purposes: + // 1) Idempotency - This name is generated by the CO to achieve + // idempotency. The Plugin SHOULD ensure that multiple + // `CreateVolume` calls for the same name do not result in more + // than one piece of storage provisioned corresponding to that + // name. If a Plugin is unable to enforce idempotency, the CO's + // error recovery logic could result in multiple (unused) volumes + // being provisioned. + // In the case of error, the CO MUST handle the gRPC error codes + // per the recovery behavior defined in the "CreateVolume Errors" + // section below. + // The CO is responsible for cleaning up volumes it provisioned + // that it no longer needs. If the CO is uncertain whether a volume + // was provisioned or not when a `CreateVolume` call fails, the CO + // MAY call `CreateVolume` again, with the same name, to ensure the + // volume exists and to retrieve the volume's `volume_id` (unless + // otherwise prohibited by "CreateVolume Errors"). + // 2) Suggested name - Some storage systems allow callers to specify + // an identifier by which to refer to the newly provisioned + // storage. If a storage system supports this, it can optionally + // use this name as the identifier for the new volume. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 1; + + // This field is OPTIONAL. This allows the CO to specify the capacity + // requirement of the volume to be provisioned. If not specified, the + // Plugin MAY choose an implementation-defined capacity range. If + // specified it MUST always be honored, even when creating volumes + // from a source; which MAY force some backends to internally extend + // the volume after creating it. + CapacityRange capacity_range = 2; + + // The capabilities that the provisioned volume MUST have. SP MUST + // provision a volume that will satisfy ALL of the capabilities + // specified in this list. Otherwise SP MUST return the appropriate + // gRPC error code. + // The Plugin MUST assume that the CO MAY use the provisioned volume + // with ANY of the capabilities specified in this list. + // For example, a CO MAY specify two volume capabilities: one with + // access mode SINGLE_NODE_WRITER and another with access mode + // MULTI_NODE_READER_ONLY. In this case, the SP MUST verify that the + // provisioned volume can be used in either mode. + // This also enables the CO to do early validation: If ANY of the + // specified volume capabilities are not supported by the SP, the call + // MUST return the appropriate gRPC error code. + // This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 3; + + // Plugin specific creation-time parameters passed in as opaque + // key-value pairs. This field is OPTIONAL. The Plugin is responsible + // for parsing and validating these parameters. COs will treat + // these as opaque. + map parameters = 4; + + // Secrets required by plugin to complete volume creation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // If specified, the new volume will be pre-populated with data from + // this source. This field is OPTIONAL. + VolumeContentSource volume_content_source = 6; + + // Specifies where (regions, zones, racks, etc.) the provisioned + // volume MUST be accessible from. + // An SP SHALL advertise the requirements for topological + // accessibility information in documentation. COs SHALL only specify + // topological accessibility information supported by the SP. + // This field is OPTIONAL. + // This field SHALL NOT be specified unless the SP has the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // If this field is not specified and the SP has the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability, the SP MAY + // choose where the provisioned volume is accessible from. + TopologyRequirement accessibility_requirements = 7; + + // Plugin specific creation-time parameters passed in as opaque + // key-value pairs. These mutable_parameteres MAY also be + // changed during the lifetime of the volume via a subsequent + // `ControllerModifyVolume` RPC. This field is OPTIONAL. + // The Plugin is responsible for parsing and validating these + // parameters. COs will treat these as opaque. + + // Plugins MUST treat these + // as if they take precedence over the parameters field. + // This field SHALL NOT be specified unless the SP has the + // MODIFY_VOLUME plugin capability. + map mutable_parameters = 8; +} + +// Specifies what source the volume will be created from. One of the +// type fields MUST be specified. +message VolumeContentSource { + message SnapshotSource { + // Contains identity information for the existing source snapshot. + // This field is REQUIRED. Plugin is REQUIRED to support creating + // volume from snapshot if it supports the capability + // CREATE_DELETE_SNAPSHOT. + string snapshot_id = 1; + } + + message VolumeSource { + // Contains identity information for the existing source volume. + // This field is REQUIRED. Plugins reporting CLONE_VOLUME + // capability MUST support creating a volume from another volume. + string volume_id = 1; + } + + oneof type { + SnapshotSource snapshot = 1; + VolumeSource volume = 2; + } +} + +message CreateVolumeResponse { + // Contains all attributes of the newly created volume that are + // relevant to the CO along with information required by the Plugin + // to uniquely identify the volume. This field is REQUIRED. + Volume volume = 1; +} + +// Specify a capability of a volume. +message VolumeCapability { + // Indicate that the volume will be accessed via the block device API. + message BlockVolume { + // Intentionally empty, for now. + } + + // Indicate that the volume will be accessed via the filesystem API. + message MountVolume { + // The filesystem type. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string fs_type = 1; + + // The mount options that can be used for the volume. This field is + // OPTIONAL. `mount_flags` MAY contain sensitive information. + // Therefore, the CO and the Plugin MUST NOT leak this information + // to untrusted entities. The total size of this repeated field + // SHALL NOT exceed 4 KiB. + repeated string mount_flags = 2; + + // If SP has VOLUME_MOUNT_GROUP node capability and CO provides + // this field then SP MUST ensure that the volume_mount_group + // parameter is passed as the group identifier to the underlying + // operating system mount system call, with the understanding + // that the set of available mount call parameters and/or + // mount implementations may vary across operating systems. + // Additionally, new file and/or directory entries written to + // the underlying filesystem SHOULD be permission-labeled in such a + // manner, unless otherwise modified by a workload, that they are + // both readable and writable by said mount group identifier. + // This is an OPTIONAL field. + string volume_mount_group = 3; + } + + // Specify how a volume can be accessed. + message AccessMode { + enum Mode { + UNKNOWN = 0; + + // Can only be published once as read/write on a single node, at + // any given time. + SINGLE_NODE_WRITER = 1; + + // Can only be published once as readonly on a single node, at + // any given time. + SINGLE_NODE_READER_ONLY = 2; + + // Can be published as readonly at multiple nodes simultaneously. + MULTI_NODE_READER_ONLY = 3; + + // Can be published at multiple nodes simultaneously. Only one of + // the node can be used as read/write. The rest will be readonly. + MULTI_NODE_SINGLE_WRITER = 4; + + // Can be published as read/write at multiple nodes + // simultaneously. + MULTI_NODE_MULTI_WRITER = 5; + + // Can only be published once as read/write at a single workload + // on a single node, at any given time. SHOULD be used instead of + // SINGLE_NODE_WRITER for COs using the experimental + // SINGLE_NODE_MULTI_WRITER capability. + SINGLE_NODE_SINGLE_WRITER = 6 [(alpha_enum_value) = true]; + + // Can be published as read/write at multiple workloads on a + // single node simultaneously. SHOULD be used instead of + // SINGLE_NODE_WRITER for COs using the experimental + // SINGLE_NODE_MULTI_WRITER capability. + SINGLE_NODE_MULTI_WRITER = 7 [(alpha_enum_value) = true]; + } + + // This field is REQUIRED. + Mode mode = 1; + } + + // Specifies what API the volume will be accessed using. One of the + // following fields MUST be specified. + oneof access_type { + BlockVolume block = 1; + MountVolume mount = 2; + } + + // This is a REQUIRED field. + AccessMode access_mode = 3; +} + +// The capacity of the storage space in bytes. To specify an exact size, +// `required_bytes` and `limit_bytes` SHALL be set to the same value. At +// least one of the these fields MUST be specified. +message CapacityRange { + // Volume MUST be at least this big. This field is OPTIONAL. + // A value of 0 is equal to an unspecified field value. + // The value of this field MUST NOT be negative. + int64 required_bytes = 1; + + // Volume MUST not be bigger than this. This field is OPTIONAL. + // A value of 0 is equal to an unspecified field value. + // The value of this field MUST NOT be negative. + int64 limit_bytes = 2; +} + +// Information about a specific volume. +message Volume { + // The capacity of the volume in bytes. This field is OPTIONAL. If not + // set (value of 0), it indicates that the capacity of the volume is + // unknown (e.g., NFS share). + // The value of this field MUST NOT be negative. + int64 capacity_bytes = 1; + + // The identifier for this volume, generated by the plugin. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific volume vs all other volumes supported by this plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this volume. + // The SP is NOT responsible for global uniqueness of volume_id across + // multiple SPs. + string volume_id = 2; + + // Opaque static properties of the volume. SP MAY use this field to + // ensure subsequent volume validation and publishing calls have + // contextual information. + // The contents of this field SHALL be opaque to a CO. + // The contents of this field SHALL NOT be mutable. + // The contents of this field SHALL be safe for the CO to cache. + // The contents of this field SHOULD NOT contain sensitive + // information. + // The contents of this field SHOULD NOT be used for uniquely + // identifying a volume. The `volume_id` alone SHOULD be sufficient to + // identify the volume. + // A volume uniquely identified by `volume_id` SHALL always report the + // same volume_context. + // This field is OPTIONAL and when present MUST be passed to volume + // validation and publishing calls. + map volume_context = 3; + + // If specified, indicates that the volume is not empty and is + // pre-populated with data from the specified source. + // This field is OPTIONAL. + VolumeContentSource content_source = 4; + + // Specifies where (regions, zones, racks, etc.) the provisioned + // volume is accessible from. + // A plugin that returns this field MUST also set the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // An SP MAY specify multiple topologies to indicate the volume is + // accessible from multiple locations. + // COs MAY use this information along with the topology information + // returned by NodeGetInfo to ensure that a given volume is accessible + // from a given node when scheduling workloads. + // This field is OPTIONAL. If it is not specified, the CO MAY assume + // the volume is equally accessible from all nodes in the cluster and + // MAY schedule workloads referencing the volume on any available + // node. + // + // Example 1: + // accessible_topology = {"region": "R1", "zone": "Z2"} + // Indicates a volume accessible only from the "region" "R1" and the + // "zone" "Z2". + // + // Example 2: + // accessible_topology = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // Indicates a volume accessible from both "zone" "Z2" and "zone" "Z3" + // in the "region" "R1". + repeated Topology accessible_topology = 5; +} + +message TopologyRequirement { + // Specifies the list of topologies the provisioned volume MUST be + // accessible from. + // This field is OPTIONAL. If TopologyRequirement is specified either + // requisite or preferred or both MUST be specified. + // + // If requisite is specified, the provisioned volume MUST be + // accessible from at least one of the requisite topologies. + // + // Given + // x = number of topologies provisioned volume is accessible from + // n = number of requisite topologies + // The CO MUST ensure n >= 1. The SP MUST ensure x >= 1 + // If x==n, then the SP MUST make the provisioned volume available to + // all topologies from the list of requisite topologies. If it is + // unable to do so, the SP MUST fail the CreateVolume call. + // For example, if a volume should be accessible from a single zone, + // and requisite = + // {"region": "R1", "zone": "Z2"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and the "zone" "Z2". + // Similarly, if a volume should be accessible from two zones, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and both "zone" "Z2" and "zone" "Z3". + // + // If xn, then the SP MUST make the provisioned volume available from + // all topologies from the list of requisite topologies and MAY choose + // the remaining x-n unique topologies from the list of all possible + // topologies. If it is unable to do so, the SP MUST fail the + // CreateVolume call. + // For example, if a volume should be accessible from two zones, and + // requisite = + // {"region": "R1", "zone": "Z2"} + // then the provisioned volume MUST be accessible from the "region" + // "R1" and the "zone" "Z2" and the SP may select the second zone + // independently, e.g. "R1/Z4". + repeated Topology requisite = 1; + + // Specifies the list of topologies the CO would prefer the volume to + // be provisioned in. + // + // This field is OPTIONAL. If TopologyRequirement is specified either + // requisite or preferred or both MUST be specified. + // + // An SP MUST attempt to make the provisioned volume available using + // the preferred topologies in order from first to last. + // + // If requisite is specified, all topologies in preferred list MUST + // also be present in the list of requisite topologies. + // + // If the SP is unable to to make the provisioned volume available + // from any of the preferred topologies, the SP MAY choose a topology + // from the list of requisite topologies. + // If the list of requisite topologies is not specified, then the SP + // MAY choose from the list of all possible topologies. + // If the list of requisite topologies is specified and the SP is + // unable to to make the provisioned volume available from any of the + // requisite topologies it MUST fail the CreateVolume call. + // + // Example 1: + // Given a volume should be accessible from a single zone, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"} + // preferred = + // {"region": "R1", "zone": "Z3"} + // then the SP SHOULD first attempt to make the provisioned volume + // available from "zone" "Z3" in the "region" "R1" and fall back to + // "zone" "Z2" in the "region" "R1" if that is not possible. + // + // Example 2: + // Given a volume should be accessible from a single zone, and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"}, + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z5"} + // preferred = + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z2"} + // then the SP SHOULD first attempt to make the provisioned volume + // accessible from "zone" "Z4" in the "region" "R1" and fall back to + // "zone" "Z2" in the "region" "R1" if that is not possible. If that + // is not possible, the SP may choose between either the "zone" + // "Z3" or "Z5" in the "region" "R1". + // + // Example 3: + // Given a volume should be accessible from TWO zones (because an + // opaque parameter in CreateVolumeRequest, for example, specifies + // the volume is accessible from two zones, aka synchronously + // replicated), and + // requisite = + // {"region": "R1", "zone": "Z2"}, + // {"region": "R1", "zone": "Z3"}, + // {"region": "R1", "zone": "Z4"}, + // {"region": "R1", "zone": "Z5"} + // preferred = + // {"region": "R1", "zone": "Z5"}, + // {"region": "R1", "zone": "Z3"} + // then the SP SHOULD first attempt to make the provisioned volume + // accessible from the combination of the two "zones" "Z5" and "Z3" in + // the "region" "R1". If that's not possible, it should fall back to + // a combination of "Z5" and other possibilities from the list of + // requisite. If that's not possible, it should fall back to a + // combination of "Z3" and other possibilities from the list of + // requisite. If that's not possible, it should fall back to a + // combination of other possibilities from the list of requisite. + repeated Topology preferred = 2; +} + +// Topology is a map of topological domains to topological segments. +// A topological domain is a sub-division of a cluster, like "region", +// "zone", "rack", etc. +// A topological segment is a specific instance of a topological domain, +// like "zone3", "rack3", etc. +// For example {"com.company/zone": "Z1", "com.company/rack": "R3"} +// Valid keys have two segments: an OPTIONAL prefix and name, separated +// by a slash (/), for example: "com.company.example/zone". +// The key name segment is REQUIRED. The prefix is OPTIONAL. +// The key name MUST be 63 characters or less, begin and end with an +// alphanumeric character ([a-z0-9A-Z]), and contain only dashes (-), +// underscores (_), dots (.), or alphanumerics in between, for example +// "zone". +// The key prefix MUST be 63 characters or less, begin and end with a +// lower-case alphanumeric character ([a-z0-9]), contain only +// dashes (-), dots (.), or lower-case alphanumerics in between, and +// follow domain name notation format +// (https://tools.ietf.org/html/rfc1035#section-2.3.1). +// The key prefix SHOULD include the plugin's host company name and/or +// the plugin name, to minimize the possibility of collisions with keys +// from other plugins. +// If a key prefix is specified, it MUST be identical across all +// topology keys returned by the SP (across all RPCs). +// Keys MUST be case-insensitive. Meaning the keys "Zone" and "zone" +// MUST not both exist. +// Each value (topological segment) MUST contain 1 or more strings. +// Each string MUST be 63 characters or less and begin and end with an +// alphanumeric character with '-', '_', '.', or alphanumerics in +// between. +message Topology { + map segments = 1; +} +``` + +##### CreateVolume Errors + +If the plugin is unable to complete the CreateVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Source incompatible or not supported | 3 INVALID_ARGUMENT | Besides the general cases, this code MUST also be used to indicate when plugin supporting CREATE_DELETE_VOLUME cannot create a volume from the requested source (`SnapshotSource` or `VolumeSource`). Failure MAY be caused by not supporting the source (CO SHOULD NOT have provided that source) or incompatibility between `parameters` from the source and the ones requested for the new volume. More human-readable information SHOULD be provided in the gRPC `status.message` field if the problem is the source. | On source related issues, caller MUST use different parameters, a different source, or no source at all. | +| Source does not exist | 5 NOT_FOUND | Indicates that the specified source does not exist. | Caller MUST verify that the `volume_content_source` is correct, the source is accessible, and has not been deleted before retrying with exponential back off. | +| Volume already exists but is incompatible | 6 ALREADY_EXISTS | Indicates that a volume corresponding to the specified volume `name` already exists but is incompatible with the specified `capacity_range`, `volume_capabilities`, `parameters`, `accessibility_requirements` or `volume_content_source`. | Caller MUST fix the arguments or use a different `name` before retrying. | +| Unable to provision in `accessible_topology` | 8 RESOURCE_EXHAUSTED | Indicates that although the `accessible_topology` field is valid, a new volume can not be provisioned with the specified topology constraints. More human-readable information MAY be provided in the gRPC `status.message` field. | Caller MUST ensure that whatever is preventing volumes from being provisioned in the specified location (e.g. quota issues) is addressed before retrying with exponential backoff. | +| Unsupported `capacity_range` | 11 OUT_OF_RANGE | Indicates that the capacity range is not allowed by the Plugin, for example when trying to create a volume smaller than the source snapshot or the Plugin does not support creating volumes larger than the source snapshot or source volume. More human-readable information MAY be provided in the gRPC `status.message` field. | Caller MUST fix the capacity range before retrying. | + + +#### `DeleteVolume` + +A Controller Plugin MUST implement this RPC call if it has `CREATE_DELETE_VOLUME` capability. +This RPC will be called by the CO to deprovision a volume. + +This operation MUST be idempotent. +If a volume corresponding to the specified `volume_id` does not exist or the artifacts associated with the volume do not exist anymore, the Plugin MUST reply `0 OK`. + +CSI plugins SHOULD treat volumes independent from their snapshots. + +If the Controller Plugin supports deleting a volume without affecting its existing snapshots, then these snapshots MUST still be fully operational and acceptable as sources for new volumes as well as appear on `ListSnapshot` calls once the volume has been deleted. + +When a Controller Plugin does not support deleting a volume without affecting its existing snapshots, then the volume MUST NOT be altered in any way by the request and the operation must return the `FAILED_PRECONDITION` error code and MAY include meaningful human-readable information in the `status.message` field. + +```protobuf +message DeleteVolumeRequest { + // The ID of the volume to be deprovisioned. + // This field is REQUIRED. + string volume_id = 1; + + // Secrets required by plugin to complete volume deletion request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; +} + +message DeleteVolumeResponse { + // Intentionally empty. +} +``` + +##### DeleteVolume Errors + +If the plugin is unable to complete the DeleteVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume in use | 9 FAILED_PRECONDITION | Indicates that the volume corresponding to the specified `volume_id` could not be deleted because it is in use by another resource or has snapshots and the plugin doesn't treat them as independent entities. | Caller SHOULD ensure that there are no other resources using the volume and that it has no snapshots, and then retry with exponential back off. | + + +#### `ControllerPublishVolume` + +A Controller Plugin MUST implement this RPC call if it has `PUBLISH_UNPUBLISH_VOLUME` controller capability. +This RPC will be called by the CO when it wants to place a workload that uses the volume onto a node. +The Plugin SHOULD perform the work that is necessary for making the volume available on the given node. +The Plugin MUST NOT assume that this RPC will be executed on the node where the volume will be used. + +This operation MUST be idempotent. +If the volume corresponding to the `volume_id` has already been published at the node corresponding to the `node_id`, and is compatible with the specified `volume_capability` and `readonly` flag, the Plugin MUST reply `0 OK`. + +If the operation failed or the CO does not know if the operation has failed or not, it MAY choose to call `ControllerPublishVolume` again or choose to call `ControllerUnpublishVolume`. + +The CO MAY call this RPC for publishing a volume to multiple nodes if the volume has `MULTI_NODE` capability (i.e., `MULTI_NODE_READER_ONLY`, `MULTI_NODE_SINGLE_WRITER` or `MULTI_NODE_MULTI_WRITER`). + +```protobuf +message ControllerPublishVolumeRequest { + // The ID of the volume to be used on a node. + // This field is REQUIRED. + string volume_id = 1; + + // The ID of the node. This field is REQUIRED. The CO SHALL set this + // field to match the node ID returned by `NodeGetInfo`. + string node_id = 2; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the published volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 3; + + // Indicates SP MUST publish the volume in readonly mode. + // CO MUST set this field to false if SP does not have the + // PUBLISH_READONLY controller capability. + // This is a REQUIRED field. + bool readonly = 4; + + // Secrets required by plugin to complete controller publish volume + // request. This field is OPTIONAL. Refer to the + // `Secrets Requirements` section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 6; +} + +message ControllerPublishVolumeResponse { + // Opaque static publish properties of the volume. SP MAY use this + // field to ensure subsequent `NodeStageVolume` or `NodePublishVolume` + // calls calls have contextual information. + // The contents of this field SHALL be opaque to a CO. + // The contents of this field SHALL NOT be mutable. + // The contents of this field SHALL be safe for the CO to cache. + // The contents of this field SHOULD NOT contain sensitive + // information. + // The contents of this field SHOULD NOT be used for uniquely + // identifying a volume. The `volume_id` alone SHOULD be sufficient to + // identify the volume. + // This field is OPTIONAL and when present MUST be passed to + // subsequent `NodeStageVolume` or `NodePublishVolume` calls + map publish_context = 1; +} +``` + +##### ControllerPublishVolume Errors + +If the plugin is unable to complete the ControllerPublishVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | +| Node does not exist | 5 NOT_FOUND | Indicates that a node corresponding to the specified `node_id` does not exist. | Caller MUST verify that the `node_id` is correct and that the node is available and has not been terminated or deleted before retrying with exponential backoff. | +| Volume published but is incompatible | 6 ALREADY_EXISTS | Indicates that a volume corresponding to the specified `volume_id` has already been published at the node corresponding to the specified `node_id` but is incompatible with the specified `volume_capability` or `readonly` flag . | Caller MUST fix the arguments before retrying. | +| Volume published to another node | 9 FAILED_PRECONDITION | Indicates that a volume corresponding to the specified `volume_id` has already been published at another node and does not have MULTI_NODE volume capability. If this error code is returned, the Plugin SHOULD specify the `node_id` of the node at which the volume is published as part of the gRPC `status.message`. | Caller SHOULD ensure the specified volume is not published at any other node before retrying with exponential back off. | +| Max volumes attached | 8 RESOURCE_EXHAUSTED | Indicates that the maximum supported number of volumes that can be attached to the specified node are already attached. Therefore, this operation will fail until at least one of the existing attached volumes is detached from the node. | Caller MUST ensure that the number of volumes already attached to the node is less then the maximum supported number of volumes before retrying with exponential backoff. | + +#### `ControllerUnpublishVolume` + +Controller Plugin MUST implement this RPC call if it has `PUBLISH_UNPUBLISH_VOLUME` controller capability. +This RPC is a reverse operation of `ControllerPublishVolume`. +It MUST be called after all `NodeUnstageVolume` and `NodeUnpublishVolume` on the volume are called and succeed. +The Plugin SHOULD perform the work that is necessary for making the volume ready to be consumed by a different node. +The Plugin MUST NOT assume that this RPC will be executed on the node where the volume was previously used. + +This RPC is typically called by the CO when the workload using the volume is being moved to a different node, or all the workload using the volume on a node has finished. + +This operation MUST be idempotent. +If the volume corresponding to the `volume_id` is not attached to the node corresponding to the `node_id`, the Plugin MUST reply `0 OK`. +If the volume corresponding to the `volume_id` or the node corresponding to `node_id` cannot be found by the Plugin and the volume can be safely regarded as ControllerUnpublished from the node, the plugin SHOULD return `0 OK`. +If this operation failed, or the CO does not know if the operation failed or not, it can choose to call `ControllerUnpublishVolume` again. + +```protobuf +message ControllerUnpublishVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The ID of the node. This field is OPTIONAL. The CO SHOULD set this + // field to match the node ID returned by `NodeGetInfo` or leave it + // unset. If the value is set, the SP MUST unpublish the volume from + // the specified node. If the value is unset, the SP MUST unpublish + // the volume from all nodes it is published to. + string node_id = 2; + + // Secrets required by plugin to complete controller unpublish volume + // request. This SHOULD be the same secrets passed to the + // ControllerPublishVolume call for the specified volume. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 3 [(csi_secret) = true]; +} + +message ControllerUnpublishVolumeResponse { + // Intentionally empty. +} +``` + +##### ControllerUnpublishVolume Errors + +If the plugin is unable to complete the ControllerUnpublishVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist and volume not assumed ControllerUnpublished from node | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist and is not assumed to be ControllerUnpublished from node corresponding to the specified `node_id`. | Caller SHOULD verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | +| Node does not exist and volume not assumed ControllerUnpublished from node | 5 NOT_FOUND | Indicates that a node corresponding to the specified `node_id` does not exist and the volume corresponding to the specified `volume_id` is not assumed to be ControllerUnpublished from node. | Caller SHOULD verify that the `node_id` is correct and that the node is available and has not been terminated or deleted before retrying with exponential backoff. | + + +#### `ValidateVolumeCapabilities` + +A Controller Plugin MUST implement this RPC call. +This RPC will be called by the CO to check if a pre-provisioned volume has all the capabilities that the CO wants. +This RPC call SHALL return `confirmed` only if all the volume capabilities specified in the request are supported (see caveat below). +This operation MUST be idempotent. + +NOTE: Older plugins will parse but likely not "process" newer fields that MAY be present in capability-validation messages (and sub-messages) sent by a CO that is communicating using a newer, backwards-compatible version of the CSI protobufs. +Therefore, the CO SHALL reconcile successful capability-validation responses by comparing the validated capabilities with those that it had originally requested. + +```protobuf +message ValidateVolumeCapabilitiesRequest { + // The ID of the volume to check. This field is REQUIRED. + string volume_id = 1; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 2; + + // The capabilities that the CO wants to check for the volume. This + // call SHALL return "confirmed" only if all the volume capabilities + // specified below are supported. This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 3; + + // See CreateVolumeRequest.parameters. + // This field is OPTIONAL. + map parameters = 4; + + // Secrets required by plugin to complete volume validation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // See CreateVolumeRequest.mutable_parameters. + // This field is OPTIONAL. + map mutable_parameters = 6; +} + +message ValidateVolumeCapabilitiesResponse { + message Confirmed { + // Volume context validated by the plugin. + // This field is OPTIONAL. + map volume_context = 1; + + // Volume capabilities supported by the plugin. + // This field is REQUIRED. + repeated VolumeCapability volume_capabilities = 2; + + // The volume creation parameters validated by the plugin. + // This field is OPTIONAL. + map parameters = 3; + + // The volume creation mutable_parameters validated by the plugin. + // This field is OPTIONAL. + map mutable_parameters = 4; + } + + // Confirmed indicates to the CO the set of capabilities that the + // plugin has validated. This field SHALL only be set to a non-empty + // value for successful validation responses. + // For successful validation responses, the CO SHALL compare the + // fields of this message to the originally requested capabilities in + // order to guard against an older plugin reporting "valid" for newer + // capability fields that it does not yet understand. + // This field is OPTIONAL. + Confirmed confirmed = 1; + + // Message to the CO if `confirmed` above is empty. This field is + // OPTIONAL. + // An empty string is equal to an unspecified field value. + string message = 2; +} +``` + +##### ValidateVolumeCapabilities Errors + +If the plugin is unable to complete the ValidateVolumeCapabilities call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | + + +#### `ListVolumes` + +A Controller Plugin MUST implement this RPC call if it has `LIST_VOLUMES` capability. +The Plugin SHALL return the information about all the volumes that it knows about. +If volumes are created and/or deleted while the CO is concurrently paging through `ListVolumes` results then it is possible that the CO MAY either witness duplicate volumes in the list, not witness existing volumes, or both. +The CO SHALL NOT expect a consistent "view" of all volumes when paging through the volume list via multiple calls to `ListVolumes`. + +```protobuf +message ListVolumesRequest { + // If specified (non-zero value), the Plugin MUST NOT return more + // entries than this number in the response. If the actual number of + // entries is more than this number, the Plugin MUST set `next_token` + // in the response which can be used to get the next page of entries + // in the subsequent `ListVolumes` call. This field is OPTIONAL. If + // not specified (zero value), it means there is no restriction on the + // number of entries that can be returned. + // The value of this field MUST NOT be negative. + int32 max_entries = 1; + + // A token to specify where to start paginating. Set this field to + // `next_token` returned by a previous `ListVolumes` call to get the + // next page of entries. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string starting_token = 2; +} + +message ListVolumesResponse { + message VolumeStatus{ + // A list of all `node_id` of nodes that the volume in this entry + // is controller published on. + // This field is OPTIONAL. If it is not specified and the SP has + // the LIST_VOLUMES_PUBLISHED_NODES controller capability, the CO + // MAY assume the volume is not controller published to any nodes. + // If the field is not specified and the SP does not have the + // LIST_VOLUMES_PUBLISHED_NODES controller capability, the CO MUST + // not interpret this field. + // published_node_ids MAY include nodes not published to or + // reported by the SP. The CO MUST be resilient to that. + repeated string published_node_ids = 1; + + // Field 2 was VolumeCondition, an alpha API that has been removed. + reserved 2; + } + + message Entry { + // This field is REQUIRED + Volume volume = 1; + + // This field is OPTIONAL. This field MUST be specified if the + // LIST_VOLUMES_PUBLISHED_NODES controller capability is + // supported. + VolumeStatus status = 2; + } + + repeated Entry entries = 1; + + // This token allows you to get the next page of entries for + // `ListVolumes` request. If the number of entries is larger than + // `max_entries`, use the `next_token` as a value for the + // `starting_token` field in the next `ListVolumes` request. This + // field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string next_token = 2; +} +``` + +##### ListVolumes Errors + +If the plugin is unable to complete the ListVolumes call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Invalid `starting_token` | 10 ABORTED | Indicates that `starting_token` is not valid. | Caller SHOULD start the `ListVolumes` operation again with an empty `starting_token`. | + +#### `ControllerListVolumeHealth` + +**ALPHA FEATURE** + +A Controller Plugin MUST implement this RPC call if it has +`LIST_VOLUME_HEALTH` controller capability. + +A Controller Plugin that has the `LIST_VOLUME_HEALTH` controller capability +MUST also have the `GET_VOLUME_HEALTH` controller capability. This enables +the CO to use `ControllerGetVolumeHealth` to resolve inconsistent +`ControllerListVolumeHealth` paging results, including cases where a volume +is missed while listing volume health. + +The Plugin SHALL return the health information about all the volumes +that it knows about. + +The Plugin SHOULD omit volumes with no known adverse health condition when +listing all volume health. + +If volumes are created, deleted, or change health while the CO is +concurrently paging through `ControllerListVolumeHealth` results then it +is possible that the CO MAY either witness duplicate volumes in the +list, not witness existing volumes, or both. The CO SHALL NOT expect a +consistent "view" of all volume health when paging through the volume +health list via multiple calls to `ControllerListVolumeHealth`. + +```protobuf +message ControllerListVolumeHealthRequest { + option (alpha_message) = true; + + // If specified (non-zero value), the Plugin MUST NOT return more + // entries than this number in the response. If the actual number of + // entries is more than this number, the Plugin MUST set `next_token` + // in the response which can be used to get the next page of entries + // in the subsequent `ControllerListVolumeHealth` call. This field is + // OPTIONAL. If not specified (zero value), it means there is no + // restriction on the number of entries that can be returned. + // The value of this field MUST NOT be negative. + int32 max_entries = 1; + + // A token to specify where to start paginating. Set this field to + // `next_token` returned by a previous `ControllerListVolumeHealth` + // call to get the next page of entries. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string starting_token = 2; + + // Secrets required by plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 3 [(csi_secret) = true]; +} + +message ControllerListVolumeHealthResponse { + option (alpha_message) = true; + + // List of abnormal volume health entries. + repeated VolumeHealth entries = 1; + + // This token allows you to get the next page of entries for + // `ControllerListVolumeHealth` request. If the number of entries is + // larger than `max_entries`, use the `next_token` as a value for the + // `starting_token` field in the next request. This field is + // OPTIONAL. + // An empty string is equal to an unspecified field value. + string next_token = 2; +} + +enum VolumeHealthErrorType { + UNKNOWN_VOLUME_HEALTH_TYPE = 0; + + // The volume is usable but is not operating optimally. + DEGRADED = 1; + + // The volume is not accessible. + // When a volume is reported as INACCESSIBLE in + // ControllerListVolumeHealth or ControllerGetVolumeHealth + // RPC calls, the CO MAY interpret the result as + // volume not being accessible from all nodes in the cluster. + // + // When a volume is reported as INACCESSIBLE in NodeGetVolumeHealth + // RPC call the CO MAY interpret the result as volume not + // being accessible from that node. + INACCESSIBLE = 2; + + // Permanent data loss is known or strongly suspected on the + // underlying volume. The CO MAY use this signal to inform + // users to trigger recovery workflows such as restoring from + // a snapshot or replica. + DATA_LOSS = 3; +} + +message VolumeHealth { + option (alpha_message) = true; + + message VolumeHealthEntry { + // One or more health status values associated with the volume. + // Adverse health conditions should only be removed from reporting + // when the corresponding health problem is no longer happening. + // This field is REQUIRED. + VolumeHealthErrorType status = 1; + + // A brief CamelCase string that describes the condition and is + // suitable for machine parsing and concise CLI display. + // The Plugin MUST NOT return multiple VolumeHealthEntry messages + // for the same volume with the same (status, reason) combination. + // This field is REQUIRED. + string reason = 2; + + // A user friendly description of the volume health condition. + // This field is OPTIONAL. + string message = 3; + } + + // The ID of the volume. + // This field is REQUIRED. + string volume_id = 1; + + // Health statuses associated with the volume. An empty list means no + // adverse health condition is known by the Plugin. + // + // The SP MAY report one or more health issues with the volume at the + // same time. A future version of the CSI spec MAY introduce + // additional VolumeHealthErrorType values. A Plugin that adopts a + // newer spec version MAY report newer error types alongside existing + // ones, but MUST NOT remove an older error entry from + // health_statuses until that condition is no longer present. + // + // COs MUST ignore unknown VolumeHealthErrorType values, that they + // don't know about. + // + // For example: + // - CSI spec v1.12.3 + // A Plugin reports an adverse health condition as DEGRADED. + // - CSI spec v1.12.5 + // A new VolumeHealthErrorType value MULTIPATH_LOSS is introduced. + // The Plugin upgrades and now reports [DEGRADED, MULTIPATH_LOSS] + // for this volume. An older CO that does not recognize + // MULTIPATH_LOSS still acts on DEGRADED as before and surfaces + // the unknown value for observability without taking automated + // action on it. + // + // This field is OPTIONAL. + repeated VolumeHealthEntry health_statuses = 2; +} +``` + +##### ControllerListVolumeHealth Errors + +If the plugin is unable to complete the ControllerListVolumeHealth call +successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return +the specified gRPC error code. The CO MUST implement the specified error +recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Invalid `starting_token` | 10 ABORTED | Indicates that `starting_token` is not valid. | Caller SHOULD start the `ControllerListVolumeHealth` operation again with an empty `starting_token`. | + +#### `ControllerGetVolumeHealth` + +**ALPHA FEATURE** + +This optional RPC MAY be called by the CO to fetch health information +for a single volume from the Controller Plugin's perspective. + +A Controller Plugin MUST implement this RPC call if it has +`GET_VOLUME_HEALTH` or `LIST_VOLUME_HEALTH` controller capability. + +The Plugin SHOULD return a `VolumeHealth` entry with empty +`health_statuses` when the volume is known and no adverse health +condition exists. + +```protobuf +message ControllerGetVolumeHealthRequest { + option (alpha_message) = true; + + // The ID of the volume to fetch health information for. + // This field is REQUIRED. + string volume_id = 1; + + // Secrets required by plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; +} + +message ControllerGetVolumeHealthResponse { + option (alpha_message) = true; + + // The health information for the requested volume. + // This field is REQUIRED. + VolumeHealth volume_health = 1; +} +``` + +##### ControllerGetVolumeHealth Errors + +If the plugin is unable to complete the ControllerGetVolumeHealth call +successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return +the specified gRPC error code. The CO MUST implement the specified error +recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | + +#### `ControllerGetVolume` + +**ALPHA FEATURE** + +This optional RPC MAY be called by the CO to fetch current information about a volume. + +A Controller Plugin MUST implement this `ControllerGetVolume` RPC call if it has `GET_VOLUME` capability. + +`ControllerGetVolumeResponse` should contain current information of a volume if it exists. +If the volume does not exist any more, `ControllerGetVolume` should return gRPC error code `NOT_FOUND`. + +```protobuf +message ControllerGetVolumeRequest { + option (alpha_message) = true; + + // The ID of the volume to fetch current volume information for. + // This field is REQUIRED. + string volume_id = 1; +} + +message ControllerGetVolumeResponse { + option (alpha_message) = true; + + message VolumeStatus{ + // A list of all the `node_id` of nodes that this volume is + // controller published on. + // This field is OPTIONAL. + // This field MUST be specified if the LIST_VOLUMES_PUBLISHED_NODES + // controller capability is supported. + // published_node_ids MAY include nodes not published to or + // reported by the SP. The CO MUST be resilient to that. + repeated string published_node_ids = 1; + + // Field 2 was VolumeCondition, an alpha API that has been removed. + reserved 2; + } + + // This field is REQUIRED + Volume volume = 1; + + // This field is REQUIRED. + VolumeStatus status = 2; +} +``` + +##### ControllerGetVolume Errors + +If the plugin is unable to complete the ControllerGetVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | + + +#### `ControllerModifyVolume` + +A Controller plugin MUST implement this RPC call if the plugin has the MODIFY_VOLUME controller capability. +This RPC allows the CO to change mutable key attributes of a volume. + +This operation MUST be idempotent. +The new mutable parameters in ControllerModifyVolume can be different from the existing mutable parameters. + + +```protobuf +message ControllerModifyVolumeRequest { + // Contains identity information for the existing volume. + // This field is REQUIRED. + string volume_id = 1; + + // Secrets required by plugin to complete modify volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; + + // Plugin specific volume attributes to mutate, passed in as + // opaque key-value pairs. + // This field is REQUIRED. The Plugin is responsible for + // parsing and validating these parameters. COs will treat these + // as opaque. The CO SHOULD specify the intended values of all mutable + // parameters it intends to modify. SPs MUST NOT modify volumes based + // on the absence of keys, only keys that are specified should result + // in modifications to the volume. + map mutable_parameters = 3; +} + +message ControllerModifyVolumeResponse { +} + +``` + +##### ControllerModifyVolume Errors + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Parameters not supported | 3 INVALID_ARGUMENT | Indicates that the CO has specified invalid mutable parameter keys or values, or the specified volume cannot support the specified parameters. The SP MUST NOT have applied any modification to the volume as part of this specific call. | A caller may verify volume capabilities by calling ValidateVolumeCapabilities and then retry the request with valid mutable parameters. | +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified volume_id does not exist. | Caller MUST verify that the volume_id is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | + +#### `GetCapacity` + +A Controller Plugin MUST implement this RPC call if it has `GET_CAPACITY` controller capability. +The RPC allows the CO to query the capacity of the storage pool from which the controller provisions volumes. + +```protobuf +message GetCapacityRequest { + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes that satisfy ALL of the + // specified `volume_capabilities`. These are the same + // `volume_capabilities` the CO will use in `CreateVolumeRequest`. + // This field is OPTIONAL. + repeated VolumeCapability volume_capabilities = 1; + + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes with the given Plugin + // specific `parameters`. These are the same `parameters` the CO will + // use in `CreateVolumeRequest`. This field is OPTIONAL. + map parameters = 2; + + // If specified, the Plugin SHALL report the capacity of the storage + // that can be used to provision volumes that in the specified + // `accessible_topology`. This is the same as the + // `accessible_topology` the CO returns in a `CreateVolumeResponse`. + // This field is OPTIONAL. This field SHALL NOT be set unless the + // plugin advertises the VOLUME_ACCESSIBILITY_CONSTRAINTS capability. + Topology accessible_topology = 3; +} + +message GetCapacityResponse { + // The available capacity, in bytes, of the storage that can be used + // to provision volumes. If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the available capacity of the + // storage. This field is REQUIRED. + // The value of this field MUST NOT be negative. + int64 available_capacity = 1; + + // The largest size that may be used in a + // CreateVolumeRequest.capacity_range.required_bytes field + // to create a volume with the same parameters as those in + // GetCapacityRequest. + // + // If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the minimum volume size of the + // storage. + // + // This field is OPTIONAL. MUST NOT be negative. + // The Plugin SHOULD provide a value for this field if it has + // a maximum size for individual volumes and leave it unset + // otherwise. COs MAY use it to make decision about + // where to create volumes. + google.protobuf.Int64Value maximum_volume_size = 2; + + // The smallest size that may be used in a + // CreateVolumeRequest.capacity_range.limit_bytes field + // to create a volume with the same parameters as those in + // GetCapacityRequest. + // + // If `volume_capabilities` or `parameters` is + // specified in the request, the Plugin SHALL take those into + // consideration when calculating the maximum volume size of the + // storage. + // + // This field is OPTIONAL. MUST NOT be negative. + // The Plugin SHOULD provide a value for this field if it has + // a minimum size for individual volumes and leave it unset + // otherwise. COs MAY use it to make decision about + // where to create volumes. + google.protobuf.Int64Value minimum_volume_size = 3 + [(alpha_field) = true]; +} +``` + +##### GetCapacity Errors + +If the plugin is unable to complete the GetCapacity call successfully, it MUST return a non-ok gRPC code in the gRPC status. + +#### `ControllerGetCapabilities` + +A Controller Plugin MUST implement this RPC call. This RPC allows the CO to check the supported capabilities of controller service provided by the Plugin. + +```protobuf +message ControllerGetCapabilitiesRequest { + // Intentionally empty. +} + +message ControllerGetCapabilitiesResponse { + // All the capabilities that the controller service supports. This + // field is OPTIONAL. + repeated ControllerServiceCapability capabilities = 1; +} + +// Specifies a capability of the controller service. +message ControllerServiceCapability { + message RPC { + enum Type { + UNKNOWN = 0; + CREATE_DELETE_VOLUME = 1; + PUBLISH_UNPUBLISH_VOLUME = 2; + LIST_VOLUMES = 3; + GET_CAPACITY = 4; + // Currently the only way to consume a snapshot is to create + // a volume from it. Therefore plugins supporting + // CREATE_DELETE_SNAPSHOT MUST support creating volume from + // snapshot. + CREATE_DELETE_SNAPSHOT = 5; + LIST_SNAPSHOTS = 6; + + // Plugins supporting volume cloning at the storage level MAY + // report this capability. The source volume MUST be managed by + // the same plugin. Not all volume sources and parameters + // combinations MAY work. + CLONE_VOLUME = 7; + + // Indicates the SP supports ControllerPublishVolume.readonly + // field. + PUBLISH_READONLY = 8; + + // See VolumeExpansion for details. + EXPAND_VOLUME = 9; + + // Indicates the SP supports the + // ListVolumesResponse.entry.published_node_ids field and the + // ControllerGetVolumeResponse.published_node_ids field. + // The SP MUST also support PUBLISH_UNPUBLISH_VOLUME. + LIST_VOLUMES_PUBLISHED_NODES = 10; + + // Value 11 was VOLUME_CONDITION, an alpha API that has been + // removed. + reserved 11; + + // Indicates the SP supports the ControllerGetVolume RPC. + // This enables COs to, for example, fetch per volume + // information after a volume is provisioned. + GET_VOLUME = 12 [(alpha_enum_value) = true]; + + // Indicates the SP supports the SINGLE_NODE_SINGLE_WRITER and/or + // SINGLE_NODE_MULTI_WRITER access modes. + // These access modes are intended to replace the + // SINGLE_NODE_WRITER access mode to clarify the number of writers + // for a volume on a single node. Plugins MUST accept and allow + // use of the SINGLE_NODE_WRITER access mode when either + // SINGLE_NODE_SINGLE_WRITER and/or SINGLE_NODE_MULTI_WRITER are + // supported, in order to permit older COs to continue working. + SINGLE_NODE_MULTI_WRITER = 13 [(alpha_enum_value) = true]; + + // Indicates the SP supports modifying volume with mutable + // parameters. See ControllerModifyVolume for details. + MODIFY_VOLUME = 14; + + // Indicates the SP supports the GetSnapshot RPC. + // This enables COs to fetch an existing snapshot. + GET_SNAPSHOT = 15 [(alpha_enum_value) = true]; + + // Indicates the SP supports the ControllerGetVolumeHealth RPC. + // This enables COs to fetch health information for a single + // volume from the Controller Plugin's perspective. + GET_VOLUME_HEALTH = 16 [(alpha_enum_value) = true]; + + // Indicates the SP supports the ControllerListVolumeHealth RPC. + // This enables COs to fetch volume health information from + // the Controller Plugin's perspective. + // A plugin which supports the LIST_VOLUME_HEALTH capability + // MUST also support the GET_VOLUME_HEALTH capability. + LIST_VOLUME_HEALTH = 17 [(alpha_enum_value) = true]; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +``` + +##### ControllerGetCapabilities Errors + +If the plugin is unable to complete the ControllerGetCapabilities call successfully, it MUST return a non-ok gRPC code in the gRPC status. + +#### `CreateSnapshot` + +A Controller Plugin MUST implement this RPC call if it has `CREATE_DELETE_SNAPSHOT` controller capability. +This RPC will be called by the CO to create a new snapshot from a source volume on behalf of a user. + +This operation MUST be idempotent. +If a snapshot corresponding to the specified snapshot `name` is successfully cut and ready to use (meaning it MAY be specified as a `volume_content_source` in a `CreateVolumeRequest`), and is usable from `accessibility_requirements`, the Plugin MUST reply `0 OK` with the corresponding `CreateSnapshotResponse`. + +If an error occurs before a snapshot is cut, `CreateSnapshot` SHOULD return a corresponding gRPC error code that reflects the error condition. + +For plugins that supports snapshot post processing such as uploading, `CreateSnapshot` SHOULD return `0 OK` and `ready_to_use` SHOULD be set to `false` after the snapshot is cut but still being processed. +CO SHOULD then reissue the same `CreateSnapshotRequest` periodically until boolean `ready_to_use` flips to `true` indicating the snapshot has been "processed" and is ready to use to create new volumes. +If an error occurs during the process, `CreateSnapshot` SHOULD return a corresponding gRPC error code that reflects the error condition. + +A snapshot MAY be used as the source to provision a new volume. +A CreateVolumeRequest message MAY specify an OPTIONAL source snapshot parameter. +Reverting a snapshot, where data in the original volume is erased and replaced with data in the snapshot, is an advanced functionality not every storage system can support and therefore is currently out of scope. + +##### The ready_to_use Parameter + +Some SPs MAY "process" the snapshot after the snapshot is cut, for example, maybe uploading the snapshot somewhere after the snapshot is cut. +The post-cut process MAY be a long process that could take hours. +The CO MAY freeze the application using the source volume before taking the snapshot. +The purpose of `freeze` is to ensure the application data is in consistent state. +When `freeze` is performed, the container is paused and the application is also paused. +When `thaw` is performed, the container and the application start running again. +During the snapshot processing phase, since the snapshot is already cut, a `thaw` operation can be performed so application can start running without waiting for the process to complete. +The `ready_to_use` parameter of the snapshot will become `true` after the process is complete. + +For SPs that do not do additional processing after cut, the `ready_to_use` parameter SHOULD be `true` after the snapshot is cut. +`thaw` can be done when the `ready_to_use` parameter is `true` in this case. + +The `ready_to_use` parameter provides guidance to the CO on when it can "thaw" the application in the process of snapshotting. +If the cloud provider or storage system needs to process the snapshot after the snapshot is cut, the `ready_to_use` parameter returned by CreateSnapshot SHALL be `false`. +CO MAY continue to call CreateSnapshot while waiting for the process to complete until `ready_to_use` becomes `true`. +Note that CreateSnapshot no longer blocks after the snapshot is cut. + +A gRPC error code SHALL be returned if an error occurs during any stage of the snapshotting process. +A CO SHOULD explicitly delete snapshots when an error occurs. + +Based on this information, CO can issue repeated (idempotent) calls to CreateSnapshot, monitor the response, and make decisions. +Note that CreateSnapshot is a synchronous call and it MUST block until the snapshot is cut. + +```protobuf +message CreateSnapshotRequest { + // The ID of the source volume to be snapshotted. + // This field is REQUIRED. + string source_volume_id = 1; + + // The suggested name for the snapshot. This field is REQUIRED for + // idempotency. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 2; + + // Secrets required by plugin to complete snapshot creation request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 3 [(csi_secret) = true]; + + // Plugin specific parameters passed in as opaque key-value pairs. + // This field is OPTIONAL. The Plugin is responsible for parsing and + // validating these parameters. COs will treat these as opaque. + // Use cases for opaque parameters: + // - Specify a policy to automatically clean up the snapshot. + // - Specify an expiration date for the snapshot. + // - Specify whether the snapshot is readonly or read/write. + // - Specify if the snapshot should be replicated to some place. + // - Specify primary or secondary for replication systems that + // support snapshotting only on primary. + map parameters = 4; + + // Specifies where (regions, zones, racks, etc.) the provisioned + // snapshot MUST be usable from. A snapshot is usable from a location + // if volumes created from that snapshot are guaranteed to be + // accessible from that location. + // An SP SHALL advertise the requirements for topological + // accessibility information in documentation. COs SHALL only specify + // topological accessibility information supported by the SP. + // This field is OPTIONAL. + // This field SHALL NOT be specified unless the SP has the + // SNAPSHOT_ACCESSIBILITY_CONSTRAINTS plugin capability. + // If this field is not specified and the SP has the + // SNAPSHOT_ACCESSIBILITY_CONSTRAINTS plugin capability, the SP MAY + // choose where the provisioned snapshot is usable from. + TopologyRequirement accessibility_requirements = 5 + [(alpha_field) = true]; +} + +message CreateSnapshotResponse { + // Contains all attributes of the newly created snapshot that are + // relevant to the CO along with information required by the Plugin + // to uniquely identify the snapshot. This field is REQUIRED. + Snapshot snapshot = 1; +} + +// Information about a specific snapshot. +message Snapshot { + // This is the complete size of the snapshot in bytes. The purpose of + // this field is to give CO guidance on how much space is needed to + // create a volume from this snapshot. The size of the volume MUST NOT + // be less than the size of the source snapshot. This field is + // OPTIONAL. If this field is not set, it indicates that this size is + // unknown. The value of this field MUST NOT be negative and a size of + // zero means it is unspecified. + int64 size_bytes = 1; + + // The identifier for this snapshot, generated by the plugin. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific snapshot vs all other snapshots supported by this + // plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this snapshot. + // The SP is NOT responsible for global uniqueness of snapshot_id + // across multiple SPs. + string snapshot_id = 2; + + // Identity information for the source volume. Note that creating a + // snapshot from a snapshot is not supported here so the source has to + // be a volume. This field is REQUIRED. + string source_volume_id = 3; + + // Timestamp when the point-in-time snapshot is taken on the storage + // system. This field is REQUIRED. + .google.protobuf.Timestamp creation_time = 4; + + // Indicates if a snapshot is ready to use as a + // `volume_content_source` in a `CreateVolumeRequest`. The default + // value is false. This field is REQUIRED. + bool ready_to_use = 5; + + // The ID of the volume group snapshot that this snapshot is part of. + // It uniquely identifies the group snapshot on the storage system. + // This field is OPTIONAL. + // If this snapshot is a member of a volume group snapshot, and it + // MUST NOT be deleted as a stand alone snapshot, then the SP + // MUST provide the ID of the volume group snapshot in this field. + // If provided, CO MUST use this field in subsequent volume group + // snapshot operations to indicate that this snapshot is part of the + // specified group snapshot. + // If not provided, CO SHALL treat the snapshot as independent, + // and SP SHALL allow it to be deleted separately. + // If this message is inside a VolumeGroupSnapshot message, the value + // MUST be the same as the group_snapshot_id in that message. + string group_snapshot_id = 6; + + // Specifies where (regions, zones, racks, etc.) the provisioned + // snapshot is usable from. A snapshot is usable from a location if + // volumes created from that snapshot are guaranteed to be accessible + // from that location. + // A plugin that returns this field MUST also set the + // SNAPSHOT_ACCESSIBILITY_CONSTRAINTS plugin capability. + // An SP MAY specify multiple topologies to indicate the snapshot is + // usable from multiple locations. + // COs MAY use this information to ensure that a desired volume can + // be provisioned from a given snapshot when scheduling workloads. + // This field is OPTIONAL. If it is not specified, the CO MAY assume + // the snapshot is equally usable from all topologies in the + // cluster and MAY provision volumes referencing the snapshot as a + // source without topology constraints. + repeated Topology accessible_topology = 7 [(alpha_field) = true]; +} +``` + +##### CreateSnapshot Errors + +If the plugin is unable to complete the CreateSnapshot call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Snapshot already exists but is incompatible | 6 ALREADY_EXISTS | Indicates that a snapshot corresponding to the specified snapshot `name` already exists but is incompatible with the specified `volume_id`. | Caller MUST fix the arguments or use a different `name` before retrying. | +| Operation pending for snapshot | 10 ABORTED | Indicates that there is already an operation pending for the specified snapshot. In general the Cluster Orchestrator (CO) is responsible for ensuring that there is no more than one call "in-flight" per snapshot at a given time. However, in some circumstances, the CO MAY lose state (for example when the CO crashes and restarts), and MAY issue multiple calls simultaneously for the same snapshot. The Plugin, SHOULD handle this as gracefully as possible, and MAY return this error code to reject secondary calls. | Caller SHOULD ensure that there are no other calls pending for the specified snapshot, and then retry with exponential back off. | +| Not enough resources to create snapshot | 8 RESOURCE_EXHAUSTED | There is not enough space on the storage system to handle the create snapshot request, or the `accessibility_requirements` field is valid but a snapshot cannot be created with the specified topology constraints. More human-readable information MAY be provided in the gRPC `status.message` field. | Caller SHOULD fail this request. Future calls to CreateSnapshot MAY succeed once the limiting condition (e.g. space or topology/quota constraints in the requested location) is addressed. | + + +#### `DeleteSnapshot` + +A Controller Plugin MUST implement this RPC call if it has `CREATE_DELETE_SNAPSHOT` capability. +This RPC will be called by the CO to delete a snapshot. + +This operation MUST be idempotent. +If a snapshot corresponding to the specified `snapshot_id` does not exist or the artifacts associated with the snapshot do not exist anymore, the Plugin MUST reply `0 OK`. + +The CO SHALL NOT call this RPC with a snapshot for which SP provided a non-empty group_snapshot_id field at creation time. +A snapshot for which SP provided a non-empty group_snapshot_id indicates a snapshot that CAN NOT be deleted stand alone. +The SP MAY refuse to delete such snapshots with this RPC call and return an error instead. +For such snapshots SP MUST delete the entire snapshot group via a DeleteVolumeGroupSnapshotRequest call. + +```protobuf +message DeleteSnapshotRequest { + // The ID of the snapshot to be deleted. + // This field is REQUIRED. + string snapshot_id = 1; + + // Secrets required by plugin to complete snapshot deletion request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; +} + +message DeleteSnapshotResponse {} +``` + +##### DeleteSnapshot Errors + +If the plugin is unable to complete the DeleteSnapshot call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Snapshot is part of a group | 3 INVALID_ARGUMENT | Indicates that the snapshot corresponding to the specified `snapshot_id` could not be deleted because it is part of a group snapshot and CAN NOT be deleted stand alone. | Caller SHOULD stop calling DeleteSnapshot and call DeleteVolumeGroupSnapshot instead. | +| Snapshot in use | 9 FAILED_PRECONDITION | Indicates that the snapshot corresponding to the specified `snapshot_id` could not be deleted because it is in use by another resource. | Caller SHOULD ensure that there are no other resources using the snapshot, and then retry with exponential back off. | +| Operation pending for snapshot | 10 ABORTED | Indicates that there is already an operation pending for the specified snapshot. In general the Cluster Orchestrator (CO) is responsible for ensuring that there is no more than one call "in-flight" per snapshot at a given time. However, in some circumstances, the CO MAY lose state (for example when the CO crashes and restarts), and MAY issue multiple calls simultaneously for the same snapshot. The Plugin, SHOULD handle this as gracefully as possible, and MAY return this error code to reject secondary calls. | Caller SHOULD ensure that there are no other calls pending for the specified snapshot, and then retry with exponential back off. | + + +#### `ListSnapshots` + +A Controller Plugin MUST implement this RPC call if it has `LIST_SNAPSHOTS` capability. +The Plugin SHALL return the information about all snapshots on the storage system within the given parameters regardless of how they were created. +`ListSnapshots` SHALL NOT list a snapshot that is being created but has not been cut successfully yet. +If snapshots are created and/or deleted while the CO is concurrently paging through `ListSnapshots` results then it is possible that the CO MAY either witness duplicate snapshots in the list, not witness existing snapshots, or both. +The CO SHALL NOT expect a consistent "view" of all snapshots when paging through the snapshot list via multiple calls to `ListSnapshots`. + +```protobuf +// List all snapshots on the storage system regardless of how they were +// created. +message ListSnapshotsRequest { + // If specified (non-zero value), the Plugin MUST NOT return more + // entries than this number in the response. If the actual number of + // entries is more than this number, the Plugin MUST set `next_token` + // in the response which can be used to get the next page of entries + // in the subsequent `ListSnapshots` call. This field is OPTIONAL. If + // not specified (zero value), it means there is no restriction on the + // number of entries that can be returned. + // The value of this field MUST NOT be negative. + int32 max_entries = 1; + + // A token to specify where to start paginating. Set this field to + // `next_token` returned by a previous `ListSnapshots` call to get the + // next page of entries. This field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string starting_token = 2; + + // Identity information for the source volume. This field is OPTIONAL. + // It can be used to list snapshots by volume. + string source_volume_id = 3; + + // Identity information for a specific snapshot. This field is + // OPTIONAL. It can be used to list only a specific snapshot. + // ListSnapshots will return with current snapshot information + // and will not block if the snapshot is being processed after + // it is cut. + string snapshot_id = 4; + + // Secrets required by plugin to complete ListSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; +} + +message ListSnapshotsResponse { + message Entry { + Snapshot snapshot = 1; + } + + repeated Entry entries = 1; + + // This token allows you to get the next page of entries for + // `ListSnapshots` request. If the number of entries is larger than + // `max_entries`, use the `next_token` as a value for the + // `starting_token` field in the next `ListSnapshots` request. This + // field is OPTIONAL. + // An empty string is equal to an unspecified field value. + string next_token = 2; +} +``` + +##### ListSnapshots Errors + +If the plugin is unable to complete the ListSnapshots call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Invalid `starting_token` | 10 ABORTED | Indicates that `starting_token` is not valid. | Caller SHOULD start the `ListSnapshots` operation again with an empty `starting_token`. | + +#### `GetSnapshot` + +**ALPHA FEATURE** + +This optional RPC MAY be called by the CO to fetch current information about a snapshot. + +A Controller Plugin MUST implement this `GetSnapshot` RPC call if it has `GET_SNAPSHOT` capability. + +An SP that supports Group Controller Service and the CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT capability SHOULD implement the `GetSnapshot` RPC in order to retrieve information for individual snapshots that are part of a group snapshot. + +`GetSnapshotResponse` should contain current information of a snapshot if it exists. +If the snapshot does not exist any more, `GetSnapshot` should return gRPC error code `NOT_FOUND`. +Snapshots which were recently created and not yet ready to use must be retrievable with this RPC. + +```protobuf +message GetSnapshotRequest { + option (alpha_message) = true; + + // The ID of the snapshot to fetch current snapshot information for. + // This field is REQUIRED. + string snapshot_id = 1; + + // Secrets required by plugin to complete GetSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 2 [(csi_secret) = true]; +} + +message GetSnapshotResponse { + option (alpha_message) = true; + + // This field is REQUIRED + Snapshot snapshot = 1; +} +``` + +##### GetSnapshot Errors + +If the plugin is unable to complete the GetSnapshot call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Snapshot does not exist | 5 NOT_FOUND | Indicates that a snapshot corresponding to the specified `snapshot_id` does not exist. | Caller MUST verify that the `snapshot_id` is correct and that the snapshot is accessible and has not been deleted before retrying with exponential back off. | + +#### `ControllerExpandVolume` + +A Controller plugin MUST implement this RPC call if plugin has `EXPAND_VOLUME` controller capability. +This RPC allows the CO to expand the size of a volume. + +This operation MUST be idempotent. +If a volume corresponding to the specified volume ID is already larger than or equal to the target capacity of the expansion request, the plugin SHOULD reply 0 OK. + +This call MAY be made by the CO during any time in the lifecycle of the volume after creation if plugin has `VolumeExpansion.ONLINE` capability. +If plugin has `EXPAND_VOLUME` node capability, then `NodeExpandVolume` MUST be called after successful `ControllerExpandVolume` and `node_expansion_required` in `ControllerExpandVolumeResponse` is `true`. + +If specified, the `volume_capability` in `ControllerExpandVolumeRequest` should be same as what CO would pass in `ControllerPublishVolumeRequest`. + +If the plugin has only `VolumeExpansion.OFFLINE` expansion capability and volume is currently published or available on a node then `ControllerExpandVolume` MUST be called ONLY after either: +- The plugin has controller `PUBLISH_UNPUBLISH_VOLUME` capability and `ControllerUnpublishVolume` has been invoked successfully. + +OR ELSE + +- The plugin does NOT have controller `PUBLISH_UNPUBLISH_VOLUME` capability, the plugin has node `STAGE_UNSTAGE_VOLUME` capability, and `NodeUnstageVolume` has been completed successfully. + +OR ELSE + +- The plugin does NOT have controller `PUBLISH_UNPUBLISH_VOLUME` capability, nor node `STAGE_UNSTAGE_VOLUME` capability, and `NodeUnpublishVolume` has completed successfully. + +Examples: +* Offline Volume Expansion: + Given an ElasticSearch process that runs on Azure Disk and needs more space. + - The administrator takes the Elasticsearch server offline by stopping the workload and CO calls `ControllerUnpublishVolume`. + - The administrator requests more space for the volume from CO. + - The CO in turn first makes `ControllerExpandVolume` RPC call which results in requesting more space from Azure cloud provider for volume ID that was being used by ElasticSearch. + - Once `ControllerExpandVolume` is completed and successful, the CO will inform administrator about it and administrator will resume the ElasticSearch workload. + - On the node where the ElasticSearch workload is scheduled, the CO calls `NodeExpandVolume` after calling `NodeStageVolume`. + - Calling `NodeExpandVolume` on volume results in expanding the underlying file system and added space becomes available to workload when it starts up. +* Online Volume Expansion: + Given a Mysql server running on Openstack Cinder and needs more space. + - The administrator requests more space for volume from the CO. + - The CO in turn first makes `ControllerExpandVolume` RPC call which results in requesting more space from Openstack Cinder for given volume. + - On the node where the mysql workload is running, the CO calls `NodeExpandVolume` while volume is in-use using the path where the volume is staged. + - Calling `NodeExpandVolume` on volume results in expanding the underlying file system and added space automatically becomes available to mysql workload without any downtime. + + +```protobuf +message ControllerExpandVolumeRequest { + // The ID of the volume to expand. This field is REQUIRED. + string volume_id = 1; + + // This allows CO to specify the capacity requirements of the volume + // after expansion. This field is REQUIRED. + CapacityRange capacity_range = 2; + + // Secrets required by the plugin for expanding the volume. + // This field is OPTIONAL. + map secrets = 3 [(csi_secret) = true]; + + // Volume capability describing how the CO intends to use this volume. + // This allows SP to determine if volume is being used as a block + // device or mounted file system. For example - if volume is + // being used as a block device - the SP MAY set + // node_expansion_required to false in ControllerExpandVolumeResponse + // to skip invocation of NodeExpandVolume on the node by the CO. + // This is an OPTIONAL field. + VolumeCapability volume_capability = 4; +} + +message ControllerExpandVolumeResponse { + // Capacity of volume after expansion. This field is REQUIRED. + int64 capacity_bytes = 1; + + // Whether node expansion is required for the volume. When true + // the CO MUST make NodeExpandVolume RPC call on the node. This field + // is REQUIRED. + bool node_expansion_required = 2; +} +``` + +##### ControllerExpandVolume Errors + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Exceeds capabilities | 3 INVALID_ARGUMENT | Indicates that the CO has specified capabilities not supported by the volume. | Caller MAY verify volume capabilities by calling ValidateVolumeCapabilities and retry with matching capabilities. | +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified volume_id does not exist. | Caller MUST verify that the volume_id is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | +| Volume in use | 9 FAILED_PRECONDITION | Indicates that the volume corresponding to the specified `volume_id` could not be expanded because it is currently published on a node but the plugin does not have ONLINE expansion capability. | Caller SHOULD ensure that volume is not published and retry with exponential back off. | +| Unsupported `capacity_range` | 11 OUT_OF_RANGE | Indicates that the capacity range is not allowed by the Plugin. More human-readable information MAY be provided in the gRPC `status.message` field. | Caller MUST fix the capacity range before retrying. | + +#### RPC Interactions + +##### `CreateVolume`, `DeleteVolume`, `ListVolumes` + +It is worth noting that the plugin-generated `volume_id` is a REQUIRED field for the `DeleteVolume` RPC, as opposed to the CO-generated volume `name` that is REQUIRED for the `CreateVolume` RPC: these fields MAY NOT contain the same value. +If a `CreateVolume` operation times out, leaving the CO without an ID with which to reference a volume, and the CO *also* decides that it no longer needs/wants the volume in question then the CO MAY choose one of the following paths: + +1. Replay the `CreateVolume` RPC that timed out; upon success execute `DeleteVolume` using the known volume ID (from the response to `CreateVolume`). +2. Execute the `ListVolumes` RPC to possibly obtain a volume ID that may be used to execute a `DeleteVolume` RPC; upon success execute `DeleteVolume`. +3. The CO takes no further action regarding the timed out RPC, a volume is possibly leaked and the operator/user is expected to clean up. + +It is NOT REQUIRED for a controller plugin to implement the `LIST_VOLUMES` capability if it supports the `CREATE_DELETE_VOLUME` capability: the onus is upon the CO to take into consideration the full range of plugin capabilities before deciding how to proceed in the above scenario. + +##### `CreateSnapshot`, `DeleteSnapshot`, `ListSnapshots` + +The plugin-generated `snapshot_id` is a REQUIRED field for the `DeleteSnapshot` RPC, as opposed to the CO-generated snapshot `name` that is REQUIRED for the `CreateSnapshot` RPC. +A `CreateSnapshot` operation SHOULD return with a `snapshot_id` when the snapshot is cut successfully. +If a `CreateSnapshot` operation times out before the snapshot is cut, leaving the CO without an ID with which to reference a snapshot, and the CO also decides that it no longer needs/wants the snapshot in question then the CO MAY choose one of the following paths: + +1. Execute the `ListSnapshots` RPC to possibly obtain a snapshot ID that may be used to execute a `DeleteSnapshot` RPC; upon success execute `DeleteSnapshot`. +2. The CO takes no further action regarding the timed out RPC, a snapshot is possibly leaked and the operator/user is expected to clean up. + +It is NOT REQUIRED for a controller plugin to implement the `LIST_SNAPSHOTS` capability if it supports the `CREATE_DELETE_SNAPSHOT` capability: the onus is upon the CO to take into consideration the full range of plugin capabilities before deciding how to proceed in the above scenario. + +ListSnapshots SHALL return with current information regarding the snapshots on the storage system. +When processing is complete, the `ready_to_use` parameter of the snapshot from ListSnapshots SHALL become `true`. +The downside of calling ListSnapshots is that ListSnapshots will not return a gRPC error code if an error occurs during the processing. So calling CreateSnapshot repeatedly is the preferred way to check if the processing is complete. + +### Node Service RPC + +#### `NodeStageVolume` + +A Node Plugin MUST implement this RPC call if it has `STAGE_UNSTAGE_VOLUME` node capability. + +This RPC is called by the CO prior to the volume being consumed by any workloads on the node by `NodePublishVolume`. +The Plugin SHALL assume that this RPC will be executed on the node where the volume will be used. +This RPC SHOULD be called by the CO when a workload that wants to use the specified volume is placed (scheduled) on the specified node for the first time or for the first time since a `NodeUnstageVolume` call for the specified volume was called and returned success on that node. + +If the corresponding Controller Plugin has `PUBLISH_UNPUBLISH_VOLUME` controller capability and the Node Plugin has `STAGE_UNSTAGE_VOLUME` capability, then the CO MUST guarantee that this RPC is called after `ControllerPublishVolume` is called for the given volume on the given node and returns a success. +The CO MUST guarantee that this RPC is called and returns a success before any `NodePublishVolume` is called for the given volume on the given node. + +This operation MUST be idempotent. +If the volume corresponding to the `volume_id` is already staged to the `staging_target_path`, and is identical to the specified `volume_capability` the Plugin MUST reply `0 OK`. + +If this RPC failed, or the CO does not know if it failed or not, it MAY choose to call `NodeStageVolume` again, or choose to call `NodeUnstageVolume`. + +```protobuf +message NodeStageVolumeRequest { + // The ID of the volume to publish. This field is REQUIRED. + string volume_id = 1; + + // The CO SHALL set this field to the value returned by + // `ControllerPublishVolume` if the corresponding Controller Plugin + // has `PUBLISH_UNPUBLISH_VOLUME` controller capability, and SHALL be + // left unset if the corresponding Controller Plugin does not have + // this capability. This is an OPTIONAL field. + map publish_context = 2; + + // The path to which the volume MAY be staged. It MUST be an + // absolute path in the root filesystem of the process serving this + // request, and MUST be a directory. The CO SHALL ensure that there + // is only one `staging_target_path` per volume. The CO SHALL ensure + // that the path is directory and that the process serving the + // request has `read` and `write` permission to that directory. The + // CO SHALL be responsible for creating the directory if it does not + // exist. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the staged volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 4; + + // Secrets required by plugin to complete node stage volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 6; +} + +message NodeStageVolumeResponse { + // Intentionally empty. +} +``` + +#### NodeStageVolume Errors + +If the plugin is unable to complete the NodeStageVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | +| Volume published but is incompatible | 6 ALREADY_EXISTS | Indicates that a volume corresponding to the specified `volume_id` has already been published at the specified `staging_target_path` but is incompatible with the specified `volume_capability` flag. | Caller MUST fix the arguments before retrying. | +| Exceeds capabilities | 9 FAILED_PRECONDITION | Indicates that the CO has specified capabilities not supported by the volume. | Caller MAY choose to call `ValidateVolumeCapabilities` to validate the volume capabilities, or wait for the volume to be unpublished on the node. | + +#### `NodeUnstageVolume` + +A Node Plugin MUST implement this RPC call if it has `STAGE_UNSTAGE_VOLUME` node capability. + +This RPC is a reverse operation of `NodeStageVolume`. +This RPC MUST undo the work by the corresponding `NodeStageVolume`. +This RPC SHALL be called by the CO once for each `staging_target_path` that was successfully setup via `NodeStageVolume`. + +If the corresponding Controller Plugin has `PUBLISH_UNPUBLISH_VOLUME` controller capability and the Node Plugin has `STAGE_UNSTAGE_VOLUME` capability, the CO MUST guarantee that this RPC is called and returns success before calling `ControllerUnpublishVolume` for the given node and the given volume. +The CO MUST guarantee that this RPC is called after all `NodeUnpublishVolume` have been called and returned success for the given volume on the given node. + +The Plugin SHALL assume that this RPC will be executed on the node where the volume is being used. + +This RPC MAY be called by the CO when the workload using the volume is being moved to a different node, or all the workloads using the volume on a node have finished. + +This operation MUST be idempotent. +If the volume corresponding to the `volume_id` is not staged to the `staging_target_path`, the Plugin MUST reply `0 OK`. + +If this RPC failed, or the CO does not know if it failed or not, it MAY choose to call `NodeUnstageVolume` again. + +```protobuf +message NodeUnstageVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path at which the volume was staged. It MUST be an absolute + // path in the root filesystem of the process serving this request. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 2; +} + +message NodeUnstageVolumeResponse { + // Intentionally empty. +} +``` + +#### NodeUnstageVolume Errors + +If the plugin is unable to complete the NodeUnstageVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | + +#### RPC Interactions and Reference Counting +`NodeStageVolume`, `NodeUnstageVolume`, `NodePublishVolume`, `NodeUnpublishVolume` + +The following interaction semantics ARE REQUIRED if the plugin advertises the `STAGE_UNSTAGE_VOLUME` capability. +`NodeStageVolume` MUST be called and return success once per volume per node before any `NodePublishVolume` MAY be called for the volume. +All `NodeUnpublishVolume` MUST be called and return success for a volume before `NodeUnstageVolume` MAY be called for the volume. + +Note that this requires that all COs MUST support reference counting of volumes so that if `STAGE_UNSTAGE_VOLUME` is advertised by the SP, the CO MUST fulfill the above interaction semantics. + +#### `NodePublishVolume` + +This RPC is called by the CO when a workload that wants to use the specified volume is placed (scheduled) on a node. +The Plugin SHALL assume that this RPC will be executed on the node where the volume will be used. + +If the corresponding Controller Plugin has `PUBLISH_UNPUBLISH_VOLUME` controller capability, the CO MUST guarantee that this RPC is called after `ControllerPublishVolume` is called for the given volume on the given node and returns a success. + +This operation MUST be idempotent. +If the volume corresponding to the `volume_id` has already been published at the specified `target_path`, and is compatible with the specified `volume_capability` and `readonly` flag, the Plugin MUST reply `0 OK`. + +If this RPC failed, or the CO does not know if it failed or not, it MAY choose to call `NodePublishVolume` again, or choose to call `NodeUnpublishVolume`. + +This RPC MAY be called by the CO multiple times on the same node for the same volume with a possibly different `target_path` and/or other arguments if the volume supports either `MULTI_NODE_...` or `SINGLE_NODE_MULTI_WRITER` access modes (see second table). +The possible `MULTI_NODE_...` access modes are `MULTI_NODE_READER_ONLY`, `MULTI_NODE_SINGLE_WRITER` or `MULTI_NODE_MULTI_WRITER`. +COs SHOULD NOT call `NodePublishVolume` a second time with a different `volume_capability`. +If this happens, the Plugin SHOULD return `FAILED_PRECONDITION`. + +The following table shows what the Plugin SHOULD return when receiving a second `NodePublishVolume` on the same volume on the same node: + +(**Tn**: target path of the nth `NodePublishVolume`; **Pn**: other arguments of the nth `NodePublishVolume` except `secrets`) + +| | T1=T2, P1=P2 | T1=T2, P1!=P2 | T1!=T2, P1=P2 | T1!=T2, P1!=P2 | +|--------------------|-----------------|----------------|---------------------|--------------------| +| MULTI_NODE_... | OK (idempotent) | ALREADY_EXISTS | OK | OK | +| Non MULTI_NODE_... | OK (idempotent) | ALREADY_EXISTS | FAILED_PRECONDITION | FAILED_PRECONDITION| + +NOTE: If the Plugin supports the `SINGLE_NODE_MULTI_WRITER` capability, use the following table instead for what the Plugin SHOULD return when receiving a second `NodePublishVolume` on the same volume on the same node: + +| | T1=T2, P1=P2 | T1=T2, P1!=P2 | T1!=T2, P1=P2 | T1!=T2, P1!=P2 | +|---------------------------------------|-----------------|----------------|---------------------|---------------------| +| SINGLE_NODE_SINGLE_WRITER | OK (idempotent) | ALREADY_EXISTS | FAILED_PRECONDITION | FAILED_PRECONDITION | +| SINGLE_NODE_MULTI_WRITER | OK (idempotent) | ALREADY_EXISTS | OK | OK | +| MULTI_NODE_... | OK (idempotent) | ALREADY_EXISTS | OK | OK | +| Non MULTI_NODE_... | OK (idempotent) | ALREADY_EXISTS | FAILED_PRECONDITION | FAILED_PRECONDITION | + +The `SINGLE_NODE_SINGLE_WRITER` and `SINGLE_NODE_MULTI_WRITER` access modes are intended to replace the `SINGLE_NODE_WRITER` access mode to clarify the number of writers for a volume on a single node. +Plugins MUST accept and allow use of the `SINGLE_NODE_WRITER` access mode (subject to the processing rules above), when either `SINGLE_NODE_SINGLE_WRITER` and/or `SINGLE_NODE_MULTI_WRITER` are supported, in order to permit older COs to continue working. + + +```protobuf +message NodePublishVolumeRequest { + // The ID of the volume to publish. This field is REQUIRED. + string volume_id = 1; + + // The CO SHALL set this field to the value returned by + // `ControllerPublishVolume` if the corresponding Controller Plugin + // has `PUBLISH_UNPUBLISH_VOLUME` controller capability, and SHALL be + // left unset if the corresponding Controller Plugin does not have + // this capability. This is an OPTIONAL field. + map publish_context = 2; + + // The path to which the volume was staged by `NodeStageVolume`. + // It MUST be an absolute path in the root filesystem of the process + // serving this request. + // It MUST be set if the Node Plugin implements the + // `STAGE_UNSTAGE_VOLUME` node capability. + // This is an OPTIONAL field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; + + // The path to which the volume will be published. It MUST be an + // absolute path in the root filesystem of the process serving this + // request. The CO SHALL ensure uniqueness of target_path per volume. + // The CO SHALL ensure that the parent directory of this path exists + // and that the process serving the request has `read` and `write` + // permissions to that parent directory. + // For volumes with an access type of block, the SP SHALL place the + // block device at target_path. + // For volumes with an access type of mount, the SP SHALL place the + // mounted directory at target_path. + // Creation of target_path is the responsibility of the SP. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string target_path = 4; + + // Volume capability describing how the CO intends to use this volume. + // SP MUST ensure the CO can use the published volume as described. + // Otherwise SP MUST return the appropriate gRPC error code. + // This is a REQUIRED field. + VolumeCapability volume_capability = 5; + + // Indicates SP MUST publish the volume in readonly mode. + // This field is REQUIRED. + bool readonly = 6; + + // Secrets required by plugin to complete node publish volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 7 [(csi_secret) = true]; + + // Volume context as returned by SP in + // CreateVolumeResponse.Volume.volume_context. + // This field is OPTIONAL and MUST match the volume_context of the + // volume identified by `volume_id`. + map volume_context = 8; +} + +message NodePublishVolumeResponse { + // Intentionally empty. +} +``` + +##### NodePublishVolume Errors + +If the plugin is unable to complete the NodePublishVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | +| Volume published but is incompatible | 6 ALREADY_EXISTS | Indicates that a volume corresponding to the specified `volume_id` has already been published at the specified `target_path` but is incompatible with the specified `volume_capability` or `readonly` flag. | Caller MUST fix the arguments before retrying. | +| Exceeds capabilities | 9 FAILED_PRECONDITION | Indicates that the CO has specified capabilities not supported by the volume. | Caller MAY choose to call `ValidateVolumeCapabilities` to validate the volume capabilities, or wait for the volume to be unpublished on the node. | +| Staging target path not set | 9 FAILED_PRECONDITION | Indicates that `STAGE_UNSTAGE_VOLUME` capability is set but no `staging_target_path` was set. | Caller MUST make sure call to `NodeStageVolume` is made and returns success before retrying with valid `staging_target_path`. | + + + +#### `NodeUnpublishVolume` + +A Node Plugin MUST implement this RPC call. +This RPC is a reverse operation of `NodePublishVolume`. +This RPC MUST undo the work by the corresponding `NodePublishVolume`. +This RPC SHALL be called by the CO at least once for each `target_path` that was successfully setup via `NodePublishVolume`. +If the corresponding Controller Plugin has `PUBLISH_UNPUBLISH_VOLUME` controller capability, the CO SHOULD issue all `NodeUnpublishVolume` (as specified above) before calling `ControllerUnpublishVolume` for the given node and the given volume. +The Plugin SHALL assume that this RPC will be executed on the node where the volume is being used. + +This RPC is typically called by the CO when the workload using the volume is being moved to a different node, or all the workload using the volume on a node has finished. + +This operation MUST be idempotent. +If this RPC failed, or the CO does not know if it failed or not, it can choose to call `NodeUnpublishVolume` again. + +```protobuf +message NodeUnpublishVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path at which the volume was published. It MUST be an absolute + // path in the root filesystem of the process serving this request. + // The SP MUST delete the file or directory it created at this path. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string target_path = 2; +} + +message NodeUnpublishVolumeResponse { + // Intentionally empty. +} +``` + +##### NodeUnpublishVolume Errors + +If the plugin is unable to complete the NodeUnpublishVolume call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | + + +#### `NodeGetVolumeStats` + +A Node plugin MUST implement this RPC call if it has GET_VOLUME_STATS node capability. +`NodeGetVolumeStats` RPC call returns the volume capacity statistics available for the volume. + +If the volume is being used in `BlockVolume` mode then `used` and `available` MAY be omitted from `usage` field of `NodeGetVolumeStatsResponse`. +Similarly, inode information MAY be omitted from `NodeGetVolumeStatsResponse` when unavailable. + +The `staging_target_path` field is not required, for backwards compatibility, but the CO SHOULD supply it. +Plugins can use this field to determine if `volume_path` is where the volume is published or staged, +and setting this field to non-empty allows plugins to function with less stored state on the node. + +```protobuf +message NodeGetVolumeStatsRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // It can be any valid path where volume was previously + // staged or published. + // It MUST be an absolute path in the root filesystem of + // the process serving this request. + // This is a REQUIRED field. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string volume_path = 2; + + // The path where the volume is staged, if the plugin has the + // STAGE_UNSTAGE_VOLUME capability, otherwise empty. + // If not empty, it MUST be an absolute path in the root + // filesystem of the process serving this request. + // This field is OPTIONAL. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; +} + +message NodeGetVolumeStatsResponse { + // This field is OPTIONAL. + repeated VolumeUsage usage = 1; + + // Field 2 was VolumeCondition, an alpha API that has been removed. + reserved 2; +} + +message VolumeUsage { + enum Unit { + UNKNOWN = 0; + BYTES = 1; + INODES = 2; + } + // The available capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + int64 available = 1; + + // The total capacity in specified Unit. This field is REQUIRED. + // The value of this field MUST NOT be negative. + int64 total = 2; + + // The used capacity in specified Unit. This field is OPTIONAL. + // The value of this field MUST NOT be negative. + int64 used = 3; + + // Units by which values are measured. This field is REQUIRED. + Unit unit = 4; +} + +``` + +##### NodeGetVolumeStats Errors + +If the plugin is unable to complete the `NodeGetVolumeStats` call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist on specified `volume_path`. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible on specified `volume_path` and has not been deleted before retrying with exponential back off. | + +#### `NodeGetVolumeHealth` + +**ALPHA FEATURE** + +This optional RPC MAY be called by the CO to fetch volume health from +the Node Plugin's perspective. + +A Node Plugin MUST implement this RPC call if it has `GET_VOLUME_HEALTH` +node capability. + +Unlike `NodeGetVolumeStats`, this RPC MAY be called for a volume that is +not currently staged or published on the node if the CO has recently +attempted to stage or publish the volume on the node. This allows the +Node Plugin to report health for a volume that could not be staged or +published because of a health problem. + +```protobuf +message NodeGetVolumeHealthRequest { + option (alpha_message) = true; + + // The ID of the volume. + // This field is REQUIRED. + string volume_id = 1; + + // The path where the volume was published or CO attempted to + // publish on the node. If not empty, it MUST be an absolute path + // in the root filesystem of the process serving this request. + // This field is OPTIONAL. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string volume_publish_path = 2; + + // The path where the volume was staged or CO attempted to + // stage the volume, if the plugin has the + // STAGE_UNSTAGE_VOLUME capability, otherwise empty. + // If not empty, it MUST be an absolute path in the root + // filesystem of the process serving this request. + // This field is OPTIONAL. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 3; +} + +message NodeGetVolumeHealthResponse { + option (alpha_message) = true; + + // The health information for the requested volume. + // This field is REQUIRED. + VolumeHealth volume_health = 1; +} +``` + +##### NodeGetVolumeHealth Errors + +If the plugin is unable to complete the `NodeGetVolumeHealth` call +successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return +the specified gRPC error code. The CO MUST implement the specified error +recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified `volume_id` does not exist on the node. | Caller MUST verify that the `volume_id` is correct and that the volume is accessible on the node and has not been deleted before retrying with exponential back off. | + +#### `NodeGetStorageHealth` + +**ALPHA FEATURE** + +This optional RPC MAY be called by the CO to fetch storage system health +from the Node Plugin's perspective. The response describes health +conditions that can affect one or more volumes on the node, including +backend or protocol health that is not specific to a single volume. + +A Node Plugin MUST implement this RPC call if it has `GET_STORAGE_HEALTH` +node capability. + +```protobuf +message NodeGetStorageHealthRequest { + option (alpha_message) = true; + + // Secrets required by the plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 1 [(csi_secret) = true]; +} + +message NodeGetStorageHealthResponse { + option (alpha_message) = true; + + message StorageBackendHealth { + // Health status for this storage backend. + // This field is REQUIRED. + StorageHealthErrorType status = 1; + + // A brief CamelCase string that describes the condition and is + // suitable for machine parsing and concise CLI display. + // The Plugin MUST NOT return multiple StorageBackendHealth messages + // with the same (status, reason, volume_capability) combination. + // This field is REQUIRED. + string reason = 2; + + // A user friendly description of the storage health condition. + // This field is OPTIONAL. + string message = 3; + + // Volume capability affected by this storage health condition. + // This field is OPTIONAL. + VolumeCapability volume_capability = 4; + } + + // Health information for storage backends or classes available from + // this node. + // This field is OPTIONAL. + repeated StorageBackendHealth backend_health = 1; +} + +enum StorageHealthErrorType { + UNKNOWN_STORAGE_HEALTH_ERROR_TYPE = 0; + + // The storage backend is operating in a degraded + // state (e.g. reduced path count, high latency). + // Volumes using this backend may experience reduced performance. + STORAGE_DEGRADED = 1; + + // The storage backend is completely unreachable from this node. + // Volumes using this backend are expected to be unavailable. + STORAGE_UNREACHABLE = 2; +} +``` + +##### NodeGetStorageHealth Errors + +If the plugin is unable to complete the `NodeGetStorageHealth` call +successfully, it MUST return a non-ok gRPC code in the gRPC status. + +#### `NodeGetCapabilities` + +A Node Plugin MUST implement this RPC call. +This RPC allows the CO to check the supported capabilities of node service provided by the Plugin. + +```protobuf +message NodeGetCapabilitiesRequest { + // Intentionally empty. +} + +message NodeGetCapabilitiesResponse { + // All the capabilities that the node service supports. This field + // is OPTIONAL. + repeated NodeServiceCapability capabilities = 1; +} + +// Specifies a capability of the node service. +message NodeServiceCapability { + message RPC { + enum Type { + UNKNOWN = 0; + STAGE_UNSTAGE_VOLUME = 1; + // If Plugin implements GET_VOLUME_STATS capability + // then it MUST implement NodeGetVolumeStats RPC + // call for fetching volume statistics. + GET_VOLUME_STATS = 2; + // See VolumeExpansion for details. + EXPAND_VOLUME = 3; + + // Value 4 was VOLUME_CONDITION, an alpha API that has been + // removed. + reserved 4; + + // Indicates the SP supports the SINGLE_NODE_SINGLE_WRITER and/or + // SINGLE_NODE_MULTI_WRITER access modes. + // These access modes are intended to replace the + // SINGLE_NODE_WRITER access mode to clarify the number of writers + // for a volume on a single node. Plugins MUST accept and allow + // use of the SINGLE_NODE_WRITER access mode (subject to the + // processing rules for NodePublishVolume), when either + // SINGLE_NODE_SINGLE_WRITER and/or SINGLE_NODE_MULTI_WRITER are + // supported, in order to permit older COs to continue working. + SINGLE_NODE_MULTI_WRITER = 5 [(alpha_enum_value) = true]; + + // Indicates that Node service supports mounting volumes + // with provided volume group identifier during node stage + // or node publish RPC calls. + VOLUME_MOUNT_GROUP = 6; + + // Indicates the SP supports the NodeGetVolumeHealth RPC. + // This enables COs to fetch per-volume health information from + // the Node Plugin's perspective. + GET_VOLUME_HEALTH = 7 [(alpha_enum_value) = true]; + + // Indicates the SP supports the NodeGetStorageHealth RPC. + // This enables COs to fetch health of entire storage subsystem + // from node's perspective. + GET_STORAGE_HEALTH = 8 [(alpha_enum_value) = true]; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +``` + +##### NodeGetCapabilities Errors + +If the plugin is unable to complete the NodeGetCapabilities call successfully, it MUST return a non-ok gRPC code in the gRPC status. + +#### `NodeGetInfo` + +A Node Plugin MUST implement this RPC call if the plugin has `PUBLISH_UNPUBLISH_VOLUME` controller capability. +The Plugin SHALL assume that this RPC will be executed on the node where the volume will be used. +The CO SHOULD call this RPC for the node at which it wants to place the workload. +The CO MAY call this RPC more than once for a given node. +The SP SHALL NOT expect the CO to call this RPC more than once. +The result of this call will be used by CO in `ControllerPublishVolume`. + +```protobuf +message NodeGetInfoRequest { +} + +message NodeGetInfoResponse { + // The identifier of the node as understood by the SP. + // This field is REQUIRED. + // This field MUST contain enough information to uniquely identify + // this specific node vs all other nodes supported by this plugin. + // This field SHALL be used by the CO in subsequent calls, including + // `ControllerPublishVolume`, to refer to this node. + // The SP is NOT responsible for global uniqueness of node_id across + // multiple SPs. + // This field overrides the general CSI size limit. + // The size of this field SHALL NOT exceed 256 bytes. The general + // CSI size limit, 128 byte, is RECOMMENDED for best backwards + // compatibility. + string node_id = 1; + + // Maximum number of volumes that controller can publish to the node. + // If value is not set or zero CO SHALL decide how many volumes of + // this type can be published by the controller to the node. The + // plugin MUST NOT set negative values here. + // This field is OPTIONAL. + int64 max_volumes_per_node = 2; + + // Specifies where (regions, zones, racks, etc.) the node is + // accessible from. + // A plugin that returns this field MUST also set the + // VOLUME_ACCESSIBILITY_CONSTRAINTS plugin capability. + // COs MAY use this information along with the topology information + // returned in CreateVolumeResponse to ensure that a given volume is + // accessible from a given node when scheduling workloads. + // This field is OPTIONAL. If it is not specified, the CO MAY assume + // the node is not subject to any topological constraint, and MAY + // schedule workloads that reference any volume V, such that there are + // no topological constraints declared for V. + // + // Example 1: + // accessible_topology = + // {"region": "R1", "zone": "Z2"} + // Indicates the node exists within the "region" "R1" and the "zone" + // "Z2". + Topology accessible_topology = 3; +} +``` + +##### NodeGetInfo Errors + +If the plugin is unable to complete the NodeGetInfo call successfully, it MUST return a non-ok gRPC code in the gRPC status. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +#### `NodeExpandVolume` + +A Node Plugin MUST implement this RPC call if it has `EXPAND_VOLUME` node capability. +This RPC call allows CO to expand volume on a node. + +This operation MUST be idempotent. +If a volume corresponding to the specified volume ID is already larger than or equal to the target capacity of the expansion request, the plugin SHOULD reply 0 OK. + +`NodeExpandVolume` ONLY supports expansion of already node-published or node-staged volumes on the given `volume_path`. + +If plugin has `STAGE_UNSTAGE_VOLUME` node capability then: +* `NodeExpandVolume` MUST be called after successful `NodeStageVolume`. +* `NodeExpandVolume` MAY be called before or after `NodePublishVolume`. + +Otherwise `NodeExpandVolume` MUST be called after successful `NodePublishVolume`. + +If a plugin only supports expansion via the `VolumeExpansion.OFFLINE` capability, then the volume MUST first be taken offline and expanded via `ControllerExpandVolume` (see `ControllerExpandVolume` for more details), and then node-staged or node-published before it can be expanded on the node via `NodeExpandVolume`. + +The `staging_target_path` field is not required, for backwards compatibility, but the CO SHOULD supply it. +Plugins can use this field to determine if `volume_path` is where the volume is published or staged, +and setting this field to non-empty allows plugins to function with less stored state on the node. + +```protobuf +message NodeExpandVolumeRequest { + // The ID of the volume. This field is REQUIRED. + string volume_id = 1; + + // The path on which volume is available. This field is REQUIRED. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string volume_path = 2; + + // This allows CO to specify the capacity requirements of the volume + // after expansion. If capacity_range is omitted then a plugin MAY + // inspect the file system of the volume to determine the maximum + // capacity to which the volume can be expanded. In such cases a + // plugin MAY expand the volume to its maximum capacity. + // This field is OPTIONAL. + CapacityRange capacity_range = 3; + + // The path where the volume is staged, if the plugin has the + // STAGE_UNSTAGE_VOLUME capability, otherwise empty. + // If not empty, it MUST be an absolute path in the root + // filesystem of the process serving this request. + // This field is OPTIONAL. + // This field overrides the general CSI size limit. + // SP SHOULD support the maximum path length allowed by the operating + // system/filesystem, but, at a minimum, SP MUST accept a max path + // length of at least 128 bytes. + string staging_target_path = 4; + + // Volume capability describing how the CO intends to use this volume. + // This allows SP to determine if volume is being used as a block + // device or mounted file system. For example - if volume is being + // used as a block device the SP MAY choose to skip expanding the + // filesystem in NodeExpandVolume implementation but still perform + // rest of the housekeeping needed for expanding the volume. If + // volume_capability is omitted the SP MAY determine + // access_type from given volume_path for the volume and perform + // node expansion. This is an OPTIONAL field. + VolumeCapability volume_capability = 5; + + // Secrets required by plugin to complete node expand volume request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 6 + [(csi_secret) = true, (alpha_field) = true]; +} + +message NodeExpandVolumeResponse { + // The capacity of the volume in bytes. This field is OPTIONAL. + int64 capacity_bytes = 1; +} +``` + +##### NodeExpandVolume Errors + +| Condition | gRPC code | Description | Recovery Behavior | +|-----------------------|-----------|-----------------------|-----------------------------------| +| Exceeds capabilities | 3 INVALID_ARGUMENT | Indicates that the CO has specified capabilities not supported by the volume. | Caller MAY verify volume capabilities by calling ValidateVolumeCapabilities and retry with matching capabilities. | +| Volume does not exist | 5 NOT_FOUND | Indicates that a volume corresponding to the specified volume_id does not exist. | Caller MUST verify that the volume_id is correct and that the volume is accessible and has not been deleted before retrying with exponential back off. | +| Volume in use | 9 FAILED_PRECONDITION | Indicates that the volume corresponding to the specified `volume_id` could not be expanded because it is node-published or node-staged and the underlying filesystem does not support expansion of published or staged volumes. | Caller MUST NOT retry. | +| Unsupported capacity_range | 11 OUT_OF_RANGE | Indicates that the capacity range is not allowed by the Plugin. More human-readable information MAY be provided in the gRPC `status.message` field. | Caller MUST fix the capacity range before retrying. | + +### Group Controller Service RPCs + +#### `GroupControllerGetCapabilities` + +A Plugin that implements GroupController MUST implement this RPC call. +This RPC allows the CO to check the supported capabilities of group controller service provided by the Plugin. + +```protobuf +message GroupControllerGetCapabilitiesRequest { + // Intentionally empty. +} + +message GroupControllerGetCapabilitiesResponse { + // All the capabilities that the group controller service supports. + // This field is OPTIONAL. + repeated GroupControllerServiceCapability capabilities = 1; +} + +// Specifies a capability of the group controller service. +message GroupControllerServiceCapability { + message RPC { + enum Type { + UNKNOWN = 0; + + // Indicates that the group controller plugin supports + // creating, deleting, and getting details of a volume + // group snapshot. + CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT = 1; + } + + Type type = 1; + } + + oneof type { + // RPC that the controller supports. + RPC rpc = 1; + } +} +``` + +##### GroupControllerGetCapabilities Errors + +If the plugin is unable to complete the GroupControllerGetCapabilities call successfully, it MUST return a non-ok gRPC code in the gRPC status. + +#### `CreateVolumeGroupSnapshot` + +A Group Controller Plugin MUST implement this RPC call if it has `CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT` controller capability. +This RPC will be called by the CO to create a new volume group snapshot from a list of source volumes on behalf of a user. + +The purpose of this call is to request the creation of a multi-volume group snapshot. +This group snapshot MUST give a write-order consistency guarantee or fail if that's not possible. +That is to say, all the of the volume snapshots in the group MUST be taken at the same point-in-time relative to a stream of write traffic to the specified volumes. +Note that calls to this function MUST be idempotent - the function may be called multiple times for the same name, with the same `source_volume_ids` and `parameters` - the group snapshot MUST only be created once. + +If a group snapshot corresponding to the specified group snapshot `name`, `source_volume_ids`, and `parameters` is successfully created (meaning all snapshots associated with the group are successfully cut), the Plugin MUST reply `0 OK` with the corresponding `CreateVolumeGroupSnapshotResponse`. + +If an error occurs before a group snapshot is cut, `CreateVolumeGroupSnapshot` SHOULD return a corresponding gRPC error code that reflects the error condition. + +For plugins that support snapshot post processing such as uploading, CreateVolumeGroupSnapshot SHOULD return 0 OK and the ready_to_use field SHOULD be set to false after the group snapshot is cut but still being processed. +The CO SHOULD issue the CreateVolumeGroupSnapshotRequest RPC with the same arguments again periodically until the ready_to_use field has a value of true indicating all the snapshots have been "processed" and are ready to use to create new volumes. +If an error occurs during the process for any individual snapshot, CreateVolumeGroupSnapshot SHOULD return a corresponding gRPC error code that reflects the error condition. +The ready_to_use field for each individual snapshot SHOULD have a value of false until the snapshot has been "processed" and is ready to use to create new volumes. + +After snapshot creation, any individual snapshot from the group MAY be used as a source to provision a new volume. + +In the VolumeGroupSnapshot message, both snapshots and group_snapshot_id are required fields. + +If an error occurs before all the individual snapshots are cut when creating a group snapshot of multiple volumes and a group_snapshot_id is not yet available for CO to do clean up, SP MUST return an error, and SP SHOULD also do clean up and make sure no snapshots are leaked. + +```protobuf +message CreateVolumeGroupSnapshotRequest { + // The suggested name for the group snapshot. This field is REQUIRED + // for idempotency. + // Any Unicode string that conforms to the length limit is allowed + // except those containing the following banned characters: + // U+0000-U+0008, U+000B, U+000C, U+000E-U+001F, U+007F-U+009F. + // (These are control characters other than commonly used whitespace.) + string name = 1; + + // volume IDs of the source volumes to be snapshotted together. + // This field is REQUIRED. + repeated string source_volume_ids = 2; + + // Secrets required by plugin to complete + // ControllerCreateVolumeGroupSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; + + // Plugin specific parameters passed in as opaque key-value pairs. + // This field is OPTIONAL. The Plugin is responsible for parsing and + // validating these parameters. COs will treat these as opaque. + map parameters = 4; +} + +message CreateVolumeGroupSnapshotResponse { + // Contains all attributes of the newly created group snapshot. + // This field is REQUIRED. + VolumeGroupSnapshot group_snapshot = 1; +} + +message VolumeGroupSnapshot { + // The identifier for this group snapshot, generated by the plugin. + // This field MUST contain enough information to uniquely identify + // this specific snapshot vs all other group snapshots supported by + // this plugin. + // This field SHALL be used by the CO in subsequent calls to refer to + // this group snapshot. + // The SP is NOT responsible for global uniqueness of + // group_snapshot_id across multiple SPs. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshots belonging to this group. + // This field is REQUIRED. + repeated Snapshot snapshots = 2; + + // Timestamp of when the volume group snapshot was taken. + // This field is REQUIRED. + .google.protobuf.Timestamp creation_time = 3; + + // Indicates if all individual snapshots in the group snapshot + // are ready to use as a `volume_content_source` in a + // `CreateVolumeRequest`. The default value is false. + // If any snapshot in the list of snapshots in this message have + // ready_to_use set to false, the SP MUST set this field to false. + // If all of the snapshots in the list of snapshots in this message + // have ready_to_use set to true, the SP SHOULD set this field to + // true. + // This field is REQUIRED. + bool ready_to_use = 4; +} +``` + +##### CreateVolumeGroupSnapshot Errors + +If the plugin is unable to complete the CreateVolumeGroupSnapshot call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Group snapshot already exists but is incompatible | 6 ALREADY_EXISTS | Indicates that a group snapshot corresponding to the specified group snapshot `name` already exists but is incompatible with the specified `source_volume_ids` or `parameters`. | Caller MUST fix the arguments or use a different `name` before retrying. | +| Cannot snapshot multiple volumes together | 9 FAILED_PRECONDITION | Indicates that the specified volumes cannot be snapshotted together because the volumes are not configured properly based on requirements from the SP. | Caller MUST fix the configuration of the volumes so that they meet the requirements for group snapshotting before retrying. | +| Operation pending for group snapshot | 10 ABORTED | Indicates that there is already an operation pending for the a group snapshot with the exact same parameters. In general the CO is responsible for ensuring that there is no more than one call "in-flight" per group snapshot at a given time. However, in some circumstances, the CO MAY lose state (for example when the CO crashes and restarts), and MAY issue multiple calls simultaneously for the same group snapshot. The Plugin, SHOULD handle this as gracefully as possible, and MAY return this error code to reject secondary calls. | Caller SHOULD ensure that there are no other calls pending for the same group snapshot, and then retry with exponential back off. | +| Not enough space to create group snapshot | 8 RESOURCE_EXHAUSTED | There is not enough space on the storage system to handle the create group snapshot request. | Future calls to CreateVolumeGroupSnapshot MAY succeed if space is freed up. | + +#### `DeleteVolumeGroupSnapshot` + +A Controller Plugin MUST implement this RPC call if it has `CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT` capability. +This RPC will be called by the CO to delete a volume group snapshot. +This operation will delete a volume group snapshot as well as all individual snapshots that are part of this volume group snapshot. + +This operation MUST be idempotent. +If a group snapshot corresponding to the specified `group_snapshot_id` does not exist or the artifacts associated with the group snapshot do not exist anymore, the Plugin MUST reply `0 OK`. + +```protobuf +message DeleteVolumeGroupSnapshotRequest { + // The ID of the group snapshot to be deleted. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshot IDs that are part of this group snapshot. + // If SP does not need to rely on this field to delete the snapshots + // in the group, it SHOULD check this field and report an error + // if it has the ability to detect a mismatch. + // Some SPs require this list to delete the snapshots in the group. + // If SP needs to use this field to delete the snapshots in the + // group, it MUST report an error if it has the ability to detect + // a mismatch. + // This field is REQUIRED. + repeated string snapshot_ids = 2; + + // Secrets required by plugin to complete group snapshot deletion + // request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; +} + +message DeleteVolumeGroupSnapshotResponse { + // Intentionally empty. +} +``` + +##### DeleteVolumeGroupSnapshot Errors + +If the plugin is unable to complete the DeleteVolumeGroupSnapshot call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Snapshot list mismatch | 3 INVALID_ARGUMENT | Besides the general cases, this code SHOULD also be used to indicate when plugin supporting CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT detects a mismatch in the `snapshot_ids`. | If a mismatch is detected in the `snapshot_ids`, caller SHOULD use different `snapshot_ids`. | +| Volume group snapshot in use | 9 FAILED_PRECONDITION | Indicates that the volume group snapshot corresponding to the specified `group_snapshot_id` could not be deleted because it is in use by another resource. | Caller SHOULD ensure that there are no other resources using the volume group snapshot, and then retry with exponential back off. | + +#### `GetVolumeGroupSnapshot` + +This RPC MAY be called by the CO to fetch current information about a volume group snapshot. + +A Controller Plugin MUST implement this `GetVolumeGroupSnapshot` RPC call if it has `CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT` capability. + +`GetVolumeGroupSnapshotResponse` should contain current information of a volume group snapshot if it exists. +If the volume group snapshot does not exist any more, `GetVolumeGroupSnapshot` should return gRPC error code `NOT_FOUND`. + +```protobuf +message GetVolumeGroupSnapshotRequest { + // The ID of the group snapshot to fetch current group snapshot + // information for. + // This field is REQUIRED. + string group_snapshot_id = 1; + + // A list of snapshot IDs that are part of this group snapshot. + // If SP does not need to rely on this field to get the snapshots + // in the group, it SHOULD check this field and report an error + // if it has the ability to detect a mismatch. + // Some SPs require this list to get the snapshots in the group. + // If SP needs to use this field to get the snapshots in the + // group, it MUST report an error if it has the ability to detect + // a mismatch. + // This field is REQUIRED. + repeated string snapshot_ids = 2; + + // Secrets required by plugin to complete + // GetVolumeGroupSnapshot request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + // The secrets provided in this field SHOULD be the same for + // all group snapshot operations on the same group snapshot. + map secrets = 3 [(csi_secret) = true]; +} + +message GetVolumeGroupSnapshotResponse { + // This field is REQUIRED + VolumeGroupSnapshot group_snapshot = 1; +} +``` + +##### GetVolumeGroupSnapshot Errors + +If the plugin is unable to complete the GetVolumeGroupSnapshot call successfully, it MUST return a non-ok gRPC code in the gRPC status. +If the conditions defined below are encountered, the plugin MUST return the specified gRPC error code. +The CO MUST implement the specified error recovery behavior when it encounters the gRPC error code. + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Snapshot list mismatch | 3 INVALID_ARGUMENT | Besides the general cases, this code SHOULD also be used to indicate when plugin supporting CREATE_DELETE_GET_VOLUME_GROUP_SNAPSHOT detects a mismatch in the `snapshot_ids`. | If a mismatch is detected in the `snapshot_ids`, caller SHOULD use different `snapshot_ids`. | +| Volume group snapshot does not exist | 5 NOT_FOUND | Indicates that a volume group snapshot corresponding to the specified `group_snapshot_id` does not exist. | Caller MUST verify that the `group_snapshot_id` is correct and that the volume group snapshot is accessible and has not been deleted before retrying with exponential back off. | + +### Snapshot Metadata Service RPCs + +**ALPHA FEATURE** + +The Snapshot Metadata service is an optional service that is used to retrieve metadata on the allocated blocks of a single snapshot, or the changed blocks between a pair of snapshots of the same block volume. +Retrieval of the data blocks of a snapshot is not addressed by this service and it is assumed that existing mechanisms to read snapshot data will suffice. + +#### Metadata Format + +Block volume data ranges are specified by a sequence of `BlockMetadata` tuples. +Tuples in a sequence are in ascending order of `byte_offset`, with no overlap between adjacent tuples. + +```protobuf +// BlockMetadata specifies a data range. +message BlockMetadata { + // This is the zero based byte position in the volume or snapshot, + // measured from the start of the object. + // This field is REQUIRED. + int64 byte_offset = 1; + + // This is the size of the data range. + // size_bytes MUST be greater than zero. + // This field is REQUIRED. + int64 size_bytes = 2; +} +``` + +The `BlockMetadataType` enumerated type describes how the `size_bytes` fields of `BlockMetadata` tuples may vary in a sequence. +There are two prevalent styles in which data ranges are described: + +- The **FIXED_LENGTH** style requires a fixed value for the `size_bytes` field in all tuples in a given sequence. +- The **VARIABLE_LENGTH** style does not constrain the value of the `size_bytes` field in a sequence. + +The Snapshot Metadata service permits either style at the discretion of the plugin as long as the style does not change mid-stream in any given RPC. +The style is represented by the following data type: + +```protobuf +enum BlockMetadataType { + UNKNOWN = 0; + + // The FIXED_LENGTH value indicates that data ranges are + // returned in fixed size blocks. + FIXED_LENGTH = 1; + + // The VARIABLE_LENGTH value indicates that data ranges + // are returned in potentially variable sized extents. + VARIABLE_LENGTH = 2; +} +``` + +### Special considerations for processing a snapshot metadata gRPC stream + +The remote procedure calls of this service return snapshot metadata within a gRPC stream of response messages. +There are some important things for the SP and CO to consider here: + +- In normal operation an SP MUST terminate the stream only after ALL metadata is transmitted, using the language specific idiom for terminating a gRPC stream source. + This results in the CO receiving its language specific notification idiom for the end of a gRPC stream, which provides a **definitive indication that all available metadata has been received**. + +- If the SP encounters an error while recovering the metadata it MUST abort transmission of the stream with its language specific error idiom. + This results in the CO receiving the error in its language specific idiom, which will be different from the language specific idiom for the end of a gRPC stream. + +- It is possible that the gRPC stream gets interrupted for arbitrary reasons beyond the control of either the SP or the CO. + The SP will get an error when writing to the stream and MUST abort its transmission. + The CO will receive an error in its language specific idiom, which will be different from the language specific idiom for the end of a gRPC stream. + +- In all circumstances where the CO receives an error when reading from the gRPC stream it MAY attempt to **continue** the operation by re-sending its request message but with a `starting_offset` adjusted to the NEXT byte position beyond that of any metadata already received. + The SP MUST always ensure that the `starting_offset` requested be considered in the computation of the data range for the first message in the returned gRPC stream, though the data range of the first message is not required to actually include the `starting_offset` if there is no applicable data between the `starting_offset` and the start of the data range returned by the first message. + +#### `GetMetadataAllocated` + +The plugin must implement this RPC if it provides the SnapshotMetadata service. + +```protobuf +// The GetMetadataAllocatedRequest message is used to solicit metadata +// on the allocated blocks of a snapshot: i.e. this identifies the +// data ranges that have valid data as they were the target of some +// previous write operation on the volume. +message GetMetadataAllocatedRequest { + // This is the identifier of the snapshot. + // This field is REQUIRED. + string snapshot_id = 1; + + // This indicates the zero based starting byte position in the volume + // snapshot from which the result should be computed. + // It is intended to be used to continue a previously interrupted + // call. + // The CO SHOULD specify this value to be the offset of the byte + // position immediately after the last byte of the last data range + // received, if continuing an interrupted operation, or zero if not. + // The SP MUST ensure that the returned response stream does not + // contain BlockMetadata tuples that end before the requested + // starting_offset: i.e. if S is the requested starting_offset, and + // B0 is block_metadata[0] of the first message in the response + // stream, then (S < B0.byte_offset + B0.size_bytes) must be true. + // This field is REQUIRED. + int64 starting_offset = 2; + + // This is an optional parameter, and if non-zero it specifies the + // maximum number of tuples to be returned in each + // GetMetadataAllocatedResponse message returned by the RPC stream. + // The plugin will determine an appropriate value if 0, and is + // always free to send less than the requested value. + // This field is OPTIONAL. + int32 max_results = 3; + + // Secrets required by plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 4 [(csi_secret) = true]; +} + +// GetMetadataAllocatedResponse messages are returned in a gRPC stream. +// Cumulatively, they provide information on the allocated data +// ranges in the snapshot. +message GetMetadataAllocatedResponse { + // This specifies the style used in the BlockMetadata sequence. + // This value must be the same in all such messages returned by + // the stream. + // If block_metadata_type is FIXED_LENGTH, then the size_bytes field + // of each message in the block_metadata list MUST be constant. + // This field is REQUIRED. + BlockMetadataType block_metadata_type = 1; + + // This returns the capacity of the underlying volume in bytes. + // This value must be the same in all such messages returned by + // the stream. + // This field is REQUIRED. + int64 volume_capacity_bytes = 2; + + // This is a list of data range tuples. + // If the value of max_results in the GetMetadataAllocatedRequest + // message is greater than zero, then the number of entries in this + // list MUST be less than or equal to that value. + // The SP MUST respect the value of starting_offset in the request. + // The byte_offset fields of adjacent BlockMetadata messages + // MUST be strictly increasing and messages MUST NOT overlap: + // i.e. for any two BlockMetadata messages, A and B, if A is returned + // before B, then (A.byte_offset + A.size_bytes <= B.byte_offset) + // MUST be true. + // This MUST also be true if A and B are from block_metadata lists in + // different GetMetadataAllocatedResponse messages in the gRPC stream. + // This field is OPTIONAL. + repeated BlockMetadata block_metadata = 3; +} +``` + +##### GetMetadataAllocated Errors +If the plugin is unable to complete the `GetMetadataAllocated` call successfully it must return a non-OK gRPC code in the gRPC status. + +The following conditions are well defined: + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Missing or otherwise invalid argument | 3 INVALID_ARGUMENT | Indicates that a required argument field was not specified or an argument value is invalid. | The caller should correct the error and resubmit the call. | +| Invalid `snapshot_id` | 5 NOT_FOUND | Indicates that the snapshot specified was not found. | The caller should re-check that this object exists. | +| Invalid `starting_offset` | 11 OUT_OF_RANGE | The starting offset is negative or exceeds the volume size. | The caller should specify a valid offset. | + +#### `GetMetadataDelta` + +The plugin must implement this RPC if it provides the SnapshotMetadata service. + +```protobuf +// The GetMetadataDeltaRequest message is used to solicit metadata on +// the data ranges that have changed between two snapshots. +message GetMetadataDeltaRequest { + // This is the identifier of the snapshot against which changes + // are to be computed. + // This field is REQUIRED. + string base_snapshot_id = 1; + + // This is the identifier of a second snapshot in the same volume, + // created after the base snapshot. + // This field is REQUIRED. + string target_snapshot_id = 2; + + // This indicates the zero based starting byte position in the volume + // snapshot from which the result should be computed. + // It is intended to be used to continue a previously interrupted + // call. + // The CO SHOULD specify this value to be the offset of the byte + // position immediately after the last byte of the last data range + // received, if continuing an interrupted operation, or zero if not. + // The SP MUST ensure that the returned response stream does not + // contain BlockMetadata tuples that end before the requested + // starting_offset: i.e. if S is the requested starting_offset, and + // B0 is block_metadata[0] of the first message in the response + // stream, then (S < B0.byte_offset + B0.size_bytes) must be true. + // This field is REQUIRED. + int64 starting_offset = 3; + + // This is an optional parameter, and if non-zero it specifies the + // maximum number of tuples to be returned in each + // GetMetadataDeltaResponse message returned by the RPC stream. + // The plugin will determine an appropriate value if 0, and is + // always free to send less than the requested value. + // This field is OPTIONAL. + int32 max_results = 4; + + // Secrets required by plugin to complete the request. + // This field is OPTIONAL. Refer to the `Secrets Requirements` + // section on how to use this field. + map secrets = 5 [(csi_secret) = true]; +} + +// GetMetadataDeltaResponse messages are returned in a gRPC stream. +// Cumulatively, they provide information on the data ranges that +// have changed between the base and target snapshots specified +// in the GetMetadataDeltaRequest message. +message GetMetadataDeltaResponse { + // This specifies the style used in the BlockMetadata sequence. + // This value must be the same in all such messages returned by + // the stream. + // If block_metadata_type is FIXED_LENGTH, then the size_bytes field + // of each message in the block_metadata list MUST be constant. + // This field is REQUIRED. + BlockMetadataType block_metadata_type = 1; + + // This returns the capacity of the underlying volume in bytes. + // This value must be the same in all such messages returned by + // the stream. + // This field is REQUIRED. + int64 volume_capacity_bytes = 2; + + // This is a list of data range tuples. + // If the value of max_results in the GetMetadataDeltaRequest message + // is greater than zero, then the number of entries in this list MUST + // be less than or equal to that value. + // The SP MUST respect the value of starting_offset in the request. + // The byte_offset fields of adjacent BlockMetadata messages + // MUST be strictly increasing and messages MUST NOT overlap: + // i.e. for any two BlockMetadata messages, A and B, if A is returned + // before B, then (A.byte_offset + A.size_bytes <= B.byte_offset) + // MUST be true. + // This MUST also be true if A and B are from block_metadata lists in + // different GetMetadataDeltaResponse messages in the gRPC stream. + // This field is OPTIONAL. + repeated BlockMetadata block_metadata = 3; +} +``` + +##### GetMetadataDelta Errors +If the plugin is unable to complete the `GetMetadataDelta` call successfully it must return a non-OK gRPC code in the gRPC status. + +The following conditions are well defined: + +| Condition | gRPC Code | Description | Recovery Behavior | +|-----------|-----------|-------------|-------------------| +| Missing or otherwise invalid argument | 3 INVALID_ARGUMENT | Indicates that a required argument field was not specified or an argument value is invalid. | The caller should correct the error and resubmit the call. | +| Invalid `base_snapshot_id` or `target_snapshot_id` | 5 NOT_FOUND | Indicates that the snapshots specified were not found. | The caller should re-check that these objects exist. | +| Changed block tracking is not enabled | 9 FAILED_PRECONDITION | Changed block tracking has not been enabled in the underlying storage subsystem. | The caller should perform a full backup instead. The operation would proceed if changed block tracking was enabled in the storage subsystem, but how this is to be accomplished is beyond the scope of this specification. | +| Invalid `starting_offset` | 11 OUT_OF_RANGE | The starting offset is negative or exceeds the volume size. | The caller should specify a valid offset. | + +## Protocol + +### Connectivity + +* A CO SHALL communicate with a Plugin using gRPC to access the `Identity`, and (optionally) the `Controller` and `Node` services. + * proto3 SHOULD be used with gRPC, as per the [official recommendations](http://www.grpc.io/docs/guides/#protocol-buffer-versions). + * All Plugins SHALL implement the REQUIRED Identity service RPCs. + Support for OPTIONAL RPCs is reported by the `ControllerGetCapabilities` and `NodeGetCapabilities` RPC calls. +* The CO SHALL provide the listen-address for the Plugin by way of the `CSI_ENDPOINT` environment variable. + Plugin components SHALL create, bind, and listen for RPCs on the specified listen address. + * Only UNIX Domain Sockets MAY be used as endpoints. + This will likely change in a future version of this specification to support non-UNIX platforms. +* All supported RPC services MUST be available at the listen address of the Plugin. + +### Security + +* The CO operator and Plugin Supervisor SHOULD take steps to ensure that any and all communication between the CO and Plugin Service are secured according to best practices. +* Communication between a CO and a Plugin SHALL be transported over UNIX Domain Sockets. + * gRPC is compatible with UNIX Domain Sockets; it is the responsibility of the CO operator and Plugin Supervisor to properly secure access to the Domain Socket using OS filesystem ACLs and/or other OS-specific security context tooling. + * SP’s supplying stand-alone Plugin controller appliances, or other remote components that are incompatible with UNIX Domain Sockets MUST provide a software component that proxies communication between a UNIX Domain Socket and the remote component(s). + Proxy components transporting communication over IP networks SHALL be responsible for securing communications over such networks. +* Both the CO and Plugin SHOULD avoid accidental leakage of sensitive information (such as redacting such information from log files). + +### Debugging + +* Debugging and tracing are supported by external, CSI-independent additions and extensions to gRPC APIs, such as [OpenTracing](https://github.com/grpc-ecosystem/grpc-opentracing). + +## Configuration and Operation + +### General Configuration + +* The `CSI_ENDPOINT` environment variable SHALL be supplied to the Plugin by the Plugin Supervisor. +* An operator SHALL configure the CO to connect to the Plugin via the listen address identified by `CSI_ENDPOINT` variable. +* With exception to sensitive data, Plugin configuration SHOULD be specified by environment variables, whenever possible, instead of by command line flags or bind-mounted/injected files. + + +#### Plugin Bootstrap Example + +* Supervisor -> Plugin: `CSI_ENDPOINT=unix:///path/to/unix/domain/socket.sock`. +* Operator -> CO: use plugin at endpoint `unix:///path/to/unix/domain/socket.sock`. +* CO: monitor `/path/to/unix/domain/socket.sock`. +* Plugin: read `CSI_ENDPOINT`, create UNIX socket at specified path, bind and listen. +* CO: observe that socket now exists, establish connection. +* CO: invoke `GetPluginCapabilities`. + +#### Filesystem + +* Plugins SHALL NOT specify requirements that include or otherwise reference directories and/or files on the root filesystem of the CO. +* Plugins SHALL NOT create additional files or directories adjacent to the UNIX socket specified by `CSI_ENDPOINT`; violations of this requirement constitute "abuse". + * The Plugin Supervisor is the ultimate authority of the directory in which the UNIX socket endpoint is created and MAY enforce policies to prevent and/or mitigate abuse of the directory by Plugins. + +### Supervised Lifecycle Management + +* For Plugins packaged in software form: + * Plugin Packages SHOULD use a well-documented container image format (e.g., Docker, OCI). + * The chosen package image format MAY expose configurable Plugin properties as environment variables, unless otherwise indicated in the section below. + Variables so exposed SHOULD be assigned default values in the image manifest. + * A Plugin Supervisor MAY programmatically evaluate or otherwise scan a Plugin Package’s image manifest in order to discover configurable environment variables. + * A Plugin SHALL NOT assume that an operator or Plugin Supervisor will scan an image manifest for environment variables. + +#### Environment Variables + +* Variables defined by this specification SHALL be identifiable by their `CSI_` name prefix. +* Configuration properties not defined by the CSI specification SHALL NOT use the same `CSI_` name prefix; this prefix is reserved for common configuration properties defined by the CSI specification. +* The Plugin Supervisor SHOULD supply all RECOMMENDED CSI environment variables to a Plugin. +* The Plugin Supervisor SHALL supply all REQUIRED CSI environment variables to a Plugin. + +##### `CSI_ENDPOINT` + +Network endpoint at which a Plugin SHALL host CSI RPC services. The general format is: + + {scheme}://{authority}{endpoint} + +The following address types SHALL be supported by Plugins: + + unix:///path/to/unix/socket.sock + +Note: All UNIX endpoints SHALL end with `.sock`. See [gRPC Name Resolution](https://github.com/grpc/grpc/blob/master/doc/naming.md). + +This variable is REQUIRED. + +#### Operational Recommendations + +The Plugin Supervisor expects that a Plugin SHALL act as a long-running service vs. an on-demand, CLI-driven process. + +Supervised plugins MAY be isolated and/or resource-bounded. + +##### Logging + +* Plugins SHOULD generate log messages to ONLY standard output and/or standard error. + * In this case the Plugin Supervisor SHALL assume responsibility for all log lifecycle management. +* Plugin implementations that deviate from the above recommendation SHALL clearly and unambiguously document the following: + * Logging configuration flags and/or variables, including working sample configurations. + * Default log destination(s) (where do the logs go if no configuration is specified?) + * Log lifecycle management ownership and related guidance (size limits, rate limits, rolling, archiving, expunging, etc.) applicable to the logging mechanism embedded within the Plugin. +* Plugins SHOULD NOT write potentially sensitive data to logs (e.g. secrets). + +##### Available Services + +* Plugin Packages MAY support all or a subset of CSI services; service combinations MAY be configurable at runtime by the Plugin Supervisor. + * A plugin MUST know the "mode" in which it is operating (e.g. node, controller, or both). + * This specification does not dictate the mechanism by which mode of operation MUST be discovered, and instead places that burden upon the SP. +* Misconfigured plugin software SHOULD fail-fast with an OS-appropriate error code. + +##### Linux Capabilities + +* Plugin Supervisor SHALL guarantee that plugins will have `CAP_SYS_ADMIN` capability on Linux when running on Nodes. +* Plugins SHOULD clearly document any additionally required capabilities and/or security context. + +##### Namespaces + +* A Plugin SHOULD NOT assume that it is in the same [Linux namespaces](https://en.wikipedia.org/wiki/Linux_namespaces) as the Plugin Supervisor. + The CO MUST clearly document the [mount propagation](https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt) requirements for Node Plugins and the Plugin Supervisor SHALL satisfy the CO’s requirements. + +##### Cgroups Isolation + +* A Plugin MAY be constrained by cgroups. +* An operator or Plugin Supervisor MAY configure the devices cgroups subsystem to ensure that a Plugin MAY access requisite devices. +* A Plugin Supervisor MAY define resource limits for a Plugin. + +##### Resource Requirements + +* SPs SHOULD unambiguously document all of a Plugin’s resource requirements. diff --git a/driver/controller.go b/driver/controller.go index 8e220b95..bfacbb74 100644 --- a/driver/controller.go +++ b/driver/controller.go @@ -23,12 +23,13 @@ import ( "fmt" "net/http" "regexp" + "slices" "sort" "strconv" "strings" "time" - "github.com/cloudscale-ch/cloudscale-go-sdk/v7" + "github.com/cloudscale-ch/cloudscale-go-sdk/v10" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" @@ -46,17 +47,17 @@ const ( ) const ( - // allowed size increments for SSDs + // SSDStepSizeGB is the allowed size increments for SSDs. SSDStepSizeGB = 1 - // allowed size increments for bulk disks + // BulkStepSizeGB is the allowed size increments for bulk disks. BulkStepSizeGB = 100 // PublishInfoVolumeName is used to pass the volume name from // `ControllerPublishVolume` to `NodeStageVolume or `NodePublishVolume` PublishInfoVolumeName = DriverName + "/volume-name" - // Storage type of the volume, must be either "ssd" or "bulk" + // StorageTypeAttribute is the storage type of the volume, must be either "ssd" or "bulk" StorageTypeAttribute = DriverName + "/volume-type" ) @@ -228,8 +229,7 @@ func (d *Driver) createVolumeFromSnapshot(ctx context.Context, req *csi.CreateVo // Verify snapshot exists and get its properties, must return NotFound when snapshot does not exist. snapshot, err := d.cloudscaleClient.VolumeSnapshots.Get(ctx, sourceSnapshotID) if err != nil { - var errorResponse *cloudscale.ErrorResponse - if errors.As(err, &errorResponse) { + if errorResponse, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok { if errorResponse.StatusCode == http.StatusNotFound { return nil, status.Errorf(codes.NotFound, "source snapshot %s not found", sourceSnapshotID) } @@ -237,9 +237,20 @@ func (d *Driver) createVolumeFromSnapshot(ctx context.Context, req *csi.CreateVo return nil, status.Errorf(codes.Internal, "failed to get source snapshot: %v", err) } + // Fetch source volume to determine storage type + sourceVolume, err := d.cloudscaleClient.Volumes.Get(ctx, snapshot.SourceVolume.UUID) + if err != nil { + if errorResponse, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok { + if errorResponse.StatusCode == http.StatusNotFound { + return nil, status.Errorf(codes.NotFound, "source volume %s not found", sourceSnapshotID) + } + } + return nil, status.Errorf(codes.Internal, "failed to get source volume for snapshot: %v", err) + } + ll = ll.WithFields(logrus.Fields{ "snapshot_size_gb": snapshot.SizeGB, - "snapshot_volume_type": snapshot.SourceVolume.Type, + "snapshot_volume_type": sourceVolume.Type, "snapshot_zone": snapshot.Zone, }) @@ -248,7 +259,7 @@ func (d *Driver) createVolumeFromSnapshot(ctx context.Context, req *csi.CreateVo // targetSizeGB is used to track whether we must expand to meet the requested capacity. targetSizeGB := snapshot.SizeGB - storageType := snapshot.SourceVolume.Type + storageType := sourceVolume.Type if req.CapacityRange != nil { calculatedSize, err := calculateStorageGB(req.CapacityRange, storageType) @@ -411,8 +422,7 @@ func (d *Driver) DeleteVolume(ctx context.Context, req *csi.DeleteVolumeRequest) err := d.cloudscaleClient.Volumes.Delete(ctx, req.VolumeId) if err != nil { - var errorResponse *cloudscale.ErrorResponse - if errors.As(err, &errorResponse) { + if errorResponse, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok { if errorResponse.StatusCode == http.StatusNotFound { // To make it idempotent, the volume might already have been // deleted, so a 404 is ok. @@ -487,13 +497,7 @@ func (d *Driver) ControllerPublishVolume(ctx context.Context, req *csi.Controlle } if volume.ServerUUIDs != nil && len(*volume.ServerUUIDs) > 0 { - alreadyAttachedToRequestedNode := false - for _, serverUUID := range *volume.ServerUUIDs { - if serverUUID == req.NodeId { - alreadyAttachedToRequestedNode = true - break - } - } + alreadyAttachedToRequestedNode := slices.Contains(*volume.ServerUUIDs, req.NodeId) if alreadyAttachedToRequestedNode { ll.Info("volume is already attached to the requested node") @@ -522,7 +526,6 @@ func (d *Driver) ControllerPublishVolume(ctx context.Context, req *csi.Controlle if maxVolumesPerServerErrorMessageRe.MatchString(err.Error()) { return nil, status.Error(codes.ResourceExhausted, err.Error()) } - return nil, reraiseNotFound(err, ll, "attaching volume") } @@ -558,8 +561,7 @@ func (d *Driver) ControllerUnpublishVolume(ctx context.Context, req *csi.Control // check if volume exist before trying to detach it volume, err := d.cloudscaleClient.Volumes.Get(ctx, req.VolumeId) if err != nil { - var errorResponse *cloudscale.ErrorResponse - if errors.As(err, &errorResponse) { + if errorResponse, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok { if errorResponse.StatusCode == http.StatusNotFound { ll.Info("assuming volume is detached because it does not exist") return &csi.ControllerUnpublishVolumeResponse{}, nil @@ -694,11 +696,11 @@ func (d *Driver) GetCapacity(ctx context.Context, req *csi.GetCapacityRequest) ( // ControllerGetCapabilities returns the capabilities of the controller service. func (d *Driver) ControllerGetCapabilities(ctx context.Context, req *csi.ControllerGetCapabilitiesRequest) (*csi.ControllerGetCapabilitiesResponse, error) { - newCap := func(cap csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability { + newCap := func(rpcType csi.ControllerServiceCapability_RPC_Type) *csi.ControllerServiceCapability { return &csi.ControllerServiceCapability{ Type: &csi.ControllerServiceCapability_Rpc{ Rpc: &csi.ControllerServiceCapability_RPC{ - Type: cap, + Type: rpcType, }, }, } @@ -771,8 +773,7 @@ func (d *Driver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequ ll.Info("find existing volume snapshots with same name") allSnapshots, err := d.cloudscaleClient.VolumeSnapshots.List(ctx, cloudscale.WithNameFilter(req.Name)) if err != nil { - var errorResponse *cloudscale.ErrorResponse - if errors.As(err, &errorResponse) { + if errorResponse, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok { ll.WithFields(logrus.Fields{ "status_code": errorResponse.StatusCode, "error": err, @@ -816,8 +817,7 @@ func (d *Driver) CreateSnapshot(ctx context.Context, req *csi.CreateSnapshotRequ ll.WithField("volume_snapshot_create_request", volumeSnapshotCreateRequest).Info("creating volume snapshot") snapshot, err := d.cloudscaleClient.VolumeSnapshots.Create(ctx, volumeSnapshotCreateRequest) if err != nil { - var errorResponse *cloudscale.ErrorResponse - if errors.As(err, &errorResponse) { + if errorResponse, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok { ll.WithFields(logrus.Fields{ "status_code": errorResponse.StatusCode, "error": err, @@ -861,8 +861,7 @@ func (d *Driver) DeleteSnapshot(ctx context.Context, req *csi.DeleteSnapshotRequ // Cloudscale handles the deletion asynchronously. The operation is idempotent. err := d.cloudscaleClient.VolumeSnapshots.Delete(ctx, req.SnapshotId) if err != nil { - var errorResponse *cloudscale.ErrorResponse - if errors.As(err, &errorResponse) { + if errorResponse, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok { if errorResponse.StatusCode == http.StatusNotFound { // To make it idempotent, the snapshot might already have been // deleted, so a 404 is ok. @@ -1039,8 +1038,7 @@ func (d *Driver) ControllerExpandVolume(ctx context.Context, req *csi.Controller nodeExpansionRequired := true if req.GetVolumeCapability() != nil { - switch req.GetVolumeCapability().GetAccessType().(type) { - case *csi.VolumeCapability_Block: + if _, ok := req.GetVolumeCapability().GetAccessType().(*csi.VolumeCapability_Block); ok { log.Info("node expansion is not required for block volumes") nodeExpansionRequired = false } @@ -1094,13 +1092,13 @@ func calculateStorageGB(capRange *csi.CapacityRange, storageType string) (int, e steps := requiredBytes / GB / int64(sizeIncrements) if steps*GB*int64(sizeIncrements) < requiredBytes { - steps += 1 + steps++ } sizeGB := steps * int64(sizeIncrements) - if limitSet && limitBytes < (int64(sizeGB)*GB) { - return 0, fmt.Errorf("for required (%v) limit (%v) must be at least %v for type '%s'", formatBytes(requiredBytes), formatBytes(limitBytes), formatBytes(int64(sizeGB)*GB), storageType) + if limitSet && limitBytes < (sizeGB*GB) { + return 0, fmt.Errorf("for required (%v) limit (%v) must be at least %v for type '%s'", formatBytes(requiredBytes), formatBytes(limitBytes), formatBytes(sizeGB*GB), storageType) } return int(sizeGB), nil } @@ -1111,16 +1109,16 @@ func formatBytes(inputBytes int64) string { switch { case inputBytes >= TB: - output = output / TB + output /= TB unit = "Ti" case inputBytes >= GB: - output = output / GB + output /= GB unit = "Gi" case inputBytes >= MB: - output = output / MB + output /= MB unit = "Mi" case inputBytes >= KB: - output = output / KB + output /= KB unit = "Ki" case inputBytes == 0: return "0" @@ -1166,8 +1164,7 @@ func validateLuksCapabilities(caps []*csi.VolumeCapability) []string { } func reraiseNotFound(err error, log *logrus.Entry, operation string) error { - var errorResponse *cloudscale.ErrorResponse - if errors.As(err, &errorResponse) { + if errorResponse, ok := errors.AsType[*cloudscale.ErrorResponse](err); ok { lt := log.WithFields(logrus.Fields{ "error": err, "errorResponse": errorResponse, @@ -1175,10 +1172,10 @@ func reraiseNotFound(err error, log *logrus.Entry, operation string) error { if errorResponse.StatusCode == http.StatusNotFound { lt.Warnf("%q: Server or volume not found", operation) return status.Error(codes.NotFound, err.Error()) - } else { - lt.Warnf("%q: operation failed", operation) - return status.Errorf(codes.Aborted, "%s: Request failed", operation) } + + lt.Warnf("%q: operation failed", operation) + return status.Errorf(codes.Aborted, "%s: Request failed", operation) } log.Warnf("%q: random error", operation) return status.Errorf(codes.Aborted, "%s: Random error", operation) diff --git a/driver/controller_test.go b/driver/controller_test.go index 60c1157a..6ff4414e 100644 --- a/driver/controller_test.go +++ b/driver/controller_test.go @@ -72,21 +72,18 @@ func calcStorageGbBulk(reqGb int, limitGb int) (int, error) { if reqGb == -1 { if limitGb == -1 { return calculateStorageGB(&csi.CapacityRange{}, "bulk") - } else { - return calculateStorageGB(&csi.CapacityRange{ - LimitBytes: int64(limitGb * GB), - }, "bulk") - } - } else { - if limitGb == -1 { - return calculateStorageGB(&csi.CapacityRange{ - RequiredBytes: int64(reqGb * GB), - }, "bulk") - } else { - return calculateStorageGB(&csi.CapacityRange{ - RequiredBytes: int64(reqGb * GB), - LimitBytes: int64(limitGb * GB), - }, "bulk") } + return calculateStorageGB(&csi.CapacityRange{ + LimitBytes: int64(limitGb * GB), + }, "bulk") + } + if limitGb == -1 { + return calculateStorageGB(&csi.CapacityRange{ + RequiredBytes: int64(reqGb * GB), + }, "bulk") } + return calculateStorageGB(&csi.CapacityRange{ + RequiredBytes: int64(reqGb * GB), + LimitBytes: int64(limitGb * GB), + }, "bulk") } diff --git a/driver/driver.go b/driver/driver.go index 683befe6..258e49c7 100644 --- a/driver/driver.go +++ b/driver/driver.go @@ -27,7 +27,7 @@ import ( "path/filepath" "sync" - "github.com/cloudscale-ch/cloudscale-go-sdk/v7" + "github.com/cloudscale-ch/cloudscale-go-sdk/v10" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/sirupsen/logrus" "golang.org/x/oauth2" @@ -57,7 +57,7 @@ type Driver struct { csi.UnimplementedNodeServer endpoint string - serverId string + serverID string zone string srv *grpc.Server @@ -85,7 +85,7 @@ func NewDriver(ep, token, urlstr string, logLevel logrus.Level) (*Driver, error) oauthClient := oauth2.NewClient(context.Background(), tokenSource) metadataClient := cloudscale.NewMetadataClient(nil) - metadata, err := metadataClient.GetMetadata() + metadata, err := metadataClient.GetMetadata(context.Background()) if err != nil { return nil, fmt.Errorf("couldn't get metadata: %s", err) } @@ -93,7 +93,7 @@ func NewDriver(ep, token, urlstr string, logLevel logrus.Level) (*Driver, error) // We don't have any other information than the availability zone. Just use // it as the zone for now. zone := metadata.AvailabilityZone - serverId := metadata.Meta.CloudscaleUUID + serverID := metadata.Meta.CloudscaleUUID cloudscaleClient := cloudscale.NewClient(oauthClient) baseURL, err := url.Parse(urlstr) @@ -106,13 +106,13 @@ func NewDriver(ep, token, urlstr string, logLevel logrus.Level) (*Driver, error) logger.SetLevel(logLevel) log := logger.WithFields(logrus.Fields{ "zone": zone, - "node_id": serverId, + "node_id": serverID, "version": version, }) return &Driver{ endpoint: ep, - serverId: serverId, + serverID: serverID, zone: zone, cloudscaleClient: cloudscaleClient, mounter: newMounter(), @@ -122,7 +122,7 @@ func NewDriver(ep, token, urlstr string, logLevel logrus.Level) (*Driver, error) } // Run starts the CSI plugin by communication over the given endpoint -func (d *Driver) Run() error { +func (d *Driver) Run(ctx context.Context) error { u, err := url.Parse(d.endpoint) if err != nil { return fmt.Errorf("unable to parse address: %q", err) @@ -136,23 +136,24 @@ func (d *Driver) Run() error { // CSI plugins talk only over UNIX sockets currently if u.Scheme != "unix" { return fmt.Errorf("currently only unix domain sockets are supported, have: %s", u.Scheme) - } else { - // remove the socket if it's already there. This can happen if we - // deploy a new version and the socket was created from the old running - // plugin. - d.log.WithField("socket", addr).Info("removing socket") - if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to remove unix domain socket file %s, error: %s", addr, err) - } } - listener, err := net.Listen(u.Scheme, addr) + // remove the socket if it's already there. This can happen if we + // deploy a new version and the socket was created from the old running + // plugin. + d.log.WithField("socket", addr).Info("removing socket") + if err := os.Remove(addr); err != nil && !os.IsNotExist(err) { + return fmt.Errorf("failed to remove unix domain socket file %s, error: %s", addr, err) + } + + var lc net.ListenConfig + listener, err := lc.Listen(ctx, u.Scheme, addr) if err != nil { return fmt.Errorf("failed to listen: %v", err) } // log response errors for better observability - errHandler := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + errHandler := func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { resp, err := handler(ctx, req) if err != nil { d.log.WithError(err).WithField("method", info.FullMethod).Error("method failed") @@ -165,6 +166,11 @@ func (d *Driver) Run() error { csi.RegisterControllerServer(d.srv, d) csi.RegisterNodeServer(d.srv, d) + go func() { + <-ctx.Done() + d.Stop() + }() + d.ready = true // we're now ready to go! d.log.WithField("addr", addr).Info("server started") return d.srv.Serve(listener) @@ -177,15 +183,15 @@ func (d *Driver) Stop() { d.readyMu.Unlock() d.log.Info("server stopped") - d.srv.Stop() + d.srv.GracefulStop() } +// GetVersion returns the current release version, as inserted at build time. +// // When building any packages that import version, pass the build/install cmd // ldflags like so: // // go build -ldflags "-X github.com/cloudscale-ch/csi-cloudscale/driver.version=0.0.1" -// -// GetVersion returns the current release version, as inserted at build time. func GetVersion() string { return version } diff --git a/driver/driver_test.go b/driver/driver_test.go index f2c9133d..d965f2d1 100644 --- a/driver/driver_test.go +++ b/driver/driver_test.go @@ -31,7 +31,7 @@ import ( "time" "github.com/cenkalti/backoff/v5" - "github.com/cloudscale-ch/cloudscale-go-sdk/v7" + "github.com/cloudscale-ch/cloudscale-go-sdk/v10" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/google/uuid" "github.com/kubernetes-csi/csi-test/v5/pkg/sanity" @@ -49,7 +49,7 @@ const ( type idGenerator struct{} -var DefaultZone = cloudscale.Zone{Slug: "dev1"} +var DefaultZone = cloudscale.ZoneStub{Slug: "dev1"} func TestDriverSuite(t *testing.T) { socket := "/tmp/csi.sock" @@ -64,7 +64,7 @@ func TestDriverSuite(t *testing.T) { defer driver.Stop() go func() { - if err := driver.Run(); err != nil { + if err := driver.Run(t.Context()); err != nil { panic(err) } }() @@ -236,7 +236,7 @@ func (f *fakeMounter) HasRequiredSize(log *logrus.Entry, path string, requiredSi return true, nil } -func (f *fakeMounter) FinalizeVolumeAttachmentAndFindPath(logger *logrus.Entry, target string) (string, error) { +func (f *fakeMounter) FinalizeVolumeAttachmentAndFindPath(ctx context.Context, logger *logrus.Entry, target string) (string, error) { path := "SomePath" return path, nil } @@ -263,7 +263,12 @@ func (f *FakeVolumeServiceOperations) Create(ctx context.Context, createRequest return nil, err } vol.SizeGB = snap.SizeGB - vol.Type = snap.SourceVolume.Type + // Look up the source volume to get its type. + sourceVol, err := f.fakeClient.Volumes.Get(ctx, snap.SourceVolume.UUID) + if err != nil { + return nil, fmt.Errorf("failed to get source volume: %s", err) + } + vol.Type = sourceVol.Type vol.Zone = snap.Zone } else { vol.Zone = DefaultZone @@ -320,7 +325,7 @@ func extractParams(modifiers []cloudscale.ListRequestModifier) url.Values { // undoing the cloudscale.WithNameFilter(volumeName) magic modifierFunc := modifiers[0] - req, _ := http.NewRequest("GET", "http://example.com", nil) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://example.com", nil) modifierFunc(req) params, err := url.ParseQuery(req.URL.RawQuery) if err != nil { @@ -461,7 +466,6 @@ type FakeVolumeSnapshotServiceOperations struct { } func (f FakeVolumeSnapshotServiceOperations) Create(ctx context.Context, createRequest *cloudscale.VolumeSnapshotCreateRequest) (*cloudscale.VolumeSnapshot, error) { - vol, err := f.fakeClient.Volumes.Get(ctx, createRequest.SourceVolume) if err != nil { return nil, err @@ -490,9 +494,8 @@ func (f FakeVolumeSnapshotServiceOperations) Create(ctx context.Context, createR SizeGB: vol.SizeGB, CreatedAt: time.Now().UTC().Format(time.RFC3339), Status: "available", - SourceVolume: cloudscale.VolumeStub{ + SourceVolume: cloudscale.SourceVolumeStub{ UUID: createRequest.SourceVolume, - Type: vol.Type, }, } snap.Zone = vol.Zone @@ -505,7 +508,6 @@ func (f *FakeVolumeSnapshotServiceOperations) Get( ctx context.Context, snapshotID string, ) (*cloudscale.VolumeSnapshot, error) { - snap, ok := f.snapshots[snapshotID] if !ok { return nil, generateNotFoundError() @@ -567,10 +569,12 @@ func generateNotFoundError() *cloudscale.ErrorResponse { } } +//nolint:unparam // keep `n` parameter for potential future use func randString(n int) string { const letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" b := make([]byte, n) for i := range b { + //nolint:gosec // G404: weak random generator acceptable in test code b[i] = letterBytes[rand.Intn(len(letterBytes))] } return string(b) @@ -602,11 +606,11 @@ type FakeBlockingMounter struct { // FinalizeVolumeAttachmentAndFindPath blocks until signaled, allowing tests to control // the order of execution for concurrency testing. -func (f *FakeBlockingMounter) FinalizeVolumeAttachmentAndFindPath(logger *logrus.Entry, volumeID string) (string, error) { +func (f *FakeBlockingMounter) FinalizeVolumeAttachmentAndFindPath(ctx context.Context, logger *logrus.Entry, volumeID string) (string, error) { executeOp := make(chan struct{}) f.ReadyToExecute <- executeOp <-executeOp - return f.fakeMounter.FinalizeVolumeAttachmentAndFindPath(logger, volumeID) + return f.fakeMounter.FinalizeVolumeAttachmentAndFindPath(ctx, logger, volumeID) } // NewFakeBlockingMounter creates a new FakeBlockingMounter with the given channel. @@ -631,8 +635,7 @@ func TestNodeStageVolume_ConcurrentSameVolume(t *testing.T) { driver := initBlockingDriver(t, readyToExecute) // Create the volume in the fake client first - ctx := t.Context() - vol, err := driver.cloudscaleClient.Volumes.Create(ctx, &cloudscale.VolumeCreateRequest{ + vol, err := driver.cloudscaleClient.Volumes.Create(t.Context(), &cloudscale.VolumeCreateRequest{ Name: "test-volume", SizeGB: 10, Type: "ssd", @@ -664,7 +667,7 @@ func TestNodeStageVolume_ConcurrentSameVolume(t *testing.T) { runNodeStage := func(req *csi.NodeStageVolumeRequest) <-chan error { response := make(chan error, 1) go func() { - _, err := driver.NodeStageVolume(context.Background(), req) + _, err := driver.NodeStageVolume(t.Context(), req) response <- err }() return response @@ -706,10 +709,8 @@ func TestNodeStageVolume_ConcurrentDifferentVolumes(t *testing.T) { readyToExecute := make(chan chan struct{}, 2) driver := initBlockingDriver(t, readyToExecute) - ctx := t.Context() - // Create two different volumes - vol1, err := driver.cloudscaleClient.Volumes.Create(ctx, &cloudscale.VolumeCreateRequest{ + vol1, err := driver.cloudscaleClient.Volumes.Create(t.Context(), &cloudscale.VolumeCreateRequest{ Name: "test-volume-1", SizeGB: 10, Type: "ssd", @@ -718,7 +719,7 @@ func TestNodeStageVolume_ConcurrentDifferentVolumes(t *testing.T) { t.Fatalf("Failed to create volume 1: %v", err) } - vol2, err := driver.cloudscaleClient.Volumes.Create(ctx, &cloudscale.VolumeCreateRequest{ + vol2, err := driver.cloudscaleClient.Volumes.Create(t.Context(), &cloudscale.VolumeCreateRequest{ Name: "test-volume-2", SizeGB: 10, Type: "ssd", @@ -751,7 +752,7 @@ func TestNodeStageVolume_ConcurrentDifferentVolumes(t *testing.T) { runNodeStage := func(req *csi.NodeStageVolumeRequest) <-chan error { response := make(chan error, 1) go func() { - _, err := driver.NodeStageVolume(context.Background(), req) + _, err := driver.NodeStageVolume(t.Context(), req) response <- err }() return response @@ -794,10 +795,8 @@ func TestNodeOperations_CrossOperationLocking(t *testing.T) { readyToExecute := make(chan chan struct{}, 1) driver := initBlockingDriver(t, readyToExecute) - ctx := t.Context() - // Create a volume - vol, err := driver.cloudscaleClient.Volumes.Create(ctx, &cloudscale.VolumeCreateRequest{ + vol, err := driver.cloudscaleClient.Volumes.Create(t.Context(), &cloudscale.VolumeCreateRequest{ Name: "test-volume", SizeGB: 10, Type: "ssd", @@ -832,7 +831,7 @@ func TestNodeOperations_CrossOperationLocking(t *testing.T) { runNodeStage := func(req *csi.NodeStageVolumeRequest) <-chan error { response := make(chan error, 1) go func() { - _, err := driver.NodeStageVolume(context.Background(), req) + _, err := driver.NodeStageVolume(t.Context(), req) response <- err }() return response @@ -841,7 +840,7 @@ func TestNodeOperations_CrossOperationLocking(t *testing.T) { runNodeUnstage := func(req *csi.NodeUnstageVolumeRequest) <-chan error { response := make(chan error, 1) go func() { - _, err := driver.NodeUnstageVolume(context.Background(), req) + _, err := driver.NodeUnstageVolume(t.Context(), req) response <- err }() return response @@ -889,13 +888,13 @@ func newFakeMounter() *fakeMounter { // client containing a single server, and a dummy unix endpoint. func newTestDriver(t *testing.T, m Mounter) *Driver { t.Helper() - const serverId = "987654" + const serverID = "987654" return &Driver{ endpoint: "unix:///tmp/csi-test.sock", - serverId: serverId, + serverID: serverID, zone: DefaultZone.Slug, cloudscaleClient: NewFakeClient(map[string]*cloudscale.Server{ - serverId: {UUID: serverId}, + serverID: {UUID: serverID}, }), mounter: m, log: logrus.New().WithField("test_enabled", true), @@ -957,7 +956,7 @@ func TestNodeStageVolume_Restage_NonLUKS_ByIdEquivalent(t *testing.T) { fm.blockDeviceNumbers["SomePath"] = unix.Mkdev(8, 16) fm.filesystemDeviceNumbers[stagingPath] = unix.Mkdev(8, 16) - _, err = driver.NodeStageVolume(context.Background(), makeStageReq(vol.UUID, stagingPath, vol.Name, false)) + _, err = driver.NodeStageVolume(t.Context(), makeStageReq(vol.UUID, stagingPath, vol.Name, false)) if err != nil { t.Fatalf("expected no error on re-stage, got: %v", err) } @@ -979,7 +978,7 @@ func TestNodeStageVolume_Restage_NonLUKS_StaleDevice(t *testing.T) { fm.blockDeviceNumbers["SomePath"] = unix.Mkdev(8, 16) // what we expect fm.filesystemDeviceNumbers[stagingPath] = unix.Mkdev(8, 32) // actually mounted device - _, err = driver.NodeStageVolume(context.Background(), makeStageReq(vol.UUID, stagingPath, vol.Name, false)) + _, err = driver.NodeStageVolume(t.Context(), makeStageReq(vol.UUID, stagingPath, vol.Name, false)) if err == nil { t.Fatal("expected FailedPrecondition on stale mount, got nil") } @@ -1005,7 +1004,7 @@ func TestNodeStageVolume_Restage_LUKS_ByIdEquivalent(t *testing.T) { fm.blockDeviceNumbers[mapper] = unix.Mkdev(252, 0) fm.filesystemDeviceNumbers[stagingPath] = unix.Mkdev(252, 0) - _, err = driver.NodeStageVolume(context.Background(), makeStageReq(vol.UUID, stagingPath, vol.Name, true)) + _, err = driver.NodeStageVolume(t.Context(), makeStageReq(vol.UUID, stagingPath, vol.Name, true)) if err != nil { t.Fatalf("expected no error on re-stage, got: %v", err) } @@ -1027,7 +1026,7 @@ func TestNodeStageVolume_Restage_LUKS_RawDeviceMount(t *testing.T) { fm.blockDeviceNumbers[mapper] = unix.Mkdev(252, 0) // we expect the mapper fm.filesystemDeviceNumbers[stagingPath] = unix.Mkdev(8, 16) // but the mount is from raw sdb - _, err = driver.NodeStageVolume(context.Background(), makeStageReq(vol.UUID, stagingPath, vol.Name, true)) + _, err = driver.NodeStageVolume(t.Context(), makeStageReq(vol.UUID, stagingPath, vol.Name, true)) if err == nil { t.Fatal("expected FailedPrecondition on stale LUKS mount, got nil") } @@ -1040,7 +1039,7 @@ func TestNodeStageVolume_Restage_LUKS_RawDeviceMount(t *testing.T) { // createVolumeForTest is a helper that creates a CSI volume and returns the volume ID. func createVolumeForTest(t *testing.T, driver *Driver, name string) string { t.Helper() - vol, err := driver.CreateVolume(context.Background(), &csi.CreateVolumeRequest{ + vol, err := driver.CreateVolume(t.Context(), &csi.CreateVolumeRequest{ Name: name, VolumeCapabilities: makeVolumeCapabilityObject(false), CapacityRange: &csi.CapacityRange{RequiredBytes: 1 * GB}, @@ -1055,7 +1054,7 @@ func createVolumeForTest(t *testing.T, driver *Driver, name string) string { // createSnapshotForTest is a helper that creates a CSI snapshot and returns the snapshot response. func createSnapshotForTest(t *testing.T, driver *Driver, name, volumeID string) *csi.CreateSnapshotResponse { t.Helper() - snap, err := driver.CreateSnapshot(context.Background(), &csi.CreateSnapshotRequest{ + snap, err := driver.CreateSnapshot(t.Context(), &csi.CreateSnapshotRequest{ Name: name, SourceVolumeId: volumeID, }) @@ -1066,15 +1065,14 @@ func createSnapshotForTest(t *testing.T, driver *Driver, name, volumeID string) } func TestListSnapshots_All(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() volID := createVolumeForTest(t, driver, "vol-list-all") createSnapshotForTest(t, driver, "snap-1", volID) createSnapshotForTest(t, driver, "snap-2", volID) createSnapshotForTest(t, driver, "snap-3", volID) - resp, err := driver.ListSnapshots(ctx, &csi.ListSnapshotsRequest{}) + resp, err := driver.ListSnapshots(t.Context(), &csi.ListSnapshotsRequest{}) if err != nil { t.Fatalf("ListSnapshots returned error: %v", err) } @@ -1095,14 +1093,13 @@ func TestListSnapshots_All(t *testing.T) { } func TestListSnapshots_BySnapshotId(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() volID := createVolumeForTest(t, driver, "vol-by-id") snap1 := createSnapshotForTest(t, driver, "snap-target", volID) createSnapshotForTest(t, driver, "snap-other", volID) - resp, err := driver.ListSnapshots(ctx, &csi.ListSnapshotsRequest{ + resp, err := driver.ListSnapshots(t.Context(), &csi.ListSnapshotsRequest{ SnapshotId: snap1.Snapshot.SnapshotId, }) if err != nil { @@ -1117,10 +1114,9 @@ func TestListSnapshots_BySnapshotId(t *testing.T) { } func TestListSnapshots_BySnapshotId_NotFound(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() - resp, err := driver.ListSnapshots(ctx, &csi.ListSnapshotsRequest{ + resp, err := driver.ListSnapshots(t.Context(), &csi.ListSnapshotsRequest{ SnapshotId: "non-existent-id", }) if err != nil { @@ -1132,15 +1128,14 @@ func TestListSnapshots_BySnapshotId_NotFound(t *testing.T) { } func TestListSnapshots_BySourceVolumeId(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() volID1 := createVolumeForTest(t, driver, "vol-source-1") volID2 := createVolumeForTest(t, driver, "vol-source-2") createSnapshotForTest(t, driver, "snap-vol1", volID1) createSnapshotForTest(t, driver, "snap-vol2", volID2) - resp, err := driver.ListSnapshots(ctx, &csi.ListSnapshotsRequest{ + resp, err := driver.ListSnapshots(t.Context(), &csi.ListSnapshotsRequest{ SourceVolumeId: volID1, }) if err != nil { @@ -1155,10 +1150,9 @@ func TestListSnapshots_BySourceVolumeId(t *testing.T) { } func TestListSnapshots_BySourceVolumeId_NotFound(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() - resp, err := driver.ListSnapshots(ctx, &csi.ListSnapshotsRequest{ + resp, err := driver.ListSnapshots(t.Context(), &csi.ListSnapshotsRequest{ SourceVolumeId: "non-existent-volume", }) if err != nil { @@ -1170,16 +1164,15 @@ func TestListSnapshots_BySourceVolumeId_NotFound(t *testing.T) { } func TestListSnapshots_Pagination(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() volID := createVolumeForTest(t, driver, "vol-pagination") - for i := 0; i < 5; i++ { + for i := range 5 { createSnapshotForTest(t, driver, "snap-page-"+strconv.Itoa(i), volID) } // Request first page with max_entries=2 - resp, err := driver.ListSnapshots(ctx, &csi.ListSnapshotsRequest{ + resp, err := driver.ListSnapshots(t.Context(), &csi.ListSnapshotsRequest{ MaxEntries: 2, }) if err != nil { @@ -1193,7 +1186,7 @@ func TestListSnapshots_Pagination(t *testing.T) { } // Request second page with starting_token - resp2, err := driver.ListSnapshots(ctx, &csi.ListSnapshotsRequest{ + resp2, err := driver.ListSnapshots(t.Context(), &csi.ListSnapshotsRequest{ StartingToken: resp.NextToken, }) if err != nil { @@ -1219,10 +1212,9 @@ func TestListSnapshots_Pagination(t *testing.T) { } func TestListSnapshots_InvalidStartingToken(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() - _, err := driver.ListSnapshots(ctx, &csi.ListSnapshotsRequest{ + _, err := driver.ListSnapshots(t.Context(), &csi.ListSnapshotsRequest{ StartingToken: "not-a-number", }) if err == nil { @@ -1253,7 +1245,6 @@ func TestCreateSnapshot_SnapshotLimitExhausted(t *testing.T) { cloudscaleClient: cloudscaleClient, volumeLocks: NewVolumeLocks(), } - ctx := context.Background() volID := createVolumeForTest(t, driver, "vol-snap-limit") @@ -1262,7 +1253,7 @@ func TestCreateSnapshot_SnapshotLimitExhausted(t *testing.T) { createSnapshotForTest(t, driver, "snap-limit-2", volID) // Third snapshot should fail with ResourceExhausted - _, err := driver.CreateSnapshot(ctx, &csi.CreateSnapshotRequest{ + _, err := driver.CreateSnapshot(t.Context(), &csi.CreateSnapshotRequest{ Name: "snap-limit-3", SourceVolumeId: volID, }) @@ -1284,11 +1275,10 @@ func TestCreateSnapshot_SnapshotLimitExhausted(t *testing.T) { // the CSI spec requirement for volumes that cannot be deleted independently // of their snapshots. func TestDeleteVolume_FailsWhenSnapshotsExist(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() // Create a volume - vol, err := driver.CreateVolume(ctx, &csi.CreateVolumeRequest{ + vol, err := driver.CreateVolume(t.Context(), &csi.CreateVolumeRequest{ Name: "test-volume-with-snapshot", VolumeCapabilities: makeVolumeCapabilityObject(false), CapacityRange: &csi.CapacityRange{ @@ -1304,7 +1294,7 @@ func TestDeleteVolume_FailsWhenSnapshotsExist(t *testing.T) { volumeID := vol.Volume.VolumeId // Create a snapshot on the volume - snap, err := driver.CreateSnapshot(ctx, &csi.CreateSnapshotRequest{ + snap, err := driver.CreateSnapshot(t.Context(), &csi.CreateSnapshotRequest{ Name: "test-snapshot", SourceVolumeId: volumeID, }) @@ -1316,7 +1306,7 @@ func TestDeleteVolume_FailsWhenSnapshotsExist(t *testing.T) { } // Attempt to delete the volume — should fail with FailedPrecondition - _, err = driver.DeleteVolume(ctx, &csi.DeleteVolumeRequest{ + _, err = driver.DeleteVolume(t.Context(), &csi.DeleteVolumeRequest{ VolumeId: volumeID, }) if err == nil { @@ -1332,14 +1322,14 @@ func TestDeleteVolume_FailsWhenSnapshotsExist(t *testing.T) { } // Delete the snapshot, then delete the volume, should succeed now - _, err = driver.DeleteSnapshot(ctx, &csi.DeleteSnapshotRequest{ + _, err = driver.DeleteSnapshot(t.Context(), &csi.DeleteSnapshotRequest{ SnapshotId: snap.Snapshot.SnapshotId, }) if err != nil { t.Fatalf("Failed to delete snapshot: %v", err) } - _, err = driver.DeleteVolume(ctx, &csi.DeleteVolumeRequest{ + _, err = driver.DeleteVolume(t.Context(), &csi.DeleteVolumeRequest{ VolumeId: volumeID, }) if err != nil { @@ -1347,11 +1337,12 @@ func TestDeleteVolume_FailsWhenSnapshotsExist(t *testing.T) { } } +//nolint:unparam // keep `sizeGB` parameter for potential future use func createVolumeAndSnapshot(t *testing.T, driver *Driver, sizeGB int) (string, string) { t.Helper() - ctx := context.Background() - vol, err := driver.CreateVolume(ctx, &csi.CreateVolumeRequest{ + vol, err := driver.CreateVolume(t.Context(), &csi.CreateVolumeRequest{ + //nolint:gosec // G404: weak random generator acceptable in test code Name: "src-vol-" + strconv.Itoa(rand.Int()), VolumeCapabilities: makeVolumeCapabilityObject(false), CapacityRange: &csi.CapacityRange{RequiredBytes: int64(sizeGB) * GB}, @@ -1361,7 +1352,8 @@ func createVolumeAndSnapshot(t *testing.T, driver *Driver, sizeGB int) (string, t.Fatalf("Failed to create source volume: %v", err) } - snap, err := driver.CreateSnapshot(ctx, &csi.CreateSnapshotRequest{ + snap, err := driver.CreateSnapshot(t.Context(), &csi.CreateSnapshotRequest{ + //nolint:gosec // G404: weak random generator acceptable in test code Name: "snap-" + strconv.Itoa(rand.Int()), SourceVolumeId: vol.Volume.VolumeId, }) @@ -1375,11 +1367,10 @@ func createVolumeAndSnapshot(t *testing.T, driver *Driver, sizeGB int) (string, func TestCreateVolumeFromSnapshot_EqualSize(t *testing.T) { const snapshotSizeGiB = 5 // source volume and snapshot size (GiB) - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() _, snapshotID := createVolumeAndSnapshot(t, driver, snapshotSizeGiB) - resp, err := driver.CreateVolume(ctx, &csi.CreateVolumeRequest{ + resp, err := driver.CreateVolume(t.Context(), &csi.CreateVolumeRequest{ Name: "restored-equal", VolumeCapabilities: makeVolumeCapabilityObject(false), CapacityRange: &csi.CapacityRange{RequiredBytes: int64(snapshotSizeGiB) * GB}, @@ -1399,11 +1390,10 @@ func TestCreateVolumeFromSnapshot_LargerSize(t *testing.T) { expandedSizeGiB = 10 // requested size larger than snapshot (triggers expansion) ) - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() _, snapshotID := createVolumeAndSnapshot(t, driver, snapshotSizeGiB) - resp, err := driver.CreateVolume(ctx, &csi.CreateVolumeRequest{ + resp, err := driver.CreateVolume(t.Context(), &csi.CreateVolumeRequest{ Name: "restored-larger", VolumeCapabilities: makeVolumeCapabilityObject(false), CapacityRange: &csi.CapacityRange{RequiredBytes: int64(expandedSizeGiB) * GB}, @@ -1416,7 +1406,7 @@ func TestCreateVolumeFromSnapshot_LargerSize(t *testing.T) { assert.NoError(t, err) assert.Equal(t, int64(expandedSizeGiB*GB), resp.Volume.CapacityBytes) - vol, err := driver.cloudscaleClient.Volumes.Get(ctx, resp.Volume.VolumeId) + vol, err := driver.cloudscaleClient.Volumes.Get(t.Context(), resp.Volume.VolumeId) assert.NoError(t, err) assert.Equal(t, expandedSizeGiB, vol.SizeGB) } @@ -1427,11 +1417,10 @@ func TestCreateVolumeFromSnapshot_SmallerSize_Rejected(t *testing.T) { belowSnapshotSizeGiB = 3 // requested size smaller than snapshot (invalid) ) - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() _, snapshotID := createVolumeAndSnapshot(t, driver, snapshotSizeGiB) - _, err := driver.CreateVolume(ctx, &csi.CreateVolumeRequest{ + _, err := driver.CreateVolume(t.Context(), &csi.CreateVolumeRequest{ Name: "restored-smaller", VolumeCapabilities: makeVolumeCapabilityObject(false), CapacityRange: &csi.CapacityRange{RequiredBytes: int64(belowSnapshotSizeGiB) * GB}, @@ -1453,8 +1442,7 @@ func TestCreateVolumeFromSnapshot_Idempotent_AlreadyExpanded(t *testing.T) { expandedSizeGiB = 10 // requested size larger than snapshot (triggers expansion) ) - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() _, snapshotID := createVolumeAndSnapshot(t, driver, snapshotSizeGiB) req := &csi.CreateVolumeRequest{ @@ -1468,11 +1456,11 @@ func TestCreateVolumeFromSnapshot_Idempotent_AlreadyExpanded(t *testing.T) { }, } - resp1, err := driver.CreateVolume(ctx, req) + resp1, err := driver.CreateVolume(t.Context(), req) assert.NoError(t, err) assert.Equal(t, int64(expandedSizeGiB*GB), resp1.Volume.CapacityBytes) - resp2, err := driver.CreateVolume(ctx, req) + resp2, err := driver.CreateVolume(t.Context(), req) assert.NoError(t, err) assert.Equal(t, resp1.Volume.VolumeId, resp2.Volume.VolumeId) assert.Equal(t, int64(expandedSizeGiB*GB), resp2.Volume.CapacityBytes) @@ -1484,12 +1472,11 @@ func TestCreateVolumeFromSnapshot_Idempotent_NeedsExpansion(t *testing.T) { expandedSizeGiB = 10 // requested size larger than snapshot (triggers expansion) ) - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() _, snapshotID := createVolumeAndSnapshot(t, driver, snapshotSizeGiB) // First create at snapshot size (equal) - resp1, err := driver.CreateVolume(ctx, &csi.CreateVolumeRequest{ + resp1, err := driver.CreateVolume(t.Context(), &csi.CreateVolumeRequest{ Name: "restored-needs-expand", VolumeCapabilities: makeVolumeCapabilityObject(false), CapacityRange: &csi.CapacityRange{RequiredBytes: int64(snapshotSizeGiB) * GB}, @@ -1503,7 +1490,7 @@ func TestCreateVolumeFromSnapshot_Idempotent_NeedsExpansion(t *testing.T) { assert.Equal(t, int64(snapshotSizeGiB*GB), resp1.Volume.CapacityBytes) // Now call again with a larger size -- simulates retry after expand failure - resp2, err := driver.CreateVolume(ctx, &csi.CreateVolumeRequest{ + resp2, err := driver.CreateVolume(t.Context(), &csi.CreateVolumeRequest{ Name: "restored-needs-expand", VolumeCapabilities: makeVolumeCapabilityObject(false), CapacityRange: &csi.CapacityRange{RequiredBytes: int64(expandedSizeGiB) * GB}, @@ -1517,7 +1504,7 @@ func TestCreateVolumeFromSnapshot_Idempotent_NeedsExpansion(t *testing.T) { assert.Equal(t, resp1.Volume.VolumeId, resp2.Volume.VolumeId) assert.Equal(t, int64(expandedSizeGiB*GB), resp2.Volume.CapacityBytes) - vol, err := driver.cloudscaleClient.Volumes.Get(ctx, resp2.Volume.VolumeId) + vol, err := driver.cloudscaleClient.Volumes.Get(t.Context(), resp2.Volume.VolumeId) assert.NoError(t, err) assert.Equal(t, expandedSizeGiB, vol.SizeGB) } @@ -1536,7 +1523,7 @@ func TestControllerPublishVolume_RejectsWhenAttachedToDifferentNode(t *testing.T driver := &Driver{ endpoint: "unix:///tmp/csi-test.sock", - serverId: serverA, + serverID: serverA, zone: DefaultZone.Slug, cloudscaleClient: cloudscaleClient, mounter: &fakeMounter{mounted: map[string]string{}}, @@ -1544,11 +1531,10 @@ func TestControllerPublishVolume_RejectsWhenAttachedToDifferentNode(t *testing.T volumeLocks: NewVolumeLocks(), } - ctx := context.Background() volumeID := createVolumeForTest(t, driver, "test-vol-multiattach") // Attach volume to server A - _, err := driver.ControllerPublishVolume(ctx, &csi.ControllerPublishVolumeRequest{ + _, err := driver.ControllerPublishVolume(t.Context(), &csi.ControllerPublishVolumeRequest{ VolumeId: volumeID, NodeId: serverA, VolumeCapability: &csi.VolumeCapability{ @@ -1565,7 +1551,7 @@ func TestControllerPublishVolume_RejectsWhenAttachedToDifferentNode(t *testing.T } // Try to attach the same volume to server B — should be rejected - _, err = driver.ControllerPublishVolume(ctx, &csi.ControllerPublishVolumeRequest{ + _, err = driver.ControllerPublishVolume(t.Context(), &csi.ControllerPublishVolumeRequest{ VolumeId: volumeID, NodeId: serverB, VolumeCapability: &csi.VolumeCapability{ @@ -1590,7 +1576,7 @@ func TestControllerPublishVolume_RejectsWhenAttachedToDifferentNode(t *testing.T } // Verify the volume is still attached to server A (not silently moved) - vol, err := cloudscaleClient.Volumes.Get(ctx, volumeID) + vol, err := cloudscaleClient.Volumes.Get(t.Context(), volumeID) if err != nil { t.Fatalf("Failed to get volume: %v", err) } @@ -1611,7 +1597,7 @@ func TestControllerPublishVolume_IdempotentSameNode(t *testing.T) { driver := &Driver{ endpoint: "unix:///tmp/csi-test.sock", - serverId: serverA, + serverID: serverA, zone: DefaultZone.Slug, cloudscaleClient: cloudscaleClient, mounter: &fakeMounter{mounted: map[string]string{}}, @@ -1619,7 +1605,6 @@ func TestControllerPublishVolume_IdempotentSameNode(t *testing.T) { volumeLocks: NewVolumeLocks(), } - ctx := context.Background() volumeID := createVolumeForTest(t, driver, "test-vol-idempotent") publishReq := &csi.ControllerPublishVolumeRequest{ @@ -1639,13 +1624,13 @@ func TestControllerPublishVolume_IdempotentSameNode(t *testing.T) { } // First publish - resp1, err := driver.ControllerPublishVolume(ctx, publishReq) + resp1, err := driver.ControllerPublishVolume(t.Context(), publishReq) if err != nil { t.Fatalf("First publish failed: %v", err) } // Second publish to same node — should succeed (idempotent) - resp2, err := driver.ControllerPublishVolume(ctx, publishReq) + resp2, err := driver.ControllerPublishVolume(t.Context(), publishReq) if err != nil { t.Fatalf("Second publish (idempotent) failed: %v", err) } @@ -1667,7 +1652,7 @@ func TestControllerPublishVolume_SucceedsWhenNotAttached(t *testing.T) { driver := &Driver{ endpoint: "unix:///tmp/csi-test.sock", - serverId: serverA, + serverID: serverA, zone: DefaultZone.Slug, cloudscaleClient: cloudscaleClient, mounter: &fakeMounter{mounted: map[string]string{}}, @@ -1675,10 +1660,9 @@ func TestControllerPublishVolume_SucceedsWhenNotAttached(t *testing.T) { volumeLocks: NewVolumeLocks(), } - ctx := context.Background() volumeID := createVolumeForTest(t, driver, "test-vol-attach") - resp, err := driver.ControllerPublishVolume(ctx, &csi.ControllerPublishVolumeRequest{ + resp, err := driver.ControllerPublishVolume(t.Context(), &csi.ControllerPublishVolumeRequest{ VolumeId: volumeID, NodeId: serverA, VolumeCapability: &csi.VolumeCapability{ @@ -1699,7 +1683,7 @@ func TestControllerPublishVolume_SucceedsWhenNotAttached(t *testing.T) { } // Verify volume is attached to the server - vol, err := cloudscaleClient.Volumes.Get(ctx, volumeID) + vol, err := cloudscaleClient.Volumes.Get(t.Context(), volumeID) if err != nil { t.Fatalf("Failed to get volume: %v", err) } @@ -1711,8 +1695,7 @@ func TestControllerPublishVolume_SucceedsWhenNotAttached(t *testing.T) { // TestControllerOperations_VolumeLocks tests that concurrent controller // operations on the same volume are properly serialized with volume locks. func TestControllerOperations_VolumeLocks(t *testing.T) { - driver := createDriverForTest(t) - ctx := context.Background() + driver := createDriverForTest() volumeID := createVolumeForTest(t, driver, "test-vol-locks") // Pre-acquire the volume lock @@ -1721,7 +1704,7 @@ func TestControllerOperations_VolumeLocks(t *testing.T) { } // ControllerPublishVolume should return Aborted - _, err := driver.ControllerPublishVolume(ctx, &csi.ControllerPublishVolumeRequest{ + _, err := driver.ControllerPublishVolume(t.Context(), &csi.ControllerPublishVolumeRequest{ VolumeId: volumeID, NodeId: "some-node", VolumeCapability: &csi.VolumeCapability{ @@ -1736,27 +1719,27 @@ func TestControllerOperations_VolumeLocks(t *testing.T) { assertAbortedError(t, err, "ControllerPublishVolume") // ControllerUnpublishVolume should return Aborted - _, err = driver.ControllerUnpublishVolume(ctx, &csi.ControllerUnpublishVolumeRequest{ + _, err = driver.ControllerUnpublishVolume(t.Context(), &csi.ControllerUnpublishVolumeRequest{ VolumeId: volumeID, NodeId: "some-node", }) assertAbortedError(t, err, "ControllerUnpublishVolume") // DeleteVolume should return Aborted - _, err = driver.DeleteVolume(ctx, &csi.DeleteVolumeRequest{ + _, err = driver.DeleteVolume(t.Context(), &csi.DeleteVolumeRequest{ VolumeId: volumeID, }) assertAbortedError(t, err, "DeleteVolume") // ControllerExpandVolume should return Aborted - _, err = driver.ControllerExpandVolume(ctx, &csi.ControllerExpandVolumeRequest{ + _, err = driver.ControllerExpandVolume(t.Context(), &csi.ControllerExpandVolumeRequest{ VolumeId: volumeID, CapacityRange: &csi.CapacityRange{RequiredBytes: 10 * GB}, }) assertAbortedError(t, err, "ControllerExpandVolume") // CreateSnapshot should return Aborted (locks on source volume ID) - _, err = driver.CreateSnapshot(ctx, &csi.CreateSnapshotRequest{ + _, err = driver.CreateSnapshot(t.Context(), &csi.CreateSnapshotRequest{ Name: "snap-locked", SourceVolumeId: volumeID, }) diff --git a/driver/driver_volume_type_test.go b/driver/driver_volume_type_test.go index 608fc032..bda4e5d5 100644 --- a/driver/driver_volume_type_test.go +++ b/driver/driver_volume_type_test.go @@ -1,22 +1,21 @@ package driver import ( - "context" "testing" - "github.com/cloudscale-ch/cloudscale-go-sdk/v7" + "github.com/cloudscale-ch/cloudscale-go-sdk/v10" "github.com/container-storage-interface/spec/lib/go/csi" "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" ) func TestCreateVolumeTypeSsdWithoutExplicitlySpecifyingTheType(t *testing.T) { - driver := createDriverForTest(t) + driver := createDriverForTest() volumeName := randString(32) response, err := driver.CreateVolume( - context.Background(), + t.Context(), makeCreateVolumeRequest(volumeName, 1, "", false), ) @@ -26,7 +25,7 @@ func TestCreateVolumeTypeSsdWithoutExplicitlySpecifyingTheType(t *testing.T) { assert.Equal(t, int64(1)*GB, response.Volume.CapacityBytes) assert.Equal(t, volumeName, response.Volume.VolumeContext[PublishInfoVolumeName]) - volumes, err := driver.cloudscaleClient.Volumes.List(context.Background()) + volumes, err := driver.cloudscaleClient.Volumes.List(t.Context()) assert.NoError(t, err) assert.Equal(t, 1, len(volumes)) assert.Equal(t, 1, volumes[0].SizeGB) @@ -34,12 +33,12 @@ func TestCreateVolumeTypeSsdWithoutExplicitlySpecifyingTheType(t *testing.T) { } func TestCreateVolumeTypeSsdExplicitlySpecifyingTheType(t *testing.T) { - driver := createDriverForTest(t) + driver := createDriverForTest() volumeName := randString(32) response, err := driver.CreateVolume( - context.Background(), + t.Context(), makeCreateVolumeRequest(volumeName, 5, "ssd", false), ) @@ -49,7 +48,7 @@ func TestCreateVolumeTypeSsdExplicitlySpecifyingTheType(t *testing.T) { assert.Equal(t, int64(5)*GB, response.Volume.CapacityBytes) assert.Equal(t, volumeName, response.Volume.VolumeContext[PublishInfoVolumeName]) - volumes, err := driver.cloudscaleClient.Volumes.List(context.Background()) + volumes, err := driver.cloudscaleClient.Volumes.List(t.Context()) assert.NoError(t, err) assert.Equal(t, 1, len(volumes)) assert.Equal(t, 5, volumes[0].SizeGB) @@ -57,12 +56,12 @@ func TestCreateVolumeTypeSsdExplicitlySpecifyingTheType(t *testing.T) { } func TestCreateVolumeTypeBulk(t *testing.T) { - driver := createDriverForTest(t) + driver := createDriverForTest() volumeName := randString(32) response, err := driver.CreateVolume( - context.Background(), + t.Context(), makeCreateVolumeRequest(volumeName, 100, "bulk", false), ) @@ -72,7 +71,7 @@ func TestCreateVolumeTypeBulk(t *testing.T) { assert.Equal(t, int64(100)*GB, response.Volume.CapacityBytes) assert.Equal(t, volumeName, response.Volume.VolumeContext[PublishInfoVolumeName]) - volumes, err := driver.cloudscaleClient.Volumes.List(context.Background()) + volumes, err := driver.cloudscaleClient.Volumes.List(t.Context()) assert.NoError(t, err) assert.Equal(t, 1, len(volumes)) assert.Equal(t, 100, volumes[0].SizeGB) @@ -80,12 +79,12 @@ func TestCreateVolumeTypeBulk(t *testing.T) { } func TestCreateVolumeInvalidType(t *testing.T) { - driver := createDriverForTest(t) + driver := createDriverForTest() volumeName := randString(32) _, err := driver.CreateVolume( - context.Background(), + t.Context(), makeCreateVolumeRequest(volumeName, 100, "foo", false), ) @@ -93,12 +92,12 @@ func TestCreateVolumeInvalidType(t *testing.T) { } func TestCreateVolumeInvalidLUKSAndRaw(t *testing.T) { - driver := createDriverForTest(t) + driver := createDriverForTest() volumeName := randString(32) _, err := driver.CreateVolume( - context.Background(), + t.Context(), makeLuksCreateVolumeRequest(volumeName, 100, "ssd", true, true), ) @@ -106,12 +105,12 @@ func TestCreateVolumeInvalidLUKSAndRaw(t *testing.T) { } func TestLuksEncryptionAttributeIsSetInContext(t *testing.T) { - driver := createDriverForTest(t) + driver := createDriverForTest() // explicitly set luks encryption to false volumeName := randString(32) response, err := driver.CreateVolume( - context.Background(), + t.Context(), makeLuksCreateVolumeRequest(volumeName, 100, "bulk", false, false), ) assert.NoError(t, err) @@ -120,7 +119,7 @@ func TestLuksEncryptionAttributeIsSetInContext(t *testing.T) { // explicitly set luks encryption to true volumeName = randString(32) response, err = driver.CreateVolume( - context.Background(), + t.Context(), makeLuksCreateVolumeRequest(volumeName, 100, "bulk", true, false), ) assert.NoError(t, err) @@ -129,7 +128,7 @@ func TestLuksEncryptionAttributeIsSetInContext(t *testing.T) { // don't set the luks encryption parameter - must implicitly default to false volumeName = randString(32) response, err = driver.CreateVolume( - context.Background(), + t.Context(), makeCreateVolumeRequest(volumeName, 100, "bulk", false), ) assert.NoError(t, err) @@ -157,7 +156,6 @@ func makeCreateVolumeRequest(volumeName string, sizeGb int, volumeType string, b StorageTypeAttribute: volumeType, }, } - } func makeVolumeCapabilityObject(block bool) []*csi.VolumeCapability { @@ -174,7 +172,7 @@ func makeVolumeCapabilityObject(block bool) []*csi.VolumeCapability { } } -func createDriverForTest(t *testing.T) *Driver { +func createDriverForTest() *Driver { initialServers := map[string]*cloudscale.Server{} cloudscaleClient := NewFakeClient(initialServers) diff --git a/driver/luks_util.go b/driver/luks_util.go index 0e18e0e5..f2613ef7 100644 --- a/driver/luks_util.go +++ b/driver/luks_util.go @@ -152,6 +152,7 @@ func luksFormat(source string, mkfsCmd string, mkfsArgs []string, ctx LuksContex "args": cryptsetupArgs, }).Info("executing cryptsetup luksFormat command") + //nolint:gosec // G204: cryptsetup luksFormat is an intentional system command out, err := exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() if err != nil { return fmt.Errorf("cryptsetup luksFormat failed: %v cmd: '%s %s' output: %q", @@ -190,6 +191,7 @@ func luksFormat(source string, mkfsCmd string, mkfsArgs []string, ctx LuksContex "args": mkfsArgs, }).Info("executing format command") + //nolint:gosec // G204: mkfs command is an intentional system command mkfsOut, err := exec.Command(mkfsCmd, mkfsArgs...).CombinedOutput() if err != nil { return fmt.Errorf("formatting disk failed: %v cmd: '%s %s' output: %q", @@ -230,6 +232,7 @@ func luksClose(volume string, log *logrus.Entry) error { "args": cryptsetupArgs, }).Info("executing cryptsetup close command") + //nolint:gosec // G204: cryptsetup close is an intentional system command out, err := exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() if err != nil { return fmt.Errorf("removing luks mapping failed: %v cmd: '%s %s' output: %q", @@ -241,11 +244,11 @@ func luksClose(volume string, log *logrus.Entry) error { // checks if the given volume is formatted by checking if it is a luks volume and // if the luks volume, once opened, contains a filesystem func isLuksVolumeFormatted(volume string, ctx LuksContext, log *logrus.Entry) (formatted bool, err error) { - isLuks, err := isLuks(volume) + isLuksVolume, err := isLuks(volume) if err != nil { return false, err } - if !isLuks { + if !isLuksVolume { return false, nil } @@ -326,6 +329,8 @@ func luksOpen(volume string, keyFile string, ctx LuksContext, log *logrus.Entry) "cmd": cryptsetupCmd, "args": cryptsetupArgs, }).Info("executing cryptsetup luksOpen command") + + //nolint:gosec // G204: cryptsetup luksOpen is an intentional system command out, err := exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() if err != nil { return false, fmt.Errorf("cryptsetup luksOpen failed: %v cmd: '%s %s' output: %q", @@ -347,6 +352,7 @@ func luksResize(volume string, log *logrus.Entry) error { "args": cryptsetupArgs, }).Info("executing cryptsetup resize command") + //nolint:gosec // G204: cryptsetup resize is an intentional system command out, err := exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() if err != nil { return fmt.Errorf("cryptsetup resize failed: %v cmd: '%s %s' output: %q", @@ -365,6 +371,7 @@ func isLuks(volume string) (bool, error) { // cryptsetup isLuks exits with code 0 if the target is a luks volume; otherwise it returns // a non-zero exit code which exec.Command interprets as an error + //nolint:gosec // G204: cryptsetup isLuks is an intentional system command _, err = exec.Command(cryptsetupCmd, cryptsetupArgs...).CombinedOutput() if err != nil { return false, nil @@ -474,6 +481,7 @@ func cryptsetupStatus(name string) (cryptsetupStatusInfo, error) { if err != nil { return cryptsetupStatusInfo{}, err } + //nolint:gosec // G204: cryptsetup status is an intentional system command out, err := exec.Command(cryptsetupCmd, "status", name).CombinedOutput() info := parseCryptsetupStatus(out) if err != nil { @@ -538,6 +546,7 @@ func writeLuksKey(key string, log *logrus.Entry) (string, error) { // makes sure that the given directory is a tmpfs func checkTmpFs(dir string) bool { + //nolint:gosec // G204: df command for tmpfs check is an intentional system command out, err := exec.Command("sh", "-c", "df -T "+dir+" | tail -n1 | awk '{print $2}'").CombinedOutput() if err != nil { return false diff --git a/driver/mounter.go b/driver/mounter.go index 7f4f0446..13da7cea 100644 --- a/driver/mounter.go +++ b/driver/mounter.go @@ -98,7 +98,7 @@ type Mounter interface { // Used to find a path in /dev/disk/by-id with a serial that we have from // the cloudscale API. - FinalizeVolumeAttachmentAndFindPath(logger *logrus.Entry, VolumeId string) (string, error) + FinalizeVolumeAttachmentAndFindPath(ctx context.Context, logger *logrus.Entry, VolumeID string) (string, error) // GetStatistics returns capacity-related volume statistics for the given // volume path. @@ -179,6 +179,7 @@ func (m *mounter) Format(source, fsType string, luksContext LuksContext, log *lo "args": mkfsArgs, }).Info("executing format command") + //nolint:gosec // G204: intentional system command out, err := exec.Command(mkfsCmd, mkfsArgs...).CombinedOutput() if err != nil { return fmt.Errorf("formatting disk failed: %v cmd: '%s %s' output: %q", @@ -186,17 +187,16 @@ func (m *mounter) Format(source, fsType string, luksContext LuksContext, log *lo } return nil - } else { - err := luksContext.validate() - if err != nil { - return err - } - err = luksFormat(source, mkfsCmd, mkfsArgs, luksContext, log) - if err != nil { - return err - } - return nil } + err = luksContext.validate() + if err != nil { + return err + } + err = luksFormat(source, mkfsCmd, mkfsArgs, luksContext, log) + if err != nil { + return err + } + return nil } func (m *mounter) Mount(source, target, fsType string, luksContext LuksContext, log *logrus.Entry, options ...string) error { @@ -217,6 +217,7 @@ func (m *mounter) Mount(source, target, fsType string, luksContext LuksContext, return fmt.Errorf("failed to create target directory for raw block bind mount: %v", err) } + //nolint:gosec // G302,G304: device node bind mount: 0660 is fine, target path is controller by the CSI spec. file, err := os.OpenFile(target, os.O_CREATE, 0660) if err != nil { return fmt.Errorf("failed to create target file for raw block bind mount: %v", err) @@ -345,6 +346,7 @@ func isVolumeFormatted(source string, log *logrus.Entry) (bool, error) { }).Info("checking if source is formatted") exitCode := 0 + //nolint:gosec // G204: intentional system command cmd := exec.Command(blkidCmd, blkidArgs...) err = cmd.Run() if err != nil { @@ -356,9 +358,8 @@ func isVolumeFormatted(source string, log *logrus.Entry) (bool, error) { exitCode = ws.ExitStatus() if exitCode == blkidExitStatusNoIdentifiers { return false, nil - } else { - return false, fmt.Errorf("checking formatting failed: %v cmd: %q, args: %q", err, blkidCmd, blkidArgs) } + return false, fmt.Errorf("checking formatting failed: %v cmd: %q, args: %q", err, blkidCmd, blkidArgs) } return true, nil @@ -385,6 +386,7 @@ func (m *mounter) GetMountInfo(target string, log *logrus.Entry) (*MountInfo, er "args": findmntArgs, }).Info("checking if target is mounted") + //nolint:gosec // G204: intentional system command out, err := exec.Command(findmntCmd, findmntArgs...).CombinedOutput() if err != nil { // findmnt exits with non-zero exit status if it couldn't find anything @@ -461,9 +463,15 @@ func guessDiskIDPathByVolumeID(volumeID string, logger *logrus.Entry) string { return "" } -func (m *mounter) FinalizeVolumeAttachmentAndFindPath(logger *logrus.Entry, volumeID string) (string, error) { +func (m *mounter) FinalizeVolumeAttachmentAndFindPath(ctx context.Context, logger *logrus.Entry, volumeID string) (string, error) { numTries := 0 for { + select { + case <-ctx.Done(): + return "", ctx.Err() + default: + } + diskIDPath := guessDiskIDPathByVolumeID(volumeID, logger) if diskIDPath != "" { // Resolve and log the actual device for debugging @@ -523,6 +531,7 @@ func (m *mounter) FinalizeVolumeAttachmentAndFindPath(logger *logrus.Entry, volu // tool. Calls scsi_id on the given devicePath to get the serial number reported // by that device. func getScsiSerial(devicePath string) (string, error) { + //nolint:gosec // G204: scsi_id command is intentional for device identification out, err := exec.Command( "/usr/lib/udev/scsi_id", "--page=0x83", @@ -557,6 +566,7 @@ func runCmdWithTimeout(name string, args []string, logger *logrus.Entry, timeout ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() + //nolint:gosec // G204: intentional system command out, err := exec.CommandContext(ctx, name, args...).CombinedOutput() if err != nil { logger.WithError(err). @@ -598,8 +608,22 @@ func scsiHostRescan(logger *logrus.Entry) { for _, f := range dirs { name := scsiPath + f.Name() + "/scan" - data := []byte("- - -") - _ = os.WriteFile(name, data, 0666) + + //nolint:gosec // G304: path is safe + file, err := os.OpenFile(name, os.O_WRONLY, 0) + if err != nil { + logger.WithError(err).Warnf("scsiHostRescan: cannot open %s", name) + continue + } + + _, err = file.WriteString("- - -") + closeErr := file.Close() + + if err != nil { + logger.WithError(err).Warnf("scsiHostRescan: cannot write %s", name) + } else if closeErr != nil { + logger.WithError(closeErr).Warnf("scsiHostRescan: cannot close %s", name) + } } } @@ -631,6 +655,7 @@ func (m *mounter) FindAbsoluteDeviceByIDPath(volumeName string, log *logrus.Entr func (m *mounter) HasRequiredSize(log *logrus.Entry, path string, requiredSize int64) (bool, error) { log.Infof("Checking device size: %s", path) + //nolint:gosec // G204: blockdev command is an intentional system command output, err := exec.Command("blockdev", "--getsize64", path).CombinedOutput() if err != nil { return false, fmt.Errorf("error when getting size of block volume at path %s: output: %s, err: %v", path, string(output), err) @@ -652,6 +677,7 @@ func (m *mounter) GetStatistics(volumePath string) (volumeStatistics, error) { if isBlock { // See http://man7.org/linux/man-pages/man8/blockdev.8.html for details + //nolint:gosec // G204: blockdev command is an intentional system command output, err := exec.Command("blockdev", "getsize64", volumePath).CombinedOutput() if err != nil { return volumeStatistics{}, fmt.Errorf("error when getting size of block volume at path %s: output: %s, err: %v", volumePath, string(output), err) @@ -674,11 +700,14 @@ func (m *mounter) GetStatistics(volumePath string) (volumeStatistics, error) { return volumeStatistics{}, err } + //nolint:gosec,unconvert // G115: filesystem statistics realistically don't exceed int64 max, + // can't use uint64 because CSI protobuf uses int64. + // unnecessary conversion: darwin/linux have different `statfs` types, to avoid linting + // false positives we need to ignore this lint error. volStats := volumeStatistics{ - availableBytes: int64(statfs.Bavail) * int64(statfs.Bsize), - totalBytes: int64(statfs.Blocks) * int64(statfs.Bsize), - usedBytes: (int64(statfs.Blocks) - int64(statfs.Bfree)) * int64(statfs.Bsize), - + availableBytes: int64(statfs.Bavail) * int64(statfs.Bsize), + totalBytes: int64(statfs.Blocks) * int64(statfs.Bsize), + usedBytes: (int64(statfs.Blocks) - int64(statfs.Bfree)) * int64(statfs.Bsize), availableInodes: int64(statfs.Ffree), totalInodes: int64(statfs.Files), usedInodes: int64(statfs.Files) - int64(statfs.Ffree), @@ -705,7 +734,7 @@ func (m *mounter) GetBlockDeviceNumber(path string) (uint64, error) { if st.Mode&unix.S_IFMT != unix.S_IFBLK { return 0, fmt.Errorf("%s is not a block device", path) } - return uint64(st.Rdev), nil + return uint64(st.Rdev), nil //nolint:gosec,unconvert // needed for darwin compatibility (Rdev is int32 on darwin) } func (m *mounter) GetFilesystemDeviceNumber(path string) (uint64, error) { @@ -716,5 +745,5 @@ func (m *mounter) GetFilesystemDeviceNumber(path string) (uint64, error) { if st.Mode&unix.S_IFMT == unix.S_IFBLK { return 0, fmt.Errorf("%s is a block device, expected a filesystem path", path) } - return uint64(st.Dev), nil + return uint64(st.Dev), nil //nolint:gosec,unconvert // needed for darwin compatibility (Dev is int32 on darwin) } diff --git a/driver/node.go b/driver/node.go index 4ae0be1c..cc65e647 100644 --- a/driver/node.go +++ b/driver/node.go @@ -80,7 +80,7 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe // Apparently sometimes we need to call udevadm trigger to get the volume // properly registered in /dev/disk. More information can be found here: // https://github.com/cloudscale-ch/csi-cloudscale/issues/9 - source, err := d.mounter.FinalizeVolumeAttachmentAndFindPath(ll, req.VolumeId) + source, err := d.mounter.FinalizeVolumeAttachmentAndFindPath(ctx, ll, req.VolumeId) if err != nil { return nil, err } @@ -104,8 +104,7 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe // If it is a block volume, we do nothing for stage volume // because we bind mount the absolute device path to a file - switch req.VolumeCapability.GetAccessType().(type) { - case *csi.VolumeCapability_Block: + if _, ok := req.VolumeCapability.GetAccessType().(*csi.VolumeCapability_Block); ok { return &csi.NodeStageVolumeResponse{}, nil } @@ -226,11 +225,11 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe // If the staged device is a LUKS mapping, grow the LUKS container first so // the filesystem can see the larger size. - isLuks, _, err := isLuksMapping(devicePath) + isLuksVolume, _, err := isLuksMapping(devicePath) if err != nil { return nil, status.Errorf(codes.Internal, "NodeStageVolume unable to test if volume at %q is encrypted with LUKS: %v", devicePath, err) } - if isLuks { + if isLuksVolume { ll.Info("resizing LUKS container before filesystem resize") if err := luksResize(devicePath, ll); err != nil { return nil, status.Errorf(codes.Internal, "failed to resize LUKS container on %s: %v", devicePath, err) @@ -238,12 +237,13 @@ func (d *Driver) NodeStageVolume(ctx context.Context, req *csi.NodeStageVolumeRe } r := mount.NewResizeFs(utilexec.New()) + //nolint:staticcheck // ResizeFs.NeedResize is stub on macOS, functional on Linux needResize, err := r.NeedResize(devicePath, stagingTargetPath) - if err != nil { + if err != nil { //nolint:staticcheck // ResizeFs.NeedResize is stub on macOS, functional on Linux ll.WithError(err).Warn("unable to check if filesystem needs resize") } else if needResize { ll.Info("resizing filesystem to match block device size") - if _, err := r.Resize(devicePath, stagingTargetPath); err != nil { + if _, err := r.Resize(devicePath, stagingTargetPath); err != nil { //nolint:staticcheck // ResizeFs is stub on macOS, functional on Linux return nil, status.Errorf(codes.Internal, "failed to resize filesystem on %s: %v", devicePath, err) } ll.Info("filesystem resized successfully") @@ -397,21 +397,21 @@ func (d *Driver) NodeUnpublishVolume(ctx context.Context, req *csi.NodeUnpublish // NodeGetCapabilities returns the supported capabilities of the node server func (d *Driver) NodeGetCapabilities(ctx context.Context, req *csi.NodeGetCapabilitiesRequest) (*csi.NodeGetCapabilitiesResponse, error) { nscaps := []*csi.NodeServiceCapability{ - &csi.NodeServiceCapability{ + { Type: &csi.NodeServiceCapability_Rpc{ Rpc: &csi.NodeServiceCapability_RPC{ Type: csi.NodeServiceCapability_RPC_STAGE_UNSTAGE_VOLUME, }, }, }, - &csi.NodeServiceCapability{ + { Type: &csi.NodeServiceCapability_Rpc{ Rpc: &csi.NodeServiceCapability_RPC{ Type: csi.NodeServiceCapability_RPC_EXPAND_VOLUME, }, }, }, - &csi.NodeServiceCapability{ + { Type: &csi.NodeServiceCapability_Rpc{ Rpc: &csi.NodeServiceCapability_RPC{ Type: csi.NodeServiceCapability_RPC_GET_VOLUME_STATS, @@ -448,7 +448,7 @@ func (d *Driver) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) ( maxVolumesPerNode := getEnvAsInt("CLOUDSCALE_MAX_CSI_VOLUMES_PER_NODE", fallbackMaxVolumesPerNode) return &csi.NodeGetInfoResponse{ - NodeId: d.serverId, + NodeId: d.serverID, MaxVolumesPerNode: maxVolumesPerNode, // make sure that the driver works on this particular region only @@ -463,7 +463,6 @@ func (d *Driver) NodeGetInfo(ctx context.Context, req *csi.NodeGetInfoRequest) ( // NodeGetVolumeStats returns the volume capacity statistics available for the // the given volume. func (d *Driver) NodeGetVolumeStats(ctx context.Context, req *csi.NodeGetVolumeStatsRequest) (*csi.NodeGetVolumeStatsResponse, error) { - if req.VolumeId == "" { return nil, status.Error(codes.InvalidArgument, "NodeGetVolumeStats Volume ID must be provided") } @@ -574,8 +573,7 @@ func (d *Driver) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolume ll = ll.WithField("source", source) if req.GetVolumeCapability() != nil { - switch req.GetVolumeCapability().GetAccessType().(type) { - case *csi.VolumeCapability_Block: + if _, ok := req.GetVolumeCapability().GetAccessType().(*csi.VolumeCapability_Block); ok { ll.Info("filesystem expansion is skipped for block volumes") return &csi.NodeExpandVolumeResponse{}, nil } @@ -599,7 +597,7 @@ func (d *Driver) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolume return nil, status.Errorf(codes.Internal, "NodeExpandVolume unable to get device path for %q: %v", volumePath, err) } - isLuks, _, err := isLuksMapping(devicePath) + isLuksVolume, _, err := isLuksMapping(devicePath) if err != nil { return nil, status.Errorf(codes.Internal, "NodeExpandVolume unable to test if volume %q at %q is encrypted with luks: %v", volumePath, devicePath, err) } @@ -618,7 +616,7 @@ func (d *Driver) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolume } // the luks container must be resized if the volume was resized while the disk was mounted - if isLuks { + if isLuksVolume { ll.Info("resizing luks container") err := luksResize(devicePath, ll) if err != nil { @@ -628,7 +626,7 @@ func (d *Driver) NodeExpandVolume(ctx context.Context, req *csi.NodeExpandVolume r := mount.NewResizeFs(utilexec.New()) ll.Info("resizing volume") - if _, err := r.Resize(devicePath, volumePath); err != nil { + if _, err := r.Resize(devicePath, volumePath); err != nil { //nolint:staticcheck // ResizeFs is stub on macOS, functional on Linux return nil, status.Errorf(codes.Internal, "NodeExpandVolume could not resize volume %q (%q): %v", volumeID, req.GetVolumePath(), err) } @@ -664,11 +662,11 @@ func (d *Driver) nodePublishVolumeForFileSystem(req *csi.NodePublishVolumeReques } func (d *Driver) nodePublishVolumeForBlock(req *csi.NodePublishVolumeRequest, luksContext LuksContext, mountOptions []string, ll *logrus.Entry) error { - volumeId := req.VolumeId + volumeID := req.VolumeId - source, err := d.mounter.FindAbsoluteDeviceByIDPath(volumeId, ll) + source, err := d.mounter.FindAbsoluteDeviceByIDPath(volumeID, ll) if err != nil { - return status.Errorf(codes.Internal, "Failed to find device path for volume %s. %v", volumeId, err) + return status.Errorf(codes.Internal, "Failed to find device path for volume %s. %v", volumeID, err) } target := req.TargetPath diff --git a/driver/volumelocks_test.go b/driver/volumelocks_test.go index a06ee551..943155a8 100644 --- a/driver/volumelocks_test.go +++ b/driver/volumelocks_test.go @@ -105,10 +105,8 @@ func TestVolumeLocks_ConcurrentAcquire(t *testing.T) { var wg sync.WaitGroup // Start many goroutines trying to acquire the same lock - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func() { - defer wg.Done() + for range numGoroutines { + wg.Go(func() { if acquired := vl.TryAcquire(volumeID); acquired { mu.Lock() successCount++ @@ -117,7 +115,7 @@ func TestVolumeLocks_ConcurrentAcquire(t *testing.T) { time.Sleep(time.Millisecond) vl.Release(volumeID) } - }() + }) } wg.Wait() @@ -141,13 +139,12 @@ func TestVolumeLocks_ConcurrentDifferentVolumes(t *testing.T) { results := make([]bool, numVolumes) // Each goroutine tries to lock a different volume - for i := 0; i < numVolumes; i++ { - wg.Add(1) - go func(idx int) { - defer wg.Done() - volumeID := "test-volume-" + string(rune('A'+idx)) + for i := range numVolumes { + idx := i + wg.Go(func() { + volumeID := "test-volume-" + string(rune('A'+idx)) //nolint:gosec // G115 results[idx] = vl.TryAcquire(volumeID) - }(i) + }) } wg.Wait() @@ -166,7 +163,7 @@ func TestVolumeLocks_RapidAcquireRelease(t *testing.T) { const iterations = 1000 - for i := 0; i < iterations; i++ { + for i := range iterations { if acquired := vl.TryAcquire(volumeID); !acquired { t.Fatalf("Iteration %d: TryAcquire should succeed after release", i) } @@ -182,19 +179,18 @@ func TestVolumeLocks_ConcurrentAcquireReleaseDifferentVolumes(t *testing.T) { var wg sync.WaitGroup // Multiple goroutines doing acquire/release on different volumes - for i := 0; i < numGoroutines; i++ { - wg.Add(1) - go func(volumeNum int) { - defer wg.Done() - volumeID := "test-volume-" + string(rune('A'+volumeNum)) - for j := 0; j < iterations; j++ { + for i := range numGoroutines { + volumeNum := i + wg.Go(func() { + volumeID := "test-volume-" + string(rune('A'+volumeNum)) //nolint:gosec // G115 + for range iterations { if acquired := vl.TryAcquire(volumeID); acquired { // Simulate some work time.Sleep(time.Microsecond) vl.Release(volumeID) } } - }(i) + }) } wg.Wait() diff --git a/go.mod b/go.mod index 222413ca..630f7d58 100644 --- a/go.mod +++ b/go.mod @@ -2,7 +2,7 @@ module github.com/cloudscale-ch/csi-cloudscale require ( github.com/cenkalti/backoff/v5 v5.0.3 - github.com/cloudscale-ch/cloudscale-go-sdk/v7 v7.0.0 + github.com/cloudscale-ch/cloudscale-go-sdk/v10 v10.0.0 github.com/container-storage-interface/spec v1.12.0 github.com/golang/protobuf v1.5.4 github.com/google/uuid v1.6.0 @@ -11,7 +11,7 @@ require ( github.com/sirupsen/logrus v1.9.4 github.com/stretchr/testify v1.11.1 golang.org/x/oauth2 v0.36.0 - golang.org/x/sys v0.45.0 + golang.org/x/sys v0.47.0 google.golang.org/grpc v1.83.1 google.golang.org/protobuf v1.36.11 k8s.io/api v0.28.15 @@ -24,7 +24,7 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.10.1 // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/logr v1.4.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.22.3 // indirect @@ -48,11 +48,12 @@ require ( github.com/onsi/gomega v1.36.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - golang.org/x/net v0.55.0 // indirect - golang.org/x/term v0.43.0 // indirect - golang.org/x/text v0.37.0 // indirect + go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/term v0.45.0 // indirect + golang.org/x/text v0.41.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.44.0 // indirect + golang.org/x/tools v0.48.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -127,4 +128,4 @@ replace k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.28.15 replace k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.28.15 -go 1.25.5 +go 1.27.0 diff --git a/go.sum b/go.sum index e59a4c9b..ff9627a6 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1x github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cloudscale-ch/cloudscale-go-sdk/v7 v7.0.0 h1:uHs90VA9fnAxfvbvo2Yz/wWTzhrEU2nJKRzBU9Rc6Ng= -github.com/cloudscale-ch/cloudscale-go-sdk/v7 v7.0.0/go.mod h1:CFpGwhqMUmhTk8wWvyc2qOq4pb35eq+7f0aAlRiMr2A= +github.com/cloudscale-ch/cloudscale-go-sdk/v10 v10.0.0 h1:Ir0/Ei9mU1OWBPDb95THR5H/Cu6WiXL1W/O67KS5YMM= +github.com/cloudscale-ch/cloudscale-go-sdk/v10 v10.0.0/go.mod h1:PbmyqFmr70walcN/nbphTlznb5XadWR4fRRmbd5tV+s= github.com/container-storage-interface/spec v1.12.0 h1:zrFOEqpR5AghNaaDG4qyedwPBqU2fU0dWjLQMP/azK0= github.com/container-storage-interface/spec v1.12.0/go.mod h1:txsm+MA2B2WDa5kW69jNbqPnvTtfvZma7T/zsAZ9qX8= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -14,8 +14,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emicklei/go-restful/v3 v3.10.1 h1:rc42Y5YTp7Am7CS630D7JmhRjq4UlEUuEKfrDac4bSQ= github.com/emicklei/go-restful/v3 v3.10.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= @@ -101,16 +101,16 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU= -go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc= -go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc= -go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo= -go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58= -go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0= -go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI= -go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= -go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= -go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= +go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU= +go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ= +go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M= +go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s= +go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw= +go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA= +go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o= +go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA= +go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag= +go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -122,8 +122,8 @@ 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-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= -golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -137,15 +137,15 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= -golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= -golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= +golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= +golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= -golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -153,8 +153,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= -golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= +golang.org/x/tools v0.48.0 h1:3+hClM1aLL5mjMKm5ovokw9epgRXPuu2tILgismM6RE= +golang.org/x/tools v0.48.0/go.mod h1:08xX0orndb/F7jJxGDicx061tyd5pcMto75YMAXr6lk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/scripts/update-k8s.sh b/scripts/update-k8s.sh index 6c1cd440..d1a10f7b 100755 --- a/scripts/update-k8s.sh +++ b/scripts/update-k8s.sh @@ -33,7 +33,6 @@ done < <(curl -fsSL "https://raw.githubusercontent.com/kubernetes/kubernetes/v$K | grep -E '^[[:space:]]*k8s.io.* v0.0.0$') unset GOROOT GOPATH -export GO111MODULE=on set -x # shellcheck disable=SC2086 diff --git a/test/kubernetes/integration_test.go b/test/kubernetes/integration_test.go index 5ec52a7e..581d732f 100644 --- a/test/kubernetes/integration_test.go +++ b/test/kubernetes/integration_test.go @@ -21,7 +21,7 @@ import ( "github.com/cloudscale-ch/csi-cloudscale/driver" - "github.com/cloudscale-ch/cloudscale-go-sdk/v7" + "github.com/cloudscale-ch/cloudscale-go-sdk/v10" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" snapshotclientset "github.com/kubernetes-csi/external-snapshotter/client/v6/clientset/versioned" "github.com/stretchr/testify/assert" diff --git a/tool.mod b/tool.mod deleted file mode 100644 index 2cc8599f..00000000 --- a/tool.mod +++ /dev/null @@ -1,216 +0,0 @@ -module github.com/cloudscale-ch/csi-cloudscale/tool - -go 1.25.5 - -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint - -require ( - 4d63.com/gocheckcompilerdirectives v1.3.0 // indirect - 4d63.com/gochecknoglobals v0.2.2 // indirect - codeberg.org/chavacava/garif v0.2.0 // indirect - codeberg.org/polyfloyd/go-errorlint v1.9.0 // indirect - dev.gaijin.team/go/exhaustruct/v4 v4.0.0 // indirect - dev.gaijin.team/go/golib v0.6.0 // indirect - github.com/4meepo/tagalign v1.4.3 // indirect - github.com/Abirdcfly/dupword v0.1.7 // indirect - github.com/AdminBenni/iota-mixing v1.0.0 // indirect - github.com/AlwxSin/noinlineerr v1.0.5 // indirect - github.com/Antonboom/errname v1.1.1 // indirect - github.com/Antonboom/nilnil v1.1.1 // indirect - github.com/Antonboom/testifylint v1.6.4 // indirect - github.com/BurntSushi/toml v1.6.0 // indirect - github.com/Djarvur/go-err113 v0.1.1 // indirect - github.com/Masterminds/semver/v3 v3.4.0 // indirect - github.com/MirrexOne/unqueryvet v1.4.0 // indirect - github.com/OpenPeeDeeP/depguard/v2 v2.2.1 // indirect - github.com/alecthomas/chroma/v2 v2.21.1 // indirect - github.com/alecthomas/go-check-sumtype v0.3.1 // indirect - github.com/alexkohler/nakedret/v2 v2.0.6 // indirect - github.com/alexkohler/prealloc v1.0.1 // indirect - github.com/alfatraining/structtag v1.0.0 // indirect - github.com/alingse/asasalint v0.0.11 // indirect - github.com/alingse/nilnesserr v0.2.0 // indirect - github.com/ashanbrown/forbidigo/v2 v2.3.0 // indirect - github.com/ashanbrown/makezero/v2 v2.1.0 // indirect - github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/bkielbasa/cyclop v1.2.3 // indirect - github.com/blizzy78/varnamelen v0.8.0 // indirect - github.com/bombsimon/wsl/v4 v4.7.0 // indirect - github.com/bombsimon/wsl/v5 v5.3.0 // indirect - github.com/breml/bidichk v0.3.3 // indirect - github.com/breml/errchkjson v0.4.1 // indirect - github.com/butuzov/ireturn v0.4.0 // indirect - github.com/butuzov/mirror v1.3.0 // indirect - github.com/catenacyber/perfsprint v0.10.1 // indirect - github.com/ccojocar/zxcvbn-go v1.0.4 // indirect - github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/charithe/durationcheck v0.0.11 // indirect - github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect - github.com/charmbracelet/lipgloss v1.1.0 // indirect - github.com/charmbracelet/x/ansi v0.8.0 // indirect - github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect - github.com/charmbracelet/x/term v0.2.1 // indirect - github.com/ckaznocha/intrange v0.3.1 // indirect - github.com/curioswitch/go-reassign v0.3.0 // indirect - github.com/daixiang0/gci v0.13.7 // indirect - github.com/dave/dst v0.27.3 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/denis-tingaikin/go-header v0.5.0 // indirect - github.com/dlclark/regexp2 v1.11.5 // indirect - github.com/ettle/strcase v0.2.0 // indirect - github.com/fatih/color v1.18.0 // indirect - github.com/fatih/structtag v1.2.0 // indirect - github.com/firefart/nonamedreturns v1.0.6 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/fzipp/gocyclo v0.6.0 // indirect - github.com/ghostiam/protogetter v0.3.18 // indirect - github.com/go-critic/go-critic v0.14.3 // indirect - github.com/go-toolsmith/astcast v1.1.0 // indirect - github.com/go-toolsmith/astcopy v1.1.0 // indirect - github.com/go-toolsmith/astequal v1.2.0 // indirect - github.com/go-toolsmith/astfmt v1.1.0 // indirect - github.com/go-toolsmith/astp v1.1.0 // indirect - github.com/go-toolsmith/strparse v1.1.0 // indirect - github.com/go-toolsmith/typep v1.1.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/go-xmlfmt/xmlfmt v1.1.3 // indirect - github.com/gobwas/glob v0.2.3 // indirect - github.com/godoc-lint/godoc-lint v0.11.1 // indirect - github.com/gofrs/flock v0.13.0 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/golangci/asciicheck v0.5.0 // indirect - github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 // indirect - github.com/golangci/go-printf-func-name v0.1.1 // indirect - github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d // indirect - github.com/golangci/golangci-lint/v2 v2.8.0 // indirect - github.com/golangci/golines v0.14.0 // indirect - github.com/golangci/misspell v0.7.0 // indirect - github.com/golangci/plugin-module-register v0.1.2 // indirect - github.com/golangci/revgrep v0.8.0 // indirect - github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e // indirect - github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/gordonklaus/ineffassign v0.2.0 // indirect - github.com/gostaticanalysis/analysisutil v0.7.1 // indirect - github.com/gostaticanalysis/comment v1.5.0 // indirect - github.com/gostaticanalysis/forcetypeassert v0.2.0 // indirect - github.com/gostaticanalysis/nilerr v0.1.2 // indirect - github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect - github.com/hashicorp/go-version v1.8.0 // indirect - github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/hexops/gotextdiff v1.0.3 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jgautheron/goconst v1.8.2 // indirect - github.com/jingyugao/rowserrcheck v1.1.1 // indirect - github.com/jjti/go-spancheck v0.6.5 // indirect - github.com/julz/importas v0.2.0 // indirect - github.com/karamaru-alpha/copyloopvar v1.2.2 // indirect - github.com/kisielk/errcheck v1.9.0 // indirect - github.com/kkHAIKE/contextcheck v1.1.6 // indirect - github.com/kulti/thelper v0.7.1 // indirect - github.com/kunwardeep/paralleltest v1.0.15 // indirect - github.com/lasiar/canonicalheader v1.1.2 // indirect - github.com/ldez/exptostd v0.4.5 // indirect - github.com/ldez/gomoddirectives v0.8.0 // indirect - github.com/ldez/grignotin v0.10.1 // indirect - github.com/ldez/structtags v0.6.1 // indirect - github.com/ldez/tagliatelle v0.7.2 // indirect - github.com/ldez/usetesting v0.5.0 // indirect - github.com/leonklingele/grouper v1.1.2 // indirect - github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/macabu/inamedparam v0.2.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect - github.com/manuelarte/embeddedstructfieldcheck v0.4.0 // indirect - github.com/manuelarte/funcorder v0.5.0 // indirect - github.com/maratori/testableexamples v1.0.1 // indirect - github.com/maratori/testpackage v1.1.2 // indirect - github.com/matoous/godox v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.14 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect - github.com/mgechev/revive v1.13.0 // indirect - github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/moricho/tparallel v0.3.2 // indirect - github.com/muesli/termenv v0.16.0 // indirect - github.com/nakabonne/nestif v0.3.1 // indirect - github.com/nishanths/exhaustive v0.12.0 // indirect - github.com/nishanths/predeclared v0.2.2 // indirect - github.com/nunnatsa/ginkgolinter v0.21.2 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/prometheus/client_golang v1.12.1 // indirect - github.com/prometheus/client_model v0.2.0 // indirect - github.com/prometheus/common v0.32.1 // indirect - github.com/prometheus/procfs v0.7.3 // indirect - github.com/quasilyte/go-ruleguard v0.4.5 // indirect - github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect - github.com/quasilyte/gogrep v0.5.0 // indirect - github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect - github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect - github.com/raeperd/recvcheck v0.2.0 // indirect - github.com/rivo/uniseg v0.4.7 // indirect - github.com/rogpeppe/go-internal v1.14.1 // indirect - github.com/ryancurrah/gomodguard v1.4.1 // indirect - github.com/ryanrolds/sqlclosecheck v0.5.1 // indirect - github.com/sanposhiho/wastedassign/v2 v2.1.0 // indirect - github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect - github.com/sashamelentyev/interfacebloat v1.1.0 // indirect - github.com/sashamelentyev/usestdlibvars v1.29.0 // indirect - github.com/securego/gosec/v2 v2.22.11 // indirect - github.com/sirupsen/logrus v1.9.3 // indirect - github.com/sivchari/containedctx v1.0.3 // indirect - github.com/sonatard/noctx v0.4.0 // indirect - github.com/sourcegraph/go-diff v0.7.0 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/cobra v1.10.2 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/spf13/viper v1.12.0 // indirect - github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect - github.com/stbenjam/no-sprintf-host-port v0.3.1 // indirect - github.com/stretchr/objx v0.5.2 // indirect - github.com/stretchr/testify v1.11.1 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - github.com/tetafro/godot v1.5.4 // indirect - github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 // indirect - github.com/timonwong/loggercheck v0.11.0 // indirect - github.com/tomarrell/wrapcheck/v2 v2.12.0 // indirect - github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect - github.com/ultraware/funlen v0.2.0 // indirect - github.com/ultraware/whitespace v0.2.0 // indirect - github.com/uudashr/gocognit v1.2.0 // indirect - github.com/uudashr/iface v1.4.1 // indirect - github.com/xen0n/gosmopolitan v1.3.0 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - github.com/yagipy/maintidx v1.0.0 // indirect - github.com/yeya24/promlinter v0.3.0 // indirect - github.com/ykadowak/zerologlint v0.1.5 // indirect - gitlab.com/bosi/decorder v0.4.2 // indirect - go-simpler.org/musttag v0.14.0 // indirect - go-simpler.org/sloglint v0.11.1 // indirect - go.augendre.info/arangolint v0.3.1 // indirect - go.augendre.info/fatcontext v0.9.0 // indirect - go.uber.org/automaxprocs v1.6.0 // indirect - go.uber.org/multierr v1.10.0 // indirect - go.uber.org/zap v1.27.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 // indirect - golang.org/x/mod v0.31.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.31.0 // indirect - golang.org/x/tools v0.40.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect - honnef.co/go/tools v0.6.1 // indirect - mvdan.cc/gofumpt v0.9.2 // indirect - mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 // indirect -) diff --git a/tool.sum b/tool.sum deleted file mode 100644 index cf6f305b..00000000 --- a/tool.sum +++ /dev/null @@ -1,949 +0,0 @@ -4d63.com/gocheckcompilerdirectives v1.3.0 h1:Ew5y5CtcAAQeTVKUVFrE7EwHMrTO6BggtEj8BZSjZ3A= -4d63.com/gocheckcompilerdirectives v1.3.0/go.mod h1:ofsJ4zx2QAuIP/NO/NAh1ig6R1Fb18/GI7RVMwz7kAY= -4d63.com/gochecknoglobals v0.2.2 h1:H1vdnwnMaZdQW/N+NrkT1SZMTBmcwHe9Vq8lJcYYTtU= -4d63.com/gochecknoglobals v0.2.2/go.mod h1:lLxwTQjL5eIesRbvnzIP3jZtG140FnTdz+AlMa+ogt0= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -codeberg.org/chavacava/garif v0.2.0 h1:F0tVjhYbuOCnvNcU3YSpO6b3Waw6Bimy4K0mM8y6MfY= -codeberg.org/chavacava/garif v0.2.0/go.mod h1:P2BPbVbT4QcvLZrORc2T29szK3xEOlnl0GiPTJmEqBQ= -codeberg.org/polyfloyd/go-errorlint v1.9.0 h1:VkdEEmA1VBpH6ecQoMR4LdphVI3fA4RrCh2an7YmodI= -codeberg.org/polyfloyd/go-errorlint v1.9.0/go.mod h1:GPRRu2LzVijNn4YkrZYJfatQIdS+TrcK8rL5Xs24qw8= -dev.gaijin.team/go/exhaustruct/v4 v4.0.0 h1:873r7aNneqoBB3IaFIzhvt2RFYTuHgmMjoKfwODoI1Y= -dev.gaijin.team/go/exhaustruct/v4 v4.0.0/go.mod h1:aZ/k2o4Y05aMJtiux15x8iXaumE88YdiB0Ai4fXOzPI= -dev.gaijin.team/go/golib v0.6.0 h1:v6nnznFTs4bppib/NyU1PQxobwDHwCXXl15P7DV5Zgo= -dev.gaijin.team/go/golib v0.6.0/go.mod h1:uY1mShx8Z/aNHWDyAkZTkX+uCi5PdX7KsG1eDQa2AVE= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/4meepo/tagalign v1.4.3 h1:Bnu7jGWwbfpAie2vyl63Zup5KuRv21olsPIha53BJr8= -github.com/4meepo/tagalign v1.4.3/go.mod h1:00WwRjiuSbrRJnSVeGWPLp2epS5Q/l4UEy0apLLS37c= -github.com/Abirdcfly/dupword v0.1.7 h1:2j8sInznrje4I0CMisSL6ipEBkeJUJAmK1/lfoNGWrQ= -github.com/Abirdcfly/dupword v0.1.7/go.mod h1:K0DkBeOebJ4VyOICFdppB23Q0YMOgVafM0zYW0n9lF4= -github.com/AdminBenni/iota-mixing v1.0.0 h1:Os6lpjG2dp/AE5fYBPAA1zfa2qMdCAWwPMCgpwKq7wo= -github.com/AdminBenni/iota-mixing v1.0.0/go.mod h1:i4+tpAaB+qMVIV9OK3m4/DAynOd5bQFaOu+2AhtBCNY= -github.com/AlwxSin/noinlineerr v1.0.5 h1:RUjt63wk1AYWTXtVXbSqemlbVTb23JOSRiNsshj7TbY= -github.com/AlwxSin/noinlineerr v1.0.5/go.mod h1:+QgkkoYrMH7RHvcdxdlI7vYYEdgeoFOVjU9sUhw/rQc= -github.com/Antonboom/errname v1.1.1 h1:bllB7mlIbTVzO9jmSWVWLjxTEbGBVQ1Ff/ClQgtPw9Q= -github.com/Antonboom/errname v1.1.1/go.mod h1:gjhe24xoxXp0ScLtHzjiXp0Exi1RFLKJb0bVBtWKCWQ= -github.com/Antonboom/nilnil v1.1.1 h1:9Mdr6BYd8WHCDngQnNVV0b554xyisFioEKi30sksufQ= -github.com/Antonboom/nilnil v1.1.1/go.mod h1:yCyAmSw3doopbOWhJlVci+HuyNRuHJKIv6V2oYQa8II= -github.com/Antonboom/testifylint v1.6.4 h1:gs9fUEy+egzxkEbq9P4cpcMB6/G0DYdMeiFS87UiqmQ= -github.com/Antonboom/testifylint v1.6.4/go.mod h1:YO33FROXX2OoUfwjz8g+gUxQXio5i9qpVy7nXGbxDD4= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= -github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= -github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= -github.com/MirrexOne/unqueryvet v1.4.0 h1:6KAkqqW2KUnkl9Z0VuTphC3IXRPoFqEkJEtyxxHj5eQ= -github.com/MirrexOne/unqueryvet v1.4.0/go.mod h1:IWwCwMQlSWjAIteW0t+28Q5vouyktfujzYznSIWiuOg= -github.com/OpenPeeDeeP/depguard/v2 v2.2.1 h1:vckeWVESWp6Qog7UZSARNqfu/cZqvki8zsuj3piCMx4= -github.com/OpenPeeDeeP/depguard/v2 v2.2.1/go.mod h1:q4DKzC4UcVaAvcfd41CZh0PWpGgzrVxUYBlgKNGquUo= -github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA= -github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o= -github.com/alecthomas/go-check-sumtype v0.3.1 h1:u9aUvbGINJxLVXiFvHUlPEaD7VDULsrxJb4Aq31NLkU= -github.com/alecthomas/go-check-sumtype v0.3.1/go.mod h1:A8TSiN3UPRw3laIgWEUOHHLPa6/r9MtoigdlP5h3K/E= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/alexkohler/nakedret/v2 v2.0.6 h1:ME3Qef1/KIKr3kWX3nti3hhgNxw6aqN5pZmQiFSsuzQ= -github.com/alexkohler/nakedret/v2 v2.0.6/go.mod h1:l3RKju/IzOMQHmsEvXwkqMDzHHvurNQfAgE1eVmT40Q= -github.com/alexkohler/prealloc v1.0.1 h1:A9P1haqowqUxWvU9nk6tQ7YktXIHf+LQM9wPRhuteEE= -github.com/alexkohler/prealloc v1.0.1/go.mod h1:fT39Jge3bQrfA7nPMDngUfvUbQGQeJyGQnR+913SCig= -github.com/alfatraining/structtag v1.0.0 h1:2qmcUqNcCoyVJ0up879K614L9PazjBSFruTB0GOFjCc= -github.com/alfatraining/structtag v1.0.0/go.mod h1:p3Xi5SwzTi+Ryj64DqjLWz7XurHxbGsq6y3ubePJPus= -github.com/alingse/asasalint v0.0.11 h1:SFwnQXJ49Kx/1GghOFz1XGqHYKp21Kq1nHad/0WQRnw= -github.com/alingse/asasalint v0.0.11/go.mod h1:nCaoMhw7a9kSJObvQyVzNTPBDbNpdocqrSP7t/cW5+I= -github.com/alingse/nilnesserr v0.2.0 h1:raLem5KG7EFVb4UIDAXgrv3N2JIaffeKNtcEXkEWd/w= -github.com/alingse/nilnesserr v0.2.0/go.mod h1:1xJPrXonEtX7wyTq8Dytns5P2hNzoWymVUIaKm4HNFg= -github.com/ashanbrown/forbidigo/v2 v2.3.0 h1:OZZDOchCgsX5gvToVtEBoV2UWbFfI6RKQTir2UZzSxo= -github.com/ashanbrown/forbidigo/v2 v2.3.0/go.mod h1:5p6VmsG5/1xx3E785W9fouMxIOkvY2rRV9nMdWadd6c= -github.com/ashanbrown/makezero/v2 v2.1.0 h1:snuKYMbqosNokUKm+R6/+vOPs8yVAi46La7Ck6QYSaE= -github.com/ashanbrown/makezero/v2 v2.1.0/go.mod h1:aEGT/9q3S8DHeE57C88z2a6xydvgx8J5hgXIGWgo0MY= -github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= -github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bkielbasa/cyclop v1.2.3 h1:faIVMIGDIANuGPWH031CZJTi2ymOQBULs9H21HSMa5w= -github.com/bkielbasa/cyclop v1.2.3/go.mod h1:kHTwA9Q0uZqOADdupvcFJQtp/ksSnytRMe8ztxG8Fuo= -github.com/blizzy78/varnamelen v0.8.0 h1:oqSblyuQvFsW1hbBHh1zfwrKe3kcSj0rnXkKzsQ089M= -github.com/blizzy78/varnamelen v0.8.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k= -github.com/bombsimon/wsl/v4 v4.7.0 h1:1Ilm9JBPRczjyUs6hvOPKvd7VL1Q++PL8M0SXBDf+jQ= -github.com/bombsimon/wsl/v4 v4.7.0/go.mod h1:uV/+6BkffuzSAVYD+yGyld1AChO7/EuLrCF/8xTiapg= -github.com/bombsimon/wsl/v5 v5.3.0 h1:nZWREJFL6U3vgW/B1lfDOigl+tEF6qgs6dGGbFeR0UM= -github.com/bombsimon/wsl/v5 v5.3.0/go.mod h1:Gp8lD04z27wm3FANIUPZycXp+8huVsn0oxc+n4qfV9I= -github.com/breml/bidichk v0.3.3 h1:WSM67ztRusf1sMoqH6/c4OBCUlRVTKq+CbSeo0R17sE= -github.com/breml/bidichk v0.3.3/go.mod h1:ISbsut8OnjB367j5NseXEGGgO/th206dVa427kR8YTE= -github.com/breml/errchkjson v0.4.1 h1:keFSS8D7A2T0haP9kzZTi7o26r7kE3vymjZNeNDRDwg= -github.com/breml/errchkjson v0.4.1/go.mod h1:a23OvR6Qvcl7DG/Z4o0el6BRAjKnaReoPQFciAl9U3s= -github.com/butuzov/ireturn v0.4.0 h1:+s76bF/PfeKEdbG8b54aCocxXmi0wvYdOVsWxVO7n8E= -github.com/butuzov/ireturn v0.4.0/go.mod h1:ghI0FrCmap8pDWZwfPisFD1vEc56VKH4NpQUxDHta70= -github.com/butuzov/mirror v1.3.0 h1:HdWCXzmwlQHdVhwvsfBb2Au0r3HyINry3bDWLYXiKoc= -github.com/butuzov/mirror v1.3.0/go.mod h1:AEij0Z8YMALaq4yQj9CPPVYOyJQyiexpQEQgihajRfI= -github.com/catenacyber/perfsprint v0.10.1 h1:u7Riei30bk46XsG8nknMhKLXG9BcXz3+3tl/WpKm0PQ= -github.com/catenacyber/perfsprint v0.10.1/go.mod h1:DJTGsi/Zufpuus6XPGJyKOTMELe347o6akPvWG9Zcsc= -github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= -github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/charithe/durationcheck v0.0.11 h1:g1/EX1eIiKS57NTWsYtHDZ/APfeXKhye1DidBcABctk= -github.com/charithe/durationcheck v0.0.11/go.mod h1:x5iZaixRNl8ctbM+3B2RrPG5t856TxRyVQEnbIEM2X4= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= -github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= -github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= -github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= -github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE= -github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= -github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= -github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= -github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/ckaznocha/intrange v0.3.1 h1:j1onQyXvHUsPWujDH6WIjhyH26gkRt/txNlV7LspvJs= -github.com/ckaznocha/intrange v0.3.1/go.mod h1:QVepyz1AkUoFQkpEqksSYpNpUo3c5W7nWh/s6SHIJJk= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/curioswitch/go-reassign v0.3.0 h1:dh3kpQHuADL3cobV/sSGETA8DOv457dwl+fbBAhrQPs= -github.com/curioswitch/go-reassign v0.3.0/go.mod h1:nApPCCTtqLJN/s8HfItCcKV0jIPwluBOvZP+dsJGA88= -github.com/daixiang0/gci v0.13.7 h1:+0bG5eK9vlI08J+J/NWGbWPTNiXPG4WhNLJOkSxWITQ= -github.com/daixiang0/gci v0.13.7/go.mod h1:812WVN6JLFY9S6Tv76twqmNqevN0pa3SX3nih0brVzQ= -github.com/dave/dst v0.27.3 h1:P1HPoMza3cMEquVf9kKy8yXsFirry4zEnWOdYPOoIzY= -github.com/dave/dst v0.27.3/go.mod h1:jHh6EOibnHgcUW3WjKHisiooEkYwqpHLBSX1iOBhEyc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/denis-tingaikin/go-header v0.5.0 h1:SRdnP5ZKvcO9KKRP1KJrhFR3RrlGuD+42t4429eC9k8= -github.com/denis-tingaikin/go-header v0.5.0/go.mod h1:mMenU5bWrok6Wl2UsZjy+1okegmwQ3UgWl4V1D8gjlY= -github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= -github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/ettle/strcase v0.2.0 h1:fGNiVF21fHXpX1niBgk0aROov1LagYsOwV/xqKDKR/Q= -github.com/ettle/strcase v0.2.0/go.mod h1:DajmHElDSaX76ITe3/VHVyMin4LWSJN5Z909Wp+ED1A= -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/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4= -github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94= -github.com/firefart/nonamedreturns v1.0.6 h1:vmiBcKV/3EqKY3ZiPxCINmpS431OcE1S47AQUwhrg8E= -github.com/firefart/nonamedreturns v1.0.6/go.mod h1:R8NisJnSIpvPWheCq0mNRXJok6D8h7fagJTF8EMEwCo= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= -github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo= -github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA= -github.com/ghostiam/protogetter v0.3.18 h1:yEpghRGtP9PjKvVXtEzGpYfQj1Wl/ZehAfU6fr62Lfo= -github.com/ghostiam/protogetter v0.3.18/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI= -github.com/go-critic/go-critic v0.14.3 h1:5R1qH2iFeo4I/RJU8vTezdqs08Egi4u5p6vOESA0pog= -github.com/go-critic/go-critic v0.14.3/go.mod h1:xwntfW6SYAd7h1OqDzmN6hBX/JxsEKl5up/Y2bsxgVQ= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= -github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-toolsmith/astcast v1.1.0 h1:+JN9xZV1A+Re+95pgnMgDboWNVnIMMQXwfBwLRPgSC8= -github.com/go-toolsmith/astcast v1.1.0/go.mod h1:qdcuFWeGGS2xX5bLM/c3U9lewg7+Zu4mr+xPwZIB4ZU= -github.com/go-toolsmith/astcopy v1.1.0 h1:YGwBN0WM+ekI/6SS6+52zLDEf8Yvp3n2seZITCUBt5s= -github.com/go-toolsmith/astcopy v1.1.0/go.mod h1:hXM6gan18VA1T/daUEHCFcYiW8Ai1tIwIzHY6srfEAw= -github.com/go-toolsmith/astequal v1.0.3/go.mod h1:9Ai4UglvtR+4up+bAD4+hCj7iTo4m/OXVTSLnCyTAx4= -github.com/go-toolsmith/astequal v1.1.0/go.mod h1:sedf7VIdCL22LD8qIvv7Nn9MuWJruQA/ysswh64lffQ= -github.com/go-toolsmith/astequal v1.2.0 h1:3Fs3CYZ1k9Vo4FzFhwwewC3CHISHDnVUPC4x0bI2+Cw= -github.com/go-toolsmith/astequal v1.2.0/go.mod h1:c8NZ3+kSFtFY/8lPso4v8LuJjdJiUFVnSuU3s0qrrDY= -github.com/go-toolsmith/astfmt v1.1.0 h1:iJVPDPp6/7AaeLJEruMsBUlOYCmvg0MoCfJprsOmcco= -github.com/go-toolsmith/astfmt v1.1.0/go.mod h1:OrcLlRwu0CuiIBp/8b5PYF9ktGVZUjlNMV634mhwuQ4= -github.com/go-toolsmith/astp v1.1.0 h1:dXPuCl6u2llURjdPLLDxJeZInAeZ0/eZwFJmqZMnpQA= -github.com/go-toolsmith/astp v1.1.0/go.mod h1:0T1xFGz9hicKs8Z5MfAqSUitoUYS30pDMsRVIDHs8CA= -github.com/go-toolsmith/strparse v1.0.0/go.mod h1:YI2nUKP9YGZnL/L1/DLFBfixrcjslWct4wyljWhSRy8= -github.com/go-toolsmith/strparse v1.1.0 h1:GAioeZUK9TGxnLS+qfdqNbA4z0SSm5zVNtCQiyP2Bvw= -github.com/go-toolsmith/strparse v1.1.0/go.mod h1:7ksGy58fsaQkGQlY8WVoBFNyEPMGuJin1rfoPS4lBSQ= -github.com/go-toolsmith/typep v1.1.0 h1:fIRYDyF+JywLfqzyhdiHzRop/GQDxxNhLGQ6gFUNHus= -github.com/go-toolsmith/typep v1.1.0/go.mod h1:fVIw+7zjdsMxDA3ITWnH1yOiw1rnTQKCsF/sk2H/qig= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/go-xmlfmt/xmlfmt v1.1.3 h1:t8Ey3Uy7jDSEisW2K3somuMKIpzktkWptA0iFCnRUWY= -github.com/go-xmlfmt/xmlfmt v1.1.3/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM= -github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= -github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/godoc-lint/godoc-lint v0.11.1 h1:z9as8Qjiy6miRIa3VRymTa+Gt2RLnGICVikcvlUVOaA= -github.com/godoc-lint/godoc-lint v0.11.1/go.mod h1:BAqayheFSuZrEAqCRxgw9MyvsM+S/hZwJbU1s/ejRj8= -github.com/gofrs/flock v0.13.0 h1:95JolYOvGMqeH31+FC7D2+uULf6mG61mEZ/A8dRYMzw= -github.com/gofrs/flock v0.13.0/go.mod h1:jxeyy9R1auM5S6JYDBhDt+E2TCo7DkratH4Pgi8P+Z0= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -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/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/golangci/asciicheck v0.5.0 h1:jczN/BorERZwK8oiFBOGvlGPknhvq0bjnysTj4nUfo0= -github.com/golangci/asciicheck v0.5.0/go.mod h1:5RMNAInbNFw2krqN6ibBxN/zfRFa9S6tA1nPdM0l8qQ= -github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32 h1:WUvBfQL6EW/40l6OmeSBYQJNSif4O11+bmWEz+C7FYw= -github.com/golangci/dupl v0.0.0-20250308024227-f665c8d69b32/go.mod h1:NUw9Zr2Sy7+HxzdjIULge71wI6yEg1lWQr7Evcu8K0E= -github.com/golangci/go-printf-func-name v0.1.1 h1:hIYTFJqAGp1iwoIfsNTpoq1xZAarogrvjO9AfiW3B4U= -github.com/golangci/go-printf-func-name v0.1.1/go.mod h1:Es64MpWEZbh0UBtTAICOZiB+miW53w/K9Or/4QogJss= -github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d h1:viFft9sS/dxoYY0aiOTsLKO2aZQAPT4nlQCsimGcSGE= -github.com/golangci/gofmt v0.0.0-20250106114630-d62b90e6713d/go.mod h1:ivJ9QDg0XucIkmwhzCDsqcnxxlDStoTl89jDMIoNxKY= -github.com/golangci/golangci-lint/v2 v2.8.0 h1:wJnr3hJWY3eVzOUcfwbDc2qbi2RDEpvLmQeNFaPSNYA= -github.com/golangci/golangci-lint/v2 v2.8.0/go.mod h1:xl+HafQ9xoP8rzw0z5AwnO5kynxtb80e8u02Ej/47RI= -github.com/golangci/golines v0.14.0 h1:xt9d3RKBjhasA3qpoXs99J2xN2t6eBlpLHt0TrgyyXc= -github.com/golangci/golines v0.14.0/go.mod h1:gf555vPG2Ia7mmy2mzmhVQbVjuK8Orw0maR1G4vVAAQ= -github.com/golangci/misspell v0.7.0 h1:4GOHr/T1lTW0hhR4tgaaV1WS/lJ+ncvYCoFKmqJsj0c= -github.com/golangci/misspell v0.7.0/go.mod h1:WZyyI2P3hxPY2UVHs3cS8YcllAeyfquQcKfdeE9AFVg= -github.com/golangci/plugin-module-register v0.1.2 h1:e5WM6PO6NIAEcij3B053CohVp3HIYbzSuP53UAYgOpg= -github.com/golangci/plugin-module-register v0.1.2/go.mod h1:1+QGTsKBvAIvPvoY/os+G5eoqxWn70HYDm2uvUyGuVw= -github.com/golangci/revgrep v0.8.0 h1:EZBctwbVd0aMeRnNUsFogoyayvKHyxlV3CdUA46FX2s= -github.com/golangci/revgrep v0.8.0/go.mod h1:U4R/s9dlXZsg8uJmaR1GrloUr14D7qDl8gi2iPXJH8k= -github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e h1:ai0EfmVYE2bRA5htgAG9r7s3tHsfjIhN98WshBTJ9jM= -github.com/golangci/swaggoswag v0.0.0-20250504205917-77f2aca3143e/go.mod h1:Vrn4B5oR9qRwM+f54koyeH3yzphlecwERs0el27Fr/s= -github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e h1:gD6P7NEo7Eqtt0ssnqSJNNndxe69DOQ24A5h7+i3KpM= -github.com/golangci/unconvert v0.0.0-20250410112200-a129a6e6413e/go.mod h1:h+wZwLjUTJnm/P2rwlbJdRPZXOzaT36/FwnPnY2inzc= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -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= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gordonklaus/ineffassign v0.2.0 h1:Uths4KnmwxNJNzq87fwQQDDnbNb7De00VOk9Nu0TySs= -github.com/gordonklaus/ineffassign v0.2.0/go.mod h1:TIpymnagPSexySzs7F9FnO1XFTy8IT3a59vmZp5Y9Lw= -github.com/gostaticanalysis/analysisutil v0.7.1 h1:ZMCjoue3DtDWQ5WyU16YbjbQEQ3VuzwxALrpYd+HeKk= -github.com/gostaticanalysis/analysisutil v0.7.1/go.mod h1:v21E3hY37WKMGSnbsw2S/ojApNWb6C1//mXO48CXbVc= -github.com/gostaticanalysis/comment v1.4.2/go.mod h1:KLUTGDv6HOCotCH8h2erHKmpci2ZoR8VPu34YA2uzdM= -github.com/gostaticanalysis/comment v1.5.0 h1:X82FLl+TswsUMpMh17srGRuKaaXprTaytmEpgnKIDu8= -github.com/gostaticanalysis/comment v1.5.0/go.mod h1:V6eb3gpCv9GNVqb6amXzEUX3jXLVK/AdA+IrAMSqvEc= -github.com/gostaticanalysis/forcetypeassert v0.2.0 h1:uSnWrrUEYDr86OCxWa4/Tp2jeYDlogZiZHzGkWFefTk= -github.com/gostaticanalysis/forcetypeassert v0.2.0/go.mod h1:M5iPavzE9pPqWyeiVXSFghQjljW1+l/Uke3PXHS6ILY= -github.com/gostaticanalysis/nilerr v0.1.2 h1:S6nk8a9N8g062nsx63kUkF6AzbHGw7zzyHMcpu52xQU= -github.com/gostaticanalysis/nilerr v0.1.2/go.mod h1:A19UHhoY3y8ahoL7YKz6sdjDtduwTSI4CsymaC2htPA= -github.com/gostaticanalysis/testutil v0.3.1-0.20210208050101-bfb5c8eec0e4/go.mod h1:D+FIZ+7OahH3ePw/izIEeH5I06eKs1IKI4Xr64/Am3M= -github.com/hashicorp/go-immutable-radix/v2 v2.1.0 h1:CUW5RYIcysz+D3B+l1mDeXrQ7fUvGGCwJfdASSzbrfo= -github.com/hashicorp/go-immutable-radix/v2 v2.1.0/go.mod h1:hgdqLXA4f6NIjRVisM1TJ9aOJVNRqKZj+xDGF6m7PBw= -github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.8.0 h1:KAkNb1HAiZd1ukkxDFGmokVZe1Xy9HG6NUp+bPle2i4= -github.com/hashicorp/go-version v1.8.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= -github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= -github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jgautheron/goconst v1.8.2 h1:y0XF7X8CikZ93fSNT6WBTb/NElBu9IjaY7CCYQrCMX4= -github.com/jgautheron/goconst v1.8.2/go.mod h1:A0oxgBCHy55NQn6sYpO7UdnA9p+h7cPtoOZUmvNIako= -github.com/jingyugao/rowserrcheck v1.1.1 h1:zibz55j/MJtLsjP1OF4bSdgXxwL1b+Vn7Tjzq7gFzUs= -github.com/jingyugao/rowserrcheck v1.1.1/go.mod h1:4yvlZSDb3IyDTUZJUmpZfm2Hwok+Dtp+nu2qOq+er9c= -github.com/jjti/go-spancheck v0.6.5 h1:lmi7pKxa37oKYIMScialXUK6hP3iY5F1gu+mLBPgYB8= -github.com/jjti/go-spancheck v0.6.5/go.mod h1:aEogkeatBrbYsyW6y5TgDfihCulDYciL1B7rG2vSsrU= -github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/julz/importas v0.2.0 h1:y+MJN/UdL63QbFJHws9BVC5RpA2iq0kpjrFajTGivjQ= -github.com/julz/importas v0.2.0/go.mod h1:pThlt589EnCYtMnmhmRYY/qn9lCf/frPOK+WMx3xiJY= -github.com/karamaru-alpha/copyloopvar v1.2.2 h1:yfNQvP9YaGQR7VaWLYcfZUlRP2eo2vhExWKxD/fP6q0= -github.com/karamaru-alpha/copyloopvar v1.2.2/go.mod h1:oY4rGZqZ879JkJMtX3RRkcXRkmUvH0x35ykgaKgsgJY= -github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= -github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE= -github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -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/kulti/thelper v0.7.1 h1:fI8QITAoFVLx+y+vSyuLBP+rcVIB8jKooNSCT2EiI98= -github.com/kulti/thelper v0.7.1/go.mod h1:NsMjfQEy6sd+9Kfw8kCP61W1I0nerGSYSFnGaxQkcbs= -github.com/kunwardeep/paralleltest v1.0.15 h1:ZMk4Qt306tHIgKISHWFJAO1IDQJLc6uDyJMLyncOb6w= -github.com/kunwardeep/paralleltest v1.0.15/go.mod h1:di4moFqtfz3ToSKxhNjhOZL+696QtJGCFe132CbBLGk= -github.com/lasiar/canonicalheader v1.1.2 h1:vZ5uqwvDbyJCnMhmFYimgMZnJMjwljN5VGY0VKbMXb4= -github.com/lasiar/canonicalheader v1.1.2/go.mod h1:qJCeLFS0G/QlLQ506T+Fk/fWMa2VmBUiEI2cuMK4djI= -github.com/ldez/exptostd v0.4.5 h1:kv2ZGUVI6VwRfp/+bcQ6Nbx0ghFWcGIKInkG/oFn1aQ= -github.com/ldez/exptostd v0.4.5/go.mod h1:QRjHRMXJrCTIm9WxVNH6VW7oN7KrGSht69bIRwvdFsM= -github.com/ldez/gomoddirectives v0.8.0 h1:JqIuTtgvFC2RdH1s357vrE23WJF2cpDCPFgA/TWDGpk= -github.com/ldez/gomoddirectives v0.8.0/go.mod h1:jutzamvZR4XYJLr0d5Honycp4Gy6GEg2mS9+2YX3F1Q= -github.com/ldez/grignotin v0.10.1 h1:keYi9rYsgbvqAZGI1liek5c+jv9UUjbvdj3Tbn5fn4o= -github.com/ldez/grignotin v0.10.1/go.mod h1:UlDbXFCARrXbWGNGP3S5vsysNXAPhnSuBufpTEbwOas= -github.com/ldez/structtags v0.6.1 h1:bUooFLbXx41tW8SvkfwfFkkjPYvFFs59AAMgVg6DUBk= -github.com/ldez/structtags v0.6.1/go.mod h1:YDxVSgDy/MON6ariaxLF2X09bh19qL7MtGBN5MrvbdY= -github.com/ldez/tagliatelle v0.7.2 h1:KuOlL70/fu9paxuxbeqlicJnCspCRjH0x8FW+NfgYUk= -github.com/ldez/tagliatelle v0.7.2/go.mod h1:PtGgm163ZplJfZMZ2sf5nhUT170rSuPgBimoyYtdaSI= -github.com/ldez/usetesting v0.5.0 h1:3/QtzZObBKLy1F4F8jLuKJiKBjjVFi1IavpoWbmqLwc= -github.com/ldez/usetesting v0.5.0/go.mod h1:Spnb4Qppf8JTuRgblLrEWb7IE6rDmUpGvxY3iRrzvDQ= -github.com/leonklingele/grouper v1.1.2 h1:o1ARBDLOmmasUaNDesWqWCIFH3u7hoFlM84YrjT3mIY= -github.com/leonklingele/grouper v1.1.2/go.mod h1:6D0M/HVkhs2yRKRFZUoGjeDy7EZTfFBE9gl4kjmIGkA= -github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= -github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/macabu/inamedparam v0.2.0 h1:VyPYpOc10nkhI2qeNUdh3Zket4fcZjEWe35poddBCpE= -github.com/macabu/inamedparam v0.2.0/go.mod h1:+Pee9/YfGe5LJ62pYXqB89lJ+0k5bsR8Wgz/C0Zlq3U= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= -github.com/manuelarte/embeddedstructfieldcheck v0.4.0 h1:3mAIyaGRtjK6EO9E73JlXLtiy7ha80b2ZVGyacxgfww= -github.com/manuelarte/embeddedstructfieldcheck v0.4.0/go.mod h1:z8dFSyXqp+fC6NLDSljRJeNQJJDWnY7RoWFzV3PC6UM= -github.com/manuelarte/funcorder v0.5.0 h1:llMuHXXbg7tD0i/LNw8vGnkDTHFpTnWqKPI85Rknc+8= -github.com/manuelarte/funcorder v0.5.0/go.mod h1:Yt3CiUQthSBMBxjShjdXMexmzpP8YGvGLjrxJNkO2hA= -github.com/maratori/testableexamples v1.0.1 h1:HfOQXs+XgfeRBJ+Wz0XfH+FHnoY9TVqL6Fcevpzy4q8= -github.com/maratori/testableexamples v1.0.1/go.mod h1:XE2F/nQs7B9N08JgyRmdGjYVGqxWwClLPCGSQhXQSrQ= -github.com/maratori/testpackage v1.1.2 h1:ffDSh+AgqluCLMXhM19f/cpvQAKygKAJXFl9aUjmbqs= -github.com/maratori/testpackage v1.1.2/go.mod h1:8F24GdVDFW5Ew43Et02jamrVMNXLUNaOynhDssITGfc= -github.com/matoous/godox v1.1.0 h1:W5mqwbyWrwZv6OQ5Z1a/DHGMOvXYCBP3+Ht7KMoJhq4= -github.com/matoous/godox v1.1.0/go.mod h1:jgE/3fUXiTurkdHOLT5WEkThTSuE7yxHv5iWPa80afs= -github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= -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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= -github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/mgechev/revive v1.13.0 h1:yFbEVliCVKRXY8UgwEO7EOYNopvjb1BFbmYqm9hZjBM= -github.com/mgechev/revive v1.13.0/go.mod h1:efJfeBVCX2JUumNQ7dtOLDja+QKj9mYGgEZA7rt5u+0= -github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= -github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/moricho/tparallel v0.3.2 h1:odr8aZVFA3NZrNybggMkYO3rgPRcqjeQUlBBFVxKHTI= -github.com/moricho/tparallel v0.3.2/go.mod h1:OQ+K3b4Ln3l2TZveGCywybl68glfLEwFGqvnjok8b+U= -github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= -github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/nakabonne/nestif v0.3.1 h1:wm28nZjhQY5HyYPx+weN3Q65k6ilSBxDb8v5S81B81U= -github.com/nakabonne/nestif v0.3.1/go.mod h1:9EtoZochLn5iUprVDmDjqGKPofoUEBL8U4Ngq6aY7OE= -github.com/nishanths/exhaustive v0.12.0 h1:vIY9sALmw6T/yxiASewa4TQcFsVYZQQRUQJhKRf3Swg= -github.com/nishanths/exhaustive v0.12.0/go.mod h1:mEZ95wPIZW+x8kC4TgC+9YCUgiST7ecevsVDTgc2obs= -github.com/nishanths/predeclared v0.2.2 h1:V2EPdZPliZymNAn79T8RkNApBjMmVKh5XRpLm/w98Vk= -github.com/nishanths/predeclared v0.2.2/go.mod h1:RROzoN6TnGQupbC+lqggsOlcgysk3LMK/HI84Mp280c= -github.com/nunnatsa/ginkgolinter v0.21.2 h1:khzWfm2/Br8ZemX8QM1pl72LwM+rMeW6VUbQ4rzh0Po= -github.com/nunnatsa/ginkgolinter v0.21.2/go.mod h1:GItSI5fw7mCGLPmkvGYrr1kEetZe7B593jcyOpyabsY= -github.com/otiai10/copy v1.2.0/go.mod h1:rrF5dJ5F0t/EWSYODDu4j9/vEeYHMkc8jt0zJChqQWw= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v1.0.0/go.mod h1:LskTG5wDwr8Rs+nNQ+1LlxRjAtTZZjtJW4rMXl6j4vs= -github.com/otiai10/mint v1.3.0/go.mod h1:F5AjcsTsWUqX+Na9fpHb52P8pcRX2CI6A3ctIT91xUo= -github.com/otiai10/mint v1.3.1/go.mod h1:/yxELlJQ0ufhjUwhshSj+wFjZ78CnZ48/1wtmBH1OTc= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= -github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= -github.com/prometheus/client_golang v1.12.1 h1:ZiaPsmm9uiBeaSMRznKsCDNtPCS0T3JVDGF+06gjBzk= -github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= -github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= -github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= -github.com/prometheus/common v0.32.1 h1:hWIdL3N2HoUx3B8j3YN9mWor0qhY/NlEKZEaXxuIRh4= -github.com/prometheus/common v0.32.1/go.mod h1:vu+V0TpY+O6vW9J44gczi3Ap/oXXR10b+M/gUGO4Hls= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0VU= -github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= -github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA= -github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE= -github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY= -github.com/quasilyte/go-ruleguard/dsl v0.3.23/go.mod h1:KeCP03KrjuSO0H1kTuZQCWlQPulDV6YMIXmpQss17rU= -github.com/quasilyte/gogrep v0.5.0 h1:eTKODPXbI8ffJMN+W2aE0+oL0z/nh8/5eNdiO34SOAo= -github.com/quasilyte/gogrep v0.5.0/go.mod h1:Cm9lpz9NZjEoL1tgZ2OgeUKPIxL1meE7eo60Z6Sk+Ng= -github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 h1:TCg2WBOl980XxGFEZSS6KlBGIV0diGdySzxATTWoqaU= -github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= -github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 h1:M8mH9eK4OUR4lu7Gd+PU1fV2/qnDNfzT635KRSObncs= -github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567/go.mod h1:DWNGW8A4Y+GyBgPuaQJuWiy0XYftx4Xm/y5Jqk9I6VQ= -github.com/raeperd/recvcheck v0.2.0 h1:GnU+NsbiCqdC2XX5+vMZzP+jAJC5fht7rcVTAhX74UI= -github.com/raeperd/recvcheck v0.2.0/go.mod h1:n04eYkwIR0JbgD73wT8wL4JjPC3wm0nFtzBnWNocnYU= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= -github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ryancurrah/gomodguard v1.4.1 h1:eWC8eUMNZ/wM/PWuZBv7JxxqT5fiIKSIyTvjb7Elr+g= -github.com/ryancurrah/gomodguard v1.4.1/go.mod h1:qnMJwV1hX9m+YJseXEBhd2s90+1Xn6x9dLz11ualI1I= -github.com/ryanrolds/sqlclosecheck v0.5.1 h1:dibWW826u0P8jNLsLN+En7+RqWWTYrjCB9fJfSfdyCU= -github.com/ryanrolds/sqlclosecheck v0.5.1/go.mod h1:2g3dUjoS6AL4huFdv6wn55WpLIDjY7ZgUR4J8HOO/XQ= -github.com/sanposhiho/wastedassign/v2 v2.1.0 h1:crurBF7fJKIORrV85u9UUpePDYGWnwvv3+A96WvwXT0= -github.com/sanposhiho/wastedassign/v2 v2.1.0/go.mod h1:+oSmSC+9bQ+VUAxA66nBb0Z7N8CK7mscKTDYC6aIek4= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 h1:KRzFb2m7YtdldCEkzs6KqmJw4nqEVZGK7IN2kJkjTuQ= -github.com/santhosh-tekuri/jsonschema/v6 v6.0.2/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU= -github.com/sashamelentyev/interfacebloat v1.1.0 h1:xdRdJp0irL086OyW1H/RTZTr1h/tMEOsumirXcOJqAw= -github.com/sashamelentyev/interfacebloat v1.1.0/go.mod h1:+Y9yU5YdTkrNvoX0xHc84dxiN1iBi9+G8zZIhPVoNjQ= -github.com/sashamelentyev/usestdlibvars v1.29.0 h1:8J0MoRrw4/NAXtjQqTHrbW9NN+3iMf7Knkq057v4XOQ= -github.com/sashamelentyev/usestdlibvars v1.29.0/go.mod h1:8PpnjHMk5VdeWlVb4wCdrB8PNbLqZ3wBZTZWkrpZZL8= -github.com/securego/gosec/v2 v2.22.11 h1:tW+weM/hCM/GX3iaCV91d5I6hqaRT2TPsFM1+USPXwg= -github.com/securego/gosec/v2 v2.22.11/go.mod h1:KE4MW/eH0GLWztkbt4/7XpyH0zJBBnu7sYB4l6Wn7Mw= -github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= -github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/sivchari/containedctx v1.0.3 h1:x+etemjbsh2fB5ewm5FeLNi5bUjK0V8n0RB+Wwfd0XE= -github.com/sivchari/containedctx v1.0.3/go.mod h1:c1RDvCbnJLtH4lLcYD/GqwiBSSf4F5Qk0xld2rBqzJ4= -github.com/sonatard/noctx v0.4.0 h1:7MC/5Gg4SQ4lhLYR6mvOP6mQVSxCrdyiExo7atBs27o= -github.com/sonatard/noctx v0.4.0/go.mod h1:64XdbzFb18XL4LporKXp8poqZtPKbCrqQ402CV+kJas= -github.com/sourcegraph/go-diff v0.7.0 h1:9uLlrd5T46OXs5qpp8L/MTltk0zikUGi0sNNyCpA8G0= -github.com/sourcegraph/go-diff v0.7.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= -github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= -github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= -github.com/ssgreg/nlreturn/v2 v2.2.1 h1:X4XDI7jstt3ySqGU86YGAURbxw3oTDPK9sPEi6YEwQ0= -github.com/ssgreg/nlreturn/v2 v2.2.1/go.mod h1:E/iiPB78hV7Szg2YfRgyIrk1AD6JVMTRkkxBiELzh2I= -github.com/stbenjam/no-sprintf-host-port v0.3.1 h1:AyX7+dxI4IdLBPtDbsGAyqiTSLpCP9hWRrXQDU4Cm/g= -github.com/stbenjam/no-sprintf-host-port v0.3.1/go.mod h1:ODbZesTCHMVKthBHskvUUexdcNHAQRXk9NpSsL8p/HQ= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/tenntenn/modver v1.0.1/go.mod h1:bePIyQPb7UeioSRkw3Q0XeMhYZSMx9B8ePqg6SAMGH0= -github.com/tenntenn/text/transform v0.0.0-20200319021203-7eef512accb3/go.mod h1:ON8b8w4BN/kE1EOhwT0o+d62W65a6aPw1nouo9LMgyY= -github.com/tetafro/godot v1.5.4 h1:u1ww+gqpRLiIA16yF2PV1CV1n/X3zhyezbNXC3E14Sg= -github.com/tetafro/godot v1.5.4/go.mod h1:eOkMrVQurDui411nBY2FA05EYH01r14LuWY/NrVDVcU= -github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67 h1:9LPGD+jzxMlnk5r6+hJnar67cgpDIz/iyD+rfl5r2Vk= -github.com/timakin/bodyclose v0.0.0-20241222091800-1db5c5ca4d67/go.mod h1:mkjARE7Yr8qU23YcGMSALbIxTQ9r9QBVahQOBRfU460= -github.com/timonwong/loggercheck v0.11.0 h1:jdaMpYBl+Uq9mWPXv1r8jc5fC3gyXx4/WGwTnnNKn4M= -github.com/timonwong/loggercheck v0.11.0/go.mod h1:HEAWU8djynujaAVX7QI65Myb8qgfcZ1uKbdpg3ZzKl8= -github.com/tomarrell/wrapcheck/v2 v2.12.0 h1:H/qQ1aNWz/eeIhxKAFvkfIA+N7YDvq6TWVFL27Of9is= -github.com/tomarrell/wrapcheck/v2 v2.12.0/go.mod h1:AQhQuZd0p7b6rfW+vUwHm5OMCGgp63moQ9Qr/0BpIWo= -github.com/tommy-muehle/go-mnd/v2 v2.5.1 h1:NowYhSdyE/1zwK9QCLeRb6USWdoif80Ie+v+yU8u1Zw= -github.com/tommy-muehle/go-mnd/v2 v2.5.1/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw= -github.com/ultraware/funlen v0.2.0 h1:gCHmCn+d2/1SemTdYMiKLAHFYxTYz7z9VIDRaTGyLkI= -github.com/ultraware/funlen v0.2.0/go.mod h1:ZE0q4TsJ8T1SQcjmkhN/w+MceuatI6pBFSxxyteHIJA= -github.com/ultraware/whitespace v0.2.0 h1:TYowo2m9Nfj1baEQBjuHzvMRbp19i+RCcRYrSWoFa+g= -github.com/ultraware/whitespace v0.2.0/go.mod h1:XcP1RLD81eV4BW8UhQlpaR+SDc2givTvyI8a586WjW8= -github.com/uudashr/gocognit v1.2.0 h1:3BU9aMr1xbhPlvJLSydKwdLN3tEUUrzPSSM8S4hDYRA= -github.com/uudashr/gocognit v1.2.0/go.mod h1:k/DdKPI6XBZO1q7HgoV2juESI2/Ofj9AcHPZhBBdrTU= -github.com/uudashr/iface v1.4.1 h1:J16Xl1wyNX9ofhpHmQ9h9gk5rnv2A6lX/2+APLTo0zU= -github.com/uudashr/iface v1.4.1/go.mod h1:pbeBPlbuU2qkNDn0mmfrxP2X+wjPMIQAy+r1MBXSXtg= -github.com/xen0n/gosmopolitan v1.3.0 h1:zAZI1zefvo7gcpbCOrPSHJZJYA9ZgLfJqtKzZ5pHqQM= -github.com/xen0n/gosmopolitan v1.3.0/go.mod h1:rckfr5T6o4lBtM1ga7mLGKZmLxswUoH1zxHgNXOsEt4= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= -github.com/yagipy/maintidx v1.0.0 h1:h5NvIsCz+nRDapQ0exNv4aJ0yXSI0420omVANTv3GJM= -github.com/yagipy/maintidx v1.0.0/go.mod h1:0qNf/I/CCZXSMhsRsrEPDZ+DkekpKLXAJfsTACwgXLk= -github.com/yeya24/promlinter v0.3.0 h1:JVDbMp08lVCP7Y6NP3qHroGAO6z2yGKQtS5JsjqtoFs= -github.com/yeya24/promlinter v0.3.0/go.mod h1:cDfJQQYv9uYciW60QT0eeHlFodotkYZlL+YcPQN+mW4= -github.com/ykadowak/zerologlint v0.1.5 h1:Gy/fMz1dFQN9JZTPjv1hxEk+sRWm05row04Yoolgdiw= -github.com/ykadowak/zerologlint v0.1.5/go.mod h1:KaUskqF3e/v59oPmdq1U1DnKcuHokl2/K1U4pmIELKg= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -gitlab.com/bosi/decorder v0.4.2 h1:qbQaV3zgwnBZ4zPMhGLW4KZe7A7NwxEhJx39R3shffo= -gitlab.com/bosi/decorder v0.4.2/go.mod h1:muuhHoaJkA9QLcYHq4Mj8FJUwDZ+EirSHRiaTcTf6T8= -go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= -go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= -go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= -go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= -go.augendre.info/arangolint v0.3.1 h1:n2E6p8f+zfXSFLa2e2WqFPp4bfvcuRdd50y6cT65pSo= -go.augendre.info/arangolint v0.3.1/go.mod h1:6ZKzEzIZuBQwoSvlKT+qpUfIbBfFCE5gbAoTg0/117g= -go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= -go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= -go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= -go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= -go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= -go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp/typeparams v0.0.0-20220428152302-39d4317da171/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20230203172020-98cc5a0785f9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546 h1:HDjDiATsGqvuqvkDvgJjD1IgPrVekcSXVVE21JwvzGE= -golang.org/x/exp/typeparams v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:4Mzdyp/6jzw9auFDJ3OMF5qksa7UvPnzKqTVGcb04ms= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -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= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.16.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/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.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/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-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-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/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-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/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= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/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-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -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.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -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= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200329025819-fd4102a86c65/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200724022722-7017fd6b1305/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.1.1-0.20210205202024-ef80cdb6ec6d/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1-0.20210302220138-2ac05c832e1a/go.mod h1:9bzcO0MWcOuT0tm1iBGzDVPshzfwoVvREIui8C+MHqU= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -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.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -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.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -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= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -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.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -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.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/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= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.6.1 h1:R094WgE8K4JirYjBaOpz/AvTyUu/3wbmAoskKN/pxTI= -honnef.co/go/tools v0.6.1/go.mod h1:3puzxxljPCe8RGJX7BIy1plGbxEOZni5mR2aXe3/uk4= -mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= -mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= -mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15 h1:ssMzja7PDPJV8FStj7hq9IKiuiKhgz9ErWw+m68e7DI= -mvdan.cc/unparam v0.0.0-20251027182757-5beb8c8f8f15/go.mod h1:4M5MMXl2kW6fivUT6yRGpLLPNfuGtU2Z0cPvFquGDYU= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=