diff --git a/.dockerignore b/.dockerignore index 6e4d4b26..56140f4d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -11,6 +11,7 @@ registry-scanner/hack registry-scanner/test scripts/ test/e2e +test/ginkgo test/testdata test/utils *.iml diff --git a/.github/workflows/ci-tests.yaml b/.github/workflows/ci-tests.yaml index 43b66398..a730427b 100644 --- a/.github/workflows/ci-tests.yaml +++ b/.github/workflows/ci-tests.yaml @@ -156,3 +156,136 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} files: coverage.out + test-e2e: + name: Run end-to-end tests + runs-on: ubuntu-latest + strategy: + matrix: + k3s-version: [ v1.27.1 ] + # k3s-version: [v1.20.2, v1.19.2, v1.18.9, v1.17.11, v1.16.15] + steps: + - name: Install K3D + run: | + set -x + curl -s https://raw.githubusercontent.com/rancher/k3d/main/install.sh | bash + sudo mkdir -p $HOME/.kube && sudo chown -R runner $HOME/.kube + k3d cluster create --servers 3 --image rancher/k3s:${{ matrix.k3s-version }}-k3s1 + kubectl version + k3d version + - name: Checkout code + uses: actions/checkout@v5 + - name: Setup Golang + uses: actions/setup-go@v6 + with: + go-version-file: 'test/ginkgo/go.mod' + - name: GH actions workaround - Kill XSP4 process + run: | + sudo pkill mono || true + - name: Restore go build cache + uses: actions/cache@v4 + with: + path: ~/.cache/go-build + key: ${{ runner.os }}-go-build-v1-${{ github.run_id }} + - name: Add /usr/local/bin to PATH + run: | + echo "/usr/local/bin" >> $GITHUB_PATH + - name: Download Go dependencies + run: | + cd test/ginkgo && go mod download + - name: Build local image updater, deploy operator + env: + ARGOCD_CLUSTER_CONFIG_NAMESPACES: argocd-e2e-cluster-config + K3D_CLUSTER_NAME: k3s-default + run: | + set -o pipefail + make -C test/ginkgo test-e2e-ci + + - name: Run ginkgo tests + run: | + set -o pipefail + make e2e-tests-parallel-ginkgo 2>&1 | tee /tmp/e2e-test-ginkgo.log + + - name: Save application controller and server logs + if: ${{ failure() }} + run: | + # Collect logs from test namespaces. The ginkgo tests use dynamically generated + # namespace names with the prefix 'gitops-e2e-test-'. + set -x + + # Find all gitops-e2e-test-* namespaces + E2E_NAMESPACES=$(kubectl get namespaces -o=name | grep 'gitops-e2e-test-' | sed 's|namespace/||' || true) + + if [ -n "$E2E_NAMESPACES" ]; then + for NS in $E2E_NAMESPACES; do + echo "--- Collecting resources from namespace $NS ---" + kubectl get all -n "$NS" >> /tmp/pods.log 2>&1 || true + + # Collect application controller logs + APP_CONTROLLER=$(kubectl get po -n "$NS" -o=name 2>/dev/null | grep argocd-application-controller || true) + if [ -n "$APP_CONTROLLER" ]; then + kubectl logs -n "$NS" "$APP_CONTROLLER" >> /tmp/e2e-application-controller.log 2>&1 || true + fi + + # Collect server logs + SERVER_POD=$(kubectl get po -n "$NS" -o=name 2>/dev/null | grep argocd-server || true) + if [ -n "$SERVER_POD" ]; then + kubectl logs -n "$NS" "$SERVER_POD" >> /tmp/e2e-server.log 2>&1 || true + kubectl describe -n "$NS" "$SERVER_POD" >> /tmp/e2e-server.log 2>&1 || true + fi + + # Collect image updater logs + IMAGE_UPDATER_POD=$(kubectl get po -n "$NS" -o=name 2>/dev/null | grep 'image-updater' || true) + if [ -n "$IMAGE_UPDATER_POD" ]; then + kubectl logs -n "$NS" "$IMAGE_UPDATER_POD" >> /tmp/e2e-image-updater.log 2>&1 || true + fi + done + fi + + # Also collect operator logs from argocd-operator-system namespace + OPERATOR_POD=$(kubectl get po -n argocd-operator-system -o=name 2>/dev/null | grep controller-manager || true) + if [ -n "$OPERATOR_POD" ]; then + echo "--- Collecting operator logs ---" + kubectl logs -n argocd-operator-system "$OPERATOR_POD" -c manager > /tmp/e2e-operator-run.log 2>&1 || true + fi + + - name: Upload operator logs + uses: actions/upload-artifact@v5 + with: + name: e2e-operator-run-${{ matrix.k3s-version }}.log + path: /tmp/e2e-operator-run.log + if: ${{ failure() }} + + - name: Upload ginkgo test logs + uses: actions/upload-artifact@v5 + with: + name: e2e-test-${{ matrix.k3s-version }}.log + path: /tmp/e2e-test-ginkgo.log + if: ${{ failure() }} + + - name: Upload application controller logs + uses: actions/upload-artifact@v5 + with: + name: e2e-application-controller-${{ matrix.k3s-version }}.log + path: /tmp/e2e-application-controller.log + if: ${{ failure() }} + + - name: Upload server logs + uses: actions/upload-artifact@v5 + with: + name: e2e-server-${{ matrix.k3s-version }}.log + path: /tmp/e2e-server.log + if: ${{ failure() }} + + - name: Upload image updater logs + uses: actions/upload-artifact@v5 + with: + name: e2e-image-updater-${{ matrix.k3s-version }}.log + path: /tmp/e2e-image-updater.log + if: ${{ failure() }} + + - name: Upload pod descriptions + uses: actions/upload-artifact@v5 + with: + name: e2e-pods-${{ matrix.k3s-version }}.log + path: /tmp/pods.log + if: ${{ failure() }} diff --git a/Makefile b/Makefile index ecaecf4a..afd78d75 100644 --- a/Makefile +++ b/Makefile @@ -243,6 +243,22 @@ golangci-lint: $(GOLANGCI_LINT) ## Download golangci-lint locally if necessary. $(GOLANGCI_LINT): $(LOCALBIN) $(call go-install-tool,$(GOLANGCI_LINT),github.com/golangci/golangci-lint/v2/cmd/golangci-lint,$(GOLANGCI_LINT_VERSION)) +## E2E +.PHONY: e2e-tests-sequential-ginkgo +e2e-tests-sequential-ginkgo: ginkgo + @echo "Running operator sequential Ginkgo E2E tests..." + $(GINKGO_CLI) -v --trace --timeout 90m -r ./test/ginkgo/sequential + +.PHONY: e2e-tests-parallel-ginkgo +e2e-tests-parallel-ginkgo: ginkgo + @echo "Running operator parallel Ginkgo E2E tests..." + $(GINKGO_CLI) -p -v -procs=5 --trace --timeout 90m -r ./test/ginkgo/parallel + +GINKGO_CLI = $(shell pwd)/bin/ginkgo +.PHONY: ginkgo +ginkgo: ## Download ginkgo locally if necessary. + $(call go-install-tool,$(GINKGO_CLI),github.com/onsi/ginkgo/v2/ginkgo,v2.27.2) + # go-install-tool will 'go install' any package with custom target and name of binary, if it doesn't exist # $1 - target path with name of binary # $2 - package url which can be installed diff --git a/test/ginkgo/Makefile b/test/ginkgo/Makefile new file mode 100644 index 00000000..1ad0bce4 --- /dev/null +++ b/test/ginkgo/Makefile @@ -0,0 +1,108 @@ +# Default image for argocd-operator. Can be overridden. +ARGOCD_OPERATOR_IMAGE ?= quay.io/argoprojlabs/argocd-operator:latest + +# Get version from root VERSION file +IMAGE_NAMESPACE?=quay.io/argoprojlabs +IMAGE_NAME=argocd-image-updater +ifdef IMAGE_NAMESPACE +IMAGE_PREFIX=${IMAGE_NAMESPACE}/ +else +IMAGE_PREFIX= +endif + +VERSION := $(shell cat ../../VERSION) +IMAGE_TAG?=v${VERSION} +# Image URL to use all building/pushing image targets +ARGOCD_IMAGE_UPDATER_IMAGE ?= ${IMAGE_PREFIX}${IMAGE_NAME}:${IMAGE_TAG} + +# Define the patch template +define PATCH_TEMPLATE +apiVersion: apps/v1 +kind: Deployment +metadata: + name: controller-manager + namespace: system +spec: + template: + spec: + containers: + - name: manager + env: + - name: ARGOCD_IMAGE_UPDATER_IMAGE + value: $(ARGOCD_IMAGE_UPDATER_IMAGE) +endef +export PATCH_TEMPLATE + +# Tools - assuming they are in the path or in the project's bin directory +KUSTOMIZE ?= $(CURDIR)/../../bin/kustomize +KUBECTL ?= kubectl +K3D ?= k3d + +K3D_CLUSTER_NAME ?= test-e2e-local + +ifndef ignore-not-found + ignore-not-found = false +endif + +.PHONY: deploy-argocd-operator kustomize test-e2e-local k3d-cluster-create k3d-cluster-delete k3d-image-import +kustomize: + $(MAKE) -C ../../ kustomize + +deploy-argocd-operator: kustomize ## Deploy argocd-operator from a stable git reference. + @echo "Deploying Argo CD Operator..." + @set -e; \ + TMP_DIR=$$(mktemp -d); \ + cp prereqs/kustomization.yaml $$TMP_DIR/kustomization.yaml; \ + echo "Applying argocd-operator manifests with image $(ARGOCD_OPERATOR_IMAGE)..."; \ + echo "Setting argocd-image-updater image to $(ARGOCD_IMAGE_UPDATER_IMAGE)..."; \ + echo "$$PATCH_TEMPLATE" > $$TMP_DIR/patch.yaml; \ + cd $$TMP_DIR && \ + $(KUSTOMIZE) edit set image quay.io/argoprojlabs/argocd-operator=$(ARGOCD_OPERATOR_IMAGE) && \ + $(KUSTOMIZE) edit add patch --path patch.yaml; \ + $(KUSTOMIZE) build $$TMP_DIR | $(KUBECTL) apply --server-side=true -f -; \ + rm -rf $$TMP_DIR; \ + echo "Argo CD Operator deployment initiated."; + +undeploy-argocd-operator: kustomize ## Deploy argocd-operator from a stable git reference. + @echo "Undeploying Argo CD Operator..." + @set -e; \ + TMP_DIR=$$(mktemp -d); \ + cp prereqs/kustomization.yaml $$TMP_DIR/kustomization.yaml; \ + $(KUSTOMIZE) build $$TMP_DIR | $(KUBECTL) delete --ignore-not-found=$(ignore-not-found) -f -; \ + rm -rf $$TMP_DIR; \ + echo "Argo CD Operator undeployment initiated."; + +k3d-cluster-create: ## Create k3d cluster for e2e tests + @echo "--- Creating k3d cluster $(K3D_CLUSTER_NAME) ---" + $(K3D) cluster create $(K3D_CLUSTER_NAME) + +k3d-cluster-delete: ## Delete k3d cluster for e2e tests + @echo "--- Deleting k3d cluster $(K3D_CLUSTER_NAME) ---" + $(K3D) cluster delete $(K3D_CLUSTER_NAME) + +k3d-image-import: ## Import local image to k3d cluster + @echo "--- Importing image $(ARGOCD_IMAGE_UPDATER_IMAGE) to k3d cluster $(K3D_CLUSTER_NAME) ---" + $(K3D) image import $(ARGOCD_IMAGE_UPDATER_IMAGE) -c $(K3D_CLUSTER_NAME) + +# Currently run only parallel tests because we don't have sequential tests yet. +test-e2e-local: ## Build local image updater, deploy operator, and run parallel e2e tests. + @echo "--- Creating k3d cluster---" + $(MAKE) k3d-cluster-create + @echo "--- Building local argocd-image-updater image ---" + $(MAKE) -C ../../ docker-build + @echo "--- Importing image to k3d cluster ---" + $(MAKE) k3d-image-import + @echo "--- Deploying argocd-operator with local image-updater ---" + $(MAKE) deploy-argocd-operator + @echo "--- Running Parallel E2E tests ---" + $(MAKE) -C ../../ e2e-tests-parallel-ginkgo + @echo "--- Deleting k3d cluster ---" + $(MAKE) k3d-cluster-delete + +test-e2e-ci: ## Build local image updater, deploy operator. + @echo "--- Building local argocd-image-updater image ---" + $(MAKE) -C ../../ docker-build + @echo "--- Importing image to k3d cluster ---" + $(MAKE) k3d-image-import + @echo "--- Deploying argocd-operator with local image-updater ---" + $(MAKE) deploy-argocd-operator diff --git a/test/ginkgo/README.md b/test/ginkgo/README.md new file mode 100644 index 00000000..a0d8ca8f --- /dev/null +++ b/test/ginkgo/README.md @@ -0,0 +1,185 @@ +# Argo CD Image Updater E2E Tests + +!!!note + The E2E test framework in this directory is based on the test framework from the [Argo CD Operator](https://github.com/argoproj-labs/argocd-operator/tree/master/tests/ginkgo). + +Argo CD Image Updater E2E tests are defined within the `test/ginkgo` directory. + +These tests are written with the Ginkgo/Gomega test framework. + +## Running tests + +### Running E2E tests locally + +This workflow is designed for developers to test their local changes in a realistic, automated environment. All commands should be run from the project root directory. + +#### Prerequisites + +You must have the following tools installed locally: +- [Docker](https://docs.docker.com/get-docker/) +- [k3d](https://k3d.io/#installation) + +#### Architecture + +The `test-e2e-local` target in `test/ginkgo/Makefile` automates the entire testing lifecycle: + +1. **Cluster Creation**: A new `k3d` cluster is created for a clean test run. +2. **Build**: The target calls the `docker-build` command in the root `Makefile` to build a Docker image from your current source code. +3. **Image Import**: The newly built local image is imported directly into the `k3d` cluster's nodes, making it available to Kubernetes without a registry. +4. **Operator Deployment**: The `argocd-operator` is deployed into the cluster. Its deployment is then patched to use your local image by setting the `ARGOCD_IMAGE_UPDATER_IMAGE` environment variable. +5. **Test Execution**: The Ginkgo E2E test suite is run against the deployed operator and image updater. +6. **Cluster Deletion**: After the tests complete, the `k3d` cluster is automatically deleted to clean up all resources. + +#### Instructions + +**To run the entire E2E test suite:** + +This single command will perform all the steps described above. +```bash +make -C test/ginkgo test-e2e-local +``` + +**To clean up the cluster manually:** + +This is only necessary if the `test-e2e-local` target is interrupted before it can clean up after itself. +```bash +make -C test/ginkgo k3d-cluster-delete +``` + +### Run a specific test: + +```bash +# 'make ginkgo' to download ginkgo, if needed +# Examples: +./bin/ginkgo -vv -focus "1-001_validate_image_updater_test" -r ./test/ginkgo/parallel +``` + +## Test Code + +Argo CD Image Updater E2E tests are defined within `test/ginkgo`. + +These tests are written with the [Ginkgo/Gomega test frameworks](https://github.com/onsi/ginkgo), and were ported from previous Kuttl tests. + +### Tests are currently grouped as follows: +- `sequential`: Tests that are not safe to run in parallel with other tests. + - A test is NOT safe to run in parallel with other tests if: + - It modifies resources in operator namespaces + - It modifies cluster-scoped resources, such as `ClusterRoles`/`ClusterRoleBindings`, or `Namespaces` that are shared between tests + - More generally, if it writes to a K8s resource that is used by another test. +- `parallel`: Tests that are safe to run in parallel with other tests + - A test is safe to run in parallel if it does not have any of the above problematic behaviours. + - It is fine for a parallel test to READ shared or cluster-scoped resources (such as resources in operator namespaces) + - But a parallel test should NEVER write to resources that may be shared with other tests (some cluster-scoped resources, etc.) + +*Guidance*: Look at the list of restrictions for sequential. If your test is doing any of those things, it needs to run sequential. Otherwise, parallel is fine. + +### Test fixture: +- Utility functions for writing tests can be found within the `fixture/` folder. +- `fixture/fixture.go` contains utility functions that are generally useful to writing tests. + - Most important are: + - `EnsureParallelCleanSlate`: Should be called at the beginning of every parallel test. + - `EnsureSequentialCleanSlate`: Should be called at the beginning of every sequential test. +- `fixture/(name of resource)` contains functions that are specific to working with a particular resource. + - For example, if you wanted to wait for an `Application` CR to be Synced/Healthy, you would use the functions defined in `fixture/application`. + - Likewise, if you want to check a `Deployment`, see `fixture/deployment`. +- The goal of this test fixture is to make it easy to write tests, and to ensure it is easy to understand and maintain existing tests. +- See existing k8s tests for usage examples. + +## Writing new tests + +Ginkgo tests are read from left to right. For example: +- `Expect(k8sClient.Create(ctx, argoCD)).To(Succeed())` + - Can be read as: Expect create of argo cd CR to succeed. +- `Eventually(appControllerPod, "3m", "5s").Should(k8sFixture.ExistByName())` + - Can be read as: Eventually the `(argo cd application controller pod)` should exist (within 3 minute, checking every 5 seconds.) +- `fixture.Update(argoCD, func(){ (...)})` + - Can be read as: Update Argo CD CR using the given function + +The E2E tests we use within this repo uses the standard controller-runtime k8s go API to interact with kubernetes (controller-runtime). This API is very familiar to anyone already writing go operator/controller code (such as developers of this project). + +The best way to learn how to write a new test (or matcher/fixture), is just to copy an existing one! + +### Standard patterns you can use + +#### To verify a K8s resource has an expected status/spec: +- `fixture` packages + - Fixture packages contain utility functions which exists for (nearly) all resources (described in detail elsewhere) + - Most often, a function in a `fixture` will already exist for what you are looking for. + - For example, use `argocdFixture` to check if Argo CD is available: + - `Eventually(argoCDbeta1, "5m", "5s").Should(argocdFixture.BeAvailable())` + - Consider adding new functions to fixtures, so that tests can use them as well. +- If no fixture package function exists, just use a function that returns bool + +#### To create an object: +- `Expect(k8sClient.Create(ctx, (object))).Should(Succeed())` + +#### To update an object, use `fixture.Update` +- `fixture.Update(object, func(){})` function + - Test will automatically retry the update if update fails. + - This avoids a common issue in k8s tests, where update fails which causes the test to fail. + +#### To delete a k8s object +- `Expect(k8sClient.Delete(ctx, (object))).Should(Succeed())` + - Where `(object)` is any k8s resource + +#### When writing sequential tests, ensure you: + +A) Call EnsureSequentialCleanSlate before each test: +```go + BeforeEach(func() { + fixture.EnsureSequentialCleanSlate() + } +``` + +Unlike with parallel tests, you don't need to clean up namespace after each test. Sequential will automatically clean up namespaces created via the `fixture.Create(...)Namespace` API. (But if you want to delete it using `defer`, it doesn't hurt). + +#### When writing parallel tests, ensure you: + +A) Call EnsureParallelCleanSlate before each test +```go + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + }) +``` + +B) Clean up any namespaces (or any cluster-scoped resources you created) using `defer`: +```go +// Create a namespace to use for the duration of the test, and then automatically clean it up after. +ns, cleanupFunc := fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() +defer cleanupFunc() +``` + +### General Tips +- DON'T ADD SLEEP STATEMENTS TO TESTS (unless it's absolutely necessary, but it rarely is!) + - Use `Eventually`/`Consistently` with a condition, instead. +- Use `By("")` to document each step for what the test is doing. + - This is very helpful for other team members that need to maintain your test after you wrote it. + - Also, all `By("")`s are included in test output as `Step: (...)`, which makes it easy to tell what the test is doing when the test is running. + +## Tips for debugging tests + +### If you are debugging tests in CI +- If you are debugging a test failure, considering adding a call to the `fixture.OutputDebugOnFail()` function at the end of the test. +- `OutputDebugOnFail` will output helpful information when a test fails (such as namespace contents and operator pod logs) +- See existing test code for examples. + +### If you are debugging tests locally +- Consider setting the `E2E_DEBUG_SKIP_CLEANUP` variable when debugging tests locally. +- The `E2E_DEBUG_SKIP_CLEANUP` environment variable will skip cleanup at the end of the test. + - The default E2E test behaviour is to clean up test resources at the end of the test. + - This is good when tests are succeeding, but when they are failing it can be helpful to look at the state of those K8s resources at the time of failure. + - Those old tests resources WILL still be cleaned up when you next start the test again. +- This will allow you to `kubectl get` the test resource to see why the test failed. + +Example: +```bash +E2E_DEBUG_SKIP_CLEANUP=true ./bin/ginkgo -vv -focus "1-001_validate_image_updater_test" -r ./test/ginkgo/parallel +``` + +## External Documentation + +[**Ginkgo/Gomega docs**](https://onsi.github.io/gomega/): The Ginkgo/Gomega docs are great! they are very detailed, with lots of good examples. There are also plenty of other examples of Ginkgo/Gomega you can find via searching. + +**Ask an LLM (Gemini/Cursor/etc)**: Ginkgo/gomega are popular enough that LLMs are able to answer questions and write code for them. +- For example, I performed the following Gemini Pro query, and got an excellent answer: + - `With Ginkgo/Gomega (https://onsi.github.io/gomega) and Go lang, how do I create a matcher which checks whether a Kubernetes Deployment (via Deployment go object) has ready replicas of 1` diff --git a/test/ginkgo/fixture/application/fixture.go b/test/ginkgo/fixture/application/fixture.go new file mode 100644 index 00000000..74a6dd38 --- /dev/null +++ b/test/ginkgo/fixture/application/fixture.go @@ -0,0 +1,129 @@ +package application + +import ( + "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/utils" + + "github.com/argoproj/gitops-engine/pkg/health" + "github.com/argoproj/gitops-engine/pkg/sync/common" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/gomega" //nolint:all + "k8s.io/client-go/util/retry" + + appv1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" + matcher "github.com/onsi/gomega/types" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/ginkgo/v2" //nolint:all + + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// This is intentionally NOT exported, for now. Create another function in this file/package that calls this function, and export that. +func expectedCondition(f func(app *appv1alpha1.Application) bool) matcher.GomegaMatcher { + + return WithTransform(func(app *appv1alpha1.Application) bool { + + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(app), app) + if err != nil { + GinkgoWriter.Println(err) + return false + } + + return f(app) + + }, BeTrue()) + +} + +func HaveOperationStatePhase(expectedPhase common.OperationPhase) matcher.GomegaMatcher { + + return expectedCondition(func(app *appv1alpha1.Application) bool { + + var currStatePhase string + + if app.Status.OperationState != nil { + currStatePhase = string(app.Status.OperationState.Phase) + } + + GinkgoWriter.Println("HaveOperationStatePhase - current phase:", currStatePhase, " / expected phase:", expectedPhase) + + return currStatePhase == string(expectedPhase) + + }) + +} + +func HaveHealthStatusCode(expectedHealth health.HealthStatusCode) matcher.GomegaMatcher { + + return expectedCondition(func(app *appv1alpha1.Application) bool { + + GinkgoWriter.Println("HaveHealthStatusCode - current health:", app.Status.Health.Status, "/ expected health:", expectedHealth) + + return app.Status.Health.Status == expectedHealth + + }) + +} + +// HaveSyncStatusCode waits for Argo CD to have the given sync status +func HaveSyncStatusCode(expected appv1alpha1.SyncStatusCode) matcher.GomegaMatcher { + + return expectedCondition(func(app *appv1alpha1.Application) bool { + + GinkgoWriter.Println("HaveSyncStatusCode - current syncStatusCode:", app.Status.Sync.Status, " / expected syncStatusCode:", expected) + + return app.Status.Sync.Status == expected + + }) + +} + +// Update will keep trying to update object until it succeeds, or times out. +func Update(obj *appv1alpha1.Application, modify func(*appv1alpha1.Application)) { + k8sClient, _ := utils.GetE2ETestKubeClient() + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the object + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(obj), obj) + if err != nil { + return err + } + + modify(obj) + + // Attempt to update the object + return k8sClient.Update(context.Background(), obj) + }) + Expect(err).ToNot(HaveOccurred()) + +} + +// UpdateWithError will keep trying to update object until it succeeds, or times out. +// Returns an error instead of using Gomega assertions. +func UpdateWithError(obj *appv1alpha1.Application, modify func(*appv1alpha1.Application)) error { + k8sClient, _ := utils.GetE2ETestKubeClient() + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the object + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(obj), obj) + if err != nil { + return err + } + + modify(obj) + + // Attempt to update the object + return k8sClient.Update(context.Background(), obj) + }) + + return err +} diff --git a/test/ginkgo/fixture/argocd/fixture.go b/test/ginkgo/fixture/argocd/fixture.go new file mode 100644 index 00000000..8ba469c9 --- /dev/null +++ b/test/ginkgo/fixture/argocd/fixture.go @@ -0,0 +1,216 @@ +package argocd + +import ( + "context" + "os/exec" + "time" + + "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/utils" + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/ginkgo/v2" //nolint:all + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/gomega" //nolint:all + + matcher "github.com/onsi/gomega/types" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Update will update an ArgoCD CR. Update will keep trying to update object until it succeeds, or times out. +func Update(obj *argov1beta1api.ArgoCD, modify func(*argov1beta1api.ArgoCD)) { + k8sClient, _ := utils.GetE2ETestKubeClient() + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the object + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(obj), obj) + if err != nil { + return err + } + + modify(obj) + + // Attempt to update the object + return k8sClient.Update(context.Background(), obj) + }) + Expect(err).ToNot(HaveOccurred()) + + // After we update ArgoCD CR, we should wait a few moments for the operator to reconcile the change. + // - Ideally, the ArgoCD CR would have a .status field that we could read, that would indicate which resource version/generation had been reconciled. + // - Sadly, this does not exist, so we instead must use time.Sleep() (for now) + time.Sleep(7 * time.Second) +} + +// BeAvailable waits for Argo CD instance to have .status.phase of 'Available' +func BeAvailable() matcher.GomegaMatcher { + return BeAvailableWithCustomSleepTime(10 * time.Second) +} + +// In most cases, you should probably just use 'BeAvailable'. +func BeAvailableWithCustomSleepTime(sleepTime time.Duration) matcher.GomegaMatcher { + + // Wait X seconds to allow operator to reconcile the ArgoCD CR, before we start checking if it's ready + // - We do this so that any previous calls to update the ArgoCD CR have been reconciled by the operator, before we wait to see if ArgoCD has become available. + // - I'm not aware of a way to do this without a sleep statement, but when we have something better we should do that instead. + time.Sleep(sleepTime) + + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + + if argocd.Status.Phase != "Available" { + GinkgoWriter.Println("ArgoCD status is not yet Available") + return false + } + GinkgoWriter.Println("ArgoCD status is now", argocd.Status.Phase) + + return true + }) +} + +func HavePhase(phase string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HavePhase:", "expected:", phase, "/ actual:", argocd.Status.Phase) + return argocd.Status.Phase == phase + }) +} + +func HaveRedisStatus(status string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveRedisStatus:", "expected:", status, "/ actual:", argocd.Status.Redis) + return argocd.Status.Redis == status + }) +} + +func HaveRepoStatus(status string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveRepoStatus:", "expected:", status, "/ actual:", argocd.Status.Repo) + return argocd.Status.Repo == status + }) +} + +func HaveServerStatus(status string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveServerStatus:", "expected:", status, "/ actual:", argocd.Status.Server) + return argocd.Status.Server == status + }) +} + +func HaveApplicationControllerStatus(status string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveApplicationControllerStatus:", "expected:", status, "/ actual:", argocd.Status.ApplicationController) + return argocd.Status.ApplicationController == status + }) +} + +func HaveApplicationSetControllerStatus(status string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveApplicationSetControllerStatus:", "expected:", status, "/ actual:", argocd.Status.ApplicationSetController) + return argocd.Status.ApplicationSetController == status + }) +} + +func HaveNotificationControllerStatus(status string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveNotificationControllerStatus:", "expected:", status, "/ actual:", argocd.Status.NotificationsController) + return argocd.Status.NotificationsController == status + }) +} + +func HaveSSOStatus(status string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveSSOStatus:", "expected:", status, "/ actual:", argocd.Status.SSO) + return argocd.Status.SSO == status + }) +} + +func HaveHost(host string) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveHost:", "expected:", host, "/ actual:", argocd.Status.Host) + return argocd.Status.Host == host + }) +} + +func HaveApplicationControllerOperationProcessors(operationProcessors int) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + GinkgoWriter.Println("HaveApplicationControllerOperationProcessors:", "Expected:", operationProcessors, "/ actual:", argocd.Spec.Controller.Processors.Operation) + return int(argocd.Spec.Controller.Processors.Operation) == operationProcessors + }) +} + +func HaveCondition(condition metav1.Condition) matcher.GomegaMatcher { + return fetchArgoCD(func(argocd *argov1beta1api.ArgoCD) bool { + + if len(argocd.Status.Conditions) != 1 { + GinkgoWriter.Println("HaveCondition: length is zero") + return false + } + + instanceCondition := argocd.Status.Conditions[0] + + GinkgoWriter.Println("HaveCondition - Message:", instanceCondition.Message, condition.Message) + if instanceCondition.Message != condition.Message { + GinkgoWriter.Println("HaveCondition: message does not match") + return false + } + + GinkgoWriter.Println("HaveCondition - Reason:", instanceCondition.Reason, condition.Reason) + if instanceCondition.Reason != condition.Reason { + GinkgoWriter.Println("HaveCondition: reason does not match") + return false + } + + GinkgoWriter.Println("HaveCondition - Status:", instanceCondition.Status, condition.Status) + if instanceCondition.Status != condition.Status { + GinkgoWriter.Println("HaveCondition: status does not match") + return false + } + + GinkgoWriter.Println("HaveCondition - Type:", instanceCondition.Type, condition.Type) + if instanceCondition.Type != condition.Type { + GinkgoWriter.Println("HaveCondition: type does not match") + return false + } + + return true + + }) +} + +// This is intentionally NOT exported, for now. Create another function in this file/package that calls this function, and export that. +func fetchArgoCD(f func(*argov1beta1api.ArgoCD) bool) matcher.GomegaMatcher { + + return WithTransform(func(argocd *argov1beta1api.ArgoCD) bool { + + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(argocd), argocd) + if err != nil { + GinkgoWriter.Println(err) + return false + } + + return f(argocd) + + }, BeTrue()) + +} + +func RunArgoCDCLI(args ...string) (string, error) { + + cmdArgs := append([]string{"argocd"}, args...) + + GinkgoWriter.Println("executing command", cmdArgs) + + // #nosec G204 + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + + output, err := cmd.CombinedOutput() + GinkgoWriter.Println(string(output)) + + return string(output), err +} diff --git a/test/ginkgo/fixture/deployment/fixture.go b/test/ginkgo/fixture/deployment/fixture.go new file mode 100644 index 00000000..cbb28492 --- /dev/null +++ b/test/ginkgo/fixture/deployment/fixture.go @@ -0,0 +1,418 @@ +package deployment + +import ( + "context" + "reflect" + "strings" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/ginkgo/v2" //nolint:all + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/gomega" //nolint:all + + matcher "github.com/onsi/gomega/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/utils" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/retry" +) + +func GetEnv(d *appsv1.Deployment, key string) (*string, error) { + + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + return nil, err + } + + if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(d), d); err != nil { + return nil, err + } + + containers := d.Spec.Template.Spec.Containers + + Expect(containers).Should(HaveLen(1)) + + for idx := range containers[0].Env { + + currEnv := containers[0].Env[idx] + + if currEnv.Name == key { + return &currEnv.Value, nil + } + } + + return nil, nil + +} + +func SetEnv(depl *appsv1.Deployment, key string, value string) { + + Update(depl, func(d *appsv1.Deployment) { + containers := d.Spec.Template.Spec.Containers + + Expect(containers).Should(HaveLen(1)) + + newEnvVars := []corev1.EnvVar{} + + match := false + for idx := range containers[0].Env { + + currEnv := containers[0].Env[idx] + + if currEnv.Name == key { + // replace with the value from the param + newEnvVars = append(newEnvVars, corev1.EnvVar{Name: key, Value: value}) + match = true + } else { + newEnvVars = append(newEnvVars, currEnv) + } + } + + if !match { + newEnvVars = append(newEnvVars, corev1.EnvVar{Name: key, Value: value}) + } + + containers[0].Env = newEnvVars + + }) + +} + +func RemoveEnv(depl *appsv1.Deployment, key string) { + + Update(depl, func(d *appsv1.Deployment) { + containers := d.Spec.Template.Spec.Containers + + Expect(containers).Should(HaveLen(1)) + + newEnvVars := []corev1.EnvVar{} + + for idx := range containers[0].Env { + + currEnv := containers[0].Env[idx] + + if currEnv.Name == key { + // don't add, thus causing it to be removed + } else { + newEnvVars = append(newEnvVars, currEnv) + } + } + + containers[0].Env = newEnvVars + + }) + +} + +// Update will keep trying to update object until it succeeds, or times out. +func Update(obj *appsv1.Deployment, modify func(*appsv1.Deployment)) { + k8sClient, _ := utils.GetE2ETestKubeClient() + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the object + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(obj), obj) + if err != nil { + return err + } + + modify(obj) + + // Attempt to update the object + return k8sClient.Update(context.Background(), obj) + }) + Expect(err).ToNot(HaveOccurred()) +} + +func GetTemplateSpecInitContainerByName(name string, depl appsv1.Deployment) *corev1.Container { + + for idx := range depl.Spec.Template.Spec.InitContainers { + + container := depl.Spec.Template.Spec.InitContainers[idx] + if container.Name == name { + return &container + } + } + + return nil +} + +func GetTemplateSpecContainerByName(name string, depl appsv1.Deployment) *corev1.Container { + + for idx := range depl.Spec.Template.Spec.Containers { + + container := depl.Spec.Template.Spec.Containers[idx] + if container.Name == name { + return &container + } + } + + return nil +} + +func HaveTemplateSpec(podSpec corev1.PodSpec) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + templateSpec := depl.Spec.Template.Spec + if templateSpec.NodeSelector == nil { + GinkgoWriter.Println("HaveTemplateSpec - .spec.template.spec is nil") + return false + } + GinkgoWriter.Println("HaveTemplateSpec - expected:", podSpec, "actual:", templateSpec) + return reflect.DeepEqual(podSpec, templateSpec) + }) +} + +func HaveTemplateSpecNodeSelector(nodeSelector map[string]string) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + + templateSpec := depl.Spec.Template.Spec + + if templateSpec.NodeSelector == nil { + GinkgoWriter.Println("HaveTemplateSpecNodeSelector - .spec.template.spec is nil") + return false + } + + GinkgoWriter.Println("HaveTemplateSpecNodeSelector - expected:", nodeSelector, "actual:", templateSpec.NodeSelector) + return reflect.DeepEqual(nodeSelector, templateSpec.NodeSelector) + }) + +} + +func HaveTemplateLabelWithValue(key string, value string) matcher.GomegaMatcher { + + return WithTransform(func(depl *appsv1.Deployment) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(depl), depl) + if err != nil { + GinkgoWriter.Println("HaveTemplateLabelWithValue:", err) + return false + } + + labels := depl.Spec.Template.Labels + if labels == nil { + GinkgoWriter.Println("HaveTemplateLabelWithValue - labels are nil") + return false + } + + GinkgoWriter.Println("HaveTemplateLabelWithValue - Key", key, "Expect:", value, "/ Have:", labels[key]) + + return labels[key] == value + + }, BeTrue()) +} + +func HaveTemplateAnnotationWithValue(key string, value string) matcher.GomegaMatcher { + + return WithTransform(func(depl *appsv1.Deployment) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(depl), depl) + if err != nil { + GinkgoWriter.Println("HaveTemplateAnnotationWithValue:", err) + return false + } + + annotations := depl.Spec.Template.Annotations + if annotations == nil { + GinkgoWriter.Println("HaveTemplateAnnotationWithValue - annotations are nil") + return false + } + + GinkgoWriter.Println("HaveTemplateAnnotationWithValue - Key", key, "Expect:", value, "/ Have:", annotations[key]) + + return annotations[key] == value + + }, BeTrue()) +} + +func HaveTolerations(tolerations []corev1.Toleration) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + + templateSpec := depl.Spec.Template.Spec + + GinkgoWriter.Println("HaveTolerations - expected:", tolerations, "actual:", templateSpec.Tolerations) + + return reflect.DeepEqual(templateSpec.Tolerations, tolerations) + }) + +} + +func HaveObservedGeneration(observedGeneration int) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + GinkgoWriter.Println("Deployment HaveObservedGeneration:", "expected: ", observedGeneration, "actual: ", depl.Status.ObservedGeneration) + return int64(observedGeneration) == depl.Status.ObservedGeneration + }) +} + +func HaveReplicas(replicas int) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + GinkgoWriter.Println("Deployment", depl.Name, "- HaveReplicas:", "expected: ", replicas, "actual: ", depl.Status.Replicas) + return int(depl.Status.Replicas) == replicas && depl.Generation == depl.Status.ObservedGeneration + }) +} + +func HaveReadyReplicas(readyReplicas int) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + GinkgoWriter.Println("Deployment ", depl.Name, "- HaveReadyReplicas:", "expected: ", readyReplicas, "actual: ", depl.Status.ReadyReplicas) + return int(depl.Status.ReadyReplicas) == readyReplicas && depl.Generation == depl.Status.ObservedGeneration + }) +} + +func HaveUpdatedReplicas(updatedReplicas int) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + GinkgoWriter.Println("Deployment HaveUpdatedReplicas:", "expected: ", updatedReplicas, "actual: ", depl.Status.UpdatedReplicas) + return int(depl.Status.UpdatedReplicas) == updatedReplicas && depl.Generation == depl.Status.ObservedGeneration + }) +} + +func HaveAvailableReplicas(availableReplicas int) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + GinkgoWriter.Println("Deployment HaveAvailableReplicas:", "expected: ", availableReplicas, "actual: ", depl.Status.AvailableReplicas) + return int(depl.Status.AvailableReplicas) == availableReplicas && depl.Generation == depl.Status.ObservedGeneration + }) +} + +func HaveContainerCommandSubstring(expectedCommandSubstring string, containerIndex int) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + + containers := depl.Spec.Template.Spec.Containers + + if len(containers) <= containerIndex { + GinkgoWriter.Println("current container slice has length", len(containers), "index is", containerIndex) + return false + } + + // Combine Command and Args, adding spaces (' ') between the args + var cmdLine string + + for _, val := range containers[containerIndex].Command { + if val == "" { + cmdLine += "\"\"" + " " + } else { + cmdLine += val + " " + } + } + cmdLine = strings.TrimSpace(cmdLine) + + for _, val := range containers[containerIndex].Args { + if val == "" { + cmdLine += "\"\"" + " " + } else { + cmdLine += val + " " + } + } + cmdLine = strings.TrimSpace(cmdLine) + + GinkgoWriter.Println("HaveContainerCommandSubstring: Have:") + GinkgoWriter.Println(cmdLine) + GinkgoWriter.Println("HaveContainerCommandSubstring: Expect:") + GinkgoWriter.Println(expectedCommandSubstring) + + return strings.Contains(cmdLine, expectedCommandSubstring) + + }) +} + +// HaveContainerWithEnvVar matchs when a container exists under .spec.template.spec.containers, at position 'containerIndex' in the container array, with env var key/value +func HaveContainerWithEnvVar(envKey string, envValue string, containerIndex int) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + + containers := depl.Spec.Template.Spec.Containers + + if len(containers) <= containerIndex { + GinkgoWriter.Println("current container slice has length", len(containers), "index is", containerIndex) + return false + } + + container := containers[containerIndex] + + for _, env := range container.Env { + if env.Name == envKey { + GinkgoWriter.Println("HaveContainerWithEnvVar - Key ", envKey, " Expected:", envValue, "Actual:", env.Value) + if env.Value == envValue { + return true + } + } + } + + return false + }) +} + +func HaveSpecTemplateSpecVolume(volumeParam corev1.Volume) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + + GinkgoWriter.Println("HaveSpecTemplateSpecVolume - Volumes:") + for _, volume := range depl.Spec.Template.Spec.Volumes { + GinkgoWriter.Println("-", volume) + + if reflect.DeepEqual(volumeParam, volume) { + return true + } + } + + return false + }) + +} + +func HaveConditionTypeStatus(expectedConditionType appsv1.DeploymentConditionType, expectedConditionStatus corev1.ConditionStatus) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + + GinkgoWriter.Println("Conditions:") + for _, condition := range depl.Status.Conditions { + GinkgoWriter.Println("-", condition.Type, condition.Status) + if condition.Type == expectedConditionType && condition.Status == expectedConditionStatus { + return true + } + } + + return false + }) +} + +func HaveServiceAccountName(expectedServiceAccountName string) matcher.GomegaMatcher { + return fetchDeployment(func(depl *appsv1.Deployment) bool { + + GinkgoWriter.Println("HaveServiceAccountName - Expected:", expectedServiceAccountName, "Actual:", depl.Spec.Template.Spec.ServiceAccountName, "/", depl.Spec.Template.Spec.DeprecatedServiceAccount) + + // We check both the deprecated and non-deprecated names, as these can POTENTIALLY be different values. + // - They should both have the same value 'expectedServiceAccountName' + + return depl.Spec.Template.Spec.ServiceAccountName == expectedServiceAccountName && depl.Spec.Template.Spec.DeprecatedServiceAccount == expectedServiceAccountName + }) +} + +// This is intentionally NOT exported, for now. Create another function in this file/package that calls this function, and export that. +func fetchDeployment(f func(*appsv1.Deployment) bool) matcher.GomegaMatcher { + + return WithTransform(func(depl *appsv1.Deployment) bool { + + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(depl), depl) + if err != nil { + GinkgoWriter.Println(err) + return false + } + + return f(depl) + + }, BeTrue()) + +} diff --git a/test/ginkgo/fixture/fixture.go b/test/ginkgo/fixture/fixture.go new file mode 100644 index 00000000..98b12569 --- /dev/null +++ b/test/ginkgo/fixture/fixture.go @@ -0,0 +1,609 @@ +package fixture + +import ( + "context" + "fmt" + "os" + "strings" + "sync" + "time" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/ginkgo/v2" //nolint:all + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/gomega" //nolint:all + securityv1 "github.com/openshift/api/security/v1" + + crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + "k8s.io/apimachinery/pkg/util/uuid" + "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/onsi/gomega/format" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + apierr "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + osFixture "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/os" + "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/utils" +) + +const ( + // E2ETestLabelsKey and E2ETestLabelsValue are added to cluster-scoped resources (e.g. Namespaces) created by E2E tests (where possible). On startup (and before each test for sequential tests), any resources with this label will be deleted. + E2ETestLabelsKey = "app" + E2ETestLabelsValue = "test-argo-app" +) + +var NamespaceLabels = map[string]string{E2ETestLabelsKey: E2ETestLabelsValue} + +func EnsureParallelCleanSlate() { + + // Increase the maximum length of debug output, for when tests fail + format.MaxLength = 64 * 1024 + SetDefaultEventuallyTimeout(time.Second * 60) + SetDefaultEventuallyPollingInterval(time.Second * 3) + SetDefaultConsistentlyDuration(time.Second * 10) + SetDefaultConsistentlyPollingInterval(time.Second * 1) + + // Unlike sequential clean slate, parallel clean slate cannot assume that there are no other tests running. This limits our ability to clean up old test artifacts. +} + +// EnsureSequentialCleanSlate will clean up resources that were created during previous sequential tests +// - Deletes namespaces that were created by previous tests +// - Deletes other cluster-scoped resources that were created +// - Reverts changes made to Subscription CR +// - etc +func EnsureSequentialCleanSlate() { + Expect(EnsureSequentialCleanSlateWithError()).To(Succeed()) +} + +func EnsureSequentialCleanSlateWithError() error { + + // With sequential tests, we are always safe to assume that there is no other test running. That allows us to clean up old test artifacts before new test starts. + + // Increase the maximum length of debug output, for when tests fail + format.MaxLength = 64 * 1024 + SetDefaultEventuallyTimeout(time.Second * 60) + SetDefaultEventuallyPollingInterval(time.Second * 3) + SetDefaultConsistentlyDuration(time.Second * 10) + SetDefaultConsistentlyPollingInterval(time.Second * 1) + + ctx := context.Background() + k8sClient, _ := utils.GetE2ETestKubeClient() + + // Ensure namespaces created during test are deleted + err := ensureTestNamespacesDeleted(ctx, k8sClient) + if err != nil { + return err + } + + // Clean up old cluster-scoped role from 1-034 + _ = k8sClient.Delete(ctx, &rbacv1.ClusterRole{ObjectMeta: metav1.ObjectMeta{Name: "custom-argocd-role"}}) + + if RunningOnOpenShift() { + // Delete 'restricted-dropcaps' which is created by at least one test + scc := &securityv1.SecurityContextConstraints{ + ObjectMeta: metav1.ObjectMeta{ + Name: "restricted-dropcaps", + }, + } + if err := k8sClient.Delete(ctx, scc); err != nil { + if !apierr.IsNotFound(err) { + return err + } + // Otherwise, expected error if it doesn't exist. + } + } + + return nil +} + +func CreateRandomE2ETestNamespace() *corev1.Namespace { + + randomVal := string(uuid.NewUUID()) + randomVal = randomVal[0:13] // Only use 13 characters of randomness. If we use more, then we start to hit limits on parts of code which limit # of characters to 63 + + testNamespaceName := "gitops-e2e-test-" + randomVal + + ns := CreateNamespace(testNamespaceName) + return ns +} + +func CreateRandomE2ETestNamespaceWithCleanupFunc() (*corev1.Namespace, func()) { + + ns := CreateRandomE2ETestNamespace() + return ns, nsDeletionFunc(ns) +} + +// Create namespace for tests having a specific label for identification +// - If the namespace already exists, it will be deleted first +func CreateNamespace(name string) *corev1.Namespace { + + k8sClient, _ := utils.GetE2ETestKubeClient() + + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}} + // If the Namespace already exists, delete it first + if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(ns), ns); err == nil { + // Namespace exists, so delete it first + Expect(deleteNamespaceAndVerify(context.Background(), ns.Name, k8sClient)).To(Succeed()) + } + + ns = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: NamespaceLabels, + }} + + err := k8sClient.Create(context.Background(), ns) + Expect(err).ToNot(HaveOccurred()) + + return ns +} + +func CreateNamespaceWithCleanupFunc(name string) (*corev1.Namespace, func()) { + + ns := CreateNamespace(name) + return ns, nsDeletionFunc(ns) +} + +// Create a namespace 'name' that is managed by another namespace 'managedByNamespace', via managed-by label. +func CreateManagedNamespace(name string, managedByNamespace string) *corev1.Namespace { + k8sClient, _ := utils.GetE2ETestKubeClient() + + ns := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: name}} + + // If the Namespace already exists, delete it first + if err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(ns), ns); err == nil { + // Namespace exists, so delete it first + Expect(deleteNamespaceAndVerify(context.Background(), ns.Name, k8sClient)).To(Succeed()) + } + + ns = &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + E2ETestLabelsKey: E2ETestLabelsValue, + "argocd.argoproj.io/managed-by": managedByNamespace, + }, + }} + + Expect(k8sClient.Create(context.Background(), ns)).To(Succeed()) + + return ns + +} + +func CreateManagedNamespaceWithCleanupFunc(name string, managedByNamespace string) (*corev1.Namespace, func()) { + ns := CreateManagedNamespace(name, managedByNamespace) + return ns, nsDeletionFunc(ns) +} + +// nsDeletionFunc is a convenience function that returns a function that deletes a namespace. This is used for Namespace cleanup by other functions. +func nsDeletionFunc(ns *corev1.Namespace) func() { + + return func() { + DeleteNamespace(ns) + } + +} + +func DeleteNamespace(ns *corev1.Namespace) { + // If you are debugging an E2E test and want to prevent its namespace from being deleted when the test ends (so that you can examine the state of resources in the namespace) you can set E2E_DEBUG_SKIP_CLEANUP env var. + if os.Getenv("E2E_DEBUG_SKIP_CLEANUP") != "" { + GinkgoWriter.Println("Skipping namespace cleanup as E2E_DEBUG_SKIP_CLEANUP is set") + return + } + + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + Expect(err).ToNot(HaveOccurred()) + + err = deleteNamespaceAndVerify(context.Background(), ns.Name, k8sClient) + Expect(err).ToNot(HaveOccurred()) + +} + +// EnvNonOLM checks if NON_OLM var is set; this variable is set when testing on GitOps operator that is not installed via OLM +func EnvNonOLM() bool { + _, exists := os.LookupEnv("NON_OLM") + return exists +} + +func EnvLocalRun() bool { + _, exists := os.LookupEnv("LOCAL_RUN") + return exists +} + +// EnvCI checks if CI env var is set; this variable is set when testing on GitOps Operator running via CI pipeline +func EnvCI() bool { + _, exists := os.LookupEnv("CI") + return exists +} + +func WaitForAllDeploymentsInTheNamespaceToBeReady(ns string, k8sClient client.Client) { + + Eventually(func() bool { + var deplList appsv1.DeploymentList + + if err := k8sClient.List(context.Background(), &deplList, client.InNamespace(ns)); err != nil { + GinkgoWriter.Println(err) + return false + } + + for _, depl := range deplList.Items { + + // If at least one of the Deployments has not been observed, wait and try again + if depl.Generation != depl.Status.ObservedGeneration { + return false + } + + if int64(depl.Status.Replicas) != int64(depl.Status.ReadyReplicas) { + return false + } + + } + + // All Deployments in NS are reconciled and ready + return true + + }, "3m", "1s").Should(BeTrue()) + +} + +func WaitForAllStatefulSetsInTheNamespaceToBeReady(ns string, k8sClient client.Client) { + + Eventually(func() bool { + var ssList appsv1.StatefulSetList + + if err := k8sClient.List(context.Background(), &ssList, client.InNamespace(ns)); err != nil { + GinkgoWriter.Println(err) + return false + } + + for _, ss := range ssList.Items { + + // If at least one of the StatefulSets has not been observed, wait and try again + if ss.Generation != ss.Status.ObservedGeneration { + return false + } + + if int64(ss.Status.Replicas) != int64(ss.Status.ReadyReplicas) { + return false + } + + } + + // All StatefulSets in NS are reconciled and ready + return true + + }, "3m", "1s").Should(BeTrue()) + +} + +func WaitForAllPodsInTheNamespaceToBeReady(ns string, k8sClient client.Client) { + + Eventually(func() bool { + var podList corev1.PodList + + if err := k8sClient.List(context.Background(), &podList, client.InNamespace(ns)); err != nil { + GinkgoWriter.Println(err) + return false + } + + for _, pod := range podList.Items { + for _, containerStatus := range pod.Status.ContainerStatuses { + + if !containerStatus.Ready { + GinkgoWriter.Println(pod.Name, "has container", containerStatus.Name, "which is not ready") + return false + } + } + + } + + // All Pod in NS are ready + return true + + }, "3m", "1s").Should(BeTrue()) + +} + +// Delete all namespaces having a specific label used to identify namespaces that are created by e2e tests. +func ensureTestNamespacesDeleted(ctx context.Context, k8sClient client.Client) error { + + // fetch all namespaces having given label + nsList, err := listE2ETestNamespaces(ctx, k8sClient) + if err != nil { + return fmt.Errorf("unable to delete test namespace: %w", err) + } + + // delete selected namespaces + for _, namespace := range nsList.Items { + if err := deleteNamespaceAndVerify(ctx, namespace.Name, k8sClient); err != nil { + return fmt.Errorf("unable to delete namespace '%s': %w", namespace.Name, err) + } + } + return nil +} + +// deleteNamespaceAndVerify deletes a namespace, and waits for it to be reported as deleted. +func deleteNamespaceAndVerify(ctx context.Context, namespaceParam string, k8sClient client.Client) error { + + GinkgoWriter.Println("Deleting Namespace", namespaceParam) + + // Delete the namespace: + // - Issue a request to Delete the namespace + // - Finally, we check if it has been deleted. + // Using 10 minute timeout to accommodate CI environments with limited resources + // where cleanup operations may take longer due to finalizers on ArgoCD resources. + if err := wait.PollUntilContextTimeout(ctx, time.Second*5, time.Minute*10, true, func(ctx context.Context) (done bool, err error) { + // Delete the namespace, if it exists + namespace := corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: namespaceParam, + }, + } + if err := k8sClient.Delete(ctx, &namespace); err != nil { + if !apierr.IsNotFound(err) { + GinkgoWriter.Printf("Unable to delete namespace '%s': %v\n", namespaceParam, err) + return false, nil + } + } + + if err := k8sClient.Get(ctx, client.ObjectKeyFromObject(&namespace), &namespace); err != nil { + if apierr.IsNotFound(err) { + return true, nil + } else { + GinkgoWriter.Printf("Unable to Get namespace '%s': %v\n", namespaceParam, err) + return false, nil + } + } + + return false, nil + }); err != nil { + return fmt.Errorf("namespace was never deleted, after delete was issued. '%s':%v", namespaceParam, err) + } + + return nil +} + +// Retrieve list of namespaces having a specific label used to identify namespaces that are created by e2e tests. +func listE2ETestNamespaces(ctx context.Context, k8sClient client.Client) (corev1.NamespaceList, error) { + nsList := corev1.NamespaceList{} + + // set e2e label + req, err := labels.NewRequirement(E2ETestLabelsKey, selection.Equals, []string{E2ETestLabelsValue}) + if err != nil { + return nsList, fmt.Errorf("unable to set labels while fetching list of test namespace: %w", err) + } + + // fetch all namespaces having given label + err = k8sClient.List(ctx, &nsList, &client.ListOptions{LabelSelector: labels.NewSelector().Add(*req)}) + if err != nil { + return nsList, fmt.Errorf("unable to fetch list of test namespace: %w", err) + } + return nsList, nil +} + +// Update will keep trying to update object until it succeeds, or times out. +// +//nolint:unused +func updateWithoutConflict(obj client.Object, modify func(client.Object)) error { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + return err + } + + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the object + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(obj), obj) + if err != nil { + return err + } + + modify(obj) + + // Attempt to update the object + return k8sClient.Update(context.Background(), obj) + }) + + return err +} + +type testReportEntry struct { + isOutputted bool +} + +var testReportLock sync.Mutex +var testReportMap = map[string]testReportEntry{} // acquire testReportLock before reading/writing to this map, or any values within this map + +// OutputDebugOnFail can be used to debug a failing test: it will output the operator logs and namespace info +// Parameters: +// - Will output debug information on namespaces specified as parameters. +// - Namespace parameter may be a string, *Namespace, or Namespace +func OutputDebugOnFail(namespaceParams ...any) { + + // Convert parameter to string of namespace name: + // - You can specify Namespace, *Namespae, or string, and we will convert it to string namespace + namespaces := []string{} + for _, param := range namespaceParams { + + if param == nil { + continue + } + + if str, isString := (param).(string); isString { + namespaces = append(namespaces, str) + + } else if nsPtr, isNsPtr := (param).(*corev1.Namespace); isNsPtr { + namespaces = append(namespaces, nsPtr.Name) + + } else if ns, isNs := (param).(corev1.Namespace); isNs { + namespaces = append(namespaces, ns.Name) + + } else { + Fail(fmt.Sprintf("unrecognized parameter value: %v", param)) + } + } + + csr := CurrentSpecReport() + + if !csr.Failed() || os.Getenv("SKIP_DEBUG_OUTPUT") == "true" { + return + } + + testName := strings.Join(csr.ContainerHierarchyTexts, " ") + testReportLock.Lock() + defer testReportLock.Unlock() + debugOutput, exists := testReportMap[testName] + + if exists && debugOutput.isOutputted { + // Skip output if we have already outputted once for this test + return + } + + testReportMap[testName] = testReportEntry{ + isOutputted: true, + } + + for _, namespace := range namespaces { + + kubectlOutput, err := osFixture.ExecCommandWithOutputParam(false, "kubectl", "get", "all", "-n", namespace) + if err != nil { + GinkgoWriter.Println("unable to list", namespace, err, kubectlOutput) + continue + } + + GinkgoWriter.Println("") + GinkgoWriter.Println("----------------------------------------------------------------") + GinkgoWriter.Println("'kubectl get all -n", namespace+"' output:") + GinkgoWriter.Println(kubectlOutput) + GinkgoWriter.Println("----------------------------------------------------------------") + + kubectlOutput, err = osFixture.ExecCommandWithOutputParam(false, "kubectl", "get", "deployments", "-n", namespace, "-o", "yaml") + if err != nil { + GinkgoWriter.Println("unable to list", namespace, err, kubectlOutput) + continue + } + + GinkgoWriter.Println("") + GinkgoWriter.Println("----------------------------------------------------------------") + GinkgoWriter.Println("'kubectl get deployments -n " + namespace + " -o yaml") + GinkgoWriter.Println(kubectlOutput) + GinkgoWriter.Println("----------------------------------------------------------------") + + kubectlOutput, err = osFixture.ExecCommandWithOutputParam(false, "kubectl", "get", "events", "-n", namespace) + if err != nil { + GinkgoWriter.Println("unable to get events for namespace", err, kubectlOutput) + } else { + GinkgoWriter.Println("") + GinkgoWriter.Println("----------------------------------------------------------------") + GinkgoWriter.Println("'kubectl get events -n " + namespace + ":") + GinkgoWriter.Println(kubectlOutput) + GinkgoWriter.Println("----------------------------------------------------------------") + } + + } + + kubectlOutput, err := osFixture.ExecCommandWithOutputParam(false, "kubectl", "get", "argocds", "-A", "-o", "yaml") + if err != nil { + GinkgoWriter.Println("unable to output all argo cd statuses", err, kubectlOutput) + } else { + GinkgoWriter.Println("") + GinkgoWriter.Println("----------------------------------------------------------------") + GinkgoWriter.Println("'kubectl get argocds -A -o yaml':") + GinkgoWriter.Println(kubectlOutput) + GinkgoWriter.Println("----------------------------------------------------------------") + } + + GinkgoWriter.Println("You can skip this debug output by setting 'SKIP_DEBUG_OUTPUT=true'") + +} + +// EnsureRunningOnOpenShift should be called if a test requires OpenShift (for example, it uses Route CR). +func EnsureRunningOnOpenShift() { + runningOnOpenShift := RunningOnOpenShift() + if !runningOnOpenShift { + Skip("This test requires the cluster to be OpenShift") + } +} + +// RunningOnOpenShift returns true if the cluster is an OpenShift cluster, false otherwise. +func RunningOnOpenShift() bool { + k8sClient, _ := utils.GetE2ETestKubeClient() + + crdList := crdv1.CustomResourceDefinitionList{} + Expect(k8sClient.List(context.Background(), &crdList)).To(Succeed()) + + openshiftAPIsFound := 0 + for _, crd := range crdList.Items { + if strings.Contains(crd.Spec.Group, "openshift.io") { + openshiftAPIsFound++ + } + } + return openshiftAPIsFound > 5 // I picked 5 as an arbitrary number, could also just be 1 +} + +//nolint:unused +func outputPodLog(podSubstring string) { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return + } + + // List all pods on the cluster + var podList corev1.PodList + if err := k8sClient.List(context.Background(), &podList); err != nil { + GinkgoWriter.Println(err) + return + } + + // Look specifically for operator pod + matchingPods := []corev1.Pod{} + for idx := range podList.Items { + pod := podList.Items[idx] + if strings.Contains(pod.Name, podSubstring) { + matchingPods = append(matchingPods, pod) + } + } + + if len(matchingPods) == 0 { + // This can happen when the operator is not running on the cluster + GinkgoWriter.Println("DebugOutputOperatorLogs was called, but no pods were found.") + return + } + + if len(matchingPods) != 1 { + GinkgoWriter.Println("unexpected number of operator pods", matchingPods) + return + } + + // Extract operator logs + kubectlLogOutput, err := osFixture.ExecCommandWithOutputParam(false, "kubectl", "logs", "pod/"+matchingPods[0].Name, "manager", "-n", matchingPods[0].Namespace) + if err != nil { + GinkgoWriter.Println("unable to extract operator logs", err) + return + } + + // Output only the last 500 lines + lines := strings.Split(kubectlLogOutput, "\n") + + startIndex := max(len(lines)-500, 0) + + GinkgoWriter.Println("") + GinkgoWriter.Println("----------------------------------------------------------------") + GinkgoWriter.Println("Log output from operator pod:") + for _, line := range lines[startIndex:] { + GinkgoWriter.Println(">", line) + } + GinkgoWriter.Println("----------------------------------------------------------------") + +} + +func IsUpstreamOperatorTests() bool { + return true // This function should return true if running from argocd-operator repo, false if running from gitops-operator repo. This is to distinguish between tests in upstream argocd-operator and downstream gitops-operator repos. +} diff --git a/test/ginkgo/fixture/k8s/fixture.go b/test/ginkgo/fixture/k8s/fixture.go new file mode 100644 index 00000000..4cec9cb6 --- /dev/null +++ b/test/ginkgo/fixture/k8s/fixture.go @@ -0,0 +1,161 @@ +package k8s + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "k8s.io/client-go/util/retry" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/ginkgo/v2" //nolint:all + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/gomega" //nolint:all + + matcher "github.com/onsi/gomega/types" + apierrors "k8s.io/apimachinery/pkg/api/errors" + + "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/utils" +) + +func HaveAnnotationWithValue(key string, value string) matcher.GomegaMatcher { + + return WithTransform(func(k8sObject client.Object) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) + if err != nil { + GinkgoWriter.Println("HasAnnotationWithValue:", err) + return false + } + + annotations := k8sObject.GetAnnotations() + if annotations == nil { + return false + } + + return annotations[key] == value + + }, BeTrue()) +} + +func HaveLabelWithValue(key string, value string) matcher.GomegaMatcher { + + return WithTransform(func(k8sObject client.Object) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) + if err != nil { + GinkgoWriter.Println("HaveLabelWithValue:", err) + return false + } + + labels := k8sObject.GetLabels() + if labels == nil { + GinkgoWriter.Println("HaveLabelWithValue - labels are nil") + return false + } + + GinkgoWriter.Println("HaveLabelWithValue - Key", key, "Expect:", value, "/ Have:", labels[key]) + + return labels[key] == value + + }, BeTrue()) +} + +func NotHaveLabelWithValue(key string, value string) matcher.GomegaMatcher { + + return WithTransform(func(k8sObject client.Object) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) + if err != nil { + GinkgoWriter.Println("DoesNotHaveLabelWithValue:", err) + return false + } + + labels := k8sObject.GetLabels() + if labels == nil { + return true + } + + return labels[key] != value + + }, BeTrue()) +} + +// ExistByName checks if the given k8s resource exists, when retrieving it by name/namespace. +// - It does NOT check if the resource content matches. It only checks that a resource of that type and name exists. +func ExistByName() matcher.GomegaMatcher { + + return WithTransform(func(k8sObject client.Object) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) + if err != nil { + GinkgoWriter.Println("Object does not exist in ExistByName:", k8sObject.GetName(), err) + } else { + GinkgoWriter.Println("Object exists in ExistByName:", k8sObject.GetName()) + } + return err == nil + }, BeTrue()) +} + +// NotExistByName checks if the given resource does not exist, when retrieving it by name/namespace. +// Does NOT check if the resource content matches. +func NotExistByName() matcher.GomegaMatcher { + + return WithTransform(func(k8sObject client.Object) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(k8sObject), k8sObject) + if apierrors.IsNotFound(err) { + return true + } else { + if err != nil { + GinkgoWriter.Println(err) + } + return false + } + }, BeTrue()) +} + +// Update will keep trying to update object until it succeeds, or times out. +func Update(obj client.Object, modify func(client.Object)) { + k8sClient, _ := utils.GetE2ETestKubeClient() + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the object + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(obj), obj) + if err != nil { + return err + } + + modify(obj) + + // Attempt to update the object + return k8sClient.Update(context.Background(), obj) + }) + Expect(err).ToNot(HaveOccurred()) + +} diff --git a/test/ginkgo/fixture/os/fixture.go b/test/ginkgo/fixture/os/fixture.go new file mode 100644 index 00000000..65959bcd --- /dev/null +++ b/test/ginkgo/fixture/os/fixture.go @@ -0,0 +1,37 @@ +package os + +import ( + "fmt" + "os/exec" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/ginkgo/v2" //nolint:all +) + +func ExecCommand(cmdArgs ...string) (string, error) { + return ExecCommandWithOutputParam(true, cmdArgs...) +} + +// You probably want to use ExecCommand, unless you need to supress the output of sensitive data (for example, openssl CLI output) +func ExecCommandWithOutputParam(printOutput bool, cmdArgs ...string) (string, error) { + if len(cmdArgs) == 0 { + return "", fmt.Errorf("ExecCommandWithOutputParam requires at least one argument") + } + GinkgoWriter.Println("executing command:", cmdArgs) + + // #nosec G204 + cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...) + + outputBytes, err := cmd.CombinedOutput() + + var output string + if outputBytes != nil { + output = string(outputBytes) + } + + if printOutput { + GinkgoWriter.Println(output) + } + + return output, err +} diff --git a/test/ginkgo/fixture/statefulset/fixture.go b/test/ginkgo/fixture/statefulset/fixture.go new file mode 100644 index 00000000..833def27 --- /dev/null +++ b/test/ginkgo/fixture/statefulset/fixture.go @@ -0,0 +1,283 @@ +package statefulset + +import ( + "context" + "reflect" + "strings" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/ginkgo/v2" //nolint:all + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/gomega" //nolint:all + + matcher "github.com/onsi/gomega/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/utils" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/util/retry" +) + +// Update will keep trying to update object until it succeeds, or times out. +func Update(obj *appsv1.StatefulSet, modify func(*appsv1.StatefulSet)) { + k8sClient, _ := utils.GetE2ETestKubeClient() + + err := retry.RetryOnConflict(retry.DefaultRetry, func() error { + // Retrieve the latest version of the object + err := k8sClient.Get(context.Background(), client.ObjectKeyFromObject(obj), obj) + if err != nil { + return err + } + + modify(obj) + + // Attempt to update the object + return k8sClient.Update(context.Background(), obj) + }) + Expect(err).ToNot(HaveOccurred()) +} + +func HaveReplicas(replicas int) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + GinkgoWriter.Println("StatefulSet HaveReplicas:", "expected: ", replicas, "actual: ", ss.Status.Replicas) + return int(ss.Status.Replicas) == replicas && ss.Generation == ss.Status.ObservedGeneration + }) +} + +func HaveReadyReplicas(readyReplicas int) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + GinkgoWriter.Println("StatefulSet HaveReadyReplicas:", "expected: ", readyReplicas, "actual: ", ss.Status.ReadyReplicas) + return int(ss.Status.ReadyReplicas) == readyReplicas && ss.Generation == ss.Status.ObservedGeneration + }) +} + +func GetTemplateSpecInitContainerByName(name string, ss appsv1.StatefulSet) *corev1.Container { + + for idx := range ss.Spec.Template.Spec.InitContainers { + + container := ss.Spec.Template.Spec.InitContainers[idx] + if container.Name == name { + return &container + } + } + + return nil +} + +func GetTemplateSpecContainerByName(name string, ss appsv1.StatefulSet) *corev1.Container { + + for idx := range ss.Spec.Template.Spec.Containers { + + container := ss.Spec.Template.Spec.Containers[idx] + if container.Name == name { + return &container + } + } + + return nil +} + +func HaveTemplateSpecNodeSelector(nodeSelector map[string]string) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + + templateSpec := ss.Spec.Template.Spec + + if templateSpec.NodeSelector == nil { + GinkgoWriter.Println("HaveTemplateSpecNodeSelector - .spec.template.spec is nil") + return false + } + + GinkgoWriter.Println("HaveTemplateSpecNodeSelector - expected:", nodeSelector, "actual:", templateSpec.NodeSelector) + return reflect.DeepEqual(nodeSelector, templateSpec.NodeSelector) + }) + +} + +func HaveTemplateLabelWithValue(key string, value string) matcher.GomegaMatcher { + + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(ss), ss) + if err != nil { + GinkgoWriter.Println("HaveTemplateLabelWithValue:", err) + return false + } + + labels := ss.Spec.Template.Labels + if labels == nil { + GinkgoWriter.Println("HaveTemplateLabelWithValue - labels are nil") + return false + } + + GinkgoWriter.Println("HaveTemplateLabelWithValue - Key", key, "Expect:", value, "/ Have:", labels[key]) + + return labels[key] == value + + }) +} + +func HaveTemplateAnnotationWithValue(key string, value string) matcher.GomegaMatcher { + + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println(err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(ss), ss) + if err != nil { + GinkgoWriter.Println("HaveTemplateAnnotationWithValue:", err) + return false + } + + annotations := ss.Spec.Template.Annotations + if annotations == nil { + GinkgoWriter.Println("HaveTemplateAnnotationWithValue - annotations are nil") + return false + } + + GinkgoWriter.Println("HaveTemplateAnnotationWithValue - Key", key, "Expect:", value, "/ Have:", annotations[key]) + + return annotations[key] == value + + }) +} + +func HaveTolerations(tolerations []corev1.Toleration) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + + templateSpec := ss.Spec.Template.Spec + GinkgoWriter.Println("HaveTolerations - expected:", tolerations, "actual:", templateSpec.Tolerations) + + return reflect.DeepEqual(templateSpec.Tolerations, tolerations) + }) +} + +func HaveContainerImage(containerImage string, containerIndex int) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + + containers := ss.Spec.Template.Spec.Containers + + if len(containers) <= containerIndex { + GinkgoWriter.Println("current container slide has length", len(containers), "index is", containerIndex) + return false + } + + return containers[containerIndex].Image == containerImage + + }) +} + +func NotHaveContainerImage(containerImage string, containerIndex int) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + + containers := ss.Spec.Template.Spec.Containers + + if len(containers) <= containerIndex { + GinkgoWriter.Println("current container slide has length", len(containers), "index is", containerIndex) + return false + } + + GinkgoWriter.Println("NotHaveContainerImage - expected:", containerImage, "actual:", containers[containerIndex].Image) + + return containers[containerIndex].Image != containerImage + + }) +} + +func HaveContainerCommandSubstring(expectedCommandSubstring string, containerIndex int) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + + containers := ss.Spec.Template.Spec.Containers + + if len(containers) <= containerIndex { + GinkgoWriter.Println("current container slice has length", len(containers), "index is", containerIndex) + return false + } + + // Combine Command and Args, adding spaces (' ') between the args + var cmdLine string + + for _, val := range containers[containerIndex].Command { + if val == "" { + cmdLine += "\"\"" + " " + } else { + cmdLine += val + " " + } + } + cmdLine = strings.TrimSpace(cmdLine) + + for _, val := range containers[containerIndex].Args { + if val == "" { + cmdLine += "\"\"" + " " + } else { + cmdLine += val + " " + } + } + cmdLine = strings.TrimSpace(cmdLine) + + GinkgoWriter.Println("HaveContainerCommandSubstring: Have:") + GinkgoWriter.Println(cmdLine) + GinkgoWriter.Println("HaveContainerCommandSubstring: Expect:") + GinkgoWriter.Println(expectedCommandSubstring) + + return strings.Contains(cmdLine, expectedCommandSubstring) + + }) +} + +func HaveContainerWithEnvVar(envKey string, envValue string, containerIndex int) matcher.GomegaMatcher { + return fetchStatefulSet(func(ss *appsv1.StatefulSet) bool { + + containers := ss.Spec.Template.Spec.Containers + + if len(containers) <= containerIndex { + GinkgoWriter.Println("current container slice has length", len(containers), "index is", containerIndex) + return false + } + + container := containers[containerIndex] + + for _, env := range container.Env { + if env.Name == envKey { + GinkgoWriter.Println("HaveContainerWithEnvVar - Key ", envKey, " Expected:", envValue, "Actual:", env.Value) + if env.Value == envValue { + return true + } + } + } + + return false + }) +} + +// This is intentionally NOT exported, for now. Create another function in this file/package that calls this function, and export that. +func fetchStatefulSet(f func(*appsv1.StatefulSet) bool) matcher.GomegaMatcher { + + return WithTransform(func(ss *appsv1.StatefulSet) bool { + + k8sClient, _, err := utils.GetE2ETestKubeClientWithError() + if err != nil { + GinkgoWriter.Println("fetchStatefulSet:", err) + return false + } + + err = k8sClient.Get(context.Background(), client.ObjectKeyFromObject(ss), ss) + if err != nil { + GinkgoWriter.Println("fetchStatefulSet:", err) + return false + } + + return f(ss) + + }, BeTrue()) + +} diff --git a/test/ginkgo/fixture/utils/fixtureUtils.go b/test/ginkgo/fixture/utils/fixtureUtils.go new file mode 100644 index 00000000..80a64cc2 --- /dev/null +++ b/test/ginkgo/fixture/utils/fixtureUtils.go @@ -0,0 +1,134 @@ +package utils + +import ( + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + "sigs.k8s.io/controller-runtime/pkg/client" + + argocdv1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" + + admissionv1 "k8s.io/api/admissionregistration/v1" + apps "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + rbacv1 "k8s.io/api/rbac/v1" + crdv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + + imageUpdater "github.com/argoproj-labs/argocd-image-updater/api/v1alpha1" + + argov1alpha1api "github.com/argoproj-labs/argocd-operator/api/v1alpha1" + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" + + //lint:ignore ST1001 "This is a common practice in Gomega tests for readability." + . "github.com/onsi/gomega" //nolint:all +) + +func GetE2ETestKubeClient() (client.Client, *runtime.Scheme) { + config, err := getSystemKubeConfig() + Expect(err).ToNot(HaveOccurred()) + + k8sClient, scheme, err := getKubeClient(config) + Expect(err).ToNot(HaveOccurred()) + + return k8sClient, scheme +} + +func GetE2ETestKubeClientWithError() (client.Client, *runtime.Scheme, error) { + config, err := getSystemKubeConfig() + if err != nil { + return nil, nil, err + } + + k8sClient, scheme, err := getKubeClient(config) + if err != nil { + return nil, nil, err + } + + return k8sClient, scheme, nil +} + +// getKubeClient returns a controller-runtime Client for accessing K8s API resources used by the controller. +func getKubeClient(config *rest.Config) (client.Client, *runtime.Scheme, error) { + + scheme := runtime.NewScheme() + + if err := corev1.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := apps.AddToScheme(scheme); err != nil { + return nil, nil, err + } + if err := rbacv1.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := admissionv1.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := crdv1.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := argov1beta1api.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := argocdv1alpha1.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := argov1alpha1api.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := networkingv1.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := autoscalingv2.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := batchv1.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + if err := imageUpdater.AddToScheme(scheme); err != nil { + return nil, nil, err + } + + k8sClient, err := client.New(config, client.Options{Scheme: scheme}) + if err != nil { + return nil, nil, err + } + + return k8sClient, scheme, nil + +} + +// Retrieve the system-level Kubernetes config (e.g. ~/.kube/config or service account config from volume) +func getSystemKubeConfig() (*rest.Config, error) { + + overrides := clientcmd.ConfigOverrides{} + + loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() + clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, &overrides) + + restConfig, err := clientConfig.ClientConfig() + if err != nil { + return nil, err + } + + // Increase QPS and Burst to avoid rate limiting issues during cleanup. + // The default values (5 QPS, 10 Burst) can cause "client rate limiter Wait returned an error" + // errors in CI environments with limited resources where cleanup operations are slower. + restConfig.QPS = 50 + restConfig.Burst = 100 + + return restConfig, nil +} diff --git a/test/ginkgo/go.mod b/test/ginkgo/go.mod new file mode 100644 index 00000000..c233b825 --- /dev/null +++ b/test/ginkgo/go.mod @@ -0,0 +1,215 @@ +module github.com/argoproj-labs/argocd-image-updater/test/ginkgo + +go 1.24.6 + +require ( + github.com/argoproj-labs/argocd-image-updater v1.0.1 + github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20251127152419-b7f30e034efa + github.com/argoproj/argo-cd/v3 v3.1.9 + github.com/argoproj/gitops-engine v0.7.1-0.20250905160054-e48120133eec + github.com/onsi/ginkgo/v2 v2.27.2 + github.com/onsi/gomega v1.38.2 + github.com/openshift/api v0.0.0-20240906151052-5d963dce87aa + k8s.io/api v0.33.1 + k8s.io/apiextensions-apiserver v0.33.1 + k8s.io/apimachinery v0.33.1 + k8s.io/client-go v0.33.1 + sigs.k8s.io/controller-runtime v0.21.0 +) + +require ( + cloud.google.com/go/compute/metadata v0.7.0 // indirect + dario.cat/mergo v1.0.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/MakeNowJust/heredoc v1.0.0 // indirect + github.com/Masterminds/semver/v3 v3.4.0 // indirect + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProtonMail/go-crypto v1.1.6 // indirect + github.com/argoproj/pkg v0.13.7-0.20250305113207-cbc37dc61de5 // indirect + github.com/argoproj/pkg/v2 v2.0.1 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/blang/semver/v4 v4.0.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/bombsimon/logrusr/v4 v4.1.0 // indirect + github.com/bradleyfalzon/ghinstallation/v2 v2.17.0 // indirect + github.com/casbin/casbin/v2 v2.123.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/chai2010/gettext-go v1.0.3 // indirect + github.com/cloudflare/circl v1.6.1 // indirect + github.com/cyphar/filepath-securejoin v0.4.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect + github.com/distribution/reference v0.6.0 // indirect + github.com/dlclark/regexp2 v1.11.5 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/evanphx/json-patch/v5 v5.9.11 // indirect + github.com/exponent-io/jsonpath v0.0.0-20210407135951-1de76d718b3f // indirect + github.com/fatih/camelcase v1.0.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect + github.com/go-errors/errors v1.5.1 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect + github.com/go-git/go-billy/v5 v5.6.2 // indirect + github.com/go-git/go-git/v5 v5.16.2 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/zapr v1.3.0 // 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-redis/cache/v9 v9.0.0 // indirect + github.com/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/gobwas/glob v0.2.3 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v4 v4.5.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect + github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect + github.com/google/btree v1.1.3 // indirect + github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/go-github/v69 v69.2.0 // indirect + github.com/google/go-github/v75 v75.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 // indirect + github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jonboulle/clockwork v0.5.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kevinburke/ssh_config v1.2.0 // indirect + github.com/klauspost/compress v1.18.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect + github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/mailru/easyjson v0.9.0 // indirect + github.com/mitchellh/go-wordwrap v1.0.1 // indirect + github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/term v0.5.2 // 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/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect + github.com/opencontainers/go-digest v1.0.0 // indirect + github.com/opencontainers/image-spec v1.1.1 // indirect + github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible // indirect + github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pjbgf/sha1cd v0.3.2 // indirect + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/prometheus/client_golang v1.23.2 // 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/redis/go-redis/v9 v9.8.0 // indirect + github.com/robfig/cron/v3 v3.0.2-0.20210106135023-bc59245fe10e // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.3.1 // indirect + github.com/spf13/cobra v1.10.1 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/vmihailenco/go-tinylfu v0.2.2 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/x448/float16 v0.8.4 // indirect + github.com/xanzy/ssh-agent v0.3.3 // indirect + github.com/xlab/treeprint v1.2.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.31.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/term v0.37.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/time v0.13.0 // indirect + golang.org/x/tools v0.38.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/warnings.v0 v0.1.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/apiserver v0.34.2 // indirect + k8s.io/cli-runtime v0.33.1 // indirect + k8s.io/component-base v0.33.1 // indirect + k8s.io/component-helpers v0.33.1 // indirect + k8s.io/controller-manager v0.33.1 // indirect + k8s.io/klog/v2 v2.130.1 // indirect + k8s.io/kube-aggregator v0.33.1 // indirect + k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a // indirect + k8s.io/kubectl v0.33.1 // indirect + k8s.io/kubernetes v1.33.1 // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + oras.land/oras-go/v2 v2.6.0 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect + sigs.k8s.io/kustomize/api v0.20.1 // indirect + sigs.k8s.io/kustomize/kyaml v0.20.1 // indirect + sigs.k8s.io/randfill v1.0.0 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.7.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace ( + // This replace block is from Argo CD v3.1.9 go.mod + github.com/golang/protobuf => github.com/golang/protobuf v1.5.4 + github.com/grpc-ecosystem/grpc-gateway => github.com/grpc-ecosystem/grpc-gateway v1.16.0 + + // Avoid CVE-2022-3064 + gopkg.in/yaml.v2 => gopkg.in/yaml.v2 v2.4.0 + + // Avoid CVE-2022-28948 + gopkg.in/yaml.v3 => gopkg.in/yaml.v3 v3.0.1 + + k8s.io/api => k8s.io/api v0.33.1 + k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.33.1 + k8s.io/apimachinery => k8s.io/apimachinery v0.33.1 + k8s.io/apiserver => k8s.io/apiserver v0.33.1 + k8s.io/cli-runtime => k8s.io/cli-runtime v0.33.1 + k8s.io/client-go => k8s.io/client-go v0.33.1 + k8s.io/cloud-provider => k8s.io/cloud-provider v0.33.1 + k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.33.1 + k8s.io/code-generator => k8s.io/code-generator v0.33.1 + k8s.io/component-base => k8s.io/component-base v0.33.1 + k8s.io/component-helpers => k8s.io/component-helpers v0.33.1 + k8s.io/controller-manager => k8s.io/controller-manager v0.33.1 + k8s.io/cri-api => k8s.io/cri-api v0.33.1 + k8s.io/cri-client => k8s.io/cri-client v0.33.1 + k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.33.1 + k8s.io/dynamic-resource-allocation => k8s.io/dynamic-resource-allocation v0.33.1 + k8s.io/endpointslice => k8s.io/endpointslice v0.33.1 + k8s.io/externaljwt => k8s.io/externaljwt v0.33.1 + k8s.io/kms => k8s.io/kms v0.33.1 + k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.33.1 + k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.33.1 + k8s.io/kube-proxy => k8s.io/kube-proxy v0.33.1 + k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.33.1 + k8s.io/kubectl => k8s.io/kubectl v0.33.1 + k8s.io/kubelet => k8s.io/kubelet v0.33.1 + k8s.io/kubernetes => k8s.io/kubernetes v1.33.1 + k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.33.1 + k8s.io/metrics => k8s.io/metrics v0.33.1 + k8s.io/mount-utils => k8s.io/mount-utils v0.33.1 + k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.33.1 + k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.33.1 + k8s.io/sample-cli-plugin => k8s.io/sample-cli-plugin v0.33.1 + k8s.io/sample-controller => k8s.io/sample-controller v0.33.1 +) diff --git a/test/ginkgo/go.sum b/test/ginkgo/go.sum new file mode 100644 index 00000000..8aa5e3fc --- /dev/null +++ b/test/ginkgo/go.sum @@ -0,0 +1,608 @@ +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= +dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8= +dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1 h1:5YTBM8QDVIBN3sxBil89WfdAAqDZbyJTgh688DSxX5w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.19.1/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0 h1:MhRfI58HblXzCtWEZCO0feHs8LweePB3s90r7WaR1KU= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.11.0/go.mod h1:okZ+ZURbArNdlJ+ptXoyHNuOETzOl1Oww19rm8I2WLA= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= +github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +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/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw= +github.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= +github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= +github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= +github.com/argoproj-labs/argocd-image-updater v1.0.1 h1:g6WRF33TQ0/CPDndbC97oP0aEqJMEesQenz0Cz8F6XQ= +github.com/argoproj-labs/argocd-image-updater v1.0.1/go.mod h1:PJ+Pb3faVqSzNNs35INUZYtzlaqKvBE2ZgZGdDabJQM= +github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20251127152419-b7f30e034efa h1:5a4R/OOvOnJLrvQp1moOub0h0Q66Ux4XLXqmPjjuhac= +github.com/argoproj-labs/argocd-operator v0.17.0-rc1.0.20251127152419-b7f30e034efa/go.mod h1:JUvpFGuOdBL23437e/IdBsdwUE+69J6LzKQ2Q42ycc0= +github.com/argoproj/argo-cd/v3 v3.1.9 h1:9P9vJKo1RGWu6mtQnGu61r+0h3XKlA2j3kVhwogUQ/0= +github.com/argoproj/argo-cd/v3 v3.1.9/go.mod h1:ZHb/LOz/hr88VWMJiVTd8DGYL7MheHCAT8S6DgYOBFo= +github.com/argoproj/gitops-engine v0.7.1-0.20250905160054-e48120133eec h1:rNAwbRQFvRIuW/e2bU+B10mlzghYXsnwZedYeA7Drz4= +github.com/argoproj/gitops-engine v0.7.1-0.20250905160054-e48120133eec/go.mod h1:aIBEG3ohgaC1gh/sw2On6knkSnXkqRLDoBj234Dqczw= +github.com/argoproj/pkg v0.13.7-0.20250305113207-cbc37dc61de5 h1:YBoLSjpoaJXaXAldVvBRKJuOPvIXz9UOv6S96gMJM/Q= +github.com/argoproj/pkg v0.13.7-0.20250305113207-cbc37dc61de5/go.mod h1:ebVOzFJphdN1p6EG2mIMECv/3Rk/almSaxIYuFAmsSw= +github.com/argoproj/pkg/v2 v2.0.1 h1:O/gCETzB/3+/hyFL/7d/VM/6pSOIRWIiBOTb2xqAHvc= +github.com/argoproj/pkg/v2 v2.0.1/go.mod h1:sdifF6sUTx9ifs38ZaiNMRJuMpSCBB9GulHfbPgQeRE= +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/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/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +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.17.0 h1:SmbUK/GxpAspRjSQbB6ARvH+ArzlNzTtHydNyXUQ6zg= +github.com/bradleyfalzon/ghinstallation/v2 v2.17.0/go.mod h1:vuD/xvJT9Y+ZVZRv4HQ42cMyPFIYqpc7AbB4Gvt/DlY= +github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= +github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= +github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= +github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= +github.com/casbin/casbin/v2 v2.123.0 h1:UkiMllBgn3MrwHGiZTDFVTV9up+W2CRLufZwKiuAmpA= +github.com/casbin/casbin/v2 v2.123.0/go.mod h1:Ee33aqGrmES+GNL17L0h9X28wXuo829wnNUnS0edAco= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/chai2010/gettext-go v1.0.3 h1:9liNh8t+u26xl5ddmWLmsOsdNLwkdRTg5AG+JnTiM80= +github.com/chai2010/gettext-go v1.0.3/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0= +github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= +github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s= +github.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI= +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/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= +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/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= +github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= +github.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o= +github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE= +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/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= +github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= +github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= +github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= +github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +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/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/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= +github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= +github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= +github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.6.2 h1:6Q86EsPXMa7c3YZ3aLAQsMA0VlWmy43r6FHqa/UNbRM= +github.com/go-git/go-billy/v5 v5.6.2/go.mod h1:rcFC2rAsp/erv7CMz9GczHcuD0D32fWzH+MJAU+jaUU= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= +github.com/go-git/go-git/v5 v5.16.2 h1:fT6ZIOjE5iEnkzKyxTHK1W4HGAsPhqEqiSAssSO77hM= +github.com/go-git/go-git/v5 v5.16.2/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8= +github.com/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/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.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +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-playground/webhooks/v6 v6.4.0 h1:KLa6y7bD19N48rxJDHM0DpE3T4grV7GxMy1b/aHMWPY= +github.com/go-playground/webhooks/v6 v6.4.0/go.mod h1:5lBxopx+cAJiBI4+kyRbuHrEi+hYRDdRHuRR4Ya5Ums= +github.com/go-redis/cache/v9 v9.0.0 h1:0thdtFo0xJi0/WXbRVu8B066z8OvVymXTJGaXrVWnN0= +github.com/go-redis/cache/v9 v9.0.0/go.mod h1:cMwi1N8ASBOufbIvk7cdXe2PbPjK/WMRL95FFHWsSgI= +github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +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/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= +github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= +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/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85 h1:04sojTxgYxu1L4Hn7Tgf7UVtIosVa6CuHtvNY+7T1K4= +github.com/gogits/go-gogs-client v0.0.0-20210131175652-1d7215cd8d85/go.mod h1:cY2AIrMgHm6oOHmR7jY+9TtjzSjQ3iG7tURJG3Y6XH0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +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/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= +github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +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/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= +github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/go-github/v69 v69.2.0 h1:wR+Wi/fN2zdUx9YxSmYE0ktiX9IAR/BeePzeaUUbEHE= +github.com/google/go-github/v69 v69.2.0/go.mod h1:xne4jymxLR6Uj9b7J7PyTpkMYstEMMwGZa0Aehh1azM= +github.com/google/go-github/v75 v75.0.0 h1:k7q8Bvg+W5KxRl9Tjq16a9XEgVY1pwuiG5sIL7435Ic= +github.com/google/go-github/v75 v75.0.0/go.mod h1:H3LUJEA1TCrzuUqtdAQniBNwuKiQIqdGKgBo1/M/uqI= +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/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-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8 h1:3DsUAV+VNEQa2CUVLxCY3f87278uWfIDhJnbdvDjvmE= +github.com/google/pprof v0.0.0-20251114195745-4902fdda35c8/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= +github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518 h1:UBg1xk+oAsIVbFuGg6hdfAm7EvCv3EL80vFxJNsslqw= +github.com/google/uuid v1.6.1-0.20241114170450-2d3c2a9cc518/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +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/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-middleware v1.3.0 h1:+9834+KizmvFV7pXQGSXQTsaWhq2GjuNUt0aUU0YBYw= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2 h1:sGm2vDRFUrQJO/Veii4h4zG2vvqG6uWNkBHSTqXOZk0= +github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.2/go.mod h1:wd1YpapPLivG6nQgbf7ZkG1hhSOXDhhn4MLTknx2aAc= +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/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= +github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= +github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= +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= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ktrysmt/go-bitbucket v0.9.86 h1:co80t9wS4kKgRLsvDgi+3wQPiLY30u10ehcTxgXFvzw= +github.com/ktrysmt/go-bitbucket v0.9.86/go.mod h1:/lsYmiQrBHNPTnPKF0Q+safIS7peInQOGbJXu0xPRho= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= +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/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= +github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +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/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= +github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +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/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/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= +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/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/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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= +github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= +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.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= +github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= +github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= +github.com/onsi/ginkgo/v2 v2.3.0/go.mod h1:Eew0uilEqZmIEZr8JrvYlvOM7Rr6xzTmMV8AyFNU9d0= +github.com/onsi/ginkgo/v2 v2.4.0/go.mod h1:iHkDK1fKGcBoEHT5W7YBq4RFWaQulw+caOMkAt4OrFo= +github.com/onsi/ginkgo/v2 v2.5.0/go.mod h1:Luc4sArBICYCS8THh8v3i3i5CuSZO+RaQRaJoeNwomw= +github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +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.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= +github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= +github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= +github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= +github.com/onsi/gomega v1.22.1/go.mod h1:x6n7VNe4hw0vkyYUM4mjIXx3JbLiPaBPNgB7PRQ1tuM= +github.com/onsi/gomega v1.24.0/go.mod h1:Z/NWtiqwBrwUt4/2loMmHL63EDLnYHmVbuBpDr2vQAg= +github.com/onsi/gomega v1.24.1/go.mod h1:3AOiACssS3/MajrniINInwbfOOtfZvplPzuRSmvt1jM= +github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= +github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= +github.com/openshift/api v0.0.0-20240906151052-5d963dce87aa h1:RMI6Xa+l8KriyoxsRO/swMDPyCwrxJNA9H67K0Jod/w= +github.com/openshift/api v0.0.0-20240906151052-5d963dce87aa/go.mod h1:yimSGmjsI+XF1mr+AKBs2//fSXIOhhetHGbMlBEfXbs= +github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible h1:IWzUvJ72xMjmrjR9q3H1PF+jwdN0uNQiR2t1BLNalyo= +github.com/patrickmn/go-cache v2.1.1-0.20191004192108-46f407853014+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +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/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4= +github.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= +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= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.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/r3labs/diff/v3 v3.0.1 h1:CBKqf3XmNRHXKmdU7mZP1w7TV0pDyVCis1AUHtA4Xtg= +github.com/r3labs/diff/v3 v3.0.1/go.mod h1:f1S9bourRbiM66NskseyUdo0fTmEE0qKrikYJX63dgo= +github.com/redis/go-redis/v9 v9.0.0-rc.4/go.mod h1:Vo3EsyWnicKnSKCA7HhgnvnyA74wOA69Cd2Meli5mmA= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= +github.com/robfig/cron/v3 v3.0.2-0.20210106135023-bc59245fe10e h1:0xChnl3lhHiXbgSJKgChye0D+DvoItkOdkGcwelDXH0= +github.com/robfig/cron/v3 v3.0.2-0.20210106135023-bc59245fe10e/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +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/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8= +github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +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/vmihailenco/go-tinylfu v0.2.2 h1:H1eiG6HM36iniK6+21n9LLpzx1G9R3DJa2UjUjbynsI= +github.com/vmihailenco/go-tinylfu v0.2.2/go.mod h1:CutYi2Q9puTxfcolkliPq4npPuofg9N9t8JVrjzwa3Q= +github.com/vmihailenco/msgpack/v5 v5.3.4/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +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/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= +github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= +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.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM= +go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= +go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= +go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= +go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= +go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +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/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= +go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o= +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.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.0.0-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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +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.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-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-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= +golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= +golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= +golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= +golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= +golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU= +golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI= +golang.org/x/time v0.13.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-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +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-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= +golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= +gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= +google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b h1:ULiyYQ0FdsJhwwZUwbaXpZF5yUE3h+RA+gxvBu37ucc= +google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:oDOGiMSXHL4sDTJvFvIB9nRQCGdLP1o/iVaqQK8zB+M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 h1:Wgl1rcDNThT+Zn47YyCXOXyX/COgMTIdhJ717F0l4xk= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= +gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +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/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= +k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= +k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= +k8s.io/apiserver v0.33.1 h1:yLgLUPDVC6tHbNcw5uE9mo1T6ELhJj7B0geifra3Qdo= +k8s.io/apiserver v0.33.1/go.mod h1:VMbE4ArWYLO01omz+k8hFjAdYfc3GVAYPrhP2tTKccs= +k8s.io/cli-runtime v0.33.1 h1:TvpjEtF71ViFmPeYMj1baZMJR4iWUEplklsUQ7D3quA= +k8s.io/cli-runtime v0.33.1/go.mod h1:9dz5Q4Uh8io4OWCLiEf/217DXwqNgiTS/IOuza99VZE= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= +k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/component-helpers v0.33.1 h1:DdQMww8jOr+sGhIrkz70Lp9Qerq/JzeZDBRd508DHDo= +k8s.io/component-helpers v0.33.1/go.mod h1:LQwxW5L3dH7341Unj+phndJu0Ic5UjxA//7FT8YVP5U= +k8s.io/controller-manager v0.33.1 h1:ZYTzGp2f9TVhHCvrgSQtc367yR+D3UditkHDHCZc2GU= +k8s.io/controller-manager v0.33.1/go.mod h1:p1yW7I5NFIuhXvSW9Wa/MdN3oIqXd2DRDgacb/hcUF0= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-aggregator v0.33.1 h1:PigQUqAvd6Y4hBjQAqhKz3lEJC2VHLL4bSOEuS06a40= +k8s.io/kube-aggregator v0.33.1/go.mod h1:16/wlU5Lj7hNJSv7JSu5FLvxyrgiJVLCHzfVoECAsuI= +k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a h1:ZV3Zr+/7s7aVbjNGICQt+ppKWsF1tehxggNfbM7XnG8= +k8s.io/kube-openapi v0.0.0-20250610211856-8b98d1ed966a/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= +k8s.io/kubectl v0.33.1 h1:OJUXa6FV5bap6iRy345ezEjU9dTLxqv1zFTVqmeHb6A= +k8s.io/kubectl v0.33.1/go.mod h1:Z07pGqXoP4NgITlPRrnmiM3qnoo1QrK1zjw85Aiz8J0= +k8s.io/kubernetes v1.33.1 h1:86+VVY/f11taZdpEZrNciLw1MIQhu6BFXf/OMFn5EUg= +k8s.io/kubernetes v1.33.1/go.mod h1:2nWuPk0seE4+6sd0x60wQ6rYEXcV7SoeMbU0YbFm/5k= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +oras.land/oras-go/v2 v2.6.0 h1:X4ELRsiGkrbeox69+9tzTu492FMUu7zJQW6eJU+I2oc= +oras.land/oras-go/v2 v2.6.0/go.mod h1:magiQDfG6H1O9APp+rOsvCPcW1GD2MM7vgnKY0Y+u1o= +sigs.k8s.io/controller-runtime v0.21.0 h1:CYfjpEuicjUecRk+KAeyYh+ouUBn4llGyDYytIGcJS8= +sigs.k8s.io/controller-runtime v0.21.0/go.mod h1:OSg14+F65eWqIu4DceX7k/+QRAbTTvxeQSNSOQpukWM= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/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/kyaml v0.20.1 h1:PCMnA2mrVbRP3NIB6v9kYCAc38uvFLVs8j/CD567A78= +sigs.k8s.io/kustomize/kyaml v0.20.1/go.mod h1:0EmkQHRUsJxY8Ug9Niig1pUMSCGHxQ5RklbpV/Ri6po= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= +sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +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/test/ginkgo/parallel/.gitignore b/test/ginkgo/parallel/.gitignore new file mode 100644 index 00000000..801bbb9a --- /dev/null +++ b/test/ginkgo/parallel/.gitignore @@ -0,0 +1 @@ +ginkgo.report diff --git a/test/ginkgo/parallel/1-001_validate_image_updater_test.go b/test/ginkgo/parallel/1-001_validate_image_updater_test.go new file mode 100644 index 00000000..a6b22ecc --- /dev/null +++ b/test/ginkgo/parallel/1-001_validate_image_updater_test.go @@ -0,0 +1,196 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parallel + +import ( + "context" + + applicationFixture "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/application" + appv1alpha1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1" + "github.com/argoproj/gitops-engine/pkg/health" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + imageUpdaterApi "github.com/argoproj-labs/argocd-image-updater/api/v1alpha1" + + "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture" + argocdFixture "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/argocd" + deplFixture "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/deployment" + k8sFixture "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/k8s" + ssFixture "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/statefulset" + fixtureUtils "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture/utils" + argov1beta1api "github.com/argoproj-labs/argocd-operator/api/v1beta1" +) + +var _ = Describe("GitOps Operator Parallel E2E Tests", func() { + + Context("1-001_validate_image_updater_test", func() { + + var ( + k8sClient client.Client + ctx context.Context + ns *corev1.Namespace + cleanupFunc func() + imageUpdater *imageUpdaterApi.ImageUpdater + argoCD *argov1beta1api.ArgoCD + ) + + BeforeEach(func() { + fixture.EnsureParallelCleanSlate() + + k8sClient, _ = fixtureUtils.GetE2ETestKubeClient() + ctx = context.Background() + }) + + AfterEach(func() { + if imageUpdater != nil { + By("deleting ImageUpdater CR") + Expect(k8sClient.Delete(ctx, imageUpdater)).To(Succeed()) + Eventually(imageUpdater, "2m", "5s").Should(k8sFixture.NotExistByName()) + } + + // Delete the ArgoCD CR before namespace deletion to allow the operator + // to properly clean up all managed resources. This prevents finalizer + // issues and rate limiting during cleanup in CI environments. + if argoCD != nil { + By("deleting ArgoCD CR") + Expect(k8sClient.Delete(ctx, argoCD)).To(Succeed()) + Eventually(argoCD, "5m", "10s").Should(k8sFixture.NotExistByName()) + } + + if cleanupFunc != nil { + cleanupFunc() + } + + fixture.OutputDebugOnFail(ns) + + }) + + It("ensures that Image Updater will update Argo CD Application to the latest image", func() { + + By("creating simple namespace-scoped Argo CD instance with image updater enabled") + ns, cleanupFunc = fixture.CreateRandomE2ETestNamespaceWithCleanupFunc() + + argoCD = &argov1beta1api.ArgoCD{ + ObjectMeta: metav1.ObjectMeta{Name: "argocd", Namespace: ns.Name}, + Spec: argov1beta1api.ArgoCDSpec{ + ImageUpdater: argov1beta1api.ArgoCDImageUpdaterSpec{ + Env: []corev1.EnvVar{ + { + Name: "IMAGE_UPDATER_LOGLEVEL", + Value: "trace", + }, + }, + Enabled: true}, + }, + } + Expect(k8sClient.Create(ctx, argoCD)).To(Succeed()) + + By("waiting for ArgoCD CR to be reconciled and the instance to be ready") + Eventually(argoCD, "5m", "5s").Should(argocdFixture.BeAvailable()) + + By("verifying all workloads are started") + deploymentsShouldExist := []string{"argocd-redis", "argocd-server", "argocd-repo-server", "argocd-argocd-image-updater-controller"} + for _, depl := range deploymentsShouldExist { + depl := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: depl, Namespace: ns.Name}} + Eventually(depl).Should(k8sFixture.ExistByName()) + Eventually(depl).Should(deplFixture.HaveReplicas(1)) + Eventually(depl, "3m", "5s").Should(deplFixture.HaveReadyReplicas(1), depl.Name+" was not ready") + } + + statefulSet := &appsv1.StatefulSet{ObjectMeta: metav1.ObjectMeta{Name: "argocd-application-controller", Namespace: ns.Name}} + Eventually(statefulSet).Should(k8sFixture.ExistByName()) + Eventually(statefulSet).Should(ssFixture.HaveReplicas(1)) + Eventually(statefulSet, "3m", "5s").Should(ssFixture.HaveReadyReplicas(1)) + + By("creating Application") + app := &appv1alpha1.Application{ + ObjectMeta: metav1.ObjectMeta{ + Name: "app-01", + Namespace: ns.Name, + }, + Spec: appv1alpha1.ApplicationSpec{ + Project: "default", + Source: &appv1alpha1.ApplicationSource{ + RepoURL: "https://github.com/argoproj-labs/argocd-image-updater/", + Path: "test/e2e/testdata/005-public-guestbook", + TargetRevision: "HEAD", + }, + Destination: appv1alpha1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: ns.Name, + }, + SyncPolicy: &appv1alpha1.SyncPolicy{Automated: &appv1alpha1.SyncPolicyAutomated{}}, + }, + } + Expect(k8sClient.Create(ctx, app)).To(Succeed()) + + By("verifying deploying the Application succeeded") + Eventually(app, "4m", "5s").Should(applicationFixture.HaveHealthStatusCode(health.HealthStatusHealthy)) + Eventually(app, "4m", "5s").Should(applicationFixture.HaveSyncStatusCode(appv1alpha1.SyncStatusCodeSynced)) + + By("creating ImageUpdater CR") + updateStrategy := "semver" + imageUpdater = &imageUpdaterApi.ImageUpdater{ + ObjectMeta: metav1.ObjectMeta{ + Name: "image-updater", + Namespace: ns.Name, + }, + Spec: imageUpdaterApi.ImageUpdaterSpec{ + Namespace: ns.Name, + ApplicationRefs: []imageUpdaterApi.ApplicationRef{ + { + NamePattern: "app*", + Images: []imageUpdaterApi.ImageConfig{ + { + Alias: "guestbook", + ImageName: "quay.io/dkarpele/my-guestbook:~29437546.0", + CommonUpdateSettings: &imageUpdaterApi.CommonUpdateSettings{ + UpdateStrategy: &updateStrategy, + }, + }, + }, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, imageUpdater)).To(Succeed()) + + By("ensuring that the Application image has `29437546.0` version after update") + Eventually(func() string { + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(app), app) + + if err != nil { + return "" // Let Eventually retry on error + } + + // Nil-safe check: The Kustomize block is only added by the Image Updater after its first run. + // We must check that it and its Images field exist before trying to access them. + if app.Spec.Source.Kustomize != nil && len(app.Spec.Source.Kustomize.Images) > 0 { + return string(app.Spec.Source.Kustomize.Images[0]) + } + + // Return an empty string to signify the condition is not yet met. + return "" + }, "5m", "10s").Should(Equal("quay.io/dkarpele/my-guestbook:29437546.0")) + }) + }) +}) diff --git a/test/ginkgo/parallel/suite_test.go b/test/ginkgo/parallel/suite_test.go new file mode 100644 index 00000000..81fe802b --- /dev/null +++ b/test/ginkgo/parallel/suite_test.go @@ -0,0 +1,52 @@ +/* +Copyright 2025. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package parallel + +import ( + "testing" + + // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) + // to ensure that exec-entrypoint and run can make use of them. + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + _ "k8s.io/client-go/plugin/pkg/client/auth" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/argoproj-labs/argocd-image-updater/test/ginkgo/fixture" + //+kubebuilder:scaffold:imports +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +func TestParallelSuite(t *testing.T) { + + RegisterFailHandler(Fail) + RunSpecs(t, "Parallel Test Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + // Before the test suite starts, ensure that operator is restored to default state, and all previous sequential/parallel test resources are deleted + fixture.EnsureSequentialCleanSlate() // (don't change this to parallel) +}) + +// AfterSuite is intentionally empty - cleanup is handled per-test via deferred functions +var _ = AfterSuite(func() {}) diff --git a/test/ginkgo/prereqs/kustomization.yaml b/test/ginkgo/prereqs/kustomization.yaml new file mode 100644 index 00000000..6fabfeef --- /dev/null +++ b/test/ginkgo/prereqs/kustomization.yaml @@ -0,0 +1,4 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - github.com/argoproj-labs/argocd-operator/config/default?ref=v0.17.0-rc1 diff --git a/test/ginkgo/sequential/.gitignore b/test/ginkgo/sequential/.gitignore new file mode 100644 index 00000000..801bbb9a --- /dev/null +++ b/test/ginkgo/sequential/.gitignore @@ -0,0 +1 @@ +ginkgo.report