Skip to content

Commit

Permalink
feat: Support alias for helm repo in application source
Browse files Browse the repository at this point in the history
Signed-off-by: Guillaume Doussin <guillaume.doussin@gmail.com>
  • Loading branch information
OpenGuidou committed Apr 18, 2024
1 parent 1f8acf4 commit 878bbaa
Show file tree
Hide file tree
Showing 12 changed files with 823 additions and 198 deletions.
32 changes: 32 additions & 0 deletions docs/user-guide/helm.md
Expand Up @@ -47,6 +47,38 @@ spec:

See [here](../operator-manual/declarative-setup.md#helm-chart-repositories) for more info about how to configure private Helm repositories.

## Repository alias

You can use the repository alias, as you have named it inside the repositoriesn as a repoURL in the source:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: sealed-secrets
namespace: argocd
spec:
project: default
source:
chart: sealed-secrets
repoURL: @sealed-secrets # supports also the format alias:sealed-secrets
targetRevision: 1.16.1
helm:
releaseName: sealed-secrets
destination:
server: "https://kubernetes.default.svc"
namespace: kubeseal
```

Do not forget to add the corresponding entry in the project source repositories whitelist, if needed. You have to specify both syntaxes to allow them to work.

```yaml
spec:
sourceRepos:
- '@sealed-secrets'
- 'alias:sealed-secrets'
```

## Values Files

Helm has the ability to use a different, or even multiple "values.yaml" files to derive its
Expand Down
555 changes: 402 additions & 153 deletions reposerver/apiclient/repository.pb.go

Large diffs are not rendered by default.

97 changes: 77 additions & 20 deletions reposerver/repository/repository.go
Expand Up @@ -294,7 +294,9 @@ func (s *Service) runRepoOperation(
operation func(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc) error,
settings operationSettings,
hasMultipleSources bool,
refSources map[string]*v1alpha1.RefTarget) error {
refSources map[string]*v1alpha1.RefTarget,
helmRepos []*v1alpha1.Repository,
helmRepoCreds []*v1alpha1.RepoCreds) error {

if sanitizer, ok := grpc.SanitizerFromContext(ctx); ok {
// make sure a randomized path replaced with '.' in the error message
Expand All @@ -308,7 +310,7 @@ func (s *Service) runRepoOperation(
revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
unresolvedRevision := revision
if source.IsHelm() {
helmClient, revision, err = s.newHelmClientResolveRevision(repo, revision, source.Chart, settings.noCache || settings.noRevisionCache)
helmClient, revision, err = s.newHelmClientResolveRevision(repo, revision, source.Chart, settings.noCache || settings.noRevisionCache, helmRepos, helmRepoCreds)
if err != nil {
return err
}
Expand Down Expand Up @@ -561,7 +563,7 @@ func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestReq
}

settings := operationSettings{sem: s.parallelismLimitSemaphore, noCache: q.NoCache, noRevisionCache: q.NoRevisionCache, allowConcurrent: q.ApplicationSource.AllowsConcurrentProcessing()}
err = s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature, cacheFn, operation, settings, q.HasMultipleSources, q.RefSources)
err = s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature, cacheFn, operation, settings, q.HasMultipleSources, q.RefSources, q.Repos, q.HelmRepoCreds)

// if the tarDoneCh message is sent it means that the manifest
// generation is being managed by the cmp-server. In this case
Expand Down Expand Up @@ -978,18 +980,31 @@ func getHelmRepos(appPath string, repositories []*v1alpha1.Repository, helmRepoC
reposByName[repo.Name] = repo
}
}
return toHelmRepo(retrieveRepoCredentials(dependencies, repositories, reposByName, reposByUrl, helmRepoCreds)), nil
}

func toHelmRepo(repos []*v1alpha1.Repository) []helm.HelmRepository {
helmRepos := make([]helm.HelmRepository, 0)
for _, repo := range repos {
helmRepos = append(helmRepos, helm.HelmRepository{Name: repo.Name, Repo: repo.Repo, Creds: repo.GetHelmCreds(), EnableOci: repo.EnableOCI})
}
return helmRepos
}

func retrieveRepoCredentials(reposToComplete []*v1alpha1.Repository, completedRepos []*v1alpha1.Repository, reposByName map[string]*v1alpha1.Repository, reposByUrl map[string]*v1alpha1.Repository,
helmRepoCreds []*v1alpha1.RepoCreds) []*v1alpha1.Repository {

repos := make([]helm.HelmRepository, 0)
for _, dep := range dependencies {
repos := make([]*v1alpha1.Repository, 0)
for _, rep := range reposToComplete {
// find matching repo credentials by URL or name
repo, ok := reposByUrl[dep.Repo]
if !ok && dep.Name != "" {
repo, ok = reposByName[dep.Name]
repo, ok := reposByUrl[rep.Repo]
if !ok && rep.Name != "" {
repo, ok = reposByName[rep.Name]
}
if !ok {
// if no matching repo credentials found, use the repo creds from the credential list
repo = &v1alpha1.Repository{Repo: dep.Repo, Name: dep.Name, EnableOCI: dep.EnableOCI}
if repositoryCredential := getRepoCredential(helmRepoCreds, dep.Repo); repositoryCredential != nil {
repo = &v1alpha1.Repository{Repo: rep.Repo, Name: rep.Name, EnableOCI: rep.EnableOCI}
if repositoryCredential := getRepoCredential(helmRepoCreds, rep.Repo); repositoryCredential != nil {
repo.EnableOCI = repositoryCredential.EnableOCI
repo.Password = repositoryCredential.Password
repo.Username = repositoryCredential.Username
Expand All @@ -999,18 +1014,30 @@ func getHelmRepos(appPath string, repositories []*v1alpha1.Repository, helmRepoC
} else if repo.EnableOCI {
// finally if repo is OCI and no credentials found, use the first OCI credential matching by hostname
// see https://github.com/argoproj/argo-cd/issues/14636
for _, cred := range repositories {
if depURL, err := url.Parse("oci://" + dep.Repo); err == nil && cred.EnableOCI && depURL.Host == cred.Repo {
for _, cred := range completedRepos {
if depURL, err := url.Parse("oci://" + rep.Repo); err == nil && cred.EnableOCI && depURL.Host == cred.Repo {
repo.Username = cred.Username
repo.Password = cred.Password
break
}
}
}
}
repos = append(repos, helm.HelmRepository{Name: repo.Name, Repo: repo.Repo, Creds: repo.GetHelmCreds(), EnableOci: repo.EnableOCI})
// Only add it once in the list
if !isRepoInRepoList(repo, repos) {
repos = append(repos, repo)
}
}
return repos, nil
return repos
}

func isRepoInRepoList(repo *v1alpha1.Repository, repos []*v1alpha1.Repository) bool {
for _, r := range repos {
if r.Repo == repo.Repo {
return true
}
}
return false
}

type dependencies struct {
Expand Down Expand Up @@ -2007,7 +2034,7 @@ func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppD
}

settings := operationSettings{allowConcurrent: q.Source.AllowsConcurrentProcessing(), noCache: q.NoCache, noRevisionCache: q.NoCache || q.NoRevisionCache}
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, false, cacheFn, operation, settings, false, nil)
err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, false, cacheFn, operation, settings, false, nil, nil, nil)

return res, err
}
Expand Down Expand Up @@ -2294,7 +2321,7 @@ func (s *Service) GetRevisionChartDetails(ctx context.Context, q *apiclient.Repo
log.Warnf("revision metadata cache error %s/%s/%s: %v", q.Repo.Repo, q.Name, q.Revision, err)
}
}
helmClient, revision, err := s.newHelmClientResolveRevision(q.Repo, q.Revision, q.Name, true)
helmClient, revision, err := s.newHelmClientResolveRevision(q.Repo, q.Revision, q.Name, true, q.Repos, q.HelmRepoCreds)
if err != nil {
return nil, fmt.Errorf("helm client error: %v", err)
}
Expand Down Expand Up @@ -2350,9 +2377,39 @@ func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision s
return gitClient, commitSHA, nil
}

func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noRevisionCache bool) (helm.Client, string, error) {
enableOCI := repo.EnableOCI || helm.IsHelmOciRepo(repo.Repo)
helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, repo.Proxy, helm.WithIndexCache(s.cache), helm.WithChartPaths(s.chartPaths))
func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noRevisionCache bool, repositories []*v1alpha1.Repository, helmRepoCreds []*v1alpha1.RepoCreds) (helm.Client, string, error) {

var completedRepo *v1alpha1.Repository
reposByName := make(map[string]*v1alpha1.Repository)
reposByUrl := make(map[string]*v1alpha1.Repository)
for _, repo := range repositories {
reposByUrl[repo.Repo] = repo
if repo.Name != "" {
reposByName[repo.Name] = repo
}
}
if strings.HasPrefix(repo.Repo, "@") {
repo.Name = repo.Repo[1:]
repo.Repo = ""
completedRepo = retrieveRepoCredentials([]*v1alpha1.Repository{repo}, []*v1alpha1.Repository{}, reposByName, reposByUrl, helmRepoCreds)[0]
if completedRepo.Repo == "" {
// URL could not be resolved from list of repo added
return nil, "", fmt.Errorf("repo %s not found, please add it to the repository list", repo.Name)
}
} else if strings.HasPrefix(repo.Repo, "alias:") {
repo.Name = strings.TrimPrefix(repo.Repo, "alias:")
repo.Repo = ""
completedRepo = retrieveRepoCredentials([]*v1alpha1.Repository{repo}, []*v1alpha1.Repository{}, reposByName, reposByUrl, helmRepoCreds)[0]
if completedRepo.Repo == "" {
// URL could not be resolved from list of repo added
return nil, "", fmt.Errorf("repo %s not found, please add it to the repository list", repo.Name)
}
} else {
completedRepo = repo
}

enableOCI := completedRepo.EnableOCI || helm.IsHelmOciRepo(completedRepo.Repo)
helmClient := s.newHelmClient(completedRepo.Repo, completedRepo.GetHelmCreds(), enableOCI, completedRepo.Proxy, helm.WithIndexCache(s.cache), helm.WithChartPaths(s.chartPaths))
if helm.IsVersion(revision) {
return helmClient, revision, nil
}
Expand Down Expand Up @@ -2514,7 +2571,7 @@ func (s *Service) ResolveRevision(ctx context.Context, q *apiclient.ResolveRevis
var revision string
var source = app.Spec.GetSource()
if source.IsHelm() {
_, revision, err := s.newHelmClientResolveRevision(repo, ambiguousRevision, source.Chart, true)
_, revision, err := s.newHelmClientResolveRevision(repo, ambiguousRevision, source.Chart, true, q.Repos, q.HelmRepoCreds)

if err != nil {
return &apiclient.ResolveRevisionResponse{Revision: "", AmbiguousRevision: ""}, err
Expand Down
4 changes: 4 additions & 0 deletions reposerver/repository/repository.proto
Expand Up @@ -73,6 +73,8 @@ message ResolveRevisionRequest {
github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.Repository repo = 1;
github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.Application app = 2;
string ambiguousRevision = 3;
repeated github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.Repository repos = 4;
repeated github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.RepoCreds helmRepoCreds = 5;
}

// ResolveRevisionResponse
Expand Down Expand Up @@ -164,6 +166,8 @@ message RepoServerRevisionChartDetailsRequest {
string name = 2;
// the revision within the chart
string revision = 3;
repeated github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.Repository repos = 4;
repeated github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.RepoCreds helmRepoCreds = 5;
}

// HelmAppSpec contains helm app name in source repo
Expand Down
110 changes: 104 additions & 6 deletions reposerver/repository/repository_test.go
Expand Up @@ -569,8 +569,106 @@ func TestHelmChartReferencingExternalValues_InvalidRefs(t *testing.T) {

request = &apiclient.ManifestRequest{Repo: &argoappv1.Repository{}, ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: refSources, HasMultipleSources: true, ProjectName: "something",
ProjectSourceRepos: []string{"*"}}
response, err = service.GenerateManifest(context.Background(), request)
_, err = service.GenerateManifest(context.Background(), request)
assert.Error(t, err)

}

func TestHelmChartFromRepoAlias(t *testing.T) {
service := newService(t, ".")
spec := argoappv1.ApplicationSpec{
Sources: []argoappv1.ApplicationSource{
{
RepoURL: "@custom-repo",
Chart: "my-chart",
TargetRevision: "1.1.0",
},
},
}
request := &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{
Repo: "@custom-repo",
},
Repos: []*argoappv1.Repository{
{
Name: "custom-repo",
Repo: "https://helm.example.com",
},
},
ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: make(map[string]*argoappv1.RefTarget), HasMultipleSources: true, ProjectName: "something",
ProjectSourceRepos: []string{"*"},
HelmRepoCreds: []*argoappv1.RepoCreds{
{URL: "https://helm.example.com", Username: "test", Password: "test", EnableOCI: true},
},
}
response, err := service.GenerateManifest(context.Background(), request)
assert.NoError(t, err)
assert.NotNil(t, response)
assert.Equal(t, &apiclient.ManifestResponse{
Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
Namespace: "",
Server: "",
Revision: "1.1.0",
SourceType: "Helm",
}, response)
}

func TestHelmChartFromRepoAliasWithOtherSyntax(t *testing.T) {
service := newService(t, ".")
spec := argoappv1.ApplicationSpec{
Sources: []argoappv1.ApplicationSource{
{
RepoURL: "alias:custom-repo",
Chart: "my-chart",
TargetRevision: "1.1.0",
},
},
}
request := &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{
Repo: "alias:custom-repo",
},
Repos: []*argoappv1.Repository{
{
Name: "custom-repo",
Repo: "https://helm.example.com",
},
},
ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: make(map[string]*argoappv1.RefTarget), HasMultipleSources: true, ProjectName: "something",
ProjectSourceRepos: []string{"*"},
}
response, err := service.GenerateManifest(context.Background(), request)
assert.NoError(t, err)
assert.NotNil(t, response)
assert.Equal(t, &apiclient.ManifestResponse{
Manifests: []string{"{\"apiVersion\":\"v1\",\"kind\":\"ConfigMap\",\"metadata\":{\"name\":\"my-map\"}}"},
Namespace: "",
Server: "",
Revision: "1.1.0",
SourceType: "Helm",
}, response)
}

func TestHelmChartFromRepoAliasWithoutRepositoryAdded(t *testing.T) {
service := newService(t, ".")
spec := argoappv1.ApplicationSpec{
Sources: []argoappv1.ApplicationSource{
{
RepoURL: "alias:custom-repo",
Chart: "my-chart",
TargetRevision: "1.1.0",
},
},
}
request := &apiclient.ManifestRequest{
Repo: &argoappv1.Repository{
Repo: "alias:custom-repo",
},
ApplicationSource: &spec.Sources[0], NoCache: true, RefSources: make(map[string]*argoappv1.RefTarget), HasMultipleSources: true, ProjectName: "something",
ProjectSourceRepos: []string{"*"},
}
response, err := service.GenerateManifest(context.Background(), request)
assert.Error(t, err, "repo custom-repo not found, please add it to the repository list")
assert.Nil(t, response)
}

Expand Down Expand Up @@ -1798,11 +1896,11 @@ func TestService_newHelmClientResolveRevision(t *testing.T) {
service := newService(t, ".")

t.Run("EmptyRevision", func(t *testing.T) {
_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "", true)
_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "", "", true, []*v1alpha1.Repository{}, []*argoappv1.RepoCreds{})
assert.EqualError(t, err, "invalid revision '': improper constraint: ")
})
t.Run("InvalidRevision", func(t *testing.T) {
_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "???", "", true)
_, _, err := service.newHelmClientResolveRevision(&argoappv1.Repository{}, "???", "", true, []*v1alpha1.Repository{}, []*argoappv1.RepoCreds{})
assert.EqualError(t, err, "invalid revision '???': improper constraint: ???", true)
})
}
Expand Down Expand Up @@ -3078,9 +3176,9 @@ func TestGetHelmRepo_NamedReposAlias(t *testing.T) {
helmRepos, err := getHelmRepos("./testdata/helm-with-dependencies-alias", q.Repos, q.HelmRepoCreds)
assert.Nil(t, err)

assert.Equal(t, len(helmRepos), 1)
assert.Equal(t, helmRepos[0].Username, "test-alias")
assert.Equal(t, helmRepos[0].Repo, "https://example.com")
assert.Equal(t, 1, len(helmRepos))
assert.Equal(t, "test-alias", helmRepos[0].Username)
assert.Equal(t, "https://example.com", helmRepos[0].Repo)
}

func Test_getResolvedValueFiles(t *testing.T) {
Expand Down
Expand Up @@ -5,3 +5,7 @@ dependencies:
- name: helm
repository: "alias:custom-repo-alias"
version: v1.0.0
- name: helm-2
repository: "@custom-repo-alias"
version: v1.0.0

0 comments on commit 878bbaa

Please sign in to comment.