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 May 13, 2024
1 parent c3b89ca commit 7b93cad
Show file tree
Hide file tree
Showing 30 changed files with 15,791 additions and 207 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
560 changes: 405 additions & 155 deletions reposerver/apiclient/repository.pb.go

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions reposerver/askpass/askpass.swagger.json
@@ -0,0 +1,60 @@
{
"swagger": "2.0",
"info": {
"title": "reposerver/askpass/askpass.proto",
"version": "version not set"
},
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"paths": {},
"definitions": {
"askpassCredentialsResponse": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"password": {
"type": "string"
}
}
},
"protobufAny": {
"type": "object",
"properties": {
"type_url": {
"type": "string"
},
"value": {
"type": "string",
"format": "byte"
}
}
},
"runtimeError": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"code": {
"type": "integer",
"format": "int32"
},
"message": {
"type": "string"
},
"details": {
"type": "array",
"items": {
"$ref": "#/definitions/protobufAny"
}
}
}
}
}
}
100 changes: 74 additions & 26 deletions reposerver/repository/repository.go
Expand Up @@ -55,6 +55,7 @@ import (
"github.com/argoproj/argo-cd/v2/util/gpg"
"github.com/argoproj/argo-cd/v2/util/grpc"
"github.com/argoproj/argo-cd/v2/util/helm"
helmAlias "github.com/argoproj/argo-cd/v2/util/helm"
"github.com/argoproj/argo-cd/v2/util/io"
"github.com/argoproj/argo-cd/v2/util/io/files"
pathutil "github.com/argoproj/argo-cd/v2/util/io/path"
Expand Down Expand Up @@ -294,7 +295,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 +311,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 +564,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 +981,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 +1015,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 All @@ -1034,13 +1062,10 @@ func getHelmDependencyRepos(appPath string) ([]*v1alpha1.Repository, error) {
}

for _, r := range d.Dependencies {
if strings.HasPrefix(r.Repository, "@") {
alias := helmAlias.GetRepoNameFromAlias(r.Repository)
if alias != "" {
repos = append(repos, &v1alpha1.Repository{
Name: r.Repository[1:],
})
} else if strings.HasPrefix(r.Repository, "alias:") {
repos = append(repos, &v1alpha1.Repository{
Name: strings.TrimPrefix(r.Repository, "alias:"),
Name: alias,
})
} else if u, err := url.Parse(r.Repository); err == nil && (u.Scheme == "https" || u.Scheme == "oci") {
repo := &v1alpha1.Repository{
Expand Down Expand Up @@ -2007,7 +2032,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 +2319,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 +2375,32 @@ 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
}
}
alias := helmAlias.GetRepoNameFromAlias(repo.Repo)
if alias != "" {
repo.Repo = ""
repo.Name = alias
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 +2562,7 @@ func (s *Service) ResolveRevision(ctx context.Context, q *apiclient.ResolveRevis
var revision string
var source = app.Spec.GetSourcePtrByIndex(int(q.SourceIndex))
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 @@ -74,6 +74,8 @@ message ResolveRevisionRequest {
github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.Application app = 2;
string ambiguousRevision = 3;
int64 sourceIndex = 4;
repeated github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.Repository repos = 6;
repeated github.com.argoproj.argo_cd.v2.pkg.apis.application.v1alpha1.RepoCreds helmRepoCreds = 7;
}

// ResolveRevisionResponse
Expand Down Expand Up @@ -165,6 +167,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

0 comments on commit 7b93cad

Please sign in to comment.