github.com/amy/helm@v2.7.2+incompatible/cmd/helm/install.go (about) 1 /* 2 Copyright 2016 The Kubernetes Authors All rights reserved. 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 "os" 26 "path/filepath" 27 "strings" 28 29 "text/template" 30 31 "github.com/Masterminds/sprig" 32 "github.com/ghodss/yaml" 33 "github.com/spf13/cobra" 34 35 "k8s.io/helm/pkg/chartutil" 36 "k8s.io/helm/pkg/downloader" 37 "k8s.io/helm/pkg/getter" 38 "k8s.io/helm/pkg/helm" 39 "k8s.io/helm/pkg/kube" 40 "k8s.io/helm/pkg/proto/hapi/chart" 41 "k8s.io/helm/pkg/proto/hapi/release" 42 "k8s.io/helm/pkg/repo" 43 "k8s.io/helm/pkg/strvals" 44 "net/url" 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. 55 56 $ helm install -f myvalues.yaml ./redis 57 58 or 59 60 $ helm install --set name=prod ./redis 61 62 You can specify the '--values'/'-f' flag multiple times. The priority will be given to the 63 last (right-most) file specified. For example, if both myvalues.yaml and override.yaml 64 contained a key called 'Test', the value set in override.yaml would take precedence: 65 66 $ helm install -f myvalues.yaml -f override.yaml ./redis 67 68 You can specify the '--set' flag multiple times. The priority will be given to the 69 last (right-most) set specified. For example, if both 'bar' and 'newbar' values are 70 set for a key called 'foo', the 'newbar' value would take precedence: 71 72 $ helm install --set foo=bar --set foo=newbar ./redis 73 74 75 To check the generated manifests of a release without installing the chart, 76 the '--debug' and '--dry-run' flags can be combined. This will still require a 77 round-trip to the Tiller server. 78 79 If --verify is set, the chart MUST have a provenance file, and the provenenace 80 fall MUST pass all verification steps. 81 82 There are four different ways you can express the chart you want to install: 83 84 1. By chart reference: helm install stable/mariadb 85 2. By path to a packaged chart: helm install ./nginx-1.2.3.tgz 86 3. By path to an unpacked chart directory: helm install ./nginx 87 4. By absolute URL: helm install https://example.com/charts/nginx-1.2.3.tgz 88 89 CHART REFERENCES 90 91 A chart reference is a convenient way of reference a chart in a chart repository. 92 93 When you use a chart reference ('stable/mariadb'), Helm will look in the local 94 configuration for a chart repository named 'stable', and will then look for a 95 chart in that repository whose name is 'mariadb'. It will install the latest 96 version of that chart unless you also supply a version number with the 97 '--version' flag. 98 99 To see the list of chart repositories, use 'helm repo list'. To search for 100 charts in a repository, use 'helm search'. 101 ` 102 103 type installCmd struct { 104 name string 105 namespace string 106 valueFiles valueFiles 107 chartPath string 108 dryRun bool 109 disableHooks bool 110 replace bool 111 verify bool 112 keyring string 113 out io.Writer 114 client helm.Interface 115 values []string 116 nameTemplate string 117 version string 118 timeout int64 119 wait bool 120 repoURL string 121 devel bool 122 123 certFile string 124 keyFile string 125 caFile string 126 } 127 128 type valueFiles []string 129 130 func (v *valueFiles) String() string { 131 return fmt.Sprint(*v) 132 } 133 134 func (v *valueFiles) Type() string { 135 return "valueFiles" 136 } 137 138 func (v *valueFiles) Set(value string) error { 139 for _, filePath := range strings.Split(value, ",") { 140 *v = append(*v, filePath) 141 } 142 return nil 143 } 144 145 func newInstallCmd(c helm.Interface, out io.Writer) *cobra.Command { 146 inst := &installCmd{ 147 out: out, 148 client: c, 149 } 150 151 cmd := &cobra.Command{ 152 Use: "install [CHART]", 153 Short: "install a chart archive", 154 Long: installDesc, 155 PreRunE: setupConnection, 156 RunE: func(cmd *cobra.Command, args []string) error { 157 if err := checkArgsLength(len(args), "chart name"); err != nil { 158 return err 159 } 160 161 debug("Original chart version: %q", inst.version) 162 if inst.version == "" && inst.devel { 163 debug("setting version to >0.0.0-0") 164 inst.version = ">0.0.0-0" 165 } 166 167 cp, err := locateChartPath(inst.repoURL, args[0], inst.version, inst.verify, inst.keyring, 168 inst.certFile, inst.keyFile, inst.caFile) 169 if err != nil { 170 return err 171 } 172 inst.chartPath = cp 173 inst.client = ensureHelmClient(inst.client) 174 return inst.run() 175 }, 176 } 177 178 f := cmd.Flags() 179 f.VarP(&inst.valueFiles, "values", "f", "specify values in a YAML file or a URL(can specify multiple)") 180 f.StringVarP(&inst.name, "name", "n", "", "release name. If unspecified, it will autogenerate one for you") 181 f.StringVar(&inst.namespace, "namespace", "", "namespace to install the release into. Defaults to the current kube config namespace.") 182 f.BoolVar(&inst.dryRun, "dry-run", false, "simulate an install") 183 f.BoolVar(&inst.disableHooks, "no-hooks", false, "prevent hooks from running during install") 184 f.BoolVar(&inst.replace, "replace", false, "re-use the given name, even if that name is already used. This is unsafe in production") 185 f.StringArrayVar(&inst.values, "set", []string{}, "set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 186 f.StringVar(&inst.nameTemplate, "name-template", "", "specify template used to name the release") 187 f.BoolVar(&inst.verify, "verify", false, "verify the package before installing it") 188 f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "location of public keys used for verification") 189 f.StringVar(&inst.version, "version", "", "specify the exact chart version to install. If this is not specified, the latest version is installed") 190 f.Int64Var(&inst.timeout, "timeout", 300, "time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") 191 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") 192 f.StringVar(&inst.repoURL, "repo", "", "chart repository url where to locate the requested chart") 193 f.StringVar(&inst.certFile, "cert-file", "", "identify HTTPS client using this SSL certificate file") 194 f.StringVar(&inst.keyFile, "key-file", "", "identify HTTPS client using this SSL key file") 195 f.StringVar(&inst.caFile, "ca-file", "", "verify certificates of HTTPS-enabled servers using this CA bundle") 196 f.BoolVar(&inst.devel, "devel", false, "use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") 197 198 return cmd 199 } 200 201 func (i *installCmd) run() error { 202 debug("CHART PATH: %s\n", i.chartPath) 203 204 if i.namespace == "" { 205 i.namespace = defaultNamespace() 206 } 207 208 rawVals, err := vals(i.valueFiles, i.values) 209 if err != nil { 210 return err 211 } 212 213 // If template is specified, try to run the template. 214 if i.nameTemplate != "" { 215 i.name, err = generateName(i.nameTemplate) 216 if err != nil { 217 return err 218 } 219 // Print the final name so the user knows what the final name of the release is. 220 fmt.Printf("FINAL NAME: %s\n", i.name) 221 } 222 223 // Check chart requirements to make sure all dependencies are present in /charts 224 chartRequested, err := chartutil.Load(i.chartPath) 225 if err != nil { 226 return prettyError(err) 227 } 228 229 if req, err := chartutil.LoadRequirements(chartRequested); err == nil { 230 // If checkDependencies returns an error, we have unfullfilled dependencies. 231 // As of Helm 2.4.0, this is treated as a stopping condition: 232 // https://github.com/kubernetes/helm/issues/2209 233 if err := checkDependencies(chartRequested, req); err != nil { 234 return prettyError(err) 235 } 236 } else if err != chartutil.ErrRequirementsNotFound { 237 return fmt.Errorf("cannot load requirements: %v", err) 238 } 239 240 res, err := i.client.InstallReleaseFromChart( 241 chartRequested, 242 i.namespace, 243 helm.ValueOverrides(rawVals), 244 helm.ReleaseName(i.name), 245 helm.InstallDryRun(i.dryRun), 246 helm.InstallReuseName(i.replace), 247 helm.InstallDisableHooks(i.disableHooks), 248 helm.InstallTimeout(i.timeout), 249 helm.InstallWait(i.wait)) 250 if err != nil { 251 return prettyError(err) 252 } 253 254 rel := res.GetRelease() 255 if rel == nil { 256 return nil 257 } 258 i.printRelease(rel) 259 260 // If this is a dry run, we can't display status. 261 if i.dryRun { 262 return nil 263 } 264 265 // Print the status like status command does 266 status, err := i.client.ReleaseStatus(rel.Name) 267 if err != nil { 268 return prettyError(err) 269 } 270 PrintStatus(i.out, status) 271 return nil 272 } 273 274 // Merges source and destination map, preferring values from the source map 275 func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { 276 for k, v := range src { 277 // If the key doesn't exist already, then just set the key to that value 278 if _, exists := dest[k]; !exists { 279 dest[k] = v 280 continue 281 } 282 nextMap, ok := v.(map[string]interface{}) 283 // If it isn't another map, overwrite the value 284 if !ok { 285 dest[k] = v 286 continue 287 } 288 // If the key doesn't exist already, then just set the key to that value 289 if _, exists := dest[k]; !exists { 290 dest[k] = nextMap 291 continue 292 } 293 // Edge case: If the key exists in the destination, but isn't a map 294 destMap, isMap := dest[k].(map[string]interface{}) 295 // If the source map has a map for this key, prefer it 296 if !isMap { 297 dest[k] = v 298 continue 299 } 300 // If we got to this point, it is a map in both, so merge them 301 dest[k] = mergeValues(destMap, nextMap) 302 } 303 return dest 304 } 305 306 // vals merges values from files specified via -f/--values and 307 // directly via --set, marshaling them to YAML 308 func vals(valueFiles valueFiles, values []string) ([]byte, error) { 309 base := map[string]interface{}{} 310 311 // User specified a values files via -f/--values 312 for _, filePath := range valueFiles { 313 currentMap := map[string]interface{}{} 314 315 var bytes []byte 316 var err error 317 if strings.TrimSpace(filePath) == "-" { 318 bytes, err = ioutil.ReadAll(os.Stdin) 319 } else { 320 bytes, err = readFile(filePath) 321 } 322 323 if err != nil { 324 return []byte{}, err 325 } 326 327 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 328 return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) 329 } 330 // Merge with the previous map 331 base = mergeValues(base, currentMap) 332 } 333 334 // User specified a value via --set 335 for _, value := range values { 336 if err := strvals.ParseInto(value, base); err != nil { 337 return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) 338 } 339 } 340 341 return yaml.Marshal(base) 342 } 343 344 // printRelease prints info about a release if the Debug is true. 345 func (i *installCmd) printRelease(rel *release.Release) { 346 if rel == nil { 347 return 348 } 349 // TODO: Switch to text/template like everything else. 350 fmt.Fprintf(i.out, "NAME: %s\n", rel.Name) 351 if settings.Debug { 352 printRelease(i.out, rel) 353 } 354 } 355 356 // locateChartPath looks for a chart directory in known places, and returns either the full path or an error. 357 // 358 // This does not ensure that the chart is well-formed; only that the requested filename exists. 359 // 360 // Order of resolution: 361 // - current working directory 362 // - if path is absolute or begins with '.', error out here 363 // - chart repos in $HELM_HOME 364 // - URL 365 // 366 // If 'verify' is true, this will attempt to also verify the chart. 367 func locateChartPath(repoURL, name, version string, verify bool, keyring, 368 certFile, keyFile, caFile string) (string, error) { 369 name = strings.TrimSpace(name) 370 version = strings.TrimSpace(version) 371 if fi, err := os.Stat(name); err == nil { 372 abs, err := filepath.Abs(name) 373 if err != nil { 374 return abs, err 375 } 376 if verify { 377 if fi.IsDir() { 378 return "", errors.New("cannot verify a directory") 379 } 380 if _, err := downloader.VerifyChart(abs, keyring); err != nil { 381 return "", err 382 } 383 } 384 return abs, nil 385 } 386 if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { 387 return name, fmt.Errorf("path %q not found", name) 388 } 389 390 crepo := filepath.Join(settings.Home.Repository(), name) 391 if _, err := os.Stat(crepo); err == nil { 392 return filepath.Abs(crepo) 393 } 394 395 dl := downloader.ChartDownloader{ 396 HelmHome: settings.Home, 397 Out: os.Stdout, 398 Keyring: keyring, 399 Getters: getter.All(settings), 400 } 401 if verify { 402 dl.Verify = downloader.VerifyAlways 403 } 404 if repoURL != "" { 405 chartURL, err := repo.FindChartInRepoURL(repoURL, name, version, 406 certFile, keyFile, caFile, getter.All(settings)) 407 if err != nil { 408 return "", err 409 } 410 name = chartURL 411 } 412 413 if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { 414 os.MkdirAll(settings.Home.Archive(), 0744) 415 } 416 417 filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) 418 if err == nil { 419 lname, err := filepath.Abs(filename) 420 if err != nil { 421 return filename, err 422 } 423 debug("Fetched %s to %s\n", name, filename) 424 return lname, nil 425 } else if settings.Debug { 426 return filename, err 427 } 428 429 return filename, fmt.Errorf("file %q not found", name) 430 } 431 432 func generateName(nameTemplate string) (string, error) { 433 t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) 434 if err != nil { 435 return "", err 436 } 437 var b bytes.Buffer 438 err = t.Execute(&b, nil) 439 if err != nil { 440 return "", err 441 } 442 return b.String(), nil 443 } 444 445 func defaultNamespace() string { 446 if ns, _, err := kube.GetConfig(settings.KubeContext).Namespace(); err == nil { 447 return ns 448 } 449 return "default" 450 } 451 452 func checkDependencies(ch *chart.Chart, reqs *chartutil.Requirements) error { 453 missing := []string{} 454 455 deps := ch.GetDependencies() 456 for _, r := range reqs.Dependencies { 457 found := false 458 for _, d := range deps { 459 if d.Metadata.Name == r.Name { 460 found = true 461 break 462 } 463 } 464 if !found { 465 missing = append(missing, r.Name) 466 } 467 } 468 469 if len(missing) > 0 { 470 return fmt.Errorf("found in requirements.yaml, but missing in charts/ directory: %s", strings.Join(missing, ", ")) 471 } 472 return nil 473 } 474 475 //readFile load a file from the local directory or a remote file with a url. 476 func readFile(filePath string) ([]byte, error) { 477 u, _ := url.Parse(filePath) 478 p := getter.All(settings) 479 480 // FIXME: maybe someone handle other protocols like ftp. 481 getterConstructor, err := p.ByScheme(u.Scheme) 482 483 if err != nil { 484 return ioutil.ReadFile(filePath) 485 } else { 486 getter, err := getterConstructor(filePath, "", "", "") 487 if err != nil { 488 return []byte{}, err 489 } 490 data, err := getter.Get(filePath) 491 return data.Bytes(), err 492 } 493 }