github.com/koderover/helm@v2.17.0+incompatible/cmd/helm/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 main 18 19 import ( 20 "bytes" 21 "errors" 22 "fmt" 23 "io" 24 "io/ioutil" 25 "net/url" 26 "os" 27 "path/filepath" 28 "strings" 29 "text/template" 30 31 "github.com/Masterminds/sprig" 32 "github.com/ghodss/yaml" 33 "github.com/spf13/cobra" 34 35 "k8s.io/apimachinery/pkg/util/validation" 36 "k8s.io/helm/pkg/chartutil" 37 "k8s.io/helm/pkg/downloader" 38 "k8s.io/helm/pkg/getter" 39 "k8s.io/helm/pkg/helm" 40 "k8s.io/helm/pkg/kube" 41 "k8s.io/helm/pkg/proto/hapi/release" 42 "k8s.io/helm/pkg/renderutil" 43 "k8s.io/helm/pkg/repo" 44 "k8s.io/helm/pkg/strvals" 45 ) 46 47 const installDesc = ` 48 This command installs a chart archive. 49 50 The install argument must be a chart reference, a path to a packaged chart, 51 a path to an unpacked chart directory or a URL. 52 53 To override values in a chart, use either the '--values' flag and pass in a file 54 or use the '--set' flag and pass configuration from the command line. To force string 55 values in '--set', use '--set-string' instead. In case a value is large and therefore 56 you want not to use neither '--values' nor '--set', use '--set-file' to read the 57 single large value from file. 58 59 $ helm install -f myvalues.yaml ./redis 60 61 or 62 63 $ helm install --set name=prod ./redis 64 65 or 66 67 $ helm install --set-string long_int=1234567890 ./redis 68 69 or 70 $ helm install --set-file multiline_text=path/to/textfile 71 72 You can specify the '--values'/'-f' flag multiple times. The priority will be given to the 73 last (right-most) file specified. For example, if both myvalues.yaml and override.yaml 74 contained a key called 'Test', the value set in override.yaml would take precedence: 75 76 $ helm install -f myvalues.yaml -f override.yaml ./redis 77 78 You can specify the '--set' flag multiple times. The priority will be given to the 79 last (right-most) set specified. For example, if both 'bar' and 'newbar' values are 80 set for a key called 'foo', the 'newbar' value would take precedence: 81 82 $ helm install --set foo=bar --set foo=newbar ./redis 83 84 85 To check the generated manifests of a release without installing the chart, 86 the '--debug' and '--dry-run' flags can be combined. This will still require a 87 round-trip to the Tiller server. 88 89 If --verify is set, the chart MUST have a provenance file, and the provenance 90 file MUST pass all verification steps. 91 92 There are five different ways you can express the chart you want to install: 93 94 1. By chart reference: helm install stable/mariadb 95 2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz 96 3. By path to an unpacked chart directory: helm install ./nginx 97 4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz 98 5. By chart reference and repo url: helm install --repo https://example.com/charts/ nginx 99 100 CHART REFERENCES 101 102 A chart reference is a convenient way of reference a chart in a chart repository. 103 104 When you use a chart reference with a repo prefix ('stable/mariadb'), Helm will look in the local 105 configuration for a chart repository named 'stable', and will then look for a 106 chart in that repository whose name is 'mariadb'. It will install the latest 107 version of that chart unless you also supply a version number with the 108 '--version' flag. 109 110 To see the list of chart repositories, use 'helm repo list'. To search for 111 charts in a repository, use 'helm search'. 112 ` 113 114 type installCmd struct { 115 name string 116 namespace string 117 valueFiles valueFiles 118 chartPath string 119 dryRun bool 120 disableHooks bool 121 disableCRDHook bool 122 replace bool 123 verify bool 124 keyring string 125 out io.Writer 126 client helm.Interface 127 values []string 128 stringValues []string 129 fileValues []string 130 nameTemplate string 131 version string 132 timeout int64 133 wait bool 134 atomic bool 135 repoURL string 136 username string 137 password string 138 devel bool 139 depUp bool 140 subNotes bool 141 description string 142 143 certFile string 144 keyFile string 145 caFile string 146 output string 147 } 148 149 type valueFiles []string 150 151 func (v *valueFiles) String() string { 152 return fmt.Sprint(*v) 153 } 154 155 func (v *valueFiles) Type() string { 156 return "valueFiles" 157 } 158 159 func (v *valueFiles) Set(value string) error { 160 for _, filePath := range strings.Split(value, ",") { 161 *v = append(*v, filePath) 162 } 163 return nil 164 } 165 166 func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { 167 inst := &installCmd{ 168 out: out, 169 client: c, 170 } 171 172 cmd := &cobra.Command{ 173 Use: "install [CHART]", 174 Short: "Install a chart archive", 175 Long: installDesc, 176 PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, 177 RunE: func(cmd *cobra.Command, args []string) error { 178 if err := checkArgsLength(len(args), "chart name"); err != nil { 179 return err 180 } 181 182 debug("Original chart version: %q", inst.version) 183 if inst.version == "" && inst.devel { 184 debug("setting version to >0.0.0-0") 185 inst.version = ">0.0.0-0" 186 } 187 188 cp, err := locateChartPath(inst.repoURL, inst.username, inst.password, args[0], inst.version, inst.verify, inst.keyring, 189 inst.certFile, inst.keyFile, inst.caFile) 190 if err != nil { 191 return err 192 } 193 inst.chartPath = cp 194 inst.client = ensureHelmClient(inst.client) 195 inst.wait = inst.wait || inst.atomic 196 197 return inst.run() 198 }, 199 } 200 201 f := cmd.Flags() 202 settings.AddFlagsTLS(f) 203 f.VarP(&inst.valueFiles, "values", "f", "Specify values in a YAML file or a URL(can specify multiple)") 204 f.StringVarP(&inst.name, "name", "n", "", "The release name. If unspecified, it will autogenerate one for you") 205 f.StringVar(&inst.namespace, "namespace", "", "Namespace to install the release into. Defaults to the current kube config namespace.") 206 f.BoolVar(&inst.dryRun, "dry-run", false, "Simulate an install") 207 f.BoolVar(&inst.disableHooks, "no-hooks", false, "Prevent hooks from running during install") 208 f.BoolVar(&inst.disableCRDHook, "no-crd-hook", false, "Prevent CRD hooks from running, but run other hooks") 209 f.BoolVar(&inst.replace, "replace", false, "Re-use the given name, even if that name is already used. This is unsafe in production") 210 f.StringArrayVar(&inst.values, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 211 f.StringArrayVar(&inst.stringValues, "set-string", []string{}, "Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 212 f.StringArrayVar(&inst.fileValues, "set-file", []string{}, "Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2)") 213 f.StringVar(&inst.nameTemplate, "name-template", "", "Specify template used to name the release") 214 f.BoolVar(&inst.verify, "verify", false, "Verify the package before installing it") 215 f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "Location of public keys used for verification") 216 f.StringVar(&inst.version, "version", "", "Specify the exact chart version to install. If this is not specified, the latest version is installed") 217 f.Int64Var(&inst.timeout, "timeout", 300, "Time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") 218 f.BoolVar(&inst.wait, "wait", false, "If set, will wait until all Pods, PVCs, Services, and minimum number of Pods of a Deployment are in a ready state before marking the release as successful. It will wait for as long as --timeout") 219 f.BoolVar(&inst.atomic, "atomic", false, "If set, installation process purges chart on fail, also sets --wait flag") 220 f.StringVar(&inst.repoURL, "repo", "", "Chart repository url where to locate the requested chart") 221 f.StringVar(&inst.username, "username", "", "Chart repository username where to locate the requested chart") 222 f.StringVar(&inst.password, "password", "", "Chart repository password where to locate the requested chart") 223 f.StringVar(&inst.certFile, "cert-file", "", "Identify HTTPS client using this SSL certificate file") 224 f.StringVar(&inst.keyFile, "key-file", "", "Identify HTTPS client using this SSL key file") 225 f.StringVar(&inst.caFile, "ca-file", "", "Verify certificates of HTTPS-enabled servers using this CA bundle") 226 f.BoolVar(&inst.devel, "devel", false, "Use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") 227 f.BoolVar(&inst.depUp, "dep-up", false, "Run helm dependency update before installing the chart") 228 f.BoolVar(&inst.subNotes, "render-subchart-notes", false, "Render subchart notes along with the parent") 229 f.StringVar(&inst.description, "description", "", "Specify a description for the release") 230 bindOutputFlag(cmd, &inst.output) 231 232 // set defaults from environment 233 settings.InitTLS(f) 234 235 return cmd 236 } 237 238 func (i *installCmd) run() error { 239 debug("CHART PATH: %s\n", i.chartPath) 240 241 if i.namespace == "" { 242 i.namespace = defaultNamespace() 243 } 244 245 rawVals, err := vals(i.valueFiles, i.values, i.stringValues, i.fileValues, i.certFile, i.keyFile, i.caFile) 246 if err != nil { 247 return err 248 } 249 250 // If template is specified, try to run the template. 251 if i.nameTemplate != "" { 252 i.name, err = generateName(i.nameTemplate) 253 if err != nil { 254 return err 255 } 256 // Print the final name so the user knows what the final name of the release is. 257 fmt.Printf("FINAL NAME: %s\n", i.name) 258 } 259 260 if msgs := validation.IsDNS1123Subdomain(i.name); i.name != "" && len(msgs) > 0 { 261 return fmt.Errorf("release name %s is invalid: %s", i.name, strings.Join(msgs, ";")) 262 } 263 264 // Check chart requirements to make sure all dependencies are present in /charts 265 chartRequested, err := chartutil.Load(i.chartPath) 266 if err != nil { 267 return prettyError(err) 268 } 269 270 if chartRequested.Metadata.Deprecated { 271 fmt.Fprintln(os.Stderr, "WARNING: This chart is deprecated") 272 } 273 274 if req, err := chartutil.LoadRequirements(chartRequested); err == nil { 275 // If checkDependencies returns an error, we have unfulfilled dependencies. 276 // As of Helm 2.4.0, this is treated as a stopping condition: 277 // https://github.com/kubernetes/helm/issues/2209 278 if err := renderutil.CheckDependencies(chartRequested, req); err != nil { 279 if i.depUp { 280 man := &downloader.Manager{ 281 Out: i.out, 282 ChartPath: i.chartPath, 283 HelmHome: settings.Home, 284 Keyring: defaultKeyring(), 285 SkipUpdate: false, 286 Getters: getter.All(settings), 287 } 288 if err := man.Update(); err != nil { 289 return prettyError(err) 290 } 291 292 // Update all dependencies which are present in /charts. 293 chartRequested, err = chartutil.Load(i.chartPath) 294 if err != nil { 295 return prettyError(err) 296 } 297 } else { 298 return prettyError(err) 299 } 300 301 } 302 } else if err != chartutil.ErrRequirementsNotFound { 303 return fmt.Errorf("cannot load requirements: %v", err) 304 } 305 306 res, err := i.client.InstallReleaseFromChart( 307 chartRequested, 308 i.namespace, 309 helm.ValueOverrides(rawVals), 310 helm.ReleaseName(i.name), 311 helm.InstallDryRun(i.dryRun), 312 helm.InstallReuseName(i.replace), 313 helm.InstallDisableHooks(i.disableHooks), 314 helm.InstallDisableCRDHook(i.disableCRDHook), 315 helm.InstallSubNotes(i.subNotes), 316 helm.InstallTimeout(i.timeout), 317 helm.InstallWait(i.wait), 318 helm.InstallDescription(i.description)) 319 if err != nil { 320 if i.atomic { 321 fmt.Fprintf(os.Stdout, "INSTALL FAILED\nPURGING CHART\nError: %v\n", prettyError(err)) 322 deleteSideEffects := &deleteCmd{ 323 name: i.name, 324 disableHooks: i.disableHooks, 325 purge: true, 326 timeout: i.timeout, 327 description: "", 328 dryRun: i.dryRun, 329 out: i.out, 330 client: i.client, 331 } 332 if err := deleteSideEffects.run(); err != nil { 333 return err 334 } 335 fmt.Fprintf(os.Stdout, "Successfully purged a chart!\n") 336 } 337 return prettyError(err) 338 } 339 340 rel := res.GetRelease() 341 if rel == nil { 342 return nil 343 } 344 345 if outputFormat(i.output) == outputTable { 346 i.printRelease(rel) 347 } 348 349 // If this is a dry run, we can't display status. 350 if i.dryRun { 351 // This is special casing to avoid breaking backward compatibility: 352 if res.Release.Info.Description != "Dry run complete" { 353 fmt.Fprintf(os.Stdout, "WARNING: %s\n", res.Release.Info.Description) 354 } 355 return nil 356 } 357 358 // Print the status like status command does 359 status, err := i.client.ReleaseStatus(rel.Name) 360 if err != nil { 361 return prettyError(err) 362 } 363 364 return write(i.out, &statusWriter{status}, outputFormat(i.output)) 365 } 366 367 // Merges source and destination map, preferring values from the source map 368 func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { 369 for k, v := range src { 370 // If the key doesn't exist already, then just set the key to that value 371 if _, exists := dest[k]; !exists { 372 dest[k] = v 373 continue 374 } 375 nextMap, ok := v.(map[string]interface{}) 376 // If it isn't another map, overwrite the value 377 if !ok { 378 dest[k] = v 379 continue 380 } 381 // Edge case: If the key exists in the destination, but isn't a map 382 destMap, isMap := dest[k].(map[string]interface{}) 383 // If the source map has a map for this key, prefer it 384 if !isMap { 385 dest[k] = v 386 continue 387 } 388 // If we got to this point, it is a map in both, so merge them 389 dest[k] = mergeValues(destMap, nextMap) 390 } 391 return dest 392 } 393 394 // vals merges values from files specified via -f/--values and 395 // directly via --set or --set-string or --set-file, marshaling them to YAML 396 func vals(valueFiles valueFiles, values []string, stringValues []string, fileValues []string, CertFile, KeyFile, CAFile string) ([]byte, error) { 397 base := map[string]interface{}{} 398 399 // User specified a values files via -f/--values 400 for _, filePath := range valueFiles { 401 currentMap := map[string]interface{}{} 402 403 var bytes []byte 404 var err error 405 if strings.TrimSpace(filePath) == "-" { 406 bytes, err = ioutil.ReadAll(os.Stdin) 407 } else { 408 bytes, err = readFile(filePath, CertFile, KeyFile, CAFile) 409 } 410 411 if err != nil { 412 return []byte{}, err 413 } 414 415 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 416 return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) 417 } 418 // Merge with the previous map 419 base = mergeValues(base, currentMap) 420 } 421 422 // User specified a value via --set 423 for _, value := range values { 424 if err := strvals.ParseInto(value, base); err != nil { 425 return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) 426 } 427 } 428 429 // User specified a value via --set-string 430 for _, value := range stringValues { 431 if err := strvals.ParseIntoString(value, base); err != nil { 432 return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) 433 } 434 } 435 436 // User specified a value via --set-file 437 for _, value := range fileValues { 438 reader := func(rs []rune) (interface{}, error) { 439 bytes, err := readFile(string(rs), CertFile, KeyFile, CAFile) 440 return string(bytes), err 441 } 442 if err := strvals.ParseIntoFile(value, base, reader); err != nil { 443 return []byte{}, fmt.Errorf("failed parsing --set-file data: %s", err) 444 } 445 } 446 447 return yaml.Marshal(base) 448 } 449 450 // printRelease prints info about a release if the Debug is true. 451 func (i *installCmd) printRelease(rel *release.Release) { 452 if rel == nil { 453 return 454 } 455 // TODO: Switch to text/template like everything else. 456 fmt.Fprintf(i.out, "NAME: %s\n", rel.Name) 457 if settings.Debug { 458 printRelease(i.out, rel) 459 } 460 } 461 462 // locateChartPath looks for a chart directory in known places, and returns either the full path or an error. 463 // 464 // This does not ensure that the chart is well-formed; only that the requested filename exists. 465 // 466 // Order of resolution: 467 // - current working directory 468 // - if path is absolute or begins with '.', error out here 469 // - chart repos in $HELM_HOME 470 // - URL 471 // 472 // If 'verify' is true, this will attempt to also verify the chart. 473 func locateChartPath(repoURL, username, password, name, version string, verify bool, keyring, 474 certFile, keyFile, caFile string) (string, error) { 475 name = strings.TrimSpace(name) 476 version = strings.TrimSpace(version) 477 if fi, err := os.Stat(name); err == nil { 478 abs, err := filepath.Abs(name) 479 if err != nil { 480 return abs, err 481 } 482 if verify { 483 if fi.IsDir() { 484 return "", errors.New("cannot verify a directory") 485 } 486 if _, err := downloader.VerifyChart(abs, keyring); err != nil { 487 return "", err 488 } 489 } 490 return abs, nil 491 } 492 if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { 493 return name, fmt.Errorf("path %q not found", name) 494 } 495 496 crepo := filepath.Join(settings.Home.Repository(), name) 497 if _, err := os.Stat(crepo); err == nil { 498 return filepath.Abs(crepo) 499 } 500 501 dl := downloader.ChartDownloader{ 502 HelmHome: settings.Home, 503 Out: os.Stdout, 504 Keyring: keyring, 505 Getters: getter.All(settings), 506 Username: username, 507 Password: password, 508 } 509 if verify { 510 dl.Verify = downloader.VerifyAlways 511 } 512 if repoURL != "" { 513 chartURL, err := repo.FindChartInAuthRepoURL(repoURL, username, password, name, version, 514 certFile, keyFile, caFile, getter.All(settings)) 515 if err != nil { 516 return "", err 517 } 518 name = chartURL 519 } 520 521 if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { 522 os.MkdirAll(settings.Home.Archive(), 0744) 523 } 524 525 filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) 526 if err == nil { 527 lname, err := filepath.Abs(filename) 528 if err != nil { 529 return filename, err 530 } 531 debug("Fetched %s to %s\n", name, filename) 532 return lname, nil 533 } else if settings.Debug { 534 return filename, err 535 } 536 537 return filename, fmt.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) 538 } 539 540 func generateName(nameTemplate string) (string, error) { 541 t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) 542 if err != nil { 543 return "", err 544 } 545 var b bytes.Buffer 546 err = t.Execute(&b, nil) 547 if err != nil { 548 return "", err 549 } 550 return b.String(), nil 551 } 552 553 func defaultNamespace() string { 554 if ns, _, err := kube.GetConfig(settings.KubeContext, settings.KubeConfig).Namespace(); err == nil { 555 return ns 556 } 557 return "default" 558 } 559 560 //readFile load a file from the local directory or a remote file with a url. 561 func readFile(filePath, CertFile, KeyFile, CAFile string) ([]byte, error) { 562 u, _ := url.Parse(filePath) 563 p := getter.All(settings) 564 565 // FIXME: maybe someone handle other protocols like ftp. 566 getterConstructor, err := p.ByScheme(u.Scheme) 567 568 if err != nil { 569 return ioutil.ReadFile(filePath) 570 } 571 572 getter, err := getterConstructor(filePath, CertFile, KeyFile, CAFile) 573 if err != nil { 574 return []byte{}, err 575 } 576 data, err := getter.Get(filePath) 577 return data.Bytes(), err 578 }