github.com/latiif/helm@v2.15.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 req, err := chartutil.LoadRequirements(chartRequested); err == nil { 271 // If checkDependencies returns an error, we have unfulfilled dependencies. 272 // As of Helm 2.4.0, this is treated as a stopping condition: 273 // https://github.com/kubernetes/helm/issues/2209 274 if err := renderutil.CheckDependencies(chartRequested, req); err != nil { 275 if i.depUp { 276 man := &downloader.Manager{ 277 Out: i.out, 278 ChartPath: i.chartPath, 279 HelmHome: settings.Home, 280 Keyring: defaultKeyring(), 281 SkipUpdate: false, 282 Getters: getter.All(settings), 283 } 284 if err := man.Update(); err != nil { 285 return prettyError(err) 286 } 287 288 // Update all dependencies which are present in /charts. 289 chartRequested, err = chartutil.Load(i.chartPath) 290 if err != nil { 291 return prettyError(err) 292 } 293 } else { 294 return prettyError(err) 295 } 296 297 } 298 } else if err != chartutil.ErrRequirementsNotFound { 299 return fmt.Errorf("cannot load requirements: %v", err) 300 } 301 302 res, err := i.client.InstallReleaseFromChart( 303 chartRequested, 304 i.namespace, 305 helm.ValueOverrides(rawVals), 306 helm.ReleaseName(i.name), 307 helm.InstallDryRun(i.dryRun), 308 helm.InstallReuseName(i.replace), 309 helm.InstallDisableHooks(i.disableHooks), 310 helm.InstallDisableCRDHook(i.disableCRDHook), 311 helm.InstallSubNotes(i.subNotes), 312 helm.InstallTimeout(i.timeout), 313 helm.InstallWait(i.wait), 314 helm.InstallDescription(i.description)) 315 if err != nil { 316 if i.atomic { 317 fmt.Fprintf(os.Stdout, "INSTALL FAILED\nPURGING CHART\nError: %v\n", prettyError(err)) 318 deleteSideEffects := &deleteCmd{ 319 name: i.name, 320 disableHooks: i.disableHooks, 321 purge: true, 322 timeout: i.timeout, 323 description: "", 324 dryRun: i.dryRun, 325 out: i.out, 326 client: i.client, 327 } 328 if err := deleteSideEffects.run(); err != nil { 329 return err 330 } 331 fmt.Fprintf(os.Stdout, "Successfully purged a chart!\n") 332 } 333 return prettyError(err) 334 } 335 336 rel := res.GetRelease() 337 if rel == nil { 338 return nil 339 } 340 341 if outputFormat(i.output) == outputTable { 342 i.printRelease(rel) 343 } 344 345 // If this is a dry run, we can't display status. 346 if i.dryRun { 347 // This is special casing to avoid breaking backward compatibility: 348 if res.Release.Info.Description != "Dry run complete" { 349 fmt.Fprintf(os.Stdout, "WARNING: %s\n", res.Release.Info.Description) 350 } 351 return nil 352 } 353 354 // Print the status like status command does 355 status, err := i.client.ReleaseStatus(rel.Name) 356 if err != nil { 357 return prettyError(err) 358 } 359 360 return write(i.out, &statusWriter{status}, outputFormat(i.output)) 361 } 362 363 // Merges source and destination map, preferring values from the source map 364 func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { 365 for k, v := range src { 366 // If the key doesn't exist already, then just set the key to that value 367 if _, exists := dest[k]; !exists { 368 dest[k] = v 369 continue 370 } 371 nextMap, ok := v.(map[string]interface{}) 372 // If it isn't another map, overwrite the value 373 if !ok { 374 dest[k] = v 375 continue 376 } 377 // Edge case: If the key exists in the destination, but isn't a map 378 destMap, isMap := dest[k].(map[string]interface{}) 379 // If the source map has a map for this key, prefer it 380 if !isMap { 381 dest[k] = v 382 continue 383 } 384 // If we got to this point, it is a map in both, so merge them 385 dest[k] = mergeValues(destMap, nextMap) 386 } 387 return dest 388 } 389 390 // vals merges values from files specified via -f/--values and 391 // directly via --set or --set-string or --set-file, marshaling them to YAML 392 func vals(valueFiles valueFiles, values []string, stringValues []string, fileValues []string, CertFile, KeyFile, CAFile string) ([]byte, error) { 393 base := map[string]interface{}{} 394 395 // User specified a values files via -f/--values 396 for _, filePath := range valueFiles { 397 currentMap := map[string]interface{}{} 398 399 var bytes []byte 400 var err error 401 if strings.TrimSpace(filePath) == "-" { 402 bytes, err = ioutil.ReadAll(os.Stdin) 403 } else { 404 bytes, err = readFile(filePath, CertFile, KeyFile, CAFile) 405 } 406 407 if err != nil { 408 return []byte{}, err 409 } 410 411 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 412 return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) 413 } 414 // Merge with the previous map 415 base = mergeValues(base, currentMap) 416 } 417 418 // User specified a value via --set 419 for _, value := range values { 420 if err := strvals.ParseInto(value, base); err != nil { 421 return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) 422 } 423 } 424 425 // User specified a value via --set-string 426 for _, value := range stringValues { 427 if err := strvals.ParseIntoString(value, base); err != nil { 428 return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) 429 } 430 } 431 432 // User specified a value via --set-file 433 for _, value := range fileValues { 434 reader := func(rs []rune) (interface{}, error) { 435 bytes, err := readFile(string(rs), CertFile, KeyFile, CAFile) 436 return string(bytes), err 437 } 438 if err := strvals.ParseIntoFile(value, base, reader); err != nil { 439 return []byte{}, fmt.Errorf("failed parsing --set-file data: %s", err) 440 } 441 } 442 443 return yaml.Marshal(base) 444 } 445 446 // printRelease prints info about a release if the Debug is true. 447 func (i *installCmd) printRelease(rel *release.Release) { 448 if rel == nil { 449 return 450 } 451 // TODO: Switch to text/template like everything else. 452 fmt.Fprintf(i.out, "NAME: %s\n", rel.Name) 453 if settings.Debug { 454 printRelease(i.out, rel) 455 } 456 } 457 458 // locateChartPath looks for a chart directory in known places, and returns either the full path or an error. 459 // 460 // This does not ensure that the chart is well-formed; only that the requested filename exists. 461 // 462 // Order of resolution: 463 // - current working directory 464 // - if path is absolute or begins with '.', error out here 465 // - chart repos in $HELM_HOME 466 // - URL 467 // 468 // If 'verify' is true, this will attempt to also verify the chart. 469 func locateChartPath(repoURL, username, password, name, version string, verify bool, keyring, 470 certFile, keyFile, caFile string) (string, error) { 471 name = strings.TrimSpace(name) 472 version = strings.TrimSpace(version) 473 if fi, err := os.Stat(name); err == nil { 474 abs, err := filepath.Abs(name) 475 if err != nil { 476 return abs, err 477 } 478 if verify { 479 if fi.IsDir() { 480 return "", errors.New("cannot verify a directory") 481 } 482 if _, err := downloader.VerifyChart(abs, keyring); err != nil { 483 return "", err 484 } 485 } 486 return abs, nil 487 } 488 if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { 489 return name, fmt.Errorf("path %q not found", name) 490 } 491 492 crepo := filepath.Join(settings.Home.Repository(), name) 493 if _, err := os.Stat(crepo); err == nil { 494 return filepath.Abs(crepo) 495 } 496 497 dl := downloader.ChartDownloader{ 498 HelmHome: settings.Home, 499 Out: os.Stdout, 500 Keyring: keyring, 501 Getters: getter.All(settings), 502 Username: username, 503 Password: password, 504 } 505 if verify { 506 dl.Verify = downloader.VerifyAlways 507 } 508 if repoURL != "" { 509 chartURL, err := repo.FindChartInAuthRepoURL(repoURL, username, password, name, version, 510 certFile, keyFile, caFile, getter.All(settings)) 511 if err != nil { 512 return "", err 513 } 514 name = chartURL 515 } 516 517 if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { 518 os.MkdirAll(settings.Home.Archive(), 0744) 519 } 520 521 filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) 522 if err == nil { 523 lname, err := filepath.Abs(filename) 524 if err != nil { 525 return filename, err 526 } 527 debug("Fetched %s to %s\n", name, filename) 528 return lname, nil 529 } else if settings.Debug { 530 return filename, err 531 } 532 533 return filename, fmt.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) 534 } 535 536 func generateName(nameTemplate string) (string, error) { 537 t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) 538 if err != nil { 539 return "", err 540 } 541 var b bytes.Buffer 542 err = t.Execute(&b, nil) 543 if err != nil { 544 return "", err 545 } 546 return b.String(), nil 547 } 548 549 func defaultNamespace() string { 550 if ns, _, err := kube.GetConfig(settings.KubeContext, settings.KubeConfig).Namespace(); err == nil { 551 return ns 552 } 553 return "default" 554 } 555 556 //readFile load a file from the local directory or a remote file with a url. 557 func readFile(filePath, CertFile, KeyFile, CAFile string) ([]byte, error) { 558 u, _ := url.Parse(filePath) 559 p := getter.All(settings) 560 561 // FIXME: maybe someone handle other protocols like ftp. 562 getterConstructor, err := p.ByScheme(u.Scheme) 563 564 if err != nil { 565 return ioutil.ReadFile(filePath) 566 } 567 568 getter, err := getterConstructor(filePath, CertFile, KeyFile, CAFile) 569 if err != nil { 570 return []byte{}, err 571 } 572 data, err := getter.Get(filePath) 573 return data.Bytes(), err 574 }