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