github.com/jenkins-x/jx/v2@v2.1.155/pkg/helm/helm_helpers.go (about) 1 package helm 2 3 import ( 4 "bufio" 5 "bytes" 6 "encoding/base64" 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "net/url" 11 "os" 12 "path/filepath" 13 "regexp" 14 "sort" 15 "strconv" 16 "strings" 17 18 "github.com/jenkins-x/jx/v2/pkg/versionstream" 19 "gopkg.in/src-d/go-git.v4" 20 "gopkg.in/src-d/go-git.v4/config" 21 "gopkg.in/src-d/go-git.v4/plumbing" 22 23 survey "gopkg.in/AlecAivazis/survey.v1" 24 25 "github.com/google/uuid" 26 27 "github.com/jenkins-x/jx-logging/pkg/log" 28 "github.com/jenkins-x/jx/v2/pkg/kube" 29 "github.com/jenkins-x/jx/v2/pkg/secreturl" 30 "github.com/jenkins-x/jx/v2/pkg/table" 31 "github.com/jenkins-x/jx/v2/pkg/util" 32 "k8s.io/client-go/kubernetes" 33 34 "github.com/ghodss/yaml" 35 "github.com/pkg/errors" 36 "k8s.io/helm/pkg/chartutil" 37 "k8s.io/helm/pkg/proto/hapi/chart" 38 ) 39 40 const ( 41 // ChartFileName file name for a chart 42 ChartFileName = "Chart.yaml" 43 // RequirementsFileName the file name for helm requirements 44 RequirementsFileName = "requirements.yaml" 45 // SecretsFileName the file name for secrets 46 SecretsFileName = "secrets.yaml" 47 // ValuesFileName the file name for values 48 ValuesFileName = "values.yaml" 49 // ValuesTemplateFileName a templated values.yaml file which can refer to parameter expressions 50 ValuesTemplateFileName = "values.tmpl.yaml" 51 // TemplatesDirName is the default name for the templates directory 52 TemplatesDirName = "templates" 53 54 // ParametersYAMLFile contains logical parameters (values or secrets) which can be fetched from a Secret URL or 55 // inlined if not a secret which can be referenced from a 'values.yaml` file via a `{{ .Parameters.foo.bar }}` expression 56 ParametersYAMLFile = "parameters.yaml" 57 58 // FakeChartmusuem is the url for the fake chart museum used in tests 59 FakeChartmusuem = "http://fake.chartmuseum" 60 61 // DefaultEnvironmentChartDir is the default environment path where charts are stored 62 DefaultEnvironmentChartDir = "env" 63 64 //RepoVaultPath is the path to the repo credentials in Vault 65 RepoVaultPath = "helm/repos" 66 ) 67 68 // copied from helm to minimise dependencies... 69 70 // Dependency describes a chart upon which another chart depends. 71 // 72 // Dependencies can be used to express developer intent, or to capture the state 73 // of a chart. 74 type Dependency struct { 75 // Name is the name of the dependency. 76 // 77 // This must mach the name in the dependency's Chart.yaml. 78 Name string `json:"name"` 79 // Version is the version (range) of this chart. 80 // 81 // A lock file will always produce a single version, while a dependency 82 // may contain a semantic version range. 83 Version string `json:"version,omitempty"` 84 // The URL to the repository. 85 // 86 // Appending `index.yaml` to this string should result in a URL that can be 87 // used to fetch the repository index. 88 Repository string `json:"repository"` 89 // A yaml path that resolves to a boolean, used for enabling/disabling charts (e.g. subchart1.enabled ) 90 Condition string `json:"condition,omitempty"` 91 // Tags can be used to group charts for enabling/disabling together 92 Tags []string `json:"tags,omitempty"` 93 // Enabled bool determines if chart should be loaded 94 Enabled bool `json:"enabled,omitempty"` 95 // ImportValues holds the mapping of source values to parent key to be imported. Each item can be a 96 // string or pair of child/parent sublist items. 97 ImportValues []interface{} `json:"import-values,omitempty"` 98 // Alias usable alias to be used for the chart 99 Alias string `json:"alias,omitempty"` 100 } 101 102 // ErrNoRequirementsFile to detect error condition 103 type ErrNoRequirementsFile error 104 105 // Requirements is a list of requirements for a chart. 106 // 107 // Requirements are charts upon which this chart depends. This expresses 108 // developer intent. 109 type Requirements struct { 110 Dependencies []*Dependency `json:"dependencies"` 111 } 112 113 // DepSorter Used to avoid merge conflicts by sorting deps by name 114 type DepSorter []*Dependency 115 116 func (a DepSorter) Len() int { return len(a) } 117 func (a DepSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] } 118 func (a DepSorter) Less(i, j int) bool { return a[i].Name < a[j].Name } 119 120 // SetAppVersion sets the version of the app to use 121 func (r *Requirements) SetAppVersion(app string, version string, repository string, alias string) { 122 if r.Dependencies == nil { 123 r.Dependencies = []*Dependency{} 124 } 125 for _, dep := range r.Dependencies { 126 if dep != nil && dep.Name == app { 127 if version != dep.Version { 128 dep.Version = version 129 } 130 if repository != "" { 131 dep.Repository = repository 132 } 133 if alias != "" { 134 dep.Alias = alias 135 } 136 return 137 } 138 } 139 r.Dependencies = append(r.Dependencies, &Dependency{ 140 Name: app, 141 Version: version, 142 Repository: repository, 143 Alias: alias, 144 }) 145 sort.Sort(DepSorter(r.Dependencies)) 146 } 147 148 // RemoveApplication removes the given app name. Returns true if a dependency was removed 149 func (r *Requirements) RemoveApplication(app string) bool { 150 for i, dep := range r.Dependencies { 151 if dep != nil && dep.Name == app { 152 r.Dependencies = append(r.Dependencies[:i], r.Dependencies[i+1:]...) 153 sort.Sort(DepSorter(r.Dependencies)) 154 return true 155 } 156 } 157 return false 158 } 159 160 // FindRequirementsFileName returns the default requirements.yaml file name 161 func FindRequirementsFileName(dir string) (string, error) { 162 return findFileName(dir, RequirementsFileName) 163 } 164 165 // FindChartFileName returns the default chart.yaml file name 166 func FindChartFileName(dir string) (string, error) { 167 return findFileName(dir, ChartFileName) 168 } 169 170 // FindValuesFileName returns the default values.yaml file name 171 func FindValuesFileName(dir string) (string, error) { 172 return findFileName(dir, ValuesFileName) 173 } 174 175 // FindValuesFileNameForChart returns the values.yaml file name for a given chart within the environment or the default if the chart name is empty 176 func FindValuesFileNameForChart(dir string, chartName string) (string, error) { 177 //Chart name and file name are joined here to avoid hard coding the environment 178 //The chart name is ignored in the path if it's empty 179 return findFileName(dir, filepath.Join(chartName, ValuesFileName)) 180 } 181 182 // FindTemplatesDirName returns the default templates/ dir name 183 func FindTemplatesDirName(dir string) (string, error) { 184 return findFileName(dir, TemplatesDirName) 185 } 186 187 func findFileName(dir string, fileName string) (string, error) { 188 names := []string{ 189 filepath.Join(dir, DefaultEnvironmentChartDir, fileName), 190 filepath.Join(dir, fileName), 191 } 192 for _, name := range names { 193 exists, err := util.FileExists(name) 194 if err != nil { 195 return "", err 196 } 197 if exists { 198 return name, nil 199 } 200 } 201 files, err := ioutil.ReadDir(dir) 202 if err != nil { 203 return "", err 204 } 205 for _, f := range files { 206 if f.IsDir() { 207 name := filepath.Join(dir, f.Name(), fileName) 208 exists, err := util.FileExists(name) 209 if err != nil { 210 return "", err 211 } 212 if exists { 213 return name, nil 214 } 215 } 216 } 217 dirs := []string{ 218 filepath.Join(dir, DefaultEnvironmentChartDir), 219 dir, 220 } 221 for _, d := range dirs { 222 name := filepath.Join(d, fileName) 223 exists, err := util.DirExists(d) 224 if err != nil { 225 return "", err 226 } 227 if exists { 228 return name, nil 229 } 230 } 231 return "", fmt.Errorf("could not deduce the default requirements.yaml file name") 232 } 233 234 // LoadRequirementsFile loads the requirements file or creates empty requirements if the file does not exist 235 func LoadRequirementsFile(fileName string) (*Requirements, error) { 236 exists, err := util.FileExists(fileName) 237 if err != nil { 238 return nil, err 239 } 240 if exists { 241 data, err := ioutil.ReadFile(fileName) 242 if err != nil { 243 return nil, err 244 } 245 return LoadRequirements(data) 246 } 247 r := &Requirements{} 248 return r, nil 249 } 250 251 // LoadChartFile loads the chart file or creates empty chart if the file does not exist 252 func LoadChartFile(fileName string) (*chart.Metadata, error) { 253 exists, err := util.FileExists(fileName) 254 if err != nil { 255 return nil, err 256 } 257 if exists { 258 data, err := ioutil.ReadFile(fileName) 259 if err != nil { 260 return nil, err 261 } 262 return LoadChart(data) 263 } 264 return &chart.Metadata{}, nil 265 } 266 267 // LoadValuesFile loads the values file or creates empty map if the file does not exist 268 func LoadValuesFile(fileName string) (map[string]interface{}, error) { 269 exists, err := util.FileExists(fileName) 270 if err != nil { 271 return nil, errors.Wrapf(err, "checking %s exists", fileName) 272 } 273 if exists { 274 data, err := ioutil.ReadFile(fileName) 275 if err != nil { 276 return nil, errors.Wrapf(err, "reading %s", fileName) 277 } 278 v, err := LoadValues(data) 279 if err != nil { 280 return nil, errors.Wrapf(err, "unmarshaling %s", fileName) 281 } 282 return v, nil 283 } 284 return make(map[string]interface{}), nil 285 } 286 287 // LoadParametersValuesFile loads the parameters values file or creates empty map if the file does not exist 288 func LoadParametersValuesFile(dir string) (map[string]interface{}, error) { 289 return LoadValuesFile(filepath.Join(dir, "env", ParametersYAMLFile)) 290 } 291 292 // LoadTemplatesDir loads the files in the templates dir or creates empty map if none exist 293 func LoadTemplatesDir(dirName string) (map[string]string, error) { 294 exists, err := util.DirExists(dirName) 295 if err != nil { 296 return nil, err 297 } 298 answer := make(map[string]string) 299 if exists { 300 files, err := ioutil.ReadDir(dirName) 301 if err != nil { 302 return nil, err 303 } 304 for _, f := range files { 305 filename, _ := filepath.Split(f.Name()) 306 answer[filename] = f.Name() 307 } 308 } 309 return answer, nil 310 } 311 312 // LoadRequirements loads the requirements from some data 313 func LoadRequirements(data []byte) (*Requirements, error) { 314 r := &Requirements{} 315 return r, yaml.Unmarshal(data, r) 316 } 317 318 // LoadChart loads the requirements from some data 319 func LoadChart(data []byte) (*chart.Metadata, error) { 320 r := &chart.Metadata{} 321 return r, yaml.Unmarshal(data, r) 322 } 323 324 // LoadValues loads the values from some data 325 func LoadValues(data []byte) (map[string]interface{}, error) { 326 r := map[string]interface{}{} 327 if data == nil || len(data) == 0 { 328 return r, nil 329 } 330 return r, yaml.Unmarshal(data, &r) 331 } 332 333 // SaveFile saves contents (a pointer to a data structure) to a file 334 func SaveFile(fileName string, contents interface{}) error { 335 data, err := yaml.Marshal(contents) 336 if err != nil { 337 return errors.Wrapf(err, "failed to marshal helm file %s", fileName) 338 } 339 err = ioutil.WriteFile(fileName, data, util.DefaultWritePermissions) 340 if err != nil { 341 return errors.Wrapf(err, "failed to save helm file %s", fileName) 342 } 343 return nil 344 } 345 346 func LoadChartName(chartFile string) (string, error) { 347 chart, err := chartutil.LoadChartfile(chartFile) 348 if err != nil { 349 return "", err 350 } 351 return chart.Name, nil 352 } 353 354 func LoadChartNameAndVersion(chartFile string) (string, string, error) { 355 chart, err := chartutil.LoadChartfile(chartFile) 356 if err != nil { 357 return "", "", err 358 } 359 return chart.Name, chart.Version, nil 360 } 361 362 // ModifyChart modifies the given chart using a callback 363 func ModifyChart(chartFile string, fn func(chart *chart.Metadata) error) error { 364 chart, err := chartutil.LoadChartfile(chartFile) 365 if err != nil { 366 return errors.Wrapf(err, "Failed to load chart file %s", chartFile) 367 } 368 err = fn(chart) 369 if err != nil { 370 return errors.Wrapf(err, "Failed to modify chart for file %s", chartFile) 371 } 372 err = chartutil.SaveChartfile(chartFile, chart) 373 if err != nil { 374 return errors.Wrapf(err, "Failed to save modified chart file %s", chartFile) 375 } 376 return nil 377 } 378 379 // SetChartVersion modifies the given chart file to update the version 380 func SetChartVersion(chartFile string, version string) error { 381 callback := func(chart *chart.Metadata) error { 382 chart.Version = version 383 return nil 384 } 385 return ModifyChart(chartFile, callback) 386 } 387 388 func AppendMyValues(valueFiles []string) ([]string, error) { 389 // Overwrite the values with the content of myvalues.yaml files from the current folder if exists, otherwise 390 // from ~/.jx folder also only if it's present 391 curDir, err := os.Getwd() 392 if err != nil { 393 return nil, errors.Wrap(err, "failed to get the current working directory") 394 } 395 myValuesFile := filepath.Join(curDir, "myvalues.yaml") 396 exists, err := util.FileExists(myValuesFile) 397 if err != nil { 398 return nil, errors.Wrap(err, "failed to check if the myvaules.yaml file exists in the current directory") 399 } 400 if exists { 401 valueFiles = append(valueFiles, myValuesFile) 402 log.Logger().Infof("Using local value overrides file %s", util.ColorInfo(myValuesFile)) 403 } else { 404 configDir, err := util.ConfigDir() 405 if err != nil { 406 return nil, errors.Wrap(err, "failed to read the config directory") 407 } 408 myValuesFile = filepath.Join(configDir, "myvalues.yaml") 409 exists, err = util.FileExists(myValuesFile) 410 if err != nil { 411 return nil, errors.Wrap(err, "failed to check if the myvaules.yaml file exists in the .jx directory") 412 } 413 if exists { 414 valueFiles = append(valueFiles, myValuesFile) 415 log.Logger().Infof("Using local value overrides file %s", util.ColorInfo(myValuesFile)) 416 } 417 } 418 return valueFiles, nil 419 } 420 421 // CombineValueFilesToFile iterates through the input files and combines them into a single Values object and then 422 // write it to the output file nested inside the chartName 423 func CombineValueFilesToFile(outFile string, inputFiles []string, chartName string, extraValues map[string]interface{}) error { 424 answerMap := map[string]interface{}{} 425 426 // lets load any previous values if they exist 427 exists, err := util.FileExists(outFile) 428 if err != nil { 429 return err 430 } 431 if exists { 432 answerMap, err = LoadValuesFile(outFile) 433 if err != nil { 434 return err 435 } 436 } 437 438 // now lets merge any given input files 439 answer := chartutil.Values{} 440 for _, input := range inputFiles { 441 values, err := chartutil.ReadValuesFile(input) 442 if err != nil { 443 return errors.Wrapf(err, "Failed to read helm values YAML file %s", input) 444 } 445 sourceMap := answer.AsMap() 446 util.CombineMapTrees(sourceMap, values.AsMap()) 447 answer = chartutil.Values(sourceMap) 448 } 449 m := answer.AsMap() 450 for k, v := range extraValues { 451 m[k] = v 452 } 453 answerMap[chartName] = m 454 answer = chartutil.Values(answerMap) 455 text, err := answer.YAML() 456 if err != nil { 457 return errors.Wrap(err, "Failed to marshal the combined values YAML files back to YAML") 458 } 459 err = ioutil.WriteFile(outFile, []byte(text), util.DefaultWritePermissions) 460 if err != nil { 461 return errors.Wrapf(err, "Failed to save combined helm values YAML file %s", outFile) 462 } 463 return nil 464 } 465 466 // IsGitURL tests whether the given URL is a git URL. 467 // The given URL supports <git remote url>#<branch/commit/tag> 468 // example: github.com/jenkins-x/jx#master, github.com/jenkins-x/jx#v3, github.com/jenkins-x/jx#2647027a83b543ffb886ee96dc413f860f79615d 469 func IsGitURL(url string) (bool, error) { 470 re, err := regexp.Compile(`((git|ssh|http(s)?)|(git@[\w.]+))(:(//)?)([\w.@:/\-~]+)(\.git)(/)?`) 471 if err != nil { 472 return false, err 473 } 474 return re.MatchString(url), nil 475 } 476 477 // IsCommitSHA tests whether the given s string is a commit SHA. 478 func IsCommitSHA(s string) (bool, error) { 479 re, err := regexp.Compile("^[a-f0-9]{5,40}$") 480 if err != nil { 481 return false, err 482 } 483 return re.MatchString(s), nil 484 } 485 486 // FetchChartFromGit fetch a helm chart from the given git URL, the URL support <git remote url>#<branch/commit/tag>. 487 func FetchChartFromGit(path, url string) error { 488 ok, err := IsGitURL(url) 489 if err != nil { 490 return err 491 } 492 if !ok { 493 return errors.New(fmt.Sprintf("The %s is not Git URL", url)) 494 } 495 496 ops := &git.CloneOptions{} 497 s := strings.SplitN(url, "#", 2) 498 499 ops.URL = s[0] 500 501 r, err := git.PlainClone(path, false, ops) 502 if err != nil { 503 return err 504 } 505 506 if len(s) == 1 { 507 return nil 508 } 509 510 w, err := r.Worktree() 511 if err != nil { 512 return err 513 } 514 515 // try checkout commit 516 isCommit, err := IsCommitSHA(s[1]) 517 if err != nil { 518 return err 519 } 520 if isCommit { 521 err = w.Checkout(&git.CheckoutOptions{Hash: plumbing.NewHash(s[1])}) 522 if err != nil { 523 return err 524 } 525 return nil 526 } 527 528 // try checkout from tag 529 ref, err := r.Tag(s[1]) 530 if err == nil { 531 err = w.Checkout(&git.CheckoutOptions{Hash: ref.Hash()}) 532 if err != nil { 533 return err 534 } 535 return nil 536 } 537 538 // try checkout from branch 539 err = r.Fetch(&git.FetchOptions{ 540 RefSpecs: []config.RefSpec{"refs/*:refs/*", "HEAD:refs/heads/HEAD"}, 541 }) 542 if err != nil { 543 return err 544 } 545 err = w.Checkout(&git.CheckoutOptions{Branch: plumbing.NewBranchReferenceName(s[1])}) 546 547 return err 548 } 549 550 // IsLocal returns whether this chart is being installed from the local filesystem or not 551 func IsLocal(chart string) bool { 552 b := strings.HasPrefix(chart, "/") || strings.HasPrefix(chart, ".") || strings.Count(chart, "/") > 1 553 if !b { 554 // check if file exists, then it's local 555 exists, err := util.FileExists(chart) 556 if err == nil { 557 return exists 558 } 559 } 560 return b 561 } 562 563 // InspectChart fetches the specified chart in a repo using helmer, and then calls the closure on it, before cleaning up 564 func InspectChart(chart string, version string, repo string, username string, password string, 565 helmer Helmer, inspector func(dir string) error) error { 566 isLocal := IsLocal(chart) 567 dir, err := ioutil.TempDir("", "jx-helm-fetch-") 568 if err != nil { 569 return errors.Wrapf(err, "creating tempdir") 570 } 571 572 chartFromGit, err := IsGitURL(chart) 573 574 if err != nil { 575 return errors.Wrapf(err, "checking git URL") 576 } 577 578 defer func() { 579 err1 := os.RemoveAll(dir) 580 if err1 != nil { 581 log.Logger().Warnf("Error removing %s %v", dir, err1) 582 } 583 }() 584 parts := strings.Split(chart, "/") 585 inspectPath := filepath.Join(dir, parts[len(parts)-1]) 586 587 if chartFromGit { 588 inspectPath = dir 589 downloadDir, err := ioutil.TempDir("", "jx-helm-fetch-git-") 590 defer func() { 591 err1 := os.RemoveAll(downloadDir) 592 if err1 != nil { 593 log.Logger().Warnf("Error removing %s %v", dir, err1) 594 } 595 }() 596 if err != nil { 597 return errors.Wrapf(err, "creating git repository tempdir") 598 } 599 600 err = FetchChartFromGit(downloadDir, chart) 601 if err != nil { 602 return errors.Wrapf(err, "fetching git repository") 603 } 604 605 var chartsDir []string 606 err = util.GlobAllFiles("", filepath.Join(downloadDir, "*", ChartFileName), func(path string) error { 607 chartsDir = append(chartsDir, filepath.Dir(path)) 608 return nil 609 }) 610 if err != nil { 611 return err 612 } 613 614 if len(chartsDir) == 0 { 615 return errors.New("Not found Helm chart") 616 } 617 618 if len(chartsDir) != 1 { 619 return errors.New("Cannot install multiple Helm chart") 620 } 621 622 err = util.CopyDir(chartsDir[0], dir, true) 623 if err != nil { 624 return errors.Wrapf(err, "copying %s to %s", chart, dir) 625 } 626 helmer.SetCWD(dir) 627 } else if isLocal { 628 // This is a local path 629 err := util.CopyDir(chart, dir, true) 630 if err != nil { 631 return errors.Wrapf(err, "copying %s to %s", chart, dir) 632 } 633 helmer.SetCWD(dir) 634 } else { 635 err = helmer.FetchChart(chart, version, true, dir, repo, username, password) 636 if err != nil { 637 return err 638 } 639 } 640 return inspector(inspectPath) 641 } 642 643 type InstallChartOptions struct { 644 Dir string 645 ReleaseName string 646 Chart string 647 Version string 648 Ns string 649 HelmUpdate bool 650 SetValues []string 651 SetStrings []string 652 ValueFiles []string 653 Repository string 654 Username string 655 Password string 656 VersionsDir string 657 VersionsGitURL string 658 VersionsGitRef string 659 InstallOnly bool 660 NoForce bool 661 Wait bool 662 UpgradeOnly bool 663 } 664 665 // InstallFromChartOptions uses the helmer and kubeClient interfaces to install the chart from the options, 666 // respecting the installTimeout, looking up or updating Vault with the username and password for the repo. 667 // If secretURLClient is nil then username and passwords for repos will not be looked up in Vault. 668 func InstallFromChartOptions(options InstallChartOptions, helmer Helmer, kubeClient kubernetes.Interface, 669 installTimeout string, secretURLClient secreturl.Client) error { 670 chart := options.Chart 671 if options.Version == "" { 672 versionsDir := options.VersionsDir 673 if versionsDir == "" { 674 return errors.Errorf("no VersionsDir specified when trying to install a chart") 675 } 676 var err error 677 options.Version, err = versionstream.LoadStableVersionNumber(versionsDir, versionstream.KindChart, chart) 678 if err != nil { 679 return errors.Wrapf(err, "failed to load stable version in dir %s for chart %s", versionsDir, chart) 680 } 681 } 682 if options.HelmUpdate { 683 log.Logger().Debugf("Updating Helm repository...") 684 err := helmer.UpdateRepo() 685 if err != nil { 686 return errors.Wrap(err, "failed to update repository") 687 } 688 log.Logger().Debugf("Helm repository update done.") 689 } 690 cleanup, err := options.DecorateWithSecrets(secretURLClient) 691 defer cleanup() //nolint:errcheck 692 if err != nil { 693 return errors.WithStack(err) 694 } 695 if options.Ns != "" { 696 annotations := map[string]string{"jenkins-x.io/created-by": "Jenkins X"} 697 err = kube.EnsureNamespaceCreated(kubeClient, options.Ns, nil, annotations) 698 if err != nil { 699 return errors.Wrap(err, "error creating namespace") 700 } 701 } 702 timeout, err := strconv.Atoi(installTimeout) 703 if err != nil { 704 return errors.Wrap(err, "failed to convert the timeout to an int") 705 } 706 helmer.SetCWD(options.Dir) 707 if options.InstallOnly { 708 return helmer.InstallChart(chart, options.ReleaseName, options.Ns, options.Version, timeout, 709 options.SetValues, options.SetStrings, options.ValueFiles, options.Repository, options.Username, options.Password) 710 } 711 return helmer.UpgradeChart(chart, options.ReleaseName, options.Ns, options.Version, !options.UpgradeOnly, timeout, 712 !options.NoForce, options.Wait, options.SetValues, options.SetStrings, options.ValueFiles, options.Repository, 713 options.Username, options.Password) 714 } 715 716 // HelmRepoCredentials is a map of repositories to HelmRepoCredential that stores all the helm repo credentials for 717 // the cluster 718 type HelmRepoCredentials map[string]HelmRepoCredential 719 720 // HelmRepoCredential is a username and password pair that can ben used to authenticated against a Helm repo 721 type HelmRepoCredential struct { 722 Username string `json:"username"` 723 Password string `json:"password"` 724 } 725 726 // DecorateWithSecrets will replace any vault: URIs with the secret from vault. Safe to call with a nil client ( 727 // no replacement will take place). 728 func (options *InstallChartOptions) DecorateWithSecrets(secretURLClient secreturl.Client) (func(), error) { 729 newValuesFiles, cleanup, err := DecorateWithSecrets(options.ValueFiles, secretURLClient) 730 if err != nil { 731 return cleanup, errors.WithStack(err) 732 } 733 options.ValueFiles = newValuesFiles 734 return cleanup, nil 735 } 736 737 // DecorateWithSecrets will replace any vault: URIs with the secret from vault. Safe to call with a nil client ( 738 // no replacement will take place). 739 func DecorateWithSecrets(valueFiles []string, secretURLClient secreturl.Client) ([]string, func(), error) { 740 cleanup := func() { 741 } 742 newValuesFiles := make([]string, 0) 743 if secretURLClient != nil { 744 745 cleanup = func() { 746 for _, f := range newValuesFiles { 747 err := util.DeleteFile(f) 748 if err != nil { 749 log.Logger().Errorf("Deleting temp file %s", f) 750 } 751 } 752 } 753 for _, valueFile := range valueFiles { 754 newValuesFile, err := ioutil.TempFile("", "values.yaml") 755 if err != nil { 756 return nil, cleanup, errors.Wrapf(err, "creating temp file for %s", valueFile) 757 } 758 bytes, err := ioutil.ReadFile(valueFile) 759 if err != nil { 760 return nil, cleanup, errors.Wrapf(err, "reading file %s", valueFile) 761 } 762 newValues := string(bytes) 763 if secretURLClient != nil { 764 newValues, err = secretURLClient.ReplaceURIs(newValues) 765 if err != nil { 766 return nil, cleanup, errors.Wrapf(err, "replacing vault URIs") 767 } 768 } 769 err = ioutil.WriteFile(newValuesFile.Name(), []byte(newValues), 0600) 770 if err != nil { 771 return nil, cleanup, errors.Wrapf(err, "writing new values file %s", newValuesFile.Name()) 772 } 773 newValuesFiles = append(newValuesFiles, newValuesFile.Name()) 774 } 775 } 776 return newValuesFiles, cleanup, nil 777 } 778 779 // LoadParameters loads the 'parameters.yaml' file if it exists in the current directory 780 func LoadParameters(dir string, secretURLClient secreturl.Client) (chartutil.Values, error) { 781 fileName := filepath.Join(dir, ParametersYAMLFile) 782 exists, err := util.FileExists(fileName) 783 if err != nil { 784 return nil, errors.Wrapf(err, "checking %s exists", fileName) 785 } 786 m := map[string]interface{}{} 787 if exists { 788 data, err := ioutil.ReadFile(fileName) 789 if err != nil { 790 return nil, errors.Wrapf(err, "reading %s", fileName) 791 } 792 if secretURLClient != nil { 793 text, err := secretURLClient.ReplaceURIs(string(data)) 794 if err != nil { 795 return nil, errors.Wrapf(err, "failed to convert secret URLs in parameters file %s", fileName) 796 } 797 data = []byte(text) 798 } 799 800 m, err = LoadValues(data) 801 if err != nil { 802 return nil, errors.Wrapf(err, "unmarshaling %s", fileName) 803 } 804 } 805 return chartutil.Values(m), err 806 } 807 808 // AddHelmRepoIfMissing will add the helm repo if there is no helm repo with that url present. 809 // It will generate the repoName from the url (using the host name) if the repoName is empty. 810 // The repo name may have a suffix added in order to prevent name collisions, and is returned for this reason. 811 // The username and password will be stored in vault for the URL (if vault is enabled). 812 func AddHelmRepoIfMissing(helmURL, repoName, username, password string, helmer Helmer, 813 secretURLClient secreturl.Client, handles util.IOFileHandles) (string, error) { 814 missing, existingName, err := helmer.IsRepoMissing(helmURL) 815 if err != nil { 816 return "", errors.Wrapf(err, "failed to check if the repository with URL '%s' is missing", helmURL) 817 } 818 if missing { 819 if repoName == "" { 820 // Generate the name 821 uri, err := url.Parse(helmURL) 822 if err != nil { 823 repoName = uuid.New().String() 824 log.Logger().Warnf("Unable to parse %s as URL so assigning random name %s", helmURL, repoName) 825 } else { 826 repoName = uri.Hostname() 827 } 828 } 829 // Avoid collisions 830 existingRepos, err := helmer.ListRepos() 831 if err != nil { 832 return "", errors.Wrapf(err, "listing helm repos") 833 } 834 baseName := repoName 835 for i := 0; true; i++ { 836 if _, exists := existingRepos[repoName]; exists { 837 repoName = fmt.Sprintf("%s-%d", baseName, i) 838 } else { 839 break 840 } 841 } 842 log.Logger().Infof("Adding missing Helm repo: %s %s", util.ColorInfo(repoName), util.ColorInfo(helmURL)) 843 username, password, err = DecorateWithCredentials(helmURL, username, password, secretURLClient, handles) 844 if err != nil { 845 return "", errors.WithStack(err) 846 } 847 err = helmer.AddRepo(repoName, helmURL, username, password) 848 if err != nil { 849 return "", errors.Wrapf(err, "failed to add the repository '%s' with URL '%s'", repoName, helmURL) 850 } 851 log.Logger().Infof("Successfully added Helm repository %s.", repoName) 852 } else { 853 repoName = existingName 854 } 855 return repoName, nil 856 } 857 858 // DecorateWithCredentials will, if vault is installed, store or replace the username or password 859 func DecorateWithCredentials(repo string, username string, password string, secretURLClient secreturl.Client, handles util.IOFileHandles) (string, 860 string, error) { 861 if repo != "" && secretURLClient != nil { 862 creds := HelmRepoCredentials{} 863 if err := secretURLClient.ReadObject(RepoVaultPath, &creds); err != nil { 864 log.Logger().Warnf("No secrets found on %q due: %s", RepoVaultPath, err) 865 } 866 var existingCred, cred HelmRepoCredential 867 if c, ok := creds[repo]; ok { 868 existingCred = c 869 } 870 if username != "" || password != "" { 871 cred = HelmRepoCredential{ 872 Username: username, 873 Password: password, 874 } 875 } else { 876 cred = existingCred 877 } 878 879 err := PromptForRepoCredsIfNeeded(repo, &cred, handles) 880 if err != nil { 881 return "", "", errors.Wrapf(err, "prompting for creds for %s", repo) 882 } 883 884 if cred.Password != existingCred.Password || cred.Username != existingCred.Username { 885 log.Logger().Infof("Storing credentials for %s in vault %s", repo, RepoVaultPath) 886 creds[repo] = cred 887 _, err := secretURLClient.WriteObject(RepoVaultPath, creds) 888 if err != nil { 889 return "", "", errors.Wrapf(err, "updating repo credentials in vault %s", RepoVaultPath) 890 } 891 } else { 892 log.Logger().Infof("Read credentials for %s from vault %s", repo, RepoVaultPath) 893 } 894 return cred.Username, cred.Password, nil 895 } 896 cred := HelmRepoCredential{ 897 Username: username, 898 Password: password, 899 } 900 err := PromptForRepoCredsIfNeeded(repo, &cred, handles) 901 if err != nil { 902 return "", "", errors.Wrapf(err, "prompting for creds for %s", repo) 903 } 904 return cred.Username, cred.Password, nil 905 } 906 907 // GenerateReadmeForChart generates a string that can be used as a README.MD, 908 // and includes info on the chart. 909 func GenerateReadmeForChart(name string, version string, description string, chartRepo string, 910 gitRepo string, releaseNotesURL string, appReadme string) string { 911 var readme strings.Builder 912 readme.WriteString(fmt.Sprintf("# %s\n\n|App Metadata||\n", unknownZeroValue(name))) 913 readme.WriteString("|---|---|\n") 914 if version != "" { 915 readme.WriteString(fmt.Sprintf("| **Version** | %s |\n", version)) 916 } 917 if description != "" { 918 readme.WriteString(fmt.Sprintf("| **Description** | %s |\n", description)) 919 } 920 if chartRepo != "" { 921 readme.WriteString(fmt.Sprintf("| **Chart Repository** | %s |\n", chartRepo)) 922 } 923 if gitRepo != "" { 924 readme.WriteString(fmt.Sprintf("| **Git Repository** | %s |\n", gitRepo)) 925 } 926 if releaseNotesURL != "" { 927 readme.WriteString(fmt.Sprintf("| **Release Notes** | %s |\n", releaseNotesURL)) 928 } 929 930 if appReadme != "" { 931 readme.WriteString(fmt.Sprintf("\n## App README.MD\n\n%s\n", appReadme)) 932 } 933 return readme.String() 934 } 935 936 func unknownZeroValue(value string) string { 937 if value == "" { 938 return "unknown" 939 } 940 return value 941 942 } 943 944 // SetValuesToMap converts the set of values of the form "foo.bar=123" into a helm values.yaml map structure 945 func SetValuesToMap(setValues []string) map[string]interface{} { 946 answer := map[string]interface{}{} 947 for _, setValue := range setValues { 948 tokens := strings.SplitN(setValue, "=", 2) 949 if len(tokens) > 1 { 950 path := tokens[0] 951 value := tokens[1] 952 953 // lets assume false is a boolean 954 if value == "false" { 955 util.SetMapValueViaPath(answer, path, false) 956 957 } else { 958 util.SetMapValueViaPath(answer, path, value) 959 } 960 } 961 } 962 return answer 963 } 964 965 // PromptForRepoCredsIfNeeded will prompt for repo credentials if required. It first checks the existing cred ( 966 // if any) and then prompts for new credentials up to 3 times, trying each set. 967 func PromptForRepoCredsIfNeeded(repo string, cred *HelmRepoCredential, handles util.IOFileHandles) error { 968 if repo == FakeChartmusuem || handles.In == nil || handles.Out == nil || handles.Err == nil { 969 // Avoid doing this in tests! 970 return nil 971 } 972 u := fmt.Sprintf("%s/index.yaml", strings.TrimSuffix(repo, "/")) 973 974 httpClient := &http.Client{} 975 surveyOpts := survey.WithStdio(handles.In, handles.Out, handles.Err) 976 if cred.Username == "" && cred.Password == "" { 977 // Try without any auth 978 req, err := http.NewRequest("GET", u, nil) 979 if err != nil { 980 return errors.Wrapf(err, "creating GET request to %s", u) 981 } 982 resp, err := httpClient.Do(req) 983 if err != nil { 984 return errors.Wrapf(err, "checking status code of %s", u) 985 } 986 if resp.StatusCode == 200 { 987 return nil 988 } 989 } 990 for i := 0; true; i++ { 991 req, err := http.NewRequest("GET", u, nil) 992 if err != nil { 993 return errors.Wrapf(err, "creating GET request to %s", u) 994 } 995 auth := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("%s:%s", cred.Username, cred.Password))) 996 req.Header.Add("Authorization", fmt.Sprintf("Basic %s", auth)) 997 resp, err := httpClient.Do(req) 998 if err != nil { 999 return errors.Wrapf(err, "checking status code of %s", u) 1000 } 1001 if i == 4 { 1002 return errors.Errorf("username and password for %s not valid", repo) 1003 } else if resp.StatusCode == 401 { 1004 if cred.Username != "" || cred.Password != "" { 1005 log.Logger().Errorf("Authentication for %s failed (%s/%s)", repo, cred.Username, strings.Repeat("*", 1006 len(cred.Password))) 1007 } 1008 usernamePrompt := survey.Input{ 1009 Message: "Repository username", 1010 Default: cred.Username, 1011 Help: fmt.Sprintf("Enter the username for %s", repo), 1012 } 1013 err := survey.AskOne(&usernamePrompt, &cred.Username, nil, surveyOpts) 1014 if err != nil { 1015 return errors.Wrapf(err, "asking for username") 1016 } 1017 passwordPrompt := survey.Password{ 1018 Message: "Repository password", 1019 Help: fmt.Sprintf("Enter the password for %s", repo), 1020 } 1021 err = survey.AskOne(&passwordPrompt, &cred.Password, nil, surveyOpts) 1022 if err != nil { 1023 return errors.Wrapf(err, "asking for password") 1024 } 1025 } else { 1026 break 1027 } 1028 } 1029 return nil 1030 } 1031 1032 // RenderReleasesAsTable lists the current releases in a table 1033 func RenderReleasesAsTable(releases map[string]ReleaseSummary, sortedKeys []string) (string, error) { 1034 var buffer bytes.Buffer 1035 writer := bufio.NewWriter(&buffer) 1036 t := table.CreateTable(writer) 1037 t.Separator = "\t" 1038 t.AddRow("NAME", "REVISION", "UPDATED", "STATUS", "CHART", "APP VERSION", "NAMESPACE") 1039 for _, key := range sortedKeys { 1040 info := releases[key] 1041 t.AddRow(info.ReleaseName, info.Revision, info.Updated, info.Status, info.ChartFullName, info.AppVersion, 1042 info.Namespace) 1043 } 1044 t.Render() 1045 writer.Flush() 1046 return buffer.String(), nil 1047 } 1048 1049 // UpdateRequirementsToNewVersion update dependencies with name to newVersion, returning the oldVersions 1050 func UpdateRequirementsToNewVersion(requirements *Requirements, name string, newVersion string) []string { 1051 answer := make([]string, 0) 1052 for _, dependency := range requirements.Dependencies { 1053 if dependency.Name == name { 1054 answer = append(answer, dependency.Version) 1055 dependency.Version = newVersion 1056 } 1057 } 1058 return answer 1059 } 1060 1061 // UpdateImagesInValuesToNewVersion update a (values) file, replacing that start with "Image: <name>:" to "Image: <name>:<newVersion>", 1062 // returning the oldVersions 1063 func UpdateImagesInValuesToNewVersion(data []byte, name string, newVersion string) ([]byte, []string) { 1064 oldVersions := make([]string, 0) 1065 var answer strings.Builder 1066 linePrefix := fmt.Sprintf("Image: %s:", name) 1067 for _, line := range strings.Split(string(data), "\n") { 1068 trimmedLine := strings.TrimSpace(line) 1069 if strings.HasPrefix(trimmedLine, linePrefix) { 1070 oldVersions = append(oldVersions, strings.TrimPrefix(trimmedLine, linePrefix)) 1071 answer.WriteString(linePrefix) 1072 answer.WriteString(newVersion) 1073 } else { 1074 answer.WriteString(line) 1075 } 1076 answer.WriteString("\n") 1077 } 1078 return []byte(answer.String()), oldVersions 1079 } 1080 1081 // FindLatestChart uses helmer to find the latest chart for name 1082 func FindLatestChart(name string, helmer Helmer) (*ChartSummary, error) { 1083 info, err := helmer.SearchCharts(name, true) 1084 if err != nil { 1085 return nil, err 1086 } 1087 if len(info) == 0 { 1088 return nil, fmt.Errorf("no version found for chart %s", name) 1089 } 1090 log.Logger().Debugf("found %d versions: %#v", len(info), info) 1091 return &info[0], nil 1092 }