From f0e6d3af58c29f0a95ed7f1b81ec6fed2b3cd70f Mon Sep 17 00:00:00 2001 From: TheiLLeniumStudios <104288623+TheiLLeniumStudios@users.noreply.github.com> Date: Sat, 10 Jan 2026 13:42:10 +0100 Subject: [PATCH] feat: A lot of refactoring and CSI test cases --- .gitignore | 1 + .golangci.yml | 74 ++ Makefile | 53 +- go.mod | 86 +- go.sum | 546 ++++++++- internal/pkg/app/app.go | 4 +- internal/pkg/callbacks/rolling_upgrade.go | 278 +++-- internal/pkg/controller/controller.go | 36 +- internal/pkg/controller/controller_test.go | 381 +++--- internal/pkg/handler/create.go | 13 +- internal/pkg/handler/create_test.go | 15 +- internal/pkg/handler/delete.go | 11 +- internal/pkg/handler/delete_test.go | 9 +- internal/pkg/handler/handlers_test.go | 45 +- internal/pkg/handler/pause_deployment.go | 5 +- internal/pkg/handler/pause_deployment_test.go | 7 +- internal/pkg/handler/update.go | 7 +- internal/pkg/handler/update_test.go | 5 +- internal/pkg/handler/upgrade.go | 23 +- internal/pkg/handler/upgrade_test.go | 732 +++++++++++- internal/pkg/leadership/leadership.go | 5 +- internal/pkg/leadership/leadership_test.go | 1 + internal/pkg/testutil/kube.go | 17 +- internal/pkg/util/interface.go | 8 +- internal/pkg/util/util.go | 5 +- internal/pkg/util/util_test.go | 3 +- pkg/common/common.go | 31 +- pkg/common/config.go | 5 +- pkg/kube/client.go | 4 +- scripts/e2e-cluster-cleanup.sh | 330 ++++-- scripts/e2e-cluster-setup.sh | 391 ++++++- test/e2e/README.md | 702 ++++++------ test/e2e/advanced/advanced_suite_test.go | 24 +- test/e2e/advanced/job_reload_test.go | 42 +- test/e2e/advanced/multi_container_test.go | 125 ++ test/e2e/advanced/pod_annotations_test.go | 1 + test/e2e/advanced/regex_test.go | 1 + .../e2e/annotations/annotations_suite_test.go | 67 +- test/e2e/annotations/auto_reload_test.go | 186 ++- test/e2e/annotations/combination_test.go | 1 + test/e2e/annotations/exclude_test.go | 207 +++- test/e2e/annotations/pause_period_test.go | 5 +- test/e2e/annotations/resource_ignore_test.go | 1 + test/e2e/annotations/search_match_test.go | 1 + test/e2e/argo/argo_suite_test.go | 28 +- test/e2e/argo/rollout_test.go | 15 +- test/e2e/core/core_suite_test.go | 39 +- test/e2e/core/reference_methods_test.go | 69 +- test/e2e/core/workloads_test.go | 1018 ++++++++++++----- test/e2e/csi/csi_suite_test.go | 75 ++ test/e2e/csi/csi_test.go | 390 +++++++ test/e2e/e2e_suite_test.go | 3 +- test/e2e/flags/auto_reload_all_test.go | 1 + test/e2e/flags/flags_suite_test.go | 3 +- test/e2e/flags/ignore_resources_test.go | 1 + test/e2e/flags/ignored_workloads_test.go | 1 + test/e2e/flags/namespace_ignore_test.go | 1 + test/e2e/flags/namespace_selector_test.go | 1 + test/e2e/flags/reload_on_create_test.go | 1 + test/e2e/flags/reload_on_delete_test.go | 1 + test/e2e/flags/resource_selector_test.go | 1 + test/e2e/flags/watch_globally_test.go | 1 + test/e2e/utils/annotations.go | 34 + test/e2e/utils/argo.go | 306 +---- test/e2e/utils/csi.go | 385 +++++++ test/e2e/utils/openshift.go | 242 ---- test/e2e/utils/podspec.go | 257 +++++ test/e2e/utils/resources.go | 834 ++++++-------- test/e2e/utils/testenv.go | 103 +- test/e2e/utils/wait.go | 471 +++----- test/e2e/utils/wait_helpers.go | 87 ++ test/e2e/utils/workload_adapter.go | 44 +- test/e2e/utils/workload_argo.go | 387 ++----- test/e2e/utils/workload_cronjob.go | 164 +-- test/e2e/utils/workload_daemonset.go | 193 +--- test/e2e/utils/workload_deployment.go | 81 +- test/e2e/utils/workload_job.go | 134 +-- test/e2e/utils/workload_openshift.go | 363 ++---- test/e2e/utils/workload_statefulset.go | 193 +--- 79 files changed, 6434 insertions(+), 3987 deletions(-) create mode 100644 .golangci.yml create mode 100644 test/e2e/csi/csi_suite_test.go create mode 100644 test/e2e/csi/csi_test.go create mode 100644 test/e2e/utils/csi.go create mode 100644 test/e2e/utils/podspec.go create mode 100644 test/e2e/utils/wait_helpers.go diff --git a/.gitignore b/.gitignore index 3f28c3f..b3827ff 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ test/loadtest/results test/loadtest/loadtest # Temporary NFS files .nfs* +*.test diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..8644bc0 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,74 @@ +version: "2" + +run: + go: "1.25" + timeout: 5m + allow-parallel-runners: true + +linters: + default: none + enable: + # Core linters + - errcheck + - govet + - staticcheck + - ineffassign + - unused + + # Code quality + - revive + - misspell + - unconvert + - unparam + - nakedret + - copyloopvar + + # Bug prevention + - bodyclose + - durationcheck + - errorlint + + # Test framework + - ginkgolinter + + settings: + revive: + rules: + - name: comment-spacings + - name: import-shadowing + + govet: + enable-all: true + disable: + - shadow + - fieldalignment + + errcheck: + check-type-assertions: true + exclude-functions: + - (io.Closer).Close + - (*os.File).Close + + nakedret: + max-func-lines: 30 + + exclusions: + generated: lax + rules: + - linters: + - errcheck + path: _test\.go + paths: + - third_party$ + - vendor$ + +formatters: + enable: + - gofmt + - goimports + settings: + goimports: + local-prefixes: + - github.com/stakater/Reloader + exclusions: + generated: lax diff --git a/Makefile b/Makefile index 99d107e..edc396f 100644 --- a/Makefile +++ b/Makefile @@ -143,59 +143,44 @@ manifest: docker manifest annotate --arch $(ARCH) $(REPOSITORY_GENERIC) $(REPOSITORY_ARCH) test: - "$(GOCMD)" test -timeout 1800s -v -short ./internal/... ./test/e2e/utils/... + "$(GOCMD)" test -timeout 1800s -v -short -count=1 ./internal/... ./test/e2e/utils/... ##@ E2E Tests E2E_IMG ?= ghcr.io/stakater/reloader:test E2E_TIMEOUT ?= 45m -KIND_CLUSTER ?= kind - -# Detect container runtime (docker or podman) +KIND_CLUSTER ?= reloader-e2e CONTAINER_RUNTIME ?= $(shell command -v docker 2>/dev/null || command -v podman 2>/dev/null) -.PHONY: e2e-build -e2e-build: ## Build container image for e2e testing (uses docker or podman) - $(CONTAINER_RUNTIME) build -t $(E2E_IMG) -f Dockerfile . +.PHONY: e2e-setup +e2e-setup: ## One-time setup: create Kind cluster and install dependencies (Argo, CSI, Vault) + @if kind get clusters 2>/dev/null | grep -q "^$(KIND_CLUSTER)$$"; then \ + echo "Kind cluster $(KIND_CLUSTER) already exists"; \ + else \ + echo "Creating Kind cluster $(KIND_CLUSTER)..."; \ + kind create cluster --name $(KIND_CLUSTER); \ + fi + ./scripts/e2e-cluster-setup.sh -.PHONY: e2e-load -e2e-load: ## Load e2e image to Kind cluster (handles both docker and podman) +.PHONY: e2e +e2e: ## Run e2e tests (builds image, loads to Kind, runs tests in parallel) + $(CONTAINER_RUNTIME) build -t $(E2E_IMG) -f Dockerfile . ifeq ($(notdir $(CONTAINER_RUNTIME)),podman) - @echo "Using podman: loading via image-archive..." $(CONTAINER_RUNTIME) save $(E2E_IMG) -o /tmp/reloader-e2e.tar kind load image-archive /tmp/reloader-e2e.tar --name $(KIND_CLUSTER) rm -f /tmp/reloader-e2e.tar else kind load docker-image $(E2E_IMG) --name $(KIND_CLUSTER) endif + SKIP_BUILD=true RELOADER_IMAGE=$(E2E_IMG) "$(GOCMD)" tool ginkgo --keep-going -v --timeout=$(E2E_TIMEOUT) ./test/e2e/... -.PHONY: e2e-setup -e2e-setup: e2e-build e2e-load ## Build image and load to Kind (run once before tests) - @echo "E2E setup complete. Image $(E2E_IMG) loaded to Kind cluster $(KIND_CLUSTER)" - -.PHONY: e2e-cluster-setup -e2e-cluster-setup: ## Setup e2e cluster prerequisites (Argo Rollouts, etc.) - ./scripts/e2e-cluster-setup.sh - -.PHONY: e2e-cluster-cleanup -e2e-cluster-cleanup: ## Cleanup e2e cluster resources (Argo Rollouts, test namespaces, etc.) +.PHONY: e2e-cleanup +e2e-cleanup: ## Cleanup: remove test resources and delete Kind cluster ./scripts/e2e-cluster-cleanup.sh - -.PHONY: e2e -e2e: e2e-setup e2e-cluster-setup ## Run all e2e tests (builds image, loads to Kind, sets up cluster, runs tests) - SKIP_BUILD=true RELOADER_IMAGE=$(E2E_IMG) "$(GOCMD)" test -v -count=1 -p 1 -timeout $(E2E_TIMEOUT) ./test/e2e/... - @echo "E2E tests complete. Run 'make e2e-cluster-cleanup' to cleanup cluster resources." - -.PHONY: e2e-kind-create -e2e-kind-create: ## Create Kind cluster for e2e tests - kind create cluster --name $(KIND_CLUSTER) || true + kind delete cluster --name $(KIND_CLUSTER) .PHONY: e2e-ci -e2e-ci: e2e-kind-create e2e e2e-cluster-cleanup ## Full CI pipeline: create Kind cluster, build, load, run tests, cleanup - -.PHONY: e2e-kind-delete -e2e-kind-delete: ## Delete Kind cluster used for e2e tests - kind delete cluster --name $(KIND_CLUSTER) +e2e-ci: e2e-setup e2e e2e-cleanup ## CI pipeline: setup, run tests, cleanup .PHONY: docker-build docker-build: ## Build Docker image diff --git a/go.mod b/go.mod index ab3607b..30e41ed 100644 --- a/go.mod +++ b/go.mod @@ -4,10 +4,10 @@ go 1.25.5 require ( github.com/argoproj/argo-rollouts v1.8.3 - github.com/onsi/ginkgo/v2 v2.27.2 - github.com/onsi/gomega v1.38.2 - github.com/openshift/api v0.0.0-20260102143802-d2ec16864f86 - github.com/openshift/client-go v0.0.0-20251223102348-558b0eef16bc + github.com/onsi/ginkgo/v2 v2.27.4 + github.com/onsi/gomega v1.39.0 + github.com/openshift/api v0.0.0-20260109135506-3920bba77f16 + github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13 github.com/parnurzeal/gorequest v0.3.0 github.com/prometheus/client_golang v1.23.2 github.com/sirupsen/logrus v1.9.3 @@ -17,7 +17,7 @@ require ( k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.35.0 k8s.io/kubectl v0.35.0 - k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2 + k8s.io/utils v0.0.0-20260108192941-914a6e750570 sigs.k8s.io/secrets-store-csi-driver v1.5.5 ) @@ -27,76 +27,76 @@ require ( github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 // indirect - github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/logr v1.4.3 // indirect - github.com/go-openapi/jsonpointer v0.21.1 // indirect - github.com/go-openapi/jsonreference v0.21.0 // indirect - github.com/go-openapi/swag v0.23.1 // indirect + github.com/go-openapi/jsonpointer v0.22.4 // indirect + github.com/go-openapi/jsonreference v0.21.4 // indirect + github.com/go-openapi/swag v0.25.4 // indirect + github.com/go-openapi/swag/cmdutils v0.25.4 // indirect + github.com/go-openapi/swag/conv v0.25.4 // indirect + github.com/go-openapi/swag/fileutils v0.25.4 // indirect + github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/swag/jsonutils v0.25.4 // indirect + github.com/go-openapi/swag/loading v0.25.4 // indirect + github.com/go-openapi/swag/mangling v0.25.4 // indirect + github.com/go-openapi/swag/netutils v0.25.4 // indirect + github.com/go-openapi/swag/stringutils v0.25.4 // indirect + github.com/go-openapi/swag/typeutils v0.25.4 // indirect + github.com/go-openapi/swag/yamlutils v0.25.4 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/gnostic-models v0.7.1 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/pprof v0.0.0-20260106004452-d7df1bf2cac7 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/mailru/easyjson v0.9.0 // indirect + github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/moul/http2curl v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_model v0.6.2 // indirect - github.com/prometheus/common v0.66.1 // indirect - github.com/prometheus/procfs v0.16.1 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect github.com/smartystreets/goconvey v1.7.2 // indirect - github.com/spf13/pflag v1.0.9 // indirect + github.com/spf13/pflag v1.0.10 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/mod v0.32.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect - golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.39.0 // indirect - google.golang.org/protobuf v1.36.8 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/term v0.39.0 // indirect + golang.org/x/text v0.33.0 // indirect + golang.org/x/time v0.14.0 // indirect + golang.org/x/tools v0.40.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.1 // indirect sigs.k8s.io/yaml v1.6.0 // indirect ) +tool github.com/onsi/ginkgo/v2/ginkgo + // Replacements for argo-rollouts replace ( - github.com/go-check/check => github.com/go-check/check v0.0.0-20201130134442-10cb98267c6c k8s.io/api v0.0.0 => k8s.io/api v0.35.0 k8s.io/apimachinery v0.0.0 => k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.0.0 => k8s.io/client-go v0.35.0 - k8s.io/cloud-provider v0.0.0 => k8s.io/cloud-provider v0.24.2 - k8s.io/controller-manager v0.0.0 => k8s.io/controller-manager v0.24.2 - k8s.io/cri-api v0.0.0 => k8s.io/cri-api v0.20.5-rc.0 - k8s.io/csi-translation-lib v0.0.0 => k8s.io/csi-translation-lib v0.24.2 - k8s.io/kube-aggregator v0.0.0 => k8s.io/kube-aggregator v0.24.2 - k8s.io/kube-controller-manager v0.0.0 => k8s.io/kube-controller-manager v0.24.2 - k8s.io/kube-proxy v0.0.0 => k8s.io/kube-proxy v0.24.2 - k8s.io/kube-scheduler v0.0.0 => k8s.io/kube-scheduler v0.24.2 - k8s.io/kubectl v0.0.0 => k8s.io/kubectl v0.35.0 - k8s.io/kubelet v0.0.0 => k8s.io/kubelet v0.24.2 - k8s.io/legacy-cloud-providers v0.0.0 => k8s.io/legacy-cloud-providers v0.24.2 - k8s.io/mount-utils v0.0.0 => k8s.io/mount-utils v0.20.5-rc.0 - k8s.io/sample-apiserver v0.0.0 => k8s.io/sample-apiserver v0.24.2 - k8s.io/sample-cli-plugin v0.0.0 => k8s.io/sample-cli-plugin v0.24.2 - k8s.io/sample-controller v0.0.0 => k8s.io/sample-controller v0.24.2 + k8s.io/cloud-provider v0.0.0 => k8s.io/cloud-provider v0.35.0 + k8s.io/kubelet v0.0.0 => k8s.io/kubelet v0.35.0 ) diff --git a/go.sum b/go.sum index 50dd7d0..9b7b791 100644 --- a/go.sum +++ b/go.sum @@ -1,64 +1,291 @@ +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= +github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= +github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I= +github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c= +github.com/PagerDuty/go-pagerduty v1.7.0 h1:S1NcMKECxT5hJwV4VT+QzeSsSiv4oWl1s2821dUqG/8= +github.com/PagerDuty/go-pagerduty v1.7.0/go.mod h1:PuFyJKRz1liIAH4h5KVXVD18Obpp1ZXRdxHvmGXooro= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= +github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20220708192748-b73dcb041214 h1:MdZskg1II+YVe+9ss935i8+paqqf4KEuYcTYUWSwABI= +github.com/RocketChat/Rocket.Chat.Go.SDK v0.0.0-20220708192748-b73dcb041214/go.mod h1:rjP7sIipbZcagro/6TCk6X0ZeFT2eyudH5+fve/cbBA= +github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= +github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= +github.com/antonmedv/expr v1.15.5 h1:y0Iz3cEwmpRz5/r3w4qQR0MfIqJGdGM1zbhD/v0G5Vg= +github.com/antonmedv/expr v1.15.5/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= +github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ= +github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk= github.com/argoproj/argo-rollouts v1.8.3 h1:blbtQva4IK9r6gFh+dWkCrLnFdPOWiv9ubQYu36qeaA= github.com/argoproj/argo-rollouts v1.8.3/go.mod h1:kCAUvIfMGfOyVf3lvQbBt0nqQn4Pd+zB5/YwKv+UBa8= +github.com/argoproj/notifications-engine v0.4.1-0.20240219110818-7a069766e954 h1:4jbSTsw6/9pulz2eVoLnKtn75FYIeaLCNBOA1LjG1fA= +github.com/argoproj/notifications-engine v0.4.1-0.20240219110818-7a069766e954/go.mod h1:E4gOYnn452S8c10UucTztrZx/cTGU+jgMZiqfH9HUck= +github.com/argoproj/pkg v0.13.6 h1:36WPD9MNYECHcO1/R1pj6teYspiK7uMQLCgLGft2abM= +github.com/argoproj/pkg v0.13.6/go.mod h1:I698DoJBKuvNFaixh4vFl2C88cNIT1WS7KCbz5ewyF8= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a h1:idn718Q4B6AGu/h5Sxe66HYVdqdGu2l9Iebqhi/AEoA= +github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= +github.com/aws/aws-sdk-go v1.44.116 h1:NpLIhcvLWXJZAEwvPj3TDHeqp7DleK6ZUVYyW01WNHY= +github.com/aws/aws-sdk-go v1.44.116/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go-v2 v1.32.6 h1:7BokKRgRPuGmKkFMhEg/jSul+tB9VvXhcViILtfG8b4= +github.com/aws/aws-sdk-go-v2 v1.32.6/go.mod h1:P5WJBrYqqbWVaOxgH0X/FYYD47/nooaPOZPlQdmiN2U= +github.com/aws/aws-sdk-go-v2/config v1.28.6 h1:D89IKtGrs/I3QXOLNTH93NJYtDhm8SYa9Q5CsPShmyo= +github.com/aws/aws-sdk-go-v2/config v1.28.6/go.mod h1:GDzxJ5wyyFSCoLkS+UhGB0dArhb9mI+Co4dHtoTxbko= +github.com/aws/aws-sdk-go-v2/credentials v1.17.47 h1:48bA+3/fCdi2yAwVt+3COvmatZ6jUDNkDTIsqDiMUdw= +github.com/aws/aws-sdk-go-v2/credentials v1.17.47/go.mod h1:+KdckOejLW3Ks3b0E3b5rHsr2f9yuORBum0WPnE5o5w= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21 h1:AmoU1pziydclFT/xRV+xXE/Vb8fttJCLRPv8oAkprc0= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.21/go.mod h1:AjUdLYe4Tgs6kpH4Bv7uMZo7pottoyHMn4eTcIcneaY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25 h1:s/fF4+yDQDoElYhfIVvSNyeCydfbuTKzhxSXDXCPasU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.25/go.mod h1:IgPfDv5jqFIzQSNbUEMoitNooSMXjRSDkhXv8jiROvU= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25 h1:ZntTCl5EsYnhN/IygQEUugpdwbhdkom9uHcbCftiGgA= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.25/go.mod h1:DBdPrgeocww+CSl1C8cEV8PN1mHMBhuCDLpXezyvWkE= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 h1:VaRN3TlFdd6KxX1x3ILT5ynH6HvKgqdiXoTxAF4HQcQ= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1/go.mod h1:FbtygfRFze9usAadmnGJNc8KsP346kEe+y2/oyhGAGc= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.43.3 h1:nQLG9irjDGUFXVPDHzjCGEEwh0hZ6BcxTvHOod1YsP4= +github.com/aws/aws-sdk-go-v2/service/cloudwatch v1.43.3/go.mod h1:URs8sqsyaxiAZkKP6tOEmhcs9j2ynFIomqOKY/CAHJc= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.31.3 h1:Avh8YS+sgb2OKRht0wdNwY8tqtsCzVrmc8dG8Wfy9LI= +github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.31.3/go.mod h1:HbtHaw/hnNPaiqcyYnheILVyn81wOZiX9n2gYF5tPmM= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1 h1:iXtILhvDxB6kPvEXgsDhGaZCSC6LQET5ZHSdJozeI0Y= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.1/go.mod h1:9nu0fVANtYiAePIBh2/pFUSwtJ402hLnp854CNoDOeE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6 h1:50+XsN70RS7dwJ2CkVNXzj7U2L1HKP8nqTd3XWEXBN4= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.6/go.mod h1:WqgLmwY7so32kG01zD8CPTJWVWM+TzJoOVHwTg4aPug= +github.com/aws/aws-sdk-go-v2/service/sqs v1.29.7 h1:tRNrFDGRm81e6nTX5Q4CFblea99eAfm0dxXazGpLceU= +github.com/aws/aws-sdk-go-v2/service/sqs v1.29.7/go.mod h1:8GWUDux5Z2h6z2efAtr54RdHXtLm8sq7Rg85ZNY/CZM= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.7 h1:rLnYAfXQ3YAccocshIH5mzNNwZBkBo+bP6EhIxak6Hw= +github.com/aws/aws-sdk-go-v2/service/sso v1.24.7/go.mod h1:ZHtuQJ6t9A/+YDuxOLnbryAmITtr8UysSny3qcyvJTc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6 h1:JnhTZR3PiYDNKlXy50/pNeix9aGMo6lLpXwJ1mw8MD4= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.28.6/go.mod h1:URronUEGfXZN1VpdktPSD1EkAL9mfrV+2F4sjH38qOY= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.2 h1:s4074ZO1Hk8qv65GqNXqDjmkf4HSQqJukaLuuW0TpDA= +github.com/aws/aws-sdk-go-v2/service/sts v1.33.2/go.mod h1:mVggCnIWoM09jP71Wh+ea7+5gAp53q+49wDFs1SW5z8= +github.com/aws/smithy-go v1.22.1 h1:/HPHZQ0g7f4eUeK6HKglFz8uwVfZKgoI25rb/J+dnro= +github.com/aws/smithy-go v1.22.1/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg= 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/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= +github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bombsimon/logrusr/v4 v4.1.0 h1:uZNPbwusB0eUXlO8hIUwStE6Lr5bLN6IgYgG+75kuh4= +github.com/bombsimon/logrusr/v4 v4.1.0/go.mod h1:pjfHC5e59CvjTBIU3V3sGhFWFAnsnhOR03TRc6im0l8= +github.com/bradleyfalzon/ghinstallation/v2 v2.5.0 h1:yaYcGQ7yEIGbsJfW/9z7v1sLiZg/5rSNNXwmMct5XaE= +github.com/bradleyfalzon/ghinstallation/v2 v2.5.0/go.mod h1:amcvPQMrRkWNdueWOjPytGL25xQGzox7425qMgzo+Vo= +github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= +github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 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/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= +github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= +github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/container-storage-interface/spec v1.6.0 h1:vwN9uCciKygX/a0toYryoYD5+qI9ZFeAMuhEEKO+JBA= +github.com/container-storage-interface/spec v1.6.0/go.mod h1:8K96oQNkJ7pFcC2R9Z1ynGGBB1I93kcS6PGg3SsOk8s= +github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= +github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= +github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380 h1:1NyRx2f4W4WBRyg0Kys0ZbaNmDDzZ2R/C7DTi+bbsJ0= github.com/elazarl/goproxy v0.0.0-20240726154733-8b0c20506380/go.mod h1:thX175TtLTzLj3p7N/Q9IiKZ7NF+p72cvL91emV0hzo= -github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= -github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCvpL6mnFh5mB2/l16U= +github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= +github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f h1:Wl78ApPPB2Wvf/TIe2xdyJxTlb6obmF18d8QdkxNDu4= +github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f/go.mod h1:OSYXu++VVOHnXeitef/D8n/6y4QV8uLHSFXX4NeXMGc= +github.com/fatih/camelcase v1.0.0 h1:hxNvNX/xYBp0ovncs8WyWZrOrpBNub/JfaMvbURyft8= +github.com/fatih/camelcase v1.0.0/go.mod h1:yN2Sb0lFhZJUdVvtELVWefmrXpuZESvPmqwoZc+/fpc= +github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= +github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= +github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32 h1:Mn26/9ZMNWSw9C9ERFA1PUxfmGpolnw2v0bKOREu5ew= +github.com/ghodss/yaml v1.0.1-0.20190212211648-25d852aebe32/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= +github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= +github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-openapi/jsonpointer v0.21.1 h1:whnzv/pNXtK2FbX/W9yJfRmE2gsmkfahjMKB0fZvcic= -github.com/go-openapi/jsonpointer v0.21.1/go.mod h1:50I1STOfbY1ycR8jGz8DaMeLCdXiI6aDteEdRNNzpdk= -github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= -github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= -github.com/go-openapi/swag v0.23.1 h1:lpsStH0n2ittzTnbaSloVZLuB5+fvSY/+hnagBjSNZU= -github.com/go-openapi/swag v0.23.1/go.mod h1:STZs8TbRvEQQKUA+JZNAm3EWlgaOBGpyFDqQnDHMef0= +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-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= +github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= +github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= +github.com/go-openapi/jsonreference v0.21.4 h1:24qaE2y9bx/q3uRK/qN+TDwbok1NhbSmGjjySRCHtC8= +github.com/go-openapi/jsonreference v0.21.4/go.mod h1:rIENPTjDbLpzQmQWCj5kKj3ZlmEh+EFVbz3RTUh30/4= +github.com/go-openapi/swag v0.25.4 h1:OyUPUFYDPDBMkqyxOTkqDYFnrhuhi9NR6QVUvIochMU= +github.com/go-openapi/swag v0.25.4/go.mod h1:zNfJ9WZABGHCFg2RnY0S4IOkAcVTzJ6z2Bi+Q4i6qFQ= +github.com/go-openapi/swag/cmdutils v0.25.4 h1:8rYhB5n6WawR192/BfUu2iVlxqVR9aRgGJP6WaBoW+4= +github.com/go-openapi/swag/cmdutils v0.25.4/go.mod h1:pdae/AFo6WxLl5L0rq87eRzVPm/XRHM3MoYgRMvG4A0= +github.com/go-openapi/swag/conv v0.25.4 h1:/Dd7p0LZXczgUcC/Ikm1+YqVzkEeCc9LnOWjfkpkfe4= +github.com/go-openapi/swag/conv v0.25.4/go.mod h1:3LXfie/lwoAv0NHoEuY1hjoFAYkvlqI/Bn5EQDD3PPU= +github.com/go-openapi/swag/fileutils v0.25.4 h1:2oI0XNW5y6UWZTC7vAxC8hmsK/tOkWXHJQH4lKjqw+Y= +github.com/go-openapi/swag/fileutils v0.25.4/go.mod h1:cdOT/PKbwcysVQ9Tpr0q20lQKH7MGhOEb6EwmHOirUk= +github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= +github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= +github.com/go-openapi/swag/jsonutils v0.25.4 h1:VSchfbGhD4UTf4vCdR2F4TLBdLwHyUDTd1/q4i+jGZA= +github.com/go-openapi/swag/jsonutils v0.25.4/go.mod h1:7OYGXpvVFPn4PpaSdPHJBtF0iGnbEaTk8AvBkoWnaAY= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4 h1:IACsSvBhiNJwlDix7wq39SS2Fh7lUOCJRmx/4SN4sVo= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.4/go.mod h1:Mt0Ost9l3cUzVv4OEZG+WSeoHwjWLnarzMePNDAOBiM= +github.com/go-openapi/swag/loading v0.25.4 h1:jN4MvLj0X6yhCDduRsxDDw1aHe+ZWoLjW+9ZQWIKn2s= +github.com/go-openapi/swag/loading v0.25.4/go.mod h1:rpUM1ZiyEP9+mNLIQUdMiD7dCETXvkkC30z53i+ftTE= +github.com/go-openapi/swag/mangling v0.25.4 h1:2b9kBJk9JvPgxr36V23FxJLdwBrpijI26Bx5JH4Hp48= +github.com/go-openapi/swag/mangling v0.25.4/go.mod h1:6dxwu6QyORHpIIApsdZgb6wBk/DPU15MdyYj/ikn0Hg= +github.com/go-openapi/swag/netutils v0.25.4 h1:Gqe6K71bGRb3ZQLusdI8p/y1KLgV4M/k+/HzVSqT8H0= +github.com/go-openapi/swag/netutils v0.25.4/go.mod h1:m2W8dtdaoX7oj9rEttLyTeEFFEBvnAx9qHd5nJEBzYg= +github.com/go-openapi/swag/stringutils v0.25.4 h1:O6dU1Rd8bej4HPA3/CLPciNBBDwZj9HiEpdVsb8B5A8= +github.com/go-openapi/swag/stringutils v0.25.4/go.mod h1:GTsRvhJW5xM5gkgiFe0fV3PUlFm0dr8vki6/VSRaZK0= +github.com/go-openapi/swag/typeutils v0.25.4 h1:1/fbZOUN472NTc39zpa+YGHn3jzHWhv42wAJSN91wRw= +github.com/go-openapi/swag/typeutils v0.25.4/go.mod h1:Ou7g//Wx8tTLS9vG0UmzfCsjZjKhpjxayRKTHXf2pTE= +github.com/go-openapi/swag/yamlutils v0.25.4 h1:6jdaeSItEUb7ioS9lFoCZ65Cne1/RZtPBZ9A56h92Sw= +github.com/go-openapi/swag/yamlutils v0.25.4/go.mod h1:MNzq1ulQu+yd8Kl7wPOut/YHAAU/H6hL91fF+E2RFwc= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2 h1:0+Y41Pz1NkbTHz8NngxTuAXxEodtNSI1WG1c/m5Akw4= +github.com/go-openapi/testify/enable/yaml/v2 v2.0.2/go.mod h1:kme83333GCtJQHXQ8UKX3IBZu6z8T5Dvy5+CW3NLUUg= +github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= +github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= -github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/golang/glog v1.2.2 h1:1+mZ9upx1Dh6FmUTFR1naJ77miKiXgALjWOZ3NVFPmY= +github.com/golang/glog v1.2.2/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= +github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= +github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= +github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= +github.com/google/gnostic v0.5.7-v3refs h1:FhTMOKj2VhjpouxvWJAV1TL304uMlb9zcDqkl6cEI54= +github.com/google/gnostic v0.5.7-v3refs/go.mod h1:73MKFl6jIHelAJNaBGFzt3SPtZULs9dYrGFt8OiIsHQ= +github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= +github.com/google/gnostic-models v0.7.1/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 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/go-github/v41 v41.0.0 h1:HseJrM2JFf2vfiZJ8anY2hqBjdfY1Vlj/K27ueww4gg= +github.com/google/go-github/v41 v41.0.0/go.mod h1:XgmCA5H323A9rtgExdTcnDkcqp6S30AVACCBDOonIxg= +github.com/google/go-github/v53 v53.0.0 h1:T1RyHbSnpHYnoF0ZYKiIPSgPtuJ8G6vgc0MKodXsQDQ= +github.com/google/go-github/v53 v53.0.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20260106004452-d7df1bf2cac7 h1:kmPAX+IJBcUAFTddx2+xC0H7sk2U9ijIIxZLLrPLNng= +github.com/google/pprof v0.0.0-20260106004452-d7df1bf2cac7/go.mod h1:67FPmZWbr+KDT/VlpWtw6sO9XSjpJmLuHpoLmWiTGgY= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= +github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= +github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= +github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/gregdel/pushover v1.2.1 h1:IPPJCdzXz60gMqnlzS0ZAW5z5aS1gI4nU+YM0Pe+ssA= +github.com/gregdel/pushover v1.2.1/go.mod h1:EcaO66Nn1StkpEm1iKtBTV3d2A16SoMsVER1PthX7to= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJrOjhax5N+uePQ0Fh1Z7PheYoUI/0nzkPA= +github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= +github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-plugin v1.6.2 h1:zdGAEd0V1lCaU0u+MxWQhtSDQmahpkwOun8U8EiRVog= +github.com/hashicorp/go-plugin v1.6.2/go.mod h1:CkgLQ5CZqNmdL9U9JzM532t8ZiYQ35+pj3b1FD37R0Q= +github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= +github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= +github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4= +github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b h1:ogbOPx86mIhFy764gGkqnkFC8m5PJA7sPzlk9ppLVQA= +github.com/ianlancetaylor/demangle v0.0.0-20250417193237-f615e6bd150b/go.mod h1:gx7rwoVhcfuVKG5uya9Hs3Sxj7EIvldVofAWIUtGouw= +github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= +github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb-client-go/v2 v2.14.0 h1:AjbBfJuq+QoaXNcrova8smSjwJdUHnwvfjMF71M1iI4= +github.com/influxdata/influxdb-client-go/v2 v2.14.0/go.mod h1:Ahpm3QXKMJslpXl3IftVLVezreAUtBOTZssDrjZEFHI= +github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf h1:7JTmneyiNEwVBOHSjoMxiWAqB992atOeepeFYegn5RU= +github.com/influxdata/line-protocol v0.0.0-20210922203350-b1ad95c89adf/go.mod h1:xaLFMmpvUxqXtVkUJfg9QmT88cDaCJ3ZKgdZ78oO8Qo= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= +github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= +github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/juju/ansiterm v1.0.0 h1:gmMvnZRq7JZJx6jkfSq9/+2LMrVEwGwt7UR6G+lmDEg= +github.com/juju/ansiterm v1.0.0/go.mod h1:PyXUpnI3olx3bsPcHt98FGPX/KCFZ1Fi+hw1XLI6384= +github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.5.0 h1:e8esj/e4R+SAOwFwN+n3zr0nYeCyeweozKfO23MvHzY= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= @@ -66,34 +293,92 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubernetes-csi/csi-lib-utils v0.10.0 h1:Aqm8X81eCzzfH/bvIEqSWtcbK9HF9NbFk4d+le1snVA= +github.com/kubernetes-csi/csi-lib-utils v0.10.0/go.mod h1:BmGZZB16L18+9+Lgg9YWwBKfNEHIDdgGfAyuW6p2NV0= +github.com/kubernetes-csi/csi-test/v4 v4.3.0 h1:3fi7ymnoFvCXQa/uauL1UrvnivuaT4r/gRJ2+RsQboc= +github.com/kubernetes-csi/csi-test/v4 v4.3.0/go.mod h1:qJ77AkqjA5MBoBDGKHsPqyce/6miqoid+dZ4B00Miuw= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= -github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= +github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/machinebox/graphql v0.2.2 h1:dWKpJligYKhYKO5A2gvNhkJdQMNZeChZYyBbrZkBZfo= +github.com/machinebox/graphql v0.2.2/go.mod h1:F+kbVMHuwrQ5tYgU9JXlnskM8nOaFxCAEolaQybkjWA= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= +github.com/matryer/is v1.4.0 h1:sosSmIWwkYITGrxZ25ULNDeKiMNzFSr4V/eqBQP0PeE= +github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= +github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= +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/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= +github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= +github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs= github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= -github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= -github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= -github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= -github.com/openshift/api v0.0.0-20260102143802-d2ec16864f86 h1:Vsqg+WqSA91LjrwK5lzkSCjztK/B+T8MPKI3MIALx3w= -github.com/openshift/api v0.0.0-20260102143802-d2ec16864f86/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= -github.com/openshift/client-go v0.0.0-20251223102348-558b0eef16bc h1:nIlRaJfr/yGjPV15MNF5eVHLAGyXFjcUzO+hXeWDDk8= -github.com/openshift/client-go v0.0.0-20251223102348-558b0eef16bc/go.mod h1:cs9BwTu96sm2vQvy7r9rOiltgu90M6ju2qIHFG9WU+o= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/newrelic/newrelic-client-go/v2 v2.51.3 h1:Bu/cUs6nfMjQMPBcxxHt4Xm30tKDT7ttYy/XRDsWP6Y= +github.com/newrelic/newrelic-client-go/v2 v2.51.3/go.mod h1:+RRjI3nDGWT3kLm9Oi3QxpBm70uu8q1upEHBVWCZFpo= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oapi-codegen/runtime v1.0.0 h1:P4rqFX5fMFWqRzY9M/3YF9+aPSPPB06IzP2P7oOxrWo= +github.com/oapi-codegen/runtime v1.0.0/go.mod h1:LmCUMQuPB4M/nLXilQXhHw+BLZdDb18B34OO356yJ/A= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= +github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= +github.com/onsi/ginkgo/v2 v2.27.4 h1:fcEcQW/A++6aZAZQNUmNjvA9PSOzefMJBerHJ4t8v8Y= +github.com/onsi/ginkgo/v2 v2.27.4/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/gomega v1.39.0 h1:y2ROC3hKFmQZJNFeGAMeHZKkjBL65mIZcvrLQBF9k6Q= +github.com/onsi/gomega v1.39.0/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/openshift/api v0.0.0-20260109135506-3920bba77f16 h1:EfTfmlNBtG/xauH9gcnq64J08nYTBKyilbl/EUbxGno= +github.com/openshift/api v0.0.0-20260109135506-3920bba77f16/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= +github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee h1:+Sp5GGnjHDhT/a/nQ1xdp43UscBMr7G5wxsYotyhzJ4= +github.com/openshift/build-machinery-go v0.0.0-20250530140348-dc5b2804eeee/go.mod h1:8jcm8UPtg2mCAsxfqKil1xrmRMI3a+XU2TZ9fF8A7TE= +github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13 h1:6rd4zSo2UaWQcAPZfHK9yzKVqH0BnMv1hqMzqXZyTds= +github.com/openshift/client-go v0.0.0-20260108185524-48f4ccfc4e13/go.mod h1:YvOmPmV7wcJxpfhTDuFqqs2Xpb3M3ovsM6Qs/i2ptq4= +github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.13 h1:nV98dkBpqaYbDnhefmOQ+Rn4hE+jD6AtjYHXaU5WyJI= +github.com/opsgenie/opsgenie-go-sdk-v2 v1.2.13/go.mod h1:4OjcxgwdXzezqytxN534MooNmrxRD50geWZxTD7845s= github.com/parnurzeal/gorequest v0.3.0 h1:SoFyqCDC9COr1xuS6VA8fC8RU7XyrJZN2ona1kEX7FI= github.com/parnurzeal/gorequest v0.3.0/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= +github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= +github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -103,23 +388,41 @@ github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= -github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= -github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= -github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= -github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/common/sigv4 v0.1.0 h1:qoVebwtwwEhS85Czm2dSROY5fTo2PAPEVdDeppTwGX4= +github.com/prometheus/common/sigv4 v0.1.0/go.mod h1:2Jkxxk9yYvCkE5G1sQT7GuEXm57JrvHu9k5YwTjsNtI= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= 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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/servicemeshinterface/smi-sdk-go v0.5.0 h1:9cZdhvGbGDlmnp9qqmcQL+RL6KZ3IzHfDLoA5Axg8n0= +github.com/servicemeshinterface/smi-sdk-go v0.5.0/go.mod h1:nm1Slf3pfaZPP3g2tE/K5wDmQ1uWVSP0p3uu5rQAQLc= +github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= +github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slack-go/slack v0.12.2 h1:x3OppyMyGIbbiyFhsBmpf9pwkUzMhthJMRNmNlA4LaQ= +github.com/slack-go/slack v0.12.2/go.mod h1:hlGi5oXA+Gt+yWTPP0plCdRKmjsDxecdHxYQdlMQKOw= github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/spaceapegames/go-wavefront v1.8.1 h1:Xuby0uBfw1WVxD9d+l8Gh+zINqnBfd0RJT8e/3i3vBM= +github.com/spaceapegames/go-wavefront v1.8.1/go.mod h1:GtdIjtJ0URkfPmaKx0+7vMSDvT/MON9v+4pbdagA8As= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= -github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/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= @@ -135,12 +438,70 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= +github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 h1:nrZ3ySNYwJbSpD6ce9duiP+QkD3JuLCcWkdaehUS/3Y= +github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80/go.mod h1:iFyPdL66DjUD96XmzVL3ZntbzcflLnznH0fr99w5VqE= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ= +github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0 h1:qqllXPzXh+So+mmANlX/gCJrgo+1kQyshMoQ+NASzm0= +github.com/whilp/git-urls v0.0.0-20191001220047-6db9661140c0/go.mod h1:2rx5KE5FLD0HRfkkpyn8JwbVLBdhgeiOb2D2D9LLKM4= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= +github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= +github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/etcd/api/v3 v3.5.10 h1:szRajuUUbLyppkhs9K6BRtjY37l66XQQmw7oZRANE4k= +go.etcd.io/etcd/api/v3 v3.5.10/go.mod h1:TidfmT4Uycad3NM/o25fG3J07odo4GBB9hoxaodFCtI= +go.etcd.io/etcd/client/pkg/v3 v3.5.10 h1:kfYIdQftBnbAq8pUWFXfpuuxFSKzlmM5cSn76JByiT0= +go.etcd.io/etcd/client/pkg/v3 v3.5.10/go.mod h1:DYivfIviIuQ8+/lCq4vcxuseg2P2XbHygkKwFo9fc8U= +go.etcd.io/etcd/client/v3 v3.5.10 h1:W9TXNZ+oB3MCd/8UjxHTWK5J9Nquw9fQBLJd5ne5/Ao= +go.etcd.io/etcd/client/v3 v3.5.10/go.mod h1:RVeBnDz2PUEZqTpgqwAtUd8nAPf5kjyFyND7P1VkOKc= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw= +go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y= +go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 h1:3d+S281UTjM+AbF31XSOYn1qXn3BgIdWl8HNEpx08Jk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0/go.mod h1:0+KuTDyKL4gjKCF75pHOX4wuzYDUZYfAQdSu43o+Z2I= +go.opentelemetry.io/otel/exporters/prometheus v0.38.1 h1:GwalIvFIx91qIA8qyAyqYj9lql5Ba2Oxj/jDG6+3UoU= +go.opentelemetry.io/otel/exporters/prometheus v0.38.1/go.mod h1:6K7aBvWHXRUcNYFSj6Hi5hHwzA1jYflG/T8snrX4dYM= +go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg= +go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY= +go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= +go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= +go.opentelemetry.io/otel/sdk/metric v0.38.1 h1:EkO5wI4NT/fUaoPMGc0fKV28JaWe7q4vfVpEVasGb+8= +go.opentelemetry.io/otel/sdk/metric v0.38.1/go.mod h1:Rn4kSXFF9ZQZ5lL1pxQjCbK4seiO+U7s0ncmIFJaj34= +go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0= +go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo= +go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= +go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca h1:VdD38733bfYv5tUZwEIskMM93VanwNIi5bIKnDrJdEY= +go.starlark.net v0.0.0-20230525235612-a134d8f9ddca/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +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/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= +go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -148,19 +509,23 @@ go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA= +golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= +golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= 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-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -170,60 +535,139 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/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.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc h1:bH6xUXay0AIFMElXG2rQ4uiE+7ncwtiOdPfYK1NK2XA= +golang.org/x/telemetry v0.0.0-20251203150158-8fff8a5912fc/go.mod h1:hKdjCMrbv9skySur+Nek8Hd0uJ0GuxJIoIX2payrIdQ= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= 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.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= -golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= -golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= +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/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= +golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= 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 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= -google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= +gomodules.xyz/envconfig v1.3.1-0.20190308184047-426f31af0d45 h1:juzzlx91nWAOsHuOVfXZPMXHtJEKouZvY9bBbwlOeYs= +gomodules.xyz/envconfig v1.3.1-0.20190308184047-426f31af0d45/go.mod h1:41y72mzHT7+jFNgyBpJRrZWuZJcLmLrTpq6iGgOFJMQ= +gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= +gomodules.xyz/jsonpatch/v2 v2.2.0/go.mod h1:WXp+iVDkoLQqPudfQ9GBlwB2eZ5DKOnjQZCYdOS8GPY= +gomodules.xyz/notify v0.1.1 h1:1tTuoyswmPvzqPCTEDQK8SZ3ukCxLsonAAwst2+y1a0= +gomodules.xyz/notify v0.1.1/go.mod h1:QgQyU4xEA/plJcDeT66J2Go2V7U4c0pD9wjo7HfFil4= +google.golang.org/api v0.162.0 h1:Vhs54HkaEpkMBdgGdOT2P6F0csGG/vxDS0hWHJzmmps= +google.golang.org/api v0.162.0/go.mod h1:6SulDkfoBIg4NFmCuZ39XeeAgSHCPecfSUuDyYlAHs0= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= +google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= +gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE= +gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.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= k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= +k8s.io/apiextensions-apiserver v0.29.3 h1:9HF+EtZaVpFjStakF4yVufnXGPRppWFEQ87qnO91YeI= +k8s.io/apiextensions-apiserver v0.29.3/go.mod h1:po0XiY5scnpJfFizNGo6puNU6Fq6D70UJY2Cb2KwAVc= k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apiserver v0.29.3 h1:xR7ELlJ/BZSr2n4CnD3lfA4gzFivh0wwfNfz9L0WZcE= +k8s.io/apiserver v0.29.3/go.mod h1:hrvXlwfRulbMbBgmWRQlFru2b/JySDpmzvQwwk4GUOs= +k8s.io/cli-runtime v0.35.0 h1:PEJtYS/Zr4p20PfZSLCbY6YvaoLrfByd6THQzPworUE= +k8s.io/cli-runtime v0.35.0/go.mod h1:VBRvHzosVAoVdP3XwUQn1Oqkvaa8facnokNkD7jOTMY= k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/cloud-provider v0.35.0 h1:syiBCQbKh2gho/S1BkIl006Dc44pV8eAtGZmv5NMe7M= +k8s.io/cloud-provider v0.35.0/go.mod h1:7grN+/Nt5Hf7tnSGPT3aErt4K7aQpygyCrGpbrQbzNc= +k8s.io/cluster-bootstrap v0.25.8 h1:2JoXlDAnki1rmYMdrExP5tYXJgJhCERYHtAbucjZgs8= +k8s.io/cluster-bootstrap v0.25.8/go.mod h1:O7q/A8Os259t1Tm2S9Zn9XipZ9eej0AfApj1htCT0Lc= +k8s.io/code-generator v0.34.1 h1:WpphT26E+j7tEgIUfFr5WfbJrktCGzB3JoJH9149xYc= +k8s.io/code-generator v0.34.1/go.mod h1:DeWjekbDnJWRwpw3s0Jat87c+e0TgkxoR4ar608yqvg= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= +k8s.io/component-helpers v0.35.0 h1:wcXv7HJRksgVjM4VlXJ1CNFBpyDHruRI99RrBtrJceA= +k8s.io/component-helpers v0.35.0/go.mod h1:ahX0m/LTYmu7fL3W8zYiIwnQ/5gT28Ex4o2pymF63Co= +k8s.io/controller-manager v0.29.3 h1:pvm3mirypgW7kM6dHRk6O5ANZj4bZTWirfk5gO6RlCo= +k8s.io/controller-manager v0.29.3/go.mod h1:RNxpf0d1WAo59sOLd32isWJP0oZ7Zxr+q4VEEaSq4gk= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01 h1:pWEwq4Asjm4vjW7vcsmijwBhOr1/shsbSYiWXmNGlks= +k8s.io/gengo v0.0.0-20230829151522-9cce18d56c01/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f h1:SLb+kxmzfA87x4E4brQzB33VBbT2+x7Zq9ROIHmGn9Q= +k8s.io/gengo/v2 v2.0.0-20250604051438-85fd79dbfd9f/go.mod h1:EJykeLsmFC60UQbYJezXkEsG2FLrt0GPNkU5iK5GWxU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/kms v0.29.3 h1:ReljsAUhYlm2spdT4yXmY+9a8x8dc/OT4mXvwQPPteQ= +k8s.io/kms v0.29.3/go.mod h1:TBGbJKpRUMk59neTMDMddjIDL+D4HuFUbpuiuzmOPg0= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= +k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= k8s.io/kubectl v0.35.0 h1:cL/wJKHDe8E8+rP3G7avnymcMg6bH6JEcR5w5uo06wc= k8s.io/kubectl v0.35.0/go.mod h1:VR5/TSkYyxZwrRwY5I5dDq6l5KXmiCb+9w8IKplk3Qo= -k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2 h1:OfgiEo21hGiwx1oJUU5MpEaeOEg6coWndBkZF/lkFuE= -k8s.io/utils v0.0.0-20251222233032-718f0e51e6d2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/kubelet v0.35.0 h1:8cgJHCBCKLYuuQ7/Pxb/qWbJfX1LXIw7790ce9xHq7c= +k8s.io/kubelet v0.35.0/go.mod h1:ciRzAXn7C4z5iB7FhG1L2CGPPXLTVCABDlbXt/Zz8YA= +k8s.io/kubernetes v1.29.3 h1:EuOAKN4zpiP+kBx/0e9yS5iBkPSyLml19juOqZxBtDw= +k8s.io/kubernetes v1.29.3/go.mod h1:CP+Z+S9haxyB7J+nV6ywYry4dqlphArPXjcc0CsBVXc= +k8s.io/metrics v0.35.0 h1:xVFoqtAGm2dMNJAcB5TFZJPCen0uEqqNt52wW7ABbX8= +k8s.io/metrics v0.35.0/go.mod h1:g2Up4dcBygZi2kQSEQVDByFs+VUwepJMzzQLJJLpq4M= +k8s.io/mount-utils v0.26.4 h1:yAtBd7D/AajxMhYXq1nO2sDuRCqwPtNspvJy0vqsNPQ= +k8s.io/mount-utils v0.26.4/go.mod h1:95yx9K6N37y8YZ0/lUh9U6ITosMODNaW0/v4wvaa0Xw= +k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= +k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +monis.app/mlog v0.0.2 h1:zyEt5GsmLhTafXhwidtOFriIVVdejUNc44TzDn/OZc4= +monis.app/mlog v0.0.2/go.mod h1:LtOpnndFuRGqnLBwzBvpA1DaoKuud2/moLzYXIiNl1s= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0 h1:TgtAeesdhpm2SGwkQasmbeqDo8th5wOBA5h/AjTKA4I= +sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.28.0/go.mod h1:VHVDI/KrK4fjnV61bE2g3sA7tiETLn8sooImelsCx3Y= +sigs.k8s.io/controller-runtime v0.14.6 h1:oxstGVvXGNnMvY7TAESYk+lzr6S3V5VFxQ6d92KcwQA= +sigs.k8s.io/controller-runtime v0.14.6/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= +sigs.k8s.io/kustomize/api v0.20.1 h1:iWP1Ydh3/lmldBnH/S5RXgT98vWYMaTUL1ADcr+Sv7I= +sigs.k8s.io/kustomize/api v0.20.1/go.mod h1:t6hUFxO+Ph0VxIk1sKp1WS0dOjbPCtLJ4p8aADLwqjM= +sigs.k8s.io/kustomize/kustomize/v5 v5.7.1 h1:sYJsarwy/SDJfjjLMUqwFDGPwzUtMOQ1i1Ed49+XSbw= +sigs.k8s.io/kustomize/kustomize/v5 v5.7.1/go.mod h1:+5/SrBcJ4agx1SJknGuR/c9thwRSKLxnKoI5BzXFaLU= +sigs.k8s.io/kustomize/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/secrets-store-csi-driver v1.5.5 h1:LJDpDL5TILhlP68nGvtGSlJFxSDgAD2m148NT0Ts7os= sigs.k8s.io/secrets-store-csi-driver v1.5.5/go.mod h1:i2WqLicYH00hrTG3JAzICPMF4HL4KMEORlDt9UQoZLk= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= -sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1 h1:JrhdFMqOd/+3ByqlP2I45kTOZmTRLBUm5pvRjeheg7E= +sigs.k8s.io/structured-merge-diff/v6 v6.3.1/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/pkg/app/app.go b/internal/pkg/app/app.go index 8d09188..734fd2a 100644 --- a/internal/pkg/app/app.go +++ b/internal/pkg/app/app.go @@ -4,6 +4,6 @@ import "github.com/stakater/Reloader/internal/pkg/cmd" // Run runs the command func Run() error { - cmd := cmd.NewReloaderCommand() - return cmd.Execute() + rootCmd := cmd.NewReloaderCommand() + return rootCmd.Execute() } diff --git a/internal/pkg/callbacks/rolling_upgrade.go b/internal/pkg/callbacks/rolling_upgrade.go index 13e5a63..f307c68 100644 --- a/internal/pkg/callbacks/rolling_upgrade.go +++ b/internal/pkg/callbacks/rolling_upgrade.go @@ -7,8 +7,6 @@ import ( "time" "github.com/sirupsen/logrus" - "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/pkg/kube" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" @@ -16,6 +14,9 @@ import ( "k8s.io/apimachinery/pkg/runtime" patchtypes "k8s.io/apimachinery/pkg/types" + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/pkg/kube" + "maps" argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" @@ -265,158 +266,254 @@ func GetRolloutItems(clients kube.Clients, namespace string) []runtime.Object { // GetDeploymentAnnotations returns the annotations of given deployment func GetDeploymentAnnotations(item runtime.Object) map[string]string { - if item.(*appsv1.Deployment).Annotations == nil { - item.(*appsv1.Deployment).Annotations = make(map[string]string) + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil } - return item.(*appsv1.Deployment).Annotations + if deployment.Annotations == nil { + deployment.Annotations = make(map[string]string) + } + return deployment.Annotations } // GetCronJobAnnotations returns the annotations of given cronjob func GetCronJobAnnotations(item runtime.Object) map[string]string { - if item.(*batchv1.CronJob).Annotations == nil { - item.(*batchv1.CronJob).Annotations = make(map[string]string) + cronJob, ok := item.(*batchv1.CronJob) + if !ok { + return nil } - return item.(*batchv1.CronJob).Annotations + if cronJob.Annotations == nil { + cronJob.Annotations = make(map[string]string) + } + return cronJob.Annotations } // GetJobAnnotations returns the annotations of given job func GetJobAnnotations(item runtime.Object) map[string]string { - if item.(*batchv1.Job).Annotations == nil { - item.(*batchv1.Job).Annotations = make(map[string]string) + job, ok := item.(*batchv1.Job) + if !ok { + return nil } - return item.(*batchv1.Job).Annotations + if job.Annotations == nil { + job.Annotations = make(map[string]string) + } + return job.Annotations } // GetDaemonSetAnnotations returns the annotations of given daemonSet func GetDaemonSetAnnotations(item runtime.Object) map[string]string { - if item.(*appsv1.DaemonSet).Annotations == nil { - item.(*appsv1.DaemonSet).Annotations = make(map[string]string) + daemonSet, ok := item.(*appsv1.DaemonSet) + if !ok { + return nil } - return item.(*appsv1.DaemonSet).Annotations + if daemonSet.Annotations == nil { + daemonSet.Annotations = make(map[string]string) + } + return daemonSet.Annotations } // GetStatefulSetAnnotations returns the annotations of given statefulSet func GetStatefulSetAnnotations(item runtime.Object) map[string]string { - if item.(*appsv1.StatefulSet).Annotations == nil { - item.(*appsv1.StatefulSet).Annotations = make(map[string]string) + statefulSet, ok := item.(*appsv1.StatefulSet) + if !ok { + return nil } - return item.(*appsv1.StatefulSet).Annotations + if statefulSet.Annotations == nil { + statefulSet.Annotations = make(map[string]string) + } + return statefulSet.Annotations } // GetRolloutAnnotations returns the annotations of given rollout func GetRolloutAnnotations(item runtime.Object) map[string]string { - if item.(*argorolloutv1alpha1.Rollout).Annotations == nil { - item.(*argorolloutv1alpha1.Rollout).Annotations = make(map[string]string) + rollout, ok := item.(*argorolloutv1alpha1.Rollout) + if !ok { + return nil } - return item.(*argorolloutv1alpha1.Rollout).Annotations + if rollout.Annotations == nil { + rollout.Annotations = make(map[string]string) + } + return rollout.Annotations } // GetDeploymentPodAnnotations returns the pod's annotations of given deployment func GetDeploymentPodAnnotations(item runtime.Object) map[string]string { - if item.(*appsv1.Deployment).Spec.Template.Annotations == nil { - item.(*appsv1.Deployment).Spec.Template.Annotations = make(map[string]string) + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil } - return item.(*appsv1.Deployment).Spec.Template.Annotations + if deployment.Spec.Template.Annotations == nil { + deployment.Spec.Template.Annotations = make(map[string]string) + } + return deployment.Spec.Template.Annotations } // GetCronJobPodAnnotations returns the pod's annotations of given cronjob func GetCronJobPodAnnotations(item runtime.Object) map[string]string { - if item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Annotations == nil { - item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Annotations = make(map[string]string) + cronJob, ok := item.(*batchv1.CronJob) + if !ok { + return nil } - return item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Annotations + if cronJob.Spec.JobTemplate.Spec.Template.Annotations == nil { + cronJob.Spec.JobTemplate.Spec.Template.Annotations = make(map[string]string) + } + return cronJob.Spec.JobTemplate.Spec.Template.Annotations } // GetJobPodAnnotations returns the pod's annotations of given job func GetJobPodAnnotations(item runtime.Object) map[string]string { - if item.(*batchv1.Job).Spec.Template.Annotations == nil { - item.(*batchv1.Job).Spec.Template.Annotations = make(map[string]string) + job, ok := item.(*batchv1.Job) + if !ok { + return nil } - return item.(*batchv1.Job).Spec.Template.Annotations + if job.Spec.Template.Annotations == nil { + job.Spec.Template.Annotations = make(map[string]string) + } + return job.Spec.Template.Annotations } // GetDaemonSetPodAnnotations returns the pod's annotations of given daemonSet func GetDaemonSetPodAnnotations(item runtime.Object) map[string]string { - if item.(*appsv1.DaemonSet).Spec.Template.Annotations == nil { - item.(*appsv1.DaemonSet).Spec.Template.Annotations = make(map[string]string) + daemonSet, ok := item.(*appsv1.DaemonSet) + if !ok { + return nil } - return item.(*appsv1.DaemonSet).Spec.Template.Annotations + if daemonSet.Spec.Template.Annotations == nil { + daemonSet.Spec.Template.Annotations = make(map[string]string) + } + return daemonSet.Spec.Template.Annotations } // GetStatefulSetPodAnnotations returns the pod's annotations of given statefulSet func GetStatefulSetPodAnnotations(item runtime.Object) map[string]string { - if item.(*appsv1.StatefulSet).Spec.Template.Annotations == nil { - item.(*appsv1.StatefulSet).Spec.Template.Annotations = make(map[string]string) + statefulSet, ok := item.(*appsv1.StatefulSet) + if !ok { + return nil } - return item.(*appsv1.StatefulSet).Spec.Template.Annotations + if statefulSet.Spec.Template.Annotations == nil { + statefulSet.Spec.Template.Annotations = make(map[string]string) + } + return statefulSet.Spec.Template.Annotations } // GetRolloutPodAnnotations returns the pod's annotations of given rollout func GetRolloutPodAnnotations(item runtime.Object) map[string]string { - if item.(*argorolloutv1alpha1.Rollout).Spec.Template.Annotations == nil { - item.(*argorolloutv1alpha1.Rollout).Spec.Template.Annotations = make(map[string]string) + rollout, ok := item.(*argorolloutv1alpha1.Rollout) + if !ok { + return nil } - return item.(*argorolloutv1alpha1.Rollout).Spec.Template.Annotations + if rollout.Spec.Template.Annotations == nil { + rollout.Spec.Template.Annotations = make(map[string]string) + } + return rollout.Spec.Template.Annotations } // GetDeploymentContainers returns the containers of given deployment func GetDeploymentContainers(item runtime.Object) []v1.Container { - return item.(*appsv1.Deployment).Spec.Template.Spec.Containers + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil + } + return deployment.Spec.Template.Spec.Containers } // GetCronJobContainers returns the containers of given cronjob func GetCronJobContainers(item runtime.Object) []v1.Container { - return item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.Containers + cronJob, ok := item.(*batchv1.CronJob) + if !ok { + return nil + } + return cronJob.Spec.JobTemplate.Spec.Template.Spec.Containers } // GetJobContainers returns the containers of given job func GetJobContainers(item runtime.Object) []v1.Container { - return item.(*batchv1.Job).Spec.Template.Spec.Containers + job, ok := item.(*batchv1.Job) + if !ok { + return nil + } + return job.Spec.Template.Spec.Containers } // GetDaemonSetContainers returns the containers of given daemonSet func GetDaemonSetContainers(item runtime.Object) []v1.Container { - return item.(*appsv1.DaemonSet).Spec.Template.Spec.Containers + daemonSet, ok := item.(*appsv1.DaemonSet) + if !ok { + return nil + } + return daemonSet.Spec.Template.Spec.Containers } // GetStatefulSetContainers returns the containers of given statefulSet func GetStatefulSetContainers(item runtime.Object) []v1.Container { - return item.(*appsv1.StatefulSet).Spec.Template.Spec.Containers + statefulSet, ok := item.(*appsv1.StatefulSet) + if !ok { + return nil + } + return statefulSet.Spec.Template.Spec.Containers } // GetRolloutContainers returns the containers of given rollout func GetRolloutContainers(item runtime.Object) []v1.Container { - return item.(*argorolloutv1alpha1.Rollout).Spec.Template.Spec.Containers + rollout, ok := item.(*argorolloutv1alpha1.Rollout) + if !ok { + return nil + } + return rollout.Spec.Template.Spec.Containers } // GetDeploymentInitContainers returns the containers of given deployment func GetDeploymentInitContainers(item runtime.Object) []v1.Container { - return item.(*appsv1.Deployment).Spec.Template.Spec.InitContainers + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil + } + return deployment.Spec.Template.Spec.InitContainers } // GetCronJobInitContainers returns the containers of given cronjob func GetCronJobInitContainers(item runtime.Object) []v1.Container { - return item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.InitContainers + cronJob, ok := item.(*batchv1.CronJob) + if !ok { + return nil + } + return cronJob.Spec.JobTemplate.Spec.Template.Spec.InitContainers } // GetJobInitContainers returns the containers of given job func GetJobInitContainers(item runtime.Object) []v1.Container { - return item.(*batchv1.Job).Spec.Template.Spec.InitContainers + job, ok := item.(*batchv1.Job) + if !ok { + return nil + } + return job.Spec.Template.Spec.InitContainers } // GetDaemonSetInitContainers returns the containers of given daemonSet func GetDaemonSetInitContainers(item runtime.Object) []v1.Container { - return item.(*appsv1.DaemonSet).Spec.Template.Spec.InitContainers + daemonSet, ok := item.(*appsv1.DaemonSet) + if !ok { + return nil + } + return daemonSet.Spec.Template.Spec.InitContainers } // GetStatefulSetInitContainers returns the containers of given statefulSet func GetStatefulSetInitContainers(item runtime.Object) []v1.Container { - return item.(*appsv1.StatefulSet).Spec.Template.Spec.InitContainers + statefulSet, ok := item.(*appsv1.StatefulSet) + if !ok { + return nil + } + return statefulSet.Spec.Template.Spec.InitContainers } // GetRolloutInitContainers returns the containers of given rollout func GetRolloutInitContainers(item runtime.Object) []v1.Container { - return item.(*argorolloutv1alpha1.Rollout).Spec.Template.Spec.InitContainers + rollout, ok := item.(*argorolloutv1alpha1.Rollout) + if !ok { + return nil + } + return rollout.Spec.Template.Spec.InitContainers } // GetPatchTemplates returns patch templates @@ -430,21 +527,30 @@ func GetPatchTemplates() PatchTemplates { // UpdateDeployment performs rolling upgrade on deployment func UpdateDeployment(clients kube.Clients, namespace string, resource runtime.Object) error { - deployment := resource.(*appsv1.Deployment) + deployment, ok := resource.(*appsv1.Deployment) + if !ok { + return errors.New("resource is not a Deployment") + } _, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Update(context.TODO(), deployment, meta_v1.UpdateOptions{FieldManager: "Reloader"}) return err } // PatchDeployment performs rolling upgrade on deployment func PatchDeployment(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error { - deployment := resource.(*appsv1.Deployment) + deployment, ok := resource.(*appsv1.Deployment) + if !ok { + return errors.New("resource is not a Deployment") + } _, err := clients.KubernetesClient.AppsV1().Deployments(namespace).Patch(context.TODO(), deployment.Name, patchType, bytes, meta_v1.PatchOptions{FieldManager: "Reloader"}) return err } // CreateJobFromCronjob performs rolling upgrade on cronjob func CreateJobFromCronjob(clients kube.Clients, namespace string, resource runtime.Object) error { - cronJob := resource.(*batchv1.CronJob) + cronJob, ok := resource.(*batchv1.CronJob) + if !ok { + return errors.New("resource is not a CronJob") + } annotations := make(map[string]string) annotations["cronjob.kubernetes.io/instantiate"] = "manual" @@ -470,7 +576,10 @@ func PatchCronJob(clients kube.Clients, namespace string, resource runtime.Objec // ReCreateJobFromjob performs rolling upgrade on job func ReCreateJobFromjob(clients kube.Clients, namespace string, resource runtime.Object) error { - oldJob := resource.(*batchv1.Job) + oldJob, ok := resource.(*batchv1.Job) + if !ok { + return errors.New("resource is not a Job") + } job := oldJob.DeepCopy() // Delete the old job @@ -506,33 +615,48 @@ func PatchJob(clients kube.Clients, namespace string, resource runtime.Object, p // UpdateDaemonSet performs rolling upgrade on daemonSet func UpdateDaemonSet(clients kube.Clients, namespace string, resource runtime.Object) error { - daemonSet := resource.(*appsv1.DaemonSet) + daemonSet, ok := resource.(*appsv1.DaemonSet) + if !ok { + return errors.New("resource is not a DaemonSet") + } _, err := clients.KubernetesClient.AppsV1().DaemonSets(namespace).Update(context.TODO(), daemonSet, meta_v1.UpdateOptions{FieldManager: "Reloader"}) return err } func PatchDaemonSet(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error { - daemonSet := resource.(*appsv1.DaemonSet) + daemonSet, ok := resource.(*appsv1.DaemonSet) + if !ok { + return errors.New("resource is not a DaemonSet") + } _, err := clients.KubernetesClient.AppsV1().DaemonSets(namespace).Patch(context.TODO(), daemonSet.Name, patchType, bytes, meta_v1.PatchOptions{FieldManager: "Reloader"}) return err } // UpdateStatefulSet performs rolling upgrade on statefulSet func UpdateStatefulSet(clients kube.Clients, namespace string, resource runtime.Object) error { - statefulSet := resource.(*appsv1.StatefulSet) + statefulSet, ok := resource.(*appsv1.StatefulSet) + if !ok { + return errors.New("resource is not a StatefulSet") + } _, err := clients.KubernetesClient.AppsV1().StatefulSets(namespace).Update(context.TODO(), statefulSet, meta_v1.UpdateOptions{FieldManager: "Reloader"}) return err } func PatchStatefulSet(clients kube.Clients, namespace string, resource runtime.Object, patchType patchtypes.PatchType, bytes []byte) error { - statefulSet := resource.(*appsv1.StatefulSet) + statefulSet, ok := resource.(*appsv1.StatefulSet) + if !ok { + return errors.New("resource is not a StatefulSet") + } _, err := clients.KubernetesClient.AppsV1().StatefulSets(namespace).Patch(context.TODO(), statefulSet.Name, patchType, bytes, meta_v1.PatchOptions{FieldManager: "Reloader"}) return err } // UpdateRollout performs rolling upgrade on rollout func UpdateRollout(clients kube.Clients, namespace string, resource runtime.Object) error { - rollout := resource.(*argorolloutv1alpha1.Rollout) + rollout, ok := resource.(*argorolloutv1alpha1.Rollout) + if !ok { + return errors.New("resource is not a Rollout") + } strategy := rollout.GetAnnotations()[options.RolloutStrategyAnnotation] var err error switch options.ToArgoRolloutStrategy(strategy) { @@ -550,30 +674,54 @@ func PatchRollout(clients kube.Clients, namespace string, resource runtime.Objec // GetDeploymentVolumes returns the Volumes of given deployment func GetDeploymentVolumes(item runtime.Object) []v1.Volume { - return item.(*appsv1.Deployment).Spec.Template.Spec.Volumes + deployment, ok := item.(*appsv1.Deployment) + if !ok { + return nil + } + return deployment.Spec.Template.Spec.Volumes } // GetCronJobVolumes returns the Volumes of given cronjob func GetCronJobVolumes(item runtime.Object) []v1.Volume { - return item.(*batchv1.CronJob).Spec.JobTemplate.Spec.Template.Spec.Volumes + cronJob, ok := item.(*batchv1.CronJob) + if !ok { + return nil + } + return cronJob.Spec.JobTemplate.Spec.Template.Spec.Volumes } // GetJobVolumes returns the Volumes of given job func GetJobVolumes(item runtime.Object) []v1.Volume { - return item.(*batchv1.Job).Spec.Template.Spec.Volumes + job, ok := item.(*batchv1.Job) + if !ok { + return nil + } + return job.Spec.Template.Spec.Volumes } // GetDaemonSetVolumes returns the Volumes of given daemonSet func GetDaemonSetVolumes(item runtime.Object) []v1.Volume { - return item.(*appsv1.DaemonSet).Spec.Template.Spec.Volumes + daemonSet, ok := item.(*appsv1.DaemonSet) + if !ok { + return nil + } + return daemonSet.Spec.Template.Spec.Volumes } // GetStatefulSetVolumes returns the Volumes of given statefulSet func GetStatefulSetVolumes(item runtime.Object) []v1.Volume { - return item.(*appsv1.StatefulSet).Spec.Template.Spec.Volumes + statefulSet, ok := item.(*appsv1.StatefulSet) + if !ok { + return nil + } + return statefulSet.Spec.Template.Spec.Volumes } // GetRolloutVolumes returns the Volumes of given rollout func GetRolloutVolumes(item runtime.Object) []v1.Volume { - return item.(*argorolloutv1alpha1.Rollout).Spec.Template.Spec.Volumes + rollout, ok := item.(*argorolloutv1alpha1.Rollout) + if !ok { + return nil + } + return rollout.Spec.Template.Spec.Volumes } diff --git a/internal/pkg/controller/controller.go b/internal/pkg/controller/controller.go index 1a51d9a..bcc2d8c 100644 --- a/internal/pkg/controller/controller.go +++ b/internal/pkg/controller/controller.go @@ -6,12 +6,6 @@ import ( "time" "github.com/sirupsen/logrus" - "github.com/stakater/Reloader/internal/pkg/constants" - "github.com/stakater/Reloader/internal/pkg/handler" - "github.com/stakater/Reloader/internal/pkg/metrics" - "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/internal/pkg/util" - "github.com/stakater/Reloader/pkg/kube" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/fields" @@ -24,12 +18,18 @@ import ( "k8s.io/client-go/util/workqueue" "k8s.io/kubectl/pkg/scheme" csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" + + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/handler" + "github.com/stakater/Reloader/internal/pkg/metrics" + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/internal/pkg/util" + "github.com/stakater/Reloader/pkg/kube" ) // Controller for checking events type Controller struct { client kubernetes.Interface - indexer cache.Indexer queue workqueue.TypedRateLimitingInterface[any] informer cache.Controller namespace string @@ -48,7 +48,9 @@ var selectedNamespacesCache []string // NewController for initializing a Controller func NewController( - client kubernetes.Interface, resource string, namespace string, ignoredNamespaces []string, namespaceLabelSelector string, resourceLabelSelector string, collectors metrics.Collectors) (*Controller, error) { + client kubernetes.Interface, resource string, namespace string, ignoredNamespaces []string, namespaceLabelSelector string, resourceLabelSelector string, collectors metrics.Collectors) ( + *Controller, error, +) { if options.SyncAfterRestart { secretControllerInitialized = true @@ -67,17 +69,18 @@ func NewController( eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{ Interface: client.CoreV1().Events(""), }) - recorder := eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: fmt.Sprintf("reloader-%s", resource)}) + recorder := eventBroadcaster.NewRecorder(scheme.Scheme, + v1.EventSource{Component: fmt.Sprintf("reloader-%s", resource)}) queue := workqueue.NewTypedRateLimitingQueue(workqueue.DefaultTypedControllerRateLimiter[any]()) - optionsModifier := func(options *metav1.ListOptions) { + optionsModifier := func(opts *metav1.ListOptions) { if resource == "namespaces" { - options.LabelSelector = c.namespaceSelector + opts.LabelSelector = c.namespaceSelector } else if len(c.resourceSelector) > 0 { - options.LabelSelector = c.resourceSelector + opts.LabelSelector = c.resourceSelector } else { - options.FieldSelector = fields.Everything().String() + opts.FieldSelector = fields.Everything().String() } } @@ -299,7 +302,12 @@ func (c *Controller) processNextItem() bool { startTime := time.Now() // Invoke the method containing the business logic - err := resourceHandler.(handler.ResourceHandler).Handle() + rh, ok := resourceHandler.(handler.ResourceHandler) + if !ok { + logrus.Errorf("Invalid resource handler type: %T", resourceHandler) + return true + } + err := rh.Handle() duration := time.Since(startTime) diff --git a/internal/pkg/controller/controller_test.go b/internal/pkg/controller/controller_test.go index 250dd1f..e16b3df 100644 --- a/internal/pkg/controller/controller_test.go +++ b/internal/pkg/controller/controller_test.go @@ -1,17 +1,46 @@ package controller import ( + "errors" "testing" + "time" - "github.com/stakater/Reloader/internal/pkg/handler" - "github.com/stakater/Reloader/internal/pkg/metrics" - "github.com/stakater/Reloader/internal/pkg/options" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/util/workqueue" + + "github.com/stakater/Reloader/internal/pkg/handler" + "github.com/stakater/Reloader/internal/pkg/metrics" + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/pkg/common" ) +// mockResourceHandler implements handler.ResourceHandler and handler.TimedHandler for testing. +type mockResourceHandler struct { + handleErr error + handleCalls int + enqueueTime time.Time +} + +func (m *mockResourceHandler) Handle() error { + m.handleCalls++ + return m.handleErr +} + +func (m *mockResourceHandler) GetConfig() (common.Config, string) { + return common.Config{ + ResourceName: "test-resource", + Namespace: "test-ns", + Type: "configmap", + SHAValue: "sha256:test", + }, "test-resource" +} + +func (m *mockResourceHandler) GetEnqueueTime() time.Time { + return m.enqueueTime +} + // resetGlobalState resets global variables between tests func resetGlobalState() { secretControllerInitialized = false @@ -104,11 +133,13 @@ func TestResourceInIgnoredNamespace(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - c := newTestController(tt.ignoredNamespaces, "") - result := c.resourceInIgnoredNamespace(tt.resource) - assert.Equal(t, tt.expected, result) - }) + t.Run( + tt.name, func(t *testing.T) { + c := newTestController(tt.ignoredNamespaces, "") + result := c.resourceInIgnoredNamespace(tt.resource) + assert.Equal(t, tt.expected, result) + }, + ) } } @@ -190,14 +221,16 @@ func TestResourceInSelectedNamespaces(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resetGlobalState() - selectedNamespacesCache = tt.cachedNamespaces + t.Run( + tt.name, func(t *testing.T) { + resetGlobalState() + selectedNamespacesCache = tt.cachedNamespaces - c := newTestController([]string{}, tt.namespaceSelector) - result := c.resourceInSelectedNamespaces(tt.resource) - assert.Equal(t, tt.expected, result) - }) + c := newTestController([]string{}, tt.namespaceSelector) + result := c.resourceInSelectedNamespaces(tt.resource) + assert.Equal(t, tt.expected, result) + }, + ) } } @@ -226,65 +259,67 @@ func TestAddSelectedNamespaceToCache(t *testing.T) { func TestRemoveSelectedNamespaceFromCache(t *testing.T) { tests := []struct { - name string - initialCache []string + name string + initialCache []string namespaceToRemove string - expectedCache []string + expectedCache []string }{ { - name: "Remove existing namespace", - initialCache: []string{"ns-1", "ns-2", "ns-3"}, + name: "Remove existing namespace", + initialCache: []string{"ns-1", "ns-2", "ns-3"}, namespaceToRemove: "ns-2", - expectedCache: []string{"ns-1", "ns-3"}, + expectedCache: []string{"ns-1", "ns-3"}, }, { - name: "Remove non-existing namespace", - initialCache: []string{"ns-1", "ns-2"}, + name: "Remove non-existing namespace", + initialCache: []string{"ns-1", "ns-2"}, namespaceToRemove: "ns-3", - expectedCache: []string{"ns-1", "ns-2"}, + expectedCache: []string{"ns-1", "ns-2"}, }, { - name: "Remove from empty cache", - initialCache: []string{}, + name: "Remove from empty cache", + initialCache: []string{}, namespaceToRemove: "ns-1", - expectedCache: []string{}, + expectedCache: []string{}, }, { - name: "Remove only namespace", - initialCache: []string{"ns-1"}, + name: "Remove only namespace", + initialCache: []string{"ns-1"}, namespaceToRemove: "ns-1", - expectedCache: []string{}, + expectedCache: []string{}, }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resetGlobalState() - selectedNamespacesCache = tt.initialCache + t.Run( + tt.name, func(t *testing.T) { + resetGlobalState() + selectedNamespacesCache = tt.initialCache - c := newTestController([]string{}, "env=prod") - ns := v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{Name: tt.namespaceToRemove}, - } - c.removeSelectedNamespaceFromCache(ns) + c := newTestController([]string{}, "env=prod") + ns := v1.Namespace{ + ObjectMeta: metav1.ObjectMeta{Name: tt.namespaceToRemove}, + } + c.removeSelectedNamespaceFromCache(ns) - assert.Equal(t, tt.expectedCache, selectedNamespacesCache) - }) + assert.Equal(t, tt.expectedCache, selectedNamespacesCache) + }, + ) } } func TestAddHandler(t *testing.T) { tests := []struct { - name string - reloadOnCreate string - ignoredNamespaces []string - resource interface{} - controllersInit bool - expectQueueItem bool + name string + reloadOnCreate string + ignoredNamespaces []string + resource interface{} + controllersInit bool + expectQueueItem bool }{ { - name: "Namespace resource - should not queue", - reloadOnCreate: "true", + name: "Namespace resource - should not queue", + reloadOnCreate: "true", ignoredNamespaces: []string{}, resource: &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: "test-ns"}, @@ -293,8 +328,8 @@ func TestAddHandler(t *testing.T) { expectQueueItem: false, }, { - name: "ReloadOnCreate disabled", - reloadOnCreate: "false", + name: "ReloadOnCreate disabled", + reloadOnCreate: "false", ignoredNamespaces: []string{}, resource: &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -306,8 +341,8 @@ func TestAddHandler(t *testing.T) { expectQueueItem: false, }, { - name: "ConfigMap in ignored namespace", - reloadOnCreate: "true", + name: "ConfigMap in ignored namespace", + reloadOnCreate: "true", ignoredNamespaces: []string{"kube-system"}, resource: &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -319,8 +354,8 @@ func TestAddHandler(t *testing.T) { expectQueueItem: false, }, { - name: "Controllers not initialized", - reloadOnCreate: "true", + name: "Controllers not initialized", + reloadOnCreate: "true", ignoredNamespaces: []string{}, resource: &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -332,8 +367,8 @@ func TestAddHandler(t *testing.T) { expectQueueItem: false, }, { - name: "Valid ConfigMap - should queue", - reloadOnCreate: "true", + name: "Valid ConfigMap - should queue", + reloadOnCreate: "true", ignoredNamespaces: []string{}, resource: &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -347,21 +382,23 @@ func TestAddHandler(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resetGlobalState() - options.ReloadOnCreate = tt.reloadOnCreate - secretControllerInitialized = tt.controllersInit - configmapControllerInitialized = tt.controllersInit + t.Run( + tt.name, func(t *testing.T) { + resetGlobalState() + options.ReloadOnCreate = tt.reloadOnCreate + secretControllerInitialized = tt.controllersInit + configmapControllerInitialized = tt.controllersInit - c := newTestController(tt.ignoredNamespaces, "") - c.Add(tt.resource) + c := newTestController(tt.ignoredNamespaces, "") + c.Add(tt.resource) - if tt.expectQueueItem { - assert.Equal(t, 1, c.queue.Len(), "Expected queue to have 1 item") - } else { - assert.Equal(t, 0, c.queue.Len(), "Expected queue to be empty") - } - }) + if tt.expectQueueItem { + assert.Equal(t, 1, c.queue.Len(), "Expected queue to have 1 item") + } else { + assert.Equal(t, 0, c.queue.Len(), "Expected queue to be empty") + } + }, + ) } } @@ -461,26 +498,28 @@ func TestUpdateHandler(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resetGlobalState() - if tt.cachedNamespaces != nil { - selectedNamespacesCache = tt.cachedNamespaces - } + t.Run( + tt.name, func(t *testing.T) { + resetGlobalState() + if tt.cachedNamespaces != nil { + selectedNamespacesCache = tt.cachedNamespaces + } - c := newTestController(tt.ignoredNamespaces, tt.namespaceSelector) - c.Update(tt.oldResource, tt.newResource) + c := newTestController(tt.ignoredNamespaces, tt.namespaceSelector) + c.Update(tt.oldResource, tt.newResource) - if tt.expectQueueItem { - assert.Equal(t, 1, c.queue.Len(), "Expected queue to have 1 item") - // Verify the queued item is the correct type - item, _ := c.queue.Get() - _, ok := item.(handler.ResourceUpdatedHandler) - assert.True(t, ok, "Expected ResourceUpdatedHandler in queue") - c.queue.Done(item) - } else { - assert.Equal(t, 0, c.queue.Len(), "Expected queue to be empty") - } - }) + if tt.expectQueueItem { + assert.Equal(t, 1, c.queue.Len(), "Expected queue to have 1 item") + // Verify the queued item is the correct type + item, _ := c.queue.Get() + _, ok := item.(handler.ResourceUpdatedHandler) + assert.True(t, ok, "Expected ResourceUpdatedHandler in queue") + c.queue.Done(item) + } else { + assert.Equal(t, 0, c.queue.Len(), "Expected queue to be empty") + } + }, + ) } } @@ -494,8 +533,8 @@ func TestDeleteHandler(t *testing.T) { expectQueueItem bool }{ { - name: "ReloadOnDelete disabled", - reloadOnDelete: "false", + name: "ReloadOnDelete disabled", + reloadOnDelete: "false", ignoredNamespaces: []string{}, resource: &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -507,8 +546,8 @@ func TestDeleteHandler(t *testing.T) { expectQueueItem: false, }, { - name: "ConfigMap in ignored namespace", - reloadOnDelete: "true", + name: "ConfigMap in ignored namespace", + reloadOnDelete: "true", ignoredNamespaces: []string{"kube-system"}, resource: &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -520,8 +559,8 @@ func TestDeleteHandler(t *testing.T) { expectQueueItem: false, }, { - name: "Controllers not initialized", - reloadOnDelete: "true", + name: "Controllers not initialized", + reloadOnDelete: "true", ignoredNamespaces: []string{}, resource: &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -533,8 +572,8 @@ func TestDeleteHandler(t *testing.T) { expectQueueItem: false, }, { - name: "Valid ConfigMap delete - should queue", - reloadOnDelete: "true", + name: "Valid ConfigMap delete - should queue", + reloadOnDelete: "true", ignoredNamespaces: []string{}, resource: &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -546,8 +585,8 @@ func TestDeleteHandler(t *testing.T) { expectQueueItem: true, }, { - name: "Namespace delete - updates cache", - reloadOnDelete: "false", // Disable to test cache update only + name: "Namespace delete - updates cache", + reloadOnDelete: "false", // Disable to test cache update only ignoredNamespaces: []string{}, resource: &v1.Namespace{ ObjectMeta: metav1.ObjectMeta{Name: "test-ns"}, @@ -558,64 +597,70 @@ func TestDeleteHandler(t *testing.T) { } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - resetGlobalState() - options.ReloadOnDelete = tt.reloadOnDelete - secretControllerInitialized = tt.controllersInit - configmapControllerInitialized = tt.controllersInit + t.Run( + tt.name, func(t *testing.T) { + resetGlobalState() + options.ReloadOnDelete = tt.reloadOnDelete + secretControllerInitialized = tt.controllersInit + configmapControllerInitialized = tt.controllersInit - c := newTestController(tt.ignoredNamespaces, "") - c.Delete(tt.resource) + c := newTestController(tt.ignoredNamespaces, "") + c.Delete(tt.resource) - if tt.expectQueueItem { - assert.Equal(t, 1, c.queue.Len(), "Expected queue to have 1 item") - // Verify the queued item is the correct type - item, _ := c.queue.Get() - _, ok := item.(handler.ResourceDeleteHandler) - assert.True(t, ok, "Expected ResourceDeleteHandler in queue") - c.queue.Done(item) - } else { - assert.Equal(t, 0, c.queue.Len(), "Expected queue to be empty") - } - }) + if tt.expectQueueItem { + assert.Equal(t, 1, c.queue.Len(), "Expected queue to have 1 item") + // Verify the queued item is the correct type + item, _ := c.queue.Get() + _, ok := item.(handler.ResourceDeleteHandler) + assert.True(t, ok, "Expected ResourceDeleteHandler in queue") + c.queue.Done(item) + } else { + assert.Equal(t, 0, c.queue.Len(), "Expected queue to be empty") + } + }, + ) } } func TestHandleErr(t *testing.T) { - t.Run("No error - should forget key", func(t *testing.T) { - resetGlobalState() - c := newTestController([]string{}, "") + t.Run( + "No error - should forget key", func(t *testing.T) { + resetGlobalState() + c := newTestController([]string{}, "") - key := "test-key" - // Add key to queue first - c.queue.Add(key) - item, _ := c.queue.Get() + key := "test-key" + // Add key to queue first + c.queue.Add(key) + item, _ := c.queue.Get() - // Handle with no error - c.handleErr(nil, item) - c.queue.Done(item) + // Handle with no error + c.handleErr(nil, item) + c.queue.Done(item) - // Key should be forgotten (NumRequeues should be 0) - assert.Equal(t, 0, c.queue.NumRequeues(key)) - }) + // Key should be forgotten (NumRequeues should be 0) + assert.Equal(t, 0, c.queue.NumRequeues(key)) + }, + ) - t.Run("Error at max retries - should drop key", func(t *testing.T) { - resetGlobalState() - c := newTestController([]string{}, "") + t.Run( + "Error at max retries - should drop key", func(t *testing.T) { + resetGlobalState() + c := newTestController([]string{}, "") - key := "test-key-max" + key := "test-key-max" - // Simulate 5 previous failures (max retries) - for range 5 { - c.queue.AddRateLimited(key) - } + // Simulate 5 previous failures (max retries) + for range 5 { + c.queue.AddRateLimited(key) + } - // After max retries, handleErr should forget the key - c.handleErr(assert.AnError, key) + // After max retries, handleErr should forget the key + c.handleErr(assert.AnError, key) - // Key should be forgotten - assert.Equal(t, 0, c.queue.NumRequeues(key)) - }) + // Key should be forgotten + assert.Equal(t, 0, c.queue.NumRequeues(key)) + }, + ) } func TestAddHandlerWithNamespaceEvent(t *testing.T) { @@ -654,3 +699,57 @@ func TestDeleteHandlerWithNamespaceEvent(t *testing.T) { assert.Contains(t, selectedNamespacesCache, "ns-2") assert.Equal(t, 0, c.queue.Len(), "Namespace delete should not queue anything") } + +func TestProcessNextItem(t *testing.T) { + tests := []struct { + name string + handler *mockResourceHandler + expectContinue bool + expectCalls int + }{ + { + name: "Successful handler execution", + handler: &mockResourceHandler{ + handleErr: nil, + enqueueTime: time.Now().Add(-10 * time.Millisecond), + }, + expectContinue: true, + expectCalls: 1, + }, + { + name: "Handler returns error", + handler: &mockResourceHandler{ + handleErr: errors.New("test error"), + enqueueTime: time.Now().Add(-10 * time.Millisecond), + }, + expectContinue: true, + expectCalls: 1, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + resetGlobalState() + c := newTestController([]string{}, "") + + c.queue.Add(tt.handler) + + result := c.processNextItem() + + assert.Equal(t, tt.expectContinue, result) + assert.Equal(t, tt.expectCalls, tt.handler.handleCalls) + }, + ) + } +} + +func TestProcessNextItemQueueShutdown(t *testing.T) { + resetGlobalState() + c := newTestController([]string{}, "") + + c.queue.ShutDown() + + result := c.processNextItem() + assert.False(t, result, "Should return false when queue is shutdown") +} diff --git a/internal/pkg/handler/create.go b/internal/pkg/handler/create.go index d676610..2ab2900 100644 --- a/internal/pkg/handler/create.go +++ b/internal/pkg/handler/create.go @@ -4,11 +4,12 @@ import ( "time" "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + "github.com/stakater/Reloader/internal/pkg/metrics" "github.com/stakater/Reloader/internal/pkg/options" "github.com/stakater/Reloader/pkg/common" - v1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/record" ) // ResourceCreatedHandler contains new objects @@ -59,10 +60,10 @@ func (r ResourceCreatedHandler) Handle() error { func (r ResourceCreatedHandler) GetConfig() (common.Config, string) { var oldSHAData string var config common.Config - if _, ok := r.Resource.(*v1.ConfigMap); ok { - config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap)) - } else if _, ok := r.Resource.(*v1.Secret); ok { - config = common.GetSecretConfig(r.Resource.(*v1.Secret)) + if cm, ok := r.Resource.(*v1.ConfigMap); ok { + config = common.GetConfigmapConfig(cm) + } else if secret, ok := r.Resource.(*v1.Secret); ok { + config = common.GetSecretConfig(secret) } else { logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource) } diff --git a/internal/pkg/handler/create_test.go b/internal/pkg/handler/create_test.go index 454e796..8600cba 100644 --- a/internal/pkg/handler/create_test.go +++ b/internal/pkg/handler/create_test.go @@ -3,20 +3,21 @@ package handler import ( "testing" - "github.com/stakater/Reloader/internal/pkg/constants" - "github.com/stakater/Reloader/internal/pkg/metrics" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/metrics" ) func TestResourceCreatedHandler_GetConfig(t *testing.T) { tests := []struct { - name string - resource interface{} - expectedName string - expectedNS string - expectedType string + name string + resource interface{} + expectedName string + expectedNS string + expectedType string expectSHANotEmpty bool expectOldSHAEmpty bool }{ diff --git a/internal/pkg/handler/delete.go b/internal/pkg/handler/delete.go index 34e032b..845bc87 100644 --- a/internal/pkg/handler/delete.go +++ b/internal/pkg/handler/delete.go @@ -6,6 +6,7 @@ import ( "time" "github.com/sirupsen/logrus" + "github.com/stakater/Reloader/internal/pkg/callbacks" "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/metrics" @@ -67,10 +68,10 @@ func (r ResourceDeleteHandler) Handle() error { func (r ResourceDeleteHandler) GetConfig() (common.Config, string) { var oldSHAData string var config common.Config - if _, ok := r.Resource.(*v1.ConfigMap); ok { - config = common.GetConfigmapConfig(r.Resource.(*v1.ConfigMap)) - } else if _, ok := r.Resource.(*v1.Secret); ok { - config = common.GetSecretConfig(r.Resource.(*v1.Secret)) + if cm, ok := r.Resource.(*v1.ConfigMap); ok { + config = common.GetConfigmapConfig(cm) + } else if secret, ok := r.Resource.(*v1.Secret); ok { + config = common.GetSecretConfig(secret) } else { logrus.Warnf("Invalid resource: Resource should be 'Secret' or 'Configmap' but found, %v", r.Resource) } @@ -98,7 +99,7 @@ func removeContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item run return InvokeStrategyResult{constants.NoContainerFound, nil} } - //remove if env var exists + // remove if env var exists if len(container.Env) > 0 { index := slices.IndexFunc(container.Env, func(envVariable v1.EnvVar) bool { return envVariable.Name == envVar diff --git a/internal/pkg/handler/delete_test.go b/internal/pkg/handler/delete_test.go index a5fbb59..77fc448 100644 --- a/internal/pkg/handler/delete_test.go +++ b/internal/pkg/handler/delete_test.go @@ -3,15 +3,16 @@ package handler import ( "testing" - "github.com/stakater/Reloader/internal/pkg/callbacks" - "github.com/stakater/Reloader/internal/pkg/constants" - "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/pkg/common" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + + "github.com/stakater/Reloader/internal/pkg/callbacks" + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/pkg/common" ) // mockDeploymentForDelete creates a deployment with containers for testing delete strategies diff --git a/internal/pkg/handler/handlers_test.go b/internal/pkg/handler/handlers_test.go index e5391fb..4b56358 100644 --- a/internal/pkg/handler/handlers_test.go +++ b/internal/pkg/handler/handlers_test.go @@ -3,30 +3,31 @@ package handler import ( "testing" - "github.com/stakater/Reloader/internal/pkg/constants" - "github.com/stakater/Reloader/internal/pkg/metrics" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/metrics" ) // Helper function to create a test ConfigMap -func createTestConfigMap(name, namespace string, data map[string]string) *v1.ConfigMap { +func createTestConfigMap(data map[string]string) *v1.ConfigMap { return &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, + Name: "test-cm", + Namespace: "default", }, Data: data, } } // Helper function to create a test Secret -func createTestSecret(name, namespace string, data map[string][]byte) *v1.Secret { +func createTestSecret(data map[string][]byte) *v1.Secret { return &v1.Secret{ ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, + Name: "test-secret", + Namespace: "default", }, Data: data, } @@ -42,7 +43,7 @@ func createTestCollectors() metrics.Collectors { // ============================================================ func TestResourceCreatedHandler_GetConfig_ConfigMap(t *testing.T) { - cm := createTestConfigMap("test-cm", "default", map[string]string{"key": "value"}) + cm := createTestConfigMap(map[string]string{"key": "value"}) handler := ResourceCreatedHandler{ Resource: cm, Collectors: createTestCollectors(), @@ -58,7 +59,7 @@ func TestResourceCreatedHandler_GetConfig_ConfigMap(t *testing.T) { } func TestResourceCreatedHandler_GetConfig_Secret(t *testing.T) { - secret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("value")}) + secret := createTestSecret(map[string][]byte{"key": []byte("value")}) handler := ResourceCreatedHandler{ Resource: secret, Collectors: createTestCollectors(), @@ -103,7 +104,7 @@ func TestResourceCreatedHandler_Handle_NilResource(t *testing.T) { // ============================================================ func TestResourceDeleteHandler_GetConfig_ConfigMap(t *testing.T) { - cm := createTestConfigMap("test-cm", "default", map[string]string{"key": "value"}) + cm := createTestConfigMap(map[string]string{"key": "value"}) handler := ResourceDeleteHandler{ Resource: cm, Collectors: createTestCollectors(), @@ -119,7 +120,7 @@ func TestResourceDeleteHandler_GetConfig_ConfigMap(t *testing.T) { } func TestResourceDeleteHandler_GetConfig_Secret(t *testing.T) { - secret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("value")}) + secret := createTestSecret(map[string][]byte{"key": []byte("value")}) handler := ResourceDeleteHandler{ Resource: secret, Collectors: createTestCollectors(), @@ -161,8 +162,8 @@ func TestResourceDeleteHandler_Handle_NilResource(t *testing.T) { // ============================================================ func TestResourceUpdatedHandler_GetConfig_ConfigMap(t *testing.T) { - oldCM := createTestConfigMap("test-cm", "default", map[string]string{"key": "old-value"}) - newCM := createTestConfigMap("test-cm", "default", map[string]string{"key": "new-value"}) + oldCM := createTestConfigMap(map[string]string{"key": "old-value"}) + newCM := createTestConfigMap(map[string]string{"key": "new-value"}) handler := ResourceUpdatedHandler{ Resource: newCM, @@ -182,8 +183,8 @@ func TestResourceUpdatedHandler_GetConfig_ConfigMap(t *testing.T) { } func TestResourceUpdatedHandler_GetConfig_ConfigMap_SameData(t *testing.T) { - oldCM := createTestConfigMap("test-cm", "default", map[string]string{"key": "same-value"}) - newCM := createTestConfigMap("test-cm", "default", map[string]string{"key": "same-value"}) + oldCM := createTestConfigMap(map[string]string{"key": "same-value"}) + newCM := createTestConfigMap(map[string]string{"key": "same-value"}) handler := ResourceUpdatedHandler{ Resource: newCM, @@ -199,8 +200,8 @@ func TestResourceUpdatedHandler_GetConfig_ConfigMap_SameData(t *testing.T) { } func TestResourceUpdatedHandler_GetConfig_Secret(t *testing.T) { - oldSecret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("old-value")}) - newSecret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("new-value")}) + oldSecret := createTestSecret(map[string][]byte{"key": []byte("old-value")}) + newSecret := createTestSecret(map[string][]byte{"key": []byte("new-value")}) handler := ResourceUpdatedHandler{ Resource: newSecret, @@ -219,8 +220,8 @@ func TestResourceUpdatedHandler_GetConfig_Secret(t *testing.T) { } func TestResourceUpdatedHandler_GetConfig_Secret_SameData(t *testing.T) { - oldSecret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("same-value")}) - newSecret := createTestSecret("test-secret", "default", map[string][]byte{"key": []byte("same-value")}) + oldSecret := createTestSecret(map[string][]byte{"key": []byte("same-value")}) + newSecret := createTestSecret(map[string][]byte{"key": []byte("same-value")}) handler := ResourceUpdatedHandler{ Resource: newSecret, @@ -260,7 +261,7 @@ func TestResourceUpdatedHandler_Handle_NilResource(t *testing.T) { } func TestResourceUpdatedHandler_Handle_NilOldResource(t *testing.T) { - cm := createTestConfigMap("test-cm", "default", map[string]string{"key": "value"}) + cm := createTestConfigMap(map[string]string{"key": "value"}) handler := ResourceUpdatedHandler{ Resource: cm, OldResource: nil, @@ -275,7 +276,7 @@ func TestResourceUpdatedHandler_Handle_NilOldResource(t *testing.T) { func TestResourceUpdatedHandler_Handle_NoChange(t *testing.T) { // When SHA values are the same, Handle should return nil without doing anything - cm := createTestConfigMap("test-cm", "default", map[string]string{"key": "same-value"}) + cm := createTestConfigMap(map[string]string{"key": "same-value"}) handler := ResourceUpdatedHandler{ Resource: cm, OldResource: cm, // Same resource = same SHA diff --git a/internal/pkg/handler/pause_deployment.go b/internal/pkg/handler/pause_deployment.go index 28d1b9e..d255b1c 100644 --- a/internal/pkg/handler/pause_deployment.go +++ b/internal/pkg/handler/pause_deployment.go @@ -7,11 +7,12 @@ import ( "time" "github.com/sirupsen/logrus" - "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/pkg/kube" app "k8s.io/api/apps/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" patchtypes "k8s.io/apimachinery/pkg/types" + + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/pkg/kube" ) // Keeps track of currently active timers diff --git a/internal/pkg/handler/pause_deployment_test.go b/internal/pkg/handler/pause_deployment_test.go index 19e7ac6..1f95b11 100644 --- a/internal/pkg/handler/pause_deployment_test.go +++ b/internal/pkg/handler/pause_deployment_test.go @@ -6,14 +6,15 @@ import ( "testing" "time" - "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/pkg/kube" "github.com/stretchr/testify/assert" appsv1 "k8s.io/api/apps/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" testclient "k8s.io/client-go/kubernetes/fake" + + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/pkg/kube" ) func TestIsPaused(t *testing.T) { @@ -377,7 +378,7 @@ func FindDeploymentByName(deployments []runtime.Object, deploymentName string) ( for _, deployment := range deployments { accessor, err := meta.Accessor(deployment) if err != nil { - return nil, fmt.Errorf("error getting accessor for item: %v", err) + return nil, fmt.Errorf("error getting accessor for item: %w", err) } if accessor.GetName() == deploymentName { deploymentObj, ok := deployment.(*appsv1.Deployment) diff --git a/internal/pkg/handler/update.go b/internal/pkg/handler/update.go index 3fde98e..7a1ad7d 100644 --- a/internal/pkg/handler/update.go +++ b/internal/pkg/handler/update.go @@ -4,13 +4,14 @@ import ( "time" "github.com/sirupsen/logrus" + v1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" + "github.com/stakater/Reloader/internal/pkg/metrics" "github.com/stakater/Reloader/internal/pkg/options" "github.com/stakater/Reloader/internal/pkg/util" "github.com/stakater/Reloader/pkg/common" - v1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/record" - csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" ) // ResourceUpdatedHandler contains updated objects diff --git a/internal/pkg/handler/update_test.go b/internal/pkg/handler/update_test.go index dcc1925..a10a6bf 100644 --- a/internal/pkg/handler/update_test.go +++ b/internal/pkg/handler/update_test.go @@ -3,11 +3,12 @@ package handler import ( "testing" - "github.com/stakater/Reloader/internal/pkg/constants" - "github.com/stakater/Reloader/internal/pkg/metrics" "github.com/stretchr/testify/assert" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/metrics" ) func TestResourceUpdatedHandler_GetConfig(t *testing.T) { diff --git a/internal/pkg/handler/upgrade.go b/internal/pkg/handler/upgrade.go index 982dbfa..a487040 100644 --- a/internal/pkg/handler/upgrade.go +++ b/internal/pkg/handler/upgrade.go @@ -14,14 +14,6 @@ import ( "github.com/parnurzeal/gorequest" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" - alert "github.com/stakater/Reloader/internal/pkg/alerts" - "github.com/stakater/Reloader/internal/pkg/callbacks" - "github.com/stakater/Reloader/internal/pkg/constants" - "github.com/stakater/Reloader/internal/pkg/metrics" - "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/internal/pkg/util" - "github.com/stakater/Reloader/pkg/common" - "github.com/stakater/Reloader/pkg/kube" app "k8s.io/api/apps/v1" v1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -32,6 +24,15 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/tools/record" "k8s.io/client-go/util/retry" + + alert "github.com/stakater/Reloader/internal/pkg/alerts" + "github.com/stakater/Reloader/internal/pkg/callbacks" + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/metrics" + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/internal/pkg/util" + "github.com/stakater/Reloader/pkg/common" + "github.com/stakater/Reloader/pkg/kube" ) // GetDeploymentRollingUpgradeFuncs returns all callback funcs for a deployment @@ -617,7 +618,7 @@ func updateContainerEnvVars(upgradeFuncs callbacks.RollingUpgradeFuncs, item run return InvokeStrategyResult{constants.NotUpdated, nil} } - //update if env var exists + // update if env var exists updateResult := updateEnvVar(container, envVar, config.SHAValue) // if no existing env var exists lets create one @@ -680,10 +681,10 @@ func populateAnnotationsFromSecretProviderClass(clients kube.Clients, config *co } func jsonEscape(toEscape string) (string, error) { - bytes, err := json.Marshal(toEscape) + data, err := json.Marshal(toEscape) if err != nil { return "", err } - escaped := string(bytes) + escaped := string(data) return escaped[1 : len(escaped)-1], nil } diff --git a/internal/pkg/handler/upgrade_test.go b/internal/pkg/handler/upgrade_test.go index a7d20c1..a518c38 100644 --- a/internal/pkg/handler/upgrade_test.go +++ b/internal/pkg/handler/upgrade_test.go @@ -1,20 +1,29 @@ package handler import ( + "errors" "testing" + "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" + v1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/util/retry" + "github.com/stakater/Reloader/internal/pkg/callbacks" "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/options" "github.com/stakater/Reloader/pkg/common" - "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" ) func TestGetRollingUpgradeFuncs(t *testing.T) { tests := []struct { - name string - getFuncs func() callbacks.RollingUpgradeFuncs - resourceType string + name string + getFuncs func() callbacks.RollingUpgradeFuncs + resourceType string supportsPatch bool }{ { @@ -495,12 +504,12 @@ func TestGetEnvVarName(t *testing.T) { func TestUpdateEnvVar(t *testing.T) { tests := []struct { - name string - container *v1.Container - envVar string - shaData string - expected constants.Result - newValue string // expected value after update + name string + container *v1.Container + envVar string + shaData string + expected constants.Result + newValue string // expected value after update }{ { name: "Update existing env var with different value", @@ -670,3 +679,704 @@ func TestCreateReloadedAnnotations(t *testing.T) { }) } } + +// Helper function to create a mock deployment for testing +func createTestDeployment(containers []v1.Container, initContainers []v1.Container, volumes []v1.Volume) *appsv1.Deployment { + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-deployment", + Namespace: "default", + }, + Spec: appsv1.DeploymentSpec{ + Template: v1.PodTemplateSpec{ + Spec: v1.PodSpec{ + Containers: containers, + InitContainers: initContainers, + Volumes: volumes, + }, + }, + }, + } +} + +// mockRollingUpgradeFuncs creates mock callbacks for testing getContainerUsingResource +func mockRollingUpgradeFuncs(deployment *appsv1.Deployment) callbacks.RollingUpgradeFuncs { + return callbacks.RollingUpgradeFuncs{ + VolumesFunc: func(item runtime.Object) []v1.Volume { + return deployment.Spec.Template.Spec.Volumes + }, + ContainersFunc: func(item runtime.Object) []v1.Container { + return deployment.Spec.Template.Spec.Containers + }, + InitContainersFunc: func(item runtime.Object) []v1.Container { + return deployment.Spec.Template.Spec.InitContainers + }, + } +} + +func TestGetContainerUsingResource(t *testing.T) { + tests := []struct { + name string + containers []v1.Container + initContainers []v1.Container + volumes []v1.Volume + config common.Config + autoReload bool + expectNil bool + expectedName string + }{ + { + name: "Volume mount in regular container", + containers: []v1.Container{ + { + Name: "app", + VolumeMounts: []v1.VolumeMount{ + {Name: "config-volume", MountPath: "/etc/config"}, + }, + }, + }, + initContainers: []v1.Container{}, + volumes: []v1.Volume{ + { + Name: "config-volume", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "my-configmap"}, + }, + }, + }, + }, + config: common.Config{ + ResourceName: "my-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: false, + expectNil: false, + expectedName: "app", + }, + { + name: "Volume mount in init container returns first regular container", + containers: []v1.Container{ + {Name: "main-app"}, + {Name: "sidecar"}, + }, + initContainers: []v1.Container{ + { + Name: "init", + VolumeMounts: []v1.VolumeMount{ + {Name: "secret-volume", MountPath: "/etc/secrets"}, + }, + }, + }, + volumes: []v1.Volume{ + { + Name: "secret-volume", + VolumeSource: v1.VolumeSource{ + Secret: &v1.SecretVolumeSource{SecretName: "my-secret"}, + }, + }, + }, + config: common.Config{ + ResourceName: "my-secret", + Type: constants.SecretEnvVarPostfix, + }, + autoReload: false, + expectNil: false, + expectedName: "main-app", // Returns first container when init container has the mount + }, + { + name: "EnvFrom ConfigMap in regular container", + containers: []v1.Container{ + { + Name: "app", + EnvFrom: []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "env-configmap"}, + }, + }, + }, + }, + }, + initContainers: []v1.Container{}, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "env-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: false, + expectNil: false, + expectedName: "app", + }, + { + name: "EnvFrom Secret in init container returns first regular container", + containers: []v1.Container{ + {Name: "main-app"}, + }, + initContainers: []v1.Container{ + { + Name: "init", + EnvFrom: []v1.EnvFromSource{ + { + SecretRef: &v1.SecretEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "init-secret"}, + }, + }, + }, + }, + }, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "init-secret", + Type: constants.SecretEnvVarPostfix, + }, + autoReload: false, + expectNil: false, + expectedName: "main-app", + }, + { + name: "autoReload=false with no mount returns first container (explicit annotation)", + containers: []v1.Container{ + {Name: "first-container"}, + {Name: "second-container"}, + }, + initContainers: []v1.Container{}, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "external-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: false, // Explicit annotation should use first container fallback + expectNil: false, + expectedName: "first-container", + }, + { + name: "autoReload=true with no mount returns nil", + containers: []v1.Container{ + {Name: "app"}, + }, + initContainers: []v1.Container{}, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "unmounted-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: true, // Auto mode should NOT use first container fallback + expectNil: true, + }, + { + name: "Empty containers returns nil", + containers: []v1.Container{}, + initContainers: []v1.Container{}, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "any-configmap", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: false, + expectNil: true, + }, + { + name: "Init container with volume but no regular containers returns nil", + containers: []v1.Container{}, + initContainers: []v1.Container{ + { + Name: "init", + VolumeMounts: []v1.VolumeMount{ + {Name: "config-volume", MountPath: "/etc/config"}, + }, + }, + }, + volumes: []v1.Volume{ + { + Name: "config-volume", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "init-only-cm"}, + }, + }, + }, + }, + config: common.Config{ + ResourceName: "init-only-cm", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: false, + expectNil: true, // No regular containers to return + }, + { + name: "CSI SecretProviderClass volume", + containers: []v1.Container{ + { + Name: "app", + VolumeMounts: []v1.VolumeMount{ + {Name: "csi-volume", MountPath: "/mnt/secrets"}, + }, + }, + }, + initContainers: []v1.Container{}, + volumes: []v1.Volume{ + { + Name: "csi-volume", + VolumeSource: v1.VolumeSource{ + CSI: &v1.CSIVolumeSource{ + Driver: "secrets-store.csi.k8s.io", + VolumeAttributes: map[string]string{ + "secretProviderClass": "my-spc", + }, + }, + }, + }, + }, + config: common.Config{ + ResourceName: "my-spc", + Type: constants.SecretProviderClassEnvVarPostfix, + }, + autoReload: false, + expectNil: false, + expectedName: "app", + }, + { + name: "Env ValueFrom ConfigMapKeyRef", + containers: []v1.Container{ + { + Name: "app", + Env: []v1.EnvVar{ + { + Name: "CONFIG_VALUE", + ValueFrom: &v1.EnvVarSource{ + ConfigMapKeyRef: &v1.ConfigMapKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: "keyref-cm"}, + Key: "my-key", + }, + }, + }, + }, + }, + }, + initContainers: []v1.Container{}, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "keyref-cm", + Type: constants.ConfigmapEnvVarPostfix, + }, + autoReload: false, + expectNil: false, + expectedName: "app", + }, + { + name: "Env ValueFrom SecretKeyRef", + containers: []v1.Container{ + { + Name: "app", + Env: []v1.EnvVar{ + { + Name: "SECRET_VALUE", + ValueFrom: &v1.EnvVarSource{ + SecretKeyRef: &v1.SecretKeySelector{ + LocalObjectReference: v1.LocalObjectReference{Name: "keyref-secret"}, + Key: "password", + }, + }, + }, + }, + }, + }, + initContainers: []v1.Container{}, + volumes: []v1.Volume{}, + config: common.Config{ + ResourceName: "keyref-secret", + Type: constants.SecretEnvVarPostfix, + }, + autoReload: false, + expectNil: false, + expectedName: "app", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + deployment := createTestDeployment(tt.containers, tt.initContainers, tt.volumes) + funcs := mockRollingUpgradeFuncs(deployment) + + result := getContainerUsingResource(funcs, deployment, tt.config, tt.autoReload) + + if tt.expectNil { + assert.Nil(t, result, "Expected nil container") + } else { + assert.NotNil(t, result, "Expected non-nil container") + assert.Equal(t, tt.expectedName, result.Name) + } + }) + } +} + +func TestRetryOnConflict(t *testing.T) { + tests := []struct { + name string + fnResults []struct { + matched bool + err error + } + expectMatched bool + expectError bool + }{ + { + name: "Success on first try", + fnResults: []struct { + matched bool + err error + }{ + {matched: true, err: nil}, + }, + expectMatched: true, + expectError: false, + }, + { + name: "Conflict then success", + fnResults: []struct { + matched bool + err error + }{ + {matched: false, err: apierrors.NewConflict(schema.GroupResource{Group: "", Resource: "deployments"}, "test", errors.New("conflict"))}, + {matched: true, err: nil}, + }, + expectMatched: true, + expectError: false, + }, + { + name: "Non-conflict error returns immediately", + fnResults: []struct { + matched bool + err error + }{ + {matched: false, err: errors.New("some other error")}, + }, + expectMatched: false, + expectError: true, + }, + { + name: "Multiple conflicts then success", + fnResults: []struct { + matched bool + err error + }{ + {matched: false, err: apierrors.NewConflict(schema.GroupResource{}, "test", errors.New("conflict 1"))}, + {matched: false, err: apierrors.NewConflict(schema.GroupResource{}, "test", errors.New("conflict 2"))}, + {matched: true, err: nil}, + }, + expectMatched: true, + expectError: false, + }, + { + name: "Not matched but no error", + fnResults: []struct { + matched bool + err error + }{ + {matched: false, err: nil}, + }, + expectMatched: false, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + callCount := 0 + fn := func(fetchResource bool) (bool, error) { + if callCount >= len(tt.fnResults) { + // Should not happen in tests, but return success to prevent infinite loop + return true, nil + } + result := tt.fnResults[callCount] + callCount++ + return result.matched, result.err + } + + matched, err := retryOnConflict(retry.DefaultRetry, fn) + + assert.Equal(t, tt.expectMatched, matched) + if tt.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGetVolumeMountNameCSI(t *testing.T) { + // Test CSI SecretProviderClass volume specifically + tests := []struct { + name string + volumes []v1.Volume + mountType string + volumeName string + expected string + }{ + { + name: "CSI SecretProviderClass volume match", + volumes: []v1.Volume{ + { + Name: "csi-secrets", + VolumeSource: v1.VolumeSource{ + CSI: &v1.CSIVolumeSource{ + Driver: "secrets-store.csi.k8s.io", + VolumeAttributes: map[string]string{ + "secretProviderClass": "my-vault-spc", + }, + }, + }, + }, + }, + mountType: constants.SecretProviderClassEnvVarPostfix, + volumeName: "my-vault-spc", + expected: "csi-secrets", + }, + { + name: "CSI volume with different SPC name - no match", + volumes: []v1.Volume{ + { + Name: "csi-secrets", + VolumeSource: v1.VolumeSource{ + CSI: &v1.CSIVolumeSource{ + Driver: "secrets-store.csi.k8s.io", + VolumeAttributes: map[string]string{ + "secretProviderClass": "other-spc", + }, + }, + }, + }, + }, + mountType: constants.SecretProviderClassEnvVarPostfix, + volumeName: "my-vault-spc", + expected: "", + }, + { + name: "CSI volume without secretProviderClass attribute", + volumes: []v1.Volume{ + { + Name: "csi-volume", + VolumeSource: v1.VolumeSource{ + CSI: &v1.CSIVolumeSource{ + Driver: "other-csi-driver", + VolumeAttributes: map[string]string{}, + }, + }, + }, + }, + mountType: constants.SecretProviderClassEnvVarPostfix, + volumeName: "any-spc", + expected: "", + }, + { + name: "CSI volume with nil VolumeAttributes", + volumes: []v1.Volume{ + { + Name: "csi-volume", + VolumeSource: v1.VolumeSource{ + CSI: &v1.CSIVolumeSource{ + Driver: "secrets-store.csi.k8s.io", + }, + }, + }, + }, + mountType: constants.SecretProviderClassEnvVarPostfix, + volumeName: "any-spc", + expected: "", + }, + { + name: "Multiple volumes with CSI match", + volumes: []v1.Volume{ + { + Name: "config-volume", + VolumeSource: v1.VolumeSource{ + ConfigMap: &v1.ConfigMapVolumeSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "my-cm"}, + }, + }, + }, + { + Name: "csi-secrets", + VolumeSource: v1.VolumeSource{ + CSI: &v1.CSIVolumeSource{ + Driver: "secrets-store.csi.k8s.io", + VolumeAttributes: map[string]string{ + "secretProviderClass": "target-spc", + }, + }, + }, + }, + }, + mountType: constants.SecretProviderClassEnvVarPostfix, + volumeName: "target-spc", + expected: "csi-secrets", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := getVolumeMountName(tt.volumes, tt.mountType, tt.volumeName) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestSecretProviderClassAnnotationReloaded(t *testing.T) { + tests := []struct { + name string + oldAnnotations map[string]string + newConfig common.Config + expected bool + }{ + { + name: "Annotation contains matching SPC name and SHA", + oldAnnotations: map[string]string{ + "reloader.stakater.com/last-reloaded-from": `{"name":"my-spc","sha":"abc123"}`, + }, + newConfig: common.Config{ + ResourceName: "my-spc", + SHAValue: "abc123", + }, + expected: true, + }, + { + name: "Annotation contains SPC name but different SHA", + oldAnnotations: map[string]string{ + "reloader.stakater.com/last-reloaded-from": `{"name":"my-spc","sha":"old-sha"}`, + }, + newConfig: common.Config{ + ResourceName: "my-spc", + SHAValue: "new-sha", + }, + expected: false, + }, + { + name: "Annotation contains different SPC name", + oldAnnotations: map[string]string{ + "reloader.stakater.com/last-reloaded-from": `{"name":"other-spc","sha":"abc123"}`, + }, + newConfig: common.Config{ + ResourceName: "my-spc", + SHAValue: "abc123", + }, + expected: false, + }, + { + name: "Empty annotations", + oldAnnotations: map[string]string{}, + newConfig: common.Config{ + ResourceName: "my-spc", + SHAValue: "abc123", + }, + expected: false, + }, + { + name: "Nil annotations", + oldAnnotations: nil, + newConfig: common.Config{ + ResourceName: "my-spc", + SHAValue: "abc123", + }, + expected: false, + }, + { + name: "Annotation key missing", + oldAnnotations: map[string]string{ + "other-annotation": "some-value", + }, + newConfig: common.Config{ + ResourceName: "my-spc", + SHAValue: "abc123", + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := secretProviderClassAnnotationReloaded(tt.oldAnnotations, tt.newConfig) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestInvokeReloadStrategy(t *testing.T) { + // Save original value and restore after test + originalStrategy := options.ReloadStrategy + defer func() { options.ReloadStrategy = originalStrategy }() + + // Create a minimal deployment for testing + deployment := createTestDeployment( + []v1.Container{ + { + Name: "app", + EnvFrom: []v1.EnvFromSource{ + { + ConfigMapRef: &v1.ConfigMapEnvSource{ + LocalObjectReference: v1.LocalObjectReference{Name: "my-configmap"}, + }, + }, + }, + }, + }, + []v1.Container{}, + []v1.Volume{}, + ) + deployment.Spec.Template.Annotations = map[string]string{} + + funcs := callbacks.RollingUpgradeFuncs{ + VolumesFunc: func(item runtime.Object) []v1.Volume { + return deployment.Spec.Template.Spec.Volumes + }, + ContainersFunc: func(item runtime.Object) []v1.Container { + return deployment.Spec.Template.Spec.Containers + }, + InitContainersFunc: func(item runtime.Object) []v1.Container { + return deployment.Spec.Template.Spec.InitContainers + }, + PodAnnotationsFunc: func(item runtime.Object) map[string]string { + return deployment.Spec.Template.Annotations + }, + SupportsPatch: false, + } + + config := common.Config{ + ResourceName: "my-configmap", + Type: constants.ConfigmapEnvVarPostfix, + SHAValue: "sha256:abc123", + Namespace: "default", + } + + tests := []struct { + name string + reloadStrategy string + autoReload bool + expectResult constants.Result + }{ + { + name: "Annotations strategy", + reloadStrategy: constants.AnnotationsReloadStrategy, + autoReload: false, + expectResult: constants.Updated, + }, + { + name: "Env vars strategy with container found", + reloadStrategy: constants.EnvVarsReloadStrategy, + autoReload: false, + expectResult: constants.Updated, // Creates env var when not found + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + options.ReloadStrategy = tt.reloadStrategy + // Reset annotations for each test + deployment.Spec.Template.Annotations = map[string]string{} + + result := invokeReloadStrategy(funcs, deployment, config, tt.autoReload) + assert.Equal(t, tt.expectResult, result.Result) + }) + } +} diff --git a/internal/pkg/leadership/leadership.go b/internal/pkg/leadership/leadership.go index f8c85bc..f98f299 100644 --- a/internal/pkg/leadership/leadership.go +++ b/internal/pkg/leadership/leadership.go @@ -7,11 +7,12 @@ import ( "time" "github.com/sirupsen/logrus" - "github.com/stakater/Reloader/internal/pkg/controller" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/tools/leaderelection" "k8s.io/client-go/tools/leaderelection/resourcelock" + "github.com/stakater/Reloader/internal/pkg/controller" + coordinationv1 "k8s.io/client-go/kubernetes/typed/coordination/v1" ) @@ -75,7 +76,7 @@ func RunLeaderElection(lock *resourcelock.LeaseLock, ctx context.Context, cancel func runControllers(controllers []*controller.Controller, stopChannels []chan struct{}) { for i, c := range controllers { - c := c + go c.Run(1, stopChannels[i]) } } diff --git a/internal/pkg/leadership/leadership_test.go b/internal/pkg/leadership/leadership_test.go index eed0705..b14341b 100644 --- a/internal/pkg/leadership/leadership_test.go +++ b/internal/pkg/leadership/leadership_test.go @@ -10,6 +10,7 @@ import ( "time" "github.com/sirupsen/logrus" + "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/controller" "github.com/stakater/Reloader/internal/pkg/handler" diff --git a/internal/pkg/testutil/kube.go b/internal/pkg/testutil/kube.go index a778eb1..ab64d84 100644 --- a/internal/pkg/testutil/kube.go +++ b/internal/pkg/testutil/kube.go @@ -15,14 +15,6 @@ import ( openshiftv1 "github.com/openshift/api/apps/v1" appsclient "github.com/openshift/client-go/apps/clientset/versioned" "github.com/sirupsen/logrus" - "github.com/stakater/Reloader/internal/pkg/callbacks" - "github.com/stakater/Reloader/internal/pkg/constants" - "github.com/stakater/Reloader/internal/pkg/crypto" - "github.com/stakater/Reloader/internal/pkg/metrics" - "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/internal/pkg/util" - "github.com/stakater/Reloader/pkg/common" - "github.com/stakater/Reloader/pkg/kube" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" v1 "k8s.io/api/core/v1" @@ -33,6 +25,15 @@ import ( csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" csiclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" csiclient_v1 "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned/typed/apis/v1" + + "github.com/stakater/Reloader/internal/pkg/callbacks" + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/crypto" + "github.com/stakater/Reloader/internal/pkg/metrics" + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/internal/pkg/util" + "github.com/stakater/Reloader/pkg/common" + "github.com/stakater/Reloader/pkg/kube" ) var ( diff --git a/internal/pkg/util/interface.go b/internal/pkg/util/interface.go index ff261ab..a137873 100644 --- a/internal/pkg/util/interface.go +++ b/internal/pkg/util/interface.go @@ -31,7 +31,7 @@ type ObjectMeta struct { func ToObjectMeta(kubernetesObject interface{}) ObjectMeta { objectValue := reflect.ValueOf(kubernetesObject) fieldName := reflect.TypeOf((*metav1.ObjectMeta)(nil)).Elem().Name() - field := objectValue.FieldByName(fieldName).Interface().(metav1.ObjectMeta) + field, _ := objectValue.FieldByName(fieldName).Interface().(metav1.ObjectMeta) return ObjectMeta{ ObjectMeta: field, @@ -41,9 +41,11 @@ func ToObjectMeta(kubernetesObject interface{}) ObjectMeta { // ParseBool returns result in bool format after parsing func ParseBool(value interface{}) bool { if reflect.Bool == reflect.TypeOf(value).Kind() { - return value.(bool) + b, _ := value.(bool) + return b } else if reflect.String == reflect.TypeOf(value).Kind() { - result, _ := strconv.ParseBool(value.(string)) + s, _ := value.(string) + result, _ := strconv.ParseBool(s) return result } return false diff --git a/internal/pkg/util/util.go b/internal/pkg/util/util.go index 476cdb9..abfbecb 100644 --- a/internal/pkg/util/util.go +++ b/internal/pkg/util/util.go @@ -9,11 +9,12 @@ import ( "strings" "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" + "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/crypto" "github.com/stakater/Reloader/internal/pkg/options" - v1 "k8s.io/api/core/v1" - csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" ) // ConvertToEnvVarName converts the given text into a usable env var diff --git a/internal/pkg/util/util_test.go b/internal/pkg/util/util_test.go index 338f329..161e92d 100644 --- a/internal/pkg/util/util_test.go +++ b/internal/pkg/util/util_test.go @@ -3,8 +3,9 @@ package util import ( "testing" - "github.com/stakater/Reloader/internal/pkg/options" v1 "k8s.io/api/core/v1" + + "github.com/stakater/Reloader/internal/pkg/options" ) func TestConvertToEnvVarName(t *testing.T) { diff --git a/pkg/common/common.go b/pkg/common/common.go index 7c9d61e..bebfaa9 100644 --- a/pkg/common/common.go +++ b/pkg/common/common.go @@ -8,12 +8,13 @@ import ( "strings" "github.com/sirupsen/logrus" - "github.com/stakater/Reloader/internal/pkg/constants" - "github.com/stakater/Reloader/internal/pkg/options" - "github.com/stakater/Reloader/internal/pkg/util" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/client-go/kubernetes" + + "github.com/stakater/Reloader/internal/pkg/constants" + "github.com/stakater/Reloader/internal/pkg/options" + "github.com/stakater/Reloader/internal/pkg/util" ) type Map map[string]string @@ -191,10 +192,10 @@ func GetResourceLabelSelector(slice []string) (string, error) { } // ShouldReload checks if a resource should be reloaded based on its annotations and the provided options. -func ShouldReload(config Config, resourceType string, annotations Map, podAnnotations Map, options *ReloaderOptions) ReloadCheckResult { +func ShouldReload(config Config, resourceType string, annotations Map, podAnnotations Map, reloaderOpts *ReloaderOptions) ReloadCheckResult { // Check if this workload type should be ignored - if len(options.WorkloadTypesToIgnore) > 0 { + if len(reloaderOpts.WorkloadTypesToIgnore) > 0 { ignoredWorkloadTypes, err := util.GetIgnoredWorkloadTypesList() if err != nil { logrus.Errorf("Failed to parse ignored workload types: %v", err) @@ -219,7 +220,7 @@ func ShouldReload(config Config, resourceType string, annotations Map, podAnnota } } - ignoreResourceAnnotatonValue := config.ResourceAnnotations[options.IgnoreResourceAnnotation] + ignoreResourceAnnotatonValue := config.ResourceAnnotations[reloaderOpts.IgnoreResourceAnnotation] if ignoreResourceAnnotatonValue == "true" { return ReloadCheckResult{ ShouldReload: false, @@ -227,18 +228,18 @@ func ShouldReload(config Config, resourceType string, annotations Map, podAnnota } annotationValue, found := annotations[config.Annotation] - searchAnnotationValue, foundSearchAnn := annotations[options.AutoSearchAnnotation] - reloaderEnabledValue, foundAuto := annotations[options.ReloaderAutoAnnotation] + searchAnnotationValue, foundSearchAnn := annotations[reloaderOpts.AutoSearchAnnotation] + reloaderEnabledValue, foundAuto := annotations[reloaderOpts.ReloaderAutoAnnotation] typedAutoAnnotationEnabledValue, foundTypedAuto := annotations[config.TypedAutoAnnotation] - excludeConfigmapAnnotationValue, foundExcludeConfigmap := annotations[options.ConfigmapExcludeReloaderAnnotation] - excludeSecretAnnotationValue, foundExcludeSecret := annotations[options.SecretExcludeReloaderAnnotation] - excludeSecretProviderClassProviderAnnotationValue, foundExcludeSecretProviderClass := annotations[options.SecretProviderClassExcludeReloaderAnnotation] + excludeConfigmapAnnotationValue, foundExcludeConfigmap := annotations[reloaderOpts.ConfigmapExcludeReloaderAnnotation] + excludeSecretAnnotationValue, foundExcludeSecret := annotations[reloaderOpts.SecretExcludeReloaderAnnotation] + excludeSecretProviderClassProviderAnnotationValue, foundExcludeSecretProviderClass := annotations[reloaderOpts.SecretProviderClassExcludeReloaderAnnotation] if !found && !foundAuto && !foundTypedAuto && !foundSearchAnn { annotations = podAnnotations annotationValue = annotations[config.Annotation] - searchAnnotationValue = annotations[options.AutoSearchAnnotation] - reloaderEnabledValue = annotations[options.ReloaderAutoAnnotation] + searchAnnotationValue = annotations[reloaderOpts.AutoSearchAnnotation] + reloaderEnabledValue = annotations[reloaderOpts.ReloaderAutoAnnotation] typedAutoAnnotationEnabledValue = annotations[config.TypedAutoAnnotation] } @@ -279,7 +280,7 @@ func ShouldReload(config Config, resourceType string, annotations Map, podAnnota } if searchAnnotationValue == "true" { - matchAnnotationValue := config.ResourceAnnotations[options.SearchMatchAnnotation] + matchAnnotationValue := config.ResourceAnnotations[reloaderOpts.SearchMatchAnnotation] if matchAnnotationValue == "true" { return ReloadCheckResult{ ShouldReload: true, @@ -290,7 +291,7 @@ func ShouldReload(config Config, resourceType string, annotations Map, podAnnota reloaderEnabled, _ := strconv.ParseBool(reloaderEnabledValue) typedAutoAnnotationEnabled, _ := strconv.ParseBool(typedAutoAnnotationEnabledValue) - if reloaderEnabled || typedAutoAnnotationEnabled || reloaderEnabledValue == "" && typedAutoAnnotationEnabledValue == "" && options.AutoReloadAll { + if reloaderEnabled || typedAutoAnnotationEnabled || reloaderEnabledValue == "" && typedAutoAnnotationEnabledValue == "" && reloaderOpts.AutoReloadAll { return ReloadCheckResult{ ShouldReload: true, AutoReload: true, diff --git a/pkg/common/config.go b/pkg/common/config.go index 4421fa5..6c90d08 100644 --- a/pkg/common/config.go +++ b/pkg/common/config.go @@ -1,11 +1,12 @@ package common import ( + v1 "k8s.io/api/core/v1" + csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" + "github.com/stakater/Reloader/internal/pkg/constants" "github.com/stakater/Reloader/internal/pkg/options" "github.com/stakater/Reloader/internal/pkg/util" - v1 "k8s.io/api/core/v1" - csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" ) // Config contains rolling upgrade configuration parameters diff --git a/pkg/kube/client.go b/pkg/kube/client.go index 9582929..1cfe216 100644 --- a/pkg/kube/client.go +++ b/pkg/kube/client.go @@ -137,13 +137,13 @@ func getConfig() (*rest.Config, error) { if kubeconfigPath == "" { kubeconfigPath = os.Getenv("HOME") + "/.kube/config" } - //If file exists so use that config settings + // If file exists so use that config settings if _, err := os.Stat(kubeconfigPath); err == nil { config, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath) if err != nil { return nil, err } - } else { //Use Incluster Configuration + } else { // Use Incluster Configuration config, err = rest.InClusterConfig() if err != nil { return nil, err diff --git a/scripts/e2e-cluster-cleanup.sh b/scripts/e2e-cluster-cleanup.sh index 7fb9158..b500529 100644 --- a/scripts/e2e-cluster-cleanup.sh +++ b/scripts/e2e-cluster-cleanup.sh @@ -1,93 +1,283 @@ #!/bin/bash # Cleanup script for e2e test cluster # Run this after e2e tests complete: ./scripts/e2e-cluster-cleanup.sh -# This removes Argo Rollouts, test namespaces, and cluster-scoped resources. +# +# This removes: +# - Reloader test resources (namespaces, cluster roles, etc.) +# - Vault and its namespace +# - CSI Secrets Store Driver +# - Argo Rollouts +# +# Resources are removed in reverse dependency order. -set -e +set -euo pipefail + +# ============================================================================= +# Configuration +# ============================================================================= ARGO_ROLLOUTS_VERSION="${ARGO_ROLLOUTS_VERSION:-v1.7.2}" ARGO_ROLLOUTS_NAMESPACE="argo-rollouts" +CSI_DRIVER_VERSION="${CSI_DRIVER_VERSION:-1.5.5}" +CSI_NAMESPACE="kube-system" +VAULT_NAMESPACE="vault" -echo "=== E2E Cluster Cleanup ===" +# ============================================================================= +# Helper Functions +# ============================================================================= -# Check if kubectl is available -if ! command -v kubectl &> /dev/null; then - echo "Error: kubectl is not installed or not in PATH" - exit 1 -fi +log_header() { + echo "" + echo "=== $1 ===" +} -# Check cluster connectivity -echo "Checking cluster connectivity..." -if ! kubectl cluster-info &> /dev/null; then - echo "Error: Cannot connect to Kubernetes cluster" - exit 1 -fi +log_info() { + echo "$1" +} -# ============================================================ -# Cleanup Reloader Test Resources -# ============================================================ -echo "" -echo "=== Cleaning up Reloader test resources ===" +log_success() { + echo "✓ $1" +} -# Delete test namespaces (created by test suites) -echo "Deleting test namespaces..." -for ns in $(kubectl get namespaces -o name | grep -E "reloader-" | cut -d/ -f2); do - echo " Deleting namespace: ${ns}" - kubectl delete namespace "${ns}" --ignore-not-found --wait=false -done +log_warning() { + echo "⚠ $1" +} -# Delete Reloader cluster-scoped resources -echo "Deleting Reloader cluster-scoped resources..." -for cr in $(kubectl get clusterrole -o name 2>/dev/null | grep -E "reloader-" | cut -d/ -f2); do - echo " Deleting ClusterRole: ${cr}" - kubectl delete clusterrole "${cr}" --ignore-not-found -done +log_error() { + echo "✗ $1" >&2 +} -for crb in $(kubectl get clusterrolebinding -o name 2>/dev/null | grep -E "reloader-" | cut -d/ -f2); do - echo " Deleting ClusterRoleBinding: ${crb}" - kubectl delete clusterrolebinding "${crb}" --ignore-not-found -done +check_command() { + if ! command -v "$1" &> /dev/null; then + log_error "$1 is not installed or not in PATH" + return 1 + fi + return 0 +} -# ============================================================ -# Cleanup Argo Rollouts -# ============================================================ -echo "" -echo "=== Uninstalling Argo Rollouts ===" +# Safe delete that ignores "not found" errors +safe_delete() { + kubectl delete "$@" --ignore-not-found 2>/dev/null || true +} -# First, delete the deployment to stop the controller -echo "Stopping Argo Rollouts controller..." -kubectl delete deployment argo-rollouts -n ${ARGO_ROLLOUTS_NAMESPACE} --ignore-not-found --timeout=30s 2>/dev/null || true +# ============================================================================= +# Dependency Checks +# ============================================================================= -# Delete all Rollouts and other CRs in all namespaces to avoid finalizer issues -echo "Deleting Argo Rollouts custom resources..." -ARGO_RESOURCES="rollouts analysisruns analysistemplates experiments" -for res in ${ARGO_RESOURCES}; do - kubectl delete "${res}.argoproj.io" --all --all-namespaces --ignore-not-found --timeout=30s 2>/dev/null || true -done +check_dependencies() { + log_header "Checking Dependencies" -# Delete using the install manifest -echo "Deleting Argo Rollouts installation..." -ARGO_URL="https://github.com/argoproj/argo-rollouts/releases/download/${ARGO_ROLLOUTS_VERSION}/install.yaml" -kubectl delete -f ${ARGO_URL} --ignore-not-found --timeout=60s 2>/dev/null || true + if ! check_command kubectl; then + log_error "kubectl is required for cleanup" + exit 1 + fi -# Give resources time to be cleaned up before deleting CRDs -sleep 2 + log_success "Dependencies available" +} -# Explicitly delete CRDs (cluster-scoped) -echo "Deleting Argo Rollouts CRDs..." -ARGO_CRDS="rollouts.argoproj.io analysisruns.argoproj.io analysistemplates.argoproj.io clusteranalysistemplates.argoproj.io experiments.argoproj.io" -for crd in ${ARGO_CRDS}; do - kubectl delete crd "${crd}" --ignore-not-found --timeout=30s 2>/dev/null || true -done +check_cluster_connectivity() { + log_header "Checking Cluster Connectivity" -# Delete namespace -echo "Deleting Argo Rollouts namespace..." -kubectl delete namespace ${ARGO_ROLLOUTS_NAMESPACE} --ignore-not-found --timeout=30s 2>/dev/null || true + if ! kubectl cluster-info &> /dev/null; then + log_error "Cannot connect to Kubernetes cluster" + exit 1 + fi -# Delete cluster-scoped RBAC -echo "Deleting Argo Rollouts cluster RBAC..." -kubectl delete clusterrole argo-rollouts argo-rollouts-aggregate-to-admin argo-rollouts-aggregate-to-edit argo-rollouts-aggregate-to-view --ignore-not-found 2>/dev/null || true -kubectl delete clusterrolebinding argo-rollouts --ignore-not-found 2>/dev/null || true + local context + context=$(kubectl config current-context) + log_success "Connected to cluster (context: $context)" +} -echo "" -echo "=== E2E Cluster Cleanup Complete ===" +# ============================================================================= +# Reloader Test Resources Cleanup +# ============================================================================= + +cleanup_reloader_resources() { + log_header "Cleaning Up Reloader Test Resources" + + # Delete test namespaces (created by test suites) + log_info "Deleting test namespaces..." + local namespaces + namespaces=$(kubectl get namespaces -o name 2>/dev/null | grep "reloader-" | cut -d/ -f2 || true) + if [[ -n "$namespaces" ]]; then + for ns in $namespaces; do + log_info " Deleting namespace: $ns" + kubectl delete namespace "$ns" --ignore-not-found --wait=false 2>/dev/null || true + done + else + log_info " No test namespaces found" + fi + + # Delete Reloader cluster-scoped resources + log_info "Deleting cluster roles..." + local clusterroles + clusterroles=$(kubectl get clusterrole -o name 2>/dev/null | grep "reloader-" | cut -d/ -f2 || true) + for cr in $clusterroles; do + log_info " Deleting ClusterRole: $cr" + safe_delete clusterrole "$cr" + done + + log_info "Deleting cluster role bindings..." + local clusterrolebindings + clusterrolebindings=$(kubectl get clusterrolebinding -o name 2>/dev/null | grep "reloader-" | cut -d/ -f2 || true) + for crb in $clusterrolebindings; do + log_info " Deleting ClusterRoleBinding: $crb" + safe_delete clusterrolebinding "$crb" + done + + log_success "Reloader test resources cleaned up" +} + +# ============================================================================= +# Vault Cleanup +# ============================================================================= + +cleanup_vault() { + log_header "Uninstalling Vault" + + # Check if Vault is installed + if ! kubectl get namespace "$VAULT_NAMESPACE" &> /dev/null; then + log_info "Vault namespace not found, skipping" + return 0 + fi + + # Uninstall via Helm if available + if command -v helm &> /dev/null; then + if helm list -n "$VAULT_NAMESPACE" 2>/dev/null | grep -q vault; then + log_info "Uninstalling Vault via Helm..." + helm uninstall vault -n "$VAULT_NAMESPACE" --wait --timeout 60s 2>/dev/null || true + fi + fi + + # Delete namespace + log_info "Deleting Vault namespace..." + safe_delete namespace "$VAULT_NAMESPACE" --timeout=60s + + log_success "Vault cleaned up" +} + +# ============================================================================= +# CSI Secrets Store Driver Cleanup +# ============================================================================= + +cleanup_csi_driver() { + log_header "Uninstalling CSI Secrets Store Driver" + + # Delete all SecretProviderClass resources first + log_info "Deleting SecretProviderClass resources..." + kubectl delete secretproviderclasses.secrets-store.csi.x-k8s.io \ + --all --all-namespaces --ignore-not-found --timeout=30s 2>/dev/null || true + + log_info "Deleting SecretProviderClassPodStatus resources..." + kubectl delete secretproviderclasspodstatuses.secrets-store.csi.x-k8s.io \ + --all --all-namespaces --ignore-not-found --timeout=30s 2>/dev/null || true + + # Uninstall via Helm if available + if command -v helm &> /dev/null; then + if helm list -n "$CSI_NAMESPACE" 2>/dev/null | grep -q csi-secrets-store; then + log_info "Uninstalling CSI Secrets Store Driver via Helm..." + helm uninstall csi-secrets-store -n "$CSI_NAMESPACE" --wait --timeout 60s 2>/dev/null || true + fi + else + # Fallback to kubectl delete + log_info "Deleting CSI Secrets Store Driver resources via kubectl..." + local csi_url="https://raw.githubusercontent.com/kubernetes-sigs/secrets-store-csi-driver/v${CSI_DRIVER_VERSION}/deploy/secrets-store-csi-driver.yaml" + kubectl delete -f "$csi_url" --ignore-not-found --timeout=60s 2>/dev/null || true + fi + + # Delete CRDs + log_info "Deleting CSI Secrets Store CRDs..." + local csi_crds="secretproviderclasses.secrets-store.csi.x-k8s.io secretproviderclasspodstatuses.secrets-store.csi.x-k8s.io" + for crd in $csi_crds; do + safe_delete crd "$crd" --timeout=30s + done + + log_success "CSI Secrets Store Driver cleaned up" +} + +# ============================================================================= +# Argo Rollouts Cleanup +# ============================================================================= + +cleanup_argo_rollouts() { + log_header "Uninstalling Argo Rollouts" + + # Check if Argo Rollouts is installed + if ! kubectl get namespace "$ARGO_ROLLOUTS_NAMESPACE" &> /dev/null; then + log_info "Argo Rollouts namespace not found, skipping" + return 0 + fi + + # Stop the controller first + log_info "Stopping Argo Rollouts controller..." + safe_delete deployment argo-rollouts -n "$ARGO_ROLLOUTS_NAMESPACE" --timeout=30s + + # Delete all Argo Rollouts custom resources to avoid finalizer issues + log_info "Deleting Argo Rollouts custom resources..." + local argo_resources="rollouts analysisruns analysistemplates experiments" + for res in $argo_resources; do + kubectl delete "${res}.argoproj.io" --all --all-namespaces --ignore-not-found --timeout=30s 2>/dev/null || true + done + + # Delete using the install manifest + log_info "Deleting Argo Rollouts installation..." + local argo_url="https://github.com/argoproj/argo-rollouts/releases/download/${ARGO_ROLLOUTS_VERSION}/install.yaml" + kubectl delete -f "$argo_url" --ignore-not-found --timeout=60s 2>/dev/null || true + + # Give resources time to be cleaned up + sleep 2 + + # Delete CRDs + log_info "Deleting Argo Rollouts CRDs..." + local argo_crds="rollouts.argoproj.io analysisruns.argoproj.io analysistemplates.argoproj.io clusteranalysistemplates.argoproj.io experiments.argoproj.io" + for crd in $argo_crds; do + safe_delete crd "$crd" --timeout=30s + done + + # Delete namespace + log_info "Deleting Argo Rollouts namespace..." + safe_delete namespace "$ARGO_ROLLOUTS_NAMESPACE" --timeout=30s + + # Delete cluster-scoped RBAC + log_info "Deleting Argo Rollouts cluster RBAC..." + safe_delete clusterrole argo-rollouts argo-rollouts-aggregate-to-admin argo-rollouts-aggregate-to-edit argo-rollouts-aggregate-to-view + safe_delete clusterrolebinding argo-rollouts + + log_success "Argo Rollouts cleaned up" +} + +# ============================================================================= +# Main +# ============================================================================= + +main() { + echo "=== E2E Cluster Cleanup ===" + + # Pre-flight checks + check_dependencies + check_cluster_connectivity + + # Cleanup in reverse dependency order + # 1. First cleanup test resources (they depend on everything else) + cleanup_reloader_resources + + # 2. Then Vault (depends on CSI driver) + cleanup_vault + + # 3. Then CSI driver + cleanup_csi_driver + + # 4. Finally Argo Rollouts (independent) + cleanup_argo_rollouts + + # Summary + log_header "E2E Cluster Cleanup Complete" + echo "" + echo "Removed components:" + echo " ✓ Reloader test namespaces and cluster resources" + echo " ✓ Vault" + echo " ✓ CSI Secrets Store Driver" + echo " ✓ Argo Rollouts" +} + +main "$@" diff --git a/scripts/e2e-cluster-setup.sh b/scripts/e2e-cluster-setup.sh index eec7052..20d1b81 100644 --- a/scripts/e2e-cluster-setup.sh +++ b/scripts/e2e-cluster-setup.sh @@ -1,80 +1,351 @@ #!/bin/bash # Setup script for e2e test cluster # Run this before running e2e tests: ./scripts/e2e-cluster-setup.sh -# This installs Argo Rollouts and any other prerequisites needed for e2e tests. +# +# This installs: +# - Argo Rollouts (for Rollout workload testing) +# - CSI Secrets Store Driver (for SecretProviderClass testing) +# - Vault with CSI Provider (as the secrets backend for CSI) +# +# All versions are pinned for reproducibility and can be overridden via environment variables. -set -e +set -euo pipefail +# ============================================================================= +# Configuration (all versions pinned for reproducibility) +# ============================================================================= + +# Argo Rollouts ARGO_ROLLOUTS_VERSION="${ARGO_ROLLOUTS_VERSION:-v1.7.2}" ARGO_ROLLOUTS_NAMESPACE="argo-rollouts" -echo "=== E2E Cluster Setup ===" +# CSI Secrets Store Driver +CSI_DRIVER_VERSION="${CSI_DRIVER_VERSION:-1.5.5}" +CSI_NAMESPACE="kube-system" -# Check if kubectl is available -if ! command -v kubectl &> /dev/null; then - echo "Error: kubectl is not installed or not in PATH" - exit 1 -fi +# Vault (HashiCorp) +VAULT_CHART_VERSION="${VAULT_CHART_VERSION:-0.31.0}" +VAULT_VERSION="${VAULT_VERSION:-1.20.4}" +VAULT_CSI_PROVIDER_VERSION="${VAULT_CSI_PROVIDER_VERSION:-1.7.0}" +VAULT_NAMESPACE="vault" -# Check cluster connectivity -echo "Checking cluster connectivity..." -if ! kubectl cluster-info &> /dev/null; then - echo "Error: Cannot connect to Kubernetes cluster" - exit 1 -fi -echo "Cluster connectivity verified" +# ============================================================================= +# Helper Functions +# ============================================================================= -# Install Argo Rollouts -echo "" -echo "=== Installing Argo Rollouts ${ARGO_ROLLOUTS_VERSION} ===" +log_header() { + echo "" + echo "=== $1 ===" +} -# Check if Argo Rollouts is already installed -if kubectl get crd rollouts.argoproj.io &> /dev/null; then - echo "Argo Rollouts CRD already exists, checking if controller is running..." - if kubectl get deployment argo-rollouts -n ${ARGO_ROLLOUTS_NAMESPACE} &> /dev/null; then - echo "Argo Rollouts is already installed and running" - else - echo "Argo Rollouts CRD exists but controller not running, reinstalling..." +log_info() { + echo "$1" +} + +log_success() { + echo "✓ $1" +} + +log_warning() { + echo "⚠ $1" +} + +log_error() { + echo "✗ $1" >&2 +} + +check_command() { + if ! command -v "$1" &> /dev/null; then + log_error "$1 is not installed or not in PATH" + return 1 fi -else - echo "Installing Argo Rollouts..." -fi + return 0 +} -# Create namespace (ignore if exists) -kubectl create namespace ${ARGO_ROLLOUTS_NAMESPACE} 2>/dev/null || true +wait_for_rollout() { + local resource_type="$1" + local resource_name="$2" + local namespace="$3" + local timeout="${4:-180s}" -# Install Argo Rollouts -ARGO_URL="https://github.com/argoproj/argo-rollouts/releases/download/${ARGO_ROLLOUTS_VERSION}/install.yaml" -echo "Applying manifest from: ${ARGO_URL}" -kubectl apply -n ${ARGO_ROLLOUTS_NAMESPACE} -f ${ARGO_URL} + kubectl rollout status "$resource_type/$resource_name" -n "$namespace" --timeout="$timeout" +} -# Wait for deployment to exist -echo "Waiting for deployment to be created..." -sleep 2 +wait_for_condition() { + local condition="$1" + local resource="$2" + local namespace="${3:-}" + local timeout="${4:-60s}" -# Patch deployment to remove resource requirements (for Kind cluster compatibility) -# This avoids "Insufficient ephemeral-storage" errors in resource-constrained environments -echo "Patching deployment for Kind compatibility..." -PATCH_JSON='[{"op": "remove", "path": "/spec/template/spec/containers/0/resources"}]' -if ! kubectl patch deployment argo-rollouts -n ${ARGO_ROLLOUTS_NAMESPACE} --type=json -p "${PATCH_JSON}" 2>/dev/null; then - echo "JSON patch failed, trying strategic merge..." - PATCH_JSON='{"spec":{"template":{"spec":{"containers":[{"name":"argo-rollouts","resources":{"limits":null,"requests":null}}]}}}}' - kubectl patch deployment argo-rollouts -n ${ARGO_ROLLOUTS_NAMESPACE} --type=strategic -p "${PATCH_JSON}" || echo "Warning: Failed to patch resources" -fi + if [[ -n "$namespace" ]]; then + kubectl wait --for="condition=$condition" "$resource" -n "$namespace" --timeout="$timeout" + else + kubectl wait --for="condition=$condition" "$resource" --timeout="$timeout" + fi +} -# Wait for controller to be ready -echo "Waiting for Argo Rollouts controller to be ready..." -kubectl wait --for=condition=available deployment/argo-rollouts -n ${ARGO_ROLLOUTS_NAMESPACE} --timeout=180s +# ============================================================================= +# Dependency Checks +# ============================================================================= -# Wait for CRD to be established -echo "Waiting for Argo Rollouts CRD to be established..." -kubectl wait --for=condition=established crd/rollouts.argoproj.io --timeout=60s +check_dependencies() { + log_header "Checking Dependencies" -echo "" -echo "=== E2E Cluster Setup Complete ===" -echo "Argo Rollouts ${ARGO_ROLLOUTS_VERSION} is installed and ready" -echo "" -echo "You can now run e2e tests:" -echo " make e2e-test" -echo " # or" -echo " SKIP_BUILD=true RELOADER_IMAGE=ghcr.io/stakater/reloader:test go test -v ./test/e2e/..." + local missing_deps=() + + # Required: kubectl + if ! check_command kubectl; then + missing_deps+=("kubectl") + fi + + # Required: helm (for CSI driver and Vault installation) + if ! check_command helm; then + missing_deps+=("helm") + fi + + if [[ ${#missing_deps[@]} -gt 0 ]]; then + log_error "Missing required dependencies: ${missing_deps[*]}" + log_error "Please install the missing tools and try again." + exit 1 + fi + + log_success "All required dependencies are available" +} + +check_cluster_connectivity() { + log_header "Checking Cluster Connectivity" + + if ! kubectl cluster-info &> /dev/null; then + log_error "Cannot connect to Kubernetes cluster" + log_error "Please ensure your kubeconfig is correctly configured" + exit 1 + fi + + local context + context=$(kubectl config current-context) + log_success "Connected to cluster (context: $context)" +} + +# ============================================================================= +# Argo Rollouts Installation +# ============================================================================= + +install_argo_rollouts() { + log_header "Installing Argo Rollouts ${ARGO_ROLLOUTS_VERSION}" + + # Check if already installed + if kubectl get crd rollouts.argoproj.io &> /dev/null; then + if kubectl get deployment argo-rollouts -n "$ARGO_ROLLOUTS_NAMESPACE" &> /dev/null; then + log_success "Argo Rollouts is already installed" + return 0 + fi + log_info "Argo Rollouts CRD exists but controller not running, reinstalling..." + fi + + # Create namespace + kubectl create namespace "$ARGO_ROLLOUTS_NAMESPACE" 2>/dev/null || true + + # Install from official manifest + local argo_url="https://github.com/argoproj/argo-rollouts/releases/download/${ARGO_ROLLOUTS_VERSION}/install.yaml" + log_info "Applying manifest from: $argo_url" + kubectl apply -n "$ARGO_ROLLOUTS_NAMESPACE" -f "$argo_url" + + # Wait for deployment to be created + sleep 2 + + # Patch deployment to remove resource requirements (for Kind cluster compatibility) + log_info "Patching deployment for Kind compatibility..." + local patch_json='[{"op": "remove", "path": "/spec/template/spec/containers/0/resources"}]' + if ! kubectl patch deployment argo-rollouts -n "$ARGO_ROLLOUTS_NAMESPACE" --type=json -p "$patch_json" 2>/dev/null; then + patch_json='{"spec":{"template":{"spec":{"containers":[{"name":"argo-rollouts","resources":{"limits":null,"requests":null}}]}}}}' + kubectl patch deployment argo-rollouts -n "$ARGO_ROLLOUTS_NAMESPACE" --type=strategic -p "$patch_json" 2>/dev/null || true + fi + + # Wait for controller to be ready + log_info "Waiting for Argo Rollouts controller..." + wait_for_condition "available" "deployment/argo-rollouts" "$ARGO_ROLLOUTS_NAMESPACE" "180s" + wait_for_condition "established" "crd/rollouts.argoproj.io" "" "60s" + + log_success "Argo Rollouts ${ARGO_ROLLOUTS_VERSION} installed" +} + +# ============================================================================= +# CSI Secrets Store Driver Installation +# ============================================================================= + +install_csi_driver() { + log_header "Installing CSI Secrets Store Driver ${CSI_DRIVER_VERSION}" + + # Check if already installed + if kubectl get crd secretproviderclasses.secrets-store.csi.x-k8s.io &> /dev/null; then + if kubectl get daemonset -n "$CSI_NAMESPACE" -l app=secrets-store-csi-driver &> /dev/null 2>&1; then + log_success "CSI Secrets Store Driver is already installed" + return 0 + fi + log_info "CSI Driver CRD exists but DaemonSet not found, installing..." + fi + + # Add Helm repo + helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts 2>/dev/null || true + helm repo update secrets-store-csi-driver + + # Install via Helm with pinned version + log_info "Installing via Helm (version ${CSI_DRIVER_VERSION})..." + helm upgrade --install csi-secrets-store secrets-store-csi-driver/secrets-store-csi-driver \ + --namespace "$CSI_NAMESPACE" \ + --version "$CSI_DRIVER_VERSION" \ + --set syncSecret.enabled=true \ + --set enableSecretRotation=true \ + --set rotationPollInterval=2s \ + --wait \ + --timeout 180s + + # Wait for CRDs to be established + log_info "Waiting for CRDs to be established..." + wait_for_condition "established" "crd/secretproviderclasses.secrets-store.csi.x-k8s.io" "" "60s" + wait_for_condition "established" "crd/secretproviderclasspodstatuses.secrets-store.csi.x-k8s.io" "" "60s" + + # Wait for DaemonSet to be ready (try different names as they vary by installation method) + log_info "Waiting for CSI driver pods..." + kubectl rollout status daemonset/csi-secrets-store-secrets-store-csi-driver -n "$CSI_NAMESPACE" --timeout=180s 2>/dev/null || \ + kubectl rollout status daemonset/secrets-store-csi-driver -n "$CSI_NAMESPACE" --timeout=180s 2>/dev/null || \ + log_warning "Could not verify DaemonSet status (name may vary)" + + log_success "CSI Secrets Store Driver ${CSI_DRIVER_VERSION} installed" +} + +# ============================================================================= +# Vault Installation +# ============================================================================= + +install_vault() { + log_header "Installing Vault ${VAULT_VERSION} (Chart ${VAULT_CHART_VERSION})" + + # Check if already installed + if kubectl get pods -n "$VAULT_NAMESPACE" -l app.kubernetes.io/name=vault 2>/dev/null | grep -q Running; then + log_success "Vault is already installed and running" + return 0 + fi + + # Add Helm repo + helm repo add hashicorp https://helm.releases.hashicorp.com 2>/dev/null || true + helm repo update hashicorp + + # Install Vault in dev mode with CSI provider + # Dev mode: single server, in-memory storage, pre-unsealed, root token = "root" + log_info "Installing Vault via Helm..." + helm upgrade --install vault hashicorp/vault \ + --namespace "$VAULT_NAMESPACE" \ + --create-namespace \ + --version "$VAULT_CHART_VERSION" \ + --set "server.image.tag=${VAULT_VERSION}" \ + --set "server.dev.enabled=true" \ + --set "server.dev.devRootToken=root" \ + --set "server.resources.requests.memory=64Mi" \ + --set "server.resources.requests.cpu=50m" \ + --set "server.resources.limits.memory=128Mi" \ + --set "server.resources.limits.cpu=100m" \ + --set "injector.enabled=false" \ + --set "csi.enabled=true" \ + --set "csi.image.tag=${VAULT_CSI_PROVIDER_VERSION}" \ + --set "csi.resources.requests.memory=64Mi" \ + --set "csi.resources.requests.cpu=50m" \ + --set "csi.resources.limits.memory=128Mi" \ + --set "csi.resources.limits.cpu=100m" \ + --wait \ + --timeout 180s + + # Wait for pods to be ready + log_info "Waiting for Vault pod..." + kubectl wait --for=condition=ready pod -l app.kubernetes.io/name=vault -n "$VAULT_NAMESPACE" --timeout=120s + + log_info "Waiting for Vault CSI provider..." + wait_for_rollout "daemonset" "vault-csi-provider" "$VAULT_NAMESPACE" "120s" + + log_success "Vault ${VAULT_VERSION} installed" +} + +configure_vault() { + log_header "Configuring Vault for Kubernetes Authentication" + + # Enable KV secrets engine (ignore error if already enabled - dev mode has it by default) + log_info "Enabling KV secrets engine..." + kubectl exec -n "$VAULT_NAMESPACE" vault-0 -- vault secrets enable -path=secret kv-v2 2>/dev/null || true + + # Create test secrets for e2e tests + log_info "Creating test secrets..." + kubectl exec -n "$VAULT_NAMESPACE" vault-0 -- vault kv put secret/test username="test-user" password="test-password" + kubectl exec -n "$VAULT_NAMESPACE" vault-0 -- vault kv put secret/app1 api_key="app1-api-key-v1" db_password="app1-db-pass-v1" + kubectl exec -n "$VAULT_NAMESPACE" vault-0 -- vault kv put secret/app2 api_key="app2-api-key-v1" db_password="app2-db-pass-v1" + kubectl exec -n "$VAULT_NAMESPACE" vault-0 -- vault kv put secret/rotation-test value="initial-value-v1" + + # Enable Kubernetes auth method + log_info "Enabling Kubernetes auth..." + kubectl exec -n "$VAULT_NAMESPACE" vault-0 -- vault auth enable kubernetes 2>/dev/null || true + + # Configure Kubernetes auth to use in-cluster config + log_info "Configuring Kubernetes auth..." + kubectl exec -n "$VAULT_NAMESPACE" vault-0 -- sh -c \ + 'vault write auth/kubernetes/config kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"' + + # Create policy for reading test secrets + log_info "Creating Vault policy..." + kubectl exec -n "$VAULT_NAMESPACE" vault-0 -- sh -c 'vault policy write test-policy - <_` environment variable to containers. -### Global Auto-Reload +```yaml +spec: + template: + spec: + containers: + - env: + - name: STAKATER_MY_CONFIGMAP_CONFIGMAP + value: "" +``` -**`autoReloadAll`** -- `true`: all workloads auto-reload without needing annotations -- `auto=false` on a workload still opts it out +### Reference Methods ---- +All methods are tested for Deployment, DaemonSet, and StatefulSet: -## Annotation-Specific Tests +| Method | Description | ConfigMap | Secret | CSI | +|--------|-------------|-----------|--------|-----| +| `envFrom` | All keys as env vars | ✅ | ✅ | - | +| `valueFrom.configMapKeyRef` | Single key as env var | ✅ | - | - | +| `valueFrom.secretKeyRef` | Single key as env var | - | ✅ | - | +| Volume mount | Mount as files | ✅ | ✅ | ✅ | +| Projected volume | Combined sources | ✅ | ✅ | - | +| Init container (envFrom) | Init container env | ✅ | ✅ | - | +| Init container (volume) | Init container mount | ✅ | ✅ | ✅ | -### Auto Reload Variations +### Annotations -- `reloader.stakater.com/auto: "true"` - watches both ConfigMaps and Secrets -- `reloader.stakater.com/auto: "false"` - completely disables reload -- `configmap.reloader.stakater.com/auto: "true"` - only watches ConfigMaps -- `secret.reloader.stakater.com/auto: "true"` - only watches Secrets +#### Reload Triggers -### Combining Annotations +| Annotation | Description | +|------------|-------------| +| `configmap.reloader.stakater.com/reload` | Reload on specific ConfigMap(s) change | +| `secret.reloader.stakater.com/reload` | Reload on specific Secret(s) change | +| `secretproviderclass.reloader.stakater.com/reload` | Reload on specific SPC(s) change | -- `auto=true` + explicit reload annotation work together -- Auto-detected resources + explicitly listed resources both trigger reload -- Exclude annotations override auto-detection +#### Auto-Detection -### Search & Match +| Annotation | Description | +|------------|-------------| +| `reloader.stakater.com/auto: "true"` | Auto-detect all mounted resources | +| `configmap.reloader.stakater.com/auto: "true"` | Auto-detect ConfigMaps only | +| `secret.reloader.stakater.com/auto: "true"` | Auto-detect Secrets only | +| `secretproviderclass.reloader.stakater.com/auto: "true"` | Auto-detect SPCs only | -The search/match system lets you decouple workloads from specific resource names: +#### Exclusions -1. Workload has `reloader.stakater.com/search: "true"` -2. ConfigMap has `reloader.stakater.com/match: "true"` -3. When ConfigMap changes, workload reloads +| Annotation | Description | +|------------|-------------| +| `configmaps.exclude.reloader.stakater.com/reload` | Exclude ConfigMaps from auto | +| `secrets.exclude.reloader.stakater.com/reload` | Exclude Secrets from auto | +| `secretproviderclasses.exclude.reloader.stakater.com/reload` | Exclude SPCs from auto | +| `reloader.stakater.com/ignore: "true"` | On resource: prevents any reload | -**Tests verify:** -- Reload happens when both annotations present -- No reload when workload has search but ConfigMap lacks match -- No reload when ConfigMap has match but no workload has search -- Multiple workloads can have search, only ones with search reload +#### Search & Match -### Exclude Annotations +| Annotation | Target | Description | +|------------|--------|-------------| +| `reloader.stakater.com/search: "true"` | Workload | Watch for matching resources | +| `reloader.stakater.com/match: "true"` | Resource | Trigger watchers on change | -Exclude specific resources from auto-reload: +#### Other -- `configmap.reloader.stakater.com/exclude: "config-to-skip"` -- `secret.reloader.stakater.com/exclude: "secret-to-skip"` +| Annotation | Description | +|------------|-------------| +| `reloader.stakater.com/pause-period` | Pause deployment after reload | -**Tests verify:** -- Excluded ConfigMap changes don't trigger reload -- Non-excluded ConfigMap changes still trigger reload -- Same behavior for Secrets +### CLI Flags -### Resource Ignore +Tests verify these Reloader command-line flags: -Put this on the ConfigMap/Secret itself to prevent any reload: - -- `reloader.stakater.com/ignore: "true"` - -**Tests verify:** -- ConfigMap with ignore annotation never triggers reload -- Secret with ignore annotation never triggers reload -- Even with explicit reload annotation on workload - -### Pause Period - -Delay between detecting change and triggering reload: - -- `reloader.stakater.com/pause-period: "10s"` - -**Tests verify:** -- Deployment gets paused-at annotation after reload -- Without pause-period, no paused-at annotation - ---- - -## Advanced Scenarios - -### Pod Template Annotations - -Reloader reads annotations from both places: - -1. Deployment/DaemonSet/etc metadata -2. Pod template metadata (inside spec.template.metadata) - -**Tests verify:** -- Annotation only on pod template still works -- Annotation on both locations works -- Mismatched annotations (ConfigMap annotation but updating Secret) correctly doesn't reload - -### Regex Patterns - -Use regex in the reload annotation: - -- `configmap.reloader.stakater.com/reload: "app-config-.*"` -- `secret.reloader.stakater.com/reload: "db-creds-.*"` - -**Tests verify:** -- Matching ConfigMap/Secret triggers reload -- Non-matching ConfigMap/Secret doesn't trigger reload - -### Multiple Containers - -**Tests verify:** -- Multiple containers sharing one ConfigMap - changes trigger reload -- Multiple containers with different ConfigMaps - change to either triggers reload +| Flag | Description | +|------|-------------| +| `--namespaces-to-ignore` | Skip specified namespaces | +| `--namespace-selector` | Only watch namespaces with matching labels | +| `--watch-globally` | Watch all namespaces vs own namespace only | +| `--resource-label-selector` | Only watch resources with matching labels | +| `--ignore-secrets` | Ignore all Secret changes | +| `--ignore-configmaps` | Ignore all ConfigMap changes | +| `--ignore-cronjobs` | Skip CronJob workloads | +| `--ignore-jobs` | Skip Job workloads | +| `--reload-on-create` | Trigger reload on resource creation | +| `--reload-on-delete` | Trigger reload on resource deletion | +| `--auto-reload-all` | Auto-reload all workloads without annotations | +| `--enable-csi-integration` | Enable SecretProviderClass support | --- @@ -321,99 +261,163 @@ Use regex in the reload annotation: ``` test/e2e/ -├── core/ # Main tests (all workload types) -│ ├── workloads_test.go # Basic reload behavior -│ └── reference_methods_test.go # envFrom, volumes, etc. -├── annotations/ # Annotation-specific behavior -│ ├── auto_reload_test.go -│ ├── combination_test.go -│ ├── exclude_test.go -│ ├── search_match_test.go -│ ├── pause_period_test.go -│ └── resource_ignore_test.go -├── flags/ # CLI flag behavior -│ ├── namespace_selector_test.go -│ ├── namespace_ignore_test.go -│ ├── resource_selector_test.go +├── core/ # Core workload tests +│ ├── core_suite_test.go +│ └── workloads_test.go # All workload types, both strategies +│ +├── annotations/ # Annotation behavior tests +│ ├── annotations_suite_test.go +│ ├── auto_reload_test.go # Auto-detection variations +│ ├── combination_test.go # Multiple annotations together +│ ├── exclude_test.go # Exclude annotations +│ ├── pause_period_test.go # Pause after reload +│ ├── resource_ignore_test.go # Ignore annotation on resources +│ └── search_match_test.go # Search/match pattern +│ +├── flags/ # CLI flag tests +│ ├── flags_suite_test.go +│ ├── auto_reload_all_test.go │ ├── ignore_resources_test.go │ ├── ignored_workloads_test.go -│ ├── auto_reload_all_test.go +│ ├── namespace_ignore_test.go +│ ├── namespace_selector_test.go │ ├── reload_on_create_test.go │ ├── reload_on_delete_test.go +│ ├── resource_selector_test.go │ └── watch_globally_test.go -├── advanced/ # Edge cases -│ ├── job_reload_test.go -│ ├── multi_container_test.go -│ ├── pod_annotations_test.go -│ └── regex_test.go -├── argo/ # Argo Rollouts (requires installation) +│ +├── advanced/ # Advanced scenarios +│ ├── advanced_suite_test.go +│ ├── job_reload_test.go # Job recreation +│ ├── multi_container_test.go # Multiple containers +│ ├── pod_annotations_test.go # Pod template annotations +│ └── regex_test.go # Regex patterns +│ +├── csi/ # CSI SecretProviderClass tests +│ ├── csi_suite_test.go +│ └── csi_test.go # SPC-specific scenarios +│ +├── argo/ # Argo Rollouts (requires installation) +│ ├── argo_suite_test.go │ └── rollout_test.go -├── openshift/ # OpenShift (requires cluster) -│ └── deploymentconfig_test.go -└── utils/ # Shared test helpers +│ +└── utils/ # Shared test utilities + ├── annotations.go # Annotation builders + ├── constants.go # Test constants + ├── csi.go # CSI client and helpers + ├── resources.go # Resource creation helpers + ├── testenv.go # Test environment setup + ├── wait.go # Wait/polling utilities + ├── workload_adapter.go # Workload abstraction interface + ├── workload_deployment.go # Deployment adapter + ├── workload_daemonset.go # DaemonSet adapter + ├── workload_statefulset.go # StatefulSet adapter + ├── workload_cronjob.go # CronJob adapter + ├── workload_job.go # Job adapter + ├── workload_argo.go # Argo Rollout adapter + └── workload_openshift.go # DeploymentConfig adapter ``` --- -## Debugging Failed Tests +## Debugging -### See What's Happening +### View Test Output ```bash # Verbose output -go test -v ./test/e2e/core/... +go tool ginkgo -v ./test/e2e/core/... -# Run one specific test -go test -v ./test/e2e/core/... --ginkgo.focus="should reload when ConfigMap" +# Focus on specific test +go tool ginkgo -v --focus="should reload when ConfigMap" ./test/e2e/... -# Keep the cluster around after tests -SKIP_CLEANUP=true make e2e +# Show all spec names +go tool ginkgo -v --dry-run ./test/e2e/... ``` ### Check Reloader Logs ```bash -# Find the Reloader pod +# Find Reloader pod kubectl get pods -A | grep reloader -# Check its logs -kubectl logs -n -l app=reloader-reloader --tail=100 +# View logs +kubectl logs -n -l app.kubernetes.io/name=reloader --tail=100 -f + +# Check events +kubectl get events -n --sort-by='.lastTimestamp' ``` -### Common Problems +### Inspect Test Resources -| Problem | Solution | -|---------|----------| -| Test timeout | Reloader might not be running - check pod status | -| Argo tests skipped | Install Argo Rollouts first | -| OpenShift tests skipped | Only work on OpenShift clusters | -| "resource not found" | Missing CRDs (Argo, OpenShift) | +```bash +# List test namespaces +kubectl get ns | grep reloader + +# Check workloads in test namespace +kubectl get deploy,ds,sts,cronjob,job -n + +# Check ConfigMaps/Secrets +kubectl get cm,secret -n + +# Check CSI resources +kubectl get secretproviderclass,secretproviderclasspodstatus -n +``` + +### Common Issues + +| Issue | Cause | Solution | +|-------|-------|----------| +| Tests timeout | Reloader not running | Check pod status and logs | +| CSI tests skipped | CSI driver not installed | Run `make e2e-setup` | +| Argo tests skipped | Argo Rollouts not installed | Run `make e2e-setup` | +| OpenShift tests skipped | Not an OpenShift cluster | Expected on Kind | +| "resource not found" | Missing CRDs | Install required components | +| Duplicate volume names | Test bug | Check CSI volume naming | --- -## Environment Variables +## Writing Tests -| Variable | What it does | Default | -|----------|--------------|---------| -| `RELOADER_IMAGE` | Image to test | `ghcr.io/stakater/reloader:test` | -| `SKIP_BUILD` | Don't build the image | `false` | -| `SKIP_CLEANUP` | Keep cluster after tests | `false` | -| `KIND_CLUSTER` | Kind cluster name | `kind` | -| `KUBECONFIG` | Kubernetes config path | `~/.kube/config` | +### Using the Workload Adapter Pattern ---- - -## Writing New Tests - -### For Multiple Workload Types - -Use the adapter pattern to test the same behavior across Deployments, DaemonSets, etc: +Test the same behavior across multiple workload types: ```go DescribeTable("should reload when ConfigMap changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - // ... create ConfigMap, workload, update ConfigMap, verify reload + if adapter == nil { + Skip(fmt.Sprintf("%s not available", workloadType)) + } + + // Create ConfigMap + _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "initial"}, nil) + Expect(err).NotTo(HaveOccurred()) + + // Create workload via adapter + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + ConfigMapName: configMapName, + UseConfigMapEnvFrom: true, + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + }) + Expect(err).NotTo(HaveOccurred()) + + // Wait for ready + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + // Update ConfigMap + err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "updated"}) + Expect(err).NotTo(HaveOccurred()) + + // Verify reload + reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue()) }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), @@ -421,25 +425,66 @@ DescribeTable("should reload when ConfigMap changes", ) ``` -### For Deployment-Only Tests +### Direct Resource Creation -Use the direct creation helpers: +For Deployment-specific tests: ```go -It("should reload with my specific setup", func() { +It("should reload with custom setup", func() { _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, map[string]string{"key": "value"}, nil) + Expect(err).NotTo(HaveOccurred()) _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, utils.WithConfigMapEnvFrom(configMapName), utils.WithAnnotations(utils.BuildAutoTrueAnnotation()), ) + Expect(err).NotTo(HaveOccurred()) - // Update and verify... + // ... test logic ... }) ``` -### Negative Tests (Verifying Nothing Happens) +### CSI Tests + +```go +It("should reload when SecretProviderClassPodStatus changes", func() { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI driver not installed") + } + + // Create SPC + _, err := utils.CreateSecretProviderClass(ctx, csiClient, testNamespace, spcName, nil) + Expect(err).NotTo(HaveOccurred()) + + // Create SPCPS + _, err = utils.CreateSecretProviderClassPodStatus(ctx, csiClient, testNamespace, spcpsName, spcName, + utils.NewSPCPSObjects("secret1", "v1")) + Expect(err).NotTo(HaveOccurred()) + + // Create Deployment with CSI volume + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, + utils.WithCSIVolume(spcName), + utils.WithAnnotations(utils.BuildSecretProviderClassReloadAnnotation(spcName)), + ) + Expect(err).NotTo(HaveOccurred()) + + // Update SPCPS + err = utils.UpdateSecretProviderClassPodStatus(ctx, csiClient, testNamespace, spcpsName, + utils.NewSPCPSObjects("secret1", "v2")) + Expect(err).NotTo(HaveOccurred()) + + // Verify reload + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue()) +}) +``` + +### Negative Tests + +Verify that something does NOT trigger a reload: ```go It("should NOT reload when only labels change", func() { @@ -448,10 +493,29 @@ It("should NOT reload when only labels change", func() { // Make a change that shouldn't trigger reload err = utils.UpdateConfigMapLabels(ctx, kubeClient, testNamespace, configMapName, map[string]string{"new-label": "value"}) + Expect(err).NotTo(HaveOccurred()) - // Wait a bit, then verify NO reload happened + // Wait briefly, then verify NO reload time.Sleep(utils.NegativeTestWait) - reloaded, _ := utils.WaitForDeploymentReloaded(...) - Expect(reloaded).To(BeFalse()) + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ShortTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeFalse(), "Should NOT have reloaded") }) ``` + +### Test Labels + +Use labels to categorize tests: + +```go +Entry("Deployment", Label("csi"), utils.WorkloadDeployment), +Entry("with OpenShift", Label("openshift"), utils.WorkloadDeploymentConfig), +Entry("with Argo", Label("argo"), utils.WorkloadArgoRollout), +``` + +Run by label: +```bash +go tool ginkgo --label-filter="csi" ./test/e2e/... +go tool ginkgo --label-filter="!openshift && !argo" ./test/e2e/... +``` diff --git a/test/e2e/advanced/advanced_suite_test.go b/test/e2e/advanced/advanced_suite_test.go index b6cb6e6..4d98db3 100644 --- a/test/e2e/advanced/advanced_suite_test.go +++ b/test/e2e/advanced/advanced_suite_test.go @@ -6,12 +6,17 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/stakater/Reloader/test/e2e/utils" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + csiclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" + + "github.com/stakater/Reloader/test/e2e/utils" ) var ( kubeClient kubernetes.Interface + csiClient csiclient.Interface + restConfig *rest.Config testNamespace string ctx context.Context testEnv *utils.TestEnvironment @@ -26,18 +31,25 @@ var _ = BeforeSuite(func() { var err error ctx = context.Background() - // Setup test environment testEnv, err = utils.SetupTestEnvironment(ctx, "reloader-advanced") Expect(err).NotTo(HaveOccurred(), "Failed to setup test environment") - // Export for use in tests kubeClient = testEnv.KubeClient + csiClient = testEnv.CSIClient + restConfig = testEnv.RestConfig testNamespace = testEnv.Namespace - // Deploy Reloader with annotations strategy - err = testEnv.DeployAndWait(map[string]string{ + deployValues := map[string]string{ "reloader.reloadStrategy": "annotations", - }) + "reloader.watchGlobally": "false", // Only watch own namespace to prevent cross-talk between test suites + } + + if utils.IsCSIDriverInstalled(ctx, csiClient) { + deployValues["reloader.enableCSIIntegration"] = "true" + GinkgoWriter.Println("Deploying Reloader with CSI integration support") + } + + err = testEnv.DeployAndWait(deployValues) Expect(err).NotTo(HaveOccurred(), "Failed to deploy Reloader") }) diff --git a/test/e2e/advanced/job_reload_test.go b/test/e2e/advanced/job_reload_test.go index e2d1350..9ad3e38 100644 --- a/test/e2e/advanced/job_reload_test.go +++ b/test/e2e/advanced/job_reload_test.go @@ -3,6 +3,7 @@ package advanced import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) @@ -35,8 +36,7 @@ var _ = Describe("Job Workload Recreation Tests", func() { By("Creating a Job with ConfigMap envFrom") job, err := utils.CreateJob(ctx, kubeClient, testNamespace, jobName, utils.WithJobConfigMapEnvFrom(configMapName), - utils.WithJobAnnotations(utils.BuildConfigMapReloadAnnotation(configMapName)), - ) + utils.WithJobAnnotations(utils.BuildConfigMapReloadAnnotation(configMapName))) Expect(err).NotTo(HaveOccurred()) originalUID := string(job.UID) @@ -50,8 +50,8 @@ var _ = Describe("Job Workload Recreation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Job to be recreated (new UID)") - _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, - originalUID, utils.ReloadTimeout) + _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, originalUID, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(recreated).To(BeTrue(), "Job should be recreated with new UID when ConfigMap changes") }) @@ -65,10 +65,8 @@ var _ = Describe("Job Workload Recreation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Creating a Job with Secret envFrom") - job, err := utils.CreateJob(ctx, kubeClient, testNamespace, jobName, - utils.WithJobSecretEnvFrom(secretName), - utils.WithJobAnnotations(utils.BuildSecretReloadAnnotation(secretName)), - ) + job, err := utils.CreateJob(ctx, kubeClient, testNamespace, jobName, utils.WithJobSecretEnvFrom(secretName), + utils.WithJobAnnotations(utils.BuildSecretReloadAnnotation(secretName))) Expect(err).NotTo(HaveOccurred()) originalUID := string(job.UID) @@ -82,8 +80,8 @@ var _ = Describe("Job Workload Recreation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Job to be recreated (new UID)") - _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, - originalUID, utils.ReloadTimeout) + _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, originalUID, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(recreated).To(BeTrue(), "Job should be recreated with new UID when Secret changes") }) @@ -99,8 +97,7 @@ var _ = Describe("Job Workload Recreation Tests", func() { By("Creating a Job with auto annotation") job, err := utils.CreateJob(ctx, kubeClient, testNamespace, jobName, utils.WithJobConfigMapEnvFrom(configMapName), - utils.WithJobAnnotations(utils.BuildAutoTrueAnnotation()), - ) + utils.WithJobAnnotations(utils.BuildAutoTrueAnnotation())) Expect(err).NotTo(HaveOccurred()) originalUID := string(job.UID) @@ -114,8 +111,8 @@ var _ = Describe("Job Workload Recreation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Job to be recreated (new UID)") - _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, - originalUID, utils.ReloadTimeout) + _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, originalUID, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(recreated).To(BeTrue(), "Job with auto=true should be recreated when ConfigMap changes") }) @@ -131,8 +128,7 @@ var _ = Describe("Job Workload Recreation Tests", func() { By("Creating a Job with valueFrom.configMapKeyRef") job, err := utils.CreateJob(ctx, kubeClient, testNamespace, jobName, utils.WithJobConfigMapKeyRef(configMapName, "config_key", "MY_CONFIG"), - utils.WithJobAnnotations(utils.BuildConfigMapReloadAnnotation(configMapName)), - ) + utils.WithJobAnnotations(utils.BuildConfigMapReloadAnnotation(configMapName))) Expect(err).NotTo(HaveOccurred()) originalUID := string(job.UID) @@ -146,10 +142,11 @@ var _ = Describe("Job Workload Recreation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Job to be recreated (new UID)") - _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, - originalUID, utils.ReloadTimeout) + _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, originalUID, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) - Expect(recreated).To(BeTrue(), "Job with valueFrom.configMapKeyRef should be recreated when ConfigMap changes") + Expect(recreated).To(BeTrue(), + "Job with valueFrom.configMapKeyRef should be recreated when ConfigMap changes") }) }) @@ -163,8 +160,7 @@ var _ = Describe("Job Workload Recreation Tests", func() { By("Creating a Job with valueFrom.secretKeyRef") job, err := utils.CreateJob(ctx, kubeClient, testNamespace, jobName, utils.WithJobSecretKeyRef(secretName, "secret_key", "MY_SECRET"), - utils.WithJobAnnotations(utils.BuildSecretReloadAnnotation(secretName)), - ) + utils.WithJobAnnotations(utils.BuildSecretReloadAnnotation(secretName))) Expect(err).NotTo(HaveOccurred()) originalUID := string(job.UID) @@ -178,8 +174,8 @@ var _ = Describe("Job Workload Recreation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Job to be recreated (new UID)") - _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, - originalUID, utils.ReloadTimeout) + _, recreated, err := utils.WaitForJobRecreated(ctx, kubeClient, testNamespace, jobName, originalUID, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(recreated).To(BeTrue(), "Job with valueFrom.secretKeyRef should be recreated when Secret changes") }) diff --git a/test/e2e/advanced/multi_container_test.go b/test/e2e/advanced/multi_container_test.go index 1b77c41..0c84bad 100644 --- a/test/e2e/advanced/multi_container_test.go +++ b/test/e2e/advanced/multi_container_test.go @@ -1,8 +1,12 @@ package advanced import ( + "fmt" + "time" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) @@ -91,4 +95,125 @@ var _ = Describe("Multi-Container Tests", func() { Expect(reloaded).To(BeTrue(), "Deployment should be reloaded when first container's ConfigMap changes") }) }) + + Context("Init container with CSI volume", Label("csi"), func() { + var ( + spcName string + vaultSecretPath string + ) + + BeforeEach(func() { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + spcName = utils.RandName("spc") + vaultSecretPath = fmt.Sprintf("secret/%s", utils.RandName("test")) + }) + + AfterEach(func() { + if spcName != "" { + _ = utils.DeleteSecretProviderClass(ctx, csiClient, testNamespace, spcName) + } + if vaultSecretPath != "" { + _ = utils.DeleteVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath) + } + }) + + It("should reload when SecretProviderClassPodStatus used by init container changes", func() { + By("Creating a Vault secret") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, map[string]string{ + "api_key": "initial-init-value", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Deployment with init container using CSI volume") + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, + utils.WithInitContainerCSIVolume(spcName), + utils.WithAnnotations(utils.BuildSecretProviderClassReloadAnnotation(spcName)), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, map[string]string{ + "api_key": "updated-init-value", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync (SPCPS version change)") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be reloaded") + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "Deployment with init container using CSI volume should be reloaded") + }) + + It("should reload with auto annotation when init container CSI volume changes", func() { + By("Creating a Vault secret") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, map[string]string{ + "api_key": "initial-init-auto-value", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Deployment with init container using CSI volume and auto annotation") + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, + utils.WithInitContainerCSIVolume(spcName), + utils.WithAnnotations(utils.BuildAutoTrueAnnotation()), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, map[string]string{ + "api_key": "updated-init-auto-value", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync (SPCPS version change)") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be reloaded") + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "Deployment with init container CSI volume and auto=true should be reloaded") + }) + }) }) diff --git a/test/e2e/advanced/pod_annotations_test.go b/test/e2e/advanced/pod_annotations_test.go index 25b8419..0f86b14 100644 --- a/test/e2e/advanced/pod_annotations_test.go +++ b/test/e2e/advanced/pod_annotations_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/advanced/regex_test.go b/test/e2e/advanced/regex_test.go index 67efe97..4ace786 100644 --- a/test/e2e/advanced/regex_test.go +++ b/test/e2e/advanced/regex_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/annotations/annotations_suite_test.go b/test/e2e/annotations/annotations_suite_test.go index a500b04..ac5ea98 100644 --- a/test/e2e/annotations/annotations_suite_test.go +++ b/test/e2e/annotations/annotations_suite_test.go @@ -6,14 +6,17 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/stakater/Reloader/test/e2e/utils" - "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + csiclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" + + "github.com/stakater/Reloader/test/e2e/utils" ) var ( kubeClient kubernetes.Interface - dynamicClient dynamic.Interface + csiClient csiclient.Interface + restConfig *rest.Config testNamespace string ctx context.Context cancel context.CancelFunc @@ -25,35 +28,43 @@ func TestAnnotations(t *testing.T) { RunSpecs(t, "Annotations Strategy E2E Suite") } -var _ = BeforeSuite(func() { - var err error - ctx, cancel = context.WithCancel(context.Background()) +var _ = BeforeSuite( + func() { + var err error + ctx, cancel = context.WithCancel(context.Background()) - // Setup test environment - testEnv, err = utils.SetupTestEnvironment(ctx, "reloader-annotations-test") - Expect(err).NotTo(HaveOccurred(), "Failed to setup test environment") + testEnv, err = utils.SetupTestEnvironment(ctx, "reloader-annotations-test") + Expect(err).NotTo(HaveOccurred(), "Failed to setup test environment") - // Export for use in tests - kubeClient = testEnv.KubeClient - dynamicClient = testEnv.DynamicClient - testNamespace = testEnv.Namespace + kubeClient = testEnv.KubeClient + csiClient = testEnv.CSIClient + restConfig = testEnv.RestConfig + testNamespace = testEnv.Namespace - // Deploy Reloader with annotations strategy - err = testEnv.DeployAndWait(map[string]string{ - "reloader.reloadStrategy": "annotations", + deployValues := map[string]string{ + "reloader.reloadStrategy": "annotations", + "reloader.watchGlobally": "false", // Only watch own namespace to prevent cross-talk between test suites + } + + if utils.IsCSIDriverInstalled(ctx, csiClient) { + deployValues["reloader.enableCSIIntegration"] = "true" + GinkgoWriter.Println("Deploying Reloader with CSI integration support") + } + + err = testEnv.DeployAndWait(deployValues) + Expect(err).NotTo(HaveOccurred(), "Failed to deploy Reloader") }) - Expect(err).NotTo(HaveOccurred(), "Failed to deploy Reloader") -}) -var _ = AfterSuite(func() { - if testEnv != nil { - err := testEnv.Cleanup() - Expect(err).NotTo(HaveOccurred(), "Failed to cleanup test environment") - } +var _ = AfterSuite( + func() { + if testEnv != nil { + err := testEnv.Cleanup() + Expect(err).NotTo(HaveOccurred(), "Failed to cleanup test environment") + } - if cancel != nil { - cancel() - } + if cancel != nil { + cancel() + } - GinkgoWriter.Println("Annotations E2E Suite cleanup complete") -}) + GinkgoWriter.Println("Annotations E2E Suite cleanup complete") + }) diff --git a/test/e2e/annotations/auto_reload_test.go b/test/e2e/annotations/auto_reload_test.go index baa0e92..f89ebb2 100644 --- a/test/e2e/annotations/auto_reload_test.go +++ b/test/e2e/annotations/auto_reload_test.go @@ -1,30 +1,40 @@ package annotations import ( + "fmt" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) var _ = Describe("Auto Reload Annotation Tests", func() { var ( - deploymentName string - configMapName string - secretName string + deploymentName string + configMapName string + secretName string + spcName string + vaultSecretPath string ) BeforeEach(func() { deploymentName = utils.RandName("deploy") configMapName = utils.RandName("cm") secretName = utils.RandName("secret") + spcName = utils.RandName("spc") + vaultSecretPath = fmt.Sprintf("secret/%s", utils.RandName("test")) }) AfterEach(func() { _ = utils.DeleteDeployment(ctx, kubeClient, testNamespace, deploymentName) _ = utils.DeleteConfigMap(ctx, kubeClient, testNamespace, configMapName) _ = utils.DeleteSecret(ctx, kubeClient, testNamespace, secretName) + if csiClient != nil { + _ = utils.DeleteSecretProviderClass(ctx, csiClient, testNamespace, spcName) + } + _ = utils.DeleteVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath) }) Context("with reloader.stakater.com/auto=true annotation", func() { @@ -225,6 +235,176 @@ var _ = Describe("Auto Reload Annotation Tests", func() { }) }) + Context("with secretproviderclass.reloader.stakater.com/auto=true annotation", Label("csi"), func() { + BeforeEach(func() { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + }) + + It("should reload Deployment when SecretProviderClassPodStatus changes", func() { + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Deployment with secretproviderclass auto=true annotation") + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, + utils.WithCSIVolume(spcName), + utils.WithAnnotations(utils.BuildSecretProviderClassAutoAnnotation()), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Printf("Found SPCPS: %s\n", spcpsName) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Printf("Initial SPCPS version: %s\n", initialVersion) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Println("CSI driver synced new secret version") + + By("Waiting for Deployment to be reloaded") + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "Deployment should have been reloaded for Vault secret change") + }) + + It("should NOT reload Deployment when ConfigMap changes (only SPC auto enabled)", func() { + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating a ConfigMap") + _, err = utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "initial"}, nil) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Deployment with CSI volume AND ConfigMap, but only SPC auto annotation") + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, + utils.WithCSIVolume(spcName), + utils.WithConfigMapEnvFrom(configMapName), + utils.WithAnnotations(utils.BuildSecretProviderClassAutoAnnotation()), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the ConfigMap (should NOT trigger reload with SPC auto only)") + err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "updated"}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying Deployment was NOT reloaded for ConfigMap change") + time.Sleep(utils.NegativeTestWait) + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ShortTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeFalse(), "Deployment with SPC auto only should NOT have been reloaded for ConfigMap change") + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret (should trigger reload)") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be reloaded for SPC change") + reloaded, err = utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "Deployment should have been reloaded for Vault secret change") + }) + + It("should reload when using combined auto=true annotation for SPC", func() { + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Deployment with CSI volume and general auto=true annotation") + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, + utils.WithCSIVolume(spcName), + utils.WithAnnotations(utils.BuildAutoTrueAnnotation()), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be reloaded") + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "Deployment with auto=true should have been reloaded for Vault secret change") + }) + }) + Context("with auto annotation and explicit reload annotation together", func() { It("should reload when auto-detected resource changes", func() { configMapName2 := utils.RandName("cm2") diff --git a/test/e2e/annotations/combination_test.go b/test/e2e/annotations/combination_test.go index 3d13d7a..44c5c6e 100644 --- a/test/e2e/annotations/combination_test.go +++ b/test/e2e/annotations/combination_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/annotations/exclude_test.go b/test/e2e/annotations/exclude_test.go index 831895d..63c314c 100644 --- a/test/e2e/annotations/exclude_test.go +++ b/test/e2e/annotations/exclude_test.go @@ -1,10 +1,12 @@ package annotations import ( + "fmt" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) @@ -15,7 +17,6 @@ var _ = Describe("Exclude Annotation Tests", func() { configMapName2 string secretName string secretName2 string - excludeNS string ) BeforeEach(func() { @@ -24,35 +25,29 @@ var _ = Describe("Exclude Annotation Tests", func() { configMapName2 = utils.RandName("cm2") secretName = utils.RandName("secret") secretName2 = utils.RandName("secret2") - excludeNS = "exclude-" + utils.RandName("ns") - - // Create test namespace - err := utils.CreateNamespace(ctx, kubeClient, excludeNS) - Expect(err).NotTo(HaveOccurred()) }) AfterEach(func() { - _ = utils.DeleteDeployment(ctx, kubeClient, excludeNS, deploymentName) - _ = utils.DeleteConfigMap(ctx, kubeClient, excludeNS, configMapName) - _ = utils.DeleteConfigMap(ctx, kubeClient, excludeNS, configMapName2) - _ = utils.DeleteSecret(ctx, kubeClient, excludeNS, secretName) - _ = utils.DeleteSecret(ctx, kubeClient, excludeNS, secretName2) - _ = utils.DeleteNamespace(ctx, kubeClient, excludeNS) + _ = utils.DeleteDeployment(ctx, kubeClient, testNamespace, deploymentName) + _ = utils.DeleteConfigMap(ctx, kubeClient, testNamespace, configMapName) + _ = utils.DeleteConfigMap(ctx, kubeClient, testNamespace, configMapName2) + _ = utils.DeleteSecret(ctx, kubeClient, testNamespace, secretName) + _ = utils.DeleteSecret(ctx, kubeClient, testNamespace, secretName2) }) Context("ConfigMap exclude annotation", func() { It("should NOT reload when excluded ConfigMap changes", func() { By("Creating two ConfigMaps") - _, err := utils.CreateConfigMap(ctx, kubeClient, excludeNS, configMapName, + _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, map[string]string{"key": "initial"}, nil) Expect(err).NotTo(HaveOccurred()) - _, err = utils.CreateConfigMap(ctx, kubeClient, excludeNS, configMapName2, + _, err = utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName2, map[string]string{"key2": "initial2"}, nil) Expect(err).NotTo(HaveOccurred()) By("Creating a Deployment with auto=true and configmaps.exclude annotation") - _, err = utils.CreateDeployment(ctx, kubeClient, excludeNS, deploymentName, + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, utils.WithConfigMapEnvFrom(configMapName), utils.WithConfigMapEnvFrom(configMapName2), utils.WithAnnotations(utils.MergeAnnotations( @@ -63,17 +58,17 @@ var _ = Describe("Exclude Annotation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Deployment to be ready") - err = utils.WaitForDeploymentReady(ctx, kubeClient, excludeNS, deploymentName, utils.DeploymentReady) + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) Expect(err).NotTo(HaveOccurred()) By("Updating the excluded ConfigMap") - err = utils.UpdateConfigMap(ctx, kubeClient, excludeNS, configMapName, + err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, map[string]string{"key": "updated"}) Expect(err).NotTo(HaveOccurred()) By("Verifying Deployment was NOT reloaded (excluded ConfigMap)") time.Sleep(utils.NegativeTestWait) - reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, excludeNS, deploymentName, + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, utils.AnnotationLastReloadedFrom, utils.ShortTimeout) Expect(err).NotTo(HaveOccurred()) Expect(reloaded).To(BeFalse(), "Deployment should NOT reload when excluded ConfigMap changes") @@ -81,16 +76,16 @@ var _ = Describe("Exclude Annotation Tests", func() { It("should reload when non-excluded ConfigMap changes", func() { By("Creating two ConfigMaps") - _, err := utils.CreateConfigMap(ctx, kubeClient, excludeNS, configMapName, + _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, map[string]string{"key": "initial"}, nil) Expect(err).NotTo(HaveOccurred()) - _, err = utils.CreateConfigMap(ctx, kubeClient, excludeNS, configMapName2, + _, err = utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName2, map[string]string{"key2": "initial2"}, nil) Expect(err).NotTo(HaveOccurred()) By("Creating a Deployment with auto=true and configmaps.exclude annotation") - _, err = utils.CreateDeployment(ctx, kubeClient, excludeNS, deploymentName, + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, utils.WithConfigMapEnvFrom(configMapName), utils.WithConfigMapEnvFrom(configMapName2), utils.WithAnnotations(utils.MergeAnnotations( @@ -101,16 +96,16 @@ var _ = Describe("Exclude Annotation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Deployment to be ready") - err = utils.WaitForDeploymentReady(ctx, kubeClient, excludeNS, deploymentName, utils.DeploymentReady) + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) Expect(err).NotTo(HaveOccurred()) By("Updating the non-excluded ConfigMap") - err = utils.UpdateConfigMap(ctx, kubeClient, excludeNS, configMapName2, + err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName2, map[string]string{"key2": "updated2"}) Expect(err).NotTo(HaveOccurred()) By("Waiting for Deployment to be reloaded") - reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, excludeNS, deploymentName, + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(reloaded).To(BeTrue(), "Deployment should reload when non-excluded ConfigMap changes") @@ -120,16 +115,16 @@ var _ = Describe("Exclude Annotation Tests", func() { Context("Secret exclude annotation", func() { It("should NOT reload when excluded Secret changes", func() { By("Creating two Secrets") - _, err := utils.CreateSecretFromStrings(ctx, kubeClient, excludeNS, secretName, + _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, map[string]string{"password": "initial"}, nil) Expect(err).NotTo(HaveOccurred()) - _, err = utils.CreateSecretFromStrings(ctx, kubeClient, excludeNS, secretName2, + _, err = utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName2, map[string]string{"password2": "initial2"}, nil) Expect(err).NotTo(HaveOccurred()) By("Creating a Deployment with auto=true and secrets.exclude annotation") - _, err = utils.CreateDeployment(ctx, kubeClient, excludeNS, deploymentName, + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, utils.WithSecretEnvFrom(secretName), utils.WithSecretEnvFrom(secretName2), utils.WithAnnotations(utils.MergeAnnotations( @@ -140,17 +135,17 @@ var _ = Describe("Exclude Annotation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Deployment to be ready") - err = utils.WaitForDeploymentReady(ctx, kubeClient, excludeNS, deploymentName, utils.DeploymentReady) + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) Expect(err).NotTo(HaveOccurred()) By("Updating the excluded Secret") - err = utils.UpdateSecretFromStrings(ctx, kubeClient, excludeNS, secretName, + err = utils.UpdateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, map[string]string{"password": "updated"}) Expect(err).NotTo(HaveOccurred()) By("Verifying Deployment was NOT reloaded (excluded Secret)") time.Sleep(utils.NegativeTestWait) - reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, excludeNS, deploymentName, + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, utils.AnnotationLastReloadedFrom, utils.ShortTimeout) Expect(err).NotTo(HaveOccurred()) Expect(reloaded).To(BeFalse(), "Deployment should NOT reload when excluded Secret changes") @@ -158,16 +153,16 @@ var _ = Describe("Exclude Annotation Tests", func() { It("should reload when non-excluded Secret changes", func() { By("Creating two Secrets") - _, err := utils.CreateSecretFromStrings(ctx, kubeClient, excludeNS, secretName, + _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, map[string]string{"password": "initial"}, nil) Expect(err).NotTo(HaveOccurred()) - _, err = utils.CreateSecretFromStrings(ctx, kubeClient, excludeNS, secretName2, + _, err = utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName2, map[string]string{"password2": "initial2"}, nil) Expect(err).NotTo(HaveOccurred()) By("Creating a Deployment with auto=true and secrets.exclude annotation") - _, err = utils.CreateDeployment(ctx, kubeClient, excludeNS, deploymentName, + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, utils.WithSecretEnvFrom(secretName), utils.WithSecretEnvFrom(secretName2), utils.WithAnnotations(utils.MergeAnnotations( @@ -178,19 +173,159 @@ var _ = Describe("Exclude Annotation Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Deployment to be ready") - err = utils.WaitForDeploymentReady(ctx, kubeClient, excludeNS, deploymentName, utils.DeploymentReady) + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) Expect(err).NotTo(HaveOccurred()) By("Updating the non-excluded Secret") - err = utils.UpdateSecretFromStrings(ctx, kubeClient, excludeNS, secretName2, + err = utils.UpdateSecretFromStrings(ctx, kubeClient, testNamespace, secretName2, map[string]string{"password2": "updated2"}) Expect(err).NotTo(HaveOccurred()) By("Waiting for Deployment to be reloaded") - reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, excludeNS, deploymentName, + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(reloaded).To(BeTrue(), "Deployment should reload when non-excluded Secret changes") }) }) + + Context("SecretProviderClass exclude annotation", Label("csi"), func() { + var ( + spcName string + spcName2 string + vaultSecretPath string + vaultSecretPath2 string + ) + + BeforeEach(func() { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + spcName = utils.RandName("spc") + spcName2 = utils.RandName("spc2") + vaultSecretPath = fmt.Sprintf("secret/%s", utils.RandName("test")) + vaultSecretPath2 = fmt.Sprintf("secret/%s", utils.RandName("test2")) + }) + + AfterEach(func() { + _ = utils.DeleteSecretProviderClass(ctx, csiClient, testNamespace, spcName) + _ = utils.DeleteSecretProviderClass(ctx, csiClient, testNamespace, spcName2) + _ = utils.DeleteVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath) + _ = utils.DeleteVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath2) + }) + + It("should NOT reload when excluded SecretProviderClassPodStatus changes", func() { + By("Creating Vault secret for the excluded SPC") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, map[string]string{ + "api_key": "initial-excluded-value", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Creating SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Deployment with auto=true and secretproviderclasses.exclude annotation") + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, + utils.WithCSIVolume(spcName), + utils.WithAnnotations(utils.MergeAnnotations( + utils.BuildAutoTrueAnnotation(), + utils.BuildSecretProviderClassExcludeAnnotation(spcName), + )), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret for excluded SPC") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, map[string]string{ + "api_key": "updated-excluded-value", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync (SPCPS version change)") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying Deployment was NOT reloaded (excluded SPC)") + time.Sleep(utils.NegativeTestWait) + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ShortTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeFalse(), "Deployment should NOT reload when excluded SecretProviderClassPodStatus changes") + }) + + It("should reload when non-excluded SecretProviderClassPodStatus changes", func() { + By("Creating two Vault secrets") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, map[string]string{ + "api_key": "initial-excluded-value", + }) + Expect(err).NotTo(HaveOccurred()) + + err = utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath2, map[string]string{ + "api_key": "initial-nonexcluded-value", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Creating two SecretProviderClasses") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName2, vaultSecretPath2, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating a Deployment with auto=true and secretproviderclasses.exclude for first SPC only") + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, deploymentName, + utils.WithCSIVolume(spcName), + utils.WithCSIVolume(spcName2), + utils.WithAnnotations(utils.MergeAnnotations( + utils.BuildAutoTrueAnnotation(), + utils.BuildSecretProviderClassExcludeAnnotation(spcName), + )), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS for non-excluded SPC") + // We need to find SPCPS for the non-excluded SPC (spcName2) + spcpsName2, err := utils.FindSPCPSForSPC(ctx, csiClient, testNamespace, spcName2, 30*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version for non-excluded SPC") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName2) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret for non-excluded SPC") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath2, map[string]string{ + "api_key": "updated-nonexcluded-value", + }) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync (SPCPS version change)") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName2, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be reloaded") + reloaded, err := utils.WaitForDeploymentReloaded(ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "Deployment should reload when non-excluded SecretProviderClassPodStatus changes") + }) + }) }) diff --git a/test/e2e/annotations/pause_period_test.go b/test/e2e/annotations/pause_period_test.go index 225ce0a..7176d83 100644 --- a/test/e2e/annotations/pause_period_test.go +++ b/test/e2e/annotations/pause_period_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) @@ -58,7 +59,7 @@ var _ = Describe("Pause Period Tests", func() { By("Verifying Deployment has paused-at annotation") paused, err := utils.WaitForDeploymentPaused(ctx, kubeClient, testNamespace, deploymentName, - "utils.AnnotationDeploymentPausedAt", utils.ShortTimeout) + utils.AnnotationDeploymentPausedAt, utils.ShortTimeout) Expect(err).NotTo(HaveOccurred()) Expect(paused).To(BeTrue(), "Deployment should have paused-at annotation after reload") }) @@ -94,7 +95,7 @@ var _ = Describe("Pause Period Tests", func() { By("Verifying Deployment does NOT have paused-at annotation") time.Sleep(utils.NegativeTestWait) paused, err := utils.WaitForDeploymentPaused(ctx, kubeClient, testNamespace, deploymentName, - "utils.AnnotationDeploymentPausedAt", utils.ShortTimeout) + utils.AnnotationDeploymentPausedAt, utils.ShortTimeout) Expect(err).NotTo(HaveOccurred()) Expect(paused).To(BeFalse(), "Deployment should NOT have paused-at annotation without pause-period") }) diff --git a/test/e2e/annotations/resource_ignore_test.go b/test/e2e/annotations/resource_ignore_test.go index d6ed661..2be5670 100644 --- a/test/e2e/annotations/resource_ignore_test.go +++ b/test/e2e/annotations/resource_ignore_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/annotations/search_match_test.go b/test/e2e/annotations/search_match_test.go index 73868c8..aec1678 100644 --- a/test/e2e/annotations/search_match_test.go +++ b/test/e2e/annotations/search_match_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/argo/argo_suite_test.go b/test/e2e/argo/argo_suite_test.go index d3071ee..0dcf616 100644 --- a/test/e2e/argo/argo_suite_test.go +++ b/test/e2e/argo/argo_suite_test.go @@ -4,19 +4,20 @@ import ( "context" "testing" + rolloutsclient "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/stakater/Reloader/test/e2e/utils" - "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + + "github.com/stakater/Reloader/test/e2e/utils" ) var ( - kubeClient kubernetes.Interface - dynamicClient dynamic.Interface - testNamespace string - ctx context.Context - testEnv *utils.TestEnvironment + kubeClient kubernetes.Interface + rolloutsClient rolloutsclient.Interface + testNamespace string + ctx context.Context + testEnv *utils.TestEnvironment ) func TestArgo(t *testing.T) { @@ -28,24 +29,18 @@ var _ = BeforeSuite(func() { var err error ctx = context.Background() - // Setup test environment testEnv, err = utils.SetupTestEnvironment(ctx, "reloader-argo") Expect(err).NotTo(HaveOccurred(), "Failed to setup test environment") - // Export for use in tests kubeClient = testEnv.KubeClient - dynamicClient = testEnv.DynamicClient + rolloutsClient = testEnv.RolloutsClient testNamespace = testEnv.Namespace - // Check if Argo Rollouts is installed - // NOTE: Argo Rollouts should be pre-installed using: ./scripts/e2e-cluster-setup.sh - // This suite does NOT install Argo Rollouts to ensure consistent behavior across all test suites. - if !utils.IsArgoRolloutsInstalled(ctx, dynamicClient) { + if !utils.IsArgoRolloutsInstalled(ctx, rolloutsClient) { Skip("Argo Rollouts is not installed. Run ./scripts/e2e-cluster-setup.sh first") } GinkgoWriter.Println("Argo Rollouts is installed") - // Deploy Reloader with Argo Rollouts support err = testEnv.DeployAndWait(map[string]string{ "reloader.reloadStrategy": "annotations", "reloader.isArgoRollouts": "true", @@ -54,13 +49,10 @@ var _ = BeforeSuite(func() { }) var _ = AfterSuite(func() { - // Cleanup test environment (Reloader + namespace) if testEnv != nil { err := testEnv.Cleanup() Expect(err).NotTo(HaveOccurred(), "Failed to cleanup test environment") } - // NOTE: Argo Rollouts is NOT uninstalled here to allow other test suites (core/) - // to run Argo tests. Cleanup is handled by: ./scripts/e2e-cluster-cleanup.sh GinkgoWriter.Println("Argo Rollouts E2E Suite cleanup complete (Argo Rollouts preserved for other suites)") }) diff --git a/test/e2e/argo/rollout_test.go b/test/e2e/argo/rollout_test.go index 5542f42..32a27b8 100644 --- a/test/e2e/argo/rollout_test.go +++ b/test/e2e/argo/rollout_test.go @@ -3,6 +3,7 @@ package argo import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) @@ -22,7 +23,7 @@ var _ = Describe("Argo Rollout Strategy Tests", func() { }) AfterEach(func() { - _ = utils.DeleteArgoRollout(ctx, dynamicClient, testNamespace, rolloutName) + _ = utils.DeleteRollout(ctx, rolloutsClient, testNamespace, rolloutName) _ = utils.DeleteConfigMap(ctx, kubeClient, testNamespace, configMapName) }) @@ -36,14 +37,14 @@ var _ = Describe("Argo Rollout Strategy Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Creating an Argo Rollout with auto=true (default strategy)") - err = utils.CreateArgoRollout(ctx, dynamicClient, testNamespace, rolloutName, + _, err = utils.CreateRollout(ctx, rolloutsClient, testNamespace, rolloutName, utils.WithRolloutConfigMapEnvFrom(configMapName), utils.WithRolloutAnnotations(utils.BuildAutoTrueAnnotation()), ) Expect(err).NotTo(HaveOccurred()) By("Waiting for Rollout to be ready") - err = utils.WaitForRolloutReady(ctx, dynamicClient, testNamespace, rolloutName, utils.DeploymentReady) + err = utils.WaitForRolloutReady(ctx, rolloutsClient, testNamespace, rolloutName, utils.DeploymentReady) Expect(err).NotTo(HaveOccurred()) By("Updating the ConfigMap") @@ -52,7 +53,7 @@ var _ = Describe("Argo Rollout Strategy Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Rollout to be reloaded with annotation") - reloaded, err := utils.WaitForRolloutReloaded(ctx, dynamicClient, testNamespace, rolloutName, + reloaded, err := utils.WaitForRolloutReloaded(ctx, rolloutsClient, testNamespace, rolloutName, utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(reloaded).To(BeTrue(), "Argo Rollout should be reloaded with default rollout strategy") @@ -66,7 +67,7 @@ var _ = Describe("Argo Rollout Strategy Tests", func() { By("Creating an Argo Rollout with restart strategy annotation") // Note: auto annotation goes on pod template, rollout-strategy goes on object metadata - err = utils.CreateArgoRollout(ctx, dynamicClient, testNamespace, rolloutName, + _, err = utils.CreateRollout(ctx, rolloutsClient, testNamespace, rolloutName, utils.WithRolloutConfigMapEnvFrom(configMapName), utils.WithRolloutAnnotations(utils.BuildAutoTrueAnnotation()), utils.WithRolloutObjectAnnotations(utils.BuildRolloutRestartStrategyAnnotation()), @@ -74,7 +75,7 @@ var _ = Describe("Argo Rollout Strategy Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Rollout to be ready") - err = utils.WaitForRolloutReady(ctx, dynamicClient, testNamespace, rolloutName, utils.DeploymentReady) + err = utils.WaitForRolloutReady(ctx, rolloutsClient, testNamespace, rolloutName, utils.DeploymentReady) Expect(err).NotTo(HaveOccurred()) By("Updating the ConfigMap") @@ -83,7 +84,7 @@ var _ = Describe("Argo Rollout Strategy Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for Rollout to have restartAt field set") - restarted, err := utils.WaitForRolloutRestartAt(ctx, dynamicClient, testNamespace, rolloutName, utils.ReloadTimeout) + restarted, err := utils.WaitForRolloutRestartAt(ctx, rolloutsClient, testNamespace, rolloutName, utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(restarted).To(BeTrue(), "Argo Rollout should have restartAt field set with restart strategy") }) diff --git a/test/e2e/core/core_suite_test.go b/test/e2e/core/core_suite_test.go index 5564946..b47b964 100644 --- a/test/e2e/core/core_suite_test.go +++ b/test/e2e/core/core_suite_test.go @@ -6,15 +6,17 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/stakater/Reloader/test/e2e/utils" - "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" -) + "k8s.io/client-go/rest" + csiclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" + "github.com/stakater/Reloader/test/e2e/utils" +) var ( kubeClient kubernetes.Interface - dynamicClient dynamic.Interface + csiClient csiclient.Interface + restConfig *rest.Config testNamespace string ctx context.Context cancel context.CancelFunc @@ -31,46 +33,45 @@ var _ = BeforeSuite(func() { var err error ctx, cancel = context.WithCancel(context.Background()) - // Setup test environment testEnv, err = utils.SetupTestEnvironment(ctx, "reloader-core-test") Expect(err).NotTo(HaveOccurred(), "Failed to setup test environment") - // Export for use in tests kubeClient = testEnv.KubeClient - dynamicClient = testEnv.DynamicClient + csiClient = testEnv.CSIClient + restConfig = testEnv.RestConfig testNamespace = testEnv.Namespace - // Create adapter registry - registry = utils.NewAdapterRegistry(kubeClient, dynamicClient) + registry = utils.NewAdapterRegistry(kubeClient) - // Register ArgoRolloutAdapter if Argo Rollouts is installed - if utils.IsArgoRolloutsInstalled(ctx, dynamicClient) { + if utils.IsArgoRolloutsInstalled(ctx, testEnv.RolloutsClient) { GinkgoWriter.Println("Argo Rollouts detected, registering ArgoRolloutAdapter") - registry.RegisterAdapter(utils.NewArgoRolloutAdapter(dynamicClient)) + registry.RegisterAdapter(utils.NewArgoRolloutAdapter(testEnv.RolloutsClient)) } else { GinkgoWriter.Println("Argo Rollouts not detected, skipping ArgoRolloutAdapter registration") } - // Register DeploymentConfigAdapter if OpenShift is available - if utils.HasDeploymentConfigSupport(testEnv.DiscoveryClient) { + if utils.HasDeploymentConfigSupport(testEnv.DiscoveryClient) && testEnv.OpenShiftClient != nil { GinkgoWriter.Println("OpenShift detected, registering DeploymentConfigAdapter") - registry.RegisterAdapter(utils.NewDeploymentConfigAdapter(dynamicClient)) + registry.RegisterAdapter(utils.NewDeploymentConfigAdapter(testEnv.OpenShiftClient)) } else { GinkgoWriter.Println("OpenShift not detected, skipping DeploymentConfigAdapter registration") } - // Deploy Reloader with default annotations strategy - // Individual test contexts will redeploy with different strategies if needed deployValues := map[string]string{ "reloader.reloadStrategy": "annotations", + "reloader.watchGlobally": "false", // Only watch own namespace to prevent cross-talk between test suites } - // Enable Argo Rollouts support if Argo is installed - if utils.IsArgoRolloutsInstalled(ctx, dynamicClient) { + if utils.IsArgoRolloutsInstalled(ctx, testEnv.RolloutsClient) { deployValues["reloader.isArgoRollouts"] = "true" GinkgoWriter.Println("Deploying Reloader with Argo Rollouts support") } + if utils.IsCSIDriverInstalled(ctx, csiClient) { + deployValues["reloader.enableCSIIntegration"] = "true" + GinkgoWriter.Println("Deploying Reloader with CSI integration support") + } + err = testEnv.DeployAndWait(deployValues) Expect(err).NotTo(HaveOccurred(), "Failed to deploy Reloader") }) diff --git a/test/e2e/core/reference_methods_test.go b/test/e2e/core/reference_methods_test.go index 38f52c5..9e13776 100644 --- a/test/e2e/core/reference_methods_test.go +++ b/test/e2e/core/reference_methods_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) @@ -33,7 +34,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when ConfigMap referenced via valueFrom.configMapKeyRef changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -46,7 +49,7 @@ var _ = Describe("Reference Method Tests", func() { UseConfigMapKeyRef: true, ConfigMapKey: "config_key", EnvVarName: "MY_CONFIG_VAR", - Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -81,7 +84,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when Secret referenced via valueFrom.secretKeyRef changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a Secret") _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, @@ -90,11 +95,11 @@ var _ = Describe("Reference Method Tests", func() { By("Creating workload with valueFrom.secretKeyRef") err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ - SecretName: secretName, + SecretName: secretName, UseSecretKeyRef: true, - SecretKey: "secret_key", - EnvVarName: "MY_SECRET_VAR", - Annotations: utils.BuildSecretReloadAnnotation(secretName), + SecretKey: "secret_key", + EnvVarName: "MY_SECRET_VAR", + Annotations: utils.BuildSecretReloadAnnotation(secretName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -129,7 +134,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when ConfigMap in projected volume changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -140,7 +147,7 @@ var _ = Describe("Reference Method Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ ConfigMapName: configMapName, UseProjectedVolume: true, - Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -170,7 +177,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when Secret in projected volume changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a Secret") _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, @@ -181,7 +190,7 @@ var _ = Describe("Reference Method Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ SecretName: secretName, UseProjectedVolume: true, - Annotations: utils.BuildSecretReloadAnnotation(secretName), + Annotations: utils.BuildSecretReloadAnnotation(secretName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -211,7 +220,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when ConfigMap changes in mixed projected volume", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap and Secret") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -260,7 +271,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when Secret changes in mixed projected volume", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap and Secret") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -314,7 +327,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when ConfigMap referenced by init container changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -325,7 +340,7 @@ var _ = Describe("Reference Method Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ ConfigMapName: configMapName, UseInitContainer: true, - Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -355,7 +370,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when Secret referenced by init container changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a Secret") _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, @@ -366,7 +383,7 @@ var _ = Describe("Reference Method Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ SecretName: secretName, UseInitContainer: true, - Annotations: utils.BuildSecretReloadAnnotation(secretName), + Annotations: utils.BuildSecretReloadAnnotation(secretName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -398,7 +415,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when ConfigMap volume mounted in init container changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -409,7 +428,7 @@ var _ = Describe("Reference Method Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ ConfigMapName: configMapName, UseInitContainerVolume: true, - Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -439,7 +458,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload when Secret volume mounted in init container changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a Secret") _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, @@ -450,7 +471,7 @@ var _ = Describe("Reference Method Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ SecretName: secretName, UseInitContainerVolume: true, - Annotations: utils.BuildSecretReloadAnnotation(secretName), + Annotations: utils.BuildSecretReloadAnnotation(secretName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -485,7 +506,9 @@ var _ = Describe("Reference Method Tests", func() { DescribeTable("should reload with auto=true when ConfigMap referenced via valueFrom changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -498,7 +521,7 @@ var _ = Describe("Reference Method Tests", func() { UseConfigMapKeyRef: true, ConfigMapKey: "auto_config_key", EnvVarName: "AUTO_CONFIG_VAR", - Annotations: utils.BuildAutoTrueAnnotation(), + Annotations: utils.BuildAutoTrueAnnotation(), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) diff --git a/test/e2e/core/workloads_test.go b/test/e2e/core/workloads_test.go index 4b49177..2cf2407 100644 --- a/test/e2e/core/workloads_test.go +++ b/test/e2e/core/workloads_test.go @@ -6,25 +6,34 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) var _ = Describe("Workload Reload Tests", func() { var ( - configMapName string - secretName string - workloadName string + configMapName string + secretName string + workloadName string + spcName string + vaultSecretPath string ) BeforeEach(func() { configMapName = utils.RandName("cm") secretName = utils.RandName("secret") workloadName = utils.RandName("workload") + spcName = utils.RandName("spc") + vaultSecretPath = fmt.Sprintf("secret/%s", utils.RandName("test")) }) AfterEach(func() { _ = utils.DeleteConfigMap(ctx, kubeClient, testNamespace, configMapName) _ = utils.DeleteSecret(ctx, kubeClient, testNamespace, secretName) + if csiClient != nil { + _ = utils.DeleteSecretProviderClass(ctx, csiClient, testNamespace, spcName) + } + _ = utils.DeleteVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath) }) // ============================================================ @@ -39,94 +48,160 @@ var _ = Describe("Workload Reload Tests", func() { } // ConfigMap reload tests for standard workloads - DescribeTable("should reload when ConfigMap changes", - func(workloadType utils.WorkloadType) { - adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + DescribeTable("should reload when ConfigMap changes", func(workloadType utils.WorkloadType) { + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } - By("Creating a ConfigMap") - _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, - map[string]string{"key": "initial"}, nil) - Expect(err).NotTo(HaveOccurred()) + By("Creating a ConfigMap") + _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "initial"}, nil) + Expect(err).NotTo(HaveOccurred()) - By("Creating workload with ConfigMap reference annotation") - err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ - ConfigMapName: configMapName, - UseConfigMapEnvFrom: true, - Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), - }) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + By("Creating workload with ConfigMap reference annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + ConfigMapName: configMapName, + UseConfigMapEnvFrom: true, + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) - By("Waiting for workload to be ready") - err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) - Expect(err).NotTo(HaveOccurred()) + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) - By("Updating the ConfigMap data") - err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, - map[string]string{"key": "updated"}) - Expect(err).NotTo(HaveOccurred()) + By("Updating the ConfigMap data") + err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "updated"}) + Expect(err).NotTo(HaveOccurred()) - By("Waiting for workload to be reloaded") - reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, - utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) - Expect(err).NotTo(HaveOccurred()) - Expect(reloaded).To(BeTrue(), "%s should have been reloaded", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), + By("Waiting for workload to be reloaded") + reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, utils.AnnotationLastReloadedFrom, + utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "%s should have been reloaded", workloadType) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), Entry("StatefulSet", utils.WorkloadStatefulSet), Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) // Secret reload tests for standard workloads - DescribeTable("should reload when Secret changes", - func(workloadType utils.WorkloadType) { - adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + DescribeTable("should reload when Secret changes", func(workloadType utils.WorkloadType) { + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } - By("Creating a Secret") - _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, - map[string]string{"password": "initial"}, nil) - Expect(err).NotTo(HaveOccurred()) + By("Creating a Secret") + _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, + map[string]string{"password": "initial"}, nil) + Expect(err).NotTo(HaveOccurred()) - By("Creating workload with Secret reference annotation") - err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ - SecretName: secretName, - UseSecretEnvFrom: true, - Annotations: utils.BuildSecretReloadAnnotation(secretName), - }) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + By("Creating workload with Secret reference annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + SecretName: secretName, + UseSecretEnvFrom: true, + Annotations: utils.BuildSecretReloadAnnotation(secretName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) - By("Waiting for workload to be ready") - err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) - Expect(err).NotTo(HaveOccurred()) + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) - By("Updating the Secret data") - err = utils.UpdateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, - map[string]string{"password": "updated"}) - Expect(err).NotTo(HaveOccurred()) + By("Updating the Secret data") + err = utils.UpdateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, + map[string]string{"password": "updated"}) + Expect(err).NotTo(HaveOccurred()) - By("Waiting for workload to be reloaded") - reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, - utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) - Expect(err).NotTo(HaveOccurred()) - Expect(reloaded).To(BeTrue(), "%s should have been reloaded", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), + By("Waiting for workload to be reloaded") + reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, utils.AnnotationLastReloadedFrom, + utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "%s should have been reloaded", workloadType) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), Entry("StatefulSet", utils.WorkloadStatefulSet), Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) + + // SecretProviderClassPodStatus (CSI) reload tests with real Vault + DescribeTable("should reload when SecretProviderClassPodStatus changes", func(workloadType utils.WorkloadType) { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } + + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, vaultSecretPath, + "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating workload with CSI volume and SPC reload annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + SPCName: spcName, + UseCSIVolume: true, + Annotations: utils.BuildSecretProviderClassReloadAnnotation(spcName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, workloadName, + utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Printf("Found SPCPS: %s\n", spcpsName) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Printf("Initial SPCPS version: %s\n", initialVersion) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, + 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Println("CSI driver synced new secret version") + + By("Waiting for workload to be reloaded") + reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, utils.AnnotationLastReloadedFrom, + utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "%s should have been reloaded when Vault secret changed", workloadType) + }, Entry("Deployment", Label("csi"), utils.WorkloadDeployment), + Entry("DaemonSet", Label("csi"), utils.WorkloadDaemonSet), + Entry("StatefulSet", Label("csi"), utils.WorkloadStatefulSet)) // Auto=true annotation tests DescribeTable("should reload with auto=true annotation when ConfigMap changes", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -137,7 +212,7 @@ var _ = Describe("Workload Reload Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ ConfigMapName: configMapName, UseConfigMapEnvFrom: true, - Annotations: utils.BuildAutoTrueAnnotation(), + Annotations: utils.BuildAutoTrueAnnotation(), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -156,19 +231,18 @@ var _ = Describe("Workload Reload Tests", func() { utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(reloaded).To(BeTrue(), "%s with auto=true should have been reloaded", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), Entry("StatefulSet", utils.WorkloadStatefulSet), Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) // Negative tests: label-only changes should NOT trigger reload DescribeTable("should NOT reload when only ConfigMap labels change (no data change)", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a ConfigMap") _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, @@ -179,7 +253,7 @@ var _ = Describe("Workload Reload Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ ConfigMapName: configMapName, UseConfigMapEnvFrom: true, - Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -199,18 +273,17 @@ var _ = Describe("Workload Reload Tests", func() { utils.AnnotationLastReloadedFrom, utils.ShortTimeout) Expect(err).NotTo(HaveOccurred()) Expect(reloaded).To(BeFalse(), "%s should NOT reload when only ConfigMap labels change", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), Entry("StatefulSet", utils.WorkloadStatefulSet), Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) DescribeTable("should NOT reload when only Secret labels change (no data change)", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } By("Creating a Secret") _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, @@ -221,7 +294,7 @@ var _ = Describe("Workload Reload Tests", func() { err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ SecretName: secretName, UseSecretEnvFrom: true, - Annotations: utils.BuildSecretReloadAnnotation(secretName), + Annotations: utils.BuildSecretReloadAnnotation(secretName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -241,13 +314,68 @@ var _ = Describe("Workload Reload Tests", func() { utils.AnnotationLastReloadedFrom, utils.ShortTimeout) Expect(err).NotTo(HaveOccurred()) Expect(reloaded).To(BeFalse(), "%s should NOT reload when only Secret labels change", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), Entry("StatefulSet", utils.WorkloadStatefulSet), Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) + + // Negative test: SPCPS label-only changes should NOT trigger reload + DescribeTable("should NOT reload when only SecretProviderClassPodStatus labels change", + func(workloadType utils.WorkloadType) { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } + + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating workload with CSI volume and SPC reload annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + SPCName: spcName, + UseCSIVolume: true, + Annotations: utils.BuildSecretProviderClassReloadAnnotation(spcName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, workloadName, + utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Updating only the SPCPS labels (no objects change)") + err = utils.UpdateSecretProviderClassPodStatusLabels(ctx, csiClient, testNamespace, spcpsName, + map[string]string{"new-label": "new-value"}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying workload was NOT reloaded (negative test)") + time.Sleep(utils.NegativeTestWait) + reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, + utils.AnnotationLastReloadedFrom, utils.ShortTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeFalse(), "%s should NOT reload when only SPCPS labels change", workloadType) + }, Entry("Deployment", Label("csi"), utils.WorkloadDeployment), + Entry("DaemonSet", Label("csi"), utils.WorkloadDaemonSet), + Entry("StatefulSet", Label("csi"), utils.WorkloadStatefulSet)) // CronJob special handling - triggers a Job instead of annotation Context("CronJob (special handling)", func() { @@ -282,7 +410,8 @@ var _ = Describe("Workload Reload Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for a Job to be created by CronJob reload") - triggered, err := cronJobAdapter.WaitForTriggeredJob(ctx, testNamespace, workloadName, utils.ReloadTimeout) + triggered, err := cronJobAdapter.WaitForTriggeredJob(ctx, testNamespace, workloadName, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(triggered).To(BeTrue(), "CronJob should have triggered a Job creation") }) @@ -308,7 +437,8 @@ var _ = Describe("Workload Reload Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for a Job to be created by CronJob reload") - triggered, err := cronJobAdapter.WaitForTriggeredJob(ctx, testNamespace, workloadName, utils.ReloadTimeout) + triggered, err := cronJobAdapter.WaitForTriggeredJob(ctx, testNamespace, workloadName, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(triggered).To(BeTrue(), "CronJob should have triggered a Job creation") }) @@ -334,135 +464,131 @@ var _ = Describe("Workload Reload Tests", func() { Expect(err).NotTo(HaveOccurred()) By("Waiting for a Job to be created by CronJob reload") - triggered, err := cronJobAdapter.WaitForTriggeredJob(ctx, testNamespace, workloadName, utils.ReloadTimeout) + triggered, err := cronJobAdapter.WaitForTriggeredJob(ctx, testNamespace, workloadName, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) Expect(triggered).To(BeTrue(), "CronJob with auto=true should have triggered a Job creation") }) }) // Volume mount tests - DescribeTable("should reload when volume-mounted ConfigMap changes", - func(workloadType utils.WorkloadType) { - adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + DescribeTable("should reload when volume-mounted ConfigMap changes", func(workloadType utils.WorkloadType) { + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } - By("Creating a ConfigMap") - _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, - map[string]string{"config.yaml": "setting: initial"}, nil) - Expect(err).NotTo(HaveOccurred()) + By("Creating a ConfigMap") + _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"config.yaml": "setting: initial"}, nil) + Expect(err).NotTo(HaveOccurred()) - By("Creating workload with ConfigMap volume") - err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ - ConfigMapName: configMapName, - UseConfigMapVolume: true, - Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), - }) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + By("Creating workload with ConfigMap volume") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + ConfigMapName: configMapName, + UseConfigMapVolume: true, + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) - By("Waiting for workload to be ready") - err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) - Expect(err).NotTo(HaveOccurred()) + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) - By("Updating the ConfigMap data") - err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, - map[string]string{"config.yaml": "setting: updated"}) - Expect(err).NotTo(HaveOccurred()) + By("Updating the ConfigMap data") + err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"config.yaml": "setting: updated"}) + Expect(err).NotTo(HaveOccurred()) - By("Waiting for workload to be reloaded") - reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, - utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) - Expect(err).NotTo(HaveOccurred()) - Expect(reloaded).To(BeTrue(), "%s with volume-mounted ConfigMap should have been reloaded", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), + By("Waiting for workload to be reloaded") + reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, utils.AnnotationLastReloadedFrom, + utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "%s with volume-mounted ConfigMap should have been reloaded", workloadType) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), Entry("StatefulSet", utils.WorkloadStatefulSet), Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) - DescribeTable("should reload when volume-mounted Secret changes", - func(workloadType utils.WorkloadType) { - adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + DescribeTable("should reload when volume-mounted Secret changes", func(workloadType utils.WorkloadType) { + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } - By("Creating a Secret") - _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, - map[string]string{"credentials.yaml": "secret: initial"}, nil) - Expect(err).NotTo(HaveOccurred()) + By("Creating a Secret") + _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, + map[string]string{"credentials.yaml": "secret: initial"}, nil) + Expect(err).NotTo(HaveOccurred()) - By("Creating workload with Secret volume") - err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ - SecretName: secretName, - UseSecretVolume: true, - Annotations: utils.BuildSecretReloadAnnotation(secretName), - }) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + By("Creating workload with Secret volume") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + SecretName: secretName, + UseSecretVolume: true, + Annotations: utils.BuildSecretReloadAnnotation(secretName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) - By("Waiting for workload to be ready") - err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) - Expect(err).NotTo(HaveOccurred()) + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) - By("Updating the Secret data") - err = utils.UpdateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, - map[string]string{"credentials.yaml": "secret: updated"}) - Expect(err).NotTo(HaveOccurred()) + By("Updating the Secret data") + err = utils.UpdateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, + map[string]string{"credentials.yaml": "secret: updated"}) + Expect(err).NotTo(HaveOccurred()) - By("Waiting for workload to be reloaded") - reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, - utils.AnnotationLastReloadedFrom, utils.ReloadTimeout) - Expect(err).NotTo(HaveOccurred()) - Expect(reloaded).To(BeTrue(), "%s with volume-mounted Secret should have been reloaded", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), + By("Waiting for workload to be reloaded") + reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, utils.AnnotationLastReloadedFrom, + utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "%s with volume-mounted Secret should have been reloaded", workloadType) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), Entry("StatefulSet", utils.WorkloadStatefulSet), Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) // Test for workloads without Reloader annotation - DescribeTable("should NOT reload without Reloader annotation", - func(workloadType utils.WorkloadType) { - adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + DescribeTable("should NOT reload without Reloader annotation", func(workloadType utils.WorkloadType) { + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } - By("Creating a ConfigMap") - _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, - map[string]string{"key": "value"}, nil) - Expect(err).NotTo(HaveOccurred()) + By("Creating a ConfigMap") + _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "value"}, nil) + Expect(err).NotTo(HaveOccurred()) - By("Creating workload WITHOUT Reloader annotation") - err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ - ConfigMapName: configMapName, - UseConfigMapEnvFrom: true, - // No Reloader annotations - }) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + By("Creating workload WITHOUT Reloader annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + ConfigMapName: configMapName, + UseConfigMapEnvFrom: true, // No Reloader annotations + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { + _ = adapter.Delete(ctx, testNamespace, workloadName) + }) - By("Waiting for workload to be ready") - err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) - Expect(err).NotTo(HaveOccurred()) + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) - By("Updating the ConfigMap data") - err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, - map[string]string{"key": "updated"}) - Expect(err).NotTo(HaveOccurred()) + By("Updating the ConfigMap data") + err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "updated"}) + Expect(err).NotTo(HaveOccurred()) - By("Verifying workload is NOT reloaded (negative test)") - time.Sleep(utils.NegativeTestWait) - reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, - utils.AnnotationLastReloadedFrom, utils.ShortTimeout) - Expect(err).NotTo(HaveOccurred()) - Expect(reloaded).To(BeFalse(), "%s without Reloader annotation should NOT be reloaded", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), - Entry("StatefulSet", utils.WorkloadStatefulSet), - ) + By("Verifying workload is NOT reloaded (negative test)") + time.Sleep(utils.NegativeTestWait) + reloaded, err := adapter.WaitReloaded(ctx, testNamespace, workloadName, utils.AnnotationLastReloadedFrom, + utils.ShortTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeFalse(), "%s without Reloader annotation should NOT be reloaded", workloadType) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), + Entry("StatefulSet", utils.WorkloadStatefulSet)) // Variable to track for use in lint _ = standardWorkloads @@ -603,10 +729,8 @@ var _ = Describe("Workload Reload Tests", func() { return "" } return deploy.Spec.Template.Annotations[utils.AnnotationLastReloadedFrom] - }, utils.ReloadTimeout, utils.DefaultInterval).ShouldNot( - Equal(firstReloadValue), - "Reload annotation should change after second update", - ) + }, utils.ReloadTimeout, utils.DefaultInterval).ShouldNot(Equal(firstReloadValue), + "Reload annotation should change after second update") }) It("should reload deployment when either ConfigMap or Secret changes", func() { @@ -628,10 +752,8 @@ var _ = Describe("Workload Reload Tests", func() { SecretName: secretName, UseConfigMapEnvFrom: true, UseSecretEnvFrom: true, - Annotations: utils.MergeAnnotations( - utils.BuildConfigMapReloadAnnotation(configMapName), - utils.BuildSecretReloadAnnotation(secretName), - ), + Annotations: utils.MergeAnnotations(utils.BuildConfigMapReloadAnnotation(configMapName), + utils.BuildSecretReloadAnnotation(secretName)), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -692,7 +814,7 @@ var _ = Describe("Workload Reload Tests", func() { // ============================================================ // ENVVARS STRATEGY TESTS // ============================================================ - Context("EnvVars Strategy", Label("envvars"), Ordered, func() { + Context("EnvVars Strategy", Label("envvars"), Ordered, ContinueOnFailure, func() { // Redeploy Reloader with envvars strategy for this context BeforeAll(func() { By("Redeploying Reloader with envvars strategy") @@ -700,9 +822,13 @@ var _ = Describe("Workload Reload Tests", func() { "reloader.reloadStrategy": "env-vars", } // Preserve Argo support if available - if utils.IsArgoRolloutsInstalled(ctx, dynamicClient) { + if utils.IsArgoRolloutsInstalled(ctx, testEnv.RolloutsClient) { deployValues["reloader.isArgoRollouts"] = "true" } + // Enable CSI integration if CSI driver is installed + if utils.IsCSIDriverInstalled(ctx, csiClient) { + deployValues["reloader.enableCSIIntegration"] = "true" + } err := testEnv.DeployAndWait(deployValues) Expect(err).NotTo(HaveOccurred(), "Failed to redeploy Reloader with envvars strategy") }) @@ -713,39 +839,137 @@ var _ = Describe("Workload Reload Tests", func() { "reloader.reloadStrategy": "annotations", } // Preserve Argo support if available - if utils.IsArgoRolloutsInstalled(ctx, dynamicClient) { + if utils.IsArgoRolloutsInstalled(ctx, testEnv.RolloutsClient) { deployValues["reloader.isArgoRollouts"] = "true" } + // Preserve CSI integration if CSI driver is installed + if utils.IsCSIDriverInstalled(ctx, csiClient) { + deployValues["reloader.enableCSIIntegration"] = "true" + } err := testEnv.DeployAndWait(deployValues) Expect(err).NotTo(HaveOccurred(), "Failed to restore Reloader to annotations strategy") }) - // EnvVar workloads (CronJob does NOT support env var strategy) - envVarWorkloads := []utils.WorkloadType{ - utils.WorkloadDeployment, - utils.WorkloadDaemonSet, - utils.WorkloadStatefulSet, - } + DescribeTable("should add STAKATER_ env var when ConfigMap changes", func(workloadType utils.WorkloadType) { + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } - DescribeTable("should add STAKATER_ env var when ConfigMap changes", + if !adapter.SupportsEnvVarStrategy() { + Skip("Workload type does not support env var strategy") + } + + By("Creating a ConfigMap") + _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "initial"}, nil) + Expect(err).NotTo(HaveOccurred()) + + By("Creating workload with ConfigMap reference annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + ConfigMapName: configMapName, + UseConfigMapEnvFrom: true, + Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the ConfigMap data") + err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "updated"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for workload to have STAKATER_ env var") + found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, utils.StakaterEnvVarPrefix, + utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue(), "%s should have STAKATER_ env var after ConfigMap change", workloadType) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), + Entry("StatefulSet", utils.WorkloadStatefulSet), + Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) + + DescribeTable("should add STAKATER_ env var when Secret changes", func(workloadType utils.WorkloadType) { + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } + + if !adapter.SupportsEnvVarStrategy() { + Skip("Workload type does not support env var strategy") + } + + By("Creating a Secret") + _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, + map[string]string{"password": "initial"}, nil) + Expect(err).NotTo(HaveOccurred()) + + By("Creating workload with Secret reference annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + SecretName: secretName, + UseSecretEnvFrom: true, + Annotations: utils.BuildSecretReloadAnnotation(secretName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Secret data") + err = utils.UpdateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, + map[string]string{"password": "updated"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for workload to have STAKATER_ env var") + found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, utils.StakaterEnvVarPrefix, + utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue(), "%s should have STAKATER_ env var after Secret change", workloadType) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), + Entry("StatefulSet", utils.WorkloadStatefulSet), + Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), + Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig)) + + // CSI SecretProviderClassPodStatus env var tests with real Vault + DescribeTable("should add STAKATER_ env var when SecretProviderClassPodStatus changes", func(workloadType utils.WorkloadType) { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } if !adapter.SupportsEnvVarStrategy() { Skip("Workload type does not support env var strategy") } - By("Creating a ConfigMap") - _, err := utils.CreateConfigMap(ctx, kubeClient, testNamespace, configMapName, - map[string]string{"key": "initial"}, nil) + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}) Expect(err).NotTo(HaveOccurred()) - By("Creating workload with ConfigMap reference annotation") + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating workload with CSI volume and SPC reload annotation") err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ - ConfigMapName: configMapName, - UseConfigMapEnvFrom: true, - Annotations: utils.BuildConfigMapReloadAnnotation(configMapName), + SPCName: spcName, + UseCSIVolume: true, + Annotations: utils.BuildSecretProviderClassReloadAnnotation(spcName), }) Expect(err).NotTo(HaveOccurred()) DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) @@ -754,74 +978,41 @@ var _ = Describe("Workload Reload Tests", func() { err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) Expect(err).NotTo(HaveOccurred()) - By("Updating the ConfigMap data") - err = utils.UpdateConfigMap(ctx, kubeClient, testNamespace, configMapName, - map[string]string{"key": "updated"}) + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, workloadName, + utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, + 10*time.Second) Expect(err).NotTo(HaveOccurred()) By("Waiting for workload to have STAKATER_ env var") - found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, - utils.StakaterEnvVarPrefix, utils.ReloadTimeout) + found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, utils.StakaterEnvVarPrefix, + utils.ReloadTimeout) Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue(), "%s should have STAKATER_ env var after ConfigMap change", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), - Entry("StatefulSet", utils.WorkloadStatefulSet), - Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) - - DescribeTable("should add STAKATER_ env var when Secret changes", - func(workloadType utils.WorkloadType) { - adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } - - if !adapter.SupportsEnvVarStrategy() { - Skip("Workload type does not support env var strategy") - } - - By("Creating a Secret") - _, err := utils.CreateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, - map[string]string{"password": "initial"}, nil) - Expect(err).NotTo(HaveOccurred()) - - By("Creating workload with Secret reference annotation") - err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ - SecretName: secretName, - UseSecretEnvFrom: true, - Annotations: utils.BuildSecretReloadAnnotation(secretName), - }) - Expect(err).NotTo(HaveOccurred()) - DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) - - By("Waiting for workload to be ready") - err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) - Expect(err).NotTo(HaveOccurred()) - - By("Updating the Secret data") - err = utils.UpdateSecretFromStrings(ctx, kubeClient, testNamespace, secretName, - map[string]string{"password": "updated"}) - Expect(err).NotTo(HaveOccurred()) - - By("Waiting for workload to have STAKATER_ env var") - found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, - utils.StakaterEnvVarPrefix, utils.ReloadTimeout) - Expect(err).NotTo(HaveOccurred()) - Expect(found).To(BeTrue(), "%s should have STAKATER_ env var after Secret change", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), - Entry("StatefulSet", utils.WorkloadStatefulSet), - Entry("ArgoRollout", Label("argo"), utils.WorkloadArgoRollout), - Entry("DeploymentConfig", Label("openshift"), utils.WorkloadDeploymentConfig), - ) + Expect(found).To(BeTrue(), "%s should have STAKATER_ env var after Vault secret change", workloadType) + }, Entry("Deployment", Label("csi"), utils.WorkloadDeployment), + Entry("DaemonSet", Label("csi"), utils.WorkloadDaemonSet), + Entry("StatefulSet", Label("csi"), utils.WorkloadStatefulSet)) // Negative tests for env var strategy DescribeTable("should NOT add STAKATER_ env var when only ConfigMap labels change", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } if !adapter.SupportsEnvVarStrategy() { Skip("Workload type does not support env var strategy") @@ -852,20 +1043,19 @@ var _ = Describe("Workload Reload Tests", func() { By("Verifying workload does NOT have STAKATER_ env var") time.Sleep(utils.NegativeTestWait) - found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, - utils.StakaterEnvVarPrefix, utils.ShortTimeout) + found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, utils.StakaterEnvVarPrefix, + utils.ShortTimeout) Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeFalse(), "%s should NOT have STAKATER_ env var for label-only change", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), - Entry("StatefulSet", utils.WorkloadStatefulSet), - ) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), + Entry("StatefulSet", utils.WorkloadStatefulSet)) DescribeTable("should NOT add STAKATER_ env var when only Secret labels change", func(workloadType utils.WorkloadType) { adapter := registry.Get(workloadType) - if adapter == nil { Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) } + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } if !adapter.SupportsEnvVarStrategy() { Skip("Workload type does not support env var strategy") @@ -896,17 +1086,255 @@ var _ = Describe("Workload Reload Tests", func() { By("Verifying workload does NOT have STAKATER_ env var") time.Sleep(utils.NegativeTestWait) - found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, - utils.StakaterEnvVarPrefix, utils.ShortTimeout) + found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, utils.StakaterEnvVarPrefix, + utils.ShortTimeout) Expect(err).NotTo(HaveOccurred()) Expect(found).To(BeFalse(), "%s should NOT have STAKATER_ env var for label-only change", workloadType) - }, - Entry("Deployment", utils.WorkloadDeployment), - Entry("DaemonSet", utils.WorkloadDaemonSet), - Entry("StatefulSet", utils.WorkloadStatefulSet), - ) + }, Entry("Deployment", utils.WorkloadDeployment), Entry("DaemonSet", utils.WorkloadDaemonSet), + Entry("StatefulSet", utils.WorkloadStatefulSet)) - // Variable to track for use in lint - _ = envVarWorkloads + // CSI SPCPS label-only change negative test with real Vault + DescribeTable("should NOT add STAKATER_ env var when only SecretProviderClassPodStatus labels change", + func(workloadType utils.WorkloadType) { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + + adapter := registry.Get(workloadType) + if adapter == nil { + Skip(fmt.Sprintf("%s adapter not available (CRD not installed)", workloadType)) + } + + if !adapter.SupportsEnvVarStrategy() { + Skip("Workload type does not support env var strategy") + } + + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating workload with CSI volume and SPC reload annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + SPCName: spcName, + UseCSIVolume: true, + Annotations: utils.BuildSecretProviderClassReloadAnnotation(spcName), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + + By("Waiting for workload to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, workloadName, + utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Updating only the SPCPS labels (should NOT trigger reload)") + err = utils.UpdateSecretProviderClassPodStatusLabels(ctx, csiClient, testNamespace, spcpsName, + map[string]string{"new-label": "new-value"}) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying workload does NOT have STAKATER_ env var") + time.Sleep(utils.NegativeTestWait) + found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, utils.StakaterEnvVarPrefix, + utils.ShortTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeFalse(), "%s should NOT have STAKATER_ env var for SPCPS label-only change", + workloadType) + }, Entry("Deployment", Label("csi"), utils.WorkloadDeployment), + Entry("DaemonSet", Label("csi"), utils.WorkloadDaemonSet), + Entry("StatefulSet", Label("csi"), utils.WorkloadStatefulSet)) + + // CSI auto annotation with EnvVar strategy and real Vault + It("should add STAKATER_ env var with secretproviderclass auto annotation", Label("csi"), func() { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + + adapter := registry.Get(utils.WorkloadDeployment) + Expect(adapter).NotTo(BeNil()) + + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, vaultSecretPath, + "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating Deployment with CSI volume and SPC auto annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + SPCName: spcName, + UseCSIVolume: true, + Annotations: utils.BuildSecretProviderClassAutoAnnotation(), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + + By("Waiting for Deployment to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, workloadName, + utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, + 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to have STAKATER_ env var") + found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, utils.StakaterEnvVarPrefix, + utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue(), "Deployment with SPC auto annotation should have STAKATER_ env var") + }) + + // CSI exclude annotation with EnvVar strategy and real Vault + It("should NOT add STAKATER_ env var when excluded SecretProviderClassPodStatus changes", Label("csi"), func() { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + + adapter := registry.Get(utils.WorkloadDeployment) + Expect(adapter).NotTo(BeNil()) + + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, vaultSecretPath, + "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating Deployment with auto=true and SPC exclude annotation") + err = adapter.Create(ctx, testNamespace, workloadName, utils.WorkloadConfig{ + SPCName: spcName, + UseCSIVolume: true, + Annotations: utils.MergeAnnotations(utils.BuildAutoTrueAnnotation(), + utils.BuildSecretProviderClassExcludeAnnotation(spcName)), + }) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = adapter.Delete(ctx, testNamespace, workloadName) }) + + By("Waiting for Deployment to be ready") + err = adapter.WaitReady(ctx, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, workloadName, + utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret (excluded SPC - should NOT trigger reload)") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, + 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying Deployment does NOT have STAKATER_ env var") + time.Sleep(utils.NegativeTestWait) + found, err := adapter.WaitEnvVar(ctx, testNamespace, workloadName, utils.StakaterEnvVarPrefix, + utils.ShortTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeFalse(), "Deployment should NOT have STAKATER_ env var for excluded SPCPS change") + }) + + // CSI init container with EnvVar strategy and real Vault + It("should add STAKATER_ env var when SecretProviderClassPodStatus used by init container changes", + Label("csi"), func() { + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed") + } + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed") + } + + By("Creating a secret in Vault") + err := utils.CreateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret(ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "api_key") + Expect(err).NotTo(HaveOccurred()) + + By("Creating Deployment with init container using CSI volume") + _, err = utils.CreateDeployment(ctx, kubeClient, testNamespace, workloadName, + utils.WithInitContainerCSIVolume(spcName), + utils.WithAnnotations(utils.BuildSecretProviderClassReloadAnnotation(spcName))) + Expect(err).NotTo(HaveOccurred()) + DeferCleanup(func() { _ = utils.DeleteDeployment(ctx, kubeClient, testNamespace, workloadName) }) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, workloadName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment(ctx, csiClient, kubeClient, testNamespace, workloadName, + utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, + 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to have STAKATER_ env var") + found, err := utils.WaitForDeploymentEnvVar(ctx, kubeClient, testNamespace, workloadName, + utils.StakaterEnvVarPrefix, utils.ReloadTimeout) + Expect(err).NotTo(HaveOccurred()) + Expect(found).To(BeTrue(), "Deployment with init container CSI should have STAKATER_ env var") + }) }) }) diff --git a/test/e2e/csi/csi_suite_test.go b/test/e2e/csi/csi_suite_test.go new file mode 100644 index 0000000..7d47a65 --- /dev/null +++ b/test/e2e/csi/csi_suite_test.go @@ -0,0 +1,75 @@ +package csi + +import ( + "context" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + csiclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" + + "github.com/stakater/Reloader/test/e2e/utils" +) + +var ( + kubeClient kubernetes.Interface + csiClient csiclient.Interface + restConfig *rest.Config + testNamespace string + ctx context.Context + cancel context.CancelFunc + testEnv *utils.TestEnvironment +) + +func TestCSI(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CSI SecretProviderClass E2E Suite") +} + +var _ = BeforeSuite(func() { + var err error + ctx, cancel = context.WithCancel(context.Background()) + + // Setup test environment + testEnv, err = utils.SetupTestEnvironment(ctx, "reloader-csi-test") + Expect(err).NotTo(HaveOccurred(), "Failed to setup test environment") + + // Export for use in tests + kubeClient = testEnv.KubeClient + csiClient = testEnv.CSIClient + restConfig = testEnv.RestConfig + testNamespace = testEnv.Namespace + + // Skip entire suite if CSI driver not installed + if !utils.IsCSIDriverInstalled(ctx, csiClient) { + Skip("CSI secrets store driver not installed - skipping CSI suite") + } + + // Skip entire suite if Vault CSI provider not installed + if !utils.IsVaultProviderInstalled(ctx, kubeClient) { + Skip("Vault CSI provider not installed - skipping CSI suite") + } + + // Deploy Reloader with annotations strategy and CSI integration enabled + err = testEnv.DeployAndWait(map[string]string{ + "reloader.reloadStrategy": "annotations", + "reloader.watchGlobally": "false", // Only watch own namespace to prevent cross-talk between test suites + "reloader.enableCSIIntegration": "true", + }) + Expect(err).NotTo(HaveOccurred(), "Failed to deploy Reloader") +}) + +var _ = AfterSuite(func() { + if testEnv != nil { + err := testEnv.Cleanup() + Expect(err).NotTo(HaveOccurred(), "Failed to cleanup test environment") + } + + if cancel != nil { + cancel() + } + + GinkgoWriter.Println("CSI E2E Suite cleanup complete") +}) diff --git a/test/e2e/csi/csi_test.go b/test/e2e/csi/csi_test.go new file mode 100644 index 0000000..71e98d2 --- /dev/null +++ b/test/e2e/csi/csi_test.go @@ -0,0 +1,390 @@ +package csi + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/stakater/Reloader/test/e2e/utils" +) + +var _ = Describe( + "CSI SecretProviderClass Tests", func() { + var ( + deploymentName string + configMapName string + spcName string + vaultSecretPath string + ) + + BeforeEach( + func() { + deploymentName = utils.RandName("deploy") + configMapName = utils.RandName("cm") + spcName = utils.RandName("spc") + // Each test gets its own Vault secret path to avoid conflicts + vaultSecretPath = fmt.Sprintf("secret/%s", utils.RandName("test")) + }, + ) + + AfterEach( + func() { + _ = utils.DeleteDeployment(ctx, kubeClient, testNamespace, deploymentName) + _ = utils.DeleteConfigMap(ctx, kubeClient, testNamespace, configMapName) + _ = utils.DeleteSecretProviderClass(ctx, csiClient, testNamespace, spcName) + // Clean up Vault secret + _ = utils.DeleteVaultSecret(ctx, kubeClient, restConfig, vaultSecretPath) + }, + ) + + Context( + "Real Vault Integration Tests", func() { + It( + "should reload when Vault secret changes", func() { + By("Creating a secret in Vault") + err := utils.CreateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "initial-value-v1"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret( + ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "api_key", + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating Deployment with CSI volume and SPC reload annotation") + _, err = utils.CreateDeployment( + ctx, kubeClient, testNamespace, deploymentName, + utils.WithCSIVolume(spcName), + utils.WithAnnotations(utils.BuildSecretProviderClassReloadAnnotation(spcName)), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS created by CSI driver") + spcpsName, err := utils.FindSPCPSForDeployment( + ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady, + ) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Printf("Found SPCPS: %s\n", spcpsName) + + By("Getting initial SPCPS version") + initialVersion, err := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Printf("Initial SPCPS version: %s\n", initialVersion) + + By("Updating the Vault secret") + err = utils.UpdateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"api_key": "updated-value-v2"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync the new secret version") + // CSI rotation poll interval is 10s, wait up to 30s for sync + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Println("CSI driver synced new secret version") + + By("Waiting for Deployment to be reloaded by Reloader") + reloaded, err := utils.WaitForDeploymentReloaded( + ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "Deployment should have been reloaded after Vault secret change") + }, + ) + + It( + "should handle multiple Vault secret updates", func() { + By("Creating a secret in Vault") + err := utils.CreateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"password": "pass-v1"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret( + ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "password", + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating Deployment with CSI volume") + _, err = utils.CreateDeployment( + ctx, kubeClient, testNamespace, deploymentName, + utils.WithCSIVolume(spcName), + utils.WithAnnotations(utils.BuildSecretProviderClassReloadAnnotation(spcName)), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the SPCPS") + spcpsName, err := utils.FindSPCPSForDeployment( + ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady, + ) + Expect(err).NotTo(HaveOccurred()) + + By("First update to Vault secret") + initialVersion, _ := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + err = utils.UpdateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"password": "pass-v2"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for first CSI sync") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for first reload") + reloaded, err := utils.WaitForDeploymentReloaded( + ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue()) + + By("Getting annotation value after first reload") + deploy, err := utils.GetDeployment(ctx, kubeClient, testNamespace, deploymentName) + Expect(err).NotTo(HaveOccurred()) + firstReloadValue := deploy.Spec.Template.Annotations[utils.AnnotationLastReloadedFrom] + Expect(firstReloadValue).NotTo(BeEmpty()) + + By("Waiting for Deployment to stabilize") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Finding the NEW SPCPS after first reload (new pod = new SPCPS)") + newSpcpsName, err := utils.FindSPCPSForDeployment( + ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady, + ) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Printf("New SPCPS after first reload: %s\n", newSpcpsName) + + By("Second update to Vault secret") + err = utils.UpdateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"password": "pass-v3"}, + ) + Expect(err).NotTo(HaveOccurred()) + + // Note: We do not wait for SPCPS version change here because: + // 1. CSI driver syncs the new secret version to SPCPS + // 2. Reloader sees SPCPS change and immediately reloads deployment + // 3. Deployment reload creates new pod -> new SPCPS (old one deleted) + // So by the time we check, the original SPCPS no longer exists. + // Instead, we directly verify the deployment annotation changed. + + By("Waiting for second reload with different annotation value") + Eventually( + func() string { + deploy, err := utils.GetDeployment(ctx, kubeClient, testNamespace, deploymentName) + if err != nil { + return "" + } + return deploy.Spec.Template.Annotations[utils.AnnotationLastReloadedFrom] + }, utils.ReloadTimeout, + ).ShouldNot(Equal(firstReloadValue), "Annotation should change after second Vault secret update") + }, + ) + }, + ) + + Context( + "Typed Auto Annotation Tests", func() { + It( + "should reload only SPC changes with secretproviderclass auto annotation, not ConfigMap", func() { + By("Creating a ConfigMap") + _, err := utils.CreateConfigMap( + ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "initial"}, nil, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a secret in Vault") + err = utils.CreateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"token": "token-v1"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret( + ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "token", + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating Deployment with ConfigMap envFrom AND CSI volume, but only SPC auto annotation") + _, err = utils.CreateDeployment( + ctx, kubeClient, testNamespace, deploymentName, + utils.WithConfigMapEnvFrom(configMapName), + utils.WithCSIVolume(spcName), + utils.WithAnnotations(utils.BuildSecretProviderClassAutoAnnotation()), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the ConfigMap (should NOT trigger reload)") + err = utils.UpdateConfigMap( + ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "updated"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying Deployment was NOT reloaded for ConfigMap change") + time.Sleep(utils.NegativeTestWait) + reloaded, err := utils.WaitForDeploymentReloaded( + ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ShortTimeout, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeFalse(), "SPC auto annotation should not trigger reload for ConfigMap changes") + + By("Finding the SPCPS") + spcpsName, err := utils.FindSPCPSForDeployment( + ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Getting SPCPS version before Vault update") + initialVersion, _ := utils.GetSPCPSVersion(ctx, csiClient, testNamespace, spcpsName) + + By("Updating the Vault secret (should trigger reload)") + err = utils.UpdateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"token": "token-v2"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for CSI driver to sync") + err = utils.WaitForSPCPSVersionChange(ctx, csiClient, testNamespace, spcpsName, initialVersion, 10*time.Second) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying Deployment WAS reloaded for Vault secret change") + reloaded, err = utils.WaitForDeploymentReloaded( + ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "SPC auto annotation should trigger reload for Vault secret changes") + }, + ) + + It( + "should reload for both ConfigMap and SPC when using combined auto=true", func() { + By("Creating a ConfigMap") + _, err := utils.CreateConfigMap( + ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "initial"}, nil, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a secret in Vault") + err = utils.CreateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"secret": "secret-v1"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating a SecretProviderClass pointing to Vault secret") + _, err = utils.CreateSecretProviderClassWithSecret( + ctx, csiClient, testNamespace, spcName, + vaultSecretPath, "secret", + ) + Expect(err).NotTo(HaveOccurred()) + + By("Creating Deployment with ConfigMap envFrom AND CSI volume with combined auto=true") + _, err = utils.CreateDeployment( + ctx, kubeClient, testNamespace, deploymentName, + utils.WithConfigMapEnvFrom(configMapName), + utils.WithCSIVolume(spcName), + utils.WithAnnotations(utils.BuildAutoTrueAnnotation()), + ) + Expect(err).NotTo(HaveOccurred()) + + By("Waiting for Deployment to be ready") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Updating the ConfigMap (should trigger reload with auto=true)") + err = utils.UpdateConfigMap( + ctx, kubeClient, testNamespace, configMapName, + map[string]string{"key": "updated"}, + ) + Expect(err).NotTo(HaveOccurred()) + + By("Verifying Deployment WAS reloaded for ConfigMap change") + reloaded, err := utils.WaitForDeploymentReloaded( + ctx, kubeClient, testNamespace, deploymentName, + utils.AnnotationLastReloadedFrom, utils.ReloadTimeout, + ) + Expect(err).NotTo(HaveOccurred()) + Expect(reloaded).To(BeTrue(), "Combined auto=true should trigger reload for ConfigMap changes") + + By("Waiting for Deployment to stabilize") + err = utils.WaitForDeploymentReady(ctx, kubeClient, testNamespace, deploymentName, utils.DeploymentReady) + Expect(err).NotTo(HaveOccurred()) + + By("Getting current annotation value") + deploy, err := utils.GetDeployment(ctx, kubeClient, testNamespace, deploymentName) + Expect(err).NotTo(HaveOccurred()) + firstReloadValue := deploy.Spec.Template.Annotations[utils.AnnotationLastReloadedFrom] + + By("Finding the NEW SPCPS after ConfigMap reload (new pod = new SPCPS)") + newSpcpsName, err := utils.FindSPCPSForDeployment( + ctx, csiClient, kubeClient, testNamespace, deploymentName, utils.DeploymentReady, + ) + Expect(err).NotTo(HaveOccurred()) + GinkgoWriter.Printf("New SPCPS after ConfigMap reload: %s\n", newSpcpsName) + + By("Updating the Vault secret (should also trigger reload with auto=true)") + err = utils.UpdateVaultSecret( + ctx, kubeClient, restConfig, vaultSecretPath, + map[string]string{"secret": "secret-v2"}, + ) + Expect(err).NotTo(HaveOccurred()) + + // Note: We don't wait for SPCPS version change here because: + // 1. CSI driver syncs the new secret version to SPCPS + // 2. Reloader sees SPCPS change and immediately reloads deployment + // 3. Deployment reload creates new pod → new SPCPS (old one deleted) + // So by the time we check, the original SPCPS no longer exists. + // Instead, we directly verify the deployment annotation changed. + + By("Verifying Deployment WAS reloaded for Vault secret change") + Eventually( + func() string { + deploy, err := utils.GetDeployment(ctx, kubeClient, testNamespace, deploymentName) + if err != nil { + return "" + } + return deploy.Spec.Template.Annotations[utils.AnnotationLastReloadedFrom] + }, utils.ReloadTimeout, + ).ShouldNot( + Equal(firstReloadValue), + "Combined auto=true should trigger reload for Vault secret changes", + ) + }, + ) + }, + ) + }, +) diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index b45374a..f5cbdbb 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -9,10 +9,11 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/stakater/Reloader/test/e2e/utils" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" + + "github.com/stakater/Reloader/test/e2e/utils" ) var ( diff --git a/test/e2e/flags/auto_reload_all_test.go b/test/e2e/flags/auto_reload_all_test.go index 54f30d4..fb638a8 100644 --- a/test/e2e/flags/auto_reload_all_test.go +++ b/test/e2e/flags/auto_reload_all_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/flags/flags_suite_test.go b/test/e2e/flags/flags_suite_test.go index f70adaf..386f8b3 100644 --- a/test/e2e/flags/flags_suite_test.go +++ b/test/e2e/flags/flags_suite_test.go @@ -6,8 +6,9 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/stakater/Reloader/test/e2e/utils" "k8s.io/client-go/kubernetes" + + "github.com/stakater/Reloader/test/e2e/utils" ) var ( diff --git a/test/e2e/flags/ignore_resources_test.go b/test/e2e/flags/ignore_resources_test.go index 5c17d82..70033f9 100644 --- a/test/e2e/flags/ignore_resources_test.go +++ b/test/e2e/flags/ignore_resources_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/flags/ignored_workloads_test.go b/test/e2e/flags/ignored_workloads_test.go index c2910c3..22f7386 100644 --- a/test/e2e/flags/ignored_workloads_test.go +++ b/test/e2e/flags/ignored_workloads_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/flags/namespace_ignore_test.go b/test/e2e/flags/namespace_ignore_test.go index 31767f9..7648030 100644 --- a/test/e2e/flags/namespace_ignore_test.go +++ b/test/e2e/flags/namespace_ignore_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/flags/namespace_selector_test.go b/test/e2e/flags/namespace_selector_test.go index 82781f3..ca7d3d2 100644 --- a/test/e2e/flags/namespace_selector_test.go +++ b/test/e2e/flags/namespace_selector_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/flags/reload_on_create_test.go b/test/e2e/flags/reload_on_create_test.go index c27a727..74f2151 100644 --- a/test/e2e/flags/reload_on_create_test.go +++ b/test/e2e/flags/reload_on_create_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/flags/reload_on_delete_test.go b/test/e2e/flags/reload_on_delete_test.go index 3e822b0..2ddce7d 100644 --- a/test/e2e/flags/reload_on_delete_test.go +++ b/test/e2e/flags/reload_on_delete_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/flags/resource_selector_test.go b/test/e2e/flags/resource_selector_test.go index 6282c40..24a6dff 100644 --- a/test/e2e/flags/resource_selector_test.go +++ b/test/e2e/flags/resource_selector_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/flags/watch_globally_test.go b/test/e2e/flags/watch_globally_test.go index c8cbf94..5ef1721 100644 --- a/test/e2e/flags/watch_globally_test.go +++ b/test/e2e/flags/watch_globally_test.go @@ -5,6 +5,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/stakater/Reloader/test/e2e/utils" ) diff --git a/test/e2e/utils/annotations.go b/test/e2e/utils/annotations.go index 1be0415..60c0132 100644 --- a/test/e2e/utils/annotations.go +++ b/test/e2e/utils/annotations.go @@ -20,6 +20,11 @@ const ( // Value: comma-separated list of Secret names, e.g., "secret1,secret2" AnnotationSecretReload = "secret.reloader.stakater.com/reload" + // AnnotationSecretProviderClassReload triggers reload when specified SecretProviderClass(es) change. + // Value: comma-separated list of SecretProviderClass names, e.g., "spc1,spc2" + // Note: Reloader actually watches SecretProviderClassPodStatus resources, not SecretProviderClass. + AnnotationSecretProviderClassReload = "secretproviderclass.reloader.stakater.com/reload" + // ============================================================ // Auto-reload annotations // ============================================================ @@ -36,6 +41,10 @@ const ( // Value: "true" or "false" AnnotationSecretAuto = "secret.reloader.stakater.com/auto" + // AnnotationSecretProviderClassAuto enables auto-reload for all referenced SecretProviderClasses only. + // Value: "true" or "false" + AnnotationSecretProviderClassAuto = "secretproviderclass.reloader.stakater.com/auto" + // ============================================================ // Exclude annotations (used with auto=true to exclude specific resources) // ============================================================ @@ -48,6 +57,10 @@ const ( // Value: comma-separated list of Secret names AnnotationSecretExclude = "secrets.exclude.reloader.stakater.com/reload" + // AnnotationSecretProviderClassExclude excludes specified SecretProviderClasses from auto-reload. + // Value: comma-separated list of SecretProviderClass names + AnnotationSecretProviderClassExclude = "secretproviderclasses.exclude.reloader.stakater.com/reload" + // ============================================================ // Search annotations (for regex matching) // ============================================================ @@ -117,6 +130,13 @@ func BuildSecretReloadAnnotation(secretNames ...string) map[string]string { } } +// BuildSecretProviderClassReloadAnnotation creates an annotation map for SecretProviderClass reload. +func BuildSecretProviderClassReloadAnnotation(spcNames ...string) map[string]string { + return map[string]string{ + AnnotationSecretProviderClassReload: joinNames(spcNames), + } +} + // BuildAutoTrueAnnotation creates an annotation map with auto=true. func BuildAutoTrueAnnotation() map[string]string { return map[string]string{ @@ -145,6 +165,13 @@ func BuildSecretAutoAnnotation() map[string]string { } } +// BuildSecretProviderClassAutoAnnotation creates an annotation map with secretproviderclass auto=true. +func BuildSecretProviderClassAutoAnnotation() map[string]string { + return map[string]string{ + AnnotationSecretProviderClassAuto: AnnotationValueTrue, + } +} + // BuildSearchAnnotation creates an annotation map to enable search mode. func BuildSearchAnnotation() map[string]string { return map[string]string{ @@ -187,6 +214,13 @@ func BuildSecretExcludeAnnotation(secretNames ...string) map[string]string { } } +// BuildSecretProviderClassExcludeAnnotation creates an annotation to exclude SecretProviderClasses from auto-reload. +func BuildSecretProviderClassExcludeAnnotation(spcNames ...string) map[string]string { + return map[string]string{ + AnnotationSecretProviderClassExclude: joinNames(spcNames), + } +} + // BuildPausePeriodAnnotation creates an annotation for deployment pause period. func BuildPausePeriodAnnotation(duration string) map[string]string { return map[string]string{ diff --git a/test/e2e/utils/argo.go b/test/e2e/utils/argo.go index 6df5cf3..b06da6c 100644 --- a/test/e2e/utils/argo.go +++ b/test/e2e/utils/argo.go @@ -2,307 +2,119 @@ package utils import ( "context" - "time" + rolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + rolloutsclient "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" ) -// ArgoRolloutGVR returns the GroupVersionResource for Argo Rollouts. -var ArgoRolloutGVR = schema.GroupVersionResource{ - Group: "argoproj.io", - Version: "v1alpha1", - Resource: "rollouts", -} - -// RolloutOption is a functional option for configuring an Argo Rollout. -type RolloutOption func(*unstructured.Unstructured) +// RolloutOption is a function that modifies a Rollout. +type RolloutOption func(*rolloutv1alpha1.Rollout) // IsArgoRolloutsInstalled checks if Argo Rollouts CRD is installed in the cluster. -func IsArgoRolloutsInstalled(ctx context.Context, dynamicClient dynamic.Interface) bool { - // Try to list rollouts - if CRD exists, this will succeed (possibly with empty list) - _, err := dynamicClient.Resource(ArgoRolloutGVR).Namespace("default").List(ctx, metav1.ListOptions{Limit: 1}) +func IsArgoRolloutsInstalled(ctx context.Context, client rolloutsclient.Interface) bool { + if client == nil { + return false + } + _, err := client.ArgoprojV1alpha1().Rollouts("default").List(ctx, metav1.ListOptions{Limit: 1}) return err == nil } -// CreateArgoRollout creates an Argo Rollout with the given options. -func CreateArgoRollout(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string, opts ...RolloutOption) error { - rollout := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "argoproj.io/v1alpha1", - "kind": "Rollout", - "metadata": map[string]interface{}{ - "name": name, - "namespace": namespace, +// CreateRollout creates an Argo Rollout with the given options. +func CreateRollout(ctx context.Context, client rolloutsclient.Interface, namespace, name string, opts ...RolloutOption) (*rolloutv1alpha1.Rollout, error) { + rollout := &rolloutv1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: rolloutv1alpha1.RolloutSpec{ + Replicas: ptr.To[int32](1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": name}, }, - "spec": map[string]interface{}{ - "replicas": int64(1), - "selector": map[string]interface{}{ - "matchLabels": map[string]interface{}{ - "app": name, - }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": name}, }, - "template": map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": name, - }, - }, - "spec": map[string]interface{}{ - "containers": []interface{}{ - map[string]interface{}{ - "name": "app", - "image": "busybox:1.36", - "command": []interface{}{"sh", "-c", "sleep 3600"}, - }, - }, - }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "main", + Image: DefaultImage, + Command: []string{"sh", "-c", DefaultCommand}, + }}, }, - "strategy": map[string]interface{}{ - "canary": map[string]interface{}{ - "steps": []interface{}{ - map[string]interface{}{ - "setWeight": int64(100), - }, - }, + }, + Strategy: rolloutv1alpha1.RolloutStrategy{ + Canary: &rolloutv1alpha1.CanaryStrategy{ + Steps: []rolloutv1alpha1.CanaryStep{ + {SetWeight: ptr.To[int32](100)}, }, }, }, }, } - // Apply options for _, opt := range opts { opt(rollout) } - _, err := dynamicClient.Resource(ArgoRolloutGVR).Namespace(namespace).Create(ctx, rollout, metav1.CreateOptions{}) - return err + return client.ArgoprojV1alpha1().Rollouts(namespace).Create(ctx, rollout, metav1.CreateOptions{}) } -// DeleteArgoRollout deletes an Argo Rollout. -func DeleteArgoRollout(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) error { - err := dynamicClient.Resource(ArgoRolloutGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) - return err -} - -// GetArgoRollout retrieves an Argo Rollout. -func GetArgoRollout(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, error) { - return dynamicClient.Resource(ArgoRolloutGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) +// DeleteRollout deletes an Argo Rollout using typed client. +func DeleteRollout(ctx context.Context, client rolloutsclient.Interface, namespace, name string) error { + return client.ArgoprojV1alpha1().Rollouts(namespace).Delete(ctx, name, metav1.DeleteOptions{}) } // WithRolloutConfigMapEnvFrom adds a ConfigMap envFrom to the Rollout. func WithRolloutConfigMapEnvFrom(configMapName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - containers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - envFrom, _, _ := unstructured.NestedSlice(container, "envFrom") - envFrom = append(envFrom, map[string]interface{}{ - "configMapRef": map[string]interface{}{ - "name": configMapName, - }, - }) - container["envFrom"] = envFrom - containers[0] = container - _ = unstructured.SetNestedSlice(rollout.Object, containers, "spec", "template", "spec", "containers") - } + return func(r *rolloutv1alpha1.Rollout) { + AddEnvFromSource(&r.Spec.Template.Spec, 0, configMapName, false) } } // WithRolloutSecretEnvFrom adds a Secret envFrom to the Rollout. func WithRolloutSecretEnvFrom(secretName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - containers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - envFrom, _, _ := unstructured.NestedSlice(container, "envFrom") - envFrom = append(envFrom, map[string]interface{}{ - "secretRef": map[string]interface{}{ - "name": secretName, - }, - }) - container["envFrom"] = envFrom - containers[0] = container - _ = unstructured.SetNestedSlice(rollout.Object, containers, "spec", "template", "spec", "containers") - } + return func(r *rolloutv1alpha1.Rollout) { + AddEnvFromSource(&r.Spec.Template.Spec, 0, secretName, true) } } // WithRolloutConfigMapVolume adds a ConfigMap volume to the Rollout. func WithRolloutConfigMapVolume(configMapName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - // Add volume - volumes, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "volumes") - volumes = append(volumes, map[string]interface{}{ - "name": configMapName + "-volume", - "configMap": map[string]interface{}{ - "name": configMapName, - }, - }) - _ = unstructured.SetNestedSlice(rollout.Object, volumes, "spec", "template", "spec", "volumes") - - // Add volumeMount - containers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - volumeMounts, _, _ := unstructured.NestedSlice(container, "volumeMounts") - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": configMapName + "-volume", - "mountPath": "/etc/config/" + configMapName, - }) - container["volumeMounts"] = volumeMounts - containers[0] = container - _ = unstructured.SetNestedSlice(rollout.Object, containers, "spec", "template", "spec", "containers") - } + return func(r *rolloutv1alpha1.Rollout) { + AddConfigMapVolume(&r.Spec.Template.Spec, 0, configMapName) } } // WithRolloutSecretVolume adds a Secret volume to the Rollout. func WithRolloutSecretVolume(secretName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - // Add volume - volumes, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "volumes") - volumes = append(volumes, map[string]interface{}{ - "name": secretName + "-volume", - "secret": map[string]interface{}{ - "secretName": secretName, - }, - }) - _ = unstructured.SetNestedSlice(rollout.Object, volumes, "spec", "template", "spec", "volumes") - - // Add volumeMount - containers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - volumeMounts, _, _ := unstructured.NestedSlice(container, "volumeMounts") - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": secretName + "-volume", - "mountPath": "/etc/secrets/" + secretName, - }) - container["volumeMounts"] = volumeMounts - containers[0] = container - _ = unstructured.SetNestedSlice(rollout.Object, containers, "spec", "template", "spec", "containers") - } + return func(r *rolloutv1alpha1.Rollout) { + AddSecretVolume(&r.Spec.Template.Spec, 0, secretName) } } -// WithRolloutAnnotations adds annotations to the Rollout's pod template. +// WithRolloutAnnotations adds annotations to the Rollout level (where Reloader checks them). func WithRolloutAnnotations(annotations map[string]string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - annotationsMap := make(map[string]interface{}) - for k, v := range annotations { - annotationsMap[k] = v + return func(r *rolloutv1alpha1.Rollout) { + if len(annotations) > 0 { + if r.Annotations == nil { + r.Annotations = make(map[string]string) + } + for k, v := range annotations { + r.Annotations[k] = v + } } - _ = unstructured.SetNestedMap(rollout.Object, annotationsMap, "spec", "template", "metadata", "annotations") } } // WithRolloutObjectAnnotations adds annotations to the Rollout's top-level metadata. -// Use this for annotations that are read from the Rollout object itself (like rollout-strategy). func WithRolloutObjectAnnotations(annotations map[string]string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - annotationsMap := make(map[string]interface{}) + return func(r *rolloutv1alpha1.Rollout) { + if r.Annotations == nil { + r.Annotations = make(map[string]string) + } for k, v := range annotations { - annotationsMap[k] = v + r.Annotations[k] = v } - _ = unstructured.SetNestedMap(rollout.Object, annotationsMap, "metadata", "annotations") } } - -// WaitForRolloutReady waits for an Argo Rollout to be ready. -func WaitForRolloutReady(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string, timeout time.Duration) error { - return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - rollout, err := dynamicClient.Resource(ArgoRolloutGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep polling - } - - // Check status.phase == "Healthy" or replicas == availableReplicas - status, found, _ := unstructured.NestedMap(rollout.Object, "status") - if !found { - return false, nil - } - - phase, _, _ := unstructured.NestedString(status, "phase") - if phase == "Healthy" { - return true, nil - } - - // Alternative: check replicas - replicas, _, _ := unstructured.NestedInt64(rollout.Object, "spec", "replicas") - availableReplicas, _, _ := unstructured.NestedInt64(status, "availableReplicas") - if replicas > 0 && replicas == availableReplicas { - return true, nil - } - - return false, nil - }) -} - -// WaitForRolloutReloaded waits for an Argo Rollout's pod template to have the reloader annotation. -func WaitForRolloutReloaded(ctx context.Context, dynamicClient dynamic.Interface, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - rollout, err := dynamicClient.Resource(ArgoRolloutGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil - } - - // Check pod template annotations - annotations, _, _ := unstructured.NestedStringMap(rollout.Object, "spec", "template", "metadata", "annotations") - if annotations != nil { - if _, ok := annotations[annotationKey]; ok { - found = true - return true, nil - } - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil -} - -// GetRolloutPodTemplateAnnotations retrieves the pod template annotations from an Argo Rollout. -func GetRolloutPodTemplateAnnotations(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (map[string]string, error) { - rollout, err := dynamicClient.Resource(ArgoRolloutGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - - annotations, _, _ := unstructured.NestedStringMap(rollout.Object, "spec", "template", "metadata", "annotations") - return annotations, nil -} - -// WaitForRolloutRestartAt waits for an Argo Rollout's spec.restartAt field to be set. -// This is used when the restart strategy is specified. -func WaitForRolloutRestartAt(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - rollout, err := dynamicClient.Resource(ArgoRolloutGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil - } - - // Check if spec.restartAt is set - restartAt, exists, _ := unstructured.NestedString(rollout.Object, "spec", "restartAt") - if exists && restartAt != "" { - found = true - return true, nil - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil -} diff --git a/test/e2e/utils/csi.go b/test/e2e/utils/csi.go new file mode 100644 index 0000000..e5c1a04 --- /dev/null +++ b/test/e2e/utils/csi.go @@ -0,0 +1,385 @@ +package utils + +import ( + "bytes" + "context" + "fmt" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "k8s.io/client-go/tools/remotecommand" + csiv1 "sigs.k8s.io/secrets-store-csi-driver/apis/v1" + csiclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" +) + +// CSI Driver constants +const ( + // CSIDriverName is the name of the secrets-store CSI driver + CSIDriverName = "secrets-store.csi.k8s.io" + + // DefaultCSIProvider is the default provider name for testing (Vault) + DefaultCSIProvider = "vault" + + // VaultAddress is the default Vault address in the cluster + VaultAddress = "http://vault.vault:8200" + + // VaultRole is the Kubernetes auth role configured in Vault for testing + VaultRole = "test-role" + + // VaultNamespace is the namespace where Vault is deployed + VaultNamespace = "vault" + + // VaultPodName is the name of the Vault pod (dev mode) + VaultPodName = "vault-0" + + // CSIVolumeName is the default volume name for CSI volumes in tests + CSIVolumeName = "csi-secrets-store" + + // CSIMountPath is the default mount path for CSI volumes in tests + CSIMountPath = "/mnt/secrets-store" + + // CSIRotationPollInterval is how often CSI driver checks for secret changes + CSIRotationPollInterval = 2 * time.Second +) + +// NewCSIClient creates a new CSI client using the default kubeconfig. +func NewCSIClient() (csiclient.Interface, error) { + kubeconfig := GetKubeconfig() + config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) + if err != nil { + return nil, fmt.Errorf("building config from kubeconfig: %w", err) + } + return NewCSIClientFromConfig(config) +} + +// NewCSIClientFromConfig creates a new CSI client from a rest.Config. +func NewCSIClientFromConfig(config *rest.Config) (csiclient.Interface, error) { + client, err := csiclient.NewForConfig(config) + if err != nil { + return nil, fmt.Errorf("creating CSI client: %w", err) + } + return client, nil +} + +// IsCSIDriverInstalled checks if the CSI secrets store driver CRDs are available in the cluster. +// This checks for the SecretProviderClass CRD which is required for CSI tests. +func IsCSIDriverInstalled(ctx context.Context, client csiclient.Interface) bool { + if client == nil { + return false + } + + // Try to list SecretProviderClasses - if CRD doesn't exist, this will fail + _, err := client.SecretsstoreV1().SecretProviderClasses("default").List(ctx, metav1.ListOptions{Limit: 1}) + return err == nil +} + +// IsVaultProviderInstalled checks if Vault CSI provider is installed by checking for the vault-csi-provider DaemonSet. +// This is used to determine if CSI tests with actual volume mounting can run. +func IsVaultProviderInstalled(ctx context.Context, kubeClient kubernetes.Interface) bool { + if kubeClient == nil { + return false + } + + // Check if vault-csi-provider DaemonSet exists in vault namespace + _, err := kubeClient.AppsV1().DaemonSets("vault").Get(ctx, "vault-csi-provider", metav1.GetOptions{}) + return err == nil +} + +// CreateSecretProviderClass creates a SecretProviderClass in the given namespace. +// If params is nil, it creates a Vault-compatible SecretProviderClass with default test settings. +func CreateSecretProviderClass(ctx context.Context, client csiclient.Interface, namespace, name string, params map[string]string) ( + *csiv1.SecretProviderClass, error, +) { + if params == nil { + // Default Vault-compatible parameters for testing + params = map[string]string{ + "vaultAddress": VaultAddress, + "roleName": VaultRole, + "objects": `- objectName: "test-secret" + secretPath: "secret/data/test" + secretKey: "username"`, + } + } + + spc := &csiv1.SecretProviderClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: csiv1.SecretProviderClassSpec{ + Provider: DefaultCSIProvider, + Parameters: params, + }, + } + + created, err := client.SecretsstoreV1().SecretProviderClasses(namespace).Create(ctx, spc, metav1.CreateOptions{}) + if err != nil { + return nil, fmt.Errorf("creating SecretProviderClass %s/%s: %w", namespace, name, err) + } + return created, nil +} + +// CreateSecretProviderClassWithSecret creates a SecretProviderClass that fetches a specific secret from Vault. +// secretPath should be like "secret/mysecret" (the function converts it to KV v2 format "secret/data/mysecret"). +// secretKey is the key within that secret to fetch. +func CreateSecretProviderClassWithSecret(ctx context.Context, client csiclient.Interface, namespace, name, secretPath, secretKey string) ( + *csiv1.SecretProviderClass, error, +) { + // Convert KV v1 style path to KV v2 data path + // "secret/foo" -> "secret/data/foo" + kvV2Path := secretPath + if strings.HasPrefix(secretPath, "secret/") && !strings.HasPrefix(secretPath, "secret/data/") { + kvV2Path = strings.Replace(secretPath, "secret/", "secret/data/", 1) + } + + params := map[string]string{ + "vaultAddress": VaultAddress, + "roleName": VaultRole, + "objects": fmt.Sprintf( + `- objectName: "%s" + secretPath: "%s" + secretKey: "%s"`, secretKey, kvV2Path, secretKey, + ), + } + return CreateSecretProviderClass(ctx, client, namespace, name, params) +} + +// DeleteSecretProviderClass deletes a SecretProviderClass by name. +func DeleteSecretProviderClass(ctx context.Context, client csiclient.Interface, namespace, name string) error { + err := client.SecretsstoreV1().SecretProviderClasses(namespace).Delete(ctx, name, metav1.DeleteOptions{}) + if err != nil { + return fmt.Errorf("deleting SecretProviderClass %s/%s: %w", namespace, name, err) + } + return nil +} + +// UpdateSecretProviderClassPodStatusLabels updates only the labels on a SecretProviderClassPodStatus. +// This should NOT trigger a reload (used for negative testing to verify Reloader ignores label-only changes). +func UpdateSecretProviderClassPodStatusLabels(ctx context.Context, client csiclient.Interface, namespace, name string, labels map[string]string) error { + spcps, err := client.SecretsstoreV1().SecretProviderClassPodStatuses(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return fmt.Errorf("getting SecretProviderClassPodStatus %s/%s: %w", namespace, name, err) + } + + if spcps.Labels == nil { + spcps.Labels = make(map[string]string) + } + for k, v := range labels { + spcps.Labels[k] = v + } + + _, err = client.SecretsstoreV1().SecretProviderClassPodStatuses(namespace).Update(ctx, spcps, metav1.UpdateOptions{}) + if err != nil { + return fmt.Errorf("updating SecretProviderClassPodStatus labels %s/%s: %w", namespace, name, err) + } + return nil +} + +// ============================================================================= +// Vault Integration Helpers +// ============================================================================= + +// CreateVaultSecret creates a new secret in Vault. +// secretPath should be like "secret/test" (without "data" prefix - it's added automatically). +// data is a map of key-value pairs to store in the secret. +func CreateVaultSecret(ctx context.Context, kubeClient kubernetes.Interface, restConfig *rest.Config, secretPath string, data map[string]string) error { + return UpdateVaultSecret(ctx, kubeClient, restConfig, secretPath, data) +} + +// UpdateVaultSecret updates a secret in Vault. This triggers the CSI driver to +// sync the new secret version, which creates/updates the SecretProviderClassPodStatus. +// secretPath should be like "secret/test" (without "data" prefix - it's added automatically). +// data is a map of key-value pairs to store in the secret. +func UpdateVaultSecret(ctx context.Context, kubeClient kubernetes.Interface, restConfig *rest.Config, secretPath string, data map[string]string) error { + // Build the vault kv put command + // Format: vault kv put secret/path key1=value1 key2=value2 + args := []string{"kv", "put", secretPath} + for k, v := range data { + args = append(args, fmt.Sprintf("%s=%s", k, v)) + } + + if err := execInVaultPod(ctx, kubeClient, restConfig, args); err != nil { + return fmt.Errorf("updating Vault secret %s: %w", secretPath, err) + } + return nil +} + +// DeleteVaultSecret deletes a secret from Vault. +// secretPath should be like "secret/test". +func DeleteVaultSecret(ctx context.Context, kubeClient kubernetes.Interface, restConfig *rest.Config, secretPath string) error { + args := []string{"kv", "metadata", "delete", secretPath} + if err := execInVaultPod(ctx, kubeClient, restConfig, args); err != nil { + // Ignore not found errors + if strings.Contains(err.Error(), "No value found") { + return nil + } + return fmt.Errorf("deleting Vault secret %s: %w", secretPath, err) + } + return nil +} + +// execInVaultPod executes a vault command in the Vault pod. +func execInVaultPod(ctx context.Context, kubeClient kubernetes.Interface, restConfig *rest.Config, args []string) error { + req := kubeClient.CoreV1().RESTClient().Post(). + Resource("pods"). + Name(VaultPodName). + Namespace(VaultNamespace). + SubResource("exec"). + VersionedParams( + &corev1.PodExecOptions{ + Container: "vault", + Command: append([]string{"vault"}, args...), + Stdout: true, + Stderr: true, + }, scheme.ParameterCodec, + ) + + exec, err := remotecommand.NewSPDYExecutor(restConfig, "POST", req.URL()) + if err != nil { + return fmt.Errorf("creating executor: %w", err) + } + + var stderr bytes.Buffer + err = exec.StreamWithContext( + ctx, remotecommand.StreamOptions{ + Stderr: &stderr, + }, + ) + if err != nil { + return fmt.Errorf("executing command: %w (stderr: %s)", err, stderr.String()) + } + + return nil +} + +// WaitForSPCPSVersionChange waits for the SecretProviderClassPodStatus objects to change +// from the initial version. This is used after updating a Vault secret to wait for CSI +// driver to sync the new version. +func WaitForSPCPSVersionChange(ctx context.Context, client csiclient.Interface, namespace, spcpsName, initialVersion string, timeout time.Duration) error { + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + spcps, err := client.SecretsstoreV1().SecretProviderClassPodStatuses(namespace).Get(ctx, spcpsName, metav1.GetOptions{}) + if err == nil && spcps.Status.Mounted && len(spcps.Status.Objects) > 0 { + // Check if any object version has changed + for _, obj := range spcps.Status.Objects { + if obj.Version != initialVersion { + return nil + } + } + } + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(1 * time.Second): + } + } + return fmt.Errorf("timeout waiting for SecretProviderClassPodStatus %s/%s version to change from %s", namespace, spcpsName, initialVersion) +} + +// FindSPCPSForDeployment finds the SecretProviderClassPodStatus created by CSI driver +// for pods of a given deployment. Returns the first matching SPCPS name. +func FindSPCPSForDeployment(ctx context.Context, csiClient csiclient.Interface, kubeClient kubernetes.Interface, namespace, deploymentName string, timeout time.Duration) ( + string, error, +) { + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + // Get pods for the deployment + pods, err := kubeClient.CoreV1().Pods(namespace).List( + ctx, metav1.ListOptions{ + LabelSelector: fmt.Sprintf("app=%s", deploymentName), + }, + ) + if err != nil { + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(1 * time.Second): + continue + } + } + + // Look for SPCPS that references any of these pods + spcpsList, err := csiClient.SecretsstoreV1().SecretProviderClassPodStatuses(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(1 * time.Second): + continue + } + } + + for _, pod := range pods.Items { + for _, spcps := range spcpsList.Items { + if spcps.Status.PodName == pod.Name && spcps.Status.Mounted { + return spcps.Name, nil + } + } + } + + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(1 * time.Second): + } + } + + return "", fmt.Errorf("timeout finding SecretProviderClassPodStatus for deployment %s/%s", namespace, deploymentName) +} + +// FindSPCPSForSPC finds the SecretProviderClassPodStatus created by CSI driver +// that references a specific SecretProviderClass. Returns the first matching SPCPS name. +func FindSPCPSForSPC(ctx context.Context, csiClient csiclient.Interface, namespace, spcName string, timeout time.Duration) (string, error) { + deadline := time.Now().Add(timeout) + + for time.Now().Before(deadline) { + spcpsList, err := csiClient.SecretsstoreV1().SecretProviderClassPodStatuses(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(1 * time.Second): + continue + } + } + + for _, spcps := range spcpsList.Items { + if spcps.Status.SecretProviderClassName == spcName && spcps.Status.Mounted { + return spcps.Name, nil + } + } + + select { + case <-ctx.Done(): + return "", ctx.Err() + case <-time.After(1 * time.Second): + } + } + + return "", fmt.Errorf("timeout finding SecretProviderClassPodStatus for SPC %s/%s", namespace, spcName) +} + +// GetSPCPSVersion gets the current version string from a SecretProviderClassPodStatus. +// Returns the version of the first object, or empty string if not found. +func GetSPCPSVersion(ctx context.Context, client csiclient.Interface, namespace, name string) (string, error) { + spcps, err := client.SecretsstoreV1().SecretProviderClassPodStatuses(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return "", fmt.Errorf("getting SecretProviderClassPodStatus %s/%s: %w", namespace, name, err) + } + if len(spcps.Status.Objects) == 0 { + return "", nil + } + // Return concatenated versions for all objects to detect any change + var versions []string + for _, obj := range spcps.Status.Objects { + versions = append(versions, obj.Version) + } + return strings.Join(versions, ","), nil +} diff --git a/test/e2e/utils/openshift.go b/test/e2e/utils/openshift.go index dac55f4..b2ec1d9 100644 --- a/test/e2e/utils/openshift.go +++ b/test/e2e/utils/openshift.go @@ -1,27 +1,9 @@ package utils import ( - "context" - "time" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" ) -// DeploymentConfigGVR returns the GroupVersionResource for OpenShift DeploymentConfigs. -var DeploymentConfigGVR = schema.GroupVersionResource{ - Group: "apps.openshift.io", - Version: "v1", - Resource: "deploymentconfigs", -} - -// DCOption is a functional option for configuring a DeploymentConfig. -type DCOption func(*unstructured.Unstructured) - // HasDeploymentConfigSupport checks if the cluster has OpenShift DeploymentConfig API available. func HasDeploymentConfigSupport(discoveryClient discovery.DiscoveryInterface) bool { _, apiLists, err := discoveryClient.ServerGroupsAndResources() @@ -39,227 +21,3 @@ func HasDeploymentConfigSupport(discoveryClient discovery.DiscoveryInterface) bo return false } - -// CreateDeploymentConfig creates an OpenShift DeploymentConfig with the given options. -func CreateDeploymentConfig(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string, opts ...DCOption) error { - dc := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "apps.openshift.io/v1", - "kind": "DeploymentConfig", - "metadata": map[string]interface{}{ - "name": name, - "namespace": namespace, - }, - "spec": map[string]interface{}{ - "replicas": int64(1), - "selector": map[string]interface{}{ - "app": name, - }, - "template": map[string]interface{}{ - "metadata": map[string]interface{}{ - "labels": map[string]interface{}{ - "app": name, - }, - }, - "spec": map[string]interface{}{ - "containers": []interface{}{ - map[string]interface{}{ - "name": "app", - "image": "busybox:1.36", - "command": []interface{}{"sh", "-c", "sleep 3600"}, - }, - }, - }, - }, - "triggers": []interface{}{ - map[string]interface{}{ - "type": "ConfigChange", - }, - }, - }, - }, - } - - // Apply options - for _, opt := range opts { - opt(dc) - } - - _, err := dynamicClient.Resource(DeploymentConfigGVR).Namespace(namespace).Create(ctx, dc, metav1.CreateOptions{}) - return err -} - -// DeleteDeploymentConfig deletes a DeploymentConfig. -func DeleteDeploymentConfig(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) error { - return dynamicClient.Resource(DeploymentConfigGVR).Namespace(namespace).Delete(ctx, name, metav1.DeleteOptions{}) -} - -// GetDeploymentConfig retrieves a DeploymentConfig. -func GetDeploymentConfig(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (*unstructured.Unstructured, error) { - return dynamicClient.Resource(DeploymentConfigGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) -} - -// WithDCConfigMapEnvFrom adds a ConfigMap envFrom to the DeploymentConfig. -func WithDCConfigMapEnvFrom(configMapName string) DCOption { - return func(dc *unstructured.Unstructured) { - containers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - envFrom, _, _ := unstructured.NestedSlice(container, "envFrom") - envFrom = append(envFrom, map[string]interface{}{ - "configMapRef": map[string]interface{}{ - "name": configMapName, - }, - }) - container["envFrom"] = envFrom - containers[0] = container - _ = unstructured.SetNestedSlice(dc.Object, containers, "spec", "template", "spec", "containers") - } - } -} - -// WithDCSecretEnvFrom adds a Secret envFrom to the DeploymentConfig. -func WithDCSecretEnvFrom(secretName string) DCOption { - return func(dc *unstructured.Unstructured) { - containers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - envFrom, _, _ := unstructured.NestedSlice(container, "envFrom") - envFrom = append(envFrom, map[string]interface{}{ - "secretRef": map[string]interface{}{ - "name": secretName, - }, - }) - container["envFrom"] = envFrom - containers[0] = container - _ = unstructured.SetNestedSlice(dc.Object, containers, "spec", "template", "spec", "containers") - } - } -} - -// WithDCConfigMapVolume adds a ConfigMap volume to the DeploymentConfig. -func WithDCConfigMapVolume(configMapName string) DCOption { - return func(dc *unstructured.Unstructured) { - // Add volume - volumes, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "volumes") - volumes = append(volumes, map[string]interface{}{ - "name": configMapName + "-volume", - "configMap": map[string]interface{}{ - "name": configMapName, - }, - }) - _ = unstructured.SetNestedSlice(dc.Object, volumes, "spec", "template", "spec", "volumes") - - // Add volumeMount - containers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - volumeMounts, _, _ := unstructured.NestedSlice(container, "volumeMounts") - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": configMapName + "-volume", - "mountPath": "/etc/config/" + configMapName, - }) - container["volumeMounts"] = volumeMounts - containers[0] = container - _ = unstructured.SetNestedSlice(dc.Object, containers, "spec", "template", "spec", "containers") - } - } -} - -// WithDCSecretVolume adds a Secret volume to the DeploymentConfig. -func WithDCSecretVolume(secretName string) DCOption { - return func(dc *unstructured.Unstructured) { - // Add volume - volumes, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "volumes") - volumes = append(volumes, map[string]interface{}{ - "name": secretName + "-volume", - "secret": map[string]interface{}{ - "secretName": secretName, - }, - }) - _ = unstructured.SetNestedSlice(dc.Object, volumes, "spec", "template", "spec", "volumes") - - // Add volumeMount - containers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - volumeMounts, _, _ := unstructured.NestedSlice(container, "volumeMounts") - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": secretName + "-volume", - "mountPath": "/etc/secrets/" + secretName, - }) - container["volumeMounts"] = volumeMounts - containers[0] = container - _ = unstructured.SetNestedSlice(dc.Object, containers, "spec", "template", "spec", "containers") - } - } -} - -// WithDCAnnotations adds annotations to the DeploymentConfig's pod template. -func WithDCAnnotations(annotations map[string]string) DCOption { - return func(dc *unstructured.Unstructured) { - annotationsMap := make(map[string]interface{}) - for k, v := range annotations { - annotationsMap[k] = v - } - _ = unstructured.SetNestedMap(dc.Object, annotationsMap, "spec", "template", "metadata", "annotations") - } -} - -// WaitForDeploymentConfigReady waits for a DeploymentConfig to be ready. -func WaitForDeploymentConfigReady(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string, timeout time.Duration) error { - return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - dc, err := dynamicClient.Resource(DeploymentConfigGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep polling - } - - // Check replicas == readyReplicas - replicas, _, _ := unstructured.NestedInt64(dc.Object, "spec", "replicas") - readyReplicas, _, _ := unstructured.NestedInt64(dc.Object, "status", "readyReplicas") - - if replicas > 0 && replicas == readyReplicas { - return true, nil - } - - return false, nil - }) -} - -// WaitForDeploymentConfigReloaded waits for a DeploymentConfig's pod template to have the reloader annotation. -func WaitForDeploymentConfigReloaded(ctx context.Context, dynamicClient dynamic.Interface, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - dc, err := dynamicClient.Resource(DeploymentConfigGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil - } - - // Check pod template annotations - annotations, _, _ := unstructured.NestedStringMap(dc.Object, "spec", "template", "metadata", "annotations") - if annotations != nil { - if _, ok := annotations[annotationKey]; ok { - found = true - return true, nil - } - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil -} - -// GetDeploymentConfigPodTemplateAnnotations retrieves the pod template annotations from a DeploymentConfig. -func GetDeploymentConfigPodTemplateAnnotations(ctx context.Context, dynamicClient dynamic.Interface, namespace, name string) (map[string]string, error) { - dc, err := dynamicClient.Resource(DeploymentConfigGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return nil, err - } - - annotations, _, _ := unstructured.NestedStringMap(dc.Object, "spec", "template", "metadata", "annotations") - return annotations, nil -} diff --git a/test/e2e/utils/podspec.go b/test/e2e/utils/podspec.go new file mode 100644 index 0000000..df9011f --- /dev/null +++ b/test/e2e/utils/podspec.go @@ -0,0 +1,257 @@ +package utils + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/utils/ptr" +) + +// AddEnvFromSource adds ConfigMap or Secret envFrom to a container. +func AddEnvFromSource(spec *corev1.PodSpec, containerIdx int, name string, isSecret bool) { + if containerIdx >= len(spec.Containers) { + return + } + source := corev1.EnvFromSource{} + if isSecret { + source.SecretRef = &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + } + } else { + source.ConfigMapRef = &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + } + } + spec.Containers[containerIdx].EnvFrom = append(spec.Containers[containerIdx].EnvFrom, source) +} + +// AddVolume adds a volume and mount to a container. +func AddVolume(spec *corev1.PodSpec, containerIdx int, volume corev1.Volume, mountPath string) { + spec.Volumes = append(spec.Volumes, volume) + if containerIdx < len(spec.Containers) { + spec.Containers[containerIdx].VolumeMounts = append( + spec.Containers[containerIdx].VolumeMounts, + corev1.VolumeMount{Name: volume.Name, MountPath: mountPath}, + ) + } +} + +// AddConfigMapVolume adds ConfigMap volume and mount. +func AddConfigMapVolume(spec *corev1.PodSpec, containerIdx int, name string) { + AddVolume(spec, containerIdx, corev1.Volume{ + Name: "cm-" + name, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + }, + }, + }, "/etc/config/"+name) +} + +// AddSecretVolume adds Secret volume and mount. +func AddSecretVolume(spec *corev1.PodSpec, containerIdx int, name string) { + AddVolume(spec, containerIdx, corev1.Volume{ + Name: "secret-" + name, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{SecretName: name}, + }, + }, "/etc/secrets/"+name) +} + +// AddProjectedVolume adds projected volume with ConfigMap and/or Secret. +func AddProjectedVolume(spec *corev1.PodSpec, containerIdx int, cmName, secretName string) { + sources := []corev1.VolumeProjection{} + if cmName != "" { + sources = append(sources, corev1.VolumeProjection{ + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, + }, + }) + } + if secretName != "" { + sources = append(sources, corev1.VolumeProjection{ + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + }, + }) + } + AddVolume(spec, containerIdx, corev1.Volume{ + Name: "projected-config", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{Sources: sources}, + }, + }, "/etc/projected") +} + +// AddKeyRef adds env var from ConfigMap or Secret key. +func AddKeyRef(spec *corev1.PodSpec, containerIdx int, resourceName, key, envVarName string, isSecret bool) { + if containerIdx >= len(spec.Containers) { + return + } + envVar := corev1.EnvVar{Name: envVarName} + if isSecret { + envVar.ValueFrom = &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: resourceName}, + Key: key, + }, + } + } else { + envVar.ValueFrom = &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: resourceName}, + Key: key, + }, + } + } + spec.Containers[containerIdx].Env = append(spec.Containers[containerIdx].Env, envVar) +} + +// AddCSIVolume adds CSI volume referencing SecretProviderClass. +func AddCSIVolume(spec *corev1.PodSpec, containerIdx int, spcName string) { + volumeName := "csi-" + spcName + mountPath := "/mnt/secrets-store/" + spcName + spec.Volumes = append(spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + CSI: &corev1.CSIVolumeSource{ + Driver: CSIDriverName, + ReadOnly: ptr.To(true), + VolumeAttributes: map[string]string{ + "secretProviderClass": spcName, + }, + }, + }, + }) + if containerIdx < len(spec.Containers) { + spec.Containers[containerIdx].VolumeMounts = append( + spec.Containers[containerIdx].VolumeMounts, + corev1.VolumeMount{Name: volumeName, MountPath: mountPath, ReadOnly: true}, + ) + } +} + +// AddInitContainer adds init container with optional envFrom references. +func AddInitContainer(spec *corev1.PodSpec, cmName, secretName string) { + init := corev1.Container{ + Name: "init", + Image: DefaultImage, + Command: []string{"sh", "-c", "echo init done"}, + } + if cmName != "" { + init.EnvFrom = append(init.EnvFrom, corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, + }, + }) + } + if secretName != "" { + init.EnvFrom = append(init.EnvFrom, corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + }, + }) + } + spec.InitContainers = append(spec.InitContainers, init) +} + +// AddInitContainerWithVolumes adds init container with volume mounts. +func AddInitContainerWithVolumes(spec *corev1.PodSpec, cmName, secretName string) { + init := corev1.Container{ + Name: "init", + Image: DefaultImage, + Command: []string{"sh", "-c", "echo init done"}, + } + if cmName != "" { + volumeName := "init-cm-" + cmName + spec.Volumes = append(spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, + }, + }, + }) + init.VolumeMounts = append(init.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/etc/init-config/" + cmName, + }) + } + if secretName != "" { + volumeName := "init-secret-" + secretName + spec.Volumes = append(spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{SecretName: secretName}, + }, + }) + init.VolumeMounts = append(init.VolumeMounts, corev1.VolumeMount{ + Name: volumeName, + MountPath: "/etc/init-secrets/" + secretName, + }) + } + spec.InitContainers = append(spec.InitContainers, init) +} + +// ApplyWorkloadConfig applies all WorkloadConfig settings to a PodSpec. +// This single function replaces all the duplicate buildXxxOptions functions. +func ApplyWorkloadConfig(spec *corev1.PodSpec, cfg WorkloadConfig) { + if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" { + AddEnvFromSource(spec, 0, cfg.ConfigMapName, false) + } + if cfg.UseSecretEnvFrom && cfg.SecretName != "" { + AddEnvFromSource(spec, 0, cfg.SecretName, true) + } + if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" { + AddConfigMapVolume(spec, 0, cfg.ConfigMapName) + } + if cfg.UseSecretVolume && cfg.SecretName != "" { + AddSecretVolume(spec, 0, cfg.SecretName) + } + if cfg.UseProjectedVolume { + AddProjectedVolume(spec, 0, cfg.ConfigMapName, cfg.SecretName) + } + if cfg.UseConfigMapKeyRef && cfg.ConfigMapName != "" { + key := cfg.ConfigMapKey + if key == "" { + key = "key" + } + envVar := cfg.EnvVarName + if envVar == "" { + envVar = "CONFIG_VAR" + } + AddKeyRef(spec, 0, cfg.ConfigMapName, key, envVar, false) + } + if cfg.UseSecretKeyRef && cfg.SecretName != "" { + key := cfg.SecretKey + if key == "" { + key = "key" + } + envVar := cfg.EnvVarName + if envVar == "" { + envVar = "SECRET_VAR" + } + AddKeyRef(spec, 0, cfg.SecretName, key, envVar, true) + } + if cfg.UseCSIVolume && cfg.SPCName != "" { + AddCSIVolume(spec, 0, cfg.SPCName) + } + if cfg.UseInitContainer { + AddInitContainer(spec, cfg.ConfigMapName, cfg.SecretName) + } + if cfg.UseInitContainerVolume { + AddInitContainerWithVolumes(spec, cfg.ConfigMapName, cfg.SecretName) + } + if cfg.UseInitContainerCSI && cfg.SPCName != "" { + AddCSIVolume(spec, 0, cfg.SPCName) + } + if cfg.MultipleContainers > 1 { + for i := 1; i < cfg.MultipleContainers; i++ { + spec.Containers = append(spec.Containers, corev1.Container{ + Name: fmt.Sprintf("container-%d", i), + Image: DefaultImage, + Command: []string{"sh", "-c", DefaultCommand}, + }) + } + } +} diff --git a/test/e2e/utils/resources.go b/test/e2e/utils/resources.go index e4dc83d..1963537 100644 --- a/test/e2e/utils/resources.go +++ b/test/e2e/utils/resources.go @@ -175,7 +175,7 @@ type DeploymentOption func(*appsv1.Deployment) // CreateDeployment creates a Deployment with the given options. func CreateDeployment(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...DeploymentOption) (*appsv1.Deployment, error) { - deploy := baseDeployment(namespace, name) + deploy := baseDeploymentResource(namespace, name) for _, opt := range opts { opt(deploy) } @@ -349,14 +349,12 @@ func WithMultipleContainers(count int) DeploymentOption { // WithMultipleContainersAndEnv creates two containers, each with a different ConfigMap envFrom. func WithMultipleContainersAndEnv(cm1Name, cm2Name string) DeploymentOption { return func(d *appsv1.Deployment) { - // First container gets the first ConfigMap d.Spec.Template.Spec.Containers[0].EnvFrom = append(d.Spec.Template.Spec.Containers[0].EnvFrom, corev1.EnvFromSource{ ConfigMapRef: &corev1.ConfigMapEnvSource{ LocalObjectReference: corev1.LocalObjectReference{Name: cm1Name}, }, }) - // Add second container with second ConfigMap d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, corev1.Container{ Name: "container-1", Image: DefaultImage, @@ -379,396 +377,6 @@ func WithReplicas(replicas int32) DeploymentOption { } } -// baseDeployment creates a base Deployment template. -func baseDeployment(namespace, name string) *appsv1.Deployment { - labels := map[string]string{"app": name} - return &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: appsv1.DeploymentSpec{ - Replicas: ptr.To(int32(1)), - Selector: &metav1.LabelSelector{ - MatchLabels: labels, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - Image: DefaultImage, - Command: []string{"sh", "-c", DefaultCommand}, - }, - }, - }, - }, - }, - } -} - -// DeleteDeployment deletes a Deployment. -func DeleteDeployment(ctx context.Context, client kubernetes.Interface, namespace, name string) error { - return client.AppsV1().Deployments(namespace).Delete(ctx, name, metav1.DeleteOptions{}) -} - -// DaemonSetOption is a functional option for configuring a DaemonSet. -type DaemonSetOption func(*appsv1.DaemonSet) - -// CreateDaemonSet creates a DaemonSet with the given options. -func CreateDaemonSet(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...DaemonSetOption) (*appsv1.DaemonSet, error) { - ds := baseDaemonSet(namespace, name) - for _, opt := range opts { - opt(ds) - } - return client.AppsV1().DaemonSets(namespace).Create(ctx, ds, metav1.CreateOptions{}) -} - -// WithDaemonSetAnnotations adds annotations to the DaemonSet metadata. -func WithDaemonSetAnnotations(annotations map[string]string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - if ds.Annotations == nil { - ds.Annotations = make(map[string]string) - } - for k, v := range annotations { - ds.Annotations[k] = v - } - } -} - -// WithDaemonSetConfigMapEnvFrom adds an envFrom reference to a ConfigMap. -func WithDaemonSetConfigMapEnvFrom(name string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - ds.Spec.Template.Spec.Containers[0].EnvFrom = append( - ds.Spec.Template.Spec.Containers[0].EnvFrom, - corev1.EnvFromSource{ - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - ) - } -} - -// WithDaemonSetSecretEnvFrom adds an envFrom reference to a Secret. -func WithDaemonSetSecretEnvFrom(name string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - ds.Spec.Template.Spec.Containers[0].EnvFrom = append( - ds.Spec.Template.Spec.Containers[0].EnvFrom, - corev1.EnvFromSource{ - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - ) - } -} - -// baseDaemonSet creates a base DaemonSet template. -func baseDaemonSet(namespace, name string) *appsv1.DaemonSet { - labels := map[string]string{"app": name} - return &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: appsv1.DaemonSetSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: labels, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - Image: DefaultImage, - Command: []string{"sh", "-c", DefaultCommand}, - }, - }, - }, - }, - }, - } -} - -// DeleteDaemonSet deletes a DaemonSet. -func DeleteDaemonSet(ctx context.Context, client kubernetes.Interface, namespace, name string) error { - return client.AppsV1().DaemonSets(namespace).Delete(ctx, name, metav1.DeleteOptions{}) -} - -// StatefulSetOption is a functional option for configuring a StatefulSet. -type StatefulSetOption func(*appsv1.StatefulSet) - -// CreateStatefulSet creates a StatefulSet with the given options. -func CreateStatefulSet(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...StatefulSetOption) (*appsv1.StatefulSet, error) { - ss := baseStatefulSet(namespace, name) - for _, opt := range opts { - opt(ss) - } - return client.AppsV1().StatefulSets(namespace).Create(ctx, ss, metav1.CreateOptions{}) -} - -// WithStatefulSetAnnotations adds annotations to the StatefulSet metadata. -func WithStatefulSetAnnotations(annotations map[string]string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - if ss.Annotations == nil { - ss.Annotations = make(map[string]string) - } - for k, v := range annotations { - ss.Annotations[k] = v - } - } -} - -// WithStatefulSetConfigMapEnvFrom adds an envFrom reference to a ConfigMap. -func WithStatefulSetConfigMapEnvFrom(name string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - ss.Spec.Template.Spec.Containers[0].EnvFrom = append( - ss.Spec.Template.Spec.Containers[0].EnvFrom, - corev1.EnvFromSource{ - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - ) - } -} - -// WithStatefulSetSecretEnvFrom adds an envFrom reference to a Secret. -func WithStatefulSetSecretEnvFrom(name string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - ss.Spec.Template.Spec.Containers[0].EnvFrom = append( - ss.Spec.Template.Spec.Containers[0].EnvFrom, - corev1.EnvFromSource{ - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - ) - } -} - -// baseStatefulSet creates a base StatefulSet template. -func baseStatefulSet(namespace, name string) *appsv1.StatefulSet { - labels := map[string]string{"app": name} - return &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: appsv1.StatefulSetSpec{ - ServiceName: name, - Replicas: ptr.To(int32(1)), - Selector: &metav1.LabelSelector{ - MatchLabels: labels, - }, - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "app", - Image: DefaultImage, - Command: []string{"sh", "-c", DefaultCommand}, - }, - }, - }, - }, - }, - } -} - -// DeleteStatefulSet deletes a StatefulSet. -func DeleteStatefulSet(ctx context.Context, client kubernetes.Interface, namespace, name string) error { - return client.AppsV1().StatefulSets(namespace).Delete(ctx, name, metav1.DeleteOptions{}) -} - -// CronJobOption is a functional option for configuring a CronJob. -type CronJobOption func(*batchv1.CronJob) - -// CreateCronJob creates a CronJob with the given options. -func CreateCronJob(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...CronJobOption) (*batchv1.CronJob, error) { - cj := baseCronJob(namespace, name) - for _, opt := range opts { - opt(cj) - } - return client.BatchV1().CronJobs(namespace).Create(ctx, cj, metav1.CreateOptions{}) -} - -// WithCronJobAnnotations adds annotations to the CronJob metadata. -func WithCronJobAnnotations(annotations map[string]string) CronJobOption { - return func(cj *batchv1.CronJob) { - if cj.Annotations == nil { - cj.Annotations = make(map[string]string) - } - for k, v := range annotations { - cj.Annotations[k] = v - } - } -} - -// WithCronJobConfigMapEnvFrom adds an envFrom reference to a ConfigMap. -func WithCronJobConfigMapEnvFrom(name string) CronJobOption { - return func(cj *batchv1.CronJob) { - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom = append( - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom, - corev1.EnvFromSource{ - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - ) - } -} - -// WithCronJobSecretEnvFrom adds an envFrom reference to a Secret. -func WithCronJobSecretEnvFrom(name string) CronJobOption { - return func(cj *batchv1.CronJob) { - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom = append( - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom, - corev1.EnvFromSource{ - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - ) - } -} - -// baseCronJob creates a base CronJob template. -func baseCronJob(namespace, name string) *batchv1.CronJob { - labels := map[string]string{"app": name} - return &batchv1.CronJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: batchv1.CronJobSpec{ - Schedule: "* * * * *", // Every minute - JobTemplate: batchv1.JobTemplateSpec{ - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyOnFailure, - Containers: []corev1.Container{ - { - Name: "job", - Image: DefaultImage, - Command: []string{"sh", "-c", "echo done"}, - }, - }, - }, - }, - }, - }, - }, - } -} - -// DeleteCronJob deletes a CronJob. -func DeleteCronJob(ctx context.Context, client kubernetes.Interface, namespace, name string) error { - return client.BatchV1().CronJobs(namespace).Delete(ctx, name, metav1.DeleteOptions{}) -} - -// JobOption is a functional option for configuring a Job. -type JobOption func(*batchv1.Job) - -// CreateJob creates a Job with the given options. -func CreateJob(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...JobOption) (*batchv1.Job, error) { - job := baseJob(namespace, name) - for _, opt := range opts { - opt(job) - } - return client.BatchV1().Jobs(namespace).Create(ctx, job, metav1.CreateOptions{}) -} - -// WithJobAnnotations adds annotations to the Job metadata. -func WithJobAnnotations(annotations map[string]string) JobOption { - return func(j *batchv1.Job) { - if j.Annotations == nil { - j.Annotations = make(map[string]string) - } - for k, v := range annotations { - j.Annotations[k] = v - } - } -} - -// WithJobConfigMapEnvFrom adds an envFrom reference to a ConfigMap. -func WithJobConfigMapEnvFrom(name string) JobOption { - return func(j *batchv1.Job) { - j.Spec.Template.Spec.Containers[0].EnvFrom = append( - j.Spec.Template.Spec.Containers[0].EnvFrom, - corev1.EnvFromSource{ - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - ) - } -} - -// WithJobSecretEnvFrom adds an envFrom reference to a Secret. -func WithJobSecretEnvFrom(name string) JobOption { - return func(j *batchv1.Job) { - j.Spec.Template.Spec.Containers[0].EnvFrom = append( - j.Spec.Template.Spec.Containers[0].EnvFrom, - corev1.EnvFromSource{ - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - ) - } -} - -// baseJob creates a base Job template. -func baseJob(namespace, name string) *batchv1.Job { - labels := map[string]string{"app": name} - return &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: namespace, - }, - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Labels: labels, - }, - Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{ - { - Name: "job", - Image: DefaultImage, - Command: []string{"sh", "-c", "echo done"}, - }, - }, - }, - }, - }, - } -} - -// DeleteJob deletes a Job. -func DeleteJob(ctx context.Context, client kubernetes.Interface, namespace, name string) error { - propagation := metav1.DeletePropagationBackground - return client.BatchV1().Jobs(namespace).Delete(ctx, name, metav1.DeleteOptions{ - PropagationPolicy: &propagation, - }) -} - // WithConfigMapKeyRef adds a valueFrom.configMapKeyRef env var to the container. func WithConfigMapKeyRef(cmName, key, envVarName string) DeploymentOption { return func(d *appsv1.Deployment) { @@ -907,150 +515,346 @@ func WithInitContainerProjectedVolume(cmName, secretName string) DeploymentOptio } } -// WithDaemonSetProjectedVolume adds a projected volume with ConfigMap and/or Secret sources to a DaemonSet. -func WithDaemonSetProjectedVolume(cmName, secretName string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - volumeName := "projected-config" - sources := []corev1.VolumeProjection{} +// WithCSIVolume adds a CSI volume referencing a SecretProviderClass to a Deployment. +func WithCSIVolume(spcName string) DeploymentOption { + return func(d *appsv1.Deployment) { + volumeName := csiVolumeName(spcName) + mountPath := csiMountPath(spcName) - if cmName != "" { - sources = append(sources, corev1.VolumeProjection{ - ConfigMap: &corev1.ConfigMapProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - }, - }) - } - if secretName != "" { - sources = append(sources, corev1.VolumeProjection{ - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, - }, - }) - } - - ds.Spec.Template.Spec.Volumes = append(ds.Spec.Template.Spec.Volumes, corev1.Volume{ + d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{ Name: volumeName, VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{ - Sources: sources, + CSI: &corev1.CSIVolumeSource{ + Driver: CSIDriverName, + ReadOnly: ptr.To(true), + VolumeAttributes: map[string]string{ + "secretProviderClass": spcName, + }, }, }, }) - ds.Spec.Template.Spec.Containers[0].VolumeMounts = append( - ds.Spec.Template.Spec.Containers[0].VolumeMounts, + d.Spec.Template.Spec.Containers[0].VolumeMounts = append( + d.Spec.Template.Spec.Containers[0].VolumeMounts, corev1.VolumeMount{ Name: volumeName, - MountPath: "/etc/projected", + MountPath: mountPath, + ReadOnly: true, }, ) } } -// WithStatefulSetProjectedVolume adds a projected volume with ConfigMap and/or Secret sources to a StatefulSet. -func WithStatefulSetProjectedVolume(cmName, secretName string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - volumeName := "projected-config" - sources := []corev1.VolumeProjection{} +// WithInitContainerCSIVolume adds an init container with a CSI volume mount. +func WithInitContainerCSIVolume(spcName string) DeploymentOption { + return func(d *appsv1.Deployment) { + volumeName := csiVolumeName(spcName) + mountPath := csiMountPath(spcName) - if cmName != "" { - sources = append(sources, corev1.VolumeProjection{ - ConfigMap: &corev1.ConfigMapProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - }, - }) + hasCSIVolume := false + for _, v := range d.Spec.Template.Spec.Volumes { + if v.Name == volumeName { + hasCSIVolume = true + break + } } - if secretName != "" { - sources = append(sources, corev1.VolumeProjection{ - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + if !hasCSIVolume { + d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + CSI: &corev1.CSIVolumeSource{ + Driver: CSIDriverName, + ReadOnly: ptr.To(true), + VolumeAttributes: map[string]string{ + "secretProviderClass": spcName, + }, + }, }, }) } - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{ - Sources: sources, + initContainer := corev1.Container{ + Name: fmt.Sprintf("init-csi-%s", spcName), + Image: DefaultImage, + Command: []string{"sh", "-c", "echo init done"}, + VolumeMounts: []corev1.VolumeMount{ + { + Name: volumeName, + MountPath: mountPath, + ReadOnly: true, }, }, - }) - ss.Spec.Template.Spec.Containers[0].VolumeMounts = append( - ss.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: "/etc/projected", - }, - ) + } + d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, initContainer) } } -// WithDaemonSetConfigMapKeyRef adds a valueFrom.configMapKeyRef env var to a DaemonSet. -func WithDaemonSetConfigMapKeyRef(cmName, key, envVarName string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - ds.Spec.Template.Spec.Containers[0].Env = append( - ds.Spec.Template.Spec.Containers[0].Env, - corev1.EnvVar{ - Name: envVarName, - ValueFrom: &corev1.EnvVarSource{ - ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - Key: key, +func baseDeploymentResource(namespace, name string) *appsv1.Deployment { + labels := map[string]string{"app": name} + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: ptr.To(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + Image: DefaultImage, + Command: []string{"sh", "-c", DefaultCommand}, + }, }, }, }, + }, + } +} + +// DeleteDeployment deletes a Deployment. +func DeleteDeployment(ctx context.Context, client kubernetes.Interface, namespace, name string) error { + return client.AppsV1().Deployments(namespace).Delete(ctx, name, metav1.DeleteOptions{}) +} + +// DaemonSetOption is a functional option for configuring a DaemonSet. +type DaemonSetOption func(*appsv1.DaemonSet) + +// CreateDaemonSet creates a DaemonSet with the given options. +func CreateDaemonSet(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...DaemonSetOption) (*appsv1.DaemonSet, error) { + ds := baseDaemonSetResource(namespace, name) + for _, opt := range opts { + opt(ds) + } + return client.AppsV1().DaemonSets(namespace).Create(ctx, ds, metav1.CreateOptions{}) +} + +// baseDaemonSetResource creates a base DaemonSet template. +func baseDaemonSetResource(namespace, name string) *appsv1.DaemonSet { + labels := map[string]string{"app": name} + return &appsv1.DaemonSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.DaemonSetSpec{ + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + Image: DefaultImage, + Command: []string{"sh", "-c", DefaultCommand}, + }, + }, + }, + }, + }, + } +} + +// DeleteDaemonSet deletes a DaemonSet. +func DeleteDaemonSet(ctx context.Context, client kubernetes.Interface, namespace, name string) error { + return client.AppsV1().DaemonSets(namespace).Delete(ctx, name, metav1.DeleteOptions{}) +} + +// StatefulSetOption is a functional option for configuring a StatefulSet. +type StatefulSetOption func(*appsv1.StatefulSet) + +// CreateStatefulSet creates a StatefulSet with the given options. +func CreateStatefulSet(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...StatefulSetOption) (*appsv1.StatefulSet, error) { + ss := baseStatefulSetResource(namespace, name) + for _, opt := range opts { + opt(ss) + } + return client.AppsV1().StatefulSets(namespace).Create(ctx, ss, metav1.CreateOptions{}) +} + +// baseStatefulSetResource creates a base StatefulSet template. +func baseStatefulSetResource(namespace, name string) *appsv1.StatefulSet { + labels := map[string]string{"app": name} + return &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: appsv1.StatefulSetSpec{ + ServiceName: name, + Replicas: ptr.To(int32(1)), + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "app", + Image: DefaultImage, + Command: []string{"sh", "-c", DefaultCommand}, + }, + }, + }, + }, + }, + } +} + +// DeleteStatefulSet deletes a StatefulSet. +func DeleteStatefulSet(ctx context.Context, client kubernetes.Interface, namespace, name string) error { + return client.AppsV1().StatefulSets(namespace).Delete(ctx, name, metav1.DeleteOptions{}) +} + +// CronJobOption is a functional option for configuring a CronJob. +type CronJobOption func(*batchv1.CronJob) + +// CreateCronJob creates a CronJob with the given options. +func CreateCronJob(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...CronJobOption) (*batchv1.CronJob, error) { + cj := baseCronJobResource(namespace, name) + for _, opt := range opts { + opt(cj) + } + return client.BatchV1().CronJobs(namespace).Create(ctx, cj, metav1.CreateOptions{}) +} + +// WithCronJobAnnotations adds annotations to the CronJob metadata. +func WithCronJobAnnotations(annotations map[string]string) CronJobOption { + return func(cj *batchv1.CronJob) { + if cj.Annotations == nil { + cj.Annotations = make(map[string]string) + } + for k, v := range annotations { + cj.Annotations[k] = v + } + } +} + +// WithCronJobConfigMapEnvFrom adds an envFrom reference to a ConfigMap. +func WithCronJobConfigMapEnvFrom(name string) CronJobOption { + return func(cj *batchv1.CronJob) { + cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom = append( + cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom, + corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + }, + }, ) } } -// WithDaemonSetSecretKeyRef adds a valueFrom.secretKeyRef env var to a DaemonSet. -func WithDaemonSetSecretKeyRef(secretName, key, envVarName string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - ds.Spec.Template.Spec.Containers[0].Env = append( - ds.Spec.Template.Spec.Containers[0].Env, - corev1.EnvVar{ - Name: envVarName, - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, - Key: key, - }, +// WithCronJobSecretEnvFrom adds an envFrom reference to a Secret. +func WithCronJobSecretEnvFrom(name string) CronJobOption { + return func(cj *batchv1.CronJob) { + cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom = append( + cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom, + corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, }, }, ) } } -// WithStatefulSetConfigMapKeyRef adds a valueFrom.configMapKeyRef env var to a StatefulSet. -func WithStatefulSetConfigMapKeyRef(cmName, key, envVarName string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - ss.Spec.Template.Spec.Containers[0].Env = append( - ss.Spec.Template.Spec.Containers[0].Env, - corev1.EnvVar{ - Name: envVarName, - ValueFrom: &corev1.EnvVarSource{ - ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - Key: key, +// baseCronJobResource creates a base CronJob template. +func baseCronJobResource(namespace, name string) *batchv1.CronJob { + labels := map[string]string{"app": name} + return &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: batchv1.CronJobSpec{ + Schedule: "* * * * *", // Every minute + JobTemplate: batchv1.JobTemplateSpec{ + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyOnFailure, + Containers: []corev1.Container{ + { + Name: "job", + Image: DefaultImage, + Command: []string{"sh", "-c", "echo done"}, + }, + }, + }, }, }, }, + }, + } +} + +// DeleteCronJob deletes a CronJob. +func DeleteCronJob(ctx context.Context, client kubernetes.Interface, namespace, name string) error { + return client.BatchV1().CronJobs(namespace).Delete(ctx, name, metav1.DeleteOptions{}) +} + +// JobOption is a functional option for configuring a Job. +type JobOption func(*batchv1.Job) + +// CreateJob creates a Job with the given options. +func CreateJob(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...JobOption) (*batchv1.Job, error) { + job := baseJobResource(namespace, name) + for _, opt := range opts { + opt(job) + } + return client.BatchV1().Jobs(namespace).Create(ctx, job, metav1.CreateOptions{}) +} + +// WithJobAnnotations adds annotations to the Job metadata. +func WithJobAnnotations(annotations map[string]string) JobOption { + return func(j *batchv1.Job) { + if j.Annotations == nil { + j.Annotations = make(map[string]string) + } + for k, v := range annotations { + j.Annotations[k] = v + } + } +} + +// WithJobConfigMapEnvFrom adds an envFrom reference to a ConfigMap. +func WithJobConfigMapEnvFrom(name string) JobOption { + return func(j *batchv1.Job) { + j.Spec.Template.Spec.Containers[0].EnvFrom = append( + j.Spec.Template.Spec.Containers[0].EnvFrom, + corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, + }, + }, ) } } -// WithStatefulSetSecretKeyRef adds a valueFrom.secretKeyRef env var to a StatefulSet. -func WithStatefulSetSecretKeyRef(secretName, key, envVarName string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - ss.Spec.Template.Spec.Containers[0].Env = append( - ss.Spec.Template.Spec.Containers[0].Env, - corev1.EnvVar{ - Name: envVarName, - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, - Key: key, - }, +// WithJobSecretEnvFrom adds an envFrom reference to a Secret. +func WithJobSecretEnvFrom(name string) JobOption { + return func(j *batchv1.Job) { + j.Spec.Template.Spec.Containers[0].EnvFrom = append( + j.Spec.Template.Spec.Containers[0].EnvFrom, + corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: name}, }, }, ) @@ -1092,3 +896,47 @@ func WithJobSecretKeyRef(secretName, key, envVarName string) JobOption { ) } } + +// baseJobResource creates a base Job template. +func baseJobResource(namespace, name string) *batchv1.Job { + labels := map[string]string{"app": name} + return &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + Spec: corev1.PodSpec{ + RestartPolicy: corev1.RestartPolicyNever, + Containers: []corev1.Container{ + { + Name: "job", + Image: DefaultImage, + Command: []string{"sh", "-c", "echo done"}, + }, + }, + }, + }, + }, + } +} + +// DeleteJob deletes a Job. +func DeleteJob(ctx context.Context, client kubernetes.Interface, namespace, name string) error { + propagation := metav1.DeletePropagationBackground + return client.BatchV1().Jobs(namespace).Delete(ctx, name, metav1.DeleteOptions{ + PropagationPolicy: &propagation, + }) +} + +func csiVolumeName(spcName string) string { + return fmt.Sprintf("csi-%s", spcName) +} + +func csiMountPath(spcName string) string { + return fmt.Sprintf("/mnt/secrets-store/%s", spcName) +} diff --git a/test/e2e/utils/testenv.go b/test/e2e/utils/testenv.go index f405073..9b4e6a9 100644 --- a/test/e2e/utils/testenv.go +++ b/test/e2e/utils/testenv.go @@ -4,12 +4,15 @@ import ( "context" "fmt" - . "github.com/onsi/ginkgo/v2" + rolloutsclient "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + "github.com/onsi/ginkgo/v2" + openshiftclient "github.com/openshift/client-go/apps/clientset/versioned" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/discovery" - "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + csiclient "sigs.k8s.io/secrets-store-csi-driver/pkg/client/clientset/versioned" ) // TestEnvironment holds the common test environment state. @@ -17,10 +20,13 @@ type TestEnvironment struct { Ctx context.Context Cancel context.CancelFunc KubeClient kubernetes.Interface - DynamicClient dynamic.Interface DiscoveryClient discovery.DiscoveryInterface + CSIClient csiclient.Interface + RolloutsClient rolloutsclient.Interface + OpenShiftClient openshiftclient.Interface + RestConfig *rest.Config Namespace string - ReleaseName string // Unique Helm release name to prevent cluster-scoped resource conflicts + ReleaseName string TestImage string ProjectDir string } @@ -35,56 +41,69 @@ func SetupTestEnvironment(ctx context.Context, namespacePrefix string) (*TestEnv var err error - // Get project directory env.ProjectDir, err = GetProjectDir() if err != nil { return nil, fmt.Errorf("getting project directory: %w", err) } - // Setup Kubernetes client kubeconfig := GetKubeconfig() - GinkgoWriter.Printf("Using kubeconfig: %s\n", kubeconfig) + ginkgo.GinkgoWriter.Printf("Using kubeconfig: %s\n", kubeconfig) config, err := clientcmd.BuildConfigFromFlags("", kubeconfig) if err != nil { return nil, fmt.Errorf("building config from kubeconfig: %w", err) } + env.RestConfig = config + env.KubeClient, err = kubernetes.NewForConfig(config) if err != nil { return nil, fmt.Errorf("creating kubernetes client: %w", err) } - env.DynamicClient, err = dynamic.NewForConfig(config) - if err != nil { - return nil, fmt.Errorf("creating dynamic client: %w", err) - } - env.DiscoveryClient, err = discovery.NewDiscoveryClientForConfig(config) if err != nil { return nil, fmt.Errorf("creating discovery client: %w", err) } - // Verify cluster connectivity - GinkgoWriter.Println("Verifying cluster connectivity...") + env.CSIClient, err = csiclient.NewForConfig(config) + if err != nil { + ginkgo.GinkgoWriter.Printf("Warning: Could not create CSI client: %v (CSI tests will be skipped)\n", err) + env.CSIClient = nil + } + + // Try to create Argo Rollouts client (optional - may not be installed) + env.RolloutsClient, err = rolloutsclient.NewForConfig(config) + if err != nil { + ginkgo.GinkgoWriter.Printf("Warning: Could not create Rollouts client: %v (Argo tests will be skipped)\n", err) + env.RolloutsClient = nil + } + + // Try to create OpenShift client (optional - may not be installed) + env.OpenShiftClient, err = openshiftclient.NewForConfig(config) + if err != nil { + ginkgo.GinkgoWriter.Printf("Warning: Could not create OpenShift client: %v (OpenShift tests will be skipped)\n", + err) + env.OpenShiftClient = nil + } + + ginkgo.GinkgoWriter.Println("Verifying cluster connectivity...") _, err = env.KubeClient.CoreV1().Namespaces().List(ctx, metav1.ListOptions{Limit: 1}) if err != nil { return nil, fmt.Errorf("connecting to kubernetes cluster: %w", err) } - GinkgoWriter.Println("Cluster connectivity verified") + ginkgo.GinkgoWriter.Println("Cluster connectivity verified") - // Create test namespace with random suffix env.Namespace = RandName(namespacePrefix) - // Use a unique release name to prevent cluster-scoped resource conflicts between test suites env.ReleaseName = RandName("reloader") - GinkgoWriter.Printf("Creating test namespace: %s\n", env.Namespace) - GinkgoWriter.Printf("Using Helm release name: %s\n", env.ReleaseName) + ginkgo.GinkgoWriter.Printf("Creating test namespace: %s\n", env.Namespace) + ginkgo.GinkgoWriter.Printf("Using Helm release name: %s\n", env.ReleaseName) if err := CreateNamespace(ctx, env.KubeClient, env.Namespace); err != nil { return nil, fmt.Errorf("creating test namespace: %w", err) } - GinkgoWriter.Printf("Using test image: %s\n", env.TestImage) - GinkgoWriter.Printf("Project directory: %s\n", env.ProjectDir) + ginkgo.GinkgoWriter.Printf("Using test image: %s\n", env.TestImage) + ginkgo.GinkgoWriter.Printf("Project directory: %s\n", env.ProjectDir) return env, nil } @@ -95,20 +114,17 @@ func (e *TestEnvironment) Cleanup() error { return nil } - GinkgoWriter.Printf("Cleaning up test namespace: %s\n", e.Namespace) - GinkgoWriter.Printf("Cleaning up Helm release: %s\n", e.ReleaseName) + ginkgo.GinkgoWriter.Printf("Cleaning up test namespace: %s\n", e.Namespace) + ginkgo.GinkgoWriter.Printf("Cleaning up Helm release: %s\n", e.ReleaseName) - // Collect Reloader logs before cleanup (useful for debugging) logs, err := GetPodLogs(e.Ctx, e.KubeClient, e.Namespace, ReloaderPodSelector(e.ReleaseName)) if err == nil && logs != "" { - GinkgoWriter.Println("Reloader logs:") - GinkgoWriter.Println(logs) + ginkgo.GinkgoWriter.Println("Reloader logs:") + ginkgo.GinkgoWriter.Println(logs) } - // Undeploy Reloader using the suite's release name _ = UndeployReloader(e.Namespace, e.ReleaseName) - // Delete test namespace if err := DeleteNamespace(e.Ctx, e.KubeClient, e.Namespace); err != nil { return fmt.Errorf("deleting namespace: %w", err) } @@ -118,27 +134,32 @@ func (e *TestEnvironment) Cleanup() error { // DeployReloaderWithStrategy deploys Reloader with the specified reload strategy. func (e *TestEnvironment) DeployReloaderWithStrategy(strategy string) error { - return e.DeployReloaderWithValues(map[string]string{ - "reloader.reloadStrategy": strategy, - }) + return e.DeployReloaderWithValues( + map[string]string{ + "reloader.reloadStrategy": strategy, + }, + ) } // DeployReloaderWithValues deploys Reloader with the specified Helm values. // Each test suite uses a unique release name to prevent cluster-scoped resource conflicts. func (e *TestEnvironment) DeployReloaderWithValues(values map[string]string) error { - GinkgoWriter.Printf("Deploying Reloader with values: %v\n", values) - return DeployReloader(DeployOptions{ - Namespace: e.Namespace, - ReleaseName: e.ReleaseName, - Image: e.TestImage, - Values: values, - }) + ginkgo.GinkgoWriter.Printf("Deploying Reloader with values: %v\n", values) + return DeployReloader( + DeployOptions{ + Namespace: e.Namespace, + ReleaseName: e.ReleaseName, + Image: e.TestImage, + Values: values, + }, + ) } // WaitForReloader waits for the Reloader deployment to be ready. func (e *TestEnvironment) WaitForReloader() error { - GinkgoWriter.Println("Waiting for Reloader to be ready...") - return WaitForDeploymentReady(e.Ctx, e.KubeClient, e.Namespace, ReloaderDeploymentName(e.ReleaseName), DeploymentReady) + ginkgo.GinkgoWriter.Println("Waiting for Reloader to be ready...") + return WaitForDeploymentReady(e.Ctx, e.KubeClient, e.Namespace, ReloaderDeploymentName(e.ReleaseName), + DeploymentReady) } // DeployAndWait deploys Reloader with the given values and waits for it to be ready. @@ -149,6 +170,6 @@ func (e *TestEnvironment) DeployAndWait(values map[string]string) error { if err := e.WaitForReloader(); err != nil { return fmt.Errorf("waiting for Reloader: %w", err) } - GinkgoWriter.Println("Reloader is ready") + ginkgo.GinkgoWriter.Println("Reloader is ready") return nil } diff --git a/test/e2e/utils/wait.go b/test/e2e/utils/wait.go index 7d77b56..e0b54d0 100644 --- a/test/e2e/utils/wait.go +++ b/test/e2e/utils/wait.go @@ -2,6 +2,7 @@ package utils import ( "context" + "errors" "fmt" "strings" "time" @@ -16,7 +17,6 @@ import ( // Timeout and interval constants for polling operations. const ( - DefaultTimeout = 30 * time.Second // General operations DefaultInterval = 1 * time.Second // Polling interval (faster feedback) ShortTimeout = 5 * time.Second // Quick checks NegativeTestWait = 3 * time.Second // Wait before checking negative conditions @@ -26,179 +26,100 @@ const ( // WaitForDeploymentReady waits for a deployment to have all replicas available. func WaitForDeploymentReady(ctx context.Context, client kubernetes.Interface, namespace, name string, timeout time.Duration) error { - return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep polling - } + return wait.PollUntilContextTimeout( + ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil + } - // Check if deployment is ready - if deploy.Status.ReadyReplicas == *deploy.Spec.Replicas && - deploy.Status.UpdatedReplicas == *deploy.Spec.Replicas && - deploy.Status.AvailableReplicas == *deploy.Spec.Replicas { - return true, nil - } + if deploy.Status.ReadyReplicas == *deploy.Spec.Replicas && + deploy.Status.UpdatedReplicas == *deploy.Spec.Replicas && + deploy.Status.AvailableReplicas == *deploy.Spec.Replicas { + return true, nil + } - return false, nil - }) + return false, nil + }, + ) } // WaitForDeploymentReloaded waits for a deployment's pod template to have the reloader annotation. // Returns true if the annotation was found, false if timeout occurred. func WaitForDeploymentReloaded(ctx context.Context, client kubernetes.Interface, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForAnnotation(ctx, func(ctx context.Context) (map[string]string, error) { deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil // Keep polling + return nil, err } - - // Check pod template annotations - if deploy.Spec.Template.Annotations != nil { - if _, ok := deploy.Spec.Template.Annotations[annotationKey]; ok { - found = true - return true, nil - } - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil + return deploy.Spec.Template.Annotations, nil + }, annotationKey, timeout) } // WaitForDaemonSetReloaded waits for a DaemonSet's pod template to have the reloader annotation. func WaitForDaemonSetReloaded(ctx context.Context, client kubernetes.Interface, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForAnnotation(ctx, func(ctx context.Context) (map[string]string, error) { ds, err := client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil + return nil, err } - - if ds.Spec.Template.Annotations != nil { - if _, ok := ds.Spec.Template.Annotations[annotationKey]; ok { - found = true - return true, nil - } - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil + return ds.Spec.Template.Annotations, nil + }, annotationKey, timeout) } // WaitForStatefulSetReloaded waits for a StatefulSet's pod template to have the reloader annotation. func WaitForStatefulSetReloaded(ctx context.Context, client kubernetes.Interface, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForAnnotation(ctx, func(ctx context.Context) (map[string]string, error) { ss, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil + return nil, err } - - if ss.Spec.Template.Annotations != nil { - if _, ok := ss.Spec.Template.Annotations[annotationKey]; ok { - found = true - return true, nil - } - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil + return ss.Spec.Template.Annotations, nil + }, annotationKey, timeout) } // WaitForCronJobReloaded waits for a CronJob's pod template to have the reloader annotation. func WaitForCronJobReloaded(ctx context.Context, client kubernetes.Interface, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForAnnotation(ctx, func(ctx context.Context) (map[string]string, error) { cj, err := client.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil + return nil, err } - - if cj.Spec.JobTemplate.Spec.Template.Annotations != nil { - if _, ok := cj.Spec.JobTemplate.Spec.Template.Annotations[annotationKey]; ok { - found = true - return true, nil - } - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil -} - -// WaitForJobCreated waits for a Job to be created with the given label selector. -func WaitForJobCreated(ctx context.Context, client kubernetes.Interface, namespace, labelSelector string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - jobs, err := client.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{ - LabelSelector: labelSelector, - }) - if err != nil { - return false, nil - } - - if len(jobs.Items) > 0 { - found = true - return true, nil - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil + return cj.Spec.JobTemplate.Spec.Template.Annotations, nil + }, annotationKey, timeout) } // WaitForCronJobTriggeredJob waits for a Job to be created by the specified CronJob. // It checks owner references to find Jobs created by Reloader's manual trigger. -func WaitForCronJobTriggeredJob(ctx context.Context, client kubernetes.Interface, namespace, cronJobName string, timeout time.Duration) (bool, error) { +func WaitForCronJobTriggeredJob(ctx context.Context, client kubernetes.Interface, namespace, cronJobName string, timeout time.Duration) ( + bool, error, +) { var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - jobs, err := client.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{}) - if err != nil { - return false, nil - } + err := wait.PollUntilContextTimeout( + ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + jobs, err := client.BatchV1().Jobs(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return false, nil + } - for _, job := range jobs.Items { - // Check if this job is owned by the CronJob - for _, ownerRef := range job.OwnerReferences { - if ownerRef.Kind == "CronJob" && ownerRef.Name == cronJobName { - // Check for the manual instantiate annotation (added by Reloader) - if job.Annotations != nil { - if _, ok := job.Annotations["cronjob.kubernetes.io/instantiate"]; ok { - found = true - return true, nil + for _, job := range jobs.Items { + for _, ownerRef := range job.OwnerReferences { + if ownerRef.Kind == "CronJob" && ownerRef.Name == cronJobName { + if job.Annotations != nil { + if _, ok := job.Annotations["cronjob.kubernetes.io/instantiate"]; ok { + found = true + return true, nil + } } } } } - } - return false, nil - }) + return false, nil + }, + ) - if err != nil && err != context.DeadlineExceeded { + if err != nil && !errors.Is(err, context.DeadlineExceeded) { return false, err } return found, nil @@ -207,160 +128,96 @@ func WaitForCronJobTriggeredJob(ctx context.Context, client kubernetes.Interface // WaitForDeploymentEnvVar waits for a deployment's containers to have an environment variable // with the given prefix (e.g., "STAKATER_"). func WaitForDeploymentEnvVar(ctx context.Context, client kubernetes.Interface, namespace, name, prefix string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForEnvVarPrefix(ctx, func(ctx context.Context) ([]corev1.Container, error) { deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil + return nil, err } - - if hasEnvVarWithPrefix(deploy.Spec.Template.Spec.Containers, prefix) { - found = true - return true, nil - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil + return deploy.Spec.Template.Spec.Containers, nil + }, prefix, timeout) } // WaitForDaemonSetEnvVar waits for a DaemonSet's containers to have an environment variable // with the given prefix. func WaitForDaemonSetEnvVar(ctx context.Context, client kubernetes.Interface, namespace, name, prefix string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForEnvVarPrefix(ctx, func(ctx context.Context) ([]corev1.Container, error) { ds, err := client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil + return nil, err } - - if hasEnvVarWithPrefix(ds.Spec.Template.Spec.Containers, prefix) { - found = true - return true, nil - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil + return ds.Spec.Template.Spec.Containers, nil + }, prefix, timeout) } // WaitForStatefulSetEnvVar waits for a StatefulSet's containers to have an environment variable // with the given prefix. func WaitForStatefulSetEnvVar(ctx context.Context, client kubernetes.Interface, namespace, name, prefix string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForEnvVarPrefix(ctx, func(ctx context.Context) ([]corev1.Container, error) { ss, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil + return nil, err } - - if hasEnvVarWithPrefix(ss.Spec.Template.Spec.Containers, prefix) { - found = true - return true, nil - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil + return ss.Spec.Template.Spec.Containers, nil + }, prefix, timeout) } // WaitForDeploymentPaused waits for a deployment to have the paused-at annotation. func WaitForDeploymentPaused(ctx context.Context, client kubernetes.Interface, namespace, name, pausedAtAnnotation string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForAnnotation(ctx, func(ctx context.Context) (map[string]string, error) { deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil + return nil, err } - - // Check deployment annotations (not pod template) - if deploy.Annotations != nil { - if _, ok := deploy.Annotations[pausedAtAnnotation]; ok { - found = true - return true, nil - } - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil + return deploy.Annotations, nil + }, pausedAtAnnotation, timeout) } // WaitForDeploymentUnpaused waits for a deployment to NOT have the paused-at annotation. func WaitForDeploymentUnpaused(ctx context.Context, client kubernetes.Interface, namespace, name, pausedAtAnnotation string, timeout time.Duration) (bool, error) { - var unpaused bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + return WaitForNoAnnotation(ctx, func(ctx context.Context) (map[string]string, error) { deploy, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { - return false, nil + return nil, err } - - // Check if paused-at annotation is gone - if deploy.Annotations == nil { - unpaused = true - return true, nil - } - if _, ok := deploy.Annotations[pausedAtAnnotation]; !ok { - unpaused = true - return true, nil - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return unpaused, nil + return deploy.Annotations, nil + }, pausedAtAnnotation, timeout) } // WaitForDaemonSetReady waits for a DaemonSet to have all pods ready. func WaitForDaemonSetReady(ctx context.Context, client kubernetes.Interface, namespace, name string, timeout time.Duration) error { - return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - ds, err := client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { + return wait.PollUntilContextTimeout( + ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + ds, err := client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil + } + + if ds.Status.DesiredNumberScheduled > 0 && + ds.Status.NumberReady == ds.Status.DesiredNumberScheduled { + return true, nil + } + return false, nil - } - - if ds.Status.DesiredNumberScheduled > 0 && - ds.Status.NumberReady == ds.Status.DesiredNumberScheduled { - return true, nil - } - - return false, nil - }) + }, + ) } // WaitForStatefulSetReady waits for a StatefulSet to have all replicas ready. func WaitForStatefulSetReady(ctx context.Context, client kubernetes.Interface, namespace, name string, timeout time.Duration) error { - return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - ss, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { + return wait.PollUntilContextTimeout( + ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + ss, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil + } + + if ss.Status.ReadyReplicas == *ss.Spec.Replicas { + return true, nil + } + return false, nil - } - - if ss.Status.ReadyReplicas == *ss.Spec.Replicas { - return true, nil - } - - return false, nil - }) + }, + ) } // GetDeployment retrieves a deployment by name. @@ -368,31 +225,18 @@ func GetDeployment(ctx context.Context, client kubernetes.Interface, namespace, return client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) } -// GetDaemonSet retrieves a DaemonSet by name. -func GetDaemonSet(ctx context.Context, client kubernetes.Interface, namespace, name string) (*appsv1.DaemonSet, error) { - return client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) -} - -// GetStatefulSet retrieves a StatefulSet by name. -func GetStatefulSet(ctx context.Context, client kubernetes.Interface, namespace, name string) (*appsv1.StatefulSet, error) { - return client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) -} - -// GetCronJob retrieves a CronJob by name. -func GetCronJob(ctx context.Context, client kubernetes.Interface, namespace, name string) (*batchv1.CronJob, error) { - return client.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) -} - // WaitForCronJobExists waits for a CronJob to exist in the cluster. // This is useful for giving Reloader time to detect and index the CronJob before making changes. func WaitForCronJobExists(ctx context.Context, client kubernetes.Interface, namespace, name string, timeout time.Duration) error { - return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - _, err := client.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep polling - } - return true, nil - }) + return wait.PollUntilContextTimeout( + ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + _, err := client.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil + } + return true, nil + }, + ) } // GetJob retrieves a Job by name. @@ -400,82 +244,57 @@ func GetJob(ctx context.Context, client kubernetes.Interface, namespace, name st return client.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) } -// hasEnvVarWithPrefix checks if any container has an environment variable with the given prefix. -func hasEnvVarWithPrefix(containers []corev1.Container, prefix string) bool { - for _, container := range containers { - for _, env := range container.Env { - if strings.HasPrefix(env.Name, prefix) { - return true - } - } - } - return false -} - // WaitForJobRecreated waits for a Job to be deleted and recreated with a new UID. // Returns the new Job's UID if recreation was detected. -func WaitForJobRecreated(ctx context.Context, client kubernetes.Interface, namespace, name, originalUID string, timeout time.Duration) (string, bool, error) { +func WaitForJobRecreated(ctx context.Context, client kubernetes.Interface, namespace, name, originalUID string, timeout time.Duration) ( + string, bool, error, +) { var newUID string var recreated bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - job, err := client.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - // Job not found means it's been deleted, keep polling for recreation + err := wait.PollUntilContextTimeout( + ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + job, err := client.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil + } + + if string(job.UID) != originalUID { + newUID = string(job.UID) + recreated = true + return true, nil + } + return false, nil - } + }, + ) - // Check if the UID has changed (indicating recreation) - if string(job.UID) != originalUID { - newUID = string(job.UID) - recreated = true - return true, nil - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { + if err != nil && !errors.Is(err, context.DeadlineExceeded) { return "", false, err } return newUID, recreated, nil } -// WaitForJobNotFound waits for a Job to be deleted. -func WaitForJobNotFound(ctx context.Context, client kubernetes.Interface, namespace, name string, timeout time.Duration) (bool, error) { - var deleted bool - - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - _, err := client.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - deleted = true - return true, nil - } - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return deleted, nil -} - // WaitForJobExists waits for a Job to exist in the cluster. func WaitForJobExists(ctx context.Context, client kubernetes.Interface, namespace, name string, timeout time.Duration) error { - return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - _, err := client.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep polling - } - return true, nil - }) + return wait.PollUntilContextTimeout( + ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + _, err := client.BatchV1().Jobs(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil // Keep polling + } + return true, nil + }, + ) } // GetPodLogs retrieves logs from pods matching the given label selector. func GetPodLogs(ctx context.Context, client kubernetes.Interface, namespace, labelSelector string) (string, error) { - pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{ - LabelSelector: labelSelector, - }) + pods, err := client.CoreV1().Pods(namespace).List( + ctx, metav1.ListOptions{ + LabelSelector: labelSelector, + }, + ) if err != nil { return "", fmt.Errorf("failed to list pods: %w", err) } @@ -483,9 +302,11 @@ func GetPodLogs(ctx context.Context, client kubernetes.Interface, namespace, lab var allLogs strings.Builder for _, pod := range pods.Items { for _, container := range pod.Spec.Containers { - logs, err := client.CoreV1().Pods(namespace).GetLogs(pod.Name, &corev1.PodLogOptions{ - Container: container.Name, - }).Do(ctx).Raw() + logs, err := client.CoreV1().Pods(namespace).GetLogs( + pod.Name, &corev1.PodLogOptions{ + Container: container.Name, + }, + ).Do(ctx).Raw() if err != nil { allLogs.WriteString(fmt.Sprintf("Error getting logs for %s/%s: %v\n", pod.Name, container.Name, err)) continue diff --git a/test/e2e/utils/wait_helpers.go b/test/e2e/utils/wait_helpers.go new file mode 100644 index 0000000..594ae70 --- /dev/null +++ b/test/e2e/utils/wait_helpers.go @@ -0,0 +1,87 @@ +package utils + +import ( + "context" + "errors" + "strings" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/wait" +) + +// AnnotationGetter retrieves annotations from a workload's pod template. +type AnnotationGetter func(ctx context.Context) (map[string]string, error) + +// ContainerGetter retrieves containers from a workload's pod template. +type ContainerGetter func(ctx context.Context) ([]corev1.Container, error) + +// WaitForAnnotation polls until an annotation key exists. +func WaitForAnnotation(ctx context.Context, getter AnnotationGetter, key string, timeout time.Duration) (bool, error) { + var found bool + err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + annotations, err := getter(ctx) + if err != nil { + return false, nil // Keep polling on errors + } + if annotations != nil { + if _, ok := annotations[key]; ok { + found = true + return true, nil + } + } + return false, nil + }) + if err != nil && !errors.Is(err, context.DeadlineExceeded) { + return false, err + } + return found, nil +} + +// WaitForNoAnnotation polls until an annotation key is absent. +func WaitForNoAnnotation(ctx context.Context, getter AnnotationGetter, key string, timeout time.Duration) (bool, error) { + var absent bool + err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + annotations, err := getter(ctx) + if err != nil { + return false, nil + } + if annotations == nil { + absent = true + return true, nil + } + if _, ok := annotations[key]; !ok { + absent = true + return true, nil + } + return false, nil + }) + if err != nil && !errors.Is(err, context.DeadlineExceeded) { + return false, err + } + return absent, nil +} + +// WaitForEnvVarPrefix polls until a container has an env var with given prefix. +func WaitForEnvVarPrefix(ctx context.Context, getter ContainerGetter, prefix string, timeout time.Duration) (bool, error) { + var found bool + err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + containers, err := getter(ctx) + if err != nil { + return false, nil + } + for _, container := range containers { + for _, env := range container.Env { + if strings.HasPrefix(env.Name, prefix) { + found = true + return true, nil + } + } + } + return false, nil + }) + if err != nil && !errors.Is(err, context.DeadlineExceeded) { + return false, err + } + return found, nil +} diff --git a/test/e2e/utils/workload_adapter.go b/test/e2e/utils/workload_adapter.go index f8374d8..0b42836 100644 --- a/test/e2e/utils/workload_adapter.go +++ b/test/e2e/utils/workload_adapter.go @@ -4,7 +4,6 @@ import ( "context" "time" - "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" ) @@ -31,14 +30,10 @@ const ( // WorkloadConfig holds configuration for workload creation. type WorkloadConfig struct { - // Resource references - ConfigMapName string - SecretName string - - // Annotations to set on the workload - Annotations map[string]string - - // Reference methods (flags - multiple can be true) + ConfigMapName string + SecretName string + SPCName string + Annotations map[string]string UseConfigMapEnvFrom bool UseSecretEnvFrom bool UseConfigMapVolume bool @@ -48,14 +43,12 @@ type WorkloadConfig struct { UseSecretKeyRef bool UseInitContainer bool UseInitContainerVolume bool - - // For valueFrom references - ConfigMapKey string - SecretKey string - EnvVarName string - - // Special options - MultipleContainers int // Number of containers (0 or 1 means single container) + UseCSIVolume bool + UseInitContainerCSI bool + ConfigMapKey string + SecretKey string + EnvVarName string + MultipleContainers int } // WorkloadAdapter provides a unified interface for all workload types. @@ -92,34 +85,27 @@ type WorkloadAdapter interface { // AdapterRegistry holds adapters for all workload types. type AdapterRegistry struct { - kubeClient kubernetes.Interface - dynamicClient dynamic.Interface - adapters map[WorkloadType]WorkloadAdapter + kubeClient kubernetes.Interface + adapters map[WorkloadType]WorkloadAdapter } // NewAdapterRegistry creates a new adapter registry with all standard adapters. -func NewAdapterRegistry(kubeClient kubernetes.Interface, dynamicClient dynamic.Interface) *AdapterRegistry { +func NewAdapterRegistry(kubeClient kubernetes.Interface) *AdapterRegistry { r := &AdapterRegistry{ - kubeClient: kubeClient, - dynamicClient: dynamicClient, - adapters: make(map[WorkloadType]WorkloadAdapter), + kubeClient: kubeClient, + adapters: make(map[WorkloadType]WorkloadAdapter), } - // Register standard adapters r.adapters[WorkloadDeployment] = NewDeploymentAdapter(kubeClient) r.adapters[WorkloadDaemonSet] = NewDaemonSetAdapter(kubeClient) r.adapters[WorkloadStatefulSet] = NewStatefulSetAdapter(kubeClient) r.adapters[WorkloadCronJob] = NewCronJobAdapter(kubeClient) r.adapters[WorkloadJob] = NewJobAdapter(kubeClient) - // Argo and OpenShift adapters are registered separately via RegisterAdapter - // as they require specific cluster support - return r } // RegisterAdapter registers a custom adapter for a workload type. -// Use this to add Argo Rollout or DeploymentConfig adapters. func (r *AdapterRegistry) RegisterAdapter(adapter WorkloadAdapter) { r.adapters[adapter.Type()] = adapter } diff --git a/test/e2e/utils/workload_argo.go b/test/e2e/utils/workload_argo.go index b2f37f7..32ee45e 100644 --- a/test/e2e/utils/workload_argo.go +++ b/test/e2e/utils/workload_argo.go @@ -2,24 +2,27 @@ package utils import ( "context" - "fmt" - "strings" + "errors" "time" + rolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + rolloutsclient "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/dynamic" + "k8s.io/utils/ptr" ) // ArgoRolloutAdapter implements WorkloadAdapter for Argo Rollouts. type ArgoRolloutAdapter struct { - dynamicClient dynamic.Interface + rolloutsClient rolloutsclient.Interface } // NewArgoRolloutAdapter creates a new ArgoRolloutAdapter. -func NewArgoRolloutAdapter(dynamicClient dynamic.Interface) *ArgoRolloutAdapter { - return &ArgoRolloutAdapter{dynamicClient: dynamicClient} +func NewArgoRolloutAdapter(rolloutsClient rolloutsclient.Interface) *ArgoRolloutAdapter { + return &ArgoRolloutAdapter{ + rolloutsClient: rolloutsClient, + } } // Type returns the workload type. @@ -29,28 +32,33 @@ func (a *ArgoRolloutAdapter) Type() WorkloadType { // Create creates an Argo Rollout with the given config. func (a *ArgoRolloutAdapter) Create(ctx context.Context, namespace, name string, cfg WorkloadConfig) error { + rollout := baseRollout(name) opts := buildRolloutOptions(cfg) - return CreateArgoRollout(ctx, a.dynamicClient, namespace, name, opts...) + for _, opt := range opts { + opt(rollout) + } + _, err := a.rolloutsClient.ArgoprojV1alpha1().Rollouts(namespace).Create(ctx, rollout, metav1.CreateOptions{}) + return err } // Delete removes the Argo Rollout. func (a *ArgoRolloutAdapter) Delete(ctx context.Context, namespace, name string) error { - return DeleteArgoRollout(ctx, a.dynamicClient, namespace, name) + return a.rolloutsClient.ArgoprojV1alpha1().Rollouts(namespace).Delete(ctx, name, metav1.DeleteOptions{}) } // WaitReady waits for the Argo Rollout to be ready. func (a *ArgoRolloutAdapter) WaitReady(ctx context.Context, namespace, name string, timeout time.Duration) error { - return WaitForRolloutReady(ctx, a.dynamicClient, namespace, name, timeout) + return WaitForRolloutReady(ctx, a.rolloutsClient, namespace, name, timeout) } // WaitReloaded waits for the Argo Rollout to have the reload annotation. func (a *ArgoRolloutAdapter) WaitReloaded(ctx context.Context, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { - return WaitForRolloutReloaded(ctx, a.dynamicClient, namespace, name, annotationKey, timeout) + return WaitForRolloutReloaded(ctx, a.rolloutsClient, namespace, name, annotationKey, timeout) } // WaitEnvVar waits for the Argo Rollout to have a STAKATER_ env var. func (a *ArgoRolloutAdapter) WaitEnvVar(ctx context.Context, namespace, name, prefix string, timeout time.Duration) (bool, error) { - return WaitForRolloutEnvVar(ctx, a.dynamicClient, namespace, name, prefix, timeout) + return WaitForRolloutEnvVar(ctx, a.rolloutsClient, namespace, name, prefix, timeout) } // SupportsEnvVarStrategy returns true as Argo Rollouts support env var reload strategy. @@ -63,277 +71,118 @@ func (a *ArgoRolloutAdapter) RequiresSpecialHandling() bool { return false } +// baseRollout returns a minimal Rollout template. +func baseRollout(name string) *rolloutv1alpha1.Rollout { + return &rolloutv1alpha1.Rollout{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: rolloutv1alpha1.RolloutSpec{ + Replicas: ptr.To[int32](1), + Selector: &metav1.LabelSelector{ + MatchLabels: map[string]string{"app": name}, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": name}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "main", + Image: DefaultImage, + Command: []string{"sh", "-c", DefaultCommand}, + }}, + }, + }, + Strategy: rolloutv1alpha1.RolloutStrategy{ + Canary: &rolloutv1alpha1.CanaryStrategy{ + Steps: []rolloutv1alpha1.CanaryStep{ + {SetWeight: ptr.To[int32](100)}, + }, + }, + }, + }, + } +} + // buildRolloutOptions converts WorkloadConfig to RolloutOption slice. func buildRolloutOptions(cfg WorkloadConfig) []RolloutOption { - var opts []RolloutOption - - // Add annotations (to pod template) - if len(cfg.Annotations) > 0 { - opts = append(opts, WithRolloutAnnotations(cfg.Annotations)) - } - - // Add envFrom references - if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" { - opts = append(opts, WithRolloutConfigMapEnvFrom(cfg.ConfigMapName)) - } - if cfg.UseSecretEnvFrom && cfg.SecretName != "" { - opts = append(opts, WithRolloutSecretEnvFrom(cfg.SecretName)) - } - - // Add volume mounts - if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" { - opts = append(opts, WithRolloutConfigMapVolume(cfg.ConfigMapName)) - } - if cfg.UseSecretVolume && cfg.SecretName != "" { - opts = append(opts, WithRolloutSecretVolume(cfg.SecretName)) - } - - // Add projected volume - if cfg.UseProjectedVolume { - opts = append(opts, WithRolloutProjectedVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add valueFrom references - if cfg.UseConfigMapKeyRef && cfg.ConfigMapName != "" { - key := cfg.ConfigMapKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "CONFIG_VAR" - } - opts = append(opts, WithRolloutConfigMapKeyRef(cfg.ConfigMapName, key, envVar)) - } - if cfg.UseSecretKeyRef && cfg.SecretName != "" { - key := cfg.SecretKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "SECRET_VAR" - } - opts = append(opts, WithRolloutSecretKeyRef(cfg.SecretName, key, envVar)) - } - - // Add init container with envFrom - if cfg.UseInitContainer { - opts = append(opts, WithRolloutInitContainer(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add init container with volume mount - if cfg.UseInitContainerVolume { - opts = append(opts, WithRolloutInitContainerVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - return opts -} - -// WithRolloutProjectedVolume adds a projected volume with ConfigMap and/or Secret sources to a Rollout. -func WithRolloutProjectedVolume(cmName, secretName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - volumeName := "projected-config" - sources := []interface{}{} - - if cmName != "" { - sources = append(sources, map[string]interface{}{ - "configMap": map[string]interface{}{ - "name": cmName, - }, - }) - } - if secretName != "" { - sources = append(sources, map[string]interface{}{ - "secret": map[string]interface{}{ - "name": secretName, - }, - }) - } - - // Add volume - volumes, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "volumes") - volumes = append(volumes, map[string]interface{}{ - "name": volumeName, - "projected": map[string]interface{}{ - "sources": sources, - }, - }) - _ = unstructured.SetNestedSlice(rollout.Object, volumes, "spec", "template", "spec", "volumes") - - // Add volumeMount - containers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - volumeMounts, _, _ := unstructured.NestedSlice(container, "volumeMounts") - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": volumeName, - "mountPath": "/etc/projected", - }) - container["volumeMounts"] = volumeMounts - containers[0] = container - _ = unstructured.SetNestedSlice(rollout.Object, containers, "spec", "template", "spec", "containers") - } + return []RolloutOption{ + func(r *rolloutv1alpha1.Rollout) { + // Set annotations on Rollout level (where Reloader checks them) + if len(cfg.Annotations) > 0 { + if r.Annotations == nil { + r.Annotations = make(map[string]string) + } + for k, v := range cfg.Annotations { + r.Annotations[k] = v + } + } + ApplyWorkloadConfig(&r.Spec.Template.Spec, cfg) + }, } } -// WithRolloutConfigMapKeyRef adds an env var with valueFrom.configMapKeyRef to a Rollout. -func WithRolloutConfigMapKeyRef(cmName, key, envVarName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - containers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - env, _, _ := unstructured.NestedSlice(container, "env") - env = append(env, map[string]interface{}{ - "name": envVarName, - "valueFrom": map[string]interface{}{ - "configMapKeyRef": map[string]interface{}{ - "name": cmName, - "key": key, - }, - }, - }) - container["env"] = env - containers[0] = container - _ = unstructured.SetNestedSlice(rollout.Object, containers, "spec", "template", "spec", "containers") - } - } -} - -// WithRolloutSecretKeyRef adds an env var with valueFrom.secretKeyRef to a Rollout. -func WithRolloutSecretKeyRef(secretName, key, envVarName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - containers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - env, _, _ := unstructured.NestedSlice(container, "env") - env = append(env, map[string]interface{}{ - "name": envVarName, - "valueFrom": map[string]interface{}{ - "secretKeyRef": map[string]interface{}{ - "name": secretName, - "key": key, - }, - }, - }) - container["env"] = env - containers[0] = container - _ = unstructured.SetNestedSlice(rollout.Object, containers, "spec", "template", "spec", "containers") - } - } -} - -// WithRolloutInitContainer adds an init container that references ConfigMap and/or Secret. -func WithRolloutInitContainer(cmName, secretName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - initContainer := map[string]interface{}{ - "name": "init", - "image": DefaultImage, - "command": []interface{}{"sh", "-c", "echo init done"}, - } - - envFrom := []interface{}{} - if cmName != "" { - envFrom = append(envFrom, map[string]interface{}{ - "configMapRef": map[string]interface{}{ - "name": cmName, - }, - }) - } - if secretName != "" { - envFrom = append(envFrom, map[string]interface{}{ - "secretRef": map[string]interface{}{ - "name": secretName, - }, - }) - } - if len(envFrom) > 0 { - initContainer["envFrom"] = envFrom - } - - initContainers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "initContainers") - initContainers = append(initContainers, initContainer) - _ = unstructured.SetNestedSlice(rollout.Object, initContainers, "spec", "template", "spec", "initContainers") - } -} - -// WithRolloutInitContainerVolume adds an init container with ConfigMap/Secret volume mounts. -func WithRolloutInitContainerVolume(cmName, secretName string) RolloutOption { - return func(rollout *unstructured.Unstructured) { - initContainer := map[string]interface{}{ - "name": "init", - "image": DefaultImage, - "command": []interface{}{"sh", "-c", "echo init done"}, - } - - volumeMounts := []interface{}{} - volumes, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "volumes") - - if cmName != "" { - volumeName := fmt.Sprintf("init-cm-%s", cmName) - volumes = append(volumes, map[string]interface{}{ - "name": volumeName, - "configMap": map[string]interface{}{ - "name": cmName, - }, - }) - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": volumeName, - "mountPath": fmt.Sprintf("/etc/init-config/%s", cmName), - }) - } - if secretName != "" { - volumeName := fmt.Sprintf("init-secret-%s", secretName) - volumes = append(volumes, map[string]interface{}{ - "name": volumeName, - "secret": map[string]interface{}{ - "secretName": secretName, - }, - }) - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": volumeName, - "mountPath": fmt.Sprintf("/etc/init-secrets/%s", secretName), - }) - } - - if len(volumeMounts) > 0 { - initContainer["volumeMounts"] = volumeMounts - } - - _ = unstructured.SetNestedSlice(rollout.Object, volumes, "spec", "template", "spec", "volumes") - - initContainers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "initContainers") - initContainers = append(initContainers, initContainer) - _ = unstructured.SetNestedSlice(rollout.Object, initContainers, "spec", "template", "spec", "initContainers") - } -} - -// WaitForRolloutEnvVar waits for an Argo Rollout's container to have an env var with the given prefix. -func WaitForRolloutEnvVar(ctx context.Context, dynamicClient dynamic.Interface, namespace, name, prefix string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - rollout, err := dynamicClient.Resource(ArgoRolloutGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) +// WaitForRolloutReady waits for an Argo Rollout to be ready using typed client. +func WaitForRolloutReady(ctx context.Context, client rolloutsclient.Interface, namespace, name string, timeout time.Duration) error { + return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + rollout, err := client.ArgoprojV1alpha1().Rollouts(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return false, nil } - containers, _, _ := unstructured.NestedSlice(rollout.Object, "spec", "template", "spec", "containers") - for _, c := range containers { - container := c.(map[string]interface{}) - env, _, _ := unstructured.NestedSlice(container, "env") - for _, e := range env { - envVar := e.(map[string]interface{}) - if name, ok := envVar["name"].(string); ok && strings.HasPrefix(name, prefix) { - found = true - return true, nil - } - } + // Check status.phase == "Healthy" or replicas == availableReplicas + if rollout.Status.Phase == rolloutv1alpha1.RolloutPhaseHealthy { + return true, nil + } + + if rollout.Spec.Replicas != nil && *rollout.Spec.Replicas > 0 && + rollout.Status.AvailableReplicas == *rollout.Spec.Replicas { + return true, nil + } + + return false, nil + }) +} + +// WaitForRolloutReloaded waits for an Argo Rollout's pod template to have the reloader annotation. +func WaitForRolloutReloaded(ctx context.Context, client rolloutsclient.Interface, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { + return WaitForAnnotation(ctx, func(ctx context.Context) (map[string]string, error) { + rollout, err := client.ArgoprojV1alpha1().Rollouts(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return rollout.Spec.Template.Annotations, nil + }, annotationKey, timeout) +} + +// WaitForRolloutEnvVar waits for an Argo Rollout's container to have an env var with the given prefix. +func WaitForRolloutEnvVar(ctx context.Context, client rolloutsclient.Interface, namespace, name, prefix string, timeout time.Duration) (bool, error) { + return WaitForEnvVarPrefix(ctx, func(ctx context.Context) ([]corev1.Container, error) { + rollout, err := client.ArgoprojV1alpha1().Rollouts(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + return rollout.Spec.Template.Spec.Containers, nil + }, prefix, timeout) +} + +// WaitForRolloutRestartAt waits for an Argo Rollout's spec.restartAt field to be set. +func WaitForRolloutRestartAt(ctx context.Context, client rolloutsclient.Interface, namespace, name string, timeout time.Duration) (bool, error) { + var found bool + err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + rollout, err := client.ArgoprojV1alpha1().Rollouts(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil + } + + if rollout.Spec.RestartAt != nil && !rollout.Spec.RestartAt.IsZero() { + found = true + return true, nil } return false, nil }) - if err != nil && err != context.DeadlineExceeded { + if err != nil && !errors.Is(err, context.DeadlineExceeded) { return false, err } return found, nil diff --git a/test/e2e/utils/workload_cronjob.go b/test/e2e/utils/workload_cronjob.go index 00d85e5..6b74bfd 100644 --- a/test/e2e/utils/workload_cronjob.go +++ b/test/e2e/utils/workload_cronjob.go @@ -5,11 +5,7 @@ import ( "time" batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // CronJobAdapter implements WorkloadAdapter for Kubernetes CronJobs. @@ -45,9 +41,6 @@ func (a *CronJobAdapter) WaitReady(ctx context.Context, namespace, name string, } // WaitReloaded waits for the CronJob to have the reload annotation OR for a triggered Job. -// For CronJobs, Reloader can either: -// 1. Add an annotation to the pod template -// 2. Trigger a new Job (which is the special handling case) func (a *CronJobAdapter) WaitReloaded(ctx context.Context, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { return WaitForCronJobReloaded(ctx, a.client, namespace, name, annotationKey, timeout) } @@ -75,149 +68,18 @@ func (a *CronJobAdapter) WaitForTriggeredJob(ctx context.Context, namespace, cro // buildCronJobOptions converts WorkloadConfig to CronJobOption slice. func buildCronJobOptions(cfg WorkloadConfig) []CronJobOption { - var opts []CronJobOption - - // Add annotations - if len(cfg.Annotations) > 0 { - opts = append(opts, WithCronJobAnnotations(cfg.Annotations)) - } - - // Add envFrom references - if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" { - opts = append(opts, WithCronJobConfigMapEnvFrom(cfg.ConfigMapName)) - } - if cfg.UseSecretEnvFrom && cfg.SecretName != "" { - opts = append(opts, WithCronJobSecretEnvFrom(cfg.SecretName)) - } - - // Add volume mounts - if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" { - opts = append(opts, WithCronJobConfigMapVolume(cfg.ConfigMapName)) - } - if cfg.UseSecretVolume && cfg.SecretName != "" { - opts = append(opts, WithCronJobSecretVolume(cfg.SecretName)) - } - - // Add projected volume - if cfg.UseProjectedVolume { - opts = append(opts, WithCronJobProjectedVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - return opts -} - -// WithCronJobConfigMapVolume adds a volume mount for a ConfigMap to a CronJob. -func WithCronJobConfigMapVolume(name string) CronJobOption { - return func(cj *batchv1.CronJob) { - volumeName := "cm-" + name - cj.Spec.JobTemplate.Spec.Template.Spec.Volumes = append( - cj.Spec.JobTemplate.Spec.Template.Spec.Volumes, - corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - }, - ) - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts = append( - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: "/etc/config/" + name, - }, - ) + return []CronJobOption{ + func(cj *batchv1.CronJob) { + // Set annotations on CronJob level (where Reloader checks them) + if len(cfg.Annotations) > 0 { + if cj.Annotations == nil { + cj.Annotations = make(map[string]string) + } + for k, v := range cfg.Annotations { + cj.Annotations[k] = v + } + } + ApplyWorkloadConfig(&cj.Spec.JobTemplate.Spec.Template.Spec, cfg) + }, } } - -// WithCronJobSecretVolume adds a volume mount for a Secret to a CronJob. -func WithCronJobSecretVolume(name string) CronJobOption { - return func(cj *batchv1.CronJob) { - volumeName := "secret-" + name - cj.Spec.JobTemplate.Spec.Template.Spec.Volumes = append( - cj.Spec.JobTemplate.Spec.Template.Spec.Volumes, - corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: name, - }, - }, - }, - ) - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts = append( - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: "/etc/secrets/" + name, - }, - ) - } -} - -// WithCronJobProjectedVolume adds a projected volume with ConfigMap and/or Secret sources to a CronJob. -func WithCronJobProjectedVolume(cmName, secretName string) CronJobOption { - return func(cj *batchv1.CronJob) { - volumeName := "projected-config" - sources := []corev1.VolumeProjection{} - - if cmName != "" { - sources = append(sources, corev1.VolumeProjection{ - ConfigMap: &corev1.ConfigMapProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - }, - }) - } - if secretName != "" { - sources = append(sources, corev1.VolumeProjection{ - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, - }, - }) - } - - cj.Spec.JobTemplate.Spec.Template.Spec.Volumes = append( - cj.Spec.JobTemplate.Spec.Template.Spec.Volumes, - corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{ - Sources: sources, - }, - }, - }, - ) - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts = append( - cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: "/etc/projected", - }, - ) - } -} - -// WaitForCronJobEnvVar waits for a CronJob's containers to have an environment variable -// with the given prefix. Note: CronJobs don't typically use this strategy. -func WaitForCronJobEnvVar(ctx context.Context, client kubernetes.Interface, namespace, name, prefix string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - cj, err := client.BatchV1().CronJobs(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil - } - - if hasEnvVarWithPrefix(cj.Spec.JobTemplate.Spec.Template.Spec.Containers, prefix) { - found = true - return true, nil - } - - return false, nil - }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil -} diff --git a/test/e2e/utils/workload_daemonset.go b/test/e2e/utils/workload_daemonset.go index 8d4d55b..12e54ab 100644 --- a/test/e2e/utils/workload_daemonset.go +++ b/test/e2e/utils/workload_daemonset.go @@ -2,11 +2,9 @@ package utils import ( "context" - "fmt" "time" appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -64,183 +62,18 @@ func (a *DaemonSetAdapter) RequiresSpecialHandling() bool { // buildDaemonSetOptions converts WorkloadConfig to DaemonSetOption slice. func buildDaemonSetOptions(cfg WorkloadConfig) []DaemonSetOption { - var opts []DaemonSetOption - - // Add annotations - if len(cfg.Annotations) > 0 { - opts = append(opts, WithDaemonSetAnnotations(cfg.Annotations)) - } - - // Add envFrom references - if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" { - opts = append(opts, WithDaemonSetConfigMapEnvFrom(cfg.ConfigMapName)) - } - if cfg.UseSecretEnvFrom && cfg.SecretName != "" { - opts = append(opts, WithDaemonSetSecretEnvFrom(cfg.SecretName)) - } - - // Add volume mounts - if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" { - opts = append(opts, WithDaemonSetConfigMapVolume(cfg.ConfigMapName)) - } - if cfg.UseSecretVolume && cfg.SecretName != "" { - opts = append(opts, WithDaemonSetSecretVolume(cfg.SecretName)) - } - - // Add projected volume - if cfg.UseProjectedVolume { - opts = append(opts, WithDaemonSetProjectedVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add valueFrom references - if cfg.UseConfigMapKeyRef && cfg.ConfigMapName != "" { - key := cfg.ConfigMapKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "CONFIG_VAR" - } - opts = append(opts, WithDaemonSetConfigMapKeyRef(cfg.ConfigMapName, key, envVar)) - } - if cfg.UseSecretKeyRef && cfg.SecretName != "" { - key := cfg.SecretKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "SECRET_VAR" - } - opts = append(opts, WithDaemonSetSecretKeyRef(cfg.SecretName, key, envVar)) - } - - // Add init container with envFrom - if cfg.UseInitContainer { - opts = append(opts, WithDaemonSetInitContainer(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add init container with volume mount - if cfg.UseInitContainerVolume { - opts = append(opts, WithDaemonSetInitContainerVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - return opts -} - -// WithDaemonSetConfigMapVolume adds a volume mount for a ConfigMap to a DaemonSet. -func WithDaemonSetConfigMapVolume(name string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - volumeName := fmt.Sprintf("cm-%s", name) - ds.Spec.Template.Spec.Volumes = append(ds.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - }) - ds.Spec.Template.Spec.Containers[0].VolumeMounts = append( - ds.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: fmt.Sprintf("/etc/config/%s", name), - }, - ) - } -} - -// WithDaemonSetSecretVolume adds a volume mount for a Secret to a DaemonSet. -func WithDaemonSetSecretVolume(name string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - volumeName := fmt.Sprintf("secret-%s", name) - ds.Spec.Template.Spec.Volumes = append(ds.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: name, - }, - }, - }) - ds.Spec.Template.Spec.Containers[0].VolumeMounts = append( - ds.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: fmt.Sprintf("/etc/secrets/%s", name), - }, - ) - } -} - -// WithDaemonSetInitContainer adds an init container that references ConfigMap and/or Secret. -func WithDaemonSetInitContainer(cmName, secretName string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - initContainer := corev1.Container{ - Name: "init", - Image: DefaultImage, - Command: []string{"sh", "-c", "echo init done"}, - } - - if cmName != "" { - initContainer.EnvFrom = append(initContainer.EnvFrom, corev1.EnvFromSource{ - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - }, - }) - } - if secretName != "" { - initContainer.EnvFrom = append(initContainer.EnvFrom, corev1.EnvFromSource{ - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, - }, - }) - } - - ds.Spec.Template.Spec.InitContainers = append(ds.Spec.Template.Spec.InitContainers, initContainer) - } -} - -// WithDaemonSetInitContainerVolume adds an init container with ConfigMap/Secret volume mounts. -func WithDaemonSetInitContainerVolume(cmName, secretName string) DaemonSetOption { - return func(ds *appsv1.DaemonSet) { - initContainer := corev1.Container{ - Name: "init", - Image: DefaultImage, - Command: []string{"sh", "-c", "echo init done"}, - } - - if cmName != "" { - volumeName := fmt.Sprintf("init-cm-%s", cmName) - ds.Spec.Template.Spec.Volumes = append(ds.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - }, - }, - }) - initContainer.VolumeMounts = append(initContainer.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: fmt.Sprintf("/etc/init-config/%s", cmName), - }) - } - if secretName != "" { - volumeName := fmt.Sprintf("init-secret-%s", secretName) - ds.Spec.Template.Spec.Volumes = append(ds.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: secretName, - }, - }, - }) - initContainer.VolumeMounts = append(initContainer.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: fmt.Sprintf("/etc/init-secrets/%s", secretName), - }) - } - - ds.Spec.Template.Spec.InitContainers = append(ds.Spec.Template.Spec.InitContainers, initContainer) + return []DaemonSetOption{ + func(ds *appsv1.DaemonSet) { + // Set annotations on DaemonSet level (where Reloader checks them) + if len(cfg.Annotations) > 0 { + if ds.Annotations == nil { + ds.Annotations = make(map[string]string) + } + for k, v := range cfg.Annotations { + ds.Annotations[k] = v + } + } + ApplyWorkloadConfig(&ds.Spec.Template.Spec, cfg) + }, } } diff --git a/test/e2e/utils/workload_deployment.go b/test/e2e/utils/workload_deployment.go index 951ba79..3a28231 100644 --- a/test/e2e/utils/workload_deployment.go +++ b/test/e2e/utils/workload_deployment.go @@ -4,6 +4,7 @@ import ( "context" "time" + appsv1 "k8s.io/api/apps/v1" "k8s.io/client-go/kubernetes" ) @@ -61,72 +62,18 @@ func (a *DeploymentAdapter) RequiresSpecialHandling() bool { // buildDeploymentOptions converts WorkloadConfig to DeploymentOption slice. func buildDeploymentOptions(cfg WorkloadConfig) []DeploymentOption { - var opts []DeploymentOption - - // Add annotations - if len(cfg.Annotations) > 0 { - opts = append(opts, WithAnnotations(cfg.Annotations)) + return []DeploymentOption{ + func(d *appsv1.Deployment) { + // Set annotations on deployment level (where Reloader checks them) + if len(cfg.Annotations) > 0 { + if d.Annotations == nil { + d.Annotations = make(map[string]string) + } + for k, v := range cfg.Annotations { + d.Annotations[k] = v + } + } + ApplyWorkloadConfig(&d.Spec.Template.Spec, cfg) + }, } - - // Add envFrom references - if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" { - opts = append(opts, WithConfigMapEnvFrom(cfg.ConfigMapName)) - } - if cfg.UseSecretEnvFrom && cfg.SecretName != "" { - opts = append(opts, WithSecretEnvFrom(cfg.SecretName)) - } - - // Add volume mounts - if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" { - opts = append(opts, WithConfigMapVolume(cfg.ConfigMapName)) - } - if cfg.UseSecretVolume && cfg.SecretName != "" { - opts = append(opts, WithSecretVolume(cfg.SecretName)) - } - - // Add projected volume - if cfg.UseProjectedVolume { - opts = append(opts, WithProjectedVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add valueFrom references - if cfg.UseConfigMapKeyRef && cfg.ConfigMapName != "" { - key := cfg.ConfigMapKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "CONFIG_VAR" - } - opts = append(opts, WithConfigMapKeyRef(cfg.ConfigMapName, key, envVar)) - } - if cfg.UseSecretKeyRef && cfg.SecretName != "" { - key := cfg.SecretKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "SECRET_VAR" - } - opts = append(opts, WithSecretKeyRef(cfg.SecretName, key, envVar)) - } - - // Add init container with envFrom - if cfg.UseInitContainer { - opts = append(opts, WithInitContainer(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add init container with volume mount - if cfg.UseInitContainerVolume { - opts = append(opts, WithInitContainerVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add multiple containers - if cfg.MultipleContainers > 1 { - opts = append(opts, WithMultipleContainers(cfg.MultipleContainers)) - } - - return opts } diff --git a/test/e2e/utils/workload_job.go b/test/e2e/utils/workload_job.go index d2a405e..15ecaa7 100644 --- a/test/e2e/utils/workload_job.go +++ b/test/e2e/utils/workload_job.go @@ -5,7 +5,6 @@ import ( "time" batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -83,125 +82,18 @@ func (a *JobAdapter) WaitForRecreation(ctx context.Context, namespace, name, ori // buildJobOptions converts WorkloadConfig to JobOption slice. func buildJobOptions(cfg WorkloadConfig) []JobOption { - var opts []JobOption - - // Add annotations - if len(cfg.Annotations) > 0 { - opts = append(opts, WithJobAnnotations(cfg.Annotations)) - } - - // Add envFrom references - if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" { - opts = append(opts, WithJobConfigMapEnvFrom(cfg.ConfigMapName)) - } - if cfg.UseSecretEnvFrom && cfg.SecretName != "" { - opts = append(opts, WithJobSecretEnvFrom(cfg.SecretName)) - } - - // Add volume mounts - if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" { - opts = append(opts, WithJobConfigMapVolume(cfg.ConfigMapName)) - } - if cfg.UseSecretVolume && cfg.SecretName != "" { - opts = append(opts, WithJobSecretVolume(cfg.SecretName)) - } - - // Add projected volume - if cfg.UseProjectedVolume { - opts = append(opts, WithJobProjectedVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - return opts -} - -// WithJobConfigMapVolume adds a volume mount for a ConfigMap to a Job. -func WithJobConfigMapVolume(name string) JobOption { - return func(j *batchv1.Job) { - volumeName := "cm-" + name - j.Spec.Template.Spec.Volumes = append( - j.Spec.Template.Spec.Volumes, - corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - }, - ) - j.Spec.Template.Spec.Containers[0].VolumeMounts = append( - j.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: "/etc/config/" + name, - }, - ) - } -} - -// WithJobSecretVolume adds a volume mount for a Secret to a Job. -func WithJobSecretVolume(name string) JobOption { - return func(j *batchv1.Job) { - volumeName := "secret-" + name - j.Spec.Template.Spec.Volumes = append( - j.Spec.Template.Spec.Volumes, - corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: name, - }, - }, - }, - ) - j.Spec.Template.Spec.Containers[0].VolumeMounts = append( - j.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: "/etc/secrets/" + name, - }, - ) - } -} - -// WithJobProjectedVolume adds a projected volume with ConfigMap and/or Secret sources to a Job. -func WithJobProjectedVolume(cmName, secretName string) JobOption { - return func(j *batchv1.Job) { - volumeName := "projected-config" - sources := []corev1.VolumeProjection{} - - if cmName != "" { - sources = append(sources, corev1.VolumeProjection{ - ConfigMap: &corev1.ConfigMapProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - }, - }) - } - if secretName != "" { - sources = append(sources, corev1.VolumeProjection{ - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, - }, - }) - } - - j.Spec.Template.Spec.Volumes = append( - j.Spec.Template.Spec.Volumes, - corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{ - Sources: sources, - }, - }, - }, - ) - j.Spec.Template.Spec.Containers[0].VolumeMounts = append( - j.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: "/etc/projected", - }, - ) + return []JobOption{ + func(job *batchv1.Job) { + // Set annotations on Job level (where Reloader checks them) + if len(cfg.Annotations) > 0 { + if job.Annotations == nil { + job.Annotations = make(map[string]string) + } + for k, v := range cfg.Annotations { + job.Annotations[k] = v + } + } + ApplyWorkloadConfig(&job.Spec.Template.Spec, cfg) + }, } } diff --git a/test/e2e/utils/workload_openshift.go b/test/e2e/utils/workload_openshift.go index e4e2455..9fd6866 100644 --- a/test/e2e/utils/workload_openshift.go +++ b/test/e2e/utils/workload_openshift.go @@ -2,24 +2,28 @@ package utils import ( "context" - "fmt" - "strings" "time" + openshiftappsv1 "github.com/openshift/api/apps/v1" + openshiftclient "github.com/openshift/client-go/apps/clientset/versioned" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/util/wait" - "k8s.io/client-go/dynamic" ) +// DCOption is a function that modifies a DeploymentConfig. +type DCOption func(*openshiftappsv1.DeploymentConfig) + // DeploymentConfigAdapter implements WorkloadAdapter for OpenShift DeploymentConfigs. type DeploymentConfigAdapter struct { - dynamicClient dynamic.Interface + openshiftClient openshiftclient.Interface } // NewDeploymentConfigAdapter creates a new DeploymentConfigAdapter. -func NewDeploymentConfigAdapter(dynamicClient dynamic.Interface) *DeploymentConfigAdapter { - return &DeploymentConfigAdapter{dynamicClient: dynamicClient} +func NewDeploymentConfigAdapter(openshiftClient openshiftclient.Interface) *DeploymentConfigAdapter { + return &DeploymentConfigAdapter{ + openshiftClient: openshiftClient, + } } // Type returns the workload type. @@ -29,28 +33,33 @@ func (a *DeploymentConfigAdapter) Type() WorkloadType { // Create creates a DeploymentConfig with the given config. func (a *DeploymentConfigAdapter) Create(ctx context.Context, namespace, name string, cfg WorkloadConfig) error { - opts := buildDCOptions(cfg) - return CreateDeploymentConfig(ctx, a.dynamicClient, namespace, name, opts...) + dc := baseDeploymentConfig(name) + opts := buildDeploymentConfigOptions(cfg) + for _, opt := range opts { + opt(dc) + } + _, err := a.openshiftClient.AppsV1().DeploymentConfigs(namespace).Create(ctx, dc, metav1.CreateOptions{}) + return err } // Delete removes the DeploymentConfig. func (a *DeploymentConfigAdapter) Delete(ctx context.Context, namespace, name string) error { - return DeleteDeploymentConfig(ctx, a.dynamicClient, namespace, name) + return a.openshiftClient.AppsV1().DeploymentConfigs(namespace).Delete(ctx, name, metav1.DeleteOptions{}) } // WaitReady waits for the DeploymentConfig to be ready. func (a *DeploymentConfigAdapter) WaitReady(ctx context.Context, namespace, name string, timeout time.Duration) error { - return WaitForDeploymentConfigReady(ctx, a.dynamicClient, namespace, name, timeout) + return WaitForDeploymentConfigReady(ctx, a.openshiftClient, namespace, name, timeout) } // WaitReloaded waits for the DeploymentConfig to have the reload annotation. func (a *DeploymentConfigAdapter) WaitReloaded(ctx context.Context, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { - return WaitForDeploymentConfigReloaded(ctx, a.dynamicClient, namespace, name, annotationKey, timeout) + return WaitForDeploymentConfigReloaded(ctx, a.openshiftClient, namespace, name, annotationKey, timeout) } // WaitEnvVar waits for the DeploymentConfig to have a STAKATER_ env var. func (a *DeploymentConfigAdapter) WaitEnvVar(ctx context.Context, namespace, name, prefix string, timeout time.Duration) (bool, error) { - return WaitForDeploymentConfigEnvVar(ctx, a.dynamicClient, namespace, name, prefix, timeout) + return WaitForDeploymentConfigEnvVar(ctx, a.openshiftClient, namespace, name, prefix, timeout) } // SupportsEnvVarStrategy returns true as DeploymentConfigs support env var reload strategy. @@ -63,278 +72,92 @@ func (a *DeploymentConfigAdapter) RequiresSpecialHandling() bool { return false } -// buildDCOptions converts WorkloadConfig to DCOption slice. -func buildDCOptions(cfg WorkloadConfig) []DCOption { - var opts []DCOption - - // Add annotations (to pod template) - if len(cfg.Annotations) > 0 { - opts = append(opts, WithDCAnnotations(cfg.Annotations)) - } - - // Add envFrom references - if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" { - opts = append(opts, WithDCConfigMapEnvFrom(cfg.ConfigMapName)) - } - if cfg.UseSecretEnvFrom && cfg.SecretName != "" { - opts = append(opts, WithDCSecretEnvFrom(cfg.SecretName)) - } - - // Add volume mounts - if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" { - opts = append(opts, WithDCConfigMapVolume(cfg.ConfigMapName)) - } - if cfg.UseSecretVolume && cfg.SecretName != "" { - opts = append(opts, WithDCSecretVolume(cfg.SecretName)) - } - - // Add projected volume - if cfg.UseProjectedVolume { - opts = append(opts, WithDCProjectedVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add valueFrom references - if cfg.UseConfigMapKeyRef && cfg.ConfigMapName != "" { - key := cfg.ConfigMapKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "CONFIG_VAR" - } - opts = append(opts, WithDCConfigMapKeyRef(cfg.ConfigMapName, key, envVar)) - } - if cfg.UseSecretKeyRef && cfg.SecretName != "" { - key := cfg.SecretKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "SECRET_VAR" - } - opts = append(opts, WithDCSecretKeyRef(cfg.SecretName, key, envVar)) - } - - // Add init container with envFrom - if cfg.UseInitContainer { - opts = append(opts, WithDCInitContainer(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add init container with volume mount - if cfg.UseInitContainerVolume { - opts = append(opts, WithDCInitContainerVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - return opts -} - -// WithDCProjectedVolume adds a projected volume with ConfigMap and/or Secret sources to a DeploymentConfig. -func WithDCProjectedVolume(cmName, secretName string) DCOption { - return func(dc *unstructured.Unstructured) { - volumeName := "projected-config" - sources := []interface{}{} - - if cmName != "" { - sources = append(sources, map[string]interface{}{ - "configMap": map[string]interface{}{ - "name": cmName, +// baseDeploymentConfig returns a minimal DeploymentConfig template. +func baseDeploymentConfig(name string) *openshiftappsv1.DeploymentConfig { + return &openshiftappsv1.DeploymentConfig{ + ObjectMeta: metav1.ObjectMeta{Name: name}, + Spec: openshiftappsv1.DeploymentConfigSpec{ + Replicas: 1, + Selector: map[string]string{"app": name}, + Template: &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": name}, }, - }) - } - if secretName != "" { - sources = append(sources, map[string]interface{}{ - "secret": map[string]interface{}{ - "name": secretName, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{{ + Name: "main", + Image: DefaultImage, + Command: []string{"sh", "-c", DefaultCommand}, + }}, }, - }) - } - - // Add volume - volumes, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "volumes") - volumes = append(volumes, map[string]interface{}{ - "name": volumeName, - "projected": map[string]interface{}{ - "sources": sources, }, - }) - _ = unstructured.SetNestedSlice(dc.Object, volumes, "spec", "template", "spec", "volumes") - - // Add volumeMount - containers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - volumeMounts, _, _ := unstructured.NestedSlice(container, "volumeMounts") - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": volumeName, - "mountPath": "/etc/projected", - }) - container["volumeMounts"] = volumeMounts - containers[0] = container - _ = unstructured.SetNestedSlice(dc.Object, containers, "spec", "template", "spec", "containers") - } + Triggers: openshiftappsv1.DeploymentTriggerPolicies{ + {Type: openshiftappsv1.DeploymentTriggerOnConfigChange}, + }, + }, } } -// WithDCConfigMapKeyRef adds an env var with valueFrom.configMapKeyRef to a DeploymentConfig. -func WithDCConfigMapKeyRef(cmName, key, envVarName string) DCOption { - return func(dc *unstructured.Unstructured) { - containers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - env, _, _ := unstructured.NestedSlice(container, "env") - env = append(env, map[string]interface{}{ - "name": envVarName, - "valueFrom": map[string]interface{}{ - "configMapKeyRef": map[string]interface{}{ - "name": cmName, - "key": key, - }, - }, - }) - container["env"] = env - containers[0] = container - _ = unstructured.SetNestedSlice(dc.Object, containers, "spec", "template", "spec", "containers") - } +// buildDeploymentConfigOptions converts WorkloadConfig to DCOption slice. +func buildDeploymentConfigOptions(cfg WorkloadConfig) []DCOption { + return []DCOption{ + func(dc *openshiftappsv1.DeploymentConfig) { + // Set annotations on DeploymentConfig level (where Reloader checks them) + if len(cfg.Annotations) > 0 { + if dc.Annotations == nil { + dc.Annotations = make(map[string]string) + } + for k, v := range cfg.Annotations { + dc.Annotations[k] = v + } + } + if dc.Spec.Template != nil { + ApplyWorkloadConfig(&dc.Spec.Template.Spec, cfg) + } + }, } } -// WithDCSecretKeyRef adds an env var with valueFrom.secretKeyRef to a DeploymentConfig. -func WithDCSecretKeyRef(secretName, key, envVarName string) DCOption { - return func(dc *unstructured.Unstructured) { - containers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "containers") - if len(containers) > 0 { - container := containers[0].(map[string]interface{}) - env, _, _ := unstructured.NestedSlice(container, "env") - env = append(env, map[string]interface{}{ - "name": envVarName, - "valueFrom": map[string]interface{}{ - "secretKeyRef": map[string]interface{}{ - "name": secretName, - "key": key, - }, - }, - }) - container["env"] = env - containers[0] = container - _ = unstructured.SetNestedSlice(dc.Object, containers, "spec", "template", "spec", "containers") - } - } -} - -// WithDCInitContainer adds an init container that references ConfigMap and/or Secret via envFrom. -func WithDCInitContainer(cmName, secretName string) DCOption { - return func(dc *unstructured.Unstructured) { - initContainer := map[string]interface{}{ - "name": "init", - "image": DefaultImage, - "command": []interface{}{"sh", "-c", "echo init done"}, - } - - envFrom := []interface{}{} - if cmName != "" { - envFrom = append(envFrom, map[string]interface{}{ - "configMapRef": map[string]interface{}{ - "name": cmName, - }, - }) - } - if secretName != "" { - envFrom = append(envFrom, map[string]interface{}{ - "secretRef": map[string]interface{}{ - "name": secretName, - }, - }) - } - if len(envFrom) > 0 { - initContainer["envFrom"] = envFrom - } - - initContainers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "initContainers") - initContainers = append(initContainers, initContainer) - _ = unstructured.SetNestedSlice(dc.Object, initContainers, "spec", "template", "spec", "initContainers") - } -} - -// WithDCInitContainerVolume adds an init container with ConfigMap/Secret volume mounts to a DeploymentConfig. -func WithDCInitContainerVolume(cmName, secretName string) DCOption { - return func(dc *unstructured.Unstructured) { - initContainer := map[string]interface{}{ - "name": "init", - "image": DefaultImage, - "command": []interface{}{"sh", "-c", "echo init done"}, - } - - volumeMounts := []interface{}{} - volumes, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "volumes") - - if cmName != "" { - volumeName := fmt.Sprintf("init-cm-%s", cmName) - volumes = append(volumes, map[string]interface{}{ - "name": volumeName, - "configMap": map[string]interface{}{ - "name": cmName, - }, - }) - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": volumeName, - "mountPath": fmt.Sprintf("/etc/init-config/%s", cmName), - }) - } - if secretName != "" { - volumeName := fmt.Sprintf("init-secret-%s", secretName) - volumes = append(volumes, map[string]interface{}{ - "name": volumeName, - "secret": map[string]interface{}{ - "secretName": secretName, - }, - }) - volumeMounts = append(volumeMounts, map[string]interface{}{ - "name": volumeName, - "mountPath": fmt.Sprintf("/etc/init-secrets/%s", secretName), - }) - } - - if len(volumeMounts) > 0 { - initContainer["volumeMounts"] = volumeMounts - } - - _ = unstructured.SetNestedSlice(dc.Object, volumes, "spec", "template", "spec", "volumes") - - initContainers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "initContainers") - initContainers = append(initContainers, initContainer) - _ = unstructured.SetNestedSlice(dc.Object, initContainers, "spec", "template", "spec", "initContainers") - } -} - -// WaitForDeploymentConfigEnvVar waits for a DeploymentConfig's container to have an env var with the given prefix. -func WaitForDeploymentConfigEnvVar(ctx context.Context, dynamicClient dynamic.Interface, namespace, name, prefix string, timeout time.Duration) (bool, error) { - var found bool - err := wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { - dc, err := dynamicClient.Resource(DeploymentConfigGVR).Namespace(namespace).Get(ctx, name, metav1.GetOptions{}) +// WaitForDeploymentConfigReady waits for a DeploymentConfig to be ready using typed client. +func WaitForDeploymentConfigReady(ctx context.Context, client openshiftclient.Interface, namespace, name string, timeout time.Duration) error { + return wait.PollUntilContextTimeout(ctx, DefaultInterval, timeout, true, func(ctx context.Context) (bool, error) { + dc, err := client.AppsV1().DeploymentConfigs(namespace).Get(ctx, name, metav1.GetOptions{}) if err != nil { return false, nil } - containers, _, _ := unstructured.NestedSlice(dc.Object, "spec", "template", "spec", "containers") - for _, c := range containers { - container := c.(map[string]interface{}) - env, _, _ := unstructured.NestedSlice(container, "env") - for _, e := range env { - envVar := e.(map[string]interface{}) - if envName, ok := envVar["name"].(string); ok && strings.HasPrefix(envName, prefix) { - found = true - return true, nil - } - } + if dc.Spec.Replicas > 0 && dc.Status.ReadyReplicas == dc.Spec.Replicas { + return true, nil } return false, nil }) - - if err != nil && err != context.DeadlineExceeded { - return false, err - } - return found, nil +} + +// WaitForDeploymentConfigReloaded waits for a DeploymentConfig's pod template to have the reloader annotation. +func WaitForDeploymentConfigReloaded(ctx context.Context, client openshiftclient.Interface, namespace, name, annotationKey string, timeout time.Duration) (bool, error) { + return WaitForAnnotation(ctx, func(ctx context.Context) (map[string]string, error) { + dc, err := client.AppsV1().DeploymentConfigs(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + if dc.Spec.Template != nil { + return dc.Spec.Template.Annotations, nil + } + return nil, nil + }, annotationKey, timeout) +} + +// WaitForDeploymentConfigEnvVar waits for a DeploymentConfig's container to have an env var with the given prefix. +func WaitForDeploymentConfigEnvVar(ctx context.Context, client openshiftclient.Interface, namespace, name, prefix string, timeout time.Duration) (bool, error) { + return WaitForEnvVarPrefix(ctx, func(ctx context.Context) ([]corev1.Container, error) { + dc, err := client.AppsV1().DeploymentConfigs(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return nil, err + } + if dc.Spec.Template != nil { + return dc.Spec.Template.Spec.Containers, nil + } + return nil, nil + }, prefix, timeout) } diff --git a/test/e2e/utils/workload_statefulset.go b/test/e2e/utils/workload_statefulset.go index fb20914..5696128 100644 --- a/test/e2e/utils/workload_statefulset.go +++ b/test/e2e/utils/workload_statefulset.go @@ -2,11 +2,9 @@ package utils import ( "context" - "fmt" "time" appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" "k8s.io/client-go/kubernetes" ) @@ -64,183 +62,18 @@ func (a *StatefulSetAdapter) RequiresSpecialHandling() bool { // buildStatefulSetOptions converts WorkloadConfig to StatefulSetOption slice. func buildStatefulSetOptions(cfg WorkloadConfig) []StatefulSetOption { - var opts []StatefulSetOption - - // Add annotations - if len(cfg.Annotations) > 0 { - opts = append(opts, WithStatefulSetAnnotations(cfg.Annotations)) - } - - // Add envFrom references - if cfg.UseConfigMapEnvFrom && cfg.ConfigMapName != "" { - opts = append(opts, WithStatefulSetConfigMapEnvFrom(cfg.ConfigMapName)) - } - if cfg.UseSecretEnvFrom && cfg.SecretName != "" { - opts = append(opts, WithStatefulSetSecretEnvFrom(cfg.SecretName)) - } - - // Add volume mounts - if cfg.UseConfigMapVolume && cfg.ConfigMapName != "" { - opts = append(opts, WithStatefulSetConfigMapVolume(cfg.ConfigMapName)) - } - if cfg.UseSecretVolume && cfg.SecretName != "" { - opts = append(opts, WithStatefulSetSecretVolume(cfg.SecretName)) - } - - // Add projected volume - if cfg.UseProjectedVolume { - opts = append(opts, WithStatefulSetProjectedVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add valueFrom references - if cfg.UseConfigMapKeyRef && cfg.ConfigMapName != "" { - key := cfg.ConfigMapKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "CONFIG_VAR" - } - opts = append(opts, WithStatefulSetConfigMapKeyRef(cfg.ConfigMapName, key, envVar)) - } - if cfg.UseSecretKeyRef && cfg.SecretName != "" { - key := cfg.SecretKey - if key == "" { - key = "key" - } - envVar := cfg.EnvVarName - if envVar == "" { - envVar = "SECRET_VAR" - } - opts = append(opts, WithStatefulSetSecretKeyRef(cfg.SecretName, key, envVar)) - } - - // Add init container with envFrom - if cfg.UseInitContainer { - opts = append(opts, WithStatefulSetInitContainer(cfg.ConfigMapName, cfg.SecretName)) - } - - // Add init container with volume mount - if cfg.UseInitContainerVolume { - opts = append(opts, WithStatefulSetInitContainerVolume(cfg.ConfigMapName, cfg.SecretName)) - } - - return opts -} - -// WithStatefulSetConfigMapVolume adds a volume mount for a ConfigMap to a StatefulSet. -func WithStatefulSetConfigMapVolume(name string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - volumeName := fmt.Sprintf("cm-%s", name) - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: name}, - }, - }, - }) - ss.Spec.Template.Spec.Containers[0].VolumeMounts = append( - ss.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: fmt.Sprintf("/etc/config/%s", name), - }, - ) - } -} - -// WithStatefulSetSecretVolume adds a volume mount for a Secret to a StatefulSet. -func WithStatefulSetSecretVolume(name string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - volumeName := fmt.Sprintf("secret-%s", name) - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: name, - }, - }, - }) - ss.Spec.Template.Spec.Containers[0].VolumeMounts = append( - ss.Spec.Template.Spec.Containers[0].VolumeMounts, - corev1.VolumeMount{ - Name: volumeName, - MountPath: fmt.Sprintf("/etc/secrets/%s", name), - }, - ) - } -} - -// WithStatefulSetInitContainer adds an init container that references ConfigMap and/or Secret. -func WithStatefulSetInitContainer(cmName, secretName string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - initContainer := corev1.Container{ - Name: "init", - Image: DefaultImage, - Command: []string{"sh", "-c", "echo init done"}, - } - - if cmName != "" { - initContainer.EnvFrom = append(initContainer.EnvFrom, corev1.EnvFromSource{ - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - }, - }) - } - if secretName != "" { - initContainer.EnvFrom = append(initContainer.EnvFrom, corev1.EnvFromSource{ - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, - }, - }) - } - - ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, initContainer) - } -} - -// WithStatefulSetInitContainerVolume adds an init container with ConfigMap/Secret volume mounts. -func WithStatefulSetInitContainerVolume(cmName, secretName string) StatefulSetOption { - return func(ss *appsv1.StatefulSet) { - initContainer := corev1.Container{ - Name: "init", - Image: DefaultImage, - Command: []string{"sh", "-c", "echo init done"}, - } - - if cmName != "" { - volumeName := fmt.Sprintf("init-cm-%s", cmName) - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: cmName}, - }, - }, - }) - initContainer.VolumeMounts = append(initContainer.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: fmt.Sprintf("/etc/init-config/%s", cmName), - }) - } - if secretName != "" { - volumeName := fmt.Sprintf("init-secret-%s", secretName) - ss.Spec.Template.Spec.Volumes = append(ss.Spec.Template.Spec.Volumes, corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: secretName, - }, - }, - }) - initContainer.VolumeMounts = append(initContainer.VolumeMounts, corev1.VolumeMount{ - Name: volumeName, - MountPath: fmt.Sprintf("/etc/init-secrets/%s", secretName), - }) - } - - ss.Spec.Template.Spec.InitContainers = append(ss.Spec.Template.Spec.InitContainers, initContainer) + return []StatefulSetOption{ + func(sts *appsv1.StatefulSet) { + // Set annotations on StatefulSet level (where Reloader checks them) + if len(cfg.Annotations) > 0 { + if sts.Annotations == nil { + sts.Annotations = make(map[string]string) + } + for k, v := range cfg.Annotations { + sts.Annotations[k] = v + } + } + ApplyWorkloadConfig(&sts.Spec.Template.Spec, cfg) + }, } }