github.com/argoproj/argo-cd/v2@v2.10.9/reposerver/repository/repository.go (about)

     1  package repository
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	goio "io"
    10  	"io/fs"
    11  	"net/url"
    12  	"os"
    13  	"path"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/golang/protobuf/ptypes/empty"
    20  
    21  	kubeyaml "k8s.io/apimachinery/pkg/util/yaml"
    22  
    23  	"k8s.io/apimachinery/pkg/api/resource"
    24  
    25  	"github.com/argoproj/argo-cd/v2/common"
    26  	"github.com/argoproj/argo-cd/v2/util/io/files"
    27  	"github.com/argoproj/argo-cd/v2/util/manifeststream"
    28  
    29  	"github.com/Masterminds/semver/v3"
    30  	"github.com/TomOnTime/utfutil"
    31  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    32  	textutils "github.com/argoproj/gitops-engine/pkg/utils/text"
    33  	"github.com/argoproj/pkg/sync"
    34  	jsonpatch "github.com/evanphx/json-patch"
    35  	gogit "github.com/go-git/go-git/v5"
    36  	"github.com/google/go-jsonnet"
    37  	"github.com/google/uuid"
    38  	grpc_retry "github.com/grpc-ecosystem/go-grpc-middleware/retry"
    39  	log "github.com/sirupsen/logrus"
    40  	"golang.org/x/sync/semaphore"
    41  	"google.golang.org/grpc/codes"
    42  	"google.golang.org/grpc/status"
    43  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    44  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    45  	"k8s.io/apimachinery/pkg/runtime"
    46  	"sigs.k8s.io/yaml"
    47  
    48  	pluginclient "github.com/argoproj/argo-cd/v2/cmpserver/apiclient"
    49  	"github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1"
    50  	"github.com/argoproj/argo-cd/v2/reposerver/apiclient"
    51  	"github.com/argoproj/argo-cd/v2/reposerver/cache"
    52  	"github.com/argoproj/argo-cd/v2/reposerver/metrics"
    53  	"github.com/argoproj/argo-cd/v2/util/app/discovery"
    54  	argopath "github.com/argoproj/argo-cd/v2/util/app/path"
    55  	"github.com/argoproj/argo-cd/v2/util/argo"
    56  	"github.com/argoproj/argo-cd/v2/util/cmp"
    57  	"github.com/argoproj/argo-cd/v2/util/git"
    58  	"github.com/argoproj/argo-cd/v2/util/glob"
    59  	"github.com/argoproj/argo-cd/v2/util/gpg"
    60  	"github.com/argoproj/argo-cd/v2/util/grpc"
    61  	"github.com/argoproj/argo-cd/v2/util/helm"
    62  	"github.com/argoproj/argo-cd/v2/util/io"
    63  	pathutil "github.com/argoproj/argo-cd/v2/util/io/path"
    64  	"github.com/argoproj/argo-cd/v2/util/kustomize"
    65  	"github.com/argoproj/argo-cd/v2/util/text"
    66  )
    67  
    68  const (
    69  	cachedManifestGenerationPrefix = "Manifest generation error (cached)"
    70  	helmDepUpMarkerFile            = ".argocd-helm-dep-up"
    71  	allowConcurrencyFile           = ".argocd-allow-concurrency"
    72  	repoSourceFile                 = ".argocd-source.yaml"
    73  	appSourceFile                  = ".argocd-source-%s.yaml"
    74  	ociPrefix                      = "oci://"
    75  )
    76  
    77  var ErrExceededMaxCombinedManifestFileSize = errors.New("exceeded max combined manifest file size")
    78  
    79  // Service implements ManifestService interface
    80  type Service struct {
    81  	gitCredsStore             git.CredsStore
    82  	rootDir                   string
    83  	gitRepoPaths              io.TempPaths
    84  	chartPaths                io.TempPaths
    85  	gitRepoInitializer        func(rootPath string) goio.Closer
    86  	repoLock                  *repositoryLock
    87  	cache                     *cache.Cache
    88  	parallelismLimitSemaphore *semaphore.Weighted
    89  	metricsServer             *metrics.MetricsServer
    90  	resourceTracking          argo.ResourceTracking
    91  	newGitClient              func(rawRepoURL string, root string, creds git.Creds, insecure bool, enableLfs bool, proxy string, opts ...git.ClientOpts) (git.Client, error)
    92  	newHelmClient             func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client
    93  	initConstants             RepoServerInitConstants
    94  	// now is usually just time.Now, but may be replaced by unit tests for testing purposes
    95  	now func() time.Time
    96  }
    97  
    98  type RepoServerInitConstants struct {
    99  	ParallelismLimit                             int64
   100  	PauseGenerationAfterFailedGenerationAttempts int
   101  	PauseGenerationOnFailureForMinutes           int
   102  	PauseGenerationOnFailureForRequests          int
   103  	SubmoduleEnabled                             bool
   104  	MaxCombinedDirectoryManifestsSize            resource.Quantity
   105  	CMPTarExcludedGlobs                          []string
   106  	AllowOutOfBoundsSymlinks                     bool
   107  	StreamedManifestMaxExtractedSize             int64
   108  	StreamedManifestMaxTarSize                   int64
   109  	HelmManifestMaxExtractedSize                 int64
   110  	HelmRegistryMaxIndexSize                     int64
   111  	DisableHelmManifestMaxExtractedSize          bool
   112  }
   113  
   114  // NewService returns a new instance of the Manifest service
   115  func NewService(metricsServer *metrics.MetricsServer, cache *cache.Cache, initConstants RepoServerInitConstants, resourceTracking argo.ResourceTracking, gitCredsStore git.CredsStore, rootDir string) *Service {
   116  	var parallelismLimitSemaphore *semaphore.Weighted
   117  	if initConstants.ParallelismLimit > 0 {
   118  		parallelismLimitSemaphore = semaphore.NewWeighted(initConstants.ParallelismLimit)
   119  	}
   120  	repoLock := NewRepositoryLock()
   121  	gitRandomizedPaths := io.NewRandomizedTempPaths(rootDir)
   122  	helmRandomizedPaths := io.NewRandomizedTempPaths(rootDir)
   123  	return &Service{
   124  		parallelismLimitSemaphore: parallelismLimitSemaphore,
   125  		repoLock:                  repoLock,
   126  		cache:                     cache,
   127  		metricsServer:             metricsServer,
   128  		newGitClient:              git.NewClientExt,
   129  		resourceTracking:          resourceTracking,
   130  		newHelmClient: func(repoURL string, creds helm.Creds, enableOci bool, proxy string, opts ...helm.ClientOpts) helm.Client {
   131  			return helm.NewClientWithLock(repoURL, creds, sync.NewKeyLock(), enableOci, proxy, opts...)
   132  		},
   133  		initConstants:      initConstants,
   134  		now:                time.Now,
   135  		gitCredsStore:      gitCredsStore,
   136  		gitRepoPaths:       gitRandomizedPaths,
   137  		chartPaths:         helmRandomizedPaths,
   138  		gitRepoInitializer: directoryPermissionInitializer,
   139  		rootDir:            rootDir,
   140  	}
   141  }
   142  
   143  func (s *Service) Init() error {
   144  	_, err := os.Stat(s.rootDir)
   145  	if os.IsNotExist(err) {
   146  		return os.MkdirAll(s.rootDir, 0300)
   147  	}
   148  	if err == nil {
   149  		// give itself read permissions to list previously written directories
   150  		err = os.Chmod(s.rootDir, 0700)
   151  	}
   152  	var dirEntries []fs.DirEntry
   153  	if err == nil {
   154  		dirEntries, err = os.ReadDir(s.rootDir)
   155  	}
   156  	if err != nil {
   157  		log.Warnf("Failed to restore cloned repositories paths: %v", err)
   158  		return nil
   159  	}
   160  
   161  	for _, file := range dirEntries {
   162  		if !file.IsDir() {
   163  			continue
   164  		}
   165  		fullPath := filepath.Join(s.rootDir, file.Name())
   166  		closer := s.gitRepoInitializer(fullPath)
   167  		if repo, err := gogit.PlainOpen(fullPath); err == nil {
   168  			if remotes, err := repo.Remotes(); err == nil && len(remotes) > 0 && len(remotes[0].Config().URLs) > 0 {
   169  				s.gitRepoPaths.Add(git.NormalizeGitURL(remotes[0].Config().URLs[0]), fullPath)
   170  			}
   171  		}
   172  		io.Close(closer)
   173  	}
   174  	// remove read permissions since no-one should be able to list the directories
   175  	return os.Chmod(s.rootDir, 0300)
   176  }
   177  
   178  // ListRefs List a subset of the refs (currently, branches and tags) of a git repo
   179  func (s *Service) ListRefs(ctx context.Context, q *apiclient.ListRefsRequest) (*apiclient.Refs, error) {
   180  	gitClient, err := s.newClient(q.Repo)
   181  	if err != nil {
   182  		return nil, fmt.Errorf("error creating git client: %w", err)
   183  	}
   184  
   185  	s.metricsServer.IncPendingRepoRequest(q.Repo.Repo)
   186  	defer s.metricsServer.DecPendingRepoRequest(q.Repo.Repo)
   187  
   188  	refs, err := gitClient.LsRefs()
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	res := apiclient.Refs{
   194  		Branches: refs.Branches,
   195  		Tags:     refs.Tags,
   196  	}
   197  
   198  	return &res, nil
   199  }
   200  
   201  // ListApps lists the contents of a GitHub repo
   202  func (s *Service) ListApps(ctx context.Context, q *apiclient.ListAppsRequest) (*apiclient.AppList, error) {
   203  	gitClient, commitSHA, err := s.newClientResolveRevision(q.Repo, q.Revision)
   204  	if err != nil {
   205  		return nil, fmt.Errorf("error setting up git client and resolving given revision: %w", err)
   206  	}
   207  	if apps, err := s.cache.ListApps(q.Repo.Repo, commitSHA); err == nil {
   208  		log.Infof("cache hit: %s/%s", q.Repo.Repo, q.Revision)
   209  		return &apiclient.AppList{Apps: apps}, nil
   210  	}
   211  
   212  	s.metricsServer.IncPendingRepoRequest(q.Repo.Repo)
   213  	defer s.metricsServer.DecPendingRepoRequest(q.Repo.Repo)
   214  
   215  	closer, err := s.repoLock.Lock(gitClient.Root(), commitSHA, true, func() (goio.Closer, error) {
   216  		return s.checkoutRevision(gitClient, commitSHA, s.initConstants.SubmoduleEnabled)
   217  	})
   218  
   219  	if err != nil {
   220  		return nil, fmt.Errorf("error acquiring repository lock: %w", err)
   221  	}
   222  
   223  	defer io.Close(closer)
   224  	apps, err := discovery.Discover(ctx, gitClient.Root(), gitClient.Root(), q.EnabledSourceTypes, s.initConstants.CMPTarExcludedGlobs)
   225  	if err != nil {
   226  		return nil, fmt.Errorf("error discovering applications: %w", err)
   227  	}
   228  	err = s.cache.SetApps(q.Repo.Repo, commitSHA, apps)
   229  	if err != nil {
   230  		log.Warnf("cache set error %s/%s: %v", q.Repo.Repo, commitSHA, err)
   231  	}
   232  	res := apiclient.AppList{Apps: apps}
   233  	return &res, nil
   234  }
   235  
   236  // ListPlugins lists the contents of a GitHub repo
   237  func (s *Service) ListPlugins(ctx context.Context, _ *empty.Empty) (*apiclient.PluginList, error) {
   238  	pluginSockFilePath := common.GetPluginSockFilePath()
   239  
   240  	sockFiles, err := os.ReadDir(pluginSockFilePath)
   241  	if err != nil {
   242  		return nil, fmt.Errorf("failed to get plugins from dir %v, error=%w", pluginSockFilePath, err)
   243  	}
   244  
   245  	var plugins []*apiclient.PluginInfo
   246  	for _, file := range sockFiles {
   247  		if file.Type() == os.ModeSocket {
   248  			plugins = append(plugins, &apiclient.PluginInfo{Name: strings.TrimSuffix(file.Name(), ".sock")})
   249  		}
   250  	}
   251  
   252  	res := apiclient.PluginList{Items: plugins}
   253  	return &res, nil
   254  }
   255  
   256  type operationSettings struct {
   257  	sem             *semaphore.Weighted
   258  	noCache         bool
   259  	noRevisionCache bool
   260  	allowConcurrent bool
   261  }
   262  
   263  // operationContext contains request values which are generated by runRepoOperation (on demand) by a call to the
   264  // provided operationContextSrc function.
   265  type operationContext struct {
   266  
   267  	// application path or helm chart path
   268  	appPath string
   269  
   270  	// output of 'git verify-(tag/commit)', if signature verification is enabled (otherwise "")
   271  	verificationResult string
   272  }
   273  
   274  // The 'operation' function parameter of 'runRepoOperation' may call this function to retrieve
   275  // the appPath or GPG verificationResult.
   276  // Failure to generate either of these values will return an error which may be cached by
   277  // the calling function (for example, 'runManifestGen')
   278  type operationContextSrc = func() (*operationContext, error)
   279  
   280  // runRepoOperation downloads either git folder or helm chart and executes specified operation
   281  // - Returns a value from the cache if present (by calling getCached(...)); if no value is present, the
   282  // provide operation(...) is called. The specific return type of this function is determined by the
   283  // calling function, via the provided  getCached(...) and operation(...) function.
   284  func (s *Service) runRepoOperation(
   285  	ctx context.Context,
   286  	revision string,
   287  	repo *v1alpha1.Repository,
   288  	source *v1alpha1.ApplicationSource,
   289  	verifyCommit bool,
   290  	cacheFn func(cacheKey string, refSourceCommitSHAs cache.ResolvedRevisions, firstInvocation bool) (bool, error),
   291  	operation func(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc) error,
   292  	settings operationSettings,
   293  	hasMultipleSources bool,
   294  	refSources map[string]*v1alpha1.RefTarget) error {
   295  
   296  	if sanitizer, ok := grpc.SanitizerFromContext(ctx); ok {
   297  		// make sure a randomized path replaced with '.' in the error message
   298  		sanitizer.AddRegexReplacement(getRepoSanitizerRegex(s.rootDir), "<path to cached source>")
   299  	}
   300  
   301  	var gitClient git.Client
   302  	var helmClient helm.Client
   303  	var err error
   304  	gitClientOpts := git.WithCache(s.cache, !settings.noRevisionCache && !settings.noCache)
   305  	revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
   306  	unresolvedRevision := revision
   307  	if source.IsHelm() {
   308  		helmClient, revision, err = s.newHelmClientResolveRevision(repo, revision, source.Chart, settings.noCache || settings.noRevisionCache)
   309  		if err != nil {
   310  			return err
   311  		}
   312  	} else {
   313  		gitClient, revision, err = s.newClientResolveRevision(repo, revision, gitClientOpts)
   314  		if err != nil {
   315  			return err
   316  		}
   317  	}
   318  
   319  	repoRefs, err := resolveReferencedSources(hasMultipleSources, source.Helm, refSources, s.newClientResolveRevision, gitClientOpts)
   320  	if err != nil {
   321  		return err
   322  	}
   323  
   324  	if !settings.noCache {
   325  		if ok, err := cacheFn(revision, repoRefs, true); ok {
   326  			return err
   327  		}
   328  	}
   329  
   330  	s.metricsServer.IncPendingRepoRequest(repo.Repo)
   331  	defer s.metricsServer.DecPendingRepoRequest(repo.Repo)
   332  
   333  	if settings.sem != nil {
   334  		err = settings.sem.Acquire(ctx, 1)
   335  		if err != nil {
   336  			return err
   337  		}
   338  		defer settings.sem.Release(1)
   339  	}
   340  
   341  	if source.IsHelm() {
   342  		if settings.noCache {
   343  			err = helmClient.CleanChartCache(source.Chart, revision)
   344  			if err != nil {
   345  				return err
   346  			}
   347  		}
   348  		helmPassCredentials := false
   349  		if source.Helm != nil {
   350  			helmPassCredentials = source.Helm.PassCredentials
   351  		}
   352  		chartPath, closer, err := helmClient.ExtractChart(source.Chart, revision, helmPassCredentials, s.initConstants.HelmManifestMaxExtractedSize, s.initConstants.DisableHelmManifestMaxExtractedSize)
   353  		if err != nil {
   354  			return err
   355  		}
   356  		defer io.Close(closer)
   357  		if !s.initConstants.AllowOutOfBoundsSymlinks {
   358  			err := argopath.CheckOutOfBoundsSymlinks(chartPath)
   359  			if err != nil {
   360  				oobError := &argopath.OutOfBoundsSymlinkError{}
   361  				if errors.As(err, &oobError) {
   362  					log.WithFields(log.Fields{
   363  						common.SecurityField: common.SecurityHigh,
   364  						"chart":              source.Chart,
   365  						"revision":           revision,
   366  						"file":               oobError.File,
   367  					}).Warn("chart contains out-of-bounds symlink")
   368  					return fmt.Errorf("chart contains out-of-bounds symlinks. file: %s", oobError.File)
   369  				} else {
   370  					return err
   371  				}
   372  			}
   373  		}
   374  		return operation(chartPath, revision, revision, func() (*operationContext, error) {
   375  			return &operationContext{chartPath, ""}, nil
   376  		})
   377  	} else {
   378  		closer, err := s.repoLock.Lock(gitClient.Root(), revision, settings.allowConcurrent, func() (goio.Closer, error) {
   379  			return s.checkoutRevision(gitClient, revision, s.initConstants.SubmoduleEnabled)
   380  		})
   381  
   382  		if err != nil {
   383  			return err
   384  		}
   385  
   386  		defer io.Close(closer)
   387  
   388  		if !s.initConstants.AllowOutOfBoundsSymlinks {
   389  			err := argopath.CheckOutOfBoundsSymlinks(gitClient.Root())
   390  			if err != nil {
   391  				oobError := &argopath.OutOfBoundsSymlinkError{}
   392  				if errors.As(err, &oobError) {
   393  					log.WithFields(log.Fields{
   394  						common.SecurityField: common.SecurityHigh,
   395  						"repo":               repo.Repo,
   396  						"revision":           revision,
   397  						"file":               oobError.File,
   398  					}).Warn("repository contains out-of-bounds symlink")
   399  					return fmt.Errorf("repository contains out-of-bounds symlinks. file: %s", oobError.File)
   400  				} else {
   401  					return err
   402  				}
   403  			}
   404  		}
   405  
   406  		var commitSHA string
   407  		if hasMultipleSources {
   408  			commitSHA = revision
   409  		} else {
   410  			commit, err := gitClient.CommitSHA()
   411  			if err != nil {
   412  				return fmt.Errorf("failed to get commit SHA: %w", err)
   413  			}
   414  			commitSHA = commit
   415  		}
   416  
   417  		// double-check locking
   418  		if !settings.noCache {
   419  			if ok, err := cacheFn(revision, repoRefs, false); ok {
   420  				return err
   421  			}
   422  		}
   423  
   424  		// Here commitSHA refers to the SHA of the actual commit, whereas revision refers to the branch/tag name etc
   425  		// We use the commitSHA to generate manifests and store them in cache, and revision to retrieve them from cache
   426  		return operation(gitClient.Root(), commitSHA, revision, func() (*operationContext, error) {
   427  			var signature string
   428  			if verifyCommit {
   429  				// When the revision is an annotated tag, we need to pass the unresolved revision (i.e. the tag name)
   430  				// to the verification routine. For everything else, we work with the SHA that the target revision is
   431  				// pointing to (i.e. the resolved revision).
   432  				var rev string
   433  				if gitClient.IsAnnotatedTag(revision) {
   434  					rev = unresolvedRevision
   435  				} else {
   436  					rev = revision
   437  				}
   438  				signature, err = gitClient.VerifyCommitSignature(rev)
   439  				if err != nil {
   440  					return nil, err
   441  				}
   442  			}
   443  			appPath, err := argopath.Path(gitClient.Root(), source.Path)
   444  			if err != nil {
   445  				return nil, err
   446  			}
   447  			return &operationContext{appPath, signature}, nil
   448  		})
   449  	}
   450  }
   451  
   452  func getRepoSanitizerRegex(rootDir string) *regexp.Regexp {
   453  	// This regex assumes that the sensitive part of the path (the component immediately after "rootDir") contains no
   454  	// spaces. This assumption allows us to avoid sanitizing "more info" in "/tmp/_argocd-repo/SENSITIVE more info".
   455  	//
   456  	// The no-spaces assumption holds for our actual use case, which is "/tmp/_argocd-repo/{random UUID}". The UUID will
   457  	// only ever contain digits and hyphens.
   458  	return regexp.MustCompile(regexp.QuoteMeta(rootDir) + `/[^ /]*`)
   459  }
   460  
   461  type gitClientGetter func(repo *v1alpha1.Repository, revision string, opts ...git.ClientOpts) (git.Client, string, error)
   462  
   463  // resolveReferencedSources resolves the revisions for the given referenced sources. This lets us invalidate the cached
   464  // when one or more referenced sources change.
   465  //
   466  // Much of this logic is duplicated in runManifestGenAsync. If making changes here, check whether runManifestGenAsync
   467  // should be updated.
   468  func resolveReferencedSources(hasMultipleSources bool, source *v1alpha1.ApplicationSourceHelm, refSources map[string]*v1alpha1.RefTarget, newClientResolveRevision gitClientGetter, gitClientOpts git.ClientOpts) (map[string]string, error) {
   469  	repoRefs := make(map[string]string)
   470  	if !hasMultipleSources || source == nil {
   471  		return repoRefs, nil
   472  	}
   473  
   474  	for _, valueFile := range source.ValueFiles {
   475  		if strings.HasPrefix(valueFile, "$") {
   476  			refVar := strings.Split(valueFile, "/")[0]
   477  
   478  			refSourceMapping, ok := refSources[refVar]
   479  			if !ok {
   480  				if len(refSources) == 0 {
   481  					return nil, fmt.Errorf("source referenced %q, but no source has a 'ref' field defined", refVar)
   482  				}
   483  				refKeys := make([]string, 0)
   484  				for refKey := range refSources {
   485  					refKeys = append(refKeys, refKey)
   486  				}
   487  				return nil, fmt.Errorf("source referenced %q, which is not one of the available sources (%s)", refVar, strings.Join(refKeys, ", "))
   488  			}
   489  			if refSourceMapping.Chart != "" {
   490  				return nil, fmt.Errorf("source has a 'chart' field defined, but Helm charts are not yet not supported for 'ref' sources")
   491  			}
   492  			normalizedRepoURL := git.NormalizeGitURL(refSourceMapping.Repo.Repo)
   493  			_, ok = repoRefs[normalizedRepoURL]
   494  			if !ok {
   495  				_, referencedCommitSHA, err := newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, gitClientOpts)
   496  				if err != nil {
   497  					log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err)
   498  					return nil, fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo)
   499  				}
   500  
   501  				repoRefs[normalizedRepoURL] = referencedCommitSHA
   502  			}
   503  		}
   504  	}
   505  	return repoRefs, nil
   506  }
   507  
   508  func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestRequest) (*apiclient.ManifestResponse, error) {
   509  	var res *apiclient.ManifestResponse
   510  	var err error
   511  
   512  	// Skip this path for ref only sources
   513  	if q.HasMultipleSources && q.ApplicationSource.Path == "" && q.ApplicationSource.Chart == "" && q.ApplicationSource.Ref != "" {
   514  		log.Debugf("Skipping manifest generation for ref only source for application: %s and ref %s", q.AppName, q.ApplicationSource.Ref)
   515  		_, revision, err := s.newClientResolveRevision(q.Repo, q.Revision, git.WithCache(s.cache, !q.NoRevisionCache && !q.NoCache))
   516  		res = &apiclient.ManifestResponse{
   517  			Revision: revision,
   518  		}
   519  		return res, err
   520  	}
   521  
   522  	cacheFn := func(cacheKey string, refSourceCommitSHAs cache.ResolvedRevisions, firstInvocation bool) (bool, error) {
   523  		ok, resp, err := s.getManifestCacheEntry(cacheKey, q, refSourceCommitSHAs, firstInvocation)
   524  		res = resp
   525  		return ok, err
   526  	}
   527  
   528  	tarConcluded := false
   529  	var promise *ManifestResponsePromise
   530  
   531  	operation := func(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc) error {
   532  		// do not generate manifests if Path and Chart fields are not set for a source in Multiple Sources
   533  		if q.HasMultipleSources && q.ApplicationSource.Path == "" && q.ApplicationSource.Chart == "" {
   534  			log.WithFields(map[string]interface{}{
   535  				"source": q.ApplicationSource,
   536  			}).Debugf("not generating manifests as path and chart fields are empty")
   537  			res = &apiclient.ManifestResponse{
   538  				Revision: commitSHA,
   539  			}
   540  			return nil
   541  		}
   542  
   543  		promise = s.runManifestGen(ctx, repoRoot, commitSHA, cacheKey, ctxSrc, q)
   544  		// The fist channel to send the message will resume this operation.
   545  		// The main purpose for using channels here is to be able to unlock
   546  		// the repository as soon as the lock in not required anymore. In
   547  		// case of CMP the repo is compressed (tgz) and sent to the cmp-server
   548  		// for manifest generation.
   549  		select {
   550  		case err := <-promise.errCh:
   551  			return err
   552  		case resp := <-promise.responseCh:
   553  			res = resp
   554  		case tarDone := <-promise.tarDoneCh:
   555  			tarConcluded = tarDone
   556  		}
   557  		return nil
   558  	}
   559  
   560  	settings := operationSettings{sem: s.parallelismLimitSemaphore, noCache: q.NoCache, noRevisionCache: q.NoRevisionCache, allowConcurrent: q.ApplicationSource.AllowsConcurrentProcessing()}
   561  	err = s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature, cacheFn, operation, settings, q.HasMultipleSources, q.RefSources)
   562  
   563  	// if the tarDoneCh message is sent it means that the manifest
   564  	// generation is being managed by the cmp-server. In this case
   565  	// we have to wait for the responseCh to send the manifest
   566  	// response.
   567  	if tarConcluded && res == nil {
   568  		select {
   569  		case resp := <-promise.responseCh:
   570  			res = resp
   571  		case err := <-promise.errCh:
   572  			return nil, err
   573  		}
   574  	}
   575  	return res, err
   576  }
   577  
   578  func (s *Service) GenerateManifestWithFiles(stream apiclient.RepoServerService_GenerateManifestWithFilesServer) error {
   579  	workDir, err := files.CreateTempDir("")
   580  	if err != nil {
   581  		return fmt.Errorf("error creating temp dir: %w", err)
   582  	}
   583  	defer func() {
   584  		if err := os.RemoveAll(workDir); err != nil {
   585  			// we panic here as the workDir may contain sensitive information
   586  			log.WithField(common.SecurityField, common.SecurityCritical).Errorf("error removing generate manifest workdir: %v", err)
   587  			panic(fmt.Sprintf("error removing generate manifest workdir: %s", err))
   588  		}
   589  	}()
   590  
   591  	req, metadata, err := manifeststream.ReceiveManifestFileStream(stream.Context(), stream, workDir, s.initConstants.StreamedManifestMaxTarSize, s.initConstants.StreamedManifestMaxExtractedSize)
   592  
   593  	if err != nil {
   594  		return fmt.Errorf("error receiving manifest file stream: %w", err)
   595  	}
   596  
   597  	if !s.initConstants.AllowOutOfBoundsSymlinks {
   598  		err := argopath.CheckOutOfBoundsSymlinks(workDir)
   599  		if err != nil {
   600  			oobError := &argopath.OutOfBoundsSymlinkError{}
   601  			if errors.As(err, &oobError) {
   602  				log.WithFields(log.Fields{
   603  					common.SecurityField: common.SecurityHigh,
   604  					"file":               oobError.File,
   605  				}).Warn("streamed files contains out-of-bounds symlink")
   606  				return fmt.Errorf("streamed files contains out-of-bounds symlinks. file: %s", oobError.File)
   607  			} else {
   608  				return err
   609  			}
   610  		}
   611  	}
   612  
   613  	promise := s.runManifestGen(stream.Context(), workDir, "streamed", metadata.Checksum, func() (*operationContext, error) {
   614  		appPath, err := argopath.Path(workDir, req.ApplicationSource.Path)
   615  		if err != nil {
   616  			return nil, fmt.Errorf("failed to get app path: %w", err)
   617  		}
   618  		return &operationContext{appPath, ""}, nil
   619  	}, req)
   620  
   621  	var res *apiclient.ManifestResponse
   622  	tarConcluded := false
   623  
   624  	select {
   625  	case err := <-promise.errCh:
   626  		return err
   627  	case tarDone := <-promise.tarDoneCh:
   628  		tarConcluded = tarDone
   629  	case resp := <-promise.responseCh:
   630  		res = resp
   631  	}
   632  
   633  	if tarConcluded && res == nil {
   634  		select {
   635  		case resp := <-promise.responseCh:
   636  			res = resp
   637  		case err := <-promise.errCh:
   638  			return err
   639  		}
   640  	}
   641  
   642  	err = stream.SendAndClose(res)
   643  	return err
   644  }
   645  
   646  type ManifestResponsePromise struct {
   647  	responseCh <-chan *apiclient.ManifestResponse
   648  	tarDoneCh  <-chan bool
   649  	errCh      <-chan error
   650  }
   651  
   652  func NewManifestResponsePromise(responseCh <-chan *apiclient.ManifestResponse, tarDoneCh <-chan bool, errCh chan error) *ManifestResponsePromise {
   653  	return &ManifestResponsePromise{
   654  		responseCh: responseCh,
   655  		tarDoneCh:  tarDoneCh,
   656  		errCh:      errCh,
   657  	}
   658  }
   659  
   660  type generateManifestCh struct {
   661  	responseCh chan<- *apiclient.ManifestResponse
   662  	tarDoneCh  chan<- bool
   663  	errCh      chan<- error
   664  }
   665  
   666  // runManifestGen will be called by runRepoOperation if:
   667  // - the cache does not contain a value for this key
   668  // - or, the cache does contain a value for this key, but it is an expired manifest generation entry
   669  // - or, NoCache is true
   670  // Returns a ManifestResponse, or an error, but not both
   671  func (s *Service) runManifestGen(ctx context.Context, repoRoot, commitSHA, cacheKey string, opContextSrc operationContextSrc, q *apiclient.ManifestRequest) *ManifestResponsePromise {
   672  
   673  	responseCh := make(chan *apiclient.ManifestResponse)
   674  	tarDoneCh := make(chan bool)
   675  	errCh := make(chan error)
   676  	responsePromise := NewManifestResponsePromise(responseCh, tarDoneCh, errCh)
   677  
   678  	channels := &generateManifestCh{
   679  		responseCh: responseCh,
   680  		tarDoneCh:  tarDoneCh,
   681  		errCh:      errCh,
   682  	}
   683  	go s.runManifestGenAsync(ctx, repoRoot, commitSHA, cacheKey, opContextSrc, q, channels)
   684  	return responsePromise
   685  }
   686  
   687  type repoRef struct {
   688  	// revision is the git revision - can be any valid revision like a branch, tag, or commit SHA.
   689  	revision string
   690  	// commitSHA is the actual commit to which revision refers.
   691  	commitSHA string
   692  	// key is the name of the key which was used to reference this repo.
   693  	key string
   694  }
   695  
   696  func (s *Service) runManifestGenAsync(ctx context.Context, repoRoot, commitSHA, cacheKey string, opContextSrc operationContextSrc, q *apiclient.ManifestRequest, ch *generateManifestCh) {
   697  	defer func() {
   698  		close(ch.errCh)
   699  		close(ch.responseCh)
   700  	}()
   701  
   702  	// GenerateManifests mutates the source (applies overrides). Those overrides shouldn't be reflected in the cache
   703  	// key. Overrides will break the cache anyway, because changes to overrides will change the revision.
   704  	appSourceCopy := q.ApplicationSource.DeepCopy()
   705  	repoRefs := make(map[string]repoRef)
   706  
   707  	var manifestGenResult *apiclient.ManifestResponse
   708  	opContext, err := opContextSrc()
   709  	if err == nil {
   710  		// Much of the multi-source handling logic is duplicated in resolveReferencedSources. If making changes here,
   711  		// check whether they should be replicated in resolveReferencedSources.
   712  		if q.HasMultipleSources {
   713  			if q.ApplicationSource.Helm != nil {
   714  
   715  				// Checkout every one of the referenced sources to the target revision before generating Manifests
   716  				for _, valueFile := range q.ApplicationSource.Helm.ValueFiles {
   717  					if strings.HasPrefix(valueFile, "$") {
   718  						refVar := strings.Split(valueFile, "/")[0]
   719  
   720  						refSourceMapping, ok := q.RefSources[refVar]
   721  						if !ok {
   722  							if len(q.RefSources) == 0 {
   723  								ch.errCh <- fmt.Errorf("source referenced %q, but no source has a 'ref' field defined", refVar)
   724  							}
   725  							refKeys := make([]string, 0)
   726  							for refKey := range q.RefSources {
   727  								refKeys = append(refKeys, refKey)
   728  							}
   729  							ch.errCh <- fmt.Errorf("source referenced %q, which is not one of the available sources (%s)", refVar, strings.Join(refKeys, ", "))
   730  							return
   731  						}
   732  						if refSourceMapping.Chart != "" {
   733  							ch.errCh <- fmt.Errorf("source has a 'chart' field defined, but Helm charts are not yet not supported for 'ref' sources")
   734  							return
   735  						}
   736  						normalizedRepoURL := git.NormalizeGitURL(refSourceMapping.Repo.Repo)
   737  						closer, ok := repoRefs[normalizedRepoURL]
   738  						if ok {
   739  							if closer.revision != refSourceMapping.TargetRevision {
   740  								ch.errCh <- fmt.Errorf("cannot reference multiple revisions for the same repository (%s references %q while %s references %q)", refVar, refSourceMapping.TargetRevision, closer.key, closer.revision)
   741  								return
   742  							}
   743  						} else {
   744  							gitClient, referencedCommitSHA, err := s.newClientResolveRevision(&refSourceMapping.Repo, refSourceMapping.TargetRevision, git.WithCache(s.cache, !q.NoRevisionCache && !q.NoCache))
   745  							if err != nil {
   746  								log.Errorf("Failed to get git client for repo %s: %v", refSourceMapping.Repo.Repo, err)
   747  								ch.errCh <- fmt.Errorf("failed to get git client for repo %s", refSourceMapping.Repo.Repo)
   748  								return
   749  							}
   750  
   751  							if git.NormalizeGitURL(q.ApplicationSource.RepoURL) == normalizedRepoURL && commitSHA != referencedCommitSHA {
   752  								ch.errCh <- fmt.Errorf("cannot reference a different revision of the same repository (%s references %q which resolves to %q while the application references %q which resolves to %q)", refVar, refSourceMapping.TargetRevision, referencedCommitSHA, q.Revision, commitSHA)
   753  								return
   754  							}
   755  							closer, err := s.repoLock.Lock(gitClient.Root(), referencedCommitSHA, true, func() (goio.Closer, error) {
   756  								return s.checkoutRevision(gitClient, referencedCommitSHA, s.initConstants.SubmoduleEnabled)
   757  							})
   758  							if err != nil {
   759  								log.Errorf("failed to acquire lock for referenced source %s", normalizedRepoURL)
   760  								ch.errCh <- err
   761  								return
   762  							}
   763  							defer func(closer goio.Closer) {
   764  								err := closer.Close()
   765  								if err != nil {
   766  									log.Errorf("Failed to release repo lock: %v", err)
   767  								}
   768  							}(closer)
   769  
   770  							// Symlink check must happen after acquiring lock.
   771  							if !s.initConstants.AllowOutOfBoundsSymlinks {
   772  								err := argopath.CheckOutOfBoundsSymlinks(gitClient.Root())
   773  								if err != nil {
   774  									oobError := &argopath.OutOfBoundsSymlinkError{}
   775  									if errors.As(err, &oobError) {
   776  										log.WithFields(log.Fields{
   777  											common.SecurityField: common.SecurityHigh,
   778  											"repo":               refSourceMapping.Repo,
   779  											"revision":           refSourceMapping.TargetRevision,
   780  											"file":               oobError.File,
   781  										}).Warn("repository contains out-of-bounds symlink")
   782  										ch.errCh <- fmt.Errorf("repository contains out-of-bounds symlinks. file: %s", oobError.File)
   783  										return
   784  									} else {
   785  										ch.errCh <- err
   786  										return
   787  									}
   788  								}
   789  							}
   790  
   791  							repoRefs[normalizedRepoURL] = repoRef{revision: refSourceMapping.TargetRevision, commitSHA: referencedCommitSHA, key: refVar}
   792  						}
   793  					}
   794  				}
   795  			}
   796  		}
   797  
   798  		manifestGenResult, err = GenerateManifests(ctx, opContext.appPath, repoRoot, commitSHA, q, false, s.gitCredsStore, s.initConstants.MaxCombinedDirectoryManifestsSize, s.gitRepoPaths, WithCMPTarDoneChannel(ch.tarDoneCh), WithCMPTarExcludedGlobs(s.initConstants.CMPTarExcludedGlobs))
   799  	}
   800  	refSourceCommitSHAs := make(map[string]string)
   801  	if len(repoRefs) > 0 {
   802  		for normalizedURL, repoRef := range repoRefs {
   803  			refSourceCommitSHAs[normalizedURL] = repoRef.commitSHA
   804  		}
   805  	}
   806  	if err != nil {
   807  		logCtx := log.WithFields(log.Fields{
   808  			"application":  q.AppName,
   809  			"appNamespace": q.Namespace,
   810  		})
   811  
   812  		// If manifest generation error caching is enabled
   813  		if s.initConstants.PauseGenerationAfterFailedGenerationAttempts > 0 {
   814  			cache.LogDebugManifestCacheKeyFields("getting manifests cache", "GenerateManifests error", cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   815  
   816  			// Retrieve a new copy (if available) of the cached response: this ensures we are updating the latest copy of the cache,
   817  			// rather than a copy of the cache that occurred before (a potentially lengthy) manifest generation.
   818  			innerRes := &cache.CachedManifestResponse{}
   819  			cacheErr := s.cache.GetManifests(cacheKey, appSourceCopy, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, innerRes, refSourceCommitSHAs)
   820  			if cacheErr != nil && cacheErr != cache.ErrCacheMiss {
   821  				logCtx.Warnf("manifest cache get error %s: %v", appSourceCopy.String(), cacheErr)
   822  				ch.errCh <- cacheErr
   823  				return
   824  			}
   825  
   826  			// If this is the first error we have seen, store the time (we only use the first failure, as this
   827  			// value is used for PauseGenerationOnFailureForMinutes)
   828  			if innerRes.FirstFailureTimestamp == 0 {
   829  				innerRes.FirstFailureTimestamp = s.now().Unix()
   830  			}
   831  
   832  			cache.LogDebugManifestCacheKeyFields("setting manifests cache", "GenerateManifests error", cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   833  
   834  			// Update the cache to include failure information
   835  			innerRes.NumberOfConsecutiveFailures++
   836  			innerRes.MostRecentError = err.Error()
   837  			cacheErr = s.cache.SetManifests(cacheKey, appSourceCopy, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, innerRes, refSourceCommitSHAs)
   838  			if cacheErr != nil {
   839  				logCtx.Warnf("manifest cache set error %s: %v", appSourceCopy.String(), cacheErr)
   840  				ch.errCh <- cacheErr
   841  				return
   842  			}
   843  
   844  		}
   845  		ch.errCh <- err
   846  		return
   847  	}
   848  
   849  	cache.LogDebugManifestCacheKeyFields("setting manifests cache", "fresh GenerateManifests response", cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   850  
   851  	// Otherwise, no error occurred, so ensure the manifest generation error data in the cache entry is reset before we cache the value
   852  	manifestGenCacheEntry := cache.CachedManifestResponse{
   853  		ManifestResponse:                manifestGenResult,
   854  		NumberOfCachedResponsesReturned: 0,
   855  		NumberOfConsecutiveFailures:     0,
   856  		FirstFailureTimestamp:           0,
   857  		MostRecentError:                 "",
   858  	}
   859  	manifestGenResult.Revision = commitSHA
   860  	manifestGenResult.VerifyResult = opContext.verificationResult
   861  	err = s.cache.SetManifests(cacheKey, appSourceCopy, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, &manifestGenCacheEntry, refSourceCommitSHAs)
   862  	if err != nil {
   863  		log.Warnf("manifest cache set error %s/%s: %v", appSourceCopy.String(), cacheKey, err)
   864  	}
   865  	ch.responseCh <- manifestGenCacheEntry.ManifestResponse
   866  }
   867  
   868  // getManifestCacheEntry returns false if the 'generate manifests' operation should be run by runRepoOperation, e.g.:
   869  // - If the cache result is empty for the requested key
   870  // - If the cache is not empty, but the cached value is a manifest generation error AND we have not yet met the failure threshold (e.g. res.NumberOfConsecutiveFailures > 0 && res.NumberOfConsecutiveFailures <  s.initConstants.PauseGenerationAfterFailedGenerationAttempts)
   871  // - If the cache is not empty, but the cache value is an error AND that generation error has expired
   872  // and returns true otherwise.
   873  // If true is returned, either the second or third parameter (but not both) will contain a value from the cache (a ManifestResponse, or error, respectively)
   874  func (s *Service) getManifestCacheEntry(cacheKey string, q *apiclient.ManifestRequest, refSourceCommitSHAs cache.ResolvedRevisions, firstInvocation bool) (bool, *apiclient.ManifestResponse, error) {
   875  	cache.LogDebugManifestCacheKeyFields("getting manifests cache", "GenerateManifest API call", cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   876  
   877  	res := cache.CachedManifestResponse{}
   878  	err := s.cache.GetManifests(cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, &res, refSourceCommitSHAs)
   879  	if err == nil {
   880  
   881  		// The cache contains an existing value
   882  
   883  		// If caching of manifest generation errors is enabled, and res is a cached manifest generation error...
   884  		if s.initConstants.PauseGenerationAfterFailedGenerationAttempts > 0 && res.FirstFailureTimestamp > 0 {
   885  
   886  			// If we are already in the 'manifest generation caching' state, due to too many consecutive failures...
   887  			if res.NumberOfConsecutiveFailures >= s.initConstants.PauseGenerationAfterFailedGenerationAttempts {
   888  
   889  				// Check if enough time has passed to try generation again (e.g. to exit the 'manifest generation caching' state)
   890  				if s.initConstants.PauseGenerationOnFailureForMinutes > 0 {
   891  
   892  					elapsedTimeInMinutes := int((s.now().Unix() - res.FirstFailureTimestamp) / 60)
   893  
   894  					// After X minutes, reset the cache and retry the operation (e.g. perhaps the error is ephemeral and has passed)
   895  					if elapsedTimeInMinutes >= s.initConstants.PauseGenerationOnFailureForMinutes {
   896  						cache.LogDebugManifestCacheKeyFields("deleting manifests cache", "manifest hash did not match or cached response is empty", cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   897  
   898  						// We can now try again, so reset the cache state and run the operation below
   899  						err = s.cache.DeleteManifests(cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   900  						if err != nil {
   901  							log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), cacheKey, err)
   902  						}
   903  						log.Infof("manifest error cache hit and reset: %s/%s", q.ApplicationSource.String(), cacheKey)
   904  						return false, nil, nil
   905  					}
   906  				}
   907  
   908  				// Check if enough cached responses have been returned to try generation again (e.g. to exit the 'manifest generation caching' state)
   909  				if s.initConstants.PauseGenerationOnFailureForRequests > 0 && res.NumberOfCachedResponsesReturned > 0 {
   910  
   911  					if res.NumberOfCachedResponsesReturned >= s.initConstants.PauseGenerationOnFailureForRequests {
   912  						cache.LogDebugManifestCacheKeyFields("deleting manifests cache", "reset after paused generation count", cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   913  
   914  						// We can now try again, so reset the error cache state and run the operation below
   915  						err = s.cache.DeleteManifests(cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   916  						if err != nil {
   917  							log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), cacheKey, err)
   918  						}
   919  						log.Infof("manifest error cache hit and reset: %s/%s", q.ApplicationSource.String(), cacheKey)
   920  						return false, nil, nil
   921  					}
   922  				}
   923  
   924  				// Otherwise, manifest generation is still paused
   925  				log.Infof("manifest error cache hit: %s/%s", q.ApplicationSource.String(), cacheKey)
   926  
   927  				cachedErrorResponse := fmt.Errorf(cachedManifestGenerationPrefix+": %s", res.MostRecentError)
   928  
   929  				if firstInvocation {
   930  					cache.LogDebugManifestCacheKeyFields("setting manifests cache", "update error count", cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, refSourceCommitSHAs)
   931  
   932  					// Increment the number of returned cached responses and push that new value to the cache
   933  					// (if we have not already done so previously in this function)
   934  					res.NumberOfCachedResponsesReturned++
   935  					err = s.cache.SetManifests(cacheKey, q.ApplicationSource, q.RefSources, q, q.Namespace, q.TrackingMethod, q.AppLabelKey, q.AppName, &res, refSourceCommitSHAs)
   936  					if err != nil {
   937  						log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), cacheKey, err)
   938  					}
   939  				}
   940  
   941  				return true, nil, cachedErrorResponse
   942  
   943  			}
   944  
   945  			// Otherwise we are not yet in the manifest generation error state, and not enough consecutive errors have
   946  			// yet occurred to put us in that state.
   947  			log.Infof("manifest error cache miss: %s/%s", q.ApplicationSource.String(), cacheKey)
   948  			return false, res.ManifestResponse, nil
   949  		}
   950  
   951  		log.Infof("manifest cache hit: %s/%s", q.ApplicationSource.String(), cacheKey)
   952  		return true, res.ManifestResponse, nil
   953  	}
   954  
   955  	if err != cache.ErrCacheMiss {
   956  		log.Warnf("manifest cache error %s: %v", q.ApplicationSource.String(), err)
   957  	} else {
   958  		log.Infof("manifest cache miss: %s/%s", q.ApplicationSource.String(), cacheKey)
   959  	}
   960  
   961  	return false, nil, nil
   962  }
   963  
   964  func getHelmRepos(appPath string, repositories []*v1alpha1.Repository, helmRepoCreds []*v1alpha1.RepoCreds) ([]helm.HelmRepository, error) {
   965  	dependencies, err := getHelmDependencyRepos(appPath)
   966  	if err != nil {
   967  		return nil, fmt.Errorf("error retrieving helm dependency repos: %w", err)
   968  	}
   969  	reposByName := make(map[string]*v1alpha1.Repository)
   970  	reposByUrl := make(map[string]*v1alpha1.Repository)
   971  	for _, repo := range repositories {
   972  		reposByUrl[repo.Repo] = repo
   973  		if repo.Name != "" {
   974  			reposByName[repo.Name] = repo
   975  		}
   976  	}
   977  
   978  	repos := make([]helm.HelmRepository, 0)
   979  	for _, dep := range dependencies {
   980  		// find matching repo credentials by URL or name
   981  		repo, ok := reposByUrl[dep.Repo]
   982  		if !ok && dep.Name != "" {
   983  			repo, ok = reposByName[dep.Name]
   984  		}
   985  		if !ok {
   986  			// if no matching repo credentials found, use the repo creds from the credential list
   987  			repo = &v1alpha1.Repository{Repo: dep.Repo, Name: dep.Name, EnableOCI: dep.EnableOCI}
   988  			if repositoryCredential := getRepoCredential(helmRepoCreds, dep.Repo); repositoryCredential != nil {
   989  				repo.EnableOCI = repositoryCredential.EnableOCI
   990  				repo.Password = repositoryCredential.Password
   991  				repo.Username = repositoryCredential.Username
   992  				repo.SSHPrivateKey = repositoryCredential.SSHPrivateKey
   993  				repo.TLSClientCertData = repositoryCredential.TLSClientCertData
   994  				repo.TLSClientCertKey = repositoryCredential.TLSClientCertKey
   995  			} else if repo.EnableOCI {
   996  				// finally if repo is OCI and no credentials found, use the first OCI credential matching by hostname
   997  				// see https://github.com/argoproj/argo-cd/issues/14636
   998  				for _, cred := range repositories {
   999  					if depURL, err := url.Parse("oci://" + dep.Repo); err == nil && cred.EnableOCI && depURL.Host == cred.Repo {
  1000  						repo.Username = cred.Username
  1001  						repo.Password = cred.Password
  1002  						break
  1003  					}
  1004  				}
  1005  			}
  1006  		}
  1007  		repos = append(repos, helm.HelmRepository{Name: repo.Name, Repo: repo.Repo, Creds: repo.GetHelmCreds(), EnableOci: repo.EnableOCI})
  1008  	}
  1009  	return repos, nil
  1010  }
  1011  
  1012  type dependencies struct {
  1013  	Dependencies []repositories `yaml:"dependencies"`
  1014  }
  1015  
  1016  type repositories struct {
  1017  	Repository string `yaml:"repository"`
  1018  }
  1019  
  1020  func getHelmDependencyRepos(appPath string) ([]*v1alpha1.Repository, error) {
  1021  	repos := make([]*v1alpha1.Repository, 0)
  1022  	f, err := os.ReadFile(filepath.Join(appPath, "Chart.yaml"))
  1023  	if err != nil {
  1024  		return nil, fmt.Errorf("error reading helm chart from %s: %w", filepath.Join(appPath, "Chart.yaml"), err)
  1025  	}
  1026  
  1027  	d := &dependencies{}
  1028  	if err = yaml.Unmarshal(f, d); err != nil {
  1029  		return nil, fmt.Errorf("error unmarshalling the helm chart while getting helm dependency repos: %w", err)
  1030  	}
  1031  
  1032  	for _, r := range d.Dependencies {
  1033  		if strings.HasPrefix(r.Repository, "@") {
  1034  			repos = append(repos, &v1alpha1.Repository{
  1035  				Name: r.Repository[1:],
  1036  			})
  1037  		} else if strings.HasPrefix(r.Repository, "alias:") {
  1038  			repos = append(repos, &v1alpha1.Repository{
  1039  				Name: strings.TrimPrefix(r.Repository, "alias:"),
  1040  			})
  1041  		} else if u, err := url.Parse(r.Repository); err == nil && (u.Scheme == "https" || u.Scheme == "oci") {
  1042  			repo := &v1alpha1.Repository{
  1043  				// trimming oci:// prefix since it is currently not supported by Argo CD (OCI repos just have no scheme)
  1044  				Repo:      strings.TrimPrefix(r.Repository, "oci://"),
  1045  				Name:      sanitizeRepoName(r.Repository),
  1046  				EnableOCI: u.Scheme == "oci",
  1047  			}
  1048  			repos = append(repos, repo)
  1049  		}
  1050  	}
  1051  
  1052  	return repos, nil
  1053  }
  1054  
  1055  func sanitizeRepoName(repoName string) string {
  1056  	return strings.ReplaceAll(repoName, "/", "-")
  1057  }
  1058  
  1059  func isConcurrencyAllowed(appPath string) bool {
  1060  	if _, err := os.Stat(path.Join(appPath, allowConcurrencyFile)); err == nil {
  1061  		return true
  1062  	}
  1063  	return false
  1064  }
  1065  
  1066  var manifestGenerateLock = sync.NewKeyLock()
  1067  
  1068  // runHelmBuild executes `helm dependency build` in a given path and ensures that it is executed only once
  1069  // if multiple threads are trying to run it.
  1070  // Multiple goroutines might process same helm app in one repo concurrently when repo server process multiple
  1071  // manifest generation requests of the same commit.
  1072  func runHelmBuild(appPath string, h helm.Helm) error {
  1073  	manifestGenerateLock.Lock(appPath)
  1074  	defer manifestGenerateLock.Unlock(appPath)
  1075  
  1076  	// the `helm dependency build` is potentially a time-consuming 1~2 seconds,
  1077  	// a marker file is used to check if command already run to avoid running it again unnecessarily
  1078  	// the file is removed when repository is re-initialized (e.g. when another commit is processed)
  1079  	markerFile := path.Join(appPath, helmDepUpMarkerFile)
  1080  	_, err := os.Stat(markerFile)
  1081  	if err == nil {
  1082  		return nil
  1083  	} else if !os.IsNotExist(err) {
  1084  		return err
  1085  	}
  1086  
  1087  	err = h.DependencyBuild()
  1088  	if err != nil {
  1089  		return fmt.Errorf("error building helm chart dependencies: %w", err)
  1090  	}
  1091  	return os.WriteFile(markerFile, []byte("marker"), 0644)
  1092  }
  1093  
  1094  func isSourcePermitted(url string, repos []string) bool {
  1095  	p := v1alpha1.AppProject{Spec: v1alpha1.AppProjectSpec{SourceRepos: repos}}
  1096  	return p.IsSourcePermitted(v1alpha1.ApplicationSource{RepoURL: url})
  1097  }
  1098  
  1099  func helmTemplate(appPath string, repoRoot string, env *v1alpha1.Env, q *apiclient.ManifestRequest, isLocal bool, gitRepoPaths io.TempPaths) ([]*unstructured.Unstructured, error) {
  1100  	concurrencyAllowed := isConcurrencyAllowed(appPath)
  1101  	if !concurrencyAllowed {
  1102  		manifestGenerateLock.Lock(appPath)
  1103  		defer manifestGenerateLock.Unlock(appPath)
  1104  	}
  1105  
  1106  	// We use the app name as Helm's release name property, which must not
  1107  	// contain any underscore characters and must not exceed 53 characters.
  1108  	// We are not interested in the fully qualified application name while
  1109  	// templating, thus, we just use the name part of the identifier.
  1110  	appName, _ := argo.ParseInstanceName(q.AppName, "")
  1111  
  1112  	templateOpts := &helm.TemplateOpts{
  1113  		Name:        appName,
  1114  		Namespace:   q.Namespace,
  1115  		KubeVersion: text.SemVer(q.KubeVersion),
  1116  		APIVersions: q.ApiVersions,
  1117  		Set:         map[string]string{},
  1118  		SetString:   map[string]string{},
  1119  		SetFile:     map[string]pathutil.ResolvedFilePath{},
  1120  	}
  1121  
  1122  	appHelm := q.ApplicationSource.Helm
  1123  	var version string
  1124  	var passCredentials bool
  1125  	if appHelm != nil {
  1126  		if appHelm.Version != "" {
  1127  			version = appHelm.Version
  1128  		}
  1129  		if appHelm.ReleaseName != "" {
  1130  			templateOpts.Name = appHelm.ReleaseName
  1131  		}
  1132  
  1133  		resolvedValueFiles, err := getResolvedValueFiles(appPath, repoRoot, env, q.GetValuesFileSchemes(), appHelm.ValueFiles, q.RefSources, gitRepoPaths, appHelm.IgnoreMissingValueFiles)
  1134  		if err != nil {
  1135  			return nil, fmt.Errorf("error resolving helm value files: %w", err)
  1136  		}
  1137  
  1138  		templateOpts.Values = resolvedValueFiles
  1139  
  1140  		if !appHelm.ValuesIsEmpty() {
  1141  			rand, err := uuid.NewRandom()
  1142  			if err != nil {
  1143  				return nil, fmt.Errorf("error generating random filename for Helm values file: %w", err)
  1144  			}
  1145  			p := path.Join(os.TempDir(), rand.String())
  1146  			defer func() {
  1147  				// do not remove the directory if it is the source has Ref field set
  1148  				if q.ApplicationSource.Ref == "" {
  1149  					_ = os.RemoveAll(p)
  1150  				}
  1151  			}()
  1152  			err = os.WriteFile(p, appHelm.ValuesYAML(), 0644)
  1153  			if err != nil {
  1154  				return nil, fmt.Errorf("error writing helm values file: %w", err)
  1155  			}
  1156  			templateOpts.Values = append(templateOpts.Values, pathutil.ResolvedFilePath(p))
  1157  		}
  1158  
  1159  		for _, p := range appHelm.Parameters {
  1160  			if p.ForceString {
  1161  				templateOpts.SetString[p.Name] = p.Value
  1162  			} else {
  1163  				templateOpts.Set[p.Name] = p.Value
  1164  			}
  1165  		}
  1166  		for _, p := range appHelm.FileParameters {
  1167  			resolvedPath, _, err := pathutil.ResolveValueFilePathOrUrl(appPath, repoRoot, env.Envsubst(p.Path), q.GetValuesFileSchemes())
  1168  			if err != nil {
  1169  				return nil, fmt.Errorf("error resolving helm value file path: %w", err)
  1170  			}
  1171  			templateOpts.SetFile[p.Name] = resolvedPath
  1172  		}
  1173  		passCredentials = appHelm.PassCredentials
  1174  		templateOpts.SkipCrds = appHelm.SkipCrds
  1175  	}
  1176  	if templateOpts.Name == "" {
  1177  		templateOpts.Name = q.AppName
  1178  	}
  1179  	for i, j := range templateOpts.Set {
  1180  		templateOpts.Set[i] = env.Envsubst(j)
  1181  	}
  1182  	for i, j := range templateOpts.SetString {
  1183  		templateOpts.SetString[i] = env.Envsubst(j)
  1184  	}
  1185  
  1186  	var proxy string
  1187  	if q.Repo != nil {
  1188  		proxy = q.Repo.Proxy
  1189  	}
  1190  
  1191  	helmRepos, err := getHelmRepos(appPath, q.Repos, q.HelmRepoCreds)
  1192  	if err != nil {
  1193  		return nil, fmt.Errorf("error getting helm repos: %w", err)
  1194  	}
  1195  
  1196  	h, err := helm.NewHelmApp(appPath, helmRepos, isLocal, version, proxy, passCredentials)
  1197  	if err != nil {
  1198  		return nil, fmt.Errorf("error initializing helm app object: %w", err)
  1199  	}
  1200  
  1201  	defer h.Dispose()
  1202  	err = h.Init()
  1203  	if err != nil {
  1204  		return nil, fmt.Errorf("error initializing helm app: %w", err)
  1205  	}
  1206  
  1207  	out, err := h.Template(templateOpts)
  1208  	if err != nil {
  1209  		if !helm.IsMissingDependencyErr(err) {
  1210  			return nil, err
  1211  		}
  1212  
  1213  		if concurrencyAllowed {
  1214  			err = runHelmBuild(appPath, h)
  1215  		} else {
  1216  			err = h.DependencyBuild()
  1217  		}
  1218  
  1219  		if err != nil {
  1220  			var reposNotPermitted []string
  1221  			// We do a sanity check here to give a nicer error message in case any of the Helm repositories are not permitted by
  1222  			// the AppProject which the application is a part of
  1223  			for _, repo := range helmRepos {
  1224  				msg := err.Error()
  1225  
  1226  				chartCannotBeReached := strings.Contains(msg, "is not a valid chart repository or cannot be reached")
  1227  				couldNotDownloadChart := strings.Contains(msg, "could not download")
  1228  
  1229  				if (chartCannotBeReached || couldNotDownloadChart) && !isSourcePermitted(repo.Repo, q.ProjectSourceRepos) {
  1230  					reposNotPermitted = append(reposNotPermitted, repo.Repo)
  1231  				}
  1232  			}
  1233  
  1234  			if len(reposNotPermitted) > 0 {
  1235  				return nil, status.Errorf(codes.PermissionDenied, "helm repos %s are not permitted in project '%s'", strings.Join(reposNotPermitted, ", "), q.ProjectName)
  1236  			}
  1237  
  1238  			return nil, err
  1239  		}
  1240  
  1241  		out, err = h.Template(templateOpts)
  1242  		if err != nil {
  1243  			return nil, err
  1244  		}
  1245  	}
  1246  	return kube.SplitYAML([]byte(out))
  1247  }
  1248  
  1249  func getResolvedValueFiles(
  1250  	appPath string,
  1251  	repoRoot string,
  1252  	env *v1alpha1.Env,
  1253  	allowedValueFilesSchemas []string,
  1254  	rawValueFiles []string,
  1255  	refSources map[string]*v1alpha1.RefTarget,
  1256  	gitRepoPaths io.TempPaths,
  1257  	ignoreMissingValueFiles bool,
  1258  ) ([]pathutil.ResolvedFilePath, error) {
  1259  	var resolvedValueFiles []pathutil.ResolvedFilePath
  1260  	for _, rawValueFile := range rawValueFiles {
  1261  		var isRemote = false
  1262  		var resolvedPath pathutil.ResolvedFilePath
  1263  		var err error
  1264  
  1265  		referencedSource := getReferencedSource(rawValueFile, refSources)
  1266  		if referencedSource != nil {
  1267  			// If the $-prefixed path appears to reference another source, do env substitution _after_ resolving that source.
  1268  			resolvedPath, err = getResolvedRefValueFile(rawValueFile, env, allowedValueFilesSchemas, referencedSource.Repo.Repo, gitRepoPaths)
  1269  			if err != nil {
  1270  				return nil, fmt.Errorf("error resolving value file path: %w", err)
  1271  			}
  1272  		} else {
  1273  			// This will resolve val to an absolute path (or an URL)
  1274  			resolvedPath, isRemote, err = pathutil.ResolveValueFilePathOrUrl(appPath, repoRoot, env.Envsubst(rawValueFile), allowedValueFilesSchemas)
  1275  			if err != nil {
  1276  				return nil, fmt.Errorf("error resolving value file path: %w", err)
  1277  			}
  1278  		}
  1279  
  1280  		if !isRemote {
  1281  			_, err = os.Stat(string(resolvedPath))
  1282  			if os.IsNotExist(err) {
  1283  				if ignoreMissingValueFiles {
  1284  					log.Debugf(" %s values file does not exist", resolvedPath)
  1285  					continue
  1286  				}
  1287  			}
  1288  		}
  1289  
  1290  		resolvedValueFiles = append(resolvedValueFiles, resolvedPath)
  1291  	}
  1292  	return resolvedValueFiles, nil
  1293  }
  1294  
  1295  func getResolvedRefValueFile(
  1296  	rawValueFile string,
  1297  	env *v1alpha1.Env,
  1298  	allowedValueFilesSchemas []string,
  1299  	refSourceRepo string,
  1300  	gitRepoPaths io.TempPaths,
  1301  ) (pathutil.ResolvedFilePath, error) {
  1302  	pathStrings := strings.Split(rawValueFile, "/")
  1303  	repoPath := gitRepoPaths.GetPathIfExists(git.NormalizeGitURL(refSourceRepo))
  1304  	if repoPath == "" {
  1305  		return "", fmt.Errorf("failed to find repo %q", refSourceRepo)
  1306  	}
  1307  	pathStrings[0] = "" // Remove first segment. It will be inserted by pathutil.ResolveValueFilePathOrUrl.
  1308  	substitutedPath := strings.Join(pathStrings, "/")
  1309  
  1310  	// Resolve the path relative to the referenced repo and block any attempt at traversal.
  1311  	resolvedPath, _, err := pathutil.ResolveValueFilePathOrUrl(repoPath, repoPath, env.Envsubst(substitutedPath), allowedValueFilesSchemas)
  1312  	if err != nil {
  1313  		return "", fmt.Errorf("error resolving value file path: %w", err)
  1314  	}
  1315  	return resolvedPath, nil
  1316  }
  1317  
  1318  func getReferencedSource(rawValueFile string, refSources map[string]*v1alpha1.RefTarget) *v1alpha1.RefTarget {
  1319  	if !strings.HasPrefix(rawValueFile, "$") {
  1320  		return nil
  1321  	}
  1322  	refVar := strings.Split(rawValueFile, "/")[0]
  1323  	referencedSource := refSources[refVar]
  1324  	return referencedSource
  1325  }
  1326  
  1327  func getRepoCredential(repoCredentials []*v1alpha1.RepoCreds, repoURL string) *v1alpha1.RepoCreds {
  1328  	for _, cred := range repoCredentials {
  1329  		url := strings.TrimPrefix(repoURL, ociPrefix)
  1330  		if strings.HasPrefix(url, cred.URL) {
  1331  			return cred
  1332  		}
  1333  	}
  1334  	return nil
  1335  }
  1336  
  1337  type GenerateManifestOpt func(*generateManifestOpt)
  1338  type generateManifestOpt struct {
  1339  	cmpTarDoneCh        chan<- bool
  1340  	cmpTarExcludedGlobs []string
  1341  }
  1342  
  1343  func newGenerateManifestOpt(opts ...GenerateManifestOpt) *generateManifestOpt {
  1344  	o := &generateManifestOpt{}
  1345  	for _, opt := range opts {
  1346  		opt(o)
  1347  	}
  1348  	return o
  1349  }
  1350  
  1351  // WithCMPTarDoneChannel defines the channel to be used to signalize when the tarball
  1352  // generation is concluded when generating manifests with the CMP server. This is used
  1353  // to unlock the git repo as soon as possible.
  1354  func WithCMPTarDoneChannel(ch chan<- bool) GenerateManifestOpt {
  1355  	return func(o *generateManifestOpt) {
  1356  		o.cmpTarDoneCh = ch
  1357  	}
  1358  }
  1359  
  1360  // WithCMPTarExcludedGlobs defines globs for files to filter out when streaming the tarball
  1361  // to a CMP sidecar.
  1362  func WithCMPTarExcludedGlobs(excludedGlobs []string) GenerateManifestOpt {
  1363  	return func(o *generateManifestOpt) {
  1364  		o.cmpTarExcludedGlobs = excludedGlobs
  1365  	}
  1366  }
  1367  
  1368  // GenerateManifests generates manifests from a path. Overrides are applied as a side effect on the given ApplicationSource.
  1369  func GenerateManifests(ctx context.Context, appPath, repoRoot, revision string, q *apiclient.ManifestRequest, isLocal bool, gitCredsStore git.CredsStore, maxCombinedManifestQuantity resource.Quantity, gitRepoPaths io.TempPaths, opts ...GenerateManifestOpt) (*apiclient.ManifestResponse, error) {
  1370  	opt := newGenerateManifestOpt(opts...)
  1371  	var targetObjs []*unstructured.Unstructured
  1372  
  1373  	resourceTracking := argo.NewResourceTracking()
  1374  
  1375  	appSourceType, err := GetAppSourceType(ctx, q.ApplicationSource, appPath, repoRoot, q.AppName, q.EnabledSourceTypes, opt.cmpTarExcludedGlobs)
  1376  	if err != nil {
  1377  		return nil, fmt.Errorf("error getting app source type: %w", err)
  1378  	}
  1379  	repoURL := ""
  1380  	if q.Repo != nil {
  1381  		repoURL = q.Repo.Repo
  1382  	}
  1383  	env := newEnv(q, revision)
  1384  
  1385  	switch appSourceType {
  1386  	case v1alpha1.ApplicationSourceTypeHelm:
  1387  		targetObjs, err = helmTemplate(appPath, repoRoot, env, q, isLocal, gitRepoPaths)
  1388  	case v1alpha1.ApplicationSourceTypeKustomize:
  1389  		kustomizeBinary := ""
  1390  		if q.KustomizeOptions != nil {
  1391  			kustomizeBinary = q.KustomizeOptions.BinaryPath
  1392  		}
  1393  		k := kustomize.NewKustomizeApp(repoRoot, appPath, q.Repo.GetGitCreds(gitCredsStore), repoURL, kustomizeBinary)
  1394  		targetObjs, _, err = k.Build(q.ApplicationSource.Kustomize, q.KustomizeOptions, env)
  1395  	case v1alpha1.ApplicationSourceTypePlugin:
  1396  		pluginName := ""
  1397  		if q.ApplicationSource.Plugin != nil {
  1398  			pluginName = q.ApplicationSource.Plugin.Name
  1399  		}
  1400  		// if pluginName is provided it has to be `<metadata.name>-<spec.version>` or just `<metadata.name>` if plugin version is empty
  1401  		targetObjs, err = runConfigManagementPluginSidecars(ctx, appPath, repoRoot, pluginName, env, q, opt.cmpTarDoneCh, opt.cmpTarExcludedGlobs)
  1402  		if err != nil {
  1403  			err = fmt.Errorf("plugin sidecar failed. %s", err.Error())
  1404  		}
  1405  	case v1alpha1.ApplicationSourceTypeDirectory:
  1406  		var directory *v1alpha1.ApplicationSourceDirectory
  1407  		if directory = q.ApplicationSource.Directory; directory == nil {
  1408  			directory = &v1alpha1.ApplicationSourceDirectory{}
  1409  		}
  1410  		logCtx := log.WithField("application", q.AppName)
  1411  		targetObjs, err = findManifests(logCtx, appPath, repoRoot, env, *directory, q.EnabledSourceTypes, maxCombinedManifestQuantity)
  1412  	}
  1413  	if err != nil {
  1414  		return nil, err
  1415  	}
  1416  
  1417  	manifests := make([]string, 0)
  1418  	for _, obj := range targetObjs {
  1419  		if obj == nil {
  1420  			continue
  1421  		}
  1422  
  1423  		var targets []*unstructured.Unstructured
  1424  		if obj.IsList() {
  1425  			err = obj.EachListItem(func(object runtime.Object) error {
  1426  				unstructuredObj, ok := object.(*unstructured.Unstructured)
  1427  				if ok {
  1428  					targets = append(targets, unstructuredObj)
  1429  					return nil
  1430  				}
  1431  				return fmt.Errorf("resource list item has unexpected type")
  1432  			})
  1433  			if err != nil {
  1434  				return nil, err
  1435  			}
  1436  		} else if isNullList(obj) {
  1437  			// noop
  1438  		} else {
  1439  			targets = []*unstructured.Unstructured{obj}
  1440  		}
  1441  
  1442  		for _, target := range targets {
  1443  			if q.AppLabelKey != "" && q.AppName != "" && !kube.IsCRD(target) {
  1444  				err = resourceTracking.SetAppInstance(target, q.AppLabelKey, q.AppName, q.Namespace, v1alpha1.TrackingMethod(q.TrackingMethod))
  1445  				if err != nil {
  1446  					return nil, fmt.Errorf("failed to set app instance tracking info on manifest: %w", err)
  1447  				}
  1448  			}
  1449  			manifestStr, err := json.Marshal(target.Object)
  1450  			if err != nil {
  1451  				return nil, err
  1452  			}
  1453  			manifests = append(manifests, string(manifestStr))
  1454  		}
  1455  	}
  1456  
  1457  	return &apiclient.ManifestResponse{
  1458  		Manifests:  manifests,
  1459  		SourceType: string(appSourceType),
  1460  	}, nil
  1461  }
  1462  
  1463  func newEnv(q *apiclient.ManifestRequest, revision string) *v1alpha1.Env {
  1464  	shortRevision := revision
  1465  	if len(shortRevision) > 7 {
  1466  		shortRevision = shortRevision[:7]
  1467  	}
  1468  	return &v1alpha1.Env{
  1469  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: q.AppName},
  1470  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: q.Namespace},
  1471  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: revision},
  1472  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION_SHORT", Value: shortRevision},
  1473  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: q.Repo.Repo},
  1474  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: q.ApplicationSource.Path},
  1475  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: q.ApplicationSource.TargetRevision},
  1476  	}
  1477  }
  1478  
  1479  // mergeSourceParameters merges parameter overrides from one or more files in
  1480  // the Git repo into the given ApplicationSource objects.
  1481  //
  1482  // If .argocd-source.yaml exists at application's path in repository, it will
  1483  // be read and merged. If appName is not the empty string, and a file named
  1484  // .argocd-source-<appName>.yaml exists, it will also be read and merged.
  1485  func mergeSourceParameters(source *v1alpha1.ApplicationSource, path, appName string) error {
  1486  	repoFilePath := filepath.Join(path, repoSourceFile)
  1487  	overrides := []string{repoFilePath}
  1488  	if appName != "" {
  1489  		overrides = append(overrides, filepath.Join(path, fmt.Sprintf(appSourceFile, appName)))
  1490  	}
  1491  
  1492  	var merged = *source.DeepCopy()
  1493  
  1494  	for _, filename := range overrides {
  1495  		info, err := os.Stat(filename)
  1496  		if os.IsNotExist(err) {
  1497  			continue
  1498  		} else if info != nil && info.IsDir() {
  1499  			continue
  1500  		} else if err != nil {
  1501  			// filename should be part of error message here
  1502  			return err
  1503  		}
  1504  
  1505  		data, err := json.Marshal(merged)
  1506  		if err != nil {
  1507  			return fmt.Errorf("%s: %v", filename, err)
  1508  		}
  1509  		patch, err := os.ReadFile(filename)
  1510  		if err != nil {
  1511  			return fmt.Errorf("%s: %v", filename, err)
  1512  		}
  1513  		patch, err = yaml.YAMLToJSON(patch)
  1514  		if err != nil {
  1515  			return fmt.Errorf("%s: %v", filename, err)
  1516  		}
  1517  		data, err = jsonpatch.MergePatch(data, patch)
  1518  		if err != nil {
  1519  			return fmt.Errorf("%s: %v", filename, err)
  1520  		}
  1521  		err = json.Unmarshal(data, &merged)
  1522  		if err != nil {
  1523  			return fmt.Errorf("%s: %v", filename, err)
  1524  		}
  1525  	}
  1526  
  1527  	// make sure only config management tools related properties are used and ignore everything else
  1528  	merged.Chart = source.Chart
  1529  	merged.Path = source.Path
  1530  	merged.RepoURL = source.RepoURL
  1531  	merged.TargetRevision = source.TargetRevision
  1532  
  1533  	*source = merged
  1534  	return nil
  1535  }
  1536  
  1537  // GetAppSourceType returns explicit application source type or examines a directory and determines its application source type
  1538  func GetAppSourceType(ctx context.Context, source *v1alpha1.ApplicationSource, appPath, repoPath, appName string, enableGenerateManifests map[string]bool, tarExcludedGlobs []string) (v1alpha1.ApplicationSourceType, error) {
  1539  	err := mergeSourceParameters(source, appPath, appName)
  1540  	if err != nil {
  1541  		return "", fmt.Errorf("error while parsing source parameters: %v", err)
  1542  	}
  1543  
  1544  	appSourceType, err := source.ExplicitType()
  1545  	if err != nil {
  1546  		return "", err
  1547  	}
  1548  	if appSourceType != nil {
  1549  		if !discovery.IsManifestGenerationEnabled(*appSourceType, enableGenerateManifests) {
  1550  			log.Debugf("Manifest generation is disabled for '%s'. Assuming plain YAML manifest.", *appSourceType)
  1551  			return v1alpha1.ApplicationSourceTypeDirectory, nil
  1552  		}
  1553  		return *appSourceType, nil
  1554  	}
  1555  	appType, err := discovery.AppType(ctx, appPath, repoPath, enableGenerateManifests, tarExcludedGlobs)
  1556  	if err != nil {
  1557  		return "", fmt.Errorf("error getting app source type: %v", err)
  1558  	}
  1559  	return v1alpha1.ApplicationSourceType(appType), nil
  1560  }
  1561  
  1562  // isNullList checks if the object is a "List" type where items is null instead of an empty list.
  1563  // Handles a corner case where obj.IsList() returns false when a manifest is like:
  1564  // ---
  1565  // apiVersion: v1
  1566  // items: null
  1567  // kind: ConfigMapList
  1568  func isNullList(obj *unstructured.Unstructured) bool {
  1569  	if _, ok := obj.Object["spec"]; ok {
  1570  		return false
  1571  	}
  1572  	if _, ok := obj.Object["status"]; ok {
  1573  		return false
  1574  	}
  1575  	field, ok := obj.Object["items"]
  1576  	if !ok {
  1577  		return false
  1578  	}
  1579  	return field == nil
  1580  }
  1581  
  1582  var manifestFile = regexp.MustCompile(`^.*\.(yaml|yml|json|jsonnet)$`)
  1583  
  1584  // findManifests looks at all yaml files in a directory and unmarshals them into a list of unstructured objects
  1585  func findManifests(logCtx *log.Entry, appPath string, repoRoot string, env *v1alpha1.Env, directory v1alpha1.ApplicationSourceDirectory, enabledManifestGeneration map[string]bool, maxCombinedManifestQuantity resource.Quantity) ([]*unstructured.Unstructured, error) {
  1586  	// Validate the directory before loading any manifests to save memory.
  1587  	potentiallyValidManifests, err := getPotentiallyValidManifests(logCtx, appPath, repoRoot, directory.Recurse, directory.Include, directory.Exclude, maxCombinedManifestQuantity)
  1588  	if err != nil {
  1589  		logCtx.Errorf("failed to get potentially valid manifests: %s", err)
  1590  		return nil, fmt.Errorf("failed to get potentially valid manifests: %w", err)
  1591  	}
  1592  
  1593  	var objs []*unstructured.Unstructured
  1594  	for _, potentiallyValidManifest := range potentiallyValidManifests {
  1595  		manifestPath := potentiallyValidManifest.path
  1596  		manifestFileInfo := potentiallyValidManifest.fileInfo
  1597  
  1598  		if strings.HasSuffix(manifestFileInfo.Name(), ".jsonnet") {
  1599  			if !discovery.IsManifestGenerationEnabled(v1alpha1.ApplicationSourceTypeDirectory, enabledManifestGeneration) {
  1600  				continue
  1601  			}
  1602  			vm, err := makeJsonnetVm(appPath, repoRoot, directory.Jsonnet, env)
  1603  			if err != nil {
  1604  				return nil, err
  1605  			}
  1606  			jsonStr, err := vm.EvaluateFile(manifestPath)
  1607  			if err != nil {
  1608  				return nil, status.Errorf(codes.FailedPrecondition, "Failed to evaluate jsonnet %q: %v", manifestFileInfo.Name(), err)
  1609  			}
  1610  
  1611  			// attempt to unmarshal either array or single object
  1612  			var jsonObjs []*unstructured.Unstructured
  1613  			err = json.Unmarshal([]byte(jsonStr), &jsonObjs)
  1614  			if err == nil {
  1615  				objs = append(objs, jsonObjs...)
  1616  			} else {
  1617  				var jsonObj unstructured.Unstructured
  1618  				err = json.Unmarshal([]byte(jsonStr), &jsonObj)
  1619  				if err != nil {
  1620  					return nil, status.Errorf(codes.FailedPrecondition, "Failed to unmarshal generated json %q: %v", manifestFileInfo.Name(), err)
  1621  				}
  1622  				objs = append(objs, &jsonObj)
  1623  			}
  1624  		} else {
  1625  			err := getObjsFromYAMLOrJson(logCtx, manifestPath, manifestFileInfo.Name(), &objs)
  1626  			if err != nil {
  1627  				return nil, err
  1628  			}
  1629  		}
  1630  	}
  1631  	return objs, nil
  1632  }
  1633  
  1634  // getObjsFromYAMLOrJson unmarshals the given yaml or json file and appends it to the given list of objects.
  1635  func getObjsFromYAMLOrJson(logCtx *log.Entry, manifestPath string, filename string, objs *[]*unstructured.Unstructured) error {
  1636  	reader, err := utfutil.OpenFile(manifestPath, utfutil.UTF8)
  1637  	if err != nil {
  1638  		return status.Errorf(codes.FailedPrecondition, "Failed to open %q", manifestPath)
  1639  	}
  1640  	defer func() {
  1641  		err := reader.Close()
  1642  		if err != nil {
  1643  			logCtx.Errorf("failed to close %q - potential memory leak", manifestPath)
  1644  		}
  1645  	}()
  1646  	if strings.HasSuffix(filename, ".json") {
  1647  		var obj unstructured.Unstructured
  1648  		decoder := json.NewDecoder(reader)
  1649  		err = decoder.Decode(&obj)
  1650  		if err != nil {
  1651  			return status.Errorf(codes.FailedPrecondition, "Failed to unmarshal %q: %v", filename, err)
  1652  		}
  1653  		if decoder.More() {
  1654  			return status.Errorf(codes.FailedPrecondition, "Found multiple objects in %q. Only single objects are allowed in JSON files.", filename)
  1655  		}
  1656  		*objs = append(*objs, &obj)
  1657  	} else {
  1658  		yamlObjs, err := splitYAMLOrJSON(reader)
  1659  		if err != nil {
  1660  			if len(yamlObjs) > 0 {
  1661  				// If we get here, we had a multiple objects in a single YAML file which had some
  1662  				// valid k8s objects, but errors parsing others (within the same file). It's very
  1663  				// likely the user messed up a portion of the YAML, so report on that.
  1664  				return status.Errorf(codes.FailedPrecondition, "Failed to unmarshal %q: %v", filename, err)
  1665  			}
  1666  			// Read the whole file to check whether it looks like a manifest.
  1667  			out, err := utfutil.ReadFile(manifestPath, utfutil.UTF8)
  1668  			// Otherwise, let's see if it looks like a resource, if yes, we return error
  1669  			if bytes.Contains(out, []byte("apiVersion:")) &&
  1670  				bytes.Contains(out, []byte("kind:")) &&
  1671  				bytes.Contains(out, []byte("metadata:")) {
  1672  				return status.Errorf(codes.FailedPrecondition, "Failed to unmarshal %q: %v", filename, err)
  1673  			}
  1674  			// Otherwise, it might be an unrelated YAML file which we will ignore
  1675  		}
  1676  		*objs = append(*objs, yamlObjs...)
  1677  	}
  1678  	return nil
  1679  }
  1680  
  1681  // splitYAMLOrJSON reads a YAML or JSON file and gets each document as an unstructured object. If the unmarshaller
  1682  // encounters an error, objects read up until the error are returned.
  1683  func splitYAMLOrJSON(reader goio.Reader) ([]*unstructured.Unstructured, error) {
  1684  	d := kubeyaml.NewYAMLOrJSONDecoder(reader, 4096)
  1685  	var objs []*unstructured.Unstructured
  1686  	for {
  1687  		u := &unstructured.Unstructured{}
  1688  		if err := d.Decode(&u); err != nil {
  1689  			if err == goio.EOF {
  1690  				break
  1691  			}
  1692  			return objs, fmt.Errorf("failed to unmarshal manifest: %v", err)
  1693  		}
  1694  		if u == nil {
  1695  			continue
  1696  		}
  1697  		objs = append(objs, u)
  1698  	}
  1699  	return objs, nil
  1700  }
  1701  
  1702  // getPotentiallyValidManifestFile checks whether the given path/FileInfo may be a valid manifest file. Returns a non-nil error if
  1703  // there was an error that should not be handled by ignoring the file. Returns non-nil realFileInfo if the file is a
  1704  // potential manifest. Returns a non-empty ignoreMessage if there's a message that should be logged about why the file
  1705  // was skipped. If realFileInfo is nil and the ignoreMessage is empty, there's no need to log the ignoreMessage; the
  1706  // file was skipped for a mundane reason.
  1707  //
  1708  // The file is still only a "potentially" valid manifest file because it could be invalid JSON or YAML, or it might not
  1709  // be a valid Kubernetes resource. This function tests everything possible without actually reading the file.
  1710  //
  1711  // repoPath must be absolute.
  1712  func getPotentiallyValidManifestFile(path string, f os.FileInfo, appPath, repoRoot, include, exclude string) (realFileInfo os.FileInfo, warning string, err error) {
  1713  	relPath, err := filepath.Rel(appPath, path)
  1714  	if err != nil {
  1715  		return nil, "", fmt.Errorf("failed to get relative path of %q: %w", path, err)
  1716  	}
  1717  
  1718  	if !manifestFile.MatchString(f.Name()) {
  1719  		return nil, "", nil
  1720  	}
  1721  
  1722  	// If the file is a symlink, these will be overridden with the destination file's info.
  1723  	var relRealPath = relPath
  1724  	realFileInfo = f
  1725  
  1726  	if files.IsSymlink(f) {
  1727  		realPath, err := filepath.EvalSymlinks(path)
  1728  		if err != nil {
  1729  			if os.IsNotExist(err) {
  1730  				return nil, fmt.Sprintf("destination of symlink %q is missing", relPath), nil
  1731  			}
  1732  			return nil, "", fmt.Errorf("failed to evaluate symlink at %q: %w", relPath, err)
  1733  		}
  1734  		if !files.Inbound(realPath, repoRoot) {
  1735  			return nil, "", fmt.Errorf("illegal filepath in symlink at %q", relPath)
  1736  		}
  1737  		realFileInfo, err = os.Stat(realPath)
  1738  		if err != nil {
  1739  			if os.IsNotExist(err) {
  1740  				// This should have been caught by filepath.EvalSymlinks, but check again since that function's docs
  1741  				// don't promise to return this error.
  1742  				return nil, fmt.Sprintf("destination of symlink %q is missing at %q", relPath, realPath), nil
  1743  			}
  1744  			return nil, "", fmt.Errorf("failed to get file info for symlink at %q to %q: %w", relPath, realPath, err)
  1745  		}
  1746  		relRealPath, err = filepath.Rel(repoRoot, realPath)
  1747  		if err != nil {
  1748  			return nil, "", fmt.Errorf("failed to get relative path of %q: %w", realPath, err)
  1749  		}
  1750  	}
  1751  
  1752  	// FileInfo.Size() behavior is platform-specific for non-regular files. Allow only regular files, so we guarantee
  1753  	// accurate file sizes.
  1754  	if !realFileInfo.Mode().IsRegular() {
  1755  		return nil, fmt.Sprintf("ignoring symlink at %q to non-regular file %q", relPath, relRealPath), nil
  1756  	}
  1757  
  1758  	if exclude != "" && glob.Match(exclude, relPath) {
  1759  		return nil, "", nil
  1760  	}
  1761  
  1762  	if include != "" && !glob.Match(include, relPath) {
  1763  		return nil, "", nil
  1764  	}
  1765  
  1766  	return realFileInfo, "", nil
  1767  }
  1768  
  1769  type potentiallyValidManifest struct {
  1770  	path     string
  1771  	fileInfo os.FileInfo
  1772  }
  1773  
  1774  // getPotentiallyValidManifests ensures that 1) there are no errors while checking for potential manifest files in the given dir
  1775  // and 2) the combined file size of the potentially-valid manifest files does not exceed the limit.
  1776  func getPotentiallyValidManifests(logCtx *log.Entry, appPath string, repoRoot string, recurse bool, include string, exclude string, maxCombinedManifestQuantity resource.Quantity) ([]potentiallyValidManifest, error) {
  1777  	maxCombinedManifestFileSize := maxCombinedManifestQuantity.Value()
  1778  	var currentCombinedManifestFileSize = int64(0)
  1779  
  1780  	var potentiallyValidManifests []potentiallyValidManifest
  1781  	err := filepath.Walk(appPath, func(path string, f os.FileInfo, err error) error {
  1782  		if err != nil {
  1783  			return err
  1784  		}
  1785  
  1786  		if f.IsDir() {
  1787  			if path != appPath && !recurse {
  1788  				return filepath.SkipDir
  1789  			}
  1790  			return nil
  1791  		}
  1792  
  1793  		realFileInfo, warning, err := getPotentiallyValidManifestFile(path, f, appPath, repoRoot, include, exclude)
  1794  		if err != nil {
  1795  			return fmt.Errorf("invalid manifest file %q: %w", path, err)
  1796  		}
  1797  		if realFileInfo == nil {
  1798  			if warning != "" {
  1799  				logCtx.Warnf("skipping manifest file %q: %s", path, warning)
  1800  			}
  1801  			return nil
  1802  		}
  1803  		// Don't count jsonnet file size against max. It's jsonnet's responsibility to manage memory usage.
  1804  		if !strings.HasSuffix(f.Name(), ".jsonnet") {
  1805  			// We use the realFileInfo size (which is guaranteed to be a regular file instead of a symlink or other
  1806  			// non-regular file) because .Size() behavior is platform-specific for non-regular files.
  1807  			currentCombinedManifestFileSize += realFileInfo.Size()
  1808  			if maxCombinedManifestFileSize != 0 && currentCombinedManifestFileSize > maxCombinedManifestFileSize {
  1809  				return ErrExceededMaxCombinedManifestFileSize
  1810  			}
  1811  		}
  1812  		potentiallyValidManifests = append(potentiallyValidManifests, potentiallyValidManifest{path: path, fileInfo: f})
  1813  		return nil
  1814  	})
  1815  	if err != nil {
  1816  		// Not wrapping, because this error should be wrapped by the caller.
  1817  		return nil, err
  1818  	}
  1819  
  1820  	return potentiallyValidManifests, nil
  1821  }
  1822  
  1823  func makeJsonnetVm(appPath string, repoRoot string, sourceJsonnet v1alpha1.ApplicationSourceJsonnet, env *v1alpha1.Env) (*jsonnet.VM, error) {
  1824  
  1825  	vm := jsonnet.MakeVM()
  1826  	for i, j := range sourceJsonnet.TLAs {
  1827  		sourceJsonnet.TLAs[i].Value = env.Envsubst(j.Value)
  1828  	}
  1829  	for i, j := range sourceJsonnet.ExtVars {
  1830  		sourceJsonnet.ExtVars[i].Value = env.Envsubst(j.Value)
  1831  	}
  1832  	for _, arg := range sourceJsonnet.TLAs {
  1833  		if arg.Code {
  1834  			vm.TLACode(arg.Name, arg.Value)
  1835  		} else {
  1836  			vm.TLAVar(arg.Name, arg.Value)
  1837  		}
  1838  	}
  1839  	for _, extVar := range sourceJsonnet.ExtVars {
  1840  		if extVar.Code {
  1841  			vm.ExtCode(extVar.Name, extVar.Value)
  1842  		} else {
  1843  			vm.ExtVar(extVar.Name, extVar.Value)
  1844  		}
  1845  	}
  1846  
  1847  	// Jsonnet Imports relative to the repository path
  1848  	jpaths := []string{appPath}
  1849  	for _, p := range sourceJsonnet.Libs {
  1850  		// the jsonnet library path is relative to the repository root, not application path
  1851  		jpath, err := pathutil.ResolveFileOrDirectoryPath(repoRoot, repoRoot, p)
  1852  		if err != nil {
  1853  			return nil, err
  1854  		}
  1855  		jpaths = append(jpaths, string(jpath))
  1856  	}
  1857  
  1858  	vm.Importer(&jsonnet.FileImporter{
  1859  		JPaths: jpaths,
  1860  	})
  1861  
  1862  	return vm, nil
  1863  }
  1864  
  1865  func getPluginEnvs(env *v1alpha1.Env, q *apiclient.ManifestRequest) ([]string, error) {
  1866  	envVars := env.Environ()
  1867  	envVars = append(envVars, "KUBE_VERSION="+text.SemVer(q.KubeVersion))
  1868  	envVars = append(envVars, "KUBE_API_VERSIONS="+strings.Join(q.ApiVersions, ","))
  1869  
  1870  	return getPluginParamEnvs(envVars, q.ApplicationSource.Plugin)
  1871  }
  1872  
  1873  // getPluginParamEnvs gets environment variables for plugin parameter announcement generation.
  1874  func getPluginParamEnvs(envVars []string, plugin *v1alpha1.ApplicationSourcePlugin) ([]string, error) {
  1875  	env := envVars
  1876  
  1877  	parsedEnv := make(v1alpha1.Env, len(env))
  1878  	for i, v := range env {
  1879  		parsedVar, err := v1alpha1.NewEnvEntry(v)
  1880  		if err != nil {
  1881  			return nil, fmt.Errorf("failed to parse env vars")
  1882  		}
  1883  		parsedEnv[i] = parsedVar
  1884  	}
  1885  
  1886  	if plugin != nil {
  1887  		pluginEnv := plugin.Env
  1888  		for _, entry := range pluginEnv {
  1889  			newValue := parsedEnv.Envsubst(entry.Value)
  1890  			env = append(env, fmt.Sprintf("ARGOCD_ENV_%s=%s", entry.Name, newValue))
  1891  		}
  1892  		paramEnv, err := plugin.Parameters.Environ()
  1893  		if err != nil {
  1894  			return nil, fmt.Errorf("failed to generate env vars from parameters: %w", err)
  1895  		}
  1896  		env = append(env, paramEnv...)
  1897  	}
  1898  
  1899  	return env, nil
  1900  }
  1901  
  1902  func runConfigManagementPluginSidecars(ctx context.Context, appPath, repoPath, pluginName string, envVars *v1alpha1.Env, q *apiclient.ManifestRequest, tarDoneCh chan<- bool, tarExcludedGlobs []string) ([]*unstructured.Unstructured, error) {
  1903  	// compute variables.
  1904  	env, err := getPluginEnvs(envVars, q)
  1905  	if err != nil {
  1906  		return nil, err
  1907  	}
  1908  
  1909  	// detect config management plugin server
  1910  	conn, cmpClient, err := discovery.DetectConfigManagementPlugin(ctx, appPath, repoPath, pluginName, env, tarExcludedGlobs)
  1911  	if err != nil {
  1912  		return nil, err
  1913  	}
  1914  	defer io.Close(conn)
  1915  
  1916  	// generate manifests using commands provided in plugin config file in detected cmp-server sidecar
  1917  	cmpManifests, err := generateManifestsCMP(ctx, appPath, repoPath, env, cmpClient, tarDoneCh, tarExcludedGlobs)
  1918  	if err != nil {
  1919  		return nil, fmt.Errorf("error generating manifests in cmp: %s", err)
  1920  	}
  1921  	var manifests []*unstructured.Unstructured
  1922  	for _, manifestString := range cmpManifests.Manifests {
  1923  		manifestObjs, err := kube.SplitYAML([]byte(manifestString))
  1924  		if err != nil {
  1925  			sanitizedManifestString := manifestString
  1926  			if len(manifestString) > 1000 {
  1927  				sanitizedManifestString = sanitizedManifestString[:1000]
  1928  			}
  1929  			log.Debugf("Failed to convert generated manifests. Beginning of generated manifests: %q", sanitizedManifestString)
  1930  			return nil, fmt.Errorf("failed to convert CMP manifests to unstructured objects: %s", err.Error())
  1931  		}
  1932  		manifests = append(manifests, manifestObjs...)
  1933  	}
  1934  	return manifests, nil
  1935  }
  1936  
  1937  // generateManifestsCMP will send the appPath files to the cmp-server over a gRPC stream.
  1938  // The cmp-server will generate the manifests. Returns a response object with the generated
  1939  // manifests.
  1940  func generateManifestsCMP(ctx context.Context, appPath, repoPath string, env []string, cmpClient pluginclient.ConfigManagementPluginServiceClient, tarDoneCh chan<- bool, tarExcludedGlobs []string) (*pluginclient.ManifestResponse, error) {
  1941  	generateManifestStream, err := cmpClient.GenerateManifest(ctx, grpc_retry.Disable())
  1942  	if err != nil {
  1943  		return nil, fmt.Errorf("error getting generateManifestStream: %w", err)
  1944  	}
  1945  	opts := []cmp.SenderOption{
  1946  		cmp.WithTarDoneChan(tarDoneCh),
  1947  	}
  1948  
  1949  	err = cmp.SendRepoStream(generateManifestStream.Context(), appPath, repoPath, generateManifestStream, env, tarExcludedGlobs, opts...)
  1950  	if err != nil {
  1951  		return nil, fmt.Errorf("error sending file to cmp-server: %s", err)
  1952  	}
  1953  
  1954  	return generateManifestStream.CloseAndRecv()
  1955  }
  1956  
  1957  func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppDetailsQuery) (*apiclient.RepoAppDetailsResponse, error) {
  1958  	res := &apiclient.RepoAppDetailsResponse{}
  1959  
  1960  	cacheFn := s.createGetAppDetailsCacheHandler(res, q)
  1961  	operation := func(repoRoot, commitSHA, revision string, ctxSrc operationContextSrc) error {
  1962  		opContext, err := ctxSrc()
  1963  		if err != nil {
  1964  			return err
  1965  		}
  1966  
  1967  		appSourceType, err := GetAppSourceType(ctx, q.Source, opContext.appPath, repoRoot, q.AppName, q.EnabledSourceTypes, s.initConstants.CMPTarExcludedGlobs)
  1968  		if err != nil {
  1969  			return err
  1970  		}
  1971  
  1972  		res.Type = string(appSourceType)
  1973  
  1974  		switch appSourceType {
  1975  		case v1alpha1.ApplicationSourceTypeHelm:
  1976  			if err := populateHelmAppDetails(res, opContext.appPath, repoRoot, q, s.gitRepoPaths); err != nil {
  1977  				return err
  1978  			}
  1979  		case v1alpha1.ApplicationSourceTypeKustomize:
  1980  			if err := populateKustomizeAppDetails(res, q, repoRoot, opContext.appPath, commitSHA, s.gitCredsStore); err != nil {
  1981  				return err
  1982  			}
  1983  		case v1alpha1.ApplicationSourceTypePlugin:
  1984  			if err := populatePluginAppDetails(ctx, res, opContext.appPath, repoRoot, q, s.gitCredsStore, s.initConstants.CMPTarExcludedGlobs); err != nil {
  1985  				return fmt.Errorf("failed to populate plugin app details: %w", err)
  1986  			}
  1987  		}
  1988  		_ = s.cache.SetAppDetails(revision, q.Source, q.RefSources, res, v1alpha1.TrackingMethod(q.TrackingMethod), nil)
  1989  		return nil
  1990  	}
  1991  
  1992  	settings := operationSettings{allowConcurrent: q.Source.AllowsConcurrentProcessing(), noCache: q.NoCache, noRevisionCache: q.NoCache || q.NoRevisionCache}
  1993  	err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, false, cacheFn, operation, settings, false, nil)
  1994  
  1995  	return res, err
  1996  }
  1997  
  1998  func (s *Service) createGetAppDetailsCacheHandler(res *apiclient.RepoAppDetailsResponse, q *apiclient.RepoServerAppDetailsQuery) func(revision string, _ cache.ResolvedRevisions, _ bool) (bool, error) {
  1999  	return func(revision string, _ cache.ResolvedRevisions, _ bool) (bool, error) {
  2000  		err := s.cache.GetAppDetails(revision, q.Source, q.RefSources, res, v1alpha1.TrackingMethod(q.TrackingMethod), nil)
  2001  		if err == nil {
  2002  			log.Infof("app details cache hit: %s/%s", revision, q.Source.Path)
  2003  			return true, nil
  2004  		}
  2005  
  2006  		if err != cache.ErrCacheMiss {
  2007  			log.Warnf("app details cache error %s: %v", revision, q.Source)
  2008  		} else {
  2009  			log.Infof("app details cache miss: %s/%s", revision, q.Source)
  2010  		}
  2011  		return false, nil
  2012  	}
  2013  }
  2014  
  2015  func populateHelmAppDetails(res *apiclient.RepoAppDetailsResponse, appPath string, repoRoot string, q *apiclient.RepoServerAppDetailsQuery, gitRepoPaths io.TempPaths) error {
  2016  	var selectedValueFiles []string
  2017  	var availableValueFiles []string
  2018  
  2019  	if q.Source.Helm != nil {
  2020  		selectedValueFiles = q.Source.Helm.ValueFiles
  2021  	}
  2022  
  2023  	err := filepath.Walk(appPath, walkHelmValueFilesInPath(appPath, &availableValueFiles))
  2024  	if err != nil {
  2025  		return err
  2026  	}
  2027  
  2028  	res.Helm = &apiclient.HelmAppSpec{ValueFiles: availableValueFiles}
  2029  	var version string
  2030  	var passCredentials bool
  2031  	if q.Source.Helm != nil {
  2032  		if q.Source.Helm.Version != "" {
  2033  			version = q.Source.Helm.Version
  2034  		}
  2035  		passCredentials = q.Source.Helm.PassCredentials
  2036  	}
  2037  	helmRepos, err := getHelmRepos(appPath, q.Repos, nil)
  2038  	if err != nil {
  2039  		return err
  2040  	}
  2041  	h, err := helm.NewHelmApp(appPath, helmRepos, false, version, q.Repo.Proxy, passCredentials)
  2042  	if err != nil {
  2043  		return err
  2044  	}
  2045  	defer h.Dispose()
  2046  	err = h.Init()
  2047  	if err != nil {
  2048  		return err
  2049  	}
  2050  
  2051  	if resolvedValuesPath, _, err := pathutil.ResolveValueFilePathOrUrl(appPath, repoRoot, "values.yaml", []string{}); err == nil {
  2052  		if err := loadFileIntoIfExists(resolvedValuesPath, &res.Helm.Values); err != nil {
  2053  			return err
  2054  		}
  2055  	} else {
  2056  		log.Warnf("Values file %s is not allowed: %v", filepath.Join(appPath, "values.yaml"), err)
  2057  	}
  2058  	ignoreMissingValueFiles := false
  2059  	if q.Source.Helm != nil {
  2060  		ignoreMissingValueFiles = q.Source.Helm.IgnoreMissingValueFiles
  2061  	}
  2062  	resolvedSelectedValueFiles, err := getResolvedValueFiles(appPath, repoRoot, &v1alpha1.Env{}, q.GetValuesFileSchemes(), selectedValueFiles, q.RefSources, gitRepoPaths, ignoreMissingValueFiles)
  2063  	if err != nil {
  2064  		return fmt.Errorf("failed to resolve value files: %w", err)
  2065  	}
  2066  	params, err := h.GetParameters(resolvedSelectedValueFiles, appPath, repoRoot)
  2067  	if err != nil {
  2068  		return err
  2069  	}
  2070  	for k, v := range params {
  2071  		res.Helm.Parameters = append(res.Helm.Parameters, &v1alpha1.HelmParameter{
  2072  			Name:  k,
  2073  			Value: v,
  2074  		})
  2075  	}
  2076  	for _, v := range fileParameters(q) {
  2077  		res.Helm.FileParameters = append(res.Helm.FileParameters, &v1alpha1.HelmFileParameter{
  2078  			Name: v.Name,
  2079  			Path: v.Path, // filepath.Join(appPath, v.Path),
  2080  		})
  2081  	}
  2082  	return nil
  2083  }
  2084  
  2085  func loadFileIntoIfExists(path pathutil.ResolvedFilePath, destination *string) error {
  2086  	stringPath := string(path)
  2087  	info, err := os.Stat(stringPath)
  2088  
  2089  	if err == nil && !info.IsDir() {
  2090  		bytes, err := os.ReadFile(stringPath)
  2091  		if err != nil {
  2092  			return fmt.Errorf("error reading file from %s: %w", stringPath, err)
  2093  		}
  2094  		*destination = string(bytes)
  2095  	}
  2096  
  2097  	return nil
  2098  }
  2099  
  2100  func walkHelmValueFilesInPath(root string, valueFiles *[]string) filepath.WalkFunc {
  2101  	return func(path string, info os.FileInfo, err error) error {
  2102  
  2103  		if err != nil {
  2104  			return fmt.Errorf("error reading helm values file from %s: %w", path, err)
  2105  		}
  2106  
  2107  		filename := info.Name()
  2108  		fileNameExt := strings.ToLower(filepath.Ext(path))
  2109  		if strings.Contains(filename, "values") && (fileNameExt == ".yaml" || fileNameExt == ".yml") {
  2110  			relPath, err := filepath.Rel(root, path)
  2111  			if err != nil {
  2112  				return fmt.Errorf("error traversing path from %s to %s: %w", root, path, err)
  2113  			}
  2114  			*valueFiles = append(*valueFiles, relPath)
  2115  		}
  2116  
  2117  		return nil
  2118  	}
  2119  }
  2120  
  2121  func populateKustomizeAppDetails(res *apiclient.RepoAppDetailsResponse, q *apiclient.RepoServerAppDetailsQuery, repoRoot string, appPath string, reversion string, credsStore git.CredsStore) error {
  2122  	res.Kustomize = &apiclient.KustomizeAppSpec{}
  2123  	kustomizeBinary := ""
  2124  	if q.KustomizeOptions != nil {
  2125  		kustomizeBinary = q.KustomizeOptions.BinaryPath
  2126  	}
  2127  	k := kustomize.NewKustomizeApp(repoRoot, appPath, q.Repo.GetGitCreds(credsStore), q.Repo.Repo, kustomizeBinary)
  2128  	fakeManifestRequest := apiclient.ManifestRequest{
  2129  		AppName:           q.AppName,
  2130  		Namespace:         "", // FIXME: omit it for now
  2131  		Repo:              q.Repo,
  2132  		ApplicationSource: q.Source,
  2133  	}
  2134  	env := newEnv(&fakeManifestRequest, reversion)
  2135  	_, images, err := k.Build(q.Source.Kustomize, q.KustomizeOptions, env)
  2136  	if err != nil {
  2137  		return err
  2138  	}
  2139  	res.Kustomize.Images = images
  2140  	return nil
  2141  }
  2142  
  2143  func populatePluginAppDetails(ctx context.Context, res *apiclient.RepoAppDetailsResponse, appPath string, repoPath string, q *apiclient.RepoServerAppDetailsQuery, store git.CredsStore, tarExcludedGlobs []string) error {
  2144  	res.Plugin = &apiclient.PluginAppSpec{}
  2145  
  2146  	envVars := []string{
  2147  		fmt.Sprintf("ARGOCD_APP_NAME=%s", q.AppName),
  2148  		fmt.Sprintf("ARGOCD_APP_SOURCE_REPO_URL=%s", q.Repo.Repo),
  2149  		fmt.Sprintf("ARGOCD_APP_SOURCE_PATH=%s", q.Source.Path),
  2150  		fmt.Sprintf("ARGOCD_APP_SOURCE_TARGET_REVISION=%s", q.Source.TargetRevision),
  2151  	}
  2152  
  2153  	env, err := getPluginParamEnvs(envVars, q.Source.Plugin)
  2154  	if err != nil {
  2155  		return fmt.Errorf("failed to get env vars for plugin: %w", err)
  2156  	}
  2157  
  2158  	pluginName := ""
  2159  	if q.Source != nil && q.Source.Plugin != nil {
  2160  		pluginName = q.Source.Plugin.Name
  2161  	}
  2162  	// detect config management plugin server (sidecar)
  2163  	conn, cmpClient, err := discovery.DetectConfigManagementPlugin(ctx, appPath, repoPath, pluginName, env, tarExcludedGlobs)
  2164  	if err != nil {
  2165  		return fmt.Errorf("failed to detect CMP for app: %w", err)
  2166  	}
  2167  	defer io.Close(conn)
  2168  
  2169  	parametersAnnouncementStream, err := cmpClient.GetParametersAnnouncement(ctx, grpc_retry.Disable())
  2170  	if err != nil {
  2171  		return fmt.Errorf("error getting parametersAnnouncementStream: %w", err)
  2172  	}
  2173  
  2174  	err = cmp.SendRepoStream(parametersAnnouncementStream.Context(), appPath, repoPath, parametersAnnouncementStream, env, tarExcludedGlobs)
  2175  	if err != nil {
  2176  		return fmt.Errorf("error sending file to cmp-server: %s", err)
  2177  	}
  2178  
  2179  	announcement, err := parametersAnnouncementStream.CloseAndRecv()
  2180  	if err != nil {
  2181  		return fmt.Errorf("failed to get parameter anouncement: %w", err)
  2182  	}
  2183  
  2184  	res.Plugin = &apiclient.PluginAppSpec{
  2185  		ParametersAnnouncement: announcement.ParameterAnnouncements,
  2186  	}
  2187  	return nil
  2188  }
  2189  
  2190  func (s *Service) GetRevisionMetadata(ctx context.Context, q *apiclient.RepoServerRevisionMetadataRequest) (*v1alpha1.RevisionMetadata, error) {
  2191  	if !(git.IsCommitSHA(q.Revision) || git.IsTruncatedCommitSHA(q.Revision)) {
  2192  		return nil, fmt.Errorf("revision %s must be resolved", q.Revision)
  2193  	}
  2194  	metadata, err := s.cache.GetRevisionMetadata(q.Repo.Repo, q.Revision)
  2195  	if err == nil {
  2196  		// The logic here is that if a signature check on metadata is requested,
  2197  		// but there is none in the cache, we handle as if we have a cache miss
  2198  		// and re-generate the meta data. Otherwise, if there is signature info
  2199  		// in the metadata, but none was requested, we remove it from the data
  2200  		// that we return.
  2201  		if q.CheckSignature && metadata.SignatureInfo == "" {
  2202  			log.Infof("revision metadata cache hit, but need to regenerate due to missing signature info: %s/%s", q.Repo.Repo, q.Revision)
  2203  		} else {
  2204  			log.Infof("revision metadata cache hit: %s/%s", q.Repo.Repo, q.Revision)
  2205  			if !q.CheckSignature {
  2206  				metadata.SignatureInfo = ""
  2207  			}
  2208  			return metadata, nil
  2209  		}
  2210  	} else {
  2211  		if err != cache.ErrCacheMiss {
  2212  			log.Warnf("revision metadata cache error %s/%s: %v", q.Repo.Repo, q.Revision, err)
  2213  		} else {
  2214  			log.Infof("revision metadata cache miss: %s/%s", q.Repo.Repo, q.Revision)
  2215  		}
  2216  	}
  2217  
  2218  	gitClient, _, err := s.newClientResolveRevision(q.Repo, q.Revision)
  2219  	if err != nil {
  2220  		return nil, err
  2221  	}
  2222  
  2223  	s.metricsServer.IncPendingRepoRequest(q.Repo.Repo)
  2224  	defer s.metricsServer.DecPendingRepoRequest(q.Repo.Repo)
  2225  
  2226  	closer, err := s.repoLock.Lock(gitClient.Root(), q.Revision, true, func() (goio.Closer, error) {
  2227  		return s.checkoutRevision(gitClient, q.Revision, s.initConstants.SubmoduleEnabled)
  2228  	})
  2229  
  2230  	if err != nil {
  2231  		return nil, fmt.Errorf("error acquiring repo lock: %w", err)
  2232  	}
  2233  
  2234  	defer io.Close(closer)
  2235  
  2236  	m, err := gitClient.RevisionMetadata(q.Revision)
  2237  	if err != nil {
  2238  		return nil, err
  2239  	}
  2240  
  2241  	// Run gpg verify-commit on the revision
  2242  	signatureInfo := ""
  2243  	if gpg.IsGPGEnabled() && q.CheckSignature {
  2244  		cs, err := gitClient.VerifyCommitSignature(q.Revision)
  2245  		if err != nil {
  2246  			log.Errorf("error verifying signature of commit '%s' in repo '%s': %v", q.Revision, q.Repo.Repo, err)
  2247  			return nil, err
  2248  		}
  2249  
  2250  		if cs != "" {
  2251  			vr := gpg.ParseGitCommitVerification(cs)
  2252  			if vr.Result == gpg.VerifyResultUnknown {
  2253  				signatureInfo = fmt.Sprintf("UNKNOWN signature: %s", vr.Message)
  2254  			} else {
  2255  				signatureInfo = fmt.Sprintf("%s signature from %s key %s", vr.Result, vr.Cipher, gpg.KeyID(vr.KeyID))
  2256  			}
  2257  		} else {
  2258  			signatureInfo = "Revision is not signed."
  2259  		}
  2260  	}
  2261  
  2262  	metadata = &v1alpha1.RevisionMetadata{Author: m.Author, Date: metav1.Time{Time: m.Date}, Tags: m.Tags, Message: m.Message, SignatureInfo: signatureInfo}
  2263  	_ = s.cache.SetRevisionMetadata(q.Repo.Repo, q.Revision, metadata)
  2264  	return metadata, nil
  2265  }
  2266  
  2267  // GetRevisionChartDetails returns the helm chart details of a given version
  2268  func (s *Service) GetRevisionChartDetails(ctx context.Context, q *apiclient.RepoServerRevisionChartDetailsRequest) (*v1alpha1.ChartDetails, error) {
  2269  	details, err := s.cache.GetRevisionChartDetails(q.Repo.Repo, q.Name, q.Revision)
  2270  	if err == nil {
  2271  		log.Infof("revision chart details cache hit: %s/%s/%s", q.Repo.Repo, q.Name, q.Revision)
  2272  		return details, nil
  2273  	} else {
  2274  		if err == cache.ErrCacheMiss {
  2275  			log.Infof("revision metadata cache miss: %s/%s/%s", q.Repo.Repo, q.Name, q.Revision)
  2276  		} else {
  2277  			log.Warnf("revision metadata cache error %s/%s/%s: %v", q.Repo.Repo, q.Name, q.Revision, err)
  2278  		}
  2279  	}
  2280  	helmClient, revision, err := s.newHelmClientResolveRevision(q.Repo, q.Revision, q.Name, true)
  2281  	if err != nil {
  2282  		return nil, fmt.Errorf("helm client error: %v", err)
  2283  	}
  2284  	chartPath, closer, err := helmClient.ExtractChart(q.Name, revision, false, s.initConstants.HelmManifestMaxExtractedSize, s.initConstants.DisableHelmManifestMaxExtractedSize)
  2285  	if err != nil {
  2286  		return nil, fmt.Errorf("error extracting chart: %v", err)
  2287  	}
  2288  	defer io.Close(closer)
  2289  	helmCmd, err := helm.NewCmdWithVersion(chartPath, helm.HelmV3, q.Repo.EnableOCI, q.Repo.Proxy)
  2290  	if err != nil {
  2291  		return nil, fmt.Errorf("error creating helm cmd: %v", err)
  2292  	}
  2293  	defer helmCmd.Close()
  2294  	helmDetails, err := helmCmd.InspectChart()
  2295  	if err != nil {
  2296  		return nil, fmt.Errorf("error inspecting chart: %v", err)
  2297  	}
  2298  	details, err = getChartDetails(helmDetails)
  2299  	if err != nil {
  2300  		return nil, fmt.Errorf("error getting chart details: %v", err)
  2301  	}
  2302  	_ = s.cache.SetRevisionChartDetails(q.Repo.Repo, q.Name, q.Revision, details)
  2303  	return details, nil
  2304  }
  2305  
  2306  func fileParameters(q *apiclient.RepoServerAppDetailsQuery) []v1alpha1.HelmFileParameter {
  2307  	if q.Source.Helm == nil {
  2308  		return nil
  2309  	}
  2310  	return q.Source.Helm.FileParameters
  2311  }
  2312  
  2313  func (s *Service) newClient(repo *v1alpha1.Repository, opts ...git.ClientOpts) (git.Client, error) {
  2314  	repoPath, err := s.gitRepoPaths.GetPath(git.NormalizeGitURL(repo.Repo))
  2315  	if err != nil {
  2316  		return nil, err
  2317  	}
  2318  	opts = append(opts, git.WithEventHandlers(metrics.NewGitClientEventHandlers(s.metricsServer)))
  2319  	return s.newGitClient(repo.Repo, repoPath, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.EnableLFS, repo.Proxy, opts...)
  2320  }
  2321  
  2322  // newClientResolveRevision is a helper to perform the common task of instantiating a git client
  2323  // and resolving a revision to a commit SHA
  2324  func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision string, opts ...git.ClientOpts) (git.Client, string, error) {
  2325  	gitClient, err := s.newClient(repo, opts...)
  2326  	if err != nil {
  2327  		return nil, "", err
  2328  	}
  2329  	commitSHA, err := gitClient.LsRemote(revision)
  2330  	if err != nil {
  2331  		return nil, "", err
  2332  	}
  2333  	return gitClient, commitSHA, nil
  2334  }
  2335  
  2336  func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string, noRevisionCache bool) (helm.Client, string, error) {
  2337  	enableOCI := repo.EnableOCI || helm.IsHelmOciRepo(repo.Repo)
  2338  	helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), enableOCI, repo.Proxy, helm.WithIndexCache(s.cache), helm.WithChartPaths(s.chartPaths))
  2339  	if helm.IsVersion(revision) {
  2340  		return helmClient, revision, nil
  2341  	}
  2342  	constraints, err := semver.NewConstraint(revision)
  2343  	if err != nil {
  2344  		return nil, "", fmt.Errorf("invalid revision '%s': %v", revision, err)
  2345  	}
  2346  
  2347  	if enableOCI {
  2348  		tags, err := helmClient.GetTags(chart, noRevisionCache)
  2349  		if err != nil {
  2350  			return nil, "", fmt.Errorf("unable to get tags: %v", err)
  2351  		}
  2352  
  2353  		version, err := tags.MaxVersion(constraints)
  2354  		if err != nil {
  2355  			return nil, "", fmt.Errorf("no version for constraints: %v", err)
  2356  		}
  2357  		return helmClient, version.String(), nil
  2358  	}
  2359  
  2360  	index, err := helmClient.GetIndex(noRevisionCache, s.initConstants.HelmRegistryMaxIndexSize)
  2361  	if err != nil {
  2362  		return nil, "", err
  2363  	}
  2364  	entries, err := index.GetEntries(chart)
  2365  	if err != nil {
  2366  		return nil, "", err
  2367  	}
  2368  	version, err := entries.MaxVersion(constraints)
  2369  	if err != nil {
  2370  		return nil, "", err
  2371  	}
  2372  	return helmClient, version.String(), nil
  2373  }
  2374  
  2375  // directoryPermissionInitializer ensures the directory has read/write/execute permissions and returns
  2376  // a function that can be used to remove all permissions.
  2377  func directoryPermissionInitializer(rootPath string) goio.Closer {
  2378  	if _, err := os.Stat(rootPath); err == nil {
  2379  		if err := os.Chmod(rootPath, 0700); err != nil {
  2380  			log.Warnf("Failed to restore read/write/execute permissions on %s: %v", rootPath, err)
  2381  		} else {
  2382  			log.Debugf("Successfully restored read/write/execute permissions on %s", rootPath)
  2383  		}
  2384  	}
  2385  
  2386  	return io.NewCloser(func() error {
  2387  		if err := os.Chmod(rootPath, 0000); err != nil {
  2388  			log.Warnf("Failed to remove permissions on %s: %v", rootPath, err)
  2389  		} else {
  2390  			log.Debugf("Successfully removed permissions on %s", rootPath)
  2391  		}
  2392  		return nil
  2393  	})
  2394  }
  2395  
  2396  // checkoutRevision is a convenience function to initialize a repo, fetch, and checkout a revision
  2397  // Returns the 40 character commit SHA after the checkout has been performed
  2398  // nolint:unparam
  2399  func (s *Service) checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) (goio.Closer, error) {
  2400  	closer := s.gitRepoInitializer(gitClient.Root())
  2401  	return closer, checkoutRevision(gitClient, revision, submoduleEnabled)
  2402  }
  2403  
  2404  func checkoutRevision(gitClient git.Client, revision string, submoduleEnabled bool) error {
  2405  	err := gitClient.Init()
  2406  	if err != nil {
  2407  		return status.Errorf(codes.Internal, "Failed to initialize git repo: %v", err)
  2408  	}
  2409  
  2410  	// Fetching with no revision first. Fetching with an explicit version can cause repo bloat. https://github.com/argoproj/argo-cd/issues/8845
  2411  	err = gitClient.Fetch("")
  2412  	if err != nil {
  2413  		return status.Errorf(codes.Internal, "Failed to fetch default: %v", err)
  2414  	}
  2415  
  2416  	err = gitClient.Checkout(revision, submoduleEnabled)
  2417  	if err != nil {
  2418  		// When fetching with no revision, only refs/heads/* and refs/remotes/origin/* are fetched. If checkout fails
  2419  		// for the given revision, try explicitly fetching it.
  2420  		log.Infof("Failed to checkout revision %s: %v", revision, err)
  2421  		log.Infof("Fallback to fetching specific revision %s. ref might not have been in the default refspec fetched.", revision)
  2422  
  2423  		err = gitClient.Fetch(revision)
  2424  		if err != nil {
  2425  			return status.Errorf(codes.Internal, "Failed to checkout revision %s: %v", revision, err)
  2426  		}
  2427  
  2428  		err = gitClient.Checkout("FETCH_HEAD", submoduleEnabled)
  2429  		if err != nil {
  2430  			return status.Errorf(codes.Internal, "Failed to checkout FETCH_HEAD: %v", err)
  2431  		}
  2432  	}
  2433  
  2434  	return err
  2435  }
  2436  
  2437  func (s *Service) GetHelmCharts(ctx context.Context, q *apiclient.HelmChartsRequest) (*apiclient.HelmChartsResponse, error) {
  2438  	index, err := s.newHelmClient(q.Repo.Repo, q.Repo.GetHelmCreds(), q.Repo.EnableOCI, q.Repo.Proxy, helm.WithChartPaths(s.chartPaths)).GetIndex(true, s.initConstants.HelmRegistryMaxIndexSize)
  2439  	if err != nil {
  2440  		return nil, err
  2441  	}
  2442  	res := apiclient.HelmChartsResponse{}
  2443  	for chartName, entries := range index.Entries {
  2444  		chart := apiclient.HelmChart{
  2445  			Name: chartName,
  2446  		}
  2447  		for _, entry := range entries {
  2448  			chart.Versions = append(chart.Versions, entry.Version)
  2449  		}
  2450  		res.Items = append(res.Items, &chart)
  2451  	}
  2452  	return &res, nil
  2453  }
  2454  
  2455  func (s *Service) TestRepository(ctx context.Context, q *apiclient.TestRepositoryRequest) (*apiclient.TestRepositoryResponse, error) {
  2456  	repo := q.Repo
  2457  	// per Type doc, "git" should be assumed if empty or absent
  2458  	if repo.Type == "" {
  2459  		repo.Type = "git"
  2460  	}
  2461  	checks := map[string]func() error{
  2462  		"git": func() error {
  2463  			return git.TestRepo(repo.Repo, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy)
  2464  		},
  2465  		"helm": func() error {
  2466  			if repo.EnableOCI {
  2467  				if !helm.IsHelmOciRepo(repo.Repo) {
  2468  					return errors.New("OCI Helm repository URL should include hostname and port only")
  2469  				}
  2470  				_, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI, repo.Proxy).TestHelmOCI()
  2471  				return err
  2472  			} else {
  2473  				_, err := helm.NewClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI, repo.Proxy).GetIndex(false, s.initConstants.HelmRegistryMaxIndexSize)
  2474  				return err
  2475  			}
  2476  		},
  2477  	}
  2478  	check := checks[repo.Type]
  2479  	apiResp := &apiclient.TestRepositoryResponse{VerifiedRepository: false}
  2480  	err := check()
  2481  	if err != nil {
  2482  		return apiResp, fmt.Errorf("error testing repository connectivity: %w", err)
  2483  	}
  2484  	return apiResp, nil
  2485  }
  2486  
  2487  // ResolveRevision resolves the revision/ambiguousRevision specified in the ResolveRevisionRequest request into a concrete revision.
  2488  func (s *Service) ResolveRevision(ctx context.Context, q *apiclient.ResolveRevisionRequest) (*apiclient.ResolveRevisionResponse, error) {
  2489  
  2490  	repo := q.Repo
  2491  	app := q.App
  2492  	ambiguousRevision := q.AmbiguousRevision
  2493  	var revision string
  2494  	var source = app.Spec.GetSource()
  2495  	if source.IsHelm() {
  2496  		_, revision, err := s.newHelmClientResolveRevision(repo, ambiguousRevision, source.Chart, true)
  2497  
  2498  		if err != nil {
  2499  			return &apiclient.ResolveRevisionResponse{Revision: "", AmbiguousRevision: ""}, err
  2500  		}
  2501  		return &apiclient.ResolveRevisionResponse{
  2502  			Revision:          revision,
  2503  			AmbiguousRevision: fmt.Sprintf("%v (%v)", ambiguousRevision, revision),
  2504  		}, nil
  2505  	} else {
  2506  		gitClient, err := git.NewClient(repo.Repo, repo.GetGitCreds(s.gitCredsStore), repo.IsInsecure(), repo.IsLFSEnabled(), repo.Proxy)
  2507  		if err != nil {
  2508  			return &apiclient.ResolveRevisionResponse{Revision: "", AmbiguousRevision: ""}, err
  2509  		}
  2510  		revision, err = gitClient.LsRemote(ambiguousRevision)
  2511  		if err != nil {
  2512  			return &apiclient.ResolveRevisionResponse{Revision: "", AmbiguousRevision: ""}, err
  2513  		}
  2514  		return &apiclient.ResolveRevisionResponse{
  2515  			Revision:          revision,
  2516  			AmbiguousRevision: fmt.Sprintf("%s (%s)", ambiguousRevision, revision),
  2517  		}, nil
  2518  	}
  2519  }
  2520  
  2521  func (s *Service) GetGitFiles(_ context.Context, request *apiclient.GitFilesRequest) (*apiclient.GitFilesResponse, error) {
  2522  	repo := request.GetRepo()
  2523  	revision := request.GetRevision()
  2524  	gitPath := request.GetPath()
  2525  	noRevisionCache := request.GetNoRevisionCache()
  2526  	enableNewGitFileGlobbing := request.GetNewGitFileGlobbingEnabled()
  2527  	if gitPath == "" {
  2528  		gitPath = "."
  2529  	}
  2530  
  2531  	if repo == nil {
  2532  		return nil, status.Error(codes.InvalidArgument, "must pass a valid repo")
  2533  	}
  2534  
  2535  	gitClient, revision, err := s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !noRevisionCache))
  2536  	if err != nil {
  2537  		return nil, status.Errorf(codes.Internal, "unable to resolve git revision %s: %v", revision, err)
  2538  	}
  2539  
  2540  	// check the cache and return the results if present
  2541  	if cachedFiles, err := s.cache.GetGitFiles(repo.Repo, revision, gitPath); err == nil {
  2542  		log.Debugf("cache hit for repo: %s revision: %s pattern: %s", repo.Repo, revision, gitPath)
  2543  		return &apiclient.GitFilesResponse{
  2544  			Map: cachedFiles,
  2545  		}, nil
  2546  	}
  2547  
  2548  	s.metricsServer.IncPendingRepoRequest(repo.Repo)
  2549  	defer s.metricsServer.DecPendingRepoRequest(repo.Repo)
  2550  
  2551  	// cache miss, generate the results
  2552  	closer, err := s.repoLock.Lock(gitClient.Root(), revision, true, func() (goio.Closer, error) {
  2553  		return s.checkoutRevision(gitClient, revision, request.GetSubmoduleEnabled())
  2554  	})
  2555  	if err != nil {
  2556  		return nil, status.Errorf(codes.Internal, "unable to checkout git repo %s with revision %s pattern %s: %v", repo.Repo, revision, gitPath, err)
  2557  	}
  2558  	defer io.Close(closer)
  2559  
  2560  	gitFiles, err := gitClient.LsFiles(gitPath, enableNewGitFileGlobbing)
  2561  	if err != nil {
  2562  		return nil, status.Errorf(codes.Internal, "unable to list files. repo %s with revision %s pattern %s: %v", repo.Repo, revision, gitPath, err)
  2563  	}
  2564  	log.Debugf("listed %d git files from %s under %s", len(gitFiles), repo.Repo, gitPath)
  2565  
  2566  	res := make(map[string][]byte)
  2567  	for _, filePath := range gitFiles {
  2568  		fileContents, err := os.ReadFile(filepath.Join(gitClient.Root(), filePath))
  2569  		if err != nil {
  2570  			return nil, status.Errorf(codes.Internal, "unable to read files. repo %s with revision %s pattern %s: %v", repo.Repo, revision, gitPath, err)
  2571  		}
  2572  		res[filePath] = fileContents
  2573  	}
  2574  
  2575  	err = s.cache.SetGitFiles(repo.Repo, revision, gitPath, res)
  2576  	if err != nil {
  2577  		log.Warnf("error caching git files for repo %s with revision %s pattern %s: %v", repo.Repo, revision, gitPath, err)
  2578  	}
  2579  
  2580  	return &apiclient.GitFilesResponse{
  2581  		Map: res,
  2582  	}, nil
  2583  }
  2584  
  2585  func (s *Service) GetGitDirectories(_ context.Context, request *apiclient.GitDirectoriesRequest) (*apiclient.GitDirectoriesResponse, error) {
  2586  	repo := request.GetRepo()
  2587  	revision := request.GetRevision()
  2588  	noRevisionCache := request.GetNoRevisionCache()
  2589  	if repo == nil {
  2590  		return nil, status.Error(codes.InvalidArgument, "must pass a valid repo")
  2591  	}
  2592  
  2593  	gitClient, revision, err := s.newClientResolveRevision(repo, revision, git.WithCache(s.cache, !noRevisionCache))
  2594  	if err != nil {
  2595  		return nil, status.Errorf(codes.Internal, "unable to resolve git revision %s: %v", revision, err)
  2596  	}
  2597  
  2598  	// check the cache and return the results if present
  2599  	if cachedPaths, err := s.cache.GetGitDirectories(repo.Repo, revision); err == nil {
  2600  		log.Debugf("cache hit for repo: %s revision: %s", repo.Repo, revision)
  2601  		return &apiclient.GitDirectoriesResponse{
  2602  			Paths: cachedPaths,
  2603  		}, nil
  2604  	}
  2605  
  2606  	s.metricsServer.IncPendingRepoRequest(repo.Repo)
  2607  	defer s.metricsServer.DecPendingRepoRequest(repo.Repo)
  2608  
  2609  	// cache miss, generate the results
  2610  	closer, err := s.repoLock.Lock(gitClient.Root(), revision, true, func() (goio.Closer, error) {
  2611  		return s.checkoutRevision(gitClient, revision, request.GetSubmoduleEnabled())
  2612  	})
  2613  	if err != nil {
  2614  		return nil, status.Errorf(codes.Internal, "unable to checkout git repo %s with revision %s: %v", repo.Repo, revision, err)
  2615  	}
  2616  	defer io.Close(closer)
  2617  
  2618  	repoRoot := gitClient.Root()
  2619  	var paths []string
  2620  	if err := filepath.WalkDir(repoRoot, func(path string, entry fs.DirEntry, fnErr error) error {
  2621  		if fnErr != nil {
  2622  			return fmt.Errorf("error walking the file tree: %w", fnErr)
  2623  		}
  2624  		if !entry.IsDir() { // Skip files: directories only
  2625  			return nil
  2626  		}
  2627  
  2628  		fname := entry.Name()
  2629  		if strings.HasPrefix(fname, ".") { // Skip all folders starts with "."
  2630  			return filepath.SkipDir
  2631  		}
  2632  
  2633  		relativePath, err := filepath.Rel(repoRoot, path)
  2634  		if err != nil {
  2635  			return fmt.Errorf("error constructing relative repo path: %w", err)
  2636  		}
  2637  
  2638  		if relativePath == "." { // Exclude '.' from results
  2639  			return nil
  2640  		}
  2641  
  2642  		paths = append(paths, relativePath)
  2643  
  2644  		return nil
  2645  	}); err != nil {
  2646  		return nil, err
  2647  	}
  2648  
  2649  	log.Debugf("found %d git paths from %s", len(paths), repo.Repo)
  2650  	err = s.cache.SetGitDirectories(repo.Repo, revision, paths)
  2651  	if err != nil {
  2652  		log.Warnf("error caching git directories for repo %s with revision %s: %v", repo.Repo, revision, err)
  2653  	}
  2654  
  2655  	return &apiclient.GitDirectoriesResponse{
  2656  		Paths: paths,
  2657  	}, nil
  2658  }