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