github.com/aaronmell/helm@v3.0.0-beta.2+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/ioutil" 23 "os" 24 "path" 25 "path/filepath" 26 "strings" 27 "text/template" 28 "time" 29 30 "github.com/Masterminds/sprig" 31 "github.com/pkg/errors" 32 apierrors "k8s.io/apimachinery/pkg/api/errors" 33 34 "helm.sh/helm/pkg/chart" 35 "helm.sh/helm/pkg/chartutil" 36 "helm.sh/helm/pkg/cli" 37 "helm.sh/helm/pkg/downloader" 38 "helm.sh/helm/pkg/engine" 39 "helm.sh/helm/pkg/getter" 40 kubefake "helm.sh/helm/pkg/kube/fake" 41 "helm.sh/helm/pkg/release" 42 "helm.sh/helm/pkg/releaseutil" 43 "helm.sh/helm/pkg/repo" 44 "helm.sh/helm/pkg/storage" 45 "helm.sh/helm/pkg/storage/driver" 46 ) 47 48 // releaseNameMaxLen is the maximum length of a release name. 49 // 50 // As of Kubernetes 1.4, the max limit on a name is 63 chars. We reserve 10 for 51 // charts to add data. Effectively, that gives us 53 chars. 52 // See https://github.com/helm/helm/issues/1528 53 const releaseNameMaxLen = 53 54 55 // NOTESFILE_SUFFIX that we want to treat special. It goes through the templating engine 56 // but it's not a yaml file (resource) hence can't have hooks, etc. And the user actually 57 // wants to see this file after rendering in the status command. However, it must be a suffix 58 // since there can be filepath in front of it. 59 const notesFileSuffix = "NOTES.txt" 60 61 const defaultDirectoryPermission = 0755 62 63 // Install performs an installation operation. 64 type Install struct { 65 cfg *Configuration 66 67 ChartPathOptions 68 69 ClientOnly bool 70 DryRun bool 71 DisableHooks bool 72 Replace bool 73 Wait bool 74 Devel bool 75 DependencyUpdate bool 76 Timeout time.Duration 77 Namespace string 78 ReleaseName string 79 GenerateName bool 80 NameTemplate string 81 OutputDir string 82 Atomic bool 83 SkipCRDs bool 84 } 85 86 // ChartPathOptions captures common options used for controlling chart paths 87 type ChartPathOptions struct { 88 CaFile string // --ca-file 89 CertFile string // --cert-file 90 KeyFile string // --key-file 91 Keyring string // --keyring 92 Password string // --password 93 RepoURL string // --repo 94 Username string // --username 95 Verify bool // --verify 96 Version string // --version 97 } 98 99 // NewInstall creates a new Install object with the given configuration. 100 func NewInstall(cfg *Configuration) *Install { 101 return &Install{ 102 cfg: cfg, 103 } 104 } 105 106 // Run executes the installation 107 // 108 // If DryRun is set to true, this will prepare the release, but not install it 109 func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) { 110 if err := i.availableName(); err != nil { 111 return nil, err 112 } 113 114 if i.ClientOnly { 115 // Add mock objects in here so it doesn't use Kube API server 116 // NOTE(bacongobbler): used for `helm template` 117 i.cfg.Capabilities = chartutil.DefaultCapabilities 118 i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard} 119 i.cfg.Releases = storage.Init(driver.NewMemory()) 120 } 121 122 if err := chartutil.ProcessDependencies(chrt, vals); err != nil { 123 return nil, err 124 } 125 126 // Make sure if Atomic is set, that wait is set as well. This makes it so 127 // the user doesn't have to specify both 128 i.Wait = i.Wait || i.Atomic 129 130 caps, err := i.cfg.getCapabilities() 131 if err != nil { 132 return nil, err 133 } 134 135 options := chartutil.ReleaseOptions{ 136 Name: i.ReleaseName, 137 Namespace: i.Namespace, 138 IsInstall: true, 139 } 140 valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps) 141 if err != nil { 142 return nil, err 143 } 144 145 rel := i.createRelease(chrt, vals) 146 147 // Pre-install anything in the crd/ directory 148 if crds := chrt.CRDs(); !i.SkipCRDs && len(crds) > 0 { 149 // We do these one at a time in the order they were read. 150 for _, obj := range crds { 151 // Read in the resources 152 res, err := i.cfg.KubeClient.Build(bytes.NewBuffer(obj.Data)) 153 if err != nil { 154 // We bail out immediately 155 return nil, errors.Wrapf(err, "failed to install CRD %s", obj.Name) 156 } 157 // On dry run, bail here 158 if i.DryRun { 159 i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.") 160 continue 161 } 162 // Send them to Kube 163 if _, err := i.cfg.KubeClient.Create(res); err != nil { 164 // If the error is CRD already exists, continue. 165 if apierrors.IsAlreadyExists(err) { 166 crdName := res[0].Name 167 i.cfg.Log("CRD %s is already present. Skipping.", crdName) 168 continue 169 } 170 return i.failRelease(rel, err) 171 } 172 } 173 } 174 175 var manifestDoc *bytes.Buffer 176 rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.OutputDir) 177 // Even for errors, attach this if available 178 if manifestDoc != nil { 179 rel.Manifest = manifestDoc.String() 180 } 181 // Check error from render 182 if err != nil { 183 rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error())) 184 // Return a release with partial data so that the client can show debugging information. 185 return rel, err 186 } 187 188 // Mark this release as in-progress 189 rel.SetStatus(release.StatusPendingInstall, "Initial install underway") 190 191 resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest)) 192 if err != nil { 193 return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest") 194 } 195 196 // Bail out here if it is a dry run 197 if i.DryRun { 198 rel.Info.Description = "Dry run complete" 199 return rel, nil 200 } 201 202 // If Replace is true, we need to supercede the last release. 203 if i.Replace { 204 if err := i.replaceRelease(rel); err != nil { 205 return nil, err 206 } 207 } 208 209 // Store the release in history before continuing (new in Helm 3). We always know 210 // that this is a create operation. 211 if err := i.cfg.Releases.Create(rel); err != nil { 212 // We could try to recover gracefully here, but since nothing has been installed 213 // yet, this is probably safer than trying to continue when we know storage is 214 // not working. 215 return rel, err 216 } 217 218 // pre-install hooks 219 if !i.DisableHooks { 220 if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil { 221 return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err)) 222 } 223 } 224 225 // At this point, we can do the install. Note that before we were detecting whether to 226 // do an update, but it's not clear whether we WANT to do an update if the re-use is set 227 // to true, since that is basically an upgrade operation. 228 if _, err := i.cfg.KubeClient.Create(resources); err != nil { 229 return i.failRelease(rel, err) 230 } 231 232 if i.Wait { 233 if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil { 234 return i.failRelease(rel, err) 235 } 236 237 } 238 239 if !i.DisableHooks { 240 if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil { 241 return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err)) 242 } 243 } 244 245 rel.SetStatus(release.StatusDeployed, "Install complete") 246 247 // This is a tricky case. The release has been created, but the result 248 // cannot be recorded. The truest thing to tell the user is that the 249 // release was created. However, the user will not be able to do anything 250 // further with this release. 251 // 252 // One possible strategy would be to do a timed retry to see if we can get 253 // this stored in the future. 254 i.recordRelease(rel) 255 256 return rel, nil 257 } 258 259 func (i *Install) failRelease(rel *release.Release, err error) (*release.Release, error) { 260 rel.SetStatus(release.StatusFailed, fmt.Sprintf("Release %q failed: %s", i.ReleaseName, err.Error())) 261 if i.Atomic { 262 i.cfg.Log("Install failed and atomic is set, uninstalling release") 263 uninstall := NewUninstall(i.cfg) 264 uninstall.DisableHooks = i.DisableHooks 265 uninstall.KeepHistory = false 266 uninstall.Timeout = i.Timeout 267 if _, uninstallErr := uninstall.Run(i.ReleaseName); uninstallErr != nil { 268 return rel, errors.Wrapf(uninstallErr, "an error occurred while uninstalling the release. original install error: %s", err) 269 } 270 return rel, errors.Wrapf(err, "release %s failed, and has been uninstalled due to atomic being set", i.ReleaseName) 271 } 272 i.recordRelease(rel) // Ignore the error, since we have another error to deal with. 273 return rel, err 274 } 275 276 // availableName tests whether a name is available 277 // 278 // Roughly, this will return an error if name is 279 // 280 // - empty 281 // - too long 282 // - already in use, and not deleted 283 // - used by a deleted release, and i.Replace is false 284 func (i *Install) availableName() error { 285 start := i.ReleaseName 286 if start == "" { 287 return errors.New("name is required") 288 } 289 290 if len(start) > releaseNameMaxLen { 291 return errors.Errorf("release name %q exceeds max length of %d", start, releaseNameMaxLen) 292 } 293 294 if i.DryRun { 295 return nil 296 } 297 298 h, err := i.cfg.Releases.History(start) 299 if err != nil || len(h) < 1 { 300 return nil 301 } 302 releaseutil.Reverse(h, releaseutil.SortByRevision) 303 rel := h[0] 304 305 if st := rel.Info.Status; i.Replace && (st == release.StatusUninstalled || st == release.StatusFailed) { 306 return nil 307 } 308 return errors.New("cannot re-use a name that is still in use") 309 } 310 311 // createRelease creates a new release object 312 func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release { 313 ts := i.cfg.Now() 314 return &release.Release{ 315 Name: i.ReleaseName, 316 Namespace: i.Namespace, 317 Chart: chrt, 318 Config: rawVals, 319 Info: &release.Info{ 320 FirstDeployed: ts, 321 LastDeployed: ts, 322 Status: release.StatusUnknown, 323 }, 324 Version: 1, 325 } 326 } 327 328 // recordRelease with an update operation in case reuse has been set. 329 func (i *Install) recordRelease(r *release.Release) error { 330 // This is a legacy function which has been reduced to a oneliner. Could probably 331 // refactor it out. 332 return i.cfg.Releases.Update(r) 333 } 334 335 // replaceRelease replaces an older release with this one 336 // 337 // This allows us to re-use names by superseding an existing release with a new one 338 func (i *Install) replaceRelease(rel *release.Release) error { 339 hist, err := i.cfg.Releases.History(rel.Name) 340 if err != nil || len(hist) == 0 { 341 // No releases exist for this name, so we can return early 342 return nil 343 } 344 345 releaseutil.Reverse(hist, releaseutil.SortByRevision) 346 last := hist[0] 347 348 // Update version to the next available 349 rel.Version = last.Version + 1 350 351 // Do not change the status of a failed release. 352 if last.Info.Status == release.StatusFailed { 353 return nil 354 } 355 356 // For any other status, mark it as superseded and store the old record 357 last.SetStatus(release.StatusSuperseded, "superseded by new release") 358 return i.recordRelease(last) 359 } 360 361 // renderResources renders the templates in a chart 362 func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, outputDir string) ([]*release.Hook, *bytes.Buffer, string, error) { 363 hs := []*release.Hook{} 364 b := bytes.NewBuffer(nil) 365 366 caps, err := c.getCapabilities() 367 if err != nil { 368 return hs, b, "", err 369 } 370 371 if ch.Metadata.KubeVersion != "" { 372 if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) { 373 return hs, b, "", errors.Errorf("chart requires kubernetesVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String()) 374 } 375 } 376 377 files, err := engine.Render(ch, values) 378 if err != nil { 379 return hs, b, "", err 380 } 381 382 // NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource, 383 // pull it out of here into a separate file so that we can actually use the output of the rendered 384 // text file. We have to spin through this map because the file contains path information, so we 385 // look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip 386 // it in the sortHooks. 387 notes := "" 388 for k, v := range files { 389 if strings.HasSuffix(k, notesFileSuffix) { 390 // Only apply the notes if it belongs to the parent chart 391 // Note: Do not use filePath.Join since it creates a path with \ which is not expected 392 if k == path.Join(ch.Name(), "templates", notesFileSuffix) { 393 notes = v 394 } 395 delete(files, k) 396 } 397 } 398 399 // Sort hooks, manifests, and partials. Only hooks and manifests are returned, 400 // as partials are not used after renderer.Render. Empty manifests are also 401 // removed here. 402 hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder) 403 if err != nil { 404 // By catching parse errors here, we can prevent bogus releases from going 405 // to Kubernetes. 406 // 407 // We return the files as a big blob of data to help the user debug parser 408 // errors. 409 for name, content := range files { 410 if strings.TrimSpace(content) == "" { 411 continue 412 } 413 fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content) 414 } 415 return hs, b, "", err 416 } 417 418 // Aggregate all valid manifests into one big doc. 419 fileWritten := make(map[string]bool) 420 for _, m := range manifests { 421 if outputDir == "" { 422 fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content) 423 } else { 424 err = writeToFile(outputDir, m.Name, m.Content, fileWritten[m.Name]) 425 if err != nil { 426 return hs, b, "", err 427 } 428 fileWritten[m.Name] = true 429 } 430 } 431 432 return hs, b, notes, nil 433 } 434 435 // write the <data> to <output-dir>/<name>. <append> controls if the file is created or content will be appended 436 func writeToFile(outputDir string, name string, data string, append bool) error { 437 outfileName := strings.Join([]string{outputDir, name}, string(filepath.Separator)) 438 439 err := ensureDirectoryForFile(outfileName) 440 if err != nil { 441 return err 442 } 443 444 f, err := createOrOpenFile(outfileName, append) 445 if err != nil { 446 return err 447 } 448 449 defer f.Close() 450 451 _, err = f.WriteString(fmt.Sprintf("---\n# Source: %s\n%s\n", name, data)) 452 453 if err != nil { 454 return err 455 } 456 457 fmt.Printf("wrote %s\n", outfileName) 458 return nil 459 } 460 461 func createOrOpenFile(filename string, append bool) (*os.File, error) { 462 if append { 463 return os.OpenFile(filename, os.O_APPEND|os.O_WRONLY, 0600) 464 } 465 return os.Create(filename) 466 } 467 468 // check if the directory exists to create file. creates if don't exists 469 func ensureDirectoryForFile(file string) error { 470 baseDir := path.Dir(file) 471 _, err := os.Stat(baseDir) 472 if err != nil && !os.IsNotExist(err) { 473 return err 474 } 475 476 return os.MkdirAll(baseDir, defaultDirectoryPermission) 477 } 478 479 // NameAndChart returns the name and chart that should be used. 480 // 481 // This will read the flags and handle name generation if necessary. 482 func (i *Install) NameAndChart(args []string) (string, string, error) { 483 flagsNotSet := func() error { 484 if i.GenerateName { 485 return errors.New("cannot set --generate-name and also specify a name") 486 } 487 if i.NameTemplate != "" { 488 return errors.New("cannot set --name-template and also specify a name") 489 } 490 return nil 491 } 492 493 if len(args) == 2 { 494 return args[0], args[1], flagsNotSet() 495 } 496 497 if i.NameTemplate != "" { 498 name, err := TemplateName(i.NameTemplate) 499 return name, args[0], err 500 } 501 502 if i.ReleaseName != "" { 503 return i.ReleaseName, args[0], nil 504 } 505 506 if !i.GenerateName { 507 return "", args[0], errors.New("must either provide a name or specify --generate-name") 508 } 509 510 base := filepath.Base(args[0]) 511 if base == "." || base == "" { 512 base = "chart" 513 } 514 515 return fmt.Sprintf("%s-%d", base, time.Now().Unix()), args[0], nil 516 } 517 518 // TemplateName renders a name template, returning the name or an error. 519 func TemplateName(nameTemplate string) (string, error) { 520 if nameTemplate == "" { 521 return "", nil 522 } 523 524 t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) 525 if err != nil { 526 return "", err 527 } 528 var b bytes.Buffer 529 if err := t.Execute(&b, nil); err != nil { 530 return "", err 531 } 532 533 return b.String(), nil 534 } 535 536 // CheckDependencies checks the dependencies for a chart. 537 func CheckDependencies(ch *chart.Chart, reqs []*chart.Dependency) error { 538 var missing []string 539 540 OUTER: 541 for _, r := range reqs { 542 for _, d := range ch.Dependencies() { 543 if d.Name() == r.Name { 544 continue OUTER 545 } 546 } 547 missing = append(missing, r.Name) 548 } 549 550 if len(missing) > 0 { 551 return errors.Errorf("found in Chart.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) 552 } 553 return nil 554 } 555 556 // LocateChart looks for a chart directory in known places, and returns either the full path or an error. 557 // 558 // This does not ensure that the chart is well-formed; only that the requested filename exists. 559 // 560 // Order of resolution: 561 // - relative to current working directory 562 // - if path is absolute or begins with '.', error out here 563 // - URL 564 // 565 // If 'verify' was set on ChartPathOptions, this will attempt to also verify the chart. 566 func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (string, error) { 567 name = strings.TrimSpace(name) 568 version := strings.TrimSpace(c.Version) 569 570 if _, err := os.Stat(name); err == nil { 571 abs, err := filepath.Abs(name) 572 if err != nil { 573 return abs, err 574 } 575 if c.Verify { 576 if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil { 577 return "", err 578 } 579 } 580 return abs, nil 581 } 582 if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { 583 return name, errors.Errorf("path %q not found", name) 584 } 585 586 dl := downloader.ChartDownloader{ 587 Out: os.Stdout, 588 Keyring: c.Keyring, 589 Getters: getter.All(settings), 590 Options: []getter.Option{ 591 getter.WithBasicAuth(c.Username, c.Password), 592 }, 593 RepositoryConfig: settings.RepositoryConfig, 594 RepositoryCache: settings.RepositoryCache, 595 } 596 if c.Verify { 597 dl.Verify = downloader.VerifyAlways 598 } 599 if c.RepoURL != "" { 600 chartURL, err := repo.FindChartInAuthRepoURL(c.RepoURL, c.Username, c.Password, name, version, 601 c.CertFile, c.KeyFile, c.CaFile, getter.All(settings)) 602 if err != nil { 603 return "", err 604 } 605 name = chartURL 606 } 607 608 if err := os.MkdirAll(settings.RepositoryCache, 0755); err != nil { 609 return "", err 610 } 611 612 filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache) 613 if err == nil { 614 lname, err := filepath.Abs(filename) 615 if err != nil { 616 return filename, err 617 } 618 return lname, nil 619 } else if settings.Debug { 620 return filename, err 621 } 622 623 return filename, errors.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) 624 }