diff --git a/docs/user-guide/helm.md b/docs/user-guide/helm.md index 3b5a5de0dc262..455e682e33bab 100644 --- a/docs/user-guide/helm.md +++ b/docs/user-guide/helm.md @@ -236,9 +236,6 @@ source: value: path/to/file.ext ``` -!!! warning "Reference in multiple sources not supported" - Please note that using a multiple sources application will not let you load the file by reference. See [argoproj/argo-cd#13220](https://github.com/argoproj/argo-cd/issues/13220) - ## Helm Release Name By default, the Helm release name is equal to the Application name to which it belongs. Sometimes, especially on a centralised Argo CD, diff --git a/reposerver/repository/repository.go b/reposerver/repository/repository.go index 9356f2d0062c7..d23bdab80cad9 100644 --- a/reposerver/repository/repository.go +++ b/reposerver/repository/repository.go @@ -472,7 +472,13 @@ func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.Applicat return repoRefs, nil } - for _, valueFile := range source.ValueFiles { + refFileParams := make([]string, 0) + for _, fileParam := range source.FileParameters { + refFileParams = append(refFileParams, fileParam.Path) + } + refCandidates := append(source.ValueFiles, refFileParams...) + + for _, valueFile := range refCandidates { if strings.HasPrefix(valueFile, "$") { refVar := strings.Split(valueFile, "/")[0] @@ -710,8 +716,14 @@ func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA, // check whether they should be replicated in resolveReferencedSources. if q.HasMultipleSources { if q.ApplicationSource.Helm != nil { + refFileParams := make([]string, 0) + for _, fileParam := range q.ApplicationSource.Helm.FileParameters { + refFileParams = append(refFileParams, fileParam.Path) + } + refCandidates := append(q.ApplicationSource.Helm.ValueFiles, refFileParams...) + // Checkout every one of the referenced sources to the target revision before generating Manifests - for _, valueFile := range q.ApplicationSource.Helm.ValueFiles { + for _, valueFile := range refCandidates { if strings.HasPrefix(valueFile, "$") { refVar := strings.Split(valueFile, "/")[0] @@ -1158,9 +1170,19 @@ func helmTemplate(appPath string, repoRoot string, env *v1alpha1.Env, q *apiclie } } for _, p := range appHelm.FileParameters { - resolvedPath, _, err := pathutil.ResolveValueFilePathOrUrl(appPath, repoRoot, env.Envsubst(p.Path), q.GetValuesFileSchemes()) - if err != nil { - return nil, fmt.Errorf("error resolving helm value file path: %w", err) + var resolvedPath pathutil.ResolvedFilePath + referencedSource := getReferencedSource(p.Path, q.RefSources) + if referencedSource != nil { + // If the $-prefixed path appears to reference another source, do env substitution _after_ resolving the source + resolvedPath, err = getResolvedRefValueFile(p.Path, env, q.GetValuesFileSchemes(), referencedSource.Repo.Repo, gitRepoPaths, referencedSource.Repo.Project) + if err != nil { + return nil, fmt.Errorf("error resolving set-file path: %w", err) + } + } else { + resolvedPath, _, err = pathutil.ResolveValueFilePathOrUrl(appPath, repoRoot, env.Envsubst(p.Path), q.GetValuesFileSchemes()) + if err != nil { + return nil, fmt.Errorf("error resolving helm value file path: %w", err) + } } templateOpts.SetFile[p.Name] = resolvedPath } diff --git a/reposerver/repository/repository_test.go b/reposerver/repository/repository_test.go index 06177b28a1d83..69e051a87f5e1 100644 --- a/reposerver/repository/repository_test.go +++ b/reposerver/repository/repository_test.go @@ -13,6 +13,7 @@ import ( "path" "path/filepath" "regexp" + "slices" "sort" "strings" "sync" @@ -2228,6 +2229,72 @@ func TestGenerateManifestWithAnnotatedTagsAndMultiSourceApp(t *testing.T) { } } +func TestGenerateMultiSourceHelmWithFileParameter(t *testing.T) { + expectedFileContent, err := os.ReadFile("../../util/helm/testdata/external/external-secret.txt") + assert.NoError(t, err) + + service := newService(t, "../../util/helm/testdata") + + testCases := []struct { + name string + refSources map[string]*argoappv1.RefTarget + expectedContent string + expectedErr bool + }{{ + name: "Successfully resolve multi-source ref for helm set-file", + refSources: map[string]*argoappv1.RefTarget{ + "$global": { + TargetRevision: "HEAD", + }, + }, + expectedContent: string(expectedFileContent), + expectedErr: false, + }, { + name: "Failed to resolve multi-source ref for helm set-file", + refSources: map[string]*argoappv1.RefTarget{}, + expectedContent: "DOES-NOT-EXIST", + expectedErr: true, + }} + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + manifestRequest := &apiclient.ManifestRequest{ + Repo: &argoappv1.Repository{}, + ApplicationSource: &argoappv1.ApplicationSource{ + Ref: "$global", + Path: "./redis", + TargetRevision: "HEAD", + Helm: &argoappv1.ApplicationSourceHelm{ + ValueFiles: []string{"$global/redis/values-production.yaml"}, + FileParameters: []argoappv1.HelmFileParameter{{ + Name: "passwordContent", + Path: "$global/external/external-secret.txt", + }}, + }, + }, + HasMultipleSources: true, + NoCache: true, + RefSources: tc.refSources, + } + + res, err := service.GenerateManifest(context.Background(), manifestRequest) + + if !tc.expectedErr { + assert.NoError(t, err) + + // Check that any of the manifests contains the secret + idx := slices.IndexFunc(res.Manifests, func(content string) bool { + return strings.Contains(content, tc.expectedContent) + }) + assert.GreaterOrEqual(t, idx, 0, "No manifest contains the value set with the helm fileParameters") + } else { + assert.Error(t, err) + } + }) + } +} + func TestFindResources(t *testing.T) { testCases := []struct { name string