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