github.com/appscode/helm@v3.0.0-alpha.1+incompatible/pkg/action/install.go (about) 1 /* 2 Copyright The Helm Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package action 18 19 import ( 20 "bytes" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "net/url" 25 "os" 26 "path" 27 "path/filepath" 28 "sort" 29 "strings" 30 "text/template" 31 "time" 32 33 "github.com/Masterminds/sprig" 34 "github.com/ghodss/yaml" 35 "github.com/pkg/errors" 36 37 "helm.sh/helm/pkg/chart" 38 "helm.sh/helm/pkg/chartutil" 39 "helm.sh/helm/pkg/cli" 40 "helm.sh/helm/pkg/downloader" 41 "helm.sh/helm/pkg/engine" 42 "helm.sh/helm/pkg/getter" 43 "helm.sh/helm/pkg/hooks" 44 "helm.sh/helm/pkg/release" 45 "helm.sh/helm/pkg/releaseutil" 46 "helm.sh/helm/pkg/repo" 47 "helm.sh/helm/pkg/strvals" 48 "helm.sh/helm/pkg/version" 49 ) 50 51 // releaseNameMaxLen is the maximum length of a release name. 52 // 53 // As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for 54 // charts to add data. Effectively, that gives us 53 chars. 55 // See https://github.com/helm/helm/issues/1528 56 const releaseNameMaxLen = 53 57 58 // NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine 59 // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually 60 // wants to see this file after rendering in the status command. However, it must be a suffix 61 // since there can be filepath in front of it. 62 const notesFileSuffix = "NOTES.txt" 63 64 // Install performs an installation operation. 65 type Install struct { 66 cfg *Configuration 67 68 ChartPathOptions 69 ValueOptions 70 71 DryRun bool 72 DisableHooks bool 73 Replace bool 74 Wait bool 75 Devel bool 76 DependencyUpdate bool 77 Timeout time.Duration 78 Namespace string 79 ReleaseName string 80 GenerateName bool 81 NameTemplate string 82 } 83 84 type ValueOptions struct { 85 ValueFiles []string 86 StringValues []string 87 Values []string 88 rawValues map[string]interface{} 89 } 90 91 type ChartPathOptions struct { 92 CaFile string // --ca-file 93 CertFile string // --cert-file 94 KeyFile string // --key-file 95 Keyring string // --keyring 96 Password string // --password 97 RepoURL string // --repo 98 Username string // --username 99 Verify bool // --verify 100 Version string // --version 101 } 102 103 // NewInstall creates a new Install object with the given configuration. 104 func NewInstall(cfg *Configuration) *Install { 105 return &Install{ 106 cfg: cfg, 107 } 108 } 109 110 // Run executes the installation 111 // 112 // If DryRun is set to true, this will prepare the release, but not install it 113 func (i *Install) Run(chrt *chart.Chart) (*release.Release, error) { 114 if err := i.availableName(); err != nil { 115 return nil, err 116 } 117 118 caps, err := i.cfg.getCapabilities() 119 if err != nil { 120 return nil, err 121 } 122 123 options := chartutil.ReleaseOptions{ 124 Name: i.ReleaseName, 125 Namespace: i.Namespace, 126 IsInstall: true, 127 } 128 valuesToRender, err := chartutil.ToRenderValues(chrt, i.rawValues, options, caps) 129 if err != nil { 130 return nil, err 131 } 132 133 rel := i.createRelease(chrt, i.rawValues) 134 var manifestDoc *bytes.Buffer 135 rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender) 136 // Even for errors, attach this if available 137 if manifestDoc != nil { 138 rel.Manifest = manifestDoc.String() 139 } 140 // Check error from render 141 if err != nil { 142 rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error())) 143 // Return a release with partial data so that the client can show debugging information. 144 return rel, err 145 } 146 147 // Mark this release as in-progress 148 rel.SetStatus(release.StatusPendingInstall, "Initial install underway") 149 if err := i.validateManifest(manifestDoc); err != nil { 150 return rel, err 151 } 152 153 // Bail out here if it is a dry run 154 if i.DryRun { 155 rel.Info.Description = "Dry run complete" 156 return rel, nil 157 } 158 159 // If Replace is true, we need to supersede the last release. 160 if i.Replace { 161 if err := i.replaceRelease(rel); err != nil { 162 return nil, err 163 } 164 } 165 166 // Store the release in history before continuing (new in Helm 3). We always know 167 // that this is a create operation. 168 if err := i.cfg.Releases.Create(rel); err != nil { 169 // We could try to recover gracefully here, but since nothing has been installed 170 // yet, this is probably safer than trying to continue when we know storage is 171 // not working. 172 return rel, err 173 } 174 175 // pre-install hooks 176 if !i.DisableHooks { 177 if err := i.execHook(rel.Hooks, hooks.PreInstall); err != nil { 178 rel.SetStatus(release.StatusFailed, "failed pre-install: "+err.Error()) 179 _ = i.replaceRelease(rel) 180 return rel, err 181 } 182 } 183 184 // At this point, we can do the install. Note that before we were detecting whether to 185 // do an update, but it's not clear whether we WANT to do an update if the re-use is set 186 // to true, since that is basically an upgrade operation. 187 buf := bytes.NewBufferString(rel.Manifest) 188 if err := i.cfg.KubeClient.Create(buf); err != nil { 189 rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) 190 i.recordRelease(rel) // Ignore the error, since we have another error to deal with. 191 return rel, errors.Wrapf(err, "release %s failed", i.ReleaseName) 192 } 193 194 if i.Wait { 195 buf := bytes.NewBufferString(rel.Manifest) 196 if err := i.cfg.KubeClient.Wait(buf, i.Timeout); err != nil { 197 rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) 198 i.recordRelease(rel) // Ignore the error, since we have another error to deal with. 199 return rel, errors.Wrapf(err, "release %s failed", i.ReleaseName) 200 } 201 202 } 203 204 if !i.DisableHooks { 205 if err := i.execHook(rel.Hooks, hooks.PostInstall); err != nil { 206 rel.SetStatus(release.StatusFailed, "failed post-install: "+err.Error()) 207 _ = i.replaceRelease(rel) 208 return rel, err 209 } 210 } 211 212 rel.SetStatus(release.StatusDeployed, "Install complete") 213 214 // This is a tricky case. The release has been created, but the result 215 // cannot be recorded. The truest thing to tell the user is that the 216 // release was created. However, the user will not be able to do anything 217 // further with this release. 218 // 219 // One possible strategy would be to do a timed retry to see if we can get 220 // this stored in the future. 221 i.recordRelease(rel) 222 223 return rel, nil 224 } 225 226 // availableName tests whether a name is available 227 // 228 // Roughly, this will return an error if name is 229 // 230 // - empty 231 // - too long 232 // - already in use, and not deleted 233 // - used by a deleted release, and i.Replace is false 234 func (i *Install) availableName() error { 235 start := i.ReleaseName 236 if start == "" { 237 return errors.New("name is required") 238 } 239 240 if len(start) > releaseNameMaxLen { 241 return errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) 242 } 243 244 h, err := i.cfg.Releases.History(start) 245 if err != nil || len(h) < 1 { 246 return nil 247 } 248 releaseutil.Reverse(h, releaseutil.SortByRevision) 249 rel := h[0] 250 251 if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) { 252 return nil 253 } 254 return errors.New("cannot re-use a name that is still in use") 255 } 256 257 // createRelease creates a new release object 258 func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release { 259 ts := i.cfg.Now() 260 return &release.Release{ 261 Name: i.ReleaseName, 262 Namespace: i.Namespace, 263 Chart: chrt, 264 Config: rawVals, 265 Info: &release.Info{ 266 FirstDeployed: ts, 267 LastDeployed: ts, 268 Status: release.StatusUnknown, 269 }, 270 Version: 1, 271 } 272 } 273 274 // recordRelease with an update operation in case reuse has been set. 275 func (i *Install) recordRelease(r *release.Release) error { 276 // This is a legacy function which has been reduced to a oneliner. Could probably 277 // refactor it out. 278 return i.cfg.Releases.Update(r) 279 } 280 281 // replaceRelease replaces an older release with this one 282 // 283 // This allows us to re-use names by superseding an existing release with a new one 284 func (i *Install) replaceRelease(rel *release.Release) error { 285 hist, err := i.cfg.Releases.History(rel.Name) 286 if err != nil || len(hist) == 0 { 287 // No releases exist for this name, so we can return early 288 return nil 289 } 290 291 releaseutil.Reverse(hist, releaseutil.SortByRevision) 292 last := hist[0] 293 294 // Update version to the next available 295 rel.Version = last.Version + 1 296 297 // Do not change the status of a failed release. 298 if last.Info.Status == release.StatusFailed { 299 return nil 300 } 301 302 // For any other status, mark it as superseded and store the old record 303 last.SetStatus(release.StatusSuperseded, "superseded by new release") 304 return i.recordRelease(last) 305 } 306 307 // renderResources renders the templates in a chart 308 func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values) ([]*release.Hook, *bytes.Buffer, string, error) { 309 hs := []*release.Hook{} 310 b := bytes.NewBuffer(nil) 311 312 caps, err := c.getCapabilities() 313 if err != nil { 314 return hs, b, "", err 315 } 316 317 if ch.Metadata.KubeVersion != "" { 318 if !version.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { 319 return hs, b, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) 320 } 321 } 322 323 files, err := engine.Render(ch, values) 324 if err != nil { 325 return hs, b, "", err 326 } 327 328 // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, 329 // pull it out of here into a separate file so that we can actually use the output of the rendered 330 // text file. We have to spin through this map because the file contains path information, so we 331 // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip 332 // it in the sortHooks. 333 notes := "" 334 for k, v := range files { 335 if strings.HasSuffix(k, notesFileSuffix) { 336 // Only apply the notes if it belongs to the parent chart 337 // Note: Do not use filePath.Join since it creates a path with \ which is not expected 338 if k == path.Join(ch.Name(), "templates", notesFileSuffix) { 339 notes = v 340 } 341 delete(files, k) 342 } 343 } 344 345 // Sort hooks, manifests, and partials. Only hooks and manifests are returned, 346 // as partials are not used after renderer.Render. Empty manifests are also 347 // removed here. 348 hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) 349 if err != nil { 350 // By catching parse errors here, we can prevent bogus releases from going 351 // to Kubernetes. 352 // 353 // We return the files as a big blob of data to help the user debug parser 354 // errors. 355 for name, content := range files { 356 if strings.TrimSpace(content) == "" { 357 continue 358 } 359 fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) 360 } 361 return hs, b, "", err 362 } 363 364 // Aggregate all valid manifests into one big doc. 365 for _, m := range manifests { 366 fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) 367 } 368 369 return hs, b, notes, nil 370 } 371 372 // validateManifest checks to see whether the given manifest is valid for the current Kubernetes 373 func (i *Install) validateManifest(manifest io.Reader) error { 374 _, err := i.cfg.KubeClient.BuildUnstructured(manifest) 375 return err 376 } 377 378 // execHook executes all of the hooks for the given hook event. 379 func (i *Install) execHook(hs []*release.Hook, hook string) error { 380 executingHooks := []*release.Hook{} 381 382 for _, h := range hs { 383 for _, e := range h.Events { 384 if string(e) == hook { 385 executingHooks = append(executingHooks, h) 386 } 387 } 388 } 389 390 sort.Sort(hookByWeight(executingHooks)) 391 392 for _, h := range executingHooks { 393 if err := deleteHookByPolicy(i.cfg, h, hooks.BeforeHookCreation); err != nil { 394 return err 395 } 396 397 b := bytes.NewBufferString(h.Manifest) 398 if err := i.cfg.KubeClient.Create(b); err != nil { 399 return errors.Wrapf(err, "warning: Release %s %s %s failed", i.ReleaseName, hook, h.Path) 400 } 401 b.Reset() 402 b.WriteString(h.Manifest) 403 404 if err := i.cfg.KubeClient.WatchUntilReady(b, i.Timeout); err != nil { 405 // If a hook is failed, checkout the annotation of the hook to determine whether the hook should be deleted 406 // under failed condition. If so, then clear the corresponding resource object in the hook 407 if err := deleteHookByPolicy(i.cfg, h, hooks.HookFailed); err != nil { 408 return err 409 } 410 return err 411 } 412 } 413 414 // If all hooks are succeeded, checkout the annotation of each hook to determine whether the hook should be deleted 415 // under succeeded condition. If so, then clear the corresponding resource object in each hook 416 for _, h := range executingHooks { 417 if err := deleteHookByPolicy(i.cfg, h, hooks.HookSucceeded); err != nil { 418 return err 419 } 420 h.LastRun = time.Now() 421 } 422 423 return nil 424 } 425 426 // deletePolices represents a mapping between the key in the annotation for label deleting policy and its real meaning 427 // FIXME: Can we refactor this out? 428 var deletePolices = map[string]release.HookDeletePolicy{ 429 hooks.HookSucceeded: release.HookSucceeded, 430 hooks.HookFailed: release.HookFailed, 431 hooks.BeforeHookCreation: release.HookBeforeHookCreation, 432 } 433 434 // hookHasDeletePolicy determines whether the defined hook deletion policy matches the hook deletion polices 435 // supported by helm. If so, mark the hook as one should be deleted. 436 func hookHasDeletePolicy(h *release.Hook, policy string) bool { 437 dp, ok := deletePolices[policy] 438 if !ok { 439 return false 440 } 441 for _, v := range h.DeletePolicies { 442 if dp == v { 443 return true 444 } 445 } 446 return false 447 } 448 449 // hookByWeight is a sorter for hooks 450 type hookByWeight []*release.Hook 451 452 func (x hookByWeight) Len() int { return len(x) } 453 func (x hookByWeight) Swap(i, j int) { x[i], x[j] = x[j], x[i] } 454 func (x hookByWeight) Less(i, j int) bool { 455 if x[i].Weight == x[j].Weight { 456 return x[i].Name < x[j].Name 457 } 458 return x[i].Weight < x[j].Weight 459 } 460 461 // NameAndChart returns the name and chart that should be used. 462 // 463 // This will read the flags and handle name generation if necessary. 464 func (i *Install) NameAndChart(args []string) (string, string, error) { 465 flagsNotSet := func() error { 466 if i.GenerateName { 467 return errors.New("cannot set --generate-name and also specify a name") 468 } 469 if i.NameTemplate != "" { 470 return errors.New("cannot set --name-template and also specify a name") 471 } 472 return nil 473 } 474 475 if len(args) == 2 { 476 return args[0], args[1], flagsNotSet() 477 } 478 479 if i.NameTemplate != "" { 480 name, err := TemplateName(i.NameTemplate) 481 return name, args[0], err 482 } 483 484 if i.ReleaseName != "" { 485 return i.ReleaseName, args[0], nil 486 } 487 488 if !i.GenerateName { 489 return "", args[0], errors.New("must either provide a name or specify --generate-name") 490 } 491 492 base := filepath.Base(args[0]) 493 if base == "." || base == "" { 494 base = "chart" 495 } 496 497 return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil 498 } 499 500 func TemplateName(nameTemplate string) (string, error) { 501 if nameTemplate == "" { 502 return "", nil 503 } 504 505 t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) 506 if err != nil { 507 return "", err 508 } 509 var b bytes.Buffer 510 if err := t.Execute(&b, nil); err != nil { 511 return "", err 512 } 513 514 return b.String(), nil 515 } 516 517 func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error { 518 var missing []string 519 520 OUTER: 521 for _, r := range reqs { 522 for _, d := range ch.Dependencies() { 523 if d.Name() == r.Name { 524 continue OUTER 525 } 526 } 527 missing = append(missing, r.Name) 528 } 529 530 if len(missing) > 0 { 531 return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) 532 } 533 return nil 534 } 535 536 // LocateChart looks for a chart directory in known places, and returns either the full path or an error. 537 // 538 // This does not ensure that the chart is well-formed; only that the requested filename exists. 539 // 540 // Order of resolution: 541 // - relative to current working directory 542 // - if path is absolute or begins with '.', error out here 543 // - chart repos in $HELM_HOME 544 // - URL 545 // 546 // If 'verify' is true, this will attempt to also verify the chart. 547 func (c *ChartPathOptions) LocateChart(name string, settings cli.EnvSettings) (string, error) { 548 name = strings.TrimSpace(name) 549 version := strings.TrimSpace(c.Version) 550 551 if _, err := os.Stat(name); err == nil { 552 abs, err := filepath.Abs(name) 553 if err != nil { 554 return abs, err 555 } 556 if c.Verify { 557 if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil { 558 return "", err 559 } 560 } 561 return abs, nil 562 } 563 if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { 564 return name, errors.Errorf("path %q not found", name) 565 } 566 567 crepo := filepath.Join(settings.Home.Repository(), name) 568 if _, err := os.Stat(crepo); err == nil { 569 return filepath.Abs(crepo) 570 } 571 572 dl := downloader.ChartDownloader{ 573 HelmHome: settings.Home, 574 Out: os.Stdout, 575 Keyring: c.Keyring, 576 Getters: getter.All(settings), 577 Username: c.Username, 578 Password: c.Password, 579 } 580 if c.Verify { 581 dl.Verify = downloader.VerifyAlways 582 } 583 if c.RepoURL != "" { 584 chartURL, err := repo.FindChartInAuthRepoURL(c.RepoURL, c.Username, c.Password, name, version, 585 c.CertFile, c.KeyFile, c.CaFile, getter.All(settings)) 586 if err != nil { 587 return "", err 588 } 589 name = chartURL 590 } 591 592 if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { 593 os.MkdirAll(settings.Home.Archive(), 0744) 594 } 595 596 filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) 597 if err == nil { 598 lname, err := filepath.Abs(filename) 599 if err != nil { 600 return filename, err 601 } 602 return lname, nil 603 } else if settings.Debug { 604 return filename, err 605 } 606 607 return filename, errors.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) 608 } 609 610 // MergeValues merges values from files specified via -f/--values and 611 // directly via --set or --set-string, marshaling them to YAML 612 func (v *ValueOptions) MergeValues(settings cli.EnvSettings) error { 613 base := map[string]interface{}{} 614 615 // User specified a values files via -f/--values 616 for _, filePath := range v.ValueFiles { 617 currentMap := map[string]interface{}{} 618 619 bytes, err := readFile(filePath, settings) 620 if err != nil { 621 return err 622 } 623 624 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 625 return errors.Wrapf(err, "failed to parse %s", filePath) 626 } 627 // Merge with the previous map 628 base = mergeMaps(base, currentMap) 629 } 630 631 // User specified a value via --set 632 for _, value := range v.Values { 633 if err := strvals.ParseInto(value, base); err != nil { 634 return errors.Wrap(err, "failed parsing --set data") 635 } 636 } 637 638 // User specified a value via --set-string 639 for _, value := range v.StringValues { 640 if err := strvals.ParseIntoString(value, base); err != nil { 641 return errors.Wrap(err, "failed parsing --set-string data") 642 } 643 } 644 645 v.rawValues = base 646 return nil 647 } 648 649 // mergeValues merges source and destination map, preferring values from the source map 650 func mergeValues(dest, src map[string]interface{}) map[string]interface{} { 651 out := make(map[string]interface{}) 652 for k, v := range dest { 653 out[k] = v 654 } 655 for k, v := range src { 656 if _, ok := out[k]; !ok { 657 // If the key doesn't exist already, then just set the key to that value 658 } else if nextMap, ok := v.(map[string]interface{}); !ok { 659 // If it isn't another map, overwrite the value 660 } else if destMap, isMap := out[k].(map[string]interface{}); !isMap { 661 // Edge case: If the key exists in the destination, but isn't a map 662 // If the source map has a map for this key, prefer it 663 } else { 664 // If we got to this point, it is a map in both, so merge them 665 out[k] = mergeValues(destMap, nextMap) 666 continue 667 } 668 out[k] = v 669 } 670 return out 671 } 672 673 func mergeMaps(a, b map[string]interface{}) map[string]interface{} { 674 out := make(map[string]interface{}, len(a)) 675 for k, v := range a { 676 out[k] = v 677 } 678 for k, v := range b { 679 if v, ok := v.(map[string]interface{}); ok { 680 if bv, ok := out[k]; ok { 681 if bv, ok := bv.(map[string]interface{}); ok { 682 out[k] = mergeMaps(bv, v) 683 continue 684 } 685 } 686 } 687 out[k] = v 688 } 689 return out 690 } 691 692 // readFile load a file from stdin, the local directory, or a remote file with a url. 693 func readFile(filePath string, settings cli.EnvSettings) ([]byte, error) { 694 if strings.TrimSpace(filePath) == "-" { 695 return ioutil.ReadAll(os.Stdin) 696 } 697 u, _ := url.Parse(filePath) 698 p := getter.All(settings) 699 700 // FIXME: maybe someone handle other protocols like ftp. 701 getterConstructor, err := p.ByScheme(u.Scheme) 702 703 if err != nil { 704 return ioutil.ReadFile(filePath) 705 } 706 707 getter, err := getterConstructor(filePath, "", "", "") 708 if err != nil { 709 return []byte{}, err 710 } 711 data, err := getter.Get(filePath) 712 return data.Bytes(), err 713 }