1package state 2 3import ( 4 "bytes" 5 "crypto/sha1" 6 "encoding/hex" 7 "errors" 8 "fmt" 9 "io" 10 "io/ioutil" 11 "os" 12 "path" 13 "path/filepath" 14 "regexp" 15 "sort" 16 "strconv" 17 "strings" 18 "sync" 19 "text/template" 20 21 "github.com/imdario/mergo" 22 "github.com/variantdev/chartify" 23 24 "github.com/roboll/helmfile/pkg/environment" 25 "github.com/roboll/helmfile/pkg/event" 26 "github.com/roboll/helmfile/pkg/helmexec" 27 "github.com/roboll/helmfile/pkg/remote" 28 "github.com/roboll/helmfile/pkg/tmpl" 29 30 "github.com/tatsushid/go-prettytable" 31 "github.com/variantdev/vals" 32 "go.uber.org/zap" 33 "gopkg.in/yaml.v2" 34) 35 36const ( 37 // EmptyTimeout represents the `--timeout` value passed to helm commands not being specified via helmfile flags. 38 // This is used by an interim solution to make the urfave/cli command report to the helmfile internal about that the 39 // --timeout flag is missingl 40 EmptyTimeout = -1 41) 42 43type ReleaseSetSpec struct { 44 DefaultHelmBinary string `yaml:"helmBinary,omitempty"` 45 46 // DefaultValues is the default values to be overrode by environment values and command-line overrides 47 DefaultValues []interface{} `yaml:"values,omitempty"` 48 49 Environments map[string]EnvironmentSpec `yaml:"environments,omitempty"` 50 51 Bases []string `yaml:"bases,omitempty"` 52 HelmDefaults HelmSpec `yaml:"helmDefaults,omitempty"` 53 Helmfiles []SubHelmfileSpec `yaml:"helmfiles,omitempty"` 54 DeprecatedContext string `yaml:"context,omitempty"` 55 DeprecatedReleases []ReleaseSpec `yaml:"charts,omitempty"` 56 OverrideNamespace string `yaml:"namespace,omitempty"` 57 Repositories []RepositorySpec `yaml:"repositories,omitempty"` 58 CommonLabels map[string]string `yaml:"commonLabels,omitempty"` 59 Releases []ReleaseSpec `yaml:"releases,omitempty"` 60 Selectors []string `yaml:"-"` 61 ApiVersions []string `yaml:"apiVersions,omitempty"` 62 63 // Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile 64 Hooks []event.Hook `yaml:"hooks,omitempty"` 65 66 Templates map[string]TemplateSpec `yaml:"templates"` 67 68 Env environment.Environment `yaml:"-"` 69 70 // If set to "Error", return an error when a subhelmfile points to a 71 // non-existent path. The default behavior is to print a warning. Note the 72 // differing default compared to other MissingFileHandlers. 73 MissingFileHandler string `yaml:"missingFileHandler,omitempty"` 74} 75 76type PullCommand struct { 77 ChartRef string 78 responseChan chan error 79} 80 81// HelmState structure for the helmfile 82type HelmState struct { 83 basePath string 84 FilePath string 85 86 ReleaseSetSpec `yaml:",inline"` 87 88 logger *zap.SugaredLogger 89 90 readFile func(string) ([]byte, error) 91 removeFile func(string) error 92 fileExists func(string) (bool, error) 93 glob func(string) ([]string, error) 94 tempDir func(string, string) (string, error) 95 directoryExistsAt func(string) bool 96 97 runner helmexec.Runner 98 valsRuntime vals.Evaluator 99 100 // RenderedValues is the helmfile-wide values that is `.Values` 101 // which is accessible from within the whole helmfile go template. 102 // Note that this is usually computed by DesiredStateLoader from ReleaseSetSpec.Env 103 RenderedValues map[string]interface{} 104} 105 106// SubHelmfileSpec defines the subhelmfile path and options 107type SubHelmfileSpec struct { 108 //path or glob pattern for the sub helmfiles 109 Path string `yaml:"path,omitempty"` 110 //chosen selectors for the sub helmfiles 111 Selectors []string `yaml:"selectors,omitempty"` 112 //do the sub helmfiles inherits from parent selectors 113 SelectorsInherited bool `yaml:"selectorsInherited,omitempty"` 114 115 Environment SubhelmfileEnvironmentSpec 116} 117 118type SubhelmfileEnvironmentSpec struct { 119 OverrideValues []interface{} `yaml:"values,omitempty"` 120} 121 122// HelmSpec to defines helmDefault values 123type HelmSpec struct { 124 KubeContext string `yaml:"kubeContext,omitempty"` 125 TillerNamespace string `yaml:"tillerNamespace,omitempty"` 126 Tillerless bool `yaml:"tillerless"` 127 Args []string `yaml:"args,omitempty"` 128 Verify bool `yaml:"verify"` 129 // Devel, when set to true, use development versions, too. Equivalent to version '>0.0.0-0' 130 Devel bool `yaml:"devel"` 131 // Wait, if set to true, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful 132 Wait bool `yaml:"wait"` 133 // Timeout is the time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300) 134 Timeout int `yaml:"timeout"` 135 // RecreatePods, when set to true, instruct helmfile to perform pods restart for the resource if applicable 136 RecreatePods bool `yaml:"recreatePods"` 137 // Force, when set to true, forces resource update through delete/recreate if needed 138 Force bool `yaml:"force"` 139 // Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt 140 Atomic bool `yaml:"atomic"` 141 // CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command 142 CleanupOnFail bool `yaml:"cleanupOnFail,omitempty"` 143 // HistoryMax, limit the maximum number of revisions saved per release. Use 0 for no limit (default 10) 144 HistoryMax *int `yaml:"historyMax,omitempty"` 145 // CreateNamespace, when set to true (default), --create-namespace is passed to helm3 on install/upgrade (ignored for helm2) 146 CreateNamespace *bool `yaml:"createNamespace,omitempty"` 147 // SkipDeps disables running `helm dependency up` and `helm dependency build` on this release's chart. 148 // This is relevant only when your release uses a local chart or a directory containing K8s manifests or a Kustomization 149 // as a Helm chart. 150 SkipDeps bool `yaml:"skipDeps"` 151 152 TLS bool `yaml:"tls"` 153 TLSCACert string `yaml:"tlsCACert,omitempty"` 154 TLSKey string `yaml:"tlsKey,omitempty"` 155 TLSCert string `yaml:"tlsCert,omitempty"` 156 DisableValidation *bool `yaml:"disableValidation,omitempty"` 157 DisableOpenAPIValidation *bool `yaml:"disableOpenAPIValidation,omitempty"` 158} 159 160// RepositorySpec that defines values for a helm repo 161type RepositorySpec struct { 162 Name string `yaml:"name,omitempty"` 163 URL string `yaml:"url,omitempty"` 164 CaFile string `yaml:"caFile,omitempty"` 165 CertFile string `yaml:"certFile,omitempty"` 166 KeyFile string `yaml:"keyFile,omitempty"` 167 Username string `yaml:"username,omitempty"` 168 Password string `yaml:"password,omitempty"` 169 Managed string `yaml:"managed,omitempty"` 170 OCI bool `yaml:"oci,omitempty"` 171} 172 173// ReleaseSpec defines the structure of a helm release 174type ReleaseSpec struct { 175 // Chart is the name of the chart being installed to create this release 176 Chart string `yaml:"chart,omitempty"` 177 // Directory is an alias to Chart which may be of more fit when you want to use a local/remote directory containing 178 // K8s manifests or Kustomization as a chart 179 Directory string `yaml:"directory,omitempty"` 180 // Version is the semver version or version constraint for the chart 181 Version string `yaml:"version,omitempty"` 182 // Verify enables signature verification on fetched chart. 183 // Beware some (or many?) chart repositories and charts don't seem to support it. 184 Verify *bool `yaml:"verify,omitempty"` 185 // Devel, when set to true, use development versions, too. Equivalent to version '>0.0.0-0' 186 Devel *bool `yaml:"devel,omitempty"` 187 // Wait, if set to true, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful 188 Wait *bool `yaml:"wait,omitempty"` 189 // Timeout is the time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks, and waits on pod/pvc/svc/deployment readiness) (default 300) 190 Timeout *int `yaml:"timeout,omitempty"` 191 // RecreatePods, when set to true, instruct helmfile to perform pods restart for the resource if applicable 192 RecreatePods *bool `yaml:"recreatePods,omitempty"` 193 // Force, when set to true, forces resource update through delete/recreate if needed 194 Force *bool `yaml:"force,omitempty"` 195 // Installed, when set to true, `delete --purge` the release 196 Installed *bool `yaml:"installed,omitempty"` 197 // Atomic, when set to true, restore previous state in case of a failed install/upgrade attempt 198 Atomic *bool `yaml:"atomic,omitempty"` 199 // CleanupOnFail, when set to true, the --cleanup-on-fail helm flag is passed to the upgrade command 200 CleanupOnFail *bool `yaml:"cleanupOnFail,omitempty"` 201 // HistoryMax, limit the maximum number of revisions saved per release. Use 0 for no limit (default 10) 202 HistoryMax *int `yaml:"historyMax,omitempty"` 203 // Condition, when set, evaluate the mapping specified in this string to a boolean which decides whether or not to process the release 204 Condition string `yaml:"condition,omitempty"` 205 // CreateNamespace, when set to true (default), --create-namespace is passed to helm3 on install (ignored for helm2) 206 CreateNamespace *bool `yaml:"createNamespace,omitempty"` 207 208 // DisableOpenAPIValidation is rarely used to bypass OpenAPI validations only that is used for e.g. 209 // work-around against broken CRs 210 // See also: 211 // - https://github.com/helm/helm/pull/6819 212 // - https://github.com/roboll/helmfile/issues/1167 213 DisableOpenAPIValidation *bool `yaml:"disableOpenAPIValidation,omitempty"` 214 215 // DisableValidation is rarely used to bypass the whole validation of manifests against the Kubernetes cluster 216 // so that `helm diff` can be run containing a chart that installs both CRD and CRs on first install. 217 // FYI, such diff without `--disable-validation` fails on first install because the K8s cluster doesn't have CRDs registered yet. 218 DisableValidation *bool `yaml:"disableValidation,omitempty"` 219 220 // DisableValidationOnInstall disables the K8s API validation while running helm-diff on the release being newly installed on helmfile-apply. 221 // It is useful when any release contains custom resources for CRDs that is not yet installed onto the cluster. 222 DisableValidationOnInstall *bool `yaml:"disableValidationOnInstall,omitempty"` 223 224 // MissingFileHandler is set to either "Error" or "Warn". "Error" instructs helmfile to fail when unable to find a values or secrets file. When "Warn", it prints the file and continues. 225 // The default value for MissingFileHandler is "Error". 226 MissingFileHandler *string `yaml:"missingFileHandler,omitempty"` 227 // Needs is the [TILLER_NS/][NS/]NAME representations of releases that this release depends on. 228 Needs []string `yaml:"needs,omitempty"` 229 230 // Hooks is a list of extension points paired with operations, that are executed in specific points of the lifecycle of releases defined in helmfile 231 Hooks []event.Hook `yaml:"hooks,omitempty"` 232 233 // Name is the name of this release 234 Name string `yaml:"name,omitempty"` 235 Namespace string `yaml:"namespace,omitempty"` 236 Labels map[string]string `yaml:"labels,omitempty"` 237 Values []interface{} `yaml:"values,omitempty"` 238 Secrets []interface{} `yaml:"secrets,omitempty"` 239 SetValues []SetValue `yaml:"set,omitempty"` 240 241 ValuesTemplate []interface{} `yaml:"valuesTemplate,omitempty"` 242 SetValuesTemplate []SetValue `yaml:"setTemplate,omitempty"` 243 244 // The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality 245 EnvValues []SetValue `yaml:"env,omitempty"` 246 247 ValuesPathPrefix string `yaml:"valuesPathPrefix,omitempty"` 248 249 TillerNamespace string `yaml:"tillerNamespace,omitempty"` 250 Tillerless *bool `yaml:"tillerless,omitempty"` 251 252 KubeContext string `yaml:"kubeContext,omitempty"` 253 254 TLS *bool `yaml:"tls,omitempty"` 255 TLSCACert string `yaml:"tlsCACert,omitempty"` 256 TLSKey string `yaml:"tlsKey,omitempty"` 257 TLSCert string `yaml:"tlsCert,omitempty"` 258 259 // These values are used in templating 260 TillerlessTemplate *string `yaml:"tillerlessTemplate,omitempty"` 261 VerifyTemplate *string `yaml:"verifyTemplate,omitempty"` 262 WaitTemplate *string `yaml:"waitTemplate,omitempty"` 263 InstalledTemplate *string `yaml:"installedTemplate,omitempty"` 264 265 // These settings requires helm-x integration to work 266 Dependencies []Dependency `yaml:"dependencies,omitempty"` 267 JSONPatches []interface{} `yaml:"jsonPatches,omitempty"` 268 StrategicMergePatches []interface{} `yaml:"strategicMergePatches,omitempty"` 269 270 // Transformers is the list of Kustomize transformers 271 // 272 // Each item can be a path to a YAML or go template file, or an embedded transformer declaration as a YAML hash. 273 // It's often used to add common labels and annotations to your resources. 274 // See https://github.com/kubernetes-sigs/kustomize/blob/master/examples/configureBuiltinPlugin.md#configuring-the-builtin-plugins-instead for more information. 275 Transformers []interface{} `yaml:"transformers,omitempty"` 276 Adopt []string `yaml:"adopt,omitempty"` 277 278 //version of the chart that has really been installed cause desired version may be fuzzy (~2.0.0) 279 installedVersion string 280 281 // ForceGoGetter forces the use of go-getter for fetching remote directory as maniefsts/chart/kustomization 282 // by parsing the url from `chart` field of the release. 283 // This is handy when getting the go-getter url parsing error when it doesn't work as expected. 284 // Without this, any error in url parsing result in silently falling-back to normal process of treating `chart:` as the regular 285 // helm chart name. 286 ForceGoGetter bool `yaml:"forceGoGetter,omitempty"` 287 288 // ForceNamespace is an experimental feature to set metadata.namespace in every K8s resource rendered by the chart, 289 // regardless of the template, even when it doesn't have `namespace: {{ .Namespace | quote }}`. 290 // This is only needed when you can't FIX your chart to have `namespace: {{ .Namespace }}` AND you're using `helmfile template`. 291 // In standard use-cases, `Namespace` should be sufficient. 292 // Use this only when you know what you want to do! 293 ForceNamespace string `yaml:"forceNamespace,omitempty"` 294 295 // SkipDeps disables running `helm dependency up` and `helm dependency build` on this release's chart. 296 // This is relevant only when your release uses a local chart or a directory containing K8s manifests or a Kustomization 297 // as a Helm chart. 298 SkipDeps *bool `yaml:"skipDeps,omitempty"` 299} 300 301type Release struct { 302 ReleaseSpec 303 304 Filtered bool 305} 306 307// SetValue are the key values to set on a helm release 308type SetValue struct { 309 Name string `yaml:"name,omitempty"` 310 Value string `yaml:"value,omitempty"` 311 File string `yaml:"file,omitempty"` 312 Values []string `yaml:"values,omitempty"` 313} 314 315// AffectedReleases hold the list of released that where updated, deleted, or in error 316type AffectedReleases struct { 317 Upgraded []*ReleaseSpec 318 Deleted []*ReleaseSpec 319 Failed []*ReleaseSpec 320} 321 322const DefaultEnv = "default" 323 324const MissingFileHandlerError = "Error" 325const MissingFileHandlerInfo = "Info" 326const MissingFileHandlerWarn = "Warn" 327const MissingFileHandlerDebug = "Debug" 328 329func (st *HelmState) ApplyOverrides(spec *ReleaseSpec) { 330 if st.OverrideNamespace != "" { 331 spec.Namespace = st.OverrideNamespace 332 333 for i := 0; i < len(spec.Needs); i++ { 334 n := spec.Needs[i] 335 if len(strings.Split(n, "/")) == 1 { 336 spec.Needs[i] = st.OverrideNamespace + "/" + n 337 } 338 } 339 } 340} 341 342type RepoUpdater interface { 343 AddRepo(name, repository, cafile, certfile, keyfile, username, password string, managed string) error 344 UpdateRepo() error 345 RegistryLogin(name string, username string, password string) error 346} 347 348// getRepositoriesToSync returns the names of repositories to be updated 349func (st *HelmState) getRepositoriesToSync() (map[string]bool, error) { 350 releases, err := st.GetSelectedReleasesWithOverrides() 351 if err != nil { 352 return nil, err 353 } 354 355 repositoriesToUpdate := map[string]bool{} 356 357 if len(releases) == 0 { 358 for _, repo := range st.Repositories { 359 repositoriesToUpdate[repo.Name] = true 360 } 361 362 return repositoriesToUpdate, nil 363 } 364 365 for _, release := range releases { 366 if release.Installed == nil || *release.Installed { 367 chart := strings.Split(release.Chart, "/") 368 if len(chart) == 1 { 369 continue 370 } 371 repositoriesToUpdate[chart[0]] = true 372 } 373 } 374 375 return repositoriesToUpdate, nil 376} 377 378func (st *HelmState) SyncRepos(helm RepoUpdater, shouldSkip map[string]bool) ([]string, error) { 379 var updated []string 380 381 for _, repo := range st.Repositories { 382 if shouldSkip[repo.Name] { 383 continue 384 } 385 var err error 386 if repo.OCI { 387 username, password := gatherOCIUsernamePassword(repo.Name, repo.Username, repo.Password) 388 if username != "" && password != "" { 389 err = helm.RegistryLogin(repo.URL, username, password) 390 } 391 } else { 392 err = helm.AddRepo(repo.Name, repo.URL, repo.CaFile, repo.CertFile, repo.KeyFile, repo.Username, repo.Password, repo.Managed) 393 } 394 395 if err != nil { 396 return nil, err 397 } 398 399 updated = append(updated, repo.Name) 400 } 401 402 return updated, nil 403} 404 405func gatherOCIUsernamePassword(repoName string, username string, password string) (string, string) { 406 var user, pass string 407 408 if username != "" { 409 user = username 410 } else if u := os.Getenv(fmt.Sprintf("%s_USERNAME", strings.ToUpper(repoName))); u != "" { 411 user = u 412 } 413 414 if password != "" { 415 pass = password 416 } else if p := os.Getenv(fmt.Sprintf("%s_PASSWORD", strings.ToUpper(repoName))); p != "" { 417 pass = p 418 } 419 420 return user, pass 421} 422 423type syncResult struct { 424 errors []*ReleaseError 425} 426 427type syncPrepareResult struct { 428 release *ReleaseSpec 429 flags []string 430 errors []*ReleaseError 431 files []string 432} 433 434// SyncReleases wrapper for executing helm upgrade on the releases 435func (st *HelmState) prepareSyncReleases(helm helmexec.Interface, additionalValues []string, concurrency int, opt ...SyncOpt) ([]syncPrepareResult, []error) { 436 opts := &SyncOpts{} 437 for _, o := range opt { 438 o.Apply(opts) 439 } 440 441 releases := []*ReleaseSpec{} 442 for i, _ := range st.Releases { 443 releases = append(releases, &st.Releases[i]) 444 } 445 446 numReleases := len(releases) 447 jobs := make(chan *ReleaseSpec, numReleases) 448 results := make(chan syncPrepareResult, numReleases) 449 450 res := []syncPrepareResult{} 451 errs := []error{} 452 453 mut := sync.Mutex{} 454 455 st.scatterGather( 456 concurrency, 457 numReleases, 458 func() { 459 for i := 0; i < numReleases; i++ { 460 jobs <- releases[i] 461 } 462 close(jobs) 463 }, 464 func(workerIndex int) { 465 for release := range jobs { 466 st.ApplyOverrides(release) 467 468 // If `installed: false`, the only potential operation on this release would be uninstalling. 469 // We skip generating values files in that case, because for an uninstall with `helm delete`, we don't need to those. 470 // The values files are for `helm upgrade -f values.yaml` calls that happens when the release has `installed: true`. 471 // This logic addresses: 472 // - https://github.com/roboll/helmfile/issues/519 473 // - https://github.com/roboll/helmfile/issues/616 474 if !release.Desired() { 475 results <- syncPrepareResult{release: release, flags: []string{}, errors: []*ReleaseError{}} 476 continue 477 } 478 479 // TODO We need a long-term fix for this :) 480 // See https://github.com/roboll/helmfile/issues/737 481 mut.Lock() 482 flags, files, flagsErr := st.flagsForUpgrade(helm, release, workerIndex) 483 mut.Unlock() 484 if flagsErr != nil { 485 results <- syncPrepareResult{errors: []*ReleaseError{newReleaseFailedError(release, flagsErr)}, files: files} 486 continue 487 } 488 489 errs := []*ReleaseError{} 490 for _, value := range additionalValues { 491 valfile, err := filepath.Abs(value) 492 if err != nil { 493 errs = append(errs, newReleaseFailedError(release, err)) 494 } 495 496 ok, err := st.fileExists(valfile) 497 if err != nil { 498 errs = append(errs, newReleaseFailedError(release, err)) 499 } else if !ok { 500 errs = append(errs, newReleaseFailedError(release, fmt.Errorf("file does not exist: %s", valfile))) 501 } 502 flags = append(flags, "--values", valfile) 503 } 504 505 if opts.Set != nil { 506 for _, s := range opts.Set { 507 flags = append(flags, "--set", s) 508 } 509 } 510 511 if opts.Wait { 512 flags = append(flags, "--wait") 513 } 514 515 if len(errs) > 0 { 516 results <- syncPrepareResult{errors: errs, files: files} 517 continue 518 } 519 520 results <- syncPrepareResult{release: release, flags: flags, errors: []*ReleaseError{}, files: files} 521 } 522 }, 523 func() { 524 for i := 0; i < numReleases; { 525 select { 526 case r := <-results: 527 for _, e := range r.errors { 528 errs = append(errs, e) 529 } 530 res = append(res, r) 531 i++ 532 } 533 } 534 }, 535 ) 536 537 return res, errs 538} 539 540func (st *HelmState) isReleaseInstalled(context helmexec.HelmContext, helm helmexec.Interface, release ReleaseSpec) (bool, error) { 541 out, err := st.listReleases(context, helm, &release) 542 if err != nil { 543 return false, err 544 } else if out != "" { 545 return true, nil 546 } 547 return false, nil 548} 549 550func (st *HelmState) DetectReleasesToBeDeletedForSync(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) { 551 detected := []ReleaseSpec{} 552 for i := range releases { 553 release := releases[i] 554 555 if !release.Desired() { 556 installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release) 557 if err != nil { 558 return nil, err 559 } else if installed { 560 // Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554) 561 r := release 562 detected = append(detected, r) 563 } 564 } 565 } 566 return detected, nil 567} 568 569func (st *HelmState) DetectReleasesToBeDeleted(helm helmexec.Interface, releases []ReleaseSpec) ([]ReleaseSpec, error) { 570 detected := []ReleaseSpec{} 571 for i := range releases { 572 release := releases[i] 573 574 installed, err := st.isReleaseInstalled(st.createHelmContext(&release, 0), helm, release) 575 if err != nil { 576 return nil, err 577 } else if installed { 578 // Otherwise `release` messed up(https://github.com/roboll/helmfile/issues/554) 579 r := release 580 detected = append(detected, r) 581 } 582 } 583 return detected, nil 584} 585 586type SyncOpts struct { 587 Set []string 588 SkipCleanup bool 589 Wait bool 590} 591 592type SyncOpt interface{ Apply(*SyncOpts) } 593 594func (o *SyncOpts) Apply(opts *SyncOpts) { 595 *opts = *o 596} 597 598func ReleaseToID(r *ReleaseSpec) string { 599 var id string 600 601 kc := r.KubeContext 602 if kc != "" { 603 id += kc + "/" 604 } 605 606 tns := r.TillerNamespace 607 if tns != "" { 608 id += tns + "/" 609 } 610 611 ns := r.Namespace 612 if ns != "" { 613 id += ns + "/" 614 } 615 616 id += r.Name 617 618 return id 619} 620 621// DeleteReleasesForSync deletes releases that are marked for deletion 622func (st *HelmState) DeleteReleasesForSync(affectedReleases *AffectedReleases, helm helmexec.Interface, workerLimit int) []error { 623 errs := []error{} 624 625 releases := st.Releases 626 627 jobQueue := make(chan *ReleaseSpec, len(releases)) 628 results := make(chan syncResult, len(releases)) 629 if workerLimit == 0 { 630 workerLimit = len(releases) 631 } 632 633 m := new(sync.Mutex) 634 635 st.scatterGather( 636 workerLimit, 637 len(releases), 638 func() { 639 for i := 0; i < len(releases); i++ { 640 jobQueue <- &releases[i] 641 } 642 close(jobQueue) 643 }, 644 func(workerIndex int) { 645 for release := range jobQueue { 646 var relErr *ReleaseError 647 context := st.createHelmContext(release, workerIndex) 648 649 if _, err := st.triggerPresyncEvent(release, "sync"); err != nil { 650 relErr = newReleaseFailedError(release, err) 651 } else { 652 var args []string 653 if helm.IsHelm3() { 654 args = []string{} 655 if release.Namespace != "" { 656 args = append(args, "--namespace", release.Namespace) 657 } 658 } else { 659 args = []string{"--purge"} 660 } 661 deletionFlags := st.appendConnectionFlags(args, helm, release) 662 m.Lock() 663 if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil { 664 affectedReleases.Failed = append(affectedReleases.Failed, release) 665 relErr = newReleaseFailedError(release, err) 666 } else if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil { 667 affectedReleases.Failed = append(affectedReleases.Failed, release) 668 relErr = newReleaseFailedError(release, err) 669 } else if _, err := st.triggerReleaseEvent("postuninstall", nil, release, "sync"); err != nil { 670 affectedReleases.Failed = append(affectedReleases.Failed, release) 671 relErr = newReleaseFailedError(release, err) 672 } else { 673 affectedReleases.Deleted = append(affectedReleases.Deleted, release) 674 } 675 m.Unlock() 676 } 677 678 if _, err := st.triggerPostsyncEvent(release, relErr, "sync"); err != nil { 679 st.logger.Warnf("warn: %v\n", err) 680 } 681 682 if _, err := st.TriggerCleanupEvent(release, "sync"); err != nil { 683 st.logger.Warnf("warn: %v\n", err) 684 } 685 686 if relErr == nil { 687 results <- syncResult{} 688 } else { 689 results <- syncResult{errors: []*ReleaseError{relErr}} 690 } 691 } 692 }, 693 func() { 694 for i := 0; i < len(releases); { 695 select { 696 case res := <-results: 697 if len(res.errors) > 0 { 698 for _, e := range res.errors { 699 errs = append(errs, e) 700 } 701 } 702 } 703 i++ 704 } 705 }, 706 ) 707 if len(errs) > 0 { 708 return errs 709 } 710 return nil 711} 712 713// SyncReleases wrapper for executing helm upgrade on the releases 714func (st *HelmState) SyncReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, additionalValues []string, workerLimit int, opt ...SyncOpt) []error { 715 opts := &SyncOpts{} 716 for _, o := range opt { 717 o.Apply(opts) 718 } 719 720 preps, prepErrs := st.prepareSyncReleases(helm, additionalValues, workerLimit, opts) 721 722 defer func() { 723 if opts.SkipCleanup { 724 return 725 } 726 727 for _, p := range preps { 728 st.removeFiles(p.files) 729 } 730 }() 731 732 if len(prepErrs) > 0 { 733 return prepErrs 734 } 735 736 errs := []error{} 737 jobQueue := make(chan *syncPrepareResult, len(preps)) 738 results := make(chan syncResult, len(preps)) 739 if workerLimit == 0 { 740 workerLimit = len(preps) 741 } 742 743 m := new(sync.Mutex) 744 745 st.scatterGather( 746 workerLimit, 747 len(preps), 748 func() { 749 for i := 0; i < len(preps); i++ { 750 jobQueue <- &preps[i] 751 } 752 close(jobQueue) 753 }, 754 func(workerIndex int) { 755 for prep := range jobQueue { 756 release := prep.release 757 flags := prep.flags 758 chart := normalizeChart(st.basePath, release.Chart) 759 var relErr *ReleaseError 760 context := st.createHelmContext(release, workerIndex) 761 762 if _, err := st.triggerPresyncEvent(release, "sync"); err != nil { 763 relErr = newReleaseFailedError(release, err) 764 } else if !release.Desired() { 765 installed, err := st.isReleaseInstalled(context, helm, *release) 766 if err != nil { 767 relErr = newReleaseFailedError(release, err) 768 } else if installed { 769 var args []string 770 if helm.IsHelm3() { 771 args = []string{} 772 } else { 773 args = []string{"--purge"} 774 } 775 deletionFlags := st.appendConnectionFlags(args, helm, release) 776 m.Lock() 777 if _, err := st.triggerReleaseEvent("preuninstall", nil, release, "sync"); err != nil { 778 affectedReleases.Failed = append(affectedReleases.Failed, release) 779 relErr = newReleaseFailedError(release, err) 780 } else if err := helm.DeleteRelease(context, release.Name, deletionFlags...); err != nil { 781 affectedReleases.Failed = append(affectedReleases.Failed, release) 782 relErr = newReleaseFailedError(release, err) 783 } else if _, err := st.triggerReleaseEvent("postuninstall", nil, release, "sync"); err != nil { 784 affectedReleases.Failed = append(affectedReleases.Failed, release) 785 relErr = newReleaseFailedError(release, err) 786 } else { 787 affectedReleases.Deleted = append(affectedReleases.Deleted, release) 788 } 789 m.Unlock() 790 } 791 } else if err := helm.SyncRelease(context, release.Name, chart, flags...); err != nil { 792 m.Lock() 793 affectedReleases.Failed = append(affectedReleases.Failed, release) 794 m.Unlock() 795 relErr = newReleaseFailedError(release, err) 796 } else { 797 m.Lock() 798 affectedReleases.Upgraded = append(affectedReleases.Upgraded, release) 799 m.Unlock() 800 installedVersion, err := st.getDeployedVersion(context, helm, release) 801 if err != nil { //err is not really impacting so just log it 802 st.logger.Debugf("getting deployed release version failed:%v", err) 803 } else { 804 release.installedVersion = installedVersion 805 } 806 } 807 808 if _, err := st.triggerPostsyncEvent(release, relErr, "sync"); err != nil { 809 st.logger.Warnf("warn: %v\n", err) 810 } 811 812 if _, err := st.TriggerCleanupEvent(release, "sync"); err != nil { 813 st.logger.Warnf("warn: %v\n", err) 814 } 815 816 if relErr == nil { 817 results <- syncResult{} 818 } else { 819 results <- syncResult{errors: []*ReleaseError{relErr}} 820 } 821 } 822 }, 823 func() { 824 for i := 0; i < len(preps); { 825 select { 826 case res := <-results: 827 if len(res.errors) > 0 { 828 for _, e := range res.errors { 829 errs = append(errs, e) 830 } 831 } 832 } 833 i++ 834 } 835 }, 836 ) 837 if len(errs) > 0 { 838 return errs 839 } 840 return nil 841} 842 843func (st *HelmState) listReleases(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) { 844 flags := st.connectionFlags(helm, release) 845 if helm.IsHelm3() && release.Namespace != "" { 846 flags = append(flags, "--namespace", release.Namespace) 847 } 848 flags = append(flags, "--deployed", "--failed", "--pending") 849 return helm.List(context, "^"+release.Name+"$", flags...) 850} 851 852func (st *HelmState) getDeployedVersion(context helmexec.HelmContext, helm helmexec.Interface, release *ReleaseSpec) (string, error) { 853 //retrieve the version 854 if out, err := st.listReleases(context, helm, release); err == nil { 855 chartName := filepath.Base(release.Chart) 856 //the regexp without escapes : .*\s.*\s.*\s.*\schartName-(.*?)\s 857 pat := regexp.MustCompile(".*\\s.*\\s.*\\s.*\\s" + chartName + "-(.*?)\\s") 858 versions := pat.FindStringSubmatch(out) 859 if len(versions) > 0 { 860 return versions[1], nil 861 } else { 862 //fails to find the version 863 return "failed to get version", errors.New("Failed to get the version for:" + chartName) 864 } 865 } else { 866 return "failed to get version", err 867 } 868} 869 870func releasesNeedCharts(releases []ReleaseSpec) []ReleaseSpec { 871 var result []ReleaseSpec 872 873 for _, r := range releases { 874 if r.Installed != nil && !*r.Installed { 875 continue 876 } 877 result = append(result, r) 878 } 879 880 return result 881} 882 883type ChartPrepareOptions struct { 884 ForceDownload bool 885 SkipRepos bool 886 SkipDeps bool 887 SkipResolve bool 888 Wait bool 889} 890 891type chartPrepareResult struct { 892 releaseName string 893 releaseNamespace string 894 releaseContext string 895 chartName string 896 chartPath string 897 err error 898 buildDeps bool 899 chartFetchedByGoGetter bool 900} 901 902func (st *HelmState) GetRepositoryAndNameFromChartName(chartName string) (*RepositorySpec, string) { 903 chart := strings.Split(chartName, "/") 904 if len(chart) == 1 { 905 return nil, chartName 906 } 907 repo := chart[0] 908 for _, r := range st.Repositories { 909 if r.Name == repo { 910 return &r, strings.Join(chart[1:], "/") 911 } 912 } 913 return nil, chartName 914} 915 916type PrepareChartKey struct { 917 Namespace, Name, KubeContext string 918} 919 920// PrepareCharts creates temporary directories of charts. 921// 922// Each resulting "chart" can be one of the followings: 923// 924// (1) local chart 925// (2) temporary local chart generated from kustomization or manifests 926// (3) remote chart 927// 928// When running `helmfile template` on helm v2, or `helmfile lint` on both helm v2 and v3, 929// PrepareCharts will download and untar charts for linting and templating. 930// 931// Otheriwse, if a chart is not a helm chart, it will call "chartify" to turn it into a chart. 932// 933// If exists, it will also patch resources by json patches, strategic-merge patches, and injectors. 934func (st *HelmState) PrepareCharts(helm helmexec.Interface, dir string, concurrency int, helmfileCommand string, opts ChartPrepareOptions) (map[PrepareChartKey]string, []error) { 935 var selected []ReleaseSpec 936 937 if len(st.Selectors) > 0 { 938 var err error 939 940 // This and releasesNeedCharts ensures that we run operations like helm-dep-build and prepare-hook calls only on 941 // releases that are (1) selected by the selectors and (2) to be installed. 942 selected, err = st.GetSelectedReleasesWithOverrides() 943 if err != nil { 944 return nil, []error{err} 945 } 946 } else { 947 selected = st.Releases 948 } 949 950 releases := releasesNeedCharts(selected) 951 952 temp := make(map[PrepareChartKey]string, len(releases)) 953 954 errs := []error{} 955 956 jobQueue := make(chan *ReleaseSpec, len(releases)) 957 results := make(chan *chartPrepareResult, len(releases)) 958 959 var helm3 bool 960 961 if helm != nil { 962 helm3 = helm.IsHelm3() 963 } 964 965 if !opts.SkipResolve { 966 updated, err := st.ResolveDeps() 967 if err != nil { 968 return nil, []error{err} 969 } 970 *st = *updated 971 } 972 973 var builds []*chartPrepareResult 974 pullChan := make(chan PullCommand) 975 defer func() { 976 close(pullChan) 977 }() 978 go st.pullChartWorker(pullChan, helm) 979 980 st.scatterGather( 981 concurrency, 982 len(releases), 983 func() { 984 for i := 0; i < len(releases); i++ { 985 jobQueue <- &releases[i] 986 } 987 close(jobQueue) 988 }, 989 func(workerIndex int) { 990 for release := range jobQueue { 991 // Call user-defined `prepare` hooks to create/modify local charts to be used by 992 // the later process. 993 // 994 // If it wasn't called here, Helmfile can end up an issue like 995 // https://github.com/roboll/helmfile/issues/1328 996 if _, err := st.triggerPrepareEvent(release, helmfileCommand); err != nil { 997 results <- &chartPrepareResult{err: err} 998 return 999 } 1000 1001 chartName := release.Chart 1002 1003 chartPath, err := st.downloadChartWithGoGetter(release) 1004 if err != nil { 1005 results <- &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)} 1006 return 1007 } 1008 chartFetchedByGoGetter := chartPath != chartName 1009 1010 if !chartFetchedByGoGetter { 1011 ociChartPath, err := st.getOCIChart(pullChan, release, dir, helm) 1012 if err != nil { 1013 results <- &chartPrepareResult{err: fmt.Errorf("release %q: %w", release.Name, err)} 1014 1015 return 1016 } 1017 1018 if ociChartPath != nil { 1019 chartPath = *ociChartPath 1020 } 1021 } 1022 1023 isLocal := st.directoryExistsAt(normalizeChart(st.basePath, chartName)) 1024 1025 chartification, clean, err := st.PrepareChartify(helm, release, chartPath, workerIndex) 1026 defer clean() 1027 if err != nil { 1028 results <- &chartPrepareResult{err: err} 1029 return 1030 } 1031 1032 var buildDeps bool 1033 1034 skipDepsGlobal := opts.SkipDeps 1035 skipDepsRelease := release.SkipDeps != nil && *release.SkipDeps 1036 skipDepsDefault := release.SkipDeps == nil && st.HelmDefaults.SkipDeps 1037 skipDeps := !isLocal || skipDepsGlobal || skipDepsRelease || skipDepsDefault 1038 1039 if chartification != nil { 1040 c := chartify.New( 1041 chartify.HelmBin(st.DefaultHelmBinary), 1042 chartify.UseHelm3(helm3), 1043 ) 1044 1045 chartifyOpts := chartification.Opts 1046 1047 if skipDeps { 1048 chartifyOpts.SkipDeps = true 1049 } 1050 1051 out, err := c.Chartify(release.Name, chartPath, chartify.WithChartifyOpts(chartifyOpts)) 1052 if err != nil { 1053 results <- &chartPrepareResult{err: err} 1054 return 1055 } else { 1056 chartPath = out 1057 } 1058 1059 // Skip `helm dep build` and `helm dep up` altogether when the chart is from remote or the dep is 1060 // explicitly skipped. 1061 buildDeps = !skipDeps 1062 } else if normalizedChart := normalizeChart(st.basePath, chartPath); st.directoryExistsAt(normalizedChart) { 1063 // At this point, we are sure that chartPath is a local directory containing either: 1064 // - A remote chart fetched by go-getter or 1065 // - A local chart 1066 // 1067 // The chart may have Chart.yaml(and requirements.yaml for Helm 2), and optionally Chart.lock/requirements.lock, 1068 // but no `charts/` directory populated at all, or a subet of chart dependencies are missing in the directory. 1069 // 1070 // In such situation, Helm fails with an error like: 1071 // Error: found in Chart.yaml, but missing in charts/ directory: cert-manager, prometheus, postgresql, gitlab-runner, grafana, redis 1072 // 1073 // (See also https://github.com/roboll/helmfile/issues/1401#issuecomment-670854495) 1074 // 1075 // To avoid it, we need to call a `helm dep build` command on the chart. 1076 // But the command may consistently fail when an outdated Chart.lock exists. 1077 // 1078 // (I've mentioned about such case in https://github.com/roboll/helmfile/pull/1400.) 1079 // 1080 // Trying to run `helm dep build` on the chart regardless of if it's from local or remote is 1081 // problematic, as usually the user would have no way to fix the remote chart on their own. 1082 // 1083 // Given that, we always run `helm dep build` on the chart here, but tolerate any error caused by it 1084 // for a remote chart, so that the user can notice/fix the issue in a local chart while 1085 // a broken remote chart won't completely block their job. 1086 chartPath = normalizedChart 1087 1088 buildDeps = !skipDeps 1089 } else if !opts.ForceDownload { 1090 // At this point, we are sure that either: 1091 // 1. It is a local chart and we can use it in later process (helm upgrade/template/lint/etc) 1092 // without any modification, or 1093 // 2. It is a remote chart which can be safely handed over to helm, 1094 // because the version of Helm used in this transaction (helm v3 or greater) support downloading 1095 // the chart instead, AND we don't need any modification to the chart 1096 // 1097 // Also see HelmState.chartVersionFlags(). For `helmfile template`, it's called before `helm template` 1098 // only on helm v3. 1099 // For helm 2, we `helm fetch` with the version flags and call `helm template` 1100 // WITHOUT the version flags. 1101 } else { 1102 pathElems := []string{ 1103 dir, 1104 } 1105 1106 if release.TillerNamespace != "" { 1107 pathElems = append(pathElems, release.TillerNamespace) 1108 } 1109 1110 if release.Namespace != "" { 1111 pathElems = append(pathElems, release.Namespace) 1112 } 1113 1114 if release.KubeContext != "" { 1115 pathElems = append(pathElems, release.KubeContext) 1116 } 1117 1118 chartVersion := "latest" 1119 if release.Version != "" { 1120 chartVersion = release.Version 1121 } 1122 1123 pathElems = append(pathElems, release.Name, chartName, chartVersion) 1124 1125 chartPath = path.Join(pathElems...) 1126 1127 // only fetch chart if it is not already fetched 1128 if _, err := os.Stat(chartPath); os.IsNotExist(err) { 1129 fetchFlags := st.chartVersionFlags(release) 1130 fetchFlags = append(fetchFlags, "--untar", "--untardir", chartPath) 1131 if err := helm.Fetch(chartName, fetchFlags...); err != nil { 1132 results <- &chartPrepareResult{err: err} 1133 return 1134 } 1135 } 1136 1137 // Set chartPath to be the path containing Chart.yaml, if found 1138 fullChartPath, err := findChartDirectory(chartPath) 1139 if err == nil { 1140 chartPath = filepath.Dir(fullChartPath) 1141 } 1142 } 1143 1144 results <- &chartPrepareResult{ 1145 releaseName: release.Name, 1146 chartName: chartName, 1147 releaseNamespace: release.Namespace, 1148 releaseContext: release.KubeContext, 1149 chartPath: chartPath, 1150 buildDeps: buildDeps, 1151 chartFetchedByGoGetter: chartFetchedByGoGetter, 1152 } 1153 } 1154 }, 1155 func() { 1156 for i := 0; i < len(releases); i++ { 1157 downloadRes := <-results 1158 1159 if downloadRes.err != nil { 1160 errs = append(errs, downloadRes.err) 1161 1162 return 1163 } 1164 temp[PrepareChartKey{ 1165 Namespace: downloadRes.releaseNamespace, 1166 KubeContext: downloadRes.releaseContext, 1167 Name: downloadRes.releaseName, 1168 }] = downloadRes.chartPath 1169 1170 if downloadRes.buildDeps { 1171 builds = append(builds, downloadRes) 1172 } 1173 } 1174 }, 1175 ) 1176 1177 if len(errs) > 0 { 1178 return nil, errs 1179 } 1180 1181 if len(builds) > 0 { 1182 if err := st.runHelmDepBuilds(helm, concurrency, builds); err != nil { 1183 return nil, []error{err} 1184 } 1185 } 1186 1187 return temp, nil 1188} 1189 1190func (st *HelmState) runHelmDepBuilds(helm helmexec.Interface, concurrency int, builds []*chartPrepareResult) error { 1191 // NOTES: 1192 // 1. `helm dep build` fails when it was run concurrency on the same chart. 1193 // To avoid that, we run `helm dep build` only once per each local chart. 1194 // 1195 // See https://github.com/roboll/helmfile/issues/1438 1196 // 2. Even if it isn't on the same local chart, `helm dep build` intermittently fails when run concurrentl 1197 // So we shouldn't use goroutines like we do for other helm operations here. 1198 // 1199 // See https://github.com/roboll/helmfile/issues/1521 1200 for _, r := range builds { 1201 if err := helm.BuildDeps(r.releaseName, r.chartPath); err != nil { 1202 if r.chartFetchedByGoGetter { 1203 diagnostic := fmt.Sprintf( 1204 "WARN: `helm dep build` failed. While processing release %q, Helmfile observed that remote chart %q fetched by go-getter is seemingly broken. "+ 1205 "One of well-known causes of this is that the chart has outdated Chart.lock, which needs the chart maintainer needs to run `helm dep up`. "+ 1206 "Helmfile is tolerating the error to avoid blocking you until the remote chart gets fixed. "+ 1207 "But this may result in any failure later if the chart is broken badly. FYI, the tolerated error was: %v", 1208 r.releaseName, 1209 r.chartName, 1210 err.Error(), 1211 ) 1212 1213 st.logger.Warn(diagnostic) 1214 1215 continue 1216 } 1217 1218 return fmt.Errorf("building dependencies of local chart: %w", err) 1219 } 1220 } 1221 1222 return nil 1223} 1224 1225type TemplateOpts struct { 1226 Set []string 1227 SkipCleanup bool 1228 OutputDirTemplate string 1229 IncludeCRDs bool 1230} 1231 1232type TemplateOpt interface{ Apply(*TemplateOpts) } 1233 1234func (o *TemplateOpts) Apply(opts *TemplateOpts) { 1235 *opts = *o 1236} 1237 1238// TemplateReleases wrapper for executing helm template on the releases 1239func (st *HelmState) TemplateReleases(helm helmexec.Interface, outputDir string, additionalValues []string, args []string, workerLimit int, 1240 validate bool, opt ...TemplateOpt) []error { 1241 1242 opts := &TemplateOpts{} 1243 for _, o := range opt { 1244 o.Apply(opts) 1245 } 1246 1247 errs := []error{} 1248 1249 for i := range st.Releases { 1250 release := &st.Releases[i] 1251 1252 if !release.Desired() { 1253 continue 1254 } 1255 1256 st.ApplyOverrides(release) 1257 1258 flags, files, err := st.flagsForTemplate(helm, release, 0) 1259 1260 defer func() { 1261 if opts.SkipCleanup { 1262 return 1263 } 1264 1265 st.removeFiles(files) 1266 }() 1267 1268 if err != nil { 1269 errs = append(errs, err) 1270 } 1271 1272 for _, value := range additionalValues { 1273 valfile, err := filepath.Abs(value) 1274 if err != nil { 1275 errs = append(errs, err) 1276 } 1277 1278 if _, err := os.Stat(valfile); os.IsNotExist(err) { 1279 errs = append(errs, err) 1280 } 1281 flags = append(flags, "--values", valfile) 1282 } 1283 1284 if opts.Set != nil { 1285 for _, s := range opts.Set { 1286 flags = append(flags, "--set", s) 1287 } 1288 } 1289 1290 if len(outputDir) > 0 || len(opts.OutputDirTemplate) > 0 { 1291 releaseOutputDir, err := st.GenerateOutputDir(outputDir, release, opts.OutputDirTemplate) 1292 if err != nil { 1293 errs = append(errs, err) 1294 } 1295 1296 flags = append(flags, "--output-dir", releaseOutputDir) 1297 st.logger.Debugf("Generating templates to : %s\n", releaseOutputDir) 1298 err = os.MkdirAll(releaseOutputDir, 0755) 1299 if err != nil { 1300 errs = append(errs, err) 1301 } 1302 } 1303 1304 if validate { 1305 flags = append(flags, "--validate") 1306 } 1307 1308 if opts.IncludeCRDs { 1309 flags = append(flags, "--include-crds") 1310 } 1311 1312 if len(errs) == 0 { 1313 if err := helm.TemplateRelease(release.Name, release.Chart, flags...); err != nil { 1314 errs = append(errs, err) 1315 } 1316 } 1317 1318 if _, err := st.TriggerCleanupEvent(release, "template"); err != nil { 1319 st.logger.Warnf("warn: %v\n", err) 1320 } 1321 } 1322 1323 if len(errs) != 0 { 1324 return errs 1325 } 1326 1327 return nil 1328} 1329 1330type WriteValuesOpts struct { 1331 Set []string 1332 OutputFileTemplate string 1333} 1334 1335type WriteValuesOpt interface{ Apply(*WriteValuesOpts) } 1336 1337func (o *WriteValuesOpts) Apply(opts *WriteValuesOpts) { 1338 *opts = *o 1339} 1340 1341// WriteReleasesValues writes values files for releases 1342func (st *HelmState) WriteReleasesValues(helm helmexec.Interface, additionalValues []string, opt ...WriteValuesOpt) []error { 1343 opts := &WriteValuesOpts{} 1344 for _, o := range opt { 1345 o.Apply(opts) 1346 } 1347 1348 for i := range st.Releases { 1349 release := &st.Releases[i] 1350 1351 if !release.Desired() { 1352 continue 1353 } 1354 1355 st.ApplyOverrides(release) 1356 1357 generatedFiles, err := st.generateValuesFiles(helm, release, i) 1358 if err != nil { 1359 return []error{err} 1360 } 1361 1362 defer func() { 1363 st.removeFiles(generatedFiles) 1364 }() 1365 1366 for _, value := range additionalValues { 1367 valfile, err := filepath.Abs(value) 1368 if err != nil { 1369 return []error{err} 1370 } 1371 1372 if _, err := os.Stat(valfile); os.IsNotExist(err) { 1373 return []error{err} 1374 } 1375 generatedFiles = append(generatedFiles, valfile) 1376 } 1377 1378 outputValuesFile, err := st.GenerateOutputFilePath(release, opts.OutputFileTemplate) 1379 if err != nil { 1380 return []error{err} 1381 } 1382 1383 if err := os.MkdirAll(filepath.Dir(outputValuesFile), 0755); err != nil { 1384 return []error{err} 1385 } 1386 1387 st.logger.Infof("Writing values file %s", outputValuesFile) 1388 1389 merged := map[string]interface{}{} 1390 1391 for _, f := range generatedFiles { 1392 src := map[string]interface{}{} 1393 1394 srcBytes, err := st.readFile(f) 1395 if err != nil { 1396 return []error{fmt.Errorf("reading %s: %w", f, err)} 1397 } 1398 1399 if err := yaml.Unmarshal(srcBytes, &src); err != nil { 1400 return []error{fmt.Errorf("unmarshalling yaml %s: %w", f, err)} 1401 } 1402 1403 if err := mergo.Merge(&merged, &src, mergo.WithOverride, mergo.WithOverwriteWithEmptyValue); err != nil { 1404 return []error{fmt.Errorf("merging %s: %w", f, err)} 1405 } 1406 } 1407 1408 var buf bytes.Buffer 1409 1410 y := yaml.NewEncoder(&buf) 1411 if err := y.Encode(merged); err != nil { 1412 return []error{err} 1413 } 1414 1415 if err := ioutil.WriteFile(outputValuesFile, buf.Bytes(), 0644); err != nil { 1416 return []error{fmt.Errorf("writing values file %s: %w", outputValuesFile, err)} 1417 } 1418 1419 if _, err := st.TriggerCleanupEvent(release, "write-values"); err != nil { 1420 st.logger.Warnf("warn: %v\n", err) 1421 } 1422 } 1423 1424 return nil 1425} 1426 1427type LintOpts struct { 1428 Set []string 1429} 1430 1431type LintOpt interface{ Apply(*LintOpts) } 1432 1433func (o *LintOpts) Apply(opts *LintOpts) { 1434 *opts = *o 1435} 1436 1437// LintReleases wrapper for executing helm lint on the releases 1438func (st *HelmState) LintReleases(helm helmexec.Interface, additionalValues []string, args []string, workerLimit int, opt ...LintOpt) []error { 1439 opts := &LintOpts{} 1440 for _, o := range opt { 1441 o.Apply(opts) 1442 } 1443 1444 // Reset the extra args if already set, not to break `helm fetch` by adding the args intended for `lint` 1445 helm.SetExtraArgs() 1446 1447 errs := []error{} 1448 1449 if len(args) > 0 { 1450 helm.SetExtraArgs(args...) 1451 } 1452 1453 for i := range st.Releases { 1454 release := st.Releases[i] 1455 1456 if !release.Desired() { 1457 continue 1458 } 1459 1460 flags, files, err := st.flagsForLint(helm, &release, 0) 1461 1462 defer st.removeFiles(files) 1463 1464 if err != nil { 1465 errs = append(errs, err) 1466 } 1467 for _, value := range additionalValues { 1468 valfile, err := filepath.Abs(value) 1469 if err != nil { 1470 errs = append(errs, err) 1471 } 1472 1473 if _, err := os.Stat(valfile); os.IsNotExist(err) { 1474 errs = append(errs, err) 1475 } 1476 flags = append(flags, "--values", valfile) 1477 } 1478 1479 if opts.Set != nil { 1480 for _, s := range opts.Set { 1481 flags = append(flags, "--set", s) 1482 } 1483 } 1484 1485 if len(errs) == 0 { 1486 if err := helm.Lint(release.Name, release.Chart, flags...); err != nil { 1487 errs = append(errs, err) 1488 } 1489 } 1490 1491 if _, err := st.TriggerCleanupEvent(&release, "lint"); err != nil { 1492 st.logger.Warnf("warn: %v\n", err) 1493 } 1494 } 1495 1496 if len(errs) != 0 { 1497 return errs 1498 } 1499 1500 return nil 1501} 1502 1503type diffResult struct { 1504 release *ReleaseSpec 1505 err *ReleaseError 1506 buf *bytes.Buffer 1507} 1508 1509type diffPrepareResult struct { 1510 release *ReleaseSpec 1511 flags []string 1512 errors []*ReleaseError 1513 files []string 1514 upgradeDueToSkippedDiff bool 1515} 1516 1517func (st *HelmState) prepareDiffReleases(helm helmexec.Interface, additionalValues []string, concurrency int, detailedExitCode, includeTests, suppressSecrets bool, opt ...DiffOpt) ([]diffPrepareResult, []error) { 1518 opts := &DiffOpts{} 1519 for _, o := range opt { 1520 o.Apply(opts) 1521 } 1522 1523 mu := &sync.Mutex{} 1524 installedReleases := map[string]bool{} 1525 1526 isInstalled := func(r *ReleaseSpec) bool { 1527 mu.Lock() 1528 defer mu.Unlock() 1529 1530 id := ReleaseToID(r) 1531 1532 if v, ok := installedReleases[id]; ok { 1533 return v 1534 } 1535 1536 v, err := st.isReleaseInstalled(st.createHelmContext(r, 0), helm, *r) 1537 if err != nil { 1538 st.logger.Warnf("confirming if the release is already installed or not: %v", err) 1539 } else { 1540 installedReleases[id] = v 1541 } 1542 1543 return v 1544 } 1545 1546 releases := []*ReleaseSpec{} 1547 for i, _ := range st.Releases { 1548 if !st.Releases[i].Desired() { 1549 continue 1550 } 1551 if st.Releases[i].Installed != nil && !*(st.Releases[i].Installed) { 1552 continue 1553 } 1554 releases = append(releases, &st.Releases[i]) 1555 } 1556 1557 numReleases := len(releases) 1558 jobs := make(chan *ReleaseSpec, numReleases) 1559 results := make(chan diffPrepareResult, numReleases) 1560 resultsMap := map[string]diffPrepareResult{} 1561 1562 rs := []diffPrepareResult{} 1563 errs := []error{} 1564 1565 mut := sync.Mutex{} 1566 1567 st.scatterGather( 1568 concurrency, 1569 numReleases, 1570 func() { 1571 for i := 0; i < numReleases; i++ { 1572 jobs <- releases[i] 1573 } 1574 close(jobs) 1575 }, 1576 func(workerIndex int) { 1577 for release := range jobs { 1578 errs := []error{} 1579 1580 st.ApplyOverrides(release) 1581 1582 if opts.SkipDiffOnInstall && !isInstalled(release) { 1583 results <- diffPrepareResult{release: release, upgradeDueToSkippedDiff: true} 1584 continue 1585 } 1586 1587 disableValidation := release.DisableValidationOnInstall != nil && *release.DisableValidationOnInstall && !isInstalled(release) 1588 1589 // TODO We need a long-term fix for this :) 1590 // See https://github.com/roboll/helmfile/issues/737 1591 mut.Lock() 1592 flags, files, err := st.flagsForDiff(helm, release, disableValidation, workerIndex) 1593 mut.Unlock() 1594 if err != nil { 1595 errs = append(errs, err) 1596 } 1597 1598 for _, value := range additionalValues { 1599 valfile, err := filepath.Abs(value) 1600 if err != nil { 1601 errs = append(errs, err) 1602 } 1603 1604 if _, err := os.Stat(valfile); os.IsNotExist(err) { 1605 errs = append(errs, err) 1606 } 1607 flags = append(flags, "--values", valfile) 1608 } 1609 1610 if detailedExitCode { 1611 flags = append(flags, "--detailed-exitcode") 1612 } 1613 1614 if includeTests { 1615 flags = append(flags, "--include-tests") 1616 } 1617 1618 if suppressSecrets { 1619 flags = append(flags, "--suppress-secrets") 1620 } 1621 1622 if opts.NoColor { 1623 flags = append(flags, "--no-color") 1624 } 1625 1626 if opts.Context > 0 { 1627 flags = append(flags, "--context", fmt.Sprintf("%d", opts.Context)) 1628 } 1629 1630 if opts.Set != nil { 1631 for _, s := range opts.Set { 1632 flags = append(flags, "--set", s) 1633 } 1634 } 1635 1636 if len(errs) > 0 { 1637 rsErrs := make([]*ReleaseError, len(errs)) 1638 for i, e := range errs { 1639 rsErrs[i] = newReleaseFailedError(release, e) 1640 } 1641 results <- diffPrepareResult{errors: rsErrs, files: files} 1642 } else { 1643 results <- diffPrepareResult{release: release, flags: flags, errors: []*ReleaseError{}, files: files} 1644 } 1645 } 1646 }, 1647 func() { 1648 for i := 0; i < numReleases; i++ { 1649 res := <-results 1650 if res.errors != nil && len(res.errors) > 0 { 1651 for _, e := range res.errors { 1652 errs = append(errs, e) 1653 } 1654 } else if res.release != nil { 1655 resultsMap[ReleaseToID(res.release)] = res 1656 } 1657 } 1658 }, 1659 ) 1660 1661 for _, r := range releases { 1662 if p, ok := resultsMap[ReleaseToID(r)]; ok { 1663 rs = append(rs, p) 1664 } 1665 } 1666 1667 return rs, errs 1668} 1669 1670func (st *HelmState) createHelmContext(spec *ReleaseSpec, workerIndex int) helmexec.HelmContext { 1671 namespace := st.HelmDefaults.TillerNamespace 1672 if spec.TillerNamespace != "" { 1673 namespace = spec.TillerNamespace 1674 } 1675 tillerless := st.HelmDefaults.Tillerless 1676 if spec.Tillerless != nil { 1677 tillerless = *spec.Tillerless 1678 } 1679 historyMax := 10 1680 if st.HelmDefaults.HistoryMax != nil { 1681 historyMax = *st.HelmDefaults.HistoryMax 1682 } 1683 if spec.HistoryMax != nil { 1684 historyMax = *spec.HistoryMax 1685 } 1686 1687 return helmexec.HelmContext{ 1688 Tillerless: tillerless, 1689 TillerNamespace: namespace, 1690 WorkerIndex: workerIndex, 1691 HistoryMax: historyMax, 1692 } 1693} 1694 1695func (st *HelmState) createHelmContextWithWriter(spec *ReleaseSpec, w io.Writer) helmexec.HelmContext { 1696 ctx := st.createHelmContext(spec, 0) 1697 1698 ctx.Writer = w 1699 1700 return ctx 1701} 1702 1703type DiffOpts struct { 1704 Context int 1705 NoColor bool 1706 Set []string 1707 1708 SkipCleanup bool 1709 1710 SkipDiffOnInstall bool 1711} 1712 1713func (o *DiffOpts) Apply(opts *DiffOpts) { 1714 *opts = *o 1715} 1716 1717type DiffOpt interface{ Apply(*DiffOpts) } 1718 1719// DiffReleases wrapper for executing helm diff on the releases 1720// It returns releases that had any changes, and errors if any. 1721// 1722// This function has responsibility to stabilize the order of writes to stdout from multiple concurrent helm-diff runs. 1723// It's required to use the stdout from helmfile-diff to detect if there was another change(s) between 2 points in time. 1724// For example, terraform-provider-helmfile runs a helmfile-diff on `terraform plan` and another on `terraform apply`. 1725// `terraform`, by design, fails when helmfile-diff outputs were not equivalent. 1726// Stabilized helmfile-diff output rescues that. 1727func (st *HelmState) DiffReleases(helm helmexec.Interface, additionalValues []string, workerLimit int, detailedExitCode, includeTests, suppressSecrets, suppressDiff, triggerCleanupEvents bool, opt ...DiffOpt) ([]ReleaseSpec, []error) { 1728 opts := &DiffOpts{} 1729 for _, o := range opt { 1730 o.Apply(opts) 1731 } 1732 1733 preps, prepErrs := st.prepareDiffReleases(helm, additionalValues, workerLimit, detailedExitCode, includeTests, suppressSecrets, opts) 1734 1735 defer func() { 1736 if opts.SkipCleanup { 1737 return 1738 } 1739 1740 for _, p := range preps { 1741 st.removeFiles(p.files) 1742 } 1743 }() 1744 1745 if len(prepErrs) > 0 { 1746 return []ReleaseSpec{}, prepErrs 1747 } 1748 1749 jobQueue := make(chan *diffPrepareResult, len(preps)) 1750 results := make(chan diffResult, len(preps)) 1751 1752 rs := []ReleaseSpec{} 1753 outputs := map[string]*bytes.Buffer{} 1754 errs := []error{} 1755 1756 // The exit code returned by helm-diff when it detected any changes 1757 HelmDiffExitCodeChanged := 2 1758 1759 st.scatterGather( 1760 workerLimit, 1761 len(preps), 1762 func() { 1763 for i := 0; i < len(preps); i++ { 1764 jobQueue <- &preps[i] 1765 } 1766 close(jobQueue) 1767 }, 1768 func(workerIndex int) { 1769 for prep := range jobQueue { 1770 flags := prep.flags 1771 release := prep.release 1772 buf := &bytes.Buffer{} 1773 if prep.upgradeDueToSkippedDiff { 1774 results <- diffResult{release, &ReleaseError{ReleaseSpec: release, err: nil, Code: HelmDiffExitCodeChanged}, buf} 1775 } else if err := helm.DiffRelease(st.createHelmContextWithWriter(release, buf), release.Name, normalizeChart(st.basePath, release.Chart), suppressDiff, flags...); err != nil { 1776 switch e := err.(type) { 1777 case helmexec.ExitError: 1778 // Propagate any non-zero exit status from the external command like `helm` that is failed under the hood 1779 results <- diffResult{release, &ReleaseError{release, err, e.ExitStatus()}, buf} 1780 default: 1781 results <- diffResult{release, &ReleaseError{release, err, 0}, buf} 1782 } 1783 } else { 1784 // diff succeeded, found no changes 1785 results <- diffResult{release, nil, buf} 1786 } 1787 1788 if triggerCleanupEvents { 1789 if _, err := st.TriggerCleanupEvent(prep.release, "diff"); err != nil { 1790 st.logger.Warnf("warn: %v\n", err) 1791 } 1792 } 1793 } 1794 }, 1795 func() { 1796 for i := 0; i < len(preps); i++ { 1797 res := <-results 1798 if res.err != nil { 1799 errs = append(errs, res.err) 1800 if res.err.Code == HelmDiffExitCodeChanged { 1801 rs = append(rs, *res.err.ReleaseSpec) 1802 } 1803 } 1804 1805 outputs[ReleaseToID(res.release)] = res.buf 1806 } 1807 }, 1808 ) 1809 1810 for _, p := range preps { 1811 if stdout, ok := outputs[ReleaseToID(p.release)]; ok { 1812 fmt.Print(stdout.String()) 1813 } 1814 } 1815 1816 return rs, errs 1817} 1818 1819func (st *HelmState) ReleaseStatuses(helm helmexec.Interface, workerLimit int) []error { 1820 return st.scatterGatherReleases(helm, workerLimit, func(release ReleaseSpec, workerIndex int) error { 1821 if !release.Desired() { 1822 return nil 1823 } 1824 1825 st.ApplyOverrides(&release) 1826 1827 flags := []string{} 1828 if helm.IsHelm3() && release.Namespace != "" { 1829 flags = append(flags, "--namespace", release.Namespace) 1830 } 1831 flags = st.appendConnectionFlags(flags, helm, &release) 1832 1833 return helm.ReleaseStatus(st.createHelmContext(&release, workerIndex), release.Name, flags...) 1834 }) 1835} 1836 1837// DeleteReleases wrapper for executing helm delete on the releases 1838func (st *HelmState) DeleteReleases(affectedReleases *AffectedReleases, helm helmexec.Interface, concurrency int, purge bool) []error { 1839 return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error { 1840 st.ApplyOverrides(&release) 1841 1842 flags := []string{} 1843 if purge && !helm.IsHelm3() { 1844 flags = append(flags, "--purge") 1845 } 1846 flags = st.appendConnectionFlags(flags, helm, &release) 1847 if helm.IsHelm3() && release.Namespace != "" { 1848 flags = append(flags, "--namespace", release.Namespace) 1849 } 1850 context := st.createHelmContext(&release, workerIndex) 1851 1852 if _, err := st.triggerReleaseEvent("preuninstall", nil, &release, "delete"); err != nil { 1853 affectedReleases.Failed = append(affectedReleases.Failed, &release) 1854 1855 return err 1856 } 1857 1858 if err := helm.DeleteRelease(context, release.Name, flags...); err != nil { 1859 affectedReleases.Failed = append(affectedReleases.Failed, &release) 1860 return err 1861 } 1862 1863 if _, err := st.triggerReleaseEvent("postuninstall", nil, &release, "delete"); err != nil { 1864 affectedReleases.Failed = append(affectedReleases.Failed, &release) 1865 return err 1866 } 1867 1868 affectedReleases.Deleted = append(affectedReleases.Deleted, &release) 1869 return nil 1870 }) 1871} 1872 1873type TestOpts struct { 1874 Logs bool 1875} 1876 1877type TestOption func(*TestOpts) 1878 1879func Logs(v bool) func(*TestOpts) { 1880 return func(o *TestOpts) { 1881 o.Logs = v 1882 } 1883} 1884 1885// TestReleases wrapper for executing helm test on the releases 1886func (st *HelmState) TestReleases(helm helmexec.Interface, cleanup bool, timeout int, concurrency int, options ...TestOption) []error { 1887 var opts TestOpts 1888 1889 for _, o := range options { 1890 o(&opts) 1891 } 1892 1893 return st.scatterGatherReleases(helm, concurrency, func(release ReleaseSpec, workerIndex int) error { 1894 if !release.Desired() { 1895 return nil 1896 } 1897 1898 flags := []string{} 1899 if helm.IsHelm3() && release.Namespace != "" { 1900 flags = append(flags, "--namespace", release.Namespace) 1901 } 1902 if cleanup && !helm.IsHelm3() { 1903 flags = append(flags, "--cleanup") 1904 } 1905 if opts.Logs { 1906 flags = append(flags, "--logs") 1907 } 1908 1909 if timeout == EmptyTimeout { 1910 flags = append(flags, st.timeoutFlags(helm, &release)...) 1911 } else { 1912 duration := strconv.Itoa(timeout) 1913 if helm.IsHelm3() { 1914 duration += "s" 1915 } 1916 flags = append(flags, "--timeout", duration) 1917 } 1918 1919 flags = st.appendConnectionFlags(flags, helm, &release) 1920 1921 return helm.TestRelease(st.createHelmContext(&release, workerIndex), release.Name, flags...) 1922 }) 1923} 1924 1925// Clean will remove any generated secrets 1926func (st *HelmState) Clean() []error { 1927 return nil 1928} 1929 1930func (st *HelmState) GetReleasesWithOverrides() []ReleaseSpec { 1931 var rs []ReleaseSpec 1932 for _, r := range st.Releases { 1933 var spec ReleaseSpec 1934 spec = r 1935 st.ApplyOverrides(&spec) 1936 rs = append(rs, spec) 1937 } 1938 return rs 1939} 1940 1941func (st *HelmState) SelectReleasesWithOverrides() ([]Release, error) { 1942 values := st.Values() 1943 rs, err := markExcludedReleases(st.GetReleasesWithOverrides(), st.Selectors, st.CommonLabels, values) 1944 if err != nil { 1945 return nil, err 1946 } 1947 return rs, nil 1948} 1949 1950func markExcludedReleases(releases []ReleaseSpec, selectors []string, commonLabels map[string]string, values map[string]interface{}) ([]Release, error) { 1951 var filteredReleases []Release 1952 filters := []ReleaseFilter{} 1953 for _, label := range selectors { 1954 f, err := ParseLabels(label) 1955 if err != nil { 1956 return nil, err 1957 } 1958 filters = append(filters, f) 1959 } 1960 for _, r := range releases { 1961 if r.Labels == nil { 1962 r.Labels = map[string]string{} 1963 } 1964 // Let the release name, namespace, and chart be used as a tag 1965 r.Labels["name"] = r.Name 1966 r.Labels["namespace"] = r.Namespace 1967 // Strip off just the last portion for the name stable/newrelic would give newrelic 1968 chartSplit := strings.Split(r.Chart, "/") 1969 r.Labels["chart"] = chartSplit[len(chartSplit)-1] 1970 //Merge CommonLabels into release labels 1971 for k, v := range commonLabels { 1972 r.Labels[k] = v 1973 } 1974 var filterMatch bool 1975 for _, f := range filters { 1976 if r.Labels == nil { 1977 r.Labels = map[string]string{} 1978 } 1979 if f.Match(r) { 1980 filterMatch = true 1981 break 1982 } 1983 } 1984 var conditionMatch bool 1985 if len(r.Condition) > 0 { 1986 conditionSplit := strings.Split(r.Condition, ".") 1987 if len(conditionSplit) != 2 { 1988 return nil, fmt.Errorf("Condition value must be in the form 'foo.enabled' where 'foo' can be modified as necessary") 1989 } 1990 if v, ok := values[conditionSplit[0]]; ok { 1991 if v == nil { 1992 panic(fmt.Sprintf("environment values field '%s' is nil", conditionSplit[0])) 1993 } 1994 if v.(map[string]interface{})["enabled"] == true { 1995 conditionMatch = true 1996 } 1997 } else { 1998 panic(fmt.Sprintf("environment values does not contain field '%s'", conditionSplit[0])) 1999 } 2000 } 2001 res := Release{ 2002 ReleaseSpec: r, 2003 Filtered: (len(filters) > 0 && !filterMatch) || (len(r.Condition) > 0 && !conditionMatch), 2004 } 2005 filteredReleases = append(filteredReleases, res) 2006 } 2007 2008 return filteredReleases, nil 2009} 2010 2011func (st *HelmState) GetSelectedReleasesWithOverrides() ([]ReleaseSpec, error) { 2012 filteredReleases, err := st.SelectReleasesWithOverrides() 2013 if err != nil { 2014 return nil, err 2015 } 2016 var releases []ReleaseSpec 2017 for _, r := range filteredReleases { 2018 if !r.Filtered { 2019 releases = append(releases, r.ReleaseSpec) 2020 } 2021 } 2022 2023 return releases, nil 2024} 2025 2026// FilterReleases allows for the execution of helm commands against a subset of the releases in the helmfile. 2027func (st *HelmState) FilterReleases() error { 2028 releases, err := st.GetSelectedReleasesWithOverrides() 2029 if err != nil { 2030 return err 2031 } 2032 st.Releases = releases 2033 return nil 2034} 2035 2036func (st *HelmState) TriggerGlobalPrepareEvent(helmfileCommand string) (bool, error) { 2037 return st.triggerGlobalReleaseEvent("prepare", nil, helmfileCommand) 2038} 2039 2040func (st *HelmState) TriggerGlobalCleanupEvent(helmfileCommand string) (bool, error) { 2041 return st.triggerGlobalReleaseEvent("cleanup", nil, helmfileCommand) 2042} 2043 2044func (st *HelmState) triggerGlobalReleaseEvent(evt string, evtErr error, helmfileCmd string) (bool, error) { 2045 bus := &event.Bus{ 2046 Hooks: st.Hooks, 2047 StateFilePath: st.FilePath, 2048 BasePath: st.basePath, 2049 Namespace: st.OverrideNamespace, 2050 Env: st.Env, 2051 Logger: st.logger, 2052 ReadFile: st.readFile, 2053 } 2054 data := map[string]interface{}{ 2055 "HelmfileCommand": helmfileCmd, 2056 } 2057 return bus.Trigger(evt, evtErr, data) 2058} 2059 2060func (st *HelmState) triggerPrepareEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { 2061 return st.triggerReleaseEvent("prepare", nil, r, helmfileCommand) 2062} 2063 2064func (st *HelmState) TriggerCleanupEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { 2065 return st.triggerReleaseEvent("cleanup", nil, r, helmfileCommand) 2066} 2067 2068func (st *HelmState) triggerPresyncEvent(r *ReleaseSpec, helmfileCommand string) (bool, error) { 2069 return st.triggerReleaseEvent("presync", nil, r, helmfileCommand) 2070} 2071 2072func (st *HelmState) triggerPostsyncEvent(r *ReleaseSpec, evtErr error, helmfileCommand string) (bool, error) { 2073 return st.triggerReleaseEvent("postsync", evtErr, r, helmfileCommand) 2074} 2075 2076func (st *HelmState) triggerReleaseEvent(evt string, evtErr error, r *ReleaseSpec, helmfileCmd string) (bool, error) { 2077 bus := &event.Bus{ 2078 Hooks: r.Hooks, 2079 StateFilePath: st.FilePath, 2080 BasePath: st.basePath, 2081 Namespace: st.OverrideNamespace, 2082 Env: st.Env, 2083 Logger: st.logger, 2084 ReadFile: st.readFile, 2085 } 2086 vals := st.Values() 2087 data := map[string]interface{}{ 2088 "Values": vals, 2089 "Release": r, 2090 "HelmfileCommand": helmfileCmd, 2091 } 2092 return bus.Trigger(evt, evtErr, data) 2093} 2094 2095// ResolveDeps returns a copy of this helmfile state with the concrete chart version numbers filled in for remote chart dependencies 2096func (st *HelmState) ResolveDeps() (*HelmState, error) { 2097 return st.mergeLockedDependencies() 2098} 2099 2100// UpdateDeps wrapper for updating dependencies on the releases 2101func (st *HelmState) UpdateDeps(helm helmexec.Interface) []error { 2102 var errs []error 2103 2104 for _, release := range st.Releases { 2105 if st.directoryExistsAt(release.Chart) { 2106 if err := helm.UpdateDeps(release.Chart); err != nil { 2107 errs = append(errs, err) 2108 } 2109 } else { 2110 st.logger.Debugf("skipped updating dependencies for remote chart %s", release.Chart) 2111 } 2112 } 2113 2114 if len(errs) == 0 { 2115 tempDir := st.tempDir 2116 if tempDir == nil { 2117 tempDir = ioutil.TempDir 2118 } 2119 _, err := st.updateDependenciesInTempDir(helm, tempDir) 2120 if err != nil { 2121 errs = append(errs, fmt.Errorf("unable to update deps: %v", err)) 2122 } 2123 } 2124 2125 if len(errs) != 0 { 2126 return errs 2127 } 2128 return nil 2129} 2130 2131func chartNameWithoutRepository(chart string) string { 2132 chartSplit := strings.Split(chart, "/") 2133 return chartSplit[len(chartSplit)-1] 2134} 2135 2136// find "Chart.yaml" 2137func findChartDirectory(topLevelDir string) (string, error) { 2138 var files []string 2139 filepath.Walk(topLevelDir, func(path string, f os.FileInfo, err error) error { 2140 if err != nil { 2141 return fmt.Errorf("error walking through %s: %v", path, err) 2142 } 2143 if !f.IsDir() { 2144 r, err := regexp.MatchString("Chart.yaml", f.Name()) 2145 if err == nil && r { 2146 files = append(files, path) 2147 } 2148 } 2149 return nil 2150 }) 2151 // Sort to get the shortest path 2152 sort.Strings(files) 2153 if len(files) > 0 { 2154 first := files[0] 2155 return first, nil 2156 } 2157 2158 return topLevelDir, errors.New("No Chart.yaml found") 2159} 2160 2161// appendConnectionFlags append all the helm command-line flags related to K8s API and Tiller connection including the kubecontext 2162func (st *HelmState) appendConnectionFlags(flags []string, helm helmexec.Interface, release *ReleaseSpec) []string { 2163 adds := st.connectionFlags(helm, release) 2164 for _, a := range adds { 2165 flags = append(flags, a) 2166 } 2167 return flags 2168} 2169 2170func (st *HelmState) connectionFlags(helm helmexec.Interface, release *ReleaseSpec) []string { 2171 flags := []string{} 2172 tillerless := st.HelmDefaults.Tillerless 2173 if release.Tillerless != nil { 2174 tillerless = *release.Tillerless 2175 } 2176 if !tillerless { 2177 if !helm.IsHelm3() { 2178 if release.TillerNamespace != "" { 2179 flags = append(flags, "--tiller-namespace", release.TillerNamespace) 2180 } else if st.HelmDefaults.TillerNamespace != "" { 2181 flags = append(flags, "--tiller-namespace", st.HelmDefaults.TillerNamespace) 2182 } 2183 } 2184 2185 if release.TLS != nil && *release.TLS || release.TLS == nil && st.HelmDefaults.TLS { 2186 flags = append(flags, "--tls") 2187 } 2188 2189 if release.TLSKey != "" { 2190 flags = append(flags, "--tls-key", release.TLSKey) 2191 } else if st.HelmDefaults.TLSKey != "" { 2192 flags = append(flags, "--tls-key", st.HelmDefaults.TLSKey) 2193 } 2194 2195 if release.TLSCert != "" { 2196 flags = append(flags, "--tls-cert", release.TLSCert) 2197 } else if st.HelmDefaults.TLSCert != "" { 2198 flags = append(flags, "--tls-cert", st.HelmDefaults.TLSCert) 2199 } 2200 2201 if release.TLSCACert != "" { 2202 flags = append(flags, "--tls-ca-cert", release.TLSCACert) 2203 } else if st.HelmDefaults.TLSCACert != "" { 2204 flags = append(flags, "--tls-ca-cert", st.HelmDefaults.TLSCACert) 2205 } 2206 2207 if release.KubeContext != "" { 2208 flags = append(flags, "--kube-context", release.KubeContext) 2209 } else if st.HelmDefaults.KubeContext != "" { 2210 flags = append(flags, "--kube-context", st.HelmDefaults.KubeContext) 2211 } 2212 } 2213 2214 return flags 2215} 2216 2217func (st *HelmState) timeoutFlags(helm helmexec.Interface, release *ReleaseSpec) []string { 2218 var flags []string 2219 2220 timeout := st.HelmDefaults.Timeout 2221 if release.Timeout != nil { 2222 timeout = *release.Timeout 2223 } 2224 if timeout != 0 { 2225 duration := strconv.Itoa(timeout) 2226 if helm.IsHelm3() { 2227 duration += "s" 2228 } 2229 flags = append(flags, "--timeout", duration) 2230 } 2231 2232 return flags 2233} 2234 2235func (st *HelmState) flagsForUpgrade(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) { 2236 flags := st.chartVersionFlags(release) 2237 2238 if release.Verify != nil && *release.Verify || release.Verify == nil && st.HelmDefaults.Verify { 2239 flags = append(flags, "--verify") 2240 } 2241 2242 if release.Wait != nil && *release.Wait || release.Wait == nil && st.HelmDefaults.Wait { 2243 flags = append(flags, "--wait") 2244 } 2245 2246 flags = append(flags, st.timeoutFlags(helm, release)...) 2247 2248 if release.Force != nil && *release.Force || release.Force == nil && st.HelmDefaults.Force { 2249 flags = append(flags, "--force") 2250 } 2251 2252 if release.RecreatePods != nil && *release.RecreatePods || release.RecreatePods == nil && st.HelmDefaults.RecreatePods { 2253 flags = append(flags, "--recreate-pods") 2254 } 2255 2256 if release.Atomic != nil && *release.Atomic || release.Atomic == nil && st.HelmDefaults.Atomic { 2257 flags = append(flags, "--atomic") 2258 } 2259 2260 if release.CleanupOnFail != nil && *release.CleanupOnFail || release.CleanupOnFail == nil && st.HelmDefaults.CleanupOnFail { 2261 flags = append(flags, "--cleanup-on-fail") 2262 } 2263 2264 if release.CreateNamespace != nil && *release.CreateNamespace || 2265 release.CreateNamespace == nil && (st.HelmDefaults.CreateNamespace == nil || *st.HelmDefaults.CreateNamespace) { 2266 if helm.IsVersionAtLeast("3.2.0") { 2267 flags = append(flags, "--create-namespace") 2268 } else if release.CreateNamespace != nil || st.HelmDefaults.CreateNamespace != nil { 2269 // createNamespace was set explicitly, but not running supported version of helm - error 2270 return nil, nil, fmt.Errorf("releases[].createNamespace requires Helm 3.2.0 or greater") 2271 } 2272 } 2273 2274 if release.DisableOpenAPIValidation != nil && *release.DisableOpenAPIValidation || 2275 release.DisableOpenAPIValidation == nil && st.HelmDefaults.DisableOpenAPIValidation != nil && *st.HelmDefaults.DisableOpenAPIValidation { 2276 flags = append(flags, "--disable-openapi-validation") 2277 } 2278 2279 flags = st.appendConnectionFlags(flags, helm, release) 2280 2281 var err error 2282 flags, err = st.appendHelmXFlags(flags, release) 2283 if err != nil { 2284 return nil, nil, err 2285 } 2286 2287 common, clean, err := st.namespaceAndValuesFlags(helm, release, workerIndex) 2288 if err != nil { 2289 return nil, clean, err 2290 } 2291 return append(flags, common...), clean, nil 2292} 2293 2294func (st *HelmState) flagsForTemplate(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) { 2295 var flags []string 2296 2297 // `helm template` in helm v2 does not support `--version` flag. So we fetch with the version flag and then template 2298 // without the flag. See PrepareCharts function to see the Helmfile implementation of chart fetching. 2299 // 2300 // `helm template` in helm v3 supports `--version` and it automatically fetches the remote chart to template, 2301 // so we skip fetching on helmfile-side and let helm fetch it. 2302 if helm.IsHelm3() { 2303 flags = st.chartVersionFlags(release) 2304 } 2305 2306 var err error 2307 flags, err = st.appendHelmXFlags(flags, release) 2308 if err != nil { 2309 return nil, nil, err 2310 } 2311 2312 flags = st.appendApiVersionsFlags(flags) 2313 2314 common, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex) 2315 if err != nil { 2316 return nil, files, err 2317 } 2318 return append(flags, common...), files, nil 2319} 2320 2321func (st *HelmState) flagsForDiff(helm helmexec.Interface, release *ReleaseSpec, disableValidation bool, workerIndex int) ([]string, []string, error) { 2322 flags := st.chartVersionFlags(release) 2323 2324 disableOpenAPIValidation := false 2325 if release.DisableOpenAPIValidation != nil { 2326 disableOpenAPIValidation = *release.DisableOpenAPIValidation 2327 } else if st.HelmDefaults.DisableOpenAPIValidation != nil { 2328 disableOpenAPIValidation = *st.HelmDefaults.DisableOpenAPIValidation 2329 } 2330 2331 if disableOpenAPIValidation { 2332 flags = append(flags, "--disable-openapi-validation") 2333 } 2334 2335 if release.DisableValidation != nil { 2336 disableValidation = *release.DisableValidation 2337 } else if st.HelmDefaults.DisableValidation != nil { 2338 disableValidation = *st.HelmDefaults.DisableValidation 2339 } 2340 2341 if disableValidation { 2342 flags = append(flags, "--disable-validation") 2343 } 2344 2345 flags = st.appendConnectionFlags(flags, helm, release) 2346 2347 var err error 2348 flags, err = st.appendHelmXFlags(flags, release) 2349 if err != nil { 2350 return nil, nil, err 2351 } 2352 2353 common, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex) 2354 if err != nil { 2355 return nil, files, err 2356 } 2357 return append(flags, common...), files, nil 2358} 2359 2360func (st *HelmState) chartVersionFlags(release *ReleaseSpec) []string { 2361 flags := []string{} 2362 2363 if release.Version != "" { 2364 flags = append(flags, "--version", release.Version) 2365 } 2366 2367 if st.isDevelopment(release) { 2368 flags = append(flags, "--devel") 2369 } 2370 2371 return flags 2372} 2373 2374func (st *HelmState) appendApiVersionsFlags(flags []string) []string { 2375 for _, a := range st.ApiVersions { 2376 flags = append(flags, "--api-versions", a) 2377 } 2378 return flags 2379} 2380 2381func (st *HelmState) isDevelopment(release *ReleaseSpec) bool { 2382 result := st.HelmDefaults.Devel 2383 if release.Devel != nil { 2384 result = *release.Devel 2385 } 2386 2387 return result 2388} 2389 2390func (st *HelmState) flagsForLint(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) { 2391 flags, files, err := st.namespaceAndValuesFlags(helm, release, workerIndex) 2392 if err != nil { 2393 return nil, files, err 2394 } 2395 2396 flags, err = st.appendHelmXFlags(flags, release) 2397 if err != nil { 2398 return nil, files, err 2399 } 2400 2401 return flags, files, nil 2402} 2403 2404func (st *HelmState) RenderReleaseValuesFileToBytes(release *ReleaseSpec, path string) ([]byte, error) { 2405 vals := st.Values() 2406 templateData := st.createReleaseTemplateData(release, vals) 2407 2408 r := tmpl.NewFileRenderer(st.readFile, filepath.Dir(path), templateData) 2409 rawBytes, err := r.RenderToBytes(path) 2410 if err != nil { 2411 return nil, err 2412 } 2413 2414 // If 'ref+.*' exists in file, run vals against the file 2415 match, err := regexp.Match("ref\\+.*", rawBytes) 2416 if err != nil { 2417 return nil, err 2418 } 2419 2420 if match { 2421 var rawYaml map[string]interface{} 2422 2423 if err := yaml.Unmarshal(rawBytes, &rawYaml); err != nil { 2424 return nil, err 2425 } 2426 2427 parsedYaml, err := st.valsRuntime.Eval(rawYaml) 2428 if err != nil { 2429 return nil, err 2430 } 2431 2432 return yaml.Marshal(parsedYaml) 2433 } 2434 2435 return rawBytes, nil 2436} 2437 2438func (st *HelmState) storage() *Storage { 2439 return &Storage{ 2440 FilePath: st.FilePath, 2441 basePath: st.basePath, 2442 glob: st.glob, 2443 logger: st.logger, 2444 } 2445} 2446 2447func (st *HelmState) ExpandedHelmfiles() ([]SubHelmfileSpec, error) { 2448 helmfiles := []SubHelmfileSpec{} 2449 for _, hf := range st.Helmfiles { 2450 if remote.IsRemote(hf.Path) { 2451 helmfiles = append(helmfiles, hf) 2452 continue 2453 } 2454 2455 matches, err := st.storage().ExpandPaths(hf.Path) 2456 if err != nil { 2457 return nil, err 2458 } 2459 if len(matches) == 0 { 2460 err := fmt.Errorf("no matches for path: %s", hf.Path) 2461 if st.MissingFileHandler == "Error" { 2462 return nil, err 2463 } 2464 st.logger.Warnf("no matches for path: %s", hf.Path) 2465 continue 2466 } 2467 for _, match := range matches { 2468 newHelmfile := hf 2469 newHelmfile.Path = match 2470 helmfiles = append(helmfiles, newHelmfile) 2471 } 2472 } 2473 2474 return helmfiles, nil 2475} 2476 2477func (st *HelmState) removeFiles(files []string) { 2478 for _, f := range files { 2479 if err := st.removeFile(f); err != nil { 2480 st.logger.Warnf("Removing %s: %v", err) 2481 } else { 2482 st.logger.Debugf("Removed %s", f) 2483 } 2484 } 2485} 2486 2487func (st *HelmState) generateTemporaryReleaseValuesFiles(release *ReleaseSpec, values []interface{}, missingFileHandler *string) ([]string, error) { 2488 generatedFiles := []string{} 2489 2490 for _, value := range values { 2491 switch typedValue := value.(type) { 2492 case string: 2493 paths, skip, err := st.storage().resolveFile(missingFileHandler, "values", typedValue) 2494 if err != nil { 2495 return generatedFiles, err 2496 } 2497 if skip { 2498 continue 2499 } 2500 2501 if len(paths) > 1 { 2502 return generatedFiles, fmt.Errorf("glob patterns in release values and secrets is not supported yet. please submit a feature request if necessary") 2503 } 2504 path := paths[0] 2505 2506 yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, path) 2507 if err != nil { 2508 return generatedFiles, fmt.Errorf("failed to render values files \"%s\": %v", typedValue, err) 2509 } 2510 2511 valfile, err := createTempValuesFile(release, yamlBytes) 2512 if err != nil { 2513 return generatedFiles, err 2514 } 2515 defer valfile.Close() 2516 2517 if _, err := valfile.Write(yamlBytes); err != nil { 2518 return generatedFiles, fmt.Errorf("failed to write %s: %v", valfile.Name(), err) 2519 } 2520 2521 st.logger.Debugf("Successfully generated the value file at %s. produced:\n%s", path, string(yamlBytes)) 2522 2523 generatedFiles = append(generatedFiles, valfile.Name()) 2524 case map[interface{}]interface{}, map[string]interface{}: 2525 valfile, err := createTempValuesFile(release, typedValue) 2526 if err != nil { 2527 return generatedFiles, err 2528 } 2529 defer valfile.Close() 2530 2531 encoder := yaml.NewEncoder(valfile) 2532 defer encoder.Close() 2533 2534 if err := encoder.Encode(typedValue); err != nil { 2535 return generatedFiles, err 2536 } 2537 2538 generatedFiles = append(generatedFiles, valfile.Name()) 2539 default: 2540 return generatedFiles, fmt.Errorf("unexpected type of value: value=%v, type=%T", typedValue, typedValue) 2541 } 2542 } 2543 return generatedFiles, nil 2544} 2545 2546func (st *HelmState) generateVanillaValuesFiles(release *ReleaseSpec) ([]string, error) { 2547 values := []interface{}{} 2548 for _, v := range release.Values { 2549 switch typedValue := v.(type) { 2550 case string: 2551 path := st.storage().normalizePath(release.ValuesPathPrefix + typedValue) 2552 values = append(values, path) 2553 default: 2554 values = append(values, v) 2555 } 2556 } 2557 2558 valuesMapSecretsRendered, err := st.valsRuntime.Eval(map[string]interface{}{"values": values}) 2559 if err != nil { 2560 return nil, err 2561 } 2562 2563 valuesSecretsRendered, ok := valuesMapSecretsRendered["values"].([]interface{}) 2564 if !ok { 2565 return nil, fmt.Errorf("Failed to render values in %s for release %s: type %T isn't supported", st.FilePath, release.Name, valuesMapSecretsRendered["values"]) 2566 } 2567 2568 generatedFiles, err := st.generateTemporaryReleaseValuesFiles(release, valuesSecretsRendered, release.MissingFileHandler) 2569 if err != nil { 2570 return nil, err 2571 } 2572 2573 return generatedFiles, nil 2574} 2575 2576func (st *HelmState) generateSecretValuesFiles(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) { 2577 var generatedFiles []string 2578 2579 for _, v := range release.Secrets { 2580 var ( 2581 paths []string 2582 skip bool 2583 err error 2584 ) 2585 2586 switch value := v.(type) { 2587 case string: 2588 paths, skip, err = st.storage().resolveFile(release.MissingFileHandler, "secrets", release.ValuesPathPrefix+value) 2589 if err != nil { 2590 return nil, err 2591 } 2592 default: 2593 bs, err := yaml.Marshal(value) 2594 if err != nil { 2595 return nil, err 2596 } 2597 2598 path, err := ioutil.TempFile(os.TempDir(), "helmfile-embdedded-secrets-*.yaml.enc") 2599 if err != nil { 2600 return nil, err 2601 } 2602 _ = path.Close() 2603 defer func() { 2604 _ = os.Remove(path.Name()) 2605 }() 2606 2607 if err := ioutil.WriteFile(path.Name(), bs, 0644); err != nil { 2608 return nil, err 2609 } 2610 2611 paths = []string{path.Name()} 2612 } 2613 2614 if skip { 2615 continue 2616 } 2617 2618 if len(paths) > 1 { 2619 return nil, fmt.Errorf("glob patterns in release secret file is not supported yet. please submit a feature request if necessary") 2620 } 2621 path := paths[0] 2622 2623 decryptFlags := st.appendConnectionFlags([]string{}, helm, release) 2624 valfile, err := helm.DecryptSecret(st.createHelmContext(release, workerIndex), path, decryptFlags...) 2625 if err != nil { 2626 return nil, err 2627 } 2628 2629 generatedFiles = append(generatedFiles, valfile) 2630 } 2631 2632 return generatedFiles, nil 2633} 2634 2635func (st *HelmState) generateValuesFiles(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, error) { 2636 valuesFiles, err := st.generateVanillaValuesFiles(release) 2637 if err != nil { 2638 return nil, err 2639 } 2640 2641 secretValuesFiles, err := st.generateSecretValuesFiles(helm, release, workerIndex) 2642 if err != nil { 2643 return nil, err 2644 } 2645 2646 files := append(valuesFiles, secretValuesFiles...) 2647 2648 return files, nil 2649} 2650 2651func (st *HelmState) namespaceAndValuesFlags(helm helmexec.Interface, release *ReleaseSpec, workerIndex int) ([]string, []string, error) { 2652 flags := []string{} 2653 if release.Namespace != "" { 2654 flags = append(flags, "--namespace", release.Namespace) 2655 } 2656 2657 var files []string 2658 2659 generatedFiles, err := st.generateValuesFiles(helm, release, workerIndex) 2660 if err != nil { 2661 return nil, files, err 2662 } 2663 2664 files = generatedFiles 2665 2666 for _, f := range generatedFiles { 2667 flags = append(flags, "--values", f) 2668 } 2669 2670 if len(release.SetValues) > 0 { 2671 for _, set := range release.SetValues { 2672 if set.Value != "" { 2673 renderedValue, err := renderValsSecrets(st.valsRuntime, set.Value) 2674 if err != nil { 2675 return nil, files, fmt.Errorf("Failed to render set value entry in %s for release %s: %v", st.FilePath, release.Name, err) 2676 } 2677 flags = append(flags, "--set", fmt.Sprintf("%s=%s", escape(set.Name), escape(renderedValue[0]))) 2678 } else if set.File != "" { 2679 flags = append(flags, "--set-file", fmt.Sprintf("%s=%s", escape(set.Name), st.storage().normalizePath(set.File))) 2680 } else if len(set.Values) > 0 { 2681 renderedValues, err := renderValsSecrets(st.valsRuntime, set.Values...) 2682 if err != nil { 2683 return nil, files, fmt.Errorf("Failed to render set values entry in %s for release %s: %v", st.FilePath, release.Name, err) 2684 } 2685 items := make([]string, len(renderedValues)) 2686 for i, raw := range renderedValues { 2687 items[i] = escape(raw) 2688 } 2689 v := strings.Join(items, ",") 2690 flags = append(flags, "--set", fmt.Sprintf("%s={%s}", escape(set.Name), v)) 2691 } 2692 } 2693 } 2694 2695 /*********** 2696 * START 'env' section for backwards compatibility 2697 ***********/ 2698 // The 'env' section is not really necessary any longer, as 'set' would now provide the same functionality 2699 if len(release.EnvValues) > 0 { 2700 val := []string{} 2701 envValErrs := []string{} 2702 for _, set := range release.EnvValues { 2703 value, isSet := os.LookupEnv(set.Value) 2704 if isSet { 2705 val = append(val, fmt.Sprintf("%s=%s", escape(set.Name), escape(value))) 2706 } else { 2707 errMsg := fmt.Sprintf("\t%s", set.Value) 2708 envValErrs = append(envValErrs, errMsg) 2709 } 2710 } 2711 if len(envValErrs) != 0 { 2712 joinedEnvVals := strings.Join(envValErrs, "\n") 2713 errMsg := fmt.Sprintf("Environment Variables not found. Please make sure they are set and try again:\n%s", joinedEnvVals) 2714 return nil, files, errors.New(errMsg) 2715 } 2716 flags = append(flags, "--set", strings.Join(val, ",")) 2717 } 2718 /************** 2719 * END 'env' section for backwards compatibility 2720 **************/ 2721 2722 return flags, files, nil 2723} 2724 2725// renderValsSecrets helper function which renders 'ref+.*' secrets 2726func renderValsSecrets(e vals.Evaluator, input ...string) ([]string, error) { 2727 output := make([]string, len(input)) 2728 if len(input) > 0 { 2729 mapRendered, err := e.Eval(map[string]interface{}{"values": input}) 2730 if err != nil { 2731 return nil, err 2732 } 2733 2734 rendered, ok := mapRendered["values"].([]interface{}) 2735 if !ok { 2736 return nil, fmt.Errorf("type %T isn't supported", mapRendered["values"]) 2737 } 2738 2739 for i := 0; i < len(rendered); i++ { 2740 output[i] = fmt.Sprintf("%v", rendered[i]) 2741 } 2742 } 2743 return output, nil 2744} 2745 2746// DisplayAffectedReleases logs the upgraded, deleted and in error releases 2747func (ar *AffectedReleases) DisplayAffectedReleases(logger *zap.SugaredLogger) { 2748 if ar.Upgraded != nil && len(ar.Upgraded) > 0 { 2749 logger.Info("\nUPDATED RELEASES:") 2750 tbl, _ := prettytable.NewTable(prettytable.Column{Header: "NAME"}, 2751 prettytable.Column{Header: "CHART", MinWidth: 6}, 2752 prettytable.Column{Header: "VERSION", AlignRight: true}, 2753 ) 2754 tbl.Separator = " " 2755 for _, release := range ar.Upgraded { 2756 tbl.AddRow(release.Name, release.Chart, release.installedVersion) 2757 } 2758 logger.Info(tbl.String()) 2759 } 2760 if ar.Deleted != nil && len(ar.Deleted) > 0 { 2761 logger.Info("\nDELETED RELEASES:") 2762 logger.Info("NAME") 2763 for _, release := range ar.Deleted { 2764 logger.Info(release.Name) 2765 } 2766 } 2767 if ar.Failed != nil && len(ar.Failed) > 0 { 2768 logger.Info("\nFAILED RELEASES:") 2769 logger.Info("NAME") 2770 for _, release := range ar.Failed { 2771 logger.Info(release.Name) 2772 } 2773 } 2774} 2775 2776func escape(value string) string { 2777 intermediate := strings.Replace(value, "{", "\\{", -1) 2778 intermediate = strings.Replace(intermediate, "}", "\\}", -1) 2779 return strings.Replace(intermediate, ",", "\\,", -1) 2780} 2781 2782//MarshalYAML will ensure we correctly marshal SubHelmfileSpec structure correctly so it can be unmarshalled at some 2783//future time 2784func (p SubHelmfileSpec) MarshalYAML() (interface{}, error) { 2785 type SubHelmfileSpecTmp struct { 2786 Path string `yaml:"path,omitempty"` 2787 Selectors []string `yaml:"selectors,omitempty"` 2788 SelectorsInherited bool `yaml:"selectorsInherited,omitempty"` 2789 OverrideValues []interface{} `yaml:"values,omitempty"` 2790 } 2791 return &SubHelmfileSpecTmp{ 2792 Path: p.Path, 2793 Selectors: p.Selectors, 2794 SelectorsInherited: p.SelectorsInherited, 2795 OverrideValues: p.Environment.OverrideValues, 2796 }, nil 2797} 2798 2799//UnmarshalYAML will unmarshal the helmfile yaml section and fill the SubHelmfileSpec structure 2800//this is required to keep allowing string scalar for defining helmfile 2801func (hf *SubHelmfileSpec) UnmarshalYAML(unmarshal func(interface{}) error) error { 2802 2803 var tmp interface{} 2804 if err := unmarshal(&tmp); err != nil { 2805 return err 2806 } 2807 2808 switch i := tmp.(type) { 2809 case string: // single path definition without sub items, legacy sub helmfile definition 2810 hf.Path = i 2811 case map[interface{}]interface{}: // helmfile path with sub section 2812 var subHelmfileSpecTmp struct { 2813 Path string `yaml:"path"` 2814 Selectors []string `yaml:"selectors"` 2815 SelectorsInherited bool `yaml:"selectorsInherited"` 2816 2817 Environment SubhelmfileEnvironmentSpec `yaml:",inline"` 2818 } 2819 if err := unmarshal(&subHelmfileSpecTmp); err != nil { 2820 return err 2821 } 2822 hf.Path = subHelmfileSpecTmp.Path 2823 hf.Selectors = subHelmfileSpecTmp.Selectors 2824 hf.SelectorsInherited = subHelmfileSpecTmp.SelectorsInherited 2825 hf.Environment = subHelmfileSpecTmp.Environment 2826 } 2827 //since we cannot make sur the "console" string can be red after the "path" we must check we don't have 2828 //a SubHelmfileSpec with only selector and no path 2829 if hf.Selectors != nil && hf.Path == "" { 2830 return fmt.Errorf("found 'selectors' definition without path: %v", hf.Selectors) 2831 } 2832 //also exclude SelectorsInherited to true and explicit selectors 2833 if hf.SelectorsInherited && len(hf.Selectors) > 0 { 2834 return fmt.Errorf("You cannot use 'SelectorsInherited: true' along with and explicit selector for path: %v", hf.Path) 2835 } 2836 return nil 2837} 2838 2839func (st *HelmState) GenerateOutputDir(outputDir string, release *ReleaseSpec, outputDirTemplate string) (string, error) { 2840 // get absolute path of state file to generate a hash 2841 // use this hash to write helm output in a specific directory by state file and release name 2842 // ie. in a directory named stateFileName-stateFileHash-releaseName 2843 stateAbsPath, err := filepath.Abs(st.FilePath) 2844 if err != nil { 2845 return stateAbsPath, err 2846 } 2847 2848 hasher := sha1.New() 2849 io.WriteString(hasher, stateAbsPath) 2850 2851 var stateFileExtension = filepath.Ext(st.FilePath) 2852 var stateFileName = st.FilePath[0 : len(st.FilePath)-len(stateFileExtension)] 2853 2854 sha1sum := hex.EncodeToString(hasher.Sum(nil))[:8] 2855 2856 var sb strings.Builder 2857 sb.WriteString(stateFileName) 2858 sb.WriteString("-") 2859 sb.WriteString(sha1sum) 2860 sb.WriteString("-") 2861 sb.WriteString(release.Name) 2862 2863 if outputDirTemplate == "" { 2864 outputDirTemplate = filepath.Join("{{ .OutputDir }}", "{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}-{{ .Release.Name}}") 2865 } 2866 2867 t, err := template.New("output-dir").Parse(outputDirTemplate) 2868 if err != nil { 2869 return "", fmt.Errorf("parsing output-dir templmate") 2870 } 2871 2872 buf := &bytes.Buffer{} 2873 2874 type state struct { 2875 BaseName string 2876 Path string 2877 AbsPath string 2878 AbsPathSHA1 string 2879 } 2880 2881 data := struct { 2882 OutputDir string 2883 State state 2884 Release *ReleaseSpec 2885 }{ 2886 OutputDir: outputDir, 2887 State: state{ 2888 BaseName: stateFileName, 2889 Path: st.FilePath, 2890 AbsPath: stateAbsPath, 2891 AbsPathSHA1: sha1sum, 2892 }, 2893 Release: release, 2894 } 2895 2896 if err := t.Execute(buf, data); err != nil { 2897 return "", fmt.Errorf("executing output-dir template: %w", err) 2898 } 2899 2900 return buf.String(), nil 2901} 2902 2903func (st *HelmState) GenerateOutputFilePath(release *ReleaseSpec, outputFileTemplate string) (string, error) { 2904 // get absolute path of state file to generate a hash 2905 // use this hash to write helm output in a specific directory by state file and release name 2906 // ie. in a directory named stateFileName-stateFileHash-releaseName 2907 stateAbsPath, err := filepath.Abs(st.FilePath) 2908 if err != nil { 2909 return stateAbsPath, err 2910 } 2911 2912 hasher := sha1.New() 2913 io.WriteString(hasher, stateAbsPath) 2914 2915 var stateFileExtension = filepath.Ext(st.FilePath) 2916 var stateFileName = st.FilePath[0 : len(st.FilePath)-len(stateFileExtension)] 2917 2918 sha1sum := hex.EncodeToString(hasher.Sum(nil))[:8] 2919 2920 var sb strings.Builder 2921 sb.WriteString(stateFileName) 2922 sb.WriteString("-") 2923 sb.WriteString(sha1sum) 2924 sb.WriteString("-") 2925 sb.WriteString(release.Name) 2926 2927 if outputFileTemplate == "" { 2928 outputFileTemplate = filepath.Join("{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}", "{{ .Release.Name }}.yaml") 2929 } 2930 2931 t, err := template.New("output-file").Parse(outputFileTemplate) 2932 if err != nil { 2933 return "", fmt.Errorf("parsing output-file templmate") 2934 } 2935 2936 buf := &bytes.Buffer{} 2937 2938 type state struct { 2939 BaseName string 2940 Path string 2941 AbsPath string 2942 AbsPathSHA1 string 2943 } 2944 2945 data := struct { 2946 State state 2947 Release *ReleaseSpec 2948 }{ 2949 State: state{ 2950 BaseName: stateFileName, 2951 Path: st.FilePath, 2952 AbsPath: stateAbsPath, 2953 AbsPathSHA1: sha1sum, 2954 }, 2955 Release: release, 2956 } 2957 2958 if err := t.Execute(buf, data); err != nil { 2959 return "", fmt.Errorf("executing output-file template: %w", err) 2960 } 2961 2962 return buf.String(), nil 2963} 2964 2965func (st *HelmState) ToYaml() (string, error) { 2966 if result, err := yaml.Marshal(st); err != nil { 2967 return "", err 2968 } else { 2969 return string(result), nil 2970 } 2971} 2972 2973func (st *HelmState) LoadYAMLForEmbedding(release *ReleaseSpec, entries []interface{}, missingFileHandler *string, pathPrefix string) ([]interface{}, error) { 2974 var result []interface{} 2975 2976 for _, v := range entries { 2977 switch t := v.(type) { 2978 case string: 2979 var values map[string]interface{} 2980 2981 paths, skip, err := st.storage().resolveFile(missingFileHandler, "values", pathPrefix+t) 2982 if err != nil { 2983 return nil, err 2984 } 2985 if skip { 2986 continue 2987 } 2988 2989 if len(paths) > 1 { 2990 return nil, fmt.Errorf("glob patterns in release values and secrets is not supported yet. please submit a feature request if necessary") 2991 } 2992 yamlOrTemplatePath := paths[0] 2993 2994 yamlBytes, err := st.RenderReleaseValuesFileToBytes(release, yamlOrTemplatePath) 2995 if err != nil { 2996 return nil, fmt.Errorf("failed to render values files \"%s\": %v", t, err) 2997 } 2998 2999 if err := yaml.Unmarshal(yamlBytes, &values); err != nil { 3000 return nil, err 3001 } 3002 3003 result = append(result, values) 3004 default: 3005 result = append(result, v) 3006 } 3007 } 3008 3009 return result, nil 3010} 3011 3012func (st *HelmState) Reverse() { 3013 for i, j := 0, len(st.Releases)-1; i < j; i, j = i+1, j-1 { 3014 st.Releases[i], st.Releases[j] = st.Releases[j], st.Releases[i] 3015 } 3016 3017 for i, j := 0, len(st.Helmfiles)-1; i < j; i, j = i+1, j-1 { 3018 st.Helmfiles[i], st.Helmfiles[j] = st.Helmfiles[j], st.Helmfiles[i] 3019 } 3020} 3021 3022func (st *HelmState) getOCIChart(pullChan chan PullCommand, release *ReleaseSpec, tempDir string, helm helmexec.Interface) (*string, error) { 3023 repo, name := st.GetRepositoryAndNameFromChartName(release.Chart) 3024 if repo == nil { 3025 return nil, nil 3026 } 3027 3028 if !repo.OCI { 3029 return nil, nil 3030 } 3031 3032 chartVersion := "latest" 3033 if release.Version != "" { 3034 chartVersion = release.Version 3035 } 3036 3037 qualifiedChartName := fmt.Sprintf("%s/%s:%s", repo.URL, name, chartVersion) 3038 3039 err := st.pullChart(pullChan, qualifiedChartName) 3040 if err != nil { 3041 return nil, err 3042 } 3043 3044 pathElems := []string{ 3045 tempDir, 3046 } 3047 3048 if release.Namespace != "" { 3049 pathElems = append(pathElems, release.Namespace) 3050 } 3051 3052 if release.KubeContext != "" { 3053 pathElems = append(pathElems, release.KubeContext) 3054 } 3055 3056 pathElems = append(pathElems, release.Name, name, chartVersion) 3057 3058 chartPath := path.Join(pathElems...) 3059 err = helm.ChartExport(qualifiedChartName, chartPath) 3060 3061 fullChartPath, err := findChartDirectory(chartPath) 3062 if err != nil { 3063 return nil, err 3064 } 3065 3066 chartPath = filepath.Dir(fullChartPath) 3067 3068 return &chartPath, nil 3069} 3070 3071// Pull charts one by one to prevent concurrent pull problems with Helm 3072func (st *HelmState) pullChartWorker(pullChan chan PullCommand, helm helmexec.Interface) { 3073 for pullCmd := range pullChan { 3074 err := helm.ChartPull(pullCmd.ChartRef) 3075 pullCmd.responseChan <- err 3076 } 3077} 3078 3079// Send a pull command to the pull worker 3080func (st *HelmState) pullChart(pullChan chan PullCommand, chartRef string) error { 3081 response := make(chan error, 1) 3082 cmd := PullCommand{ 3083 responseChan: response, 3084 ChartRef: chartRef, 3085 } 3086 pullChan <- cmd 3087 return <-response 3088} 3089