diff --git a/.github/workflows/image-reuse.yaml b/.github/workflows/image-reuse.yaml index 9cdfbc181d766..62d280c25e5aa 100644 --- a/.github/workflows/image-reuse.yaml +++ b/.github/workflows/image-reuse.yaml @@ -143,7 +143,7 @@ jobs: - name: Build and push container image id: image - uses: docker/build-push-action@4a13e500e55cf31b7a5d59a38ab2040ab0f42f56 #v5.1.0 + uses: docker/build-push-action@af5a7ed5ba88268d5278f7203fb52cd833f66d6e #v5.2.0 with: context: . platforms: ${{ inputs.platforms }} diff --git a/Makefile b/Makefile index c807af951e270..96275f9bff76e 100644 --- a/Makefile +++ b/Makefile @@ -23,6 +23,14 @@ KUBECTL_VERSION=$(shell go list -m k8s.io/client-go | head -n 1 | rev | cut -d' GOPATH?=$(shell if test -x `which go`; then go env GOPATH; else echo "$(HOME)/go"; fi) GOCACHE?=$(HOME)/.cache/go-build +# Docker command to use +DOCKER?=docker +ifeq ($(DOCKER),podman) +PODMAN_ARGS=--userns keep-id +else +PODMAN_ARGS= +endif + DOCKER_SRCDIR?=$(GOPATH)/src DOCKER_WORKDIR?=/go/src/github.com/argoproj/argo-cd @@ -76,7 +84,7 @@ SUDO?= # Runs any command in the argocd-test-utils container in server mode # Server mode container will start with uid 0 and drop privileges during runtime define run-in-test-server - $(SUDO) docker run --rm -it \ + $(SUDO) $(DOCKER) run --rm -it \ --name argocd-test-server \ -u $(CONTAINER_UID):$(CONTAINER_GID) \ -e USER_ID=$(CONTAINER_UID) \ @@ -101,13 +109,14 @@ define run-in-test-server -p ${ARGOCD_E2E_APISERVER_PORT}:8080 \ -p 4000:4000 \ -p 5000:5000 \ + $(PODMAN_ARGS) \ $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \ bash -c "$(1)" endef # Runs any command in the argocd-test-utils container in client mode define run-in-test-client - $(SUDO) docker run --rm -it \ + $(SUDO) $(DOCKER) run --rm -it \ --name argocd-test-client \ -u $(CONTAINER_UID):$(CONTAINER_GID) \ -e HOME=/home/user \ @@ -122,13 +131,14 @@ define run-in-test-client -v ${HOME}/.kube:/home/user/.kube${VOLUME_MOUNT} \ -v /tmp:/tmp${VOLUME_MOUNT} \ -w ${DOCKER_WORKDIR} \ + $(PODMAN_ARGS) \ $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) \ bash -c "$(1)" endef # define exec-in-test-server - $(SUDO) docker exec -it -u $(CONTAINER_UID):$(CONTAINER_GID) -e ARGOCD_E2E_RECORD=$(ARGOCD_E2E_RECORD) -e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) argocd-test-server $(1) + $(SUDO) $(DOCKER) exec -it -u $(CONTAINER_UID):$(CONTAINER_GID) -e ARGOCD_E2E_RECORD=$(ARGOCD_E2E_RECORD) -e ARGOCD_E2E_K3S=$(ARGOCD_E2E_K3S) argocd-test-server $(1) endef PATH:=$(PATH):$(PWD)/hack @@ -249,8 +259,8 @@ release-cli: clean-debug build-ui .PHONY: test-tools-image test-tools-image: ifndef SKIP_TEST_TOOLS_IMAGE - $(SUDO) docker build --build-arg UID=$(CONTAINER_UID) -t $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) -f test/container/Dockerfile . - $(SUDO) docker tag $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) + $(SUDO) $(DOCKER) build --build-arg UID=$(CONTAINER_UID) -t $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) -f test/container/Dockerfile . + $(SUDO) $(DOCKER) tag $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE) $(TEST_TOOLS_PREFIX)$(TEST_TOOLS_IMAGE):$(TEST_TOOLS_TAG) endif .PHONY: manifests-local @@ -280,9 +290,9 @@ controller: .PHONY: build-ui build-ui: - DOCKER_BUILDKIT=1 docker build -t argocd-ui --platform=$(TARGET_ARCH) --target argocd-ui . + DOCKER_BUILDKIT=1 $(DOCKER) build -t argocd-ui --platform=$(TARGET_ARCH) --target argocd-ui . find ./ui/dist -type f -not -name gitkeep -delete - docker run -v ${CURRENT_DIR}/ui/dist/app:/tmp/app --rm -t argocd-ui sh -c 'cp -r ./dist/app/* /tmp/app/' + $(DOCKER) run -v ${CURRENT_DIR}/ui/dist/app:/tmp/app --rm -t argocd-ui sh -c 'cp -r ./dist/app/* /tmp/app/' .PHONY: image ifeq ($(DEV_IMAGE), true) @@ -291,7 +301,7 @@ ifeq ($(DEV_IMAGE), true) # the dist directory is under .dockerignore. IMAGE_TAG="dev-$(shell git describe --always --dirty)" image: build-ui - DOCKER_BUILDKIT=1 docker build --platform=$(TARGET_ARCH) -t argocd-base --target argocd-base . + DOCKER_BUILDKIT=1 $(DOCKER) build --platform=$(TARGET_ARCH) -t argocd-base --target argocd-base . CGO_ENABLED=${CGO_FLAG} GOOS=linux GOARCH=amd64 GODEBUG="tarinsecurepath=0,zipinsecurepath=0" go build -v -ldflags '${LDFLAGS}' -o ${DIST_DIR}/argocd ./cmd ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-server ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-application-controller @@ -299,21 +309,21 @@ image: build-ui ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-cmp-server ln -sfn ${DIST_DIR}/argocd ${DIST_DIR}/argocd-dex cp Dockerfile.dev dist - DOCKER_BUILDKIT=1 docker build --platform=$(TARGET_ARCH) -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) -f dist/Dockerfile.dev dist + DOCKER_BUILDKIT=1 $(DOCKER) build --platform=$(TARGET_ARCH) -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) -f dist/Dockerfile.dev dist else image: - DOCKER_BUILDKIT=1 docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) --platform=$(TARGET_ARCH) . + DOCKER_BUILDKIT=1 $(DOCKER) build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) --platform=$(TARGET_ARCH) . endif - @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) ; fi + @if [ "$(DOCKER_PUSH)" = "true" ] ; then $(DOCKER) push $(IMAGE_PREFIX)argocd:$(IMAGE_TAG) ; fi .PHONY: armimage armimage: - docker build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)-arm . + $(DOCKER) build -t $(IMAGE_PREFIX)argocd:$(IMAGE_TAG)-arm . .PHONY: builder-image builder-image: - docker build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder . - @if [ "$(DOCKER_PUSH)" = "true" ] ; then docker push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) ; fi + $(DOCKER) build -t $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) --target builder . + @if [ "$(DOCKER_PUSH)" = "true" ] ; then $(DOCKER) push $(IMAGE_PREFIX)argo-cd-ci-builder:$(IMAGE_TAG) ; fi .PHONY: mod-download mod-download: test-tools-image @@ -424,7 +434,7 @@ debug-test-client: test-tools-image # Starts e2e server in a container .PHONY: start-e2e start-e2e: test-tools-image - docker version + $(DOCKER) version mkdir -p ${GOCACHE} $(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-e2e-local) @@ -471,7 +481,7 @@ clean: clean-debug .PHONY: start start: test-tools-image - docker version + $(DOCKER) version $(call run-in-test-server,make ARGOCD_PROCFILE=test/container/Procfile start-local ARGOCD_START=${ARGOCD_START}) # Starts a local instance of ArgoCD @@ -521,7 +531,7 @@ build-docs-local: .PHONY: build-docs build-docs: - docker run ${MKDOCS_RUN_ARGS} --rm -it -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs build' + $(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs build' .PHONY: serve-docs-local serve-docs-local: @@ -529,7 +539,7 @@ serve-docs-local: .PHONY: serve-docs serve-docs: - docker run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs serve -a $$(ip route get 1 | awk '\''{print $$7}'\''):8000' + $(DOCKER) run ${MKDOCS_RUN_ARGS} --rm -it -p 8000:8000 -v ${CURRENT_DIR}:/docs -w /docs --entrypoint "" ${MKDOCS_DOCKER_IMAGE} sh -c 'pip install -r docs/requirements.txt; mkdocs serve -a $$(ip route get 1 | awk '\''{print $$7}'\''):8000' # Verify that kubectl can connect to your K8s cluster from Docker .PHONY: verify-kube-connect diff --git a/applicationset/controllers/applicationset_controller.go b/applicationset/controllers/applicationset_controller.go index 4f5ac66fc016d..e1275e75d3ba2 100644 --- a/applicationset/controllers/applicationset_controller.go +++ b/applicationset/controllers/applicationset_controller.go @@ -124,18 +124,20 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque // Log a warning if there are unrecognized generators _ = utils.CheckInvalidGenerators(&applicationSetInfo) // desiredApplications is the main list of all expected Applications from all generators in this appset. - desiredApplications, applicationSetReason, err := r.generateApplications(logCtx, applicationSetInfo) - if err != nil { + desiredApplications, applicationSetReason, generatorsErr := r.generateApplications(logCtx, applicationSetInfo) + if generatorsErr != nil { _ = r.setApplicationSetStatusCondition(ctx, &applicationSetInfo, argov1alpha1.ApplicationSetCondition{ Type: argov1alpha1.ApplicationSetConditionErrorOccurred, - Message: err.Error(), + Message: generatorsErr.Error(), Reason: string(applicationSetReason), Status: argov1alpha1.ApplicationSetConditionStatusTrue, }, parametersGenerated, ) - return ctrl.Result{}, err + if len(desiredApplications) < 1 { + return ctrl.Result{}, generatorsErr + } } parametersGenerated = true @@ -309,7 +311,7 @@ func (r *ApplicationSetReconciler) Reconcile(ctx context.Context, req ctrl.Reque requeueAfter := r.getMinRequeueAfter(&applicationSetInfo) - if len(validateErrors) == 0 { + if len(validateErrors) == 0 && generatorsErr == nil { if err := r.setApplicationSetStatusCondition(ctx, &applicationSetInfo, argov1alpha1.ApplicationSetCondition{ diff --git a/applicationset/controllers/applicationset_controller_test.go b/applicationset/controllers/applicationset_controller_test.go index 81fbad95ac50b..c3c5f3845bea5 100644 --- a/applicationset/controllers/applicationset_controller_test.go +++ b/applicationset/controllers/applicationset_controller_test.go @@ -2423,6 +2423,91 @@ func TestReconcilerValidationProjectErrorBehaviour(t *testing.T) { assert.Error(t, err) } +func TestReconcilerCreateAppsRecoveringRenderError(t *testing.T) { + + scheme := runtime.NewScheme() + err := v1alpha1.AddToScheme(scheme) + assert.Nil(t, err) + err = v1alpha1.AddToScheme(scheme) + assert.Nil(t, err) + + project := v1alpha1.AppProject{ + ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "argocd"}, + } + appSet := v1alpha1.ApplicationSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "name", + Namespace: "argocd", + }, + Spec: v1alpha1.ApplicationSetSpec{ + GoTemplate: true, + Generators: []v1alpha1.ApplicationSetGenerator{ + { + List: &v1alpha1.ListGenerator{ + Elements: []apiextensionsv1.JSON{{ + Raw: []byte(`{"name": "very-good-app"}`), + }, { + Raw: []byte(`{"name": "bad-app"}`), + }}, + }, + }, + }, + Template: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{ + Name: "{{ index (splitList \"-\" .name ) 2 }}", + Namespace: "argocd", + }, + Spec: v1alpha1.ApplicationSpec{ + Source: &v1alpha1.ApplicationSource{RepoURL: "https://github.com/argoproj/argocd-example-apps", Path: "guestbook"}, + Project: "default", + Destination: v1alpha1.ApplicationDestination{Server: "https://kubernetes.default.svc"}, + }, + }, + }, + } + + kubeclientset := kubefake.NewSimpleClientset() + argoDBMock := dbmocks.ArgoDB{} + argoObjs := []runtime.Object{&project} + + client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(&appSet).WithIndex(&v1alpha1.Application{}, ".metadata.controller", appControllerIndexer).Build() + + r := ApplicationSetReconciler{ + Client: client, + Scheme: scheme, + Renderer: &utils.Render{}, + Recorder: record.NewFakeRecorder(1), + Cache: &fakeCache{}, + Generators: map[string]generators.Generator{ + "List": generators.NewListGenerator(), + }, + ArgoDB: &argoDBMock, + ArgoAppClientset: appclientset.NewSimpleClientset(argoObjs...), + KubeClientset: kubeclientset, + Policy: v1alpha1.ApplicationsSyncPolicySync, + ArgoCDNamespace: "argocd", + } + + req := ctrl.Request{ + NamespacedName: types.NamespacedName{ + Namespace: "argocd", + Name: "name", + }, + } + + // Verify that on generatorsError, no error is returned, but the object is requeued + res, err := r.Reconcile(context.Background(), req) + assert.Nil(t, err) + assert.True(t, res.RequeueAfter == ReconcileRequeueOnValidationError) + + var app v1alpha1.Application + + // make sure good app got created + err = r.Client.Get(context.TODO(), crtclient.ObjectKey{Namespace: "argocd", Name: "app"}, &app) + assert.NoError(t, err) + assert.Equal(t, app.Name, "app") +} + func TestSetApplicationSetStatusCondition(t *testing.T) { scheme := runtime.NewScheme() err := v1alpha1.AddToScheme(scheme) diff --git a/docs/developer-guide/toolchain-guide.md b/docs/developer-guide/toolchain-guide.md index 335180438dac6..9bba72b456f71 100644 --- a/docs/developer-guide/toolchain-guide.md +++ b/docs/developer-guide/toolchain-guide.md @@ -138,6 +138,14 @@ The following steps are required no matter whether you chose to use a virtualize export SUDO=sudo ``` + If you have podman installed, you can also leverage its rootless mode. In + order to use podman for running and testing Argo CD locally, set the + `DOCKER` environment variable to `podman` before you run `make`, e.g. + + ``` + DOCKER=podman make start + ``` + ### Clone the Argo CD repository from your personal fork on GitHub * `mkdir -p ~/go/src/github.com/argoproj` diff --git a/docs/operator-manual/argocd-cm.yaml b/docs/operator-manual/argocd-cm.yaml index a291a57a4c9dd..49458d40be929 100644 --- a/docs/operator-manual/argocd-cm.yaml +++ b/docs/operator-manual/argocd-cm.yaml @@ -235,14 +235,6 @@ data: # can be either empty, "normal" or "strict". By default, it is empty i.e. disabled. resource.respectRBAC: "normal" - # Configuration to add a config management plugin. - configManagementPlugins: | - - name: kasane - init: - command: [kasane, update] - generate: - command: [kasane, show] - # A set of settings that allow enabling or disabling the config management tool. # If unset, each defaults to "true". kustomize.enabled: true @@ -413,4 +405,4 @@ data: # Mandatory if multiple services are specified. cluster: name: some-cluster - server: https://some-cluster \ No newline at end of file + server: https://some-cluster diff --git a/docs/operator-manual/declarative-setup.md b/docs/operator-manual/declarative-setup.md index 1f7f9ab76f273..2f26a633fd0a2 100644 --- a/docs/operator-manual/declarative-setup.md +++ b/docs/operator-manual/declarative-setup.md @@ -670,9 +670,9 @@ extended to allow assumption of multiple roles, either as an explicit array of r "Statement" : { "Effect" : "Allow", "Action" : "sts:AssumeRole", - "Principal" : { - "AWS" : ":role/" - } + "Resource" : [ + ":role/" + ] } } ``` diff --git a/test/container/Dockerfile b/test/container/Dockerfile index 8c51aa2df59b7..2452507014385 100644 --- a/test/container/Dockerfile +++ b/test/container/Dockerfile @@ -6,9 +6,9 @@ FROM docker.io/library/redis:7.2.4@sha256:e647cfe134bf5e8e74e620f66346f93418acfc RUN ln -s /usr/lib/$(uname -m)-linux-gnu /usr/lib/linux-gnu # Please make sure to also check the contained yarn version and update the references below when upgrading this image's version -FROM docker.io/library/node:21.6.2@sha256:65998e325b06014d4f1417a8a6afb1540d1ac66521cca76f2221a6953947f9ee as node +FROM docker.io/library/node:21.7.1@sha256:f358dfc9506428df0b6c5bf41b198c4b93413c5e4c75e34c55f6474b964e8a0e as node -FROM docker.io/library/golang:1.21.3@sha256:02d7116222536a5cf0fcf631f90b507758b669648e0f20186d2dc94a9b419a9b as golang +FROM docker.io/library/golang:1.22.1@sha256:34ce21a9696a017249614876638ea37ceca13cdd88f582caad06f87a8aa45bf3 as golang FROM docker.io/library/registry:2.8@sha256:f4e1b878d4bc40a1f65532d68c94dcfbab56aa8cba1f00e355a206e7f6cc9111 as registry @@ -49,7 +49,7 @@ ENV GOPATH /go COPY hack/install.sh hack/tool-versions.sh go.* ./ COPY hack/installers installers -RUN ./install.sh helm-linux && \ +RUN ./install.sh helm && \ ./install.sh kustomize && \ ./install.sh codegen-tools && \ ./install.sh codegen-go-tools && \ diff --git a/test/e2e/applicationset_test.go b/test/e2e/applicationset_test.go index 5b9b8190c5437..0d4d8ea3498f5 100644 --- a/test/e2e/applicationset_test.go +++ b/test/e2e/applicationset_test.go @@ -523,6 +523,100 @@ func TestSimpleListGeneratorGoTemplate(t *testing.T) { } +func TestCreateApplicationDespiteParamsError(t *testing.T) { + expectedErrorMessage := `failed to execute go template {{.cluster}}-guestbook: template: :1:2: executing "" at <.cluster>: map has no entry for key "cluster"` + expectedConditionsParamsError := []v1alpha1.ApplicationSetCondition{ + { + Type: v1alpha1.ApplicationSetConditionErrorOccurred, + Status: v1alpha1.ApplicationSetConditionStatusTrue, + Message: expectedErrorMessage, + Reason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError, + }, + { + Type: v1alpha1.ApplicationSetConditionParametersGenerated, + Status: v1alpha1.ApplicationSetConditionStatusFalse, + Message: expectedErrorMessage, + Reason: v1alpha1.ApplicationSetReasonErrorOccurred, + }, + { + Type: v1alpha1.ApplicationSetConditionResourcesUpToDate, + Status: v1alpha1.ApplicationSetConditionStatusFalse, + Message: expectedErrorMessage, + Reason: v1alpha1.ApplicationSetReasonRenderTemplateParamsError, + }, + } + expectedApp := argov1alpha1.Application{ + TypeMeta: metav1.TypeMeta{ + Kind: application.ApplicationKind, + APIVersion: "argoproj.io/v1alpha1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: "my-cluster-guestbook", + Namespace: fixture.TestNamespace(), + Finalizers: []string{"resources-finalizer.argocd.argoproj.io"}, + }, + Spec: argov1alpha1.ApplicationSpec{ + Project: "default", + Source: &argov1alpha1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + }, + Destination: argov1alpha1.ApplicationDestination{ + Server: "https://kubernetes.default.svc", + Namespace: "guestbook", + }, + }, + } + + Given(t). + // Create a ListGenerator-based ApplicationSet + When().Create(v1alpha1.ApplicationSet{ObjectMeta: metav1.ObjectMeta{ + Name: "simple-list-generator", + }, + Spec: v1alpha1.ApplicationSetSpec{ + GoTemplate: true, + GoTemplateOptions: []string{"missingkey=error"}, + Template: v1alpha1.ApplicationSetTemplate{ + ApplicationSetTemplateMeta: v1alpha1.ApplicationSetTemplateMeta{Name: "{{.cluster}}-guestbook"}, + Spec: argov1alpha1.ApplicationSpec{ + Project: "default", + Source: &argov1alpha1.ApplicationSource{ + RepoURL: "https://github.com/argoproj/argocd-example-apps.git", + TargetRevision: "HEAD", + Path: "guestbook", + }, + Destination: argov1alpha1.ApplicationDestination{ + Server: "{{.url}}", + Namespace: "guestbook", + }, + }, + }, + Generators: []v1alpha1.ApplicationSetGenerator{ + { + List: &v1alpha1.ListGenerator{ + Elements: []apiextensionsv1.JSON{ + { + Raw: []byte(`{"cluster": "my-cluster","url": "https://kubernetes.default.svc"}`), + }, + { + Raw: []byte(`{"invalidCluster": "invalid-cluster","url": "https://kubernetes.default.svc"}`), + }}, + }, + }, + }, + }, + }).Then().Expect(ApplicationsExist([]argov1alpha1.Application{expectedApp})). + + // verify the ApplicationSet status conditions were set correctly + Expect(ApplicationSetHasConditions("simple-list-generator", expectedConditionsParamsError)). + + // Delete the ApplicationSet, and verify it deletes the Applications + When(). + Delete().Then().Expect(ApplicationsDoNotExist([]argov1alpha1.Application{expectedApp})) + +} + func TestRenderHelmValuesObject(t *testing.T) { expectedApp := argov1alpha1.Application{ diff --git a/util/helm/client.go b/util/helm/client.go index 2b9e2912349cf..75bd30d1fea13 100644 --- a/util/helm/client.go +++ b/util/helm/client.go @@ -8,7 +8,6 @@ import ( "encoding/json" "errors" "fmt" - executil "github.com/argoproj/argo-cd/v2/util/exec" "io" "net/http" "net/url" @@ -19,6 +18,8 @@ import ( "strings" "time" + executil "github.com/argoproj/argo-cd/v2/util/exec" + "github.com/argoproj/pkg/sync" log "github.com/sirupsen/logrus" "gopkg.in/yaml.v2" @@ -34,6 +35,8 @@ import ( var ( globalLock = sync.NewKeyLock() indexLock = sync.NewKeyLock() + + OCINotEnabledErr = errors.New("could not perform the action when oci is not enabled") ) type Creds struct { @@ -401,6 +404,10 @@ func getIndexURL(rawURL string) (string, error) { } func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) { + if !c.enableOci { + return nil, OCINotEnabledErr + } + tagsURL := strings.Replace(fmt.Sprintf("%s/%s", c.repoURL, chart), "https://", "", 1) indexLock.Lock(tagsURL) defer indexLock.Unlock(tagsURL) @@ -428,10 +435,12 @@ func (c *nativeHelmChart) GetTags(chart string, noCache bool) (*TagsList, error) TLSClientConfig: tlsConf, DisableKeepAlives: true, }} + + repoHost, _, _ := strings.Cut(tagsURL, "/") repo.Client = &auth.Client{ Client: client, Cache: nil, - Credential: auth.StaticCredential(c.repoURL, auth.Credential{ + Credential: auth.StaticCredential(repoHost, auth.Credential{ Username: c.creds.Username, Password: c.creds.Password, }), diff --git a/util/helm/client_test.go b/util/helm/client_test.go index 3cda26feb5f0e..6fba279df07d0 100644 --- a/util/helm/client_test.go +++ b/util/helm/client_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "math" + "net/url" "os" "strings" "testing" @@ -159,41 +160,129 @@ func TestGetIndexURL(t *testing.T) { } func TestGetTagsFromUrl(t *testing.T) { + t.Run("should return tags correctly while following the link header", func(t *testing.T) { + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + t.Logf("called %s", r.URL.Path) + responseTags := TagsList{} + w.Header().Set("Content-Type", "application/json") + if !strings.Contains(r.URL.String(), "token") { + w.Header().Set("Link", fmt.Sprintf("; rel=next", r.Host, r.URL.Path)) + responseTags.Tags = []string{"first"} + } else { + responseTags.Tags = []string{ + "second", + "2.8.0", + "2.8.0-prerelease", + "2.8.0_build", + "2.8.0-prerelease_build", + "2.8.0-prerelease.1_build.1234", + } + } + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(responseTags) + if err != nil { + t.Fatal(err) + } + })) + + client := NewClient(server.URL, Creds{InsecureSkipVerify: true}, true, "") + + tags, err := client.GetTags("mychart", true) + assert.NoError(t, err) + assert.ElementsMatch(t, tags.Tags, []string{ + "first", + "second", + "2.8.0", + "2.8.0-prerelease", + "2.8.0+build", + "2.8.0-prerelease+build", + "2.8.0-prerelease.1+build.1234", + }) + }) + + t.Run("should return an error not when oci is not enabled", func(t *testing.T) { + client := NewClient("example.com", Creds{}, false, "") + + _, err := client.GetTags("my-chart", true) + assert.ErrorIs(t, OCINotEnabledErr, err) + }) +} + +func TestGetTagsFromURLPrivateRepoAuthentication(t *testing.T) { server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { t.Logf("called %s", r.URL.Path) - responseTags := TagsList{} - w.Header().Set("Content-Type", "application/json") - if !strings.Contains(r.URL.String(), "token") { - w.Header().Set("Link", fmt.Sprintf("; rel=next", r.Host, r.URL.Path)) - responseTags.Tags = []string{"first"} - } else { - responseTags.Tags = []string{ - "second", + + authorization := r.Header.Get("Authorization") + if authorization == "" { + w.Header().Set("WWW-Authenticate", `Basic realm="helm repo to get tags"`) + w.WriteHeader(http.StatusUnauthorized) + return + } + + t.Logf("authorization received %s", authorization) + + responseTags := TagsList{ + Tags: []string{ "2.8.0", "2.8.0-prerelease", "2.8.0_build", "2.8.0-prerelease_build", "2.8.0-prerelease.1_build.1234", - } + }, } + + w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) err := json.NewEncoder(w).Encode(responseTags) if err != nil { t.Fatal(err) } })) + t.Cleanup(server.Close) - client := NewClient(server.URL, Creds{InsecureSkipVerify: true}, true, "") - - tags, err := client.GetTags("mychart", true) + serverURL, err := url.Parse(server.URL) assert.NoError(t, err) - assert.ElementsMatch(t, tags.Tags, []string{ - "first", - "second", - "2.8.0", - "2.8.0-prerelease", - "2.8.0+build", - "2.8.0-prerelease+build", - "2.8.0-prerelease.1+build.1234", - }) + + testCases := []struct { + name string + repoURL string + }{ + { + name: "should login correctly when the repo path is in the server root with http scheme", + repoURL: server.URL, + }, + { + name: "should login correctly when the repo path is not in the server root with http scheme", + repoURL: fmt.Sprintf("%s/my-repo", server.URL), + }, + { + name: "should login correctly when the repo path is in the server root without http scheme", + repoURL: serverURL.Host, + }, + { + name: "should login correctly when the repo path is not in the server root without http scheme", + repoURL: fmt.Sprintf("%s/my-repo", serverURL.Host), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + client := NewClient(testCase.repoURL, Creds{ + InsecureSkipVerify: true, + Username: "my-username", + Password: "my-password", + }, true, "") + + tags, err := client.GetTags("mychart", true) + + assert.NoError(t, err) + assert.ElementsMatch(t, tags.Tags, []string{ + "2.8.0", + "2.8.0-prerelease", + "2.8.0+build", + "2.8.0-prerelease+build", + "2.8.0-prerelease.1+build.1234", + }) + }) + } } diff --git a/util/notification/argocd/service.go b/util/notification/argocd/service.go index 426217318ce31..106f0d1ee5c24 100644 --- a/util/notification/argocd/service.go +++ b/util/notification/argocd/service.go @@ -108,11 +108,14 @@ func (svc *argoCDService) GetAppDetails(ctx context.Context, appSource *v1alpha1 var has *shared.CustomHelmAppSpec if appDetail.Helm != nil { has = &shared.CustomHelmAppSpec{ - Name: appDetail.Helm.Name, - ValueFiles: appDetail.Helm.ValueFiles, - Parameters: appDetail.Helm.Parameters, - Values: appDetail.Helm.Values, - FileParameters: appDetail.Helm.FileParameters, + HelmAppSpec: apiclient.HelmAppSpec{ + Name: appDetail.Helm.Name, + ValueFiles: appDetail.Helm.ValueFiles, + Parameters: appDetail.Helm.Parameters, + Values: appDetail.Helm.Values, + FileParameters: appDetail.Helm.FileParameters, + }, + HelmParameterOverrides: appSource.Helm.Parameters, } } return &shared.AppDetail{ diff --git a/util/notification/expression/shared/appdetail.go b/util/notification/expression/shared/appdetail.go index 6edf00c3bfe9a..2e069e072186c 100644 --- a/util/notification/expression/shared/appdetail.go +++ b/util/notification/expression/shared/appdetail.go @@ -1,6 +1,7 @@ package shared import ( + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" "time" "github.com/argoproj/argo-cd/v2/reposerver/apiclient" @@ -28,24 +29,32 @@ type AppDetail struct { Directory *apiclient.DirectoryAppSpec } -type CustomHelmAppSpec apiclient.HelmAppSpec +type CustomHelmAppSpec struct { + HelmAppSpec apiclient.HelmAppSpec + HelmParameterOverrides []v1alpha1.HelmParameter +} func (has CustomHelmAppSpec) GetParameterValueByName(Name string) string { - var value string - for i := range has.Parameters { - if has.Parameters[i].Name == Name { - value = has.Parameters[i].Value - break + // Check in overrides first + for i := range has.HelmParameterOverrides { + if has.HelmParameterOverrides[i].Name == Name { + return has.HelmParameterOverrides[i].Value + } + } + + for i := range has.HelmAppSpec.Parameters { + if has.HelmAppSpec.Parameters[i].Name == Name { + return has.HelmAppSpec.Parameters[i].Value } } - return value + return "" } func (has CustomHelmAppSpec) GetFileParameterPathByName(Name string) string { var path string - for i := range has.FileParameters { - if has.FileParameters[i].Name == Name { - path = has.FileParameters[i].Path + for i := range has.HelmAppSpec.FileParameters { + if has.HelmAppSpec.FileParameters[i].Name == Name { + path = has.HelmAppSpec.FileParameters[i].Path break } } diff --git a/util/notification/expression/shared/appdetail_test.go b/util/notification/expression/shared/appdetail_test.go new file mode 100644 index 0000000000000..65482199b9047 --- /dev/null +++ b/util/notification/expression/shared/appdetail_test.go @@ -0,0 +1,30 @@ +package shared + +import ( + "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" + "github.com/argoproj/argo-cd/v2/reposerver/apiclient" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetParameterValueByName(t *testing.T) { + helmAppSpec := CustomHelmAppSpec{ + HelmAppSpec: apiclient.HelmAppSpec{ + Parameters: []*v1alpha1.HelmParameter{ + { + Name: "param1", + Value: "value1", + }, + }, + }, + HelmParameterOverrides: []v1alpha1.HelmParameter{ + { + Name: "param1", + Value: "value-override", + }, + }, + } + + value := helmAppSpec.GetParameterValueByName("param1") + assert.Equal(t, "value-override", value) +}