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