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