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