github.com/argoproj/argo-cd@v1.8.7/reposerver/repository/repository.go (about)

     1  package repository
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"io/ioutil"
    10  	"net/url"
    11  	"os"
    12  	"os/exec"
    13  	"path"
    14  	"path/filepath"
    15  	"regexp"
    16  	"strings"
    17  	"time"
    18  
    19  	"github.com/Masterminds/semver"
    20  	"github.com/TomOnTime/utfutil"
    21  	"github.com/argoproj/gitops-engine/pkg/utils/kube"
    22  	textutils "github.com/argoproj/gitops-engine/pkg/utils/text"
    23  	"github.com/argoproj/pkg/sync"
    24  	jsonpatch "github.com/evanphx/json-patch"
    25  	"github.com/ghodss/yaml"
    26  	"github.com/google/go-jsonnet"
    27  	log "github.com/sirupsen/logrus"
    28  	"golang.org/x/sync/semaphore"
    29  	"google.golang.org/grpc/codes"
    30  	"google.golang.org/grpc/status"
    31  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    32  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    33  	"k8s.io/apimachinery/pkg/runtime"
    34  
    35  	"github.com/argoproj/argo-cd/common"
    36  	"github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    37  	"github.com/argoproj/argo-cd/reposerver/apiclient"
    38  	"github.com/argoproj/argo-cd/reposerver/cache"
    39  	reposervercache "github.com/argoproj/argo-cd/reposerver/cache"
    40  	"github.com/argoproj/argo-cd/reposerver/metrics"
    41  	"github.com/argoproj/argo-cd/util/app/discovery"
    42  	argopath "github.com/argoproj/argo-cd/util/app/path"
    43  	executil "github.com/argoproj/argo-cd/util/exec"
    44  	"github.com/argoproj/argo-cd/util/git"
    45  	"github.com/argoproj/argo-cd/util/glob"
    46  	"github.com/argoproj/argo-cd/util/gpg"
    47  	"github.com/argoproj/argo-cd/util/helm"
    48  	"github.com/argoproj/argo-cd/util/io"
    49  	"github.com/argoproj/argo-cd/util/ksonnet"
    50  	argokube "github.com/argoproj/argo-cd/util/kube"
    51  	"github.com/argoproj/argo-cd/util/kustomize"
    52  	"github.com/argoproj/argo-cd/util/security"
    53  	"github.com/argoproj/argo-cd/util/text"
    54  )
    55  
    56  const (
    57  	cachedManifestGenerationPrefix = "Manifest generation error (cached)"
    58  	helmDepUpMarkerFile            = ".argocd-helm-dep-up"
    59  	allowConcurrencyFile           = ".argocd-allow-concurrency"
    60  )
    61  
    62  // Service implements ManifestService interface
    63  type Service struct {
    64  	repoLock                  *repositoryLock
    65  	cache                     *reposervercache.Cache
    66  	parallelismLimitSemaphore *semaphore.Weighted
    67  	metricsServer             *metrics.MetricsServer
    68  	newGitClient              func(rawRepoURL string, creds git.Creds, insecure bool, enableLfs bool) (git.Client, error)
    69  	newHelmClient             func(repoURL string, creds helm.Creds, enableOci bool) helm.Client
    70  	initConstants             RepoServerInitConstants
    71  	// now is usually just time.Now, but may be replaced by unit tests for testing purposes
    72  	now func() time.Time
    73  }
    74  
    75  type RepoServerInitConstants struct {
    76  	ParallelismLimit                             int64
    77  	PauseGenerationAfterFailedGenerationAttempts int
    78  	PauseGenerationOnFailureForMinutes           int
    79  	PauseGenerationOnFailureForRequests          int
    80  }
    81  
    82  // NewService returns a new instance of the Manifest service
    83  func NewService(metricsServer *metrics.MetricsServer, cache *reposervercache.Cache, initConstants RepoServerInitConstants) *Service {
    84  	var parallelismLimitSemaphore *semaphore.Weighted
    85  	if initConstants.ParallelismLimit > 0 {
    86  		parallelismLimitSemaphore = semaphore.NewWeighted(initConstants.ParallelismLimit)
    87  	}
    88  	repoLock := NewRepositoryLock()
    89  	return &Service{
    90  		parallelismLimitSemaphore: parallelismLimitSemaphore,
    91  		repoLock:                  repoLock,
    92  		cache:                     cache,
    93  		metricsServer:             metricsServer,
    94  		newGitClient:              git.NewClient,
    95  		newHelmClient: func(repoURL string, creds helm.Creds, enableOci bool) helm.Client {
    96  			return helm.NewClientWithLock(repoURL, creds, sync.NewKeyLock(), enableOci)
    97  		},
    98  		initConstants: initConstants,
    99  		now:           time.Now,
   100  	}
   101  }
   102  
   103  // List a subset of the refs (currently, branches and tags) of a git repo
   104  func (s *Service) ListRefs(ctx context.Context, q *apiclient.ListRefsRequest) (*apiclient.Refs, error) {
   105  	gitClient, err := s.newClient(q.Repo)
   106  	if err != nil {
   107  		return nil, err
   108  	}
   109  
   110  	s.metricsServer.IncPendingRepoRequest(q.Repo.Repo)
   111  	defer s.metricsServer.DecPendingRepoRequest(q.Repo.Repo)
   112  
   113  	refs, err := gitClient.LsRefs()
   114  	if err != nil {
   115  		return nil, err
   116  	}
   117  
   118  	res := apiclient.Refs{
   119  		Branches: refs.Branches,
   120  		Tags:     refs.Tags,
   121  	}
   122  
   123  	return &res, nil
   124  }
   125  
   126  // ListApps lists the contents of a GitHub repo
   127  func (s *Service) ListApps(ctx context.Context, q *apiclient.ListAppsRequest) (*apiclient.AppList, error) {
   128  	gitClient, commitSHA, err := s.newClientResolveRevision(q.Repo, q.Revision)
   129  	if err != nil {
   130  		return nil, err
   131  	}
   132  	if apps, err := s.cache.ListApps(q.Repo.Repo, commitSHA); err == nil {
   133  		log.Infof("cache hit: %s/%s", q.Repo.Repo, q.Revision)
   134  		return &apiclient.AppList{Apps: apps}, nil
   135  	}
   136  
   137  	s.metricsServer.IncPendingRepoRequest(q.Repo.Repo)
   138  	defer s.metricsServer.DecPendingRepoRequest(q.Repo.Repo)
   139  
   140  	closer, err := s.repoLock.Lock(gitClient.Root(), commitSHA, true, func() error {
   141  		return checkoutRevision(gitClient, commitSHA)
   142  	})
   143  
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  
   148  	defer io.Close(closer)
   149  	apps, err := discovery.Discover(gitClient.Root())
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  	err = s.cache.SetApps(q.Repo.Repo, commitSHA, apps)
   154  	if err != nil {
   155  		log.Warnf("cache set error %s/%s: %v", q.Repo.Repo, commitSHA, err)
   156  	}
   157  	res := apiclient.AppList{Apps: apps}
   158  	return &res, nil
   159  }
   160  
   161  type operationSettings struct {
   162  	sem             *semaphore.Weighted
   163  	noCache         bool
   164  	allowConcurrent bool
   165  }
   166  
   167  // operationContext contains request values which are generated by runRepoOperation (on demand) by a call to the
   168  // provided operationContextSrc function.
   169  type operationContext struct {
   170  
   171  	// application path or helm chart path
   172  	appPath string
   173  
   174  	// output of 'git verify-(tag/commit)', if signature verifiction is enabled (otherwise "")
   175  	verificationResult string
   176  }
   177  
   178  // The 'operation' function parameter of 'runRepoOperation' may call this function to retrieve
   179  // the appPath or GPG verificationResult.
   180  // Failure to generate either of these values will return an error which may be cached by
   181  // the calling function (for example, 'runManifestGen')
   182  type operationContextSrc = func() (*operationContext, error)
   183  
   184  // runRepoOperation downloads either git folder or helm chart and executes specified operation
   185  // - Returns a value from the cache if present (by calling getCached(...)); if no value is present, the
   186  // provide operation(...) is called. The specific return type of this function is determined by the
   187  // calling function, via the provided  getCached(...) and operation(...) function.
   188  func (s *Service) runRepoOperation(
   189  	ctx context.Context,
   190  	revision string,
   191  	repo *v1alpha1.Repository,
   192  	source *v1alpha1.ApplicationSource,
   193  	verifyCommit bool,
   194  	getCached func(cacheKey string, firstInvocation bool) (bool, interface{}, error),
   195  	operation func(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc) (interface{}, error),
   196  	settings operationSettings) (interface{}, error) {
   197  
   198  	var gitClient git.Client
   199  	var helmClient helm.Client
   200  	var err error
   201  	revision = textutils.FirstNonEmpty(revision, source.TargetRevision)
   202  	if source.IsHelm() {
   203  		helmClient, revision, err = s.newHelmClientResolveRevision(repo, revision, source.Chart)
   204  		if err != nil {
   205  			return nil, err
   206  		}
   207  	} else {
   208  		gitClient, revision, err = s.newClientResolveRevision(repo, revision)
   209  		if err != nil {
   210  			return nil, err
   211  		}
   212  	}
   213  
   214  	if !settings.noCache {
   215  		result, obj, err := getCached(revision, true)
   216  		if result {
   217  			return obj, err
   218  		}
   219  	}
   220  
   221  	s.metricsServer.IncPendingRepoRequest(repo.Repo)
   222  	defer s.metricsServer.DecPendingRepoRequest(repo.Repo)
   223  
   224  	if settings.sem != nil {
   225  		err = settings.sem.Acquire(ctx, 1)
   226  		if err != nil {
   227  			return nil, err
   228  		}
   229  		defer settings.sem.Release(1)
   230  	}
   231  
   232  	if source.IsHelm() {
   233  		version, err := semver.NewVersion(revision)
   234  		if err != nil {
   235  			return nil, err
   236  		}
   237  		if settings.noCache {
   238  			err = helmClient.CleanChartCache(source.Chart, version)
   239  			if err != nil {
   240  				return nil, err
   241  			}
   242  		}
   243  		chartPath, closer, err := helmClient.ExtractChart(source.Chart, version)
   244  		if err != nil {
   245  			return nil, err
   246  		}
   247  		defer io.Close(closer)
   248  		return operation(chartPath, revision, revision, func() (*operationContext, error) {
   249  			return &operationContext{chartPath, ""}, nil
   250  		})
   251  	} else {
   252  		closer, err := s.repoLock.Lock(gitClient.Root(), revision, settings.allowConcurrent, func() error {
   253  			return checkoutRevision(gitClient, revision)
   254  		})
   255  
   256  		if err != nil {
   257  			return nil, err
   258  		}
   259  
   260  		defer io.Close(closer)
   261  
   262  		commitSHA, err := gitClient.CommitSHA()
   263  		if err != nil {
   264  			return nil, err
   265  		}
   266  
   267  		// double-check locking
   268  		if !settings.noCache {
   269  			result, obj, err := getCached(revision, false)
   270  			if result {
   271  				return obj, err
   272  			}
   273  		}
   274  		// Here commitSHA refers to the SHA of the actual commit, whereas revision refers to the branch/tag name etc
   275  		// We use the commitSHA to generate manifests and store them in cache, and revision to retrieve them from cache
   276  		return operation(gitClient.Root(), commitSHA, revision, func() (*operationContext, error) {
   277  			var signature string
   278  			if verifyCommit {
   279  				signature, err = gitClient.VerifyCommitSignature(revision)
   280  				if err != nil {
   281  					return nil, err
   282  				}
   283  			}
   284  			appPath, err := argopath.Path(gitClient.Root(), source.Path)
   285  			if err != nil {
   286  				return nil, err
   287  			}
   288  			return &operationContext{appPath, signature}, nil
   289  		})
   290  	}
   291  }
   292  
   293  func (s *Service) GenerateManifest(ctx context.Context, q *apiclient.ManifestRequest) (*apiclient.ManifestResponse, error) {
   294  	resultUncast, err := s.runRepoOperation(ctx, q.Revision, q.Repo, q.ApplicationSource, q.VerifySignature,
   295  		func(cacheKey string, firstInvocation bool) (bool, interface{}, error) {
   296  			return s.getManifestCacheEntry(cacheKey, q, firstInvocation)
   297  		}, func(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc) (interface{}, error) {
   298  			return s.runManifestGen(repoRoot, commitSHA, cacheKey, ctxSrc, q)
   299  		}, operationSettings{sem: s.parallelismLimitSemaphore, noCache: q.NoCache, allowConcurrent: q.ApplicationSource.AllowsConcurrentProcessing()})
   300  	result, ok := resultUncast.(*apiclient.ManifestResponse)
   301  	if result != nil && !ok {
   302  		return nil, errors.New("unexpected result type")
   303  	}
   304  
   305  	return result, err
   306  }
   307  
   308  // runManifestGenwill be called by runRepoOperation if:
   309  // - the cache does not contain a value for this key
   310  // - or, the cache does contain a value for this key, but it is an expired manifest generation entry
   311  // - or, NoCache is true
   312  // Returns a ManifestResponse, or an error, but not both
   313  func (s *Service) runManifestGen(repoRoot, commitSHA, cacheKey string, ctxSrc operationContextSrc, q *apiclient.ManifestRequest) (interface{}, error) {
   314  	var manifestGenResult *apiclient.ManifestResponse
   315  	ctx, err := ctxSrc()
   316  	if err == nil {
   317  		manifestGenResult, err = GenerateManifests(ctx.appPath, repoRoot, commitSHA, q, false)
   318  	}
   319  	if err != nil {
   320  
   321  		// If manifest generation error caching is enabled
   322  		if s.initConstants.PauseGenerationAfterFailedGenerationAttempts > 0 {
   323  
   324  			// Retrieve a new copy (if available) of the cached response: this ensures we are updating the latest copy of the cache,
   325  			// rather than a copy of the cache that occurred before (a potentially lengthy) manifest generation.
   326  			innerRes := &cache.CachedManifestResponse{}
   327  			cacheErr := s.cache.GetManifests(cacheKey, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue, innerRes)
   328  			if cacheErr != nil && cacheErr != reposervercache.ErrCacheMiss {
   329  				log.Warnf("manifest cache set error %s: %v", q.ApplicationSource.String(), cacheErr)
   330  				return nil, cacheErr
   331  			}
   332  
   333  			// If this is the first error we have seen, store the time (we only use the first failure, as this
   334  			// value is used for PauseGenerationOnFailureForMinutes)
   335  			if innerRes.FirstFailureTimestamp == 0 {
   336  				innerRes.FirstFailureTimestamp = s.now().Unix()
   337  			}
   338  
   339  			// Update the cache to include failure information
   340  			innerRes.NumberOfConsecutiveFailures++
   341  			innerRes.MostRecentError = err.Error()
   342  			cacheErr = s.cache.SetManifests(cacheKey, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue, innerRes)
   343  			if cacheErr != nil {
   344  				log.Warnf("manifest cache set error %s: %v", q.ApplicationSource.String(), cacheErr)
   345  				return nil, cacheErr
   346  			}
   347  
   348  		}
   349  		return nil, err
   350  	}
   351  	// Otherwise, no error occurred, so ensure the manifest generation error data in the cache entry is reset before we cache the value
   352  	manifestGenCacheEntry := cache.CachedManifestResponse{
   353  		ManifestResponse:                manifestGenResult,
   354  		NumberOfCachedResponsesReturned: 0,
   355  		NumberOfConsecutiveFailures:     0,
   356  		FirstFailureTimestamp:           0,
   357  		MostRecentError:                 "",
   358  	}
   359  	manifestGenResult.Revision = commitSHA
   360  	manifestGenResult.VerifyResult = ctx.verificationResult
   361  	err = s.cache.SetManifests(cacheKey, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue, &manifestGenCacheEntry)
   362  	if err != nil {
   363  		log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), cacheKey, err)
   364  	}
   365  	return manifestGenCacheEntry.ManifestResponse, nil
   366  }
   367  
   368  // getManifestCacheEntry returns false if the 'generate manifests' operation should be run by runRepoOperation, eg:
   369  // - If the cache result is empty for the requested key
   370  // - If the cache is not empty, but the cached value is a manifest generation error AND we have not yet met the failure threshold (eg res.NumberOfConsecutiveFailures > 0 && res.NumberOfConsecutiveFailures <  s.initConstants.PauseGenerationAfterFailedGenerationAttempts)
   371  // - If the cache is not empty, but the cache value is an error AND that generation error has expired
   372  // and returns true otherwise.
   373  // 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)
   374  func (s *Service) getManifestCacheEntry(cacheKey string, q *apiclient.ManifestRequest, firstInvocation bool) (bool, interface{}, error) {
   375  	res := cache.CachedManifestResponse{}
   376  	err := s.cache.GetManifests(cacheKey, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue, &res)
   377  	if err == nil {
   378  
   379  		// The cache contains an existing value
   380  
   381  		// If caching of manifest generation errors is enabled, and res is a cached manifest generation error...
   382  		if s.initConstants.PauseGenerationAfterFailedGenerationAttempts > 0 && res.FirstFailureTimestamp > 0 {
   383  
   384  			// If we are already in the 'manifest generation caching' state, due to too many consecutive failures...
   385  			if res.NumberOfConsecutiveFailures >= s.initConstants.PauseGenerationAfterFailedGenerationAttempts {
   386  
   387  				// Check if enough time has passed to try generation again (eg to exit the 'manifest generation caching' state)
   388  				if s.initConstants.PauseGenerationOnFailureForMinutes > 0 {
   389  
   390  					elapsedTimeInMinutes := int((s.now().Unix() - res.FirstFailureTimestamp) / 60)
   391  
   392  					// After X minutes, reset the cache and retry the operation (eg perhaps the error is ephemeral and has passed)
   393  					if elapsedTimeInMinutes >= s.initConstants.PauseGenerationOnFailureForMinutes {
   394  						// We can now try again, so reset the cache state and run the operation below
   395  						err = s.cache.DeleteManifests(cacheKey, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue)
   396  						if err != nil {
   397  							log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), cacheKey, err)
   398  						}
   399  						log.Infof("manifest error cache hit and reset: %s/%s", q.ApplicationSource.String(), cacheKey)
   400  						return false, nil, nil
   401  					}
   402  				}
   403  
   404  				// Check if enough cached responses have been returned to try generation again (eg to exit the 'manifest generation caching' state)
   405  				if s.initConstants.PauseGenerationOnFailureForRequests > 0 && res.NumberOfCachedResponsesReturned > 0 {
   406  
   407  					if res.NumberOfCachedResponsesReturned >= s.initConstants.PauseGenerationOnFailureForRequests {
   408  						// We can now try again, so reset the error cache state and run the operation below
   409  						err = s.cache.DeleteManifests(cacheKey, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue)
   410  						if err != nil {
   411  							log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), cacheKey, err)
   412  						}
   413  						log.Infof("manifest error cache hit and reset: %s/%s", q.ApplicationSource.String(), cacheKey)
   414  						return false, nil, nil
   415  					}
   416  				}
   417  
   418  				// Otherwise, manifest generation is still paused
   419  				log.Infof("manifest error cache hit: %s/%s", q.ApplicationSource.String(), cacheKey)
   420  
   421  				cachedErrorResponse := fmt.Errorf(cachedManifestGenerationPrefix+": %s", res.MostRecentError)
   422  
   423  				if firstInvocation {
   424  					// Increment the number of returned cached responses and push that new value to the cache
   425  					// (if we have not already done so previously in this function)
   426  					res.NumberOfCachedResponsesReturned++
   427  					err = s.cache.SetManifests(cacheKey, q.ApplicationSource, q.Namespace, q.AppLabelKey, q.AppLabelValue, &res)
   428  					if err != nil {
   429  						log.Warnf("manifest cache set error %s/%s: %v", q.ApplicationSource.String(), cacheKey, err)
   430  					}
   431  				}
   432  
   433  				return true, nil, cachedErrorResponse
   434  
   435  			}
   436  
   437  			// Otherwise we are not yet in the manifest generation error state, and not enough consecutive errors have
   438  			// yet occurred to put us in that state.
   439  			log.Infof("manifest error cache miss: %s/%s", q.ApplicationSource.String(), cacheKey)
   440  			return false, res.ManifestResponse, nil
   441  		}
   442  
   443  		log.Infof("manifest cache hit: %s/%s", q.ApplicationSource.String(), cacheKey)
   444  		return true, res.ManifestResponse, nil
   445  	}
   446  
   447  	if err != reposervercache.ErrCacheMiss {
   448  		log.Warnf("manifest cache error %s: %v", q.ApplicationSource.String(), err)
   449  	} else {
   450  		log.Infof("manifest cache miss: %s/%s", q.ApplicationSource.String(), cacheKey)
   451  	}
   452  
   453  	return false, nil, nil
   454  }
   455  
   456  func getHelmRepos(repositories []*v1alpha1.Repository) []helm.HelmRepository {
   457  	repos := make([]helm.HelmRepository, 0)
   458  	for _, repo := range repositories {
   459  		repos = append(repos, helm.HelmRepository{Name: repo.Name, Repo: repo.Repo, Creds: repo.GetHelmCreds()})
   460  	}
   461  	return repos
   462  }
   463  
   464  func isConcurrencyAllowed(appPath string) bool {
   465  	if _, err := os.Stat(path.Join(appPath, allowConcurrencyFile)); err == nil {
   466  		return true
   467  	}
   468  	return false
   469  }
   470  
   471  var manifestGenerateLock = sync.NewKeyLock()
   472  
   473  // runHelmBuild executes `helm dependency build` in a given path and ensures that it is executed only once
   474  // if multiple threads are trying to run it.
   475  // Multiple goroutines might process same helm app in one repo concurrently when repo server process multiple
   476  // manifest generation requests of the same commit.
   477  func runHelmBuild(appPath string, h helm.Helm) error {
   478  	manifestGenerateLock.Lock(appPath)
   479  	defer manifestGenerateLock.Unlock(appPath)
   480  
   481  	// the `helm dependency build` is potentially time consuming 1~2 seconds
   482  	// marker file is used to check if command already run to avoid running it again unnecessary
   483  	// file is removed when repository re-initialized (e.g. when another commit is processed)
   484  	markerFile := path.Join(appPath, helmDepUpMarkerFile)
   485  	_, err := os.Stat(markerFile)
   486  	if err == nil {
   487  		return nil
   488  	} else if !os.IsNotExist(err) {
   489  		return err
   490  	}
   491  
   492  	err = h.DependencyBuild()
   493  	if err != nil {
   494  		return err
   495  	}
   496  	return ioutil.WriteFile(markerFile, []byte("marker"), 0644)
   497  }
   498  
   499  func helmTemplate(appPath string, repoRoot string, env *v1alpha1.Env, q *apiclient.ManifestRequest, isLocal bool) ([]*unstructured.Unstructured, error) {
   500  	concurrencyAllowed := isConcurrencyAllowed(appPath)
   501  	if !concurrencyAllowed {
   502  		manifestGenerateLock.Lock(appPath)
   503  		defer manifestGenerateLock.Unlock(appPath)
   504  	}
   505  
   506  	templateOpts := &helm.TemplateOpts{
   507  		Name:        q.AppLabelValue,
   508  		Namespace:   q.Namespace,
   509  		KubeVersion: text.SemVer(q.KubeVersion),
   510  		APIVersions: q.ApiVersions,
   511  		Set:         map[string]string{},
   512  		SetString:   map[string]string{},
   513  		SetFile:     map[string]string{},
   514  	}
   515  
   516  	appHelm := q.ApplicationSource.Helm
   517  	var version string
   518  	if appHelm != nil {
   519  		if appHelm.Version != "" {
   520  			version = appHelm.Version
   521  		}
   522  		if appHelm.ReleaseName != "" {
   523  			templateOpts.Name = appHelm.ReleaseName
   524  		}
   525  
   526  		for _, val := range appHelm.ValueFiles {
   527  			// If val is not a URL, run it against the directory enforcer. If it is a URL, use it without checking
   528  			if _, err := url.ParseRequestURI(val); err != nil {
   529  
   530  				// Ensure that the repo root provided is absolute
   531  				absRepoPath, err := filepath.Abs(repoRoot)
   532  				if err != nil {
   533  					return nil, err
   534  				}
   535  
   536  				// If the path to the file is relative, join it with the current working directory (appPath)
   537  				path := val
   538  				if !filepath.IsAbs(path) {
   539  					absWorkDir, err := filepath.Abs(appPath)
   540  					if err != nil {
   541  						return nil, err
   542  					}
   543  					path = filepath.Join(absWorkDir, path)
   544  				}
   545  
   546  				_, err = security.EnforceToCurrentRoot(absRepoPath, path)
   547  				if err != nil {
   548  					return nil, err
   549  				}
   550  			}
   551  			templateOpts.Values = append(templateOpts.Values, val)
   552  		}
   553  
   554  		if appHelm.Values != "" {
   555  			file, err := ioutil.TempFile("", "values-*.yaml")
   556  			if err != nil {
   557  				return nil, err
   558  			}
   559  			p := file.Name()
   560  			defer func() { _ = os.RemoveAll(p) }()
   561  			err = ioutil.WriteFile(p, []byte(appHelm.Values), 0644)
   562  			if err != nil {
   563  				return nil, err
   564  			}
   565  			templateOpts.Values = append(templateOpts.Values, p)
   566  		}
   567  
   568  		for _, p := range appHelm.Parameters {
   569  			if p.ForceString {
   570  				templateOpts.SetString[p.Name] = p.Value
   571  			} else {
   572  				templateOpts.Set[p.Name] = p.Value
   573  			}
   574  		}
   575  		for _, p := range appHelm.FileParameters {
   576  			templateOpts.SetFile[p.Name] = p.Path
   577  		}
   578  	}
   579  	if templateOpts.Name == "" {
   580  		templateOpts.Name = q.AppLabelValue
   581  	}
   582  	for i, j := range templateOpts.Set {
   583  		templateOpts.Set[i] = env.Envsubst(j)
   584  	}
   585  	for i, j := range templateOpts.SetString {
   586  		templateOpts.SetString[i] = env.Envsubst(j)
   587  	}
   588  	for i, j := range templateOpts.SetFile {
   589  		templateOpts.SetFile[i] = env.Envsubst(j)
   590  	}
   591  	h, err := helm.NewHelmApp(appPath, getHelmRepos(q.Repos), isLocal, version)
   592  
   593  	if err != nil {
   594  		return nil, err
   595  	}
   596  	defer h.Dispose()
   597  	err = h.Init()
   598  	if err != nil {
   599  		return nil, err
   600  	}
   601  
   602  	out, err := h.Template(templateOpts)
   603  	if err != nil {
   604  		if !helm.IsMissingDependencyErr(err) {
   605  			return nil, err
   606  		}
   607  
   608  		if concurrencyAllowed {
   609  			err = runHelmBuild(appPath, h)
   610  		} else {
   611  			err = h.DependencyBuild()
   612  		}
   613  
   614  		if err != nil {
   615  			return nil, err
   616  		}
   617  
   618  		out, err = h.Template(templateOpts)
   619  		if err != nil {
   620  			return nil, err
   621  		}
   622  	}
   623  	return kube.SplitYAML([]byte(out))
   624  }
   625  
   626  // GenerateManifests generates manifests from a path
   627  func GenerateManifests(appPath, repoRoot, revision string, q *apiclient.ManifestRequest, isLocal bool) (*apiclient.ManifestResponse, error) {
   628  	var targetObjs []*unstructured.Unstructured
   629  	var dest *v1alpha1.ApplicationDestination
   630  
   631  	appSourceType, err := GetAppSourceType(q.ApplicationSource, appPath)
   632  	if err != nil {
   633  		return nil, err
   634  	}
   635  	repoURL := ""
   636  	if q.Repo != nil {
   637  		repoURL = q.Repo.Repo
   638  	}
   639  	env := newEnv(q, revision)
   640  
   641  	switch appSourceType {
   642  	case v1alpha1.ApplicationSourceTypeKsonnet:
   643  		targetObjs, dest, err = ksShow(q.AppLabelKey, appPath, q.ApplicationSource.Ksonnet)
   644  	case v1alpha1.ApplicationSourceTypeHelm:
   645  		targetObjs, err = helmTemplate(appPath, repoRoot, env, q, isLocal)
   646  	case v1alpha1.ApplicationSourceTypeKustomize:
   647  		kustomizeBinary := ""
   648  		if q.KustomizeOptions != nil {
   649  			kustomizeBinary = q.KustomizeOptions.BinaryPath
   650  		}
   651  		k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(), repoURL, kustomizeBinary)
   652  		targetObjs, _, err = k.Build(q.ApplicationSource.Kustomize, q.KustomizeOptions)
   653  	case v1alpha1.ApplicationSourceTypePlugin:
   654  		targetObjs, err = runConfigManagementPlugin(appPath, env, q, q.Repo.GetGitCreds())
   655  	case v1alpha1.ApplicationSourceTypeDirectory:
   656  		var directory *v1alpha1.ApplicationSourceDirectory
   657  		if directory = q.ApplicationSource.Directory; directory == nil {
   658  			directory = &v1alpha1.ApplicationSourceDirectory{}
   659  		}
   660  		targetObjs, err = findManifests(appPath, repoRoot, env, *directory)
   661  	}
   662  	if err != nil {
   663  		return nil, err
   664  	}
   665  
   666  	manifests := make([]string, 0)
   667  	for _, obj := range targetObjs {
   668  		var targets []*unstructured.Unstructured
   669  		if obj.IsList() {
   670  			err = obj.EachListItem(func(object runtime.Object) error {
   671  				unstructuredObj, ok := object.(*unstructured.Unstructured)
   672  				if ok {
   673  					targets = append(targets, unstructuredObj)
   674  					return nil
   675  				}
   676  				return fmt.Errorf("resource list item has unexpected type")
   677  			})
   678  			if err != nil {
   679  				return nil, err
   680  			}
   681  		} else if isNullList(obj) {
   682  			// noop
   683  		} else {
   684  			targets = []*unstructured.Unstructured{obj}
   685  		}
   686  
   687  		for _, target := range targets {
   688  			if q.AppLabelKey != "" && q.AppLabelValue != "" && !kube.IsCRD(target) {
   689  				err = argokube.SetAppInstanceLabel(target, q.AppLabelKey, q.AppLabelValue)
   690  				if err != nil {
   691  					return nil, err
   692  				}
   693  			}
   694  			manifestStr, err := json.Marshal(target.Object)
   695  			if err != nil {
   696  				return nil, err
   697  			}
   698  			manifests = append(manifests, string(manifestStr))
   699  		}
   700  	}
   701  
   702  	res := apiclient.ManifestResponse{
   703  		Manifests:  manifests,
   704  		SourceType: string(appSourceType),
   705  	}
   706  	if dest != nil {
   707  		res.Namespace = dest.Namespace
   708  		res.Server = dest.Server
   709  	}
   710  	return &res, nil
   711  }
   712  
   713  func newEnv(q *apiclient.ManifestRequest, revision string) *v1alpha1.Env {
   714  	return &v1alpha1.Env{
   715  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAME", Value: q.AppLabelValue},
   716  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_NAMESPACE", Value: q.Namespace},
   717  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_REVISION", Value: revision},
   718  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_REPO_URL", Value: q.Repo.Repo},
   719  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_PATH", Value: q.ApplicationSource.Path},
   720  		&v1alpha1.EnvEntry{Name: "ARGOCD_APP_SOURCE_TARGET_REVISION", Value: q.ApplicationSource.TargetRevision},
   721  	}
   722  }
   723  
   724  func mergeSourceParameters(source *v1alpha1.ApplicationSource, path string) error {
   725  	appFilePath := filepath.Join(path, ".argocd-source.yaml")
   726  	info, err := os.Stat(appFilePath)
   727  	if os.IsNotExist(err) {
   728  		return nil
   729  	} else if info != nil && info.IsDir() {
   730  		return nil
   731  	} else if err != nil {
   732  		return err
   733  	}
   734  	patch, err := json.Marshal(source)
   735  	if err != nil {
   736  		return err
   737  	}
   738  
   739  	data, err := ioutil.ReadFile(appFilePath)
   740  	if err != nil {
   741  		return err
   742  	}
   743  	data, err = yaml.YAMLToJSON(data)
   744  	if err != nil {
   745  		return err
   746  	}
   747  	data, err = jsonpatch.MergePatch(data, patch)
   748  	if err != nil {
   749  		return err
   750  	}
   751  	var merged v1alpha1.ApplicationSource
   752  	err = json.Unmarshal(data, &merged)
   753  	if err != nil {
   754  		return err
   755  	}
   756  	// make sure only config management tools related properties are used and ignore everything else
   757  	merged.Chart = source.Chart
   758  	merged.Path = source.Path
   759  	merged.RepoURL = source.RepoURL
   760  	merged.TargetRevision = source.TargetRevision
   761  
   762  	*source = merged
   763  	return nil
   764  }
   765  
   766  // GetAppSourceType returns explicit application source type or examines a directory and determines its application source type
   767  func GetAppSourceType(source *v1alpha1.ApplicationSource, path string) (v1alpha1.ApplicationSourceType, error) {
   768  	err := mergeSourceParameters(source, path)
   769  	if err != nil {
   770  		return "", fmt.Errorf("error while parsing .argocd-source.yaml: %v", err)
   771  	}
   772  
   773  	appSourceType, err := source.ExplicitType()
   774  	if err != nil {
   775  		return "", err
   776  	}
   777  	if appSourceType != nil {
   778  		return *appSourceType, nil
   779  	}
   780  	appType, err := discovery.AppType(path)
   781  	if err != nil {
   782  		return "", err
   783  	}
   784  	return v1alpha1.ApplicationSourceType(appType), nil
   785  }
   786  
   787  // isNullList checks if the object is a "List" type where items is null instead of an empty list.
   788  // Handles a corner case where obj.IsList() returns false when a manifest is like:
   789  // ---
   790  // apiVersion: v1
   791  // items: null
   792  // kind: ConfigMapList
   793  func isNullList(obj *unstructured.Unstructured) bool {
   794  	if _, ok := obj.Object["spec"]; ok {
   795  		return false
   796  	}
   797  	if _, ok := obj.Object["status"]; ok {
   798  		return false
   799  	}
   800  	field, ok := obj.Object["items"]
   801  	if !ok {
   802  		return false
   803  	}
   804  	return field == nil
   805  }
   806  
   807  // ksShow runs `ks show` in an app directory after setting any component parameter overrides
   808  func ksShow(appLabelKey, appPath string, ksonnetOpts *v1alpha1.ApplicationSourceKsonnet) ([]*unstructured.Unstructured, *v1alpha1.ApplicationDestination, error) {
   809  	ksApp, err := ksonnet.NewKsonnetApp(appPath)
   810  	if err != nil {
   811  		return nil, nil, status.Errorf(codes.FailedPrecondition, "unable to load application from %s: %v", appPath, err)
   812  	}
   813  	if ksonnetOpts == nil {
   814  		return nil, nil, status.Errorf(codes.InvalidArgument, "Ksonnet environment not set")
   815  	}
   816  	for _, override := range ksonnetOpts.Parameters {
   817  		err = ksApp.SetComponentParams(ksonnetOpts.Environment, override.Component, override.Name, override.Value)
   818  		if err != nil {
   819  			return nil, nil, err
   820  		}
   821  	}
   822  	dest, err := ksApp.Destination(ksonnetOpts.Environment)
   823  	if err != nil {
   824  		return nil, nil, status.Errorf(codes.InvalidArgument, err.Error())
   825  	}
   826  	targetObjs, err := ksApp.Show(ksonnetOpts.Environment)
   827  	if err == nil && appLabelKey == common.LabelKeyLegacyApplicationName {
   828  		// Address https://github.com/ksonnet/ksonnet/issues/707
   829  		for _, d := range targetObjs {
   830  			kube.UnsetLabel(d, "ksonnet.io/component")
   831  		}
   832  	}
   833  	if err != nil {
   834  		return nil, nil, err
   835  	}
   836  	return targetObjs, dest, nil
   837  }
   838  
   839  var manifestFile = regexp.MustCompile(`^.*\.(yaml|yml|json|jsonnet)$`)
   840  
   841  // findManifests looks at all yaml files in a directory and unmarshals them into a list of unstructured objects
   842  func findManifests(appPath string, repoRoot string, env *v1alpha1.Env, directory v1alpha1.ApplicationSourceDirectory) ([]*unstructured.Unstructured, error) {
   843  	var objs []*unstructured.Unstructured
   844  	err := filepath.Walk(appPath, func(path string, f os.FileInfo, err error) error {
   845  		if err != nil {
   846  			return err
   847  		}
   848  		if f.IsDir() {
   849  			if path != appPath && !directory.Recurse {
   850  				return filepath.SkipDir
   851  			} else {
   852  				return nil
   853  			}
   854  		}
   855  
   856  		if !manifestFile.MatchString(f.Name()) {
   857  			return nil
   858  		}
   859  
   860  		fileNameWithPath := filepath.Join(appPath, f.Name())
   861  		if glob.Match(directory.Exclude, fileNameWithPath) {
   862  			return nil
   863  		}
   864  
   865  		if strings.HasSuffix(f.Name(), ".jsonnet") {
   866  			vm, err := makeJsonnetVm(appPath, repoRoot, directory.Jsonnet, env)
   867  			if err != nil {
   868  				return err
   869  			}
   870  			jsonStr, err := vm.EvaluateFile(path)
   871  			if err != nil {
   872  				return status.Errorf(codes.FailedPrecondition, "Failed to evaluate jsonnet %q: %v", f.Name(), err)
   873  			}
   874  
   875  			// attempt to unmarshal either array or single object
   876  			var jsonObjs []*unstructured.Unstructured
   877  			err = json.Unmarshal([]byte(jsonStr), &jsonObjs)
   878  			if err == nil {
   879  				objs = append(objs, jsonObjs...)
   880  			} else {
   881  				var jsonObj unstructured.Unstructured
   882  				err = json.Unmarshal([]byte(jsonStr), &jsonObj)
   883  				if err != nil {
   884  					return status.Errorf(codes.FailedPrecondition, "Failed to unmarshal generated json %q: %v", f.Name(), err)
   885  				}
   886  				objs = append(objs, &jsonObj)
   887  			}
   888  		} else {
   889  			out, err := utfutil.ReadFile(path, utfutil.UTF8)
   890  			if err != nil {
   891  				return err
   892  			}
   893  			if strings.HasSuffix(f.Name(), ".json") {
   894  				var obj unstructured.Unstructured
   895  				err = json.Unmarshal(out, &obj)
   896  				if err != nil {
   897  					return status.Errorf(codes.FailedPrecondition, "Failed to unmarshal %q: %v", f.Name(), err)
   898  				}
   899  				objs = append(objs, &obj)
   900  			} else {
   901  				yamlObjs, err := kube.SplitYAML(out)
   902  				if err != nil {
   903  					if len(yamlObjs) > 0 {
   904  						// If we get here, we had a multiple objects in a single YAML file which had some
   905  						// valid k8s objects, but errors parsing others (within the same file). It's very
   906  						// likely the user messed up a portion of the YAML, so report on that.
   907  						return status.Errorf(codes.FailedPrecondition, "Failed to unmarshal %q: %v", f.Name(), err)
   908  					}
   909  					// Otherwise, let's see if it looks like a resource, if yes, we return error
   910  					if bytes.Contains(out, []byte("apiVersion:")) &&
   911  						bytes.Contains(out, []byte("kind:")) &&
   912  						bytes.Contains(out, []byte("metadata:")) {
   913  						return status.Errorf(codes.FailedPrecondition, "Failed to unmarshal %q: %v", f.Name(), err)
   914  					}
   915  					// Otherwise, it might be a unrelated YAML file which we will ignore
   916  					return nil
   917  				}
   918  				objs = append(objs, yamlObjs...)
   919  			}
   920  		}
   921  		return nil
   922  	})
   923  	if err != nil {
   924  		return nil, err
   925  	}
   926  	return objs, nil
   927  }
   928  
   929  func makeJsonnetVm(appPath string, repoRoot string, sourceJsonnet v1alpha1.ApplicationSourceJsonnet, env *v1alpha1.Env) (*jsonnet.VM, error) {
   930  
   931  	vm := jsonnet.MakeVM()
   932  	for i, j := range sourceJsonnet.TLAs {
   933  		sourceJsonnet.TLAs[i].Value = env.Envsubst(j.Value)
   934  	}
   935  	for i, j := range sourceJsonnet.ExtVars {
   936  		sourceJsonnet.ExtVars[i].Value = env.Envsubst(j.Value)
   937  	}
   938  	for _, arg := range sourceJsonnet.TLAs {
   939  		if arg.Code {
   940  			vm.TLACode(arg.Name, arg.Value)
   941  		} else {
   942  			vm.TLAVar(arg.Name, arg.Value)
   943  		}
   944  	}
   945  	for _, extVar := range sourceJsonnet.ExtVars {
   946  		if extVar.Code {
   947  			vm.ExtCode(extVar.Name, extVar.Value)
   948  		} else {
   949  			vm.ExtVar(extVar.Name, extVar.Value)
   950  		}
   951  	}
   952  
   953  	// Jsonnet Imports relative to the repository path
   954  	jpaths := []string{appPath}
   955  	for _, p := range sourceJsonnet.Libs {
   956  		jpath := path.Join(repoRoot, p)
   957  		if !strings.HasPrefix(jpath, repoRoot) {
   958  			return nil, status.Errorf(codes.FailedPrecondition, "%s: referenced library points outside the repository", p)
   959  		}
   960  		jpaths = append(jpaths, jpath)
   961  	}
   962  
   963  	vm.Importer(&jsonnet.FileImporter{
   964  		JPaths: jpaths,
   965  	})
   966  
   967  	return vm, nil
   968  }
   969  
   970  func runCommand(command v1alpha1.Command, path string, env []string) (string, error) {
   971  	if len(command.Command) == 0 {
   972  		return "", fmt.Errorf("Command is empty")
   973  	}
   974  	cmd := exec.Command(command.Command[0], append(command.Command[1:], command.Args...)...)
   975  	cmd.Env = env
   976  	cmd.Dir = path
   977  	return executil.Run(cmd)
   978  }
   979  
   980  func findPlugin(plugins []*v1alpha1.ConfigManagementPlugin, name string) *v1alpha1.ConfigManagementPlugin {
   981  	for _, plugin := range plugins {
   982  		if plugin.Name == name {
   983  			return plugin
   984  		}
   985  	}
   986  	return nil
   987  }
   988  
   989  func runConfigManagementPlugin(appPath string, envVars *v1alpha1.Env, q *apiclient.ManifestRequest, creds git.Creds) ([]*unstructured.Unstructured, error) {
   990  	concurrencyAllowed := isConcurrencyAllowed(appPath)
   991  	if !concurrencyAllowed {
   992  		manifestGenerateLock.Lock(appPath)
   993  		defer manifestGenerateLock.Unlock(appPath)
   994  	}
   995  
   996  	plugin := findPlugin(q.Plugins, q.ApplicationSource.Plugin.Name)
   997  	if plugin == nil {
   998  		return nil, fmt.Errorf("Config management plugin with name '%s' is not supported.", q.ApplicationSource.Plugin.Name)
   999  	}
  1000  	env := append(os.Environ(), envVars.Environ()...)
  1001  	if creds != nil {
  1002  		closer, environ, err := creds.Environ()
  1003  		if err != nil {
  1004  			return nil, err
  1005  		}
  1006  		defer func() { _ = closer.Close() }()
  1007  		env = append(env, environ...)
  1008  	}
  1009  	env = append(env, q.ApplicationSource.Plugin.Env.Environ()...)
  1010  	env = append(env, "KUBE_VERSION="+q.KubeVersion)
  1011  	env = append(env, "KUBE_API_VERSIONS="+strings.Join(q.ApiVersions, ","))
  1012  	if plugin.Init != nil {
  1013  		_, err := runCommand(*plugin.Init, appPath, env)
  1014  		if err != nil {
  1015  			return nil, err
  1016  		}
  1017  	}
  1018  	out, err := runCommand(plugin.Generate, appPath, env)
  1019  	if err != nil {
  1020  		return nil, err
  1021  	}
  1022  	return kube.SplitYAML([]byte(out))
  1023  }
  1024  
  1025  func (s *Service) GetAppDetails(ctx context.Context, q *apiclient.RepoServerAppDetailsQuery) (*apiclient.RepoAppDetailsResponse, error) {
  1026  
  1027  	getCached := func(revision string, _ bool) (bool, interface{}, error) {
  1028  		res := &apiclient.RepoAppDetailsResponse{}
  1029  		err := s.cache.GetAppDetails(revision, q.Source, res)
  1030  		if err == nil {
  1031  			log.Infof("app details cache hit: %s/%s", revision, q.Source.Path)
  1032  			return true, res, nil
  1033  		}
  1034  
  1035  		if err != reposervercache.ErrCacheMiss {
  1036  			log.Warnf("app details cache error %s: %v", revision, q.Source)
  1037  		} else {
  1038  			log.Infof("app details cache miss: %s/%s", revision, q.Source)
  1039  		}
  1040  		return false, nil, nil
  1041  
  1042  	}
  1043  
  1044  	resultUncast, err := s.runRepoOperation(ctx, q.Source.TargetRevision, q.Repo, q.Source, false, getCached, func(repoRoot, commitSHA, revision string, ctxSrc operationContextSrc) (interface{}, error) {
  1045  		ctx, err := ctxSrc()
  1046  		if err != nil {
  1047  			return nil, err
  1048  		}
  1049  		appPath := ctx.appPath
  1050  
  1051  		res := &apiclient.RepoAppDetailsResponse{}
  1052  		appSourceType, err := GetAppSourceType(q.Source, appPath)
  1053  		if err != nil {
  1054  			return nil, err
  1055  		}
  1056  
  1057  		res.Type = string(appSourceType)
  1058  
  1059  		switch appSourceType {
  1060  		case v1alpha1.ApplicationSourceTypeKsonnet:
  1061  			var ksonnetAppSpec apiclient.KsonnetAppSpec
  1062  			data, err := ioutil.ReadFile(filepath.Join(appPath, "app.yaml"))
  1063  			if err != nil {
  1064  				return nil, err
  1065  			}
  1066  			err = yaml.Unmarshal(data, &ksonnetAppSpec)
  1067  			if err != nil {
  1068  				return nil, err
  1069  			}
  1070  			ksApp, err := ksonnet.NewKsonnetApp(appPath)
  1071  			if err != nil {
  1072  				return nil, status.Errorf(codes.FailedPrecondition, "unable to load application from %s: %v", appPath, err)
  1073  			}
  1074  			env := ""
  1075  			if q.Source.Ksonnet != nil {
  1076  				env = q.Source.Ksonnet.Environment
  1077  			}
  1078  			params, err := ksApp.ListParams(env)
  1079  			if err != nil {
  1080  				return nil, err
  1081  			}
  1082  			ksonnetAppSpec.Parameters = params
  1083  			res.Ksonnet = &ksonnetAppSpec
  1084  		case v1alpha1.ApplicationSourceTypeHelm:
  1085  			res.Helm = &apiclient.HelmAppSpec{}
  1086  			files, err := ioutil.ReadDir(appPath)
  1087  			if err != nil {
  1088  				return nil, err
  1089  			}
  1090  			for _, f := range files {
  1091  				if f.IsDir() {
  1092  					continue
  1093  				}
  1094  				fName := f.Name()
  1095  				if strings.Contains(fName, "values") && (filepath.Ext(fName) == ".yaml" || filepath.Ext(fName) == ".yml") {
  1096  					res.Helm.ValueFiles = append(res.Helm.ValueFiles, fName)
  1097  				}
  1098  			}
  1099  			var version string
  1100  			if q.Source.Helm != nil {
  1101  				if q.Source.Helm.Version != "" {
  1102  					version = q.Source.Helm.Version
  1103  				}
  1104  			}
  1105  			h, err := helm.NewHelmApp(appPath, getHelmRepos(q.Repos), false, version)
  1106  			if err != nil {
  1107  				return nil, err
  1108  			}
  1109  			defer h.Dispose()
  1110  			err = h.Init()
  1111  			if err != nil {
  1112  				return nil, err
  1113  			}
  1114  			valuesPath := filepath.Join(appPath, "values.yaml")
  1115  			info, err := os.Stat(valuesPath)
  1116  			if err == nil && !info.IsDir() {
  1117  				bytes, err := ioutil.ReadFile(valuesPath)
  1118  				if err != nil {
  1119  					return nil, err
  1120  				}
  1121  				res.Helm.Values = string(bytes)
  1122  			}
  1123  			params, err := h.GetParameters(valueFiles(q))
  1124  			if err != nil {
  1125  				return nil, err
  1126  			}
  1127  			for k, v := range params {
  1128  				res.Helm.Parameters = append(res.Helm.Parameters, &v1alpha1.HelmParameter{
  1129  					Name:  k,
  1130  					Value: v,
  1131  				})
  1132  			}
  1133  			for _, v := range fileParameters(q) {
  1134  				res.Helm.FileParameters = append(res.Helm.FileParameters, &v1alpha1.HelmFileParameter{
  1135  					Name: v.Name,
  1136  					Path: v.Path, //filepath.Join(appPath, v.Path),
  1137  				})
  1138  			}
  1139  		case v1alpha1.ApplicationSourceTypeKustomize:
  1140  			res.Kustomize = &apiclient.KustomizeAppSpec{}
  1141  			kustomizeBinary := ""
  1142  			if q.KustomizeOptions != nil {
  1143  				kustomizeBinary = q.KustomizeOptions.BinaryPath
  1144  			}
  1145  			k := kustomize.NewKustomizeApp(appPath, q.Repo.GetGitCreds(), q.Repo.Repo, kustomizeBinary)
  1146  			_, images, err := k.Build(q.Source.Kustomize, q.KustomizeOptions)
  1147  			if err != nil {
  1148  				return nil, err
  1149  			}
  1150  			res.Kustomize.Images = images
  1151  		}
  1152  		_ = s.cache.SetAppDetails(revision, q.Source, res)
  1153  		return res, nil
  1154  	}, operationSettings{allowConcurrent: q.Source.AllowsConcurrentProcessing()})
  1155  
  1156  	result, ok := resultUncast.(*apiclient.RepoAppDetailsResponse)
  1157  	if result != nil && !ok {
  1158  		return nil, errors.New("unexpected result type")
  1159  	}
  1160  
  1161  	return result, err
  1162  }
  1163  
  1164  func (s *Service) GetRevisionMetadata(ctx context.Context, q *apiclient.RepoServerRevisionMetadataRequest) (*v1alpha1.RevisionMetadata, error) {
  1165  	if !git.IsCommitSHA(q.Revision) {
  1166  		return nil, fmt.Errorf("revision %s must be resolved", q.Revision)
  1167  	}
  1168  	metadata, err := s.cache.GetRevisionMetadata(q.Repo.Repo, q.Revision)
  1169  	if err == nil {
  1170  		// The logic here is that if a signature check on metadata is requested,
  1171  		// but there is none in the cache, we handle as if we have a cache miss
  1172  		// and re-generate the meta data. Otherwise, if there is signature info
  1173  		// in the metadata, but none was requested, we remove it from the data
  1174  		// that we return.
  1175  		if q.CheckSignature && metadata.SignatureInfo == "" {
  1176  			log.Infof("revision metadata cache hit, but need to regenerate due to missing signature info: %s/%s", q.Repo.Repo, q.Revision)
  1177  		} else {
  1178  			log.Infof("revision metadata cache hit: %s/%s", q.Repo.Repo, q.Revision)
  1179  			if !q.CheckSignature {
  1180  				metadata.SignatureInfo = ""
  1181  			}
  1182  			return metadata, nil
  1183  		}
  1184  	} else {
  1185  		if err != reposervercache.ErrCacheMiss {
  1186  			log.Warnf("revision metadata cache error %s/%s: %v", q.Repo.Repo, q.Revision, err)
  1187  		} else {
  1188  			log.Infof("revision metadata cache miss: %s/%s", q.Repo.Repo, q.Revision)
  1189  		}
  1190  	}
  1191  
  1192  	gitClient, _, err := s.newClientResolveRevision(q.Repo, q.Revision)
  1193  	if err != nil {
  1194  		return nil, err
  1195  	}
  1196  
  1197  	s.metricsServer.IncPendingRepoRequest(q.Repo.Repo)
  1198  	defer s.metricsServer.DecPendingRepoRequest(q.Repo.Repo)
  1199  
  1200  	closer, err := s.repoLock.Lock(gitClient.Root(), q.Revision, true, func() error {
  1201  		return checkoutRevision(gitClient, q.Revision)
  1202  	})
  1203  
  1204  	if err != nil {
  1205  		return nil, err
  1206  	}
  1207  
  1208  	defer io.Close(closer)
  1209  
  1210  	m, err := gitClient.RevisionMetadata(q.Revision)
  1211  	if err != nil {
  1212  		return nil, err
  1213  	}
  1214  
  1215  	// Run gpg verify-commit on the revision
  1216  	signatureInfo := ""
  1217  	if gpg.IsGPGEnabled() && q.CheckSignature {
  1218  		cs, err := gitClient.VerifyCommitSignature(q.Revision)
  1219  		if err != nil {
  1220  			log.Debugf("Could not verify commit signature: %v", err)
  1221  			return nil, err
  1222  		}
  1223  
  1224  		if cs != "" {
  1225  			vr, err := gpg.ParseGitCommitVerification(cs)
  1226  			if err != nil {
  1227  				log.Debugf("Could not parse commit verification: %v", err)
  1228  				return nil, err
  1229  			}
  1230  			signatureInfo = fmt.Sprintf("%s signature from %s key %s", vr.Result, vr.Cipher, gpg.KeyID(vr.KeyID))
  1231  		} else {
  1232  			signatureInfo = "Revision is not signed."
  1233  		}
  1234  	}
  1235  
  1236  	// discard anything after the first new line and then truncate to 64 chars
  1237  	message := text.Trunc(strings.SplitN(m.Message, "\n", 2)[0], 64)
  1238  	metadata = &v1alpha1.RevisionMetadata{Author: m.Author, Date: metav1.Time{Time: m.Date}, Tags: m.Tags, Message: message, SignatureInfo: signatureInfo}
  1239  	_ = s.cache.SetRevisionMetadata(q.Repo.Repo, q.Revision, metadata)
  1240  	return metadata, nil
  1241  }
  1242  
  1243  func valueFiles(q *apiclient.RepoServerAppDetailsQuery) []string {
  1244  	if q.Source.Helm == nil {
  1245  		return nil
  1246  	}
  1247  	return q.Source.Helm.ValueFiles
  1248  }
  1249  
  1250  func fileParameters(q *apiclient.RepoServerAppDetailsQuery) []v1alpha1.HelmFileParameter {
  1251  	if q.Source.Helm == nil {
  1252  		return nil
  1253  	}
  1254  	return q.Source.Helm.FileParameters
  1255  }
  1256  
  1257  func (s *Service) newClient(repo *v1alpha1.Repository) (git.Client, error) {
  1258  	gitClient, err := s.newGitClient(repo.Repo, repo.GetGitCreds(), repo.IsInsecure(), repo.EnableLFS)
  1259  	if err != nil {
  1260  		return nil, err
  1261  	}
  1262  	return metrics.WrapGitClient(repo.Repo, s.metricsServer, gitClient), nil
  1263  }
  1264  
  1265  // newClientResolveRevision is a helper to perform the common task of instantiating a git client
  1266  // and resolving a revision to a commit SHA
  1267  func (s *Service) newClientResolveRevision(repo *v1alpha1.Repository, revision string) (git.Client, string, error) {
  1268  	gitClient, err := s.newClient(repo)
  1269  	if err != nil {
  1270  		return nil, "", err
  1271  	}
  1272  	commitSHA, err := gitClient.LsRemote(revision)
  1273  	if err != nil {
  1274  		return nil, "", err
  1275  	}
  1276  	return gitClient, commitSHA, nil
  1277  }
  1278  
  1279  func (s *Service) newHelmClientResolveRevision(repo *v1alpha1.Repository, revision string, chart string) (helm.Client, string, error) {
  1280  	helmClient := s.newHelmClient(repo.Repo, repo.GetHelmCreds(), repo.EnableOCI || helm.IsHelmOciChart(chart))
  1281  	if helm.IsVersion(revision) {
  1282  		return helmClient, revision, nil
  1283  	}
  1284  	if repo.EnableOCI {
  1285  		return nil, "", errors.New("OCI helm registers don't support semver ranges. Exact revision must be specified.")
  1286  	}
  1287  	constraints, err := semver.NewConstraint(revision)
  1288  	if err != nil {
  1289  		return nil, "", fmt.Errorf("invalid revision '%s': %v", revision, err)
  1290  	}
  1291  	index, err := helmClient.GetIndex()
  1292  	if err != nil {
  1293  		return nil, "", err
  1294  	}
  1295  	entries, err := index.GetEntries(chart)
  1296  	if err != nil {
  1297  		return nil, "", err
  1298  	}
  1299  	version, err := entries.MaxVersion(constraints)
  1300  	if err != nil {
  1301  		return nil, "", err
  1302  	}
  1303  	return helmClient, version.String(), nil
  1304  }
  1305  
  1306  // checkoutRevision is a convenience function to initialize a repo, fetch, and checkout a revision
  1307  // Returns the 40 character commit SHA after the checkout has been performed
  1308  // nolint:unparam
  1309  func checkoutRevision(gitClient git.Client, revision string) error {
  1310  	err := gitClient.Init()
  1311  	if err != nil {
  1312  		return status.Errorf(codes.Internal, "Failed to initialize git repo: %v", err)
  1313  	}
  1314  	err = gitClient.Fetch()
  1315  	if err != nil {
  1316  		return status.Errorf(codes.Internal, "Failed to fetch git repo: %v", err)
  1317  	}
  1318  	err = gitClient.Checkout(revision)
  1319  	if err != nil {
  1320  		return status.Errorf(codes.Internal, "Failed to checkout %s: %v", revision, err)
  1321  	}
  1322  	return err
  1323  }
  1324  
  1325  func (s *Service) GetHelmCharts(ctx context.Context, q *apiclient.HelmChartsRequest) (*apiclient.HelmChartsResponse, error) {
  1326  	index, err := s.newHelmClient(q.Repo.Repo, q.Repo.GetHelmCreds(), q.Repo.EnableOCI).GetIndex()
  1327  	if err != nil {
  1328  		return nil, err
  1329  	}
  1330  	res := apiclient.HelmChartsResponse{}
  1331  	for chartName, entries := range index.Entries {
  1332  		chart := apiclient.HelmChart{
  1333  			Name: chartName,
  1334  		}
  1335  		for _, entry := range entries {
  1336  			chart.Versions = append(chart.Versions, entry.Version)
  1337  		}
  1338  		res.Items = append(res.Items, &chart)
  1339  	}
  1340  	return &res, nil
  1341  }