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