github.com/wangchanggan/helm@v0.0.0-20211020154240-11b1b7d5406d/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 // 在install命令行时,Helm 还执行了一个操作,使用kubectl port-forward临时在本地宿主机打通一个与Tiller Pod沟通的通道 177 PreRunE: func(_ *cobra.Command, _ []string) error { return setupConnection() }, 178 RunE: func(cmd *cobra.Command, args []string) error { 179 // 检查传递参数的有效性,以及是否漏传参数。 180 if err := checkArgsLength(len(args), "chart name"); err != nil { 181 return err 182 } 183 184 debug("Original chart version: %q", inst.version) 185 if inst.version == "" && inst.devel { 186 debug("setting version to >0.0.0-0") 187 inst.version = ">0.0.0-0" 188 } 189 190 // 寻找Chart位置,如果是本地目录,则返回并寻找完全路径;如果URL,则下载到指定路径后返回该路径名称。 191 cp, err := locateChartPath(inst.repoURL, inst.username, inst.password, args[0], inst.version, inst.verify, inst.keyring, 192 inst.certFile, inst.keyFile, inst.caFile) 193 if err != nil { 194 return err 195 } 196 inst.chartPath = cp 197 // 初始化Helm Client,用来与 Tiller 通信。 198 inst.client = ensureHelmClient(inst.client) 199 inst.wait = inst.wait || inst.atomic 200 // 真正的业务逻辑,分别检查Chart依赖等信息,然后给Tiller发送解压后的模板信息。 201 return inst.run() 202 }, 203 } 204 205 f := cmd.Flags() 206 settings.AddFlagsTLS(f) 207 f.VarP(&inst.valueFiles, "values", "f", "Specify values in a YAML file or a URL(can specify multiple)") 208 f.StringVarP(&inst.name, "name", "n", "", "The release name. If unspecified, it will autogenerate one for you") 209 f.StringVar(&inst.namespace, "namespace", "", "Namespace to install the release into. Defaults to the current kube config namespace.") 210 f.BoolVar(&inst.dryRun, "dry-run", false, "Simulate an install") 211 f.BoolVar(&inst.disableHooks, "no-hooks", false, "Prevent hooks from running during install") 212 f.BoolVar(&inst.disableCRDHook, "no-crd-hook", false, "Prevent CRD hooks from running, but run other hooks") 213 f.BoolVar(&inst.replace, "replace", false, "Re-use the given name, even if that name is already used. This is unsafe in production") 214 f.StringArrayVar(&inst.values, "set", []string{}, "Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)") 215 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)") 216 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)") 217 f.StringVar(&inst.nameTemplate, "name-template", "", "Specify template used to name the release") 218 f.BoolVar(&inst.verify, "verify", false, "Verify the package before installing it") 219 f.StringVar(&inst.keyring, "keyring", defaultKeyring(), "Location of public keys used for verification") 220 f.StringVar(&inst.version, "version", "", "Specify the exact chart version to install. If this is not specified, the latest version is installed") 221 f.Int64Var(&inst.timeout, "timeout", 300, "Time in seconds to wait for any individual Kubernetes operation (like Jobs for hooks)") 222 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") 223 f.BoolVar(&inst.atomic, "atomic", false, "If set, installation process purges chart on fail, also sets --wait flag") 224 f.StringVar(&inst.repoURL, "repo", "", "Chart repository url where to locate the requested chart") 225 f.StringVar(&inst.username, "username", "", "Chart repository username where to locate the requested chart") 226 f.StringVar(&inst.password, "password", "", "Chart repository password where to locate the requested chart") 227 f.StringVar(&inst.certFile, "cert-file", "", "Identify HTTPS client using this SSL certificate file") 228 f.StringVar(&inst.keyFile, "key-file", "", "Identify HTTPS client using this SSL key file") 229 f.StringVar(&inst.caFile, "ca-file", "", "Verify certificates of HTTPS-enabled servers using this CA bundle") 230 f.BoolVar(&inst.devel, "devel", false, "Use development versions, too. Equivalent to version '>0.0.0-0'. If --version is set, this is ignored.") 231 f.BoolVar(&inst.depUp, "dep-up", false, "Run helm dependency update before installing the chart") 232 f.BoolVar(&inst.subNotes, "render-subchart-notes", false, "Render subchart notes along with the parent") 233 f.StringVar(&inst.description, "description", "", "Specify a description for the release") 234 bindOutputFlag(cmd, &inst.output) 235 236 // set defaults from environment 237 settings.InitTLS(f) 238 239 return cmd 240 } 241 242 func (i *installCmd) run() error { 243 debug("CHART PATH: %s\n", i.chartPath) 244 245 // 查看是否填写了命名容间名称,如果没有填写则默认是default命名空间。 246 if i.namespace == "" { 247 i.namespace = defaultNamespace() 248 } 249 250 // 使用vals函数合并命令行"helm install -f myvalues.yam1"覆盖的参数,这里使用的"-f override.yaml"这种命令行传参方式就是通过vals函数实现的。 251 // 此函数将valueFiles,values等信息读取出来后,和已经加裁到内存的valuesMap做一个对比,用外部传入的参数覆盖当前内存中的参数 252 // 这样在继续进行后面的动作时,都是使用的外部命令行的最新参数列表。 253 rawVals, err := vals(i.valueFiles, i.values, i.stringValues, i.fileValues, i.certFile, i.keyFile, i.caFile) 254 if err != nil { 255 return err 256 } 257 258 // If template is specified, try to run the template. 259 // 如果在执行install命令时指定了template,这里就会根据template的名称使用go template模板库进行读取 260 // 同时也会自动渲染该模板,最终返回一个被渲染过的template对象。 261 if i.nameTemplate != "" { 262 i.name, err = generateName(i.nameTemplate) 263 if err != nil { 264 return err 265 } 266 // Print the final name so the user knows what the final name of the release is. 267 fmt.Printf("FINAL NAME: %s\n", i.name) 268 } 269 270 // 这检查指定的名称是否符合DNS命名规范,这个规范适用于Kubernetes各个资源的命名,算是Kubernetes各个资源部署的统一标准。 271 if msgs := validation.IsDNS1123Subdomain(i.name); i.name != "" && len(msgs) > 0 { 272 return fmt.Errorf("release name %s is invalid: %s", i.name, strings.Join(msgs, ";")) 273 } 274 275 // Check chart requirements to make sure all dependencies are present in /charts 276 // 根据前面返回的Chart本地存储路径加载对应的Chart文件。这里的Chart文件一般都是一个文件夹,里面含有values.yaml、Chart等各种文件和文件夹 277 // 该函数读取这些文件的内容后,将其初始化为一个对应的Chart,这样既能校验Chart内容的正确性也方便后面继续调用。 278 // 如果设置了.helmignore 文件,那么这个函数也会略过这些文件,不会将其序列化到Chart对象中。 279 chartRequested, err := chartutil.Load(i.chartPath) 280 if err != nil { 281 return prettyError(err) 282 } 283 284 if chartRequested.Metadata.Deprecated { 285 fmt.Fprintln(os.Stderr, "WARNING: This chart is deprecated") 286 } 287 288 // 检查是否有requirements.yaml 文件,并且将声明的文件内容使用上面介绍的Chart重新下载和读取渲染函数来重新运行一遍。 289 // 全部下载完毕后,还会再使用Load函数加载内容加载。到此,内存中的Chart结构体已经包含所有需要的文本信息。 290 if req, err := chartutil.LoadRequirements(chartRequested); err == nil { 291 // If checkDependencies returns an error, we have unfulfilled dependencies. 292 // As of Helm 2.4.0, this is treated as a stopping condition: 293 // https://github.com/kubernetes/helm/issues/2209 294 if err := renderutil.CheckDependencies(chartRequested, req); err != nil { 295 if i.depUp { 296 man := &downloader.Manager{ 297 Out: i.out, 298 ChartPath: i.chartPath, 299 HelmHome: settings.Home, 300 Keyring: defaultKeyring(), 301 SkipUpdate: false, 302 Getters: getter.All(settings), 303 } 304 if err := man.Update(); err != nil { 305 return prettyError(err) 306 } 307 308 // Update all dependencies which are present in /charts. 309 chartRequested, err = chartutil.Load(i.chartPath) 310 if err != nil { 311 return prettyError(err) 312 } 313 } else { 314 return prettyError(err) 315 } 316 317 } 318 } else if err != chartutil.ErrRequirementsNotFound { 319 return fmt.Errorf("cannot load requirements: %v", err) 320 } 321 322 res, err := i.client.InstallReleaseFromChart( 323 chartRequested, 324 i.namespace, 325 helm.ValueOverrides(rawVals), 326 helm.ReleaseName(i.name), 327 helm.InstallDryRun(i.dryRun), 328 helm.InstallReuseName(i.replace), 329 helm.InstallDisableHooks(i.disableHooks), 330 helm.InstallDisableCRDHook(i.disableCRDHook), 331 helm.InstallSubNotes(i.subNotes), 332 helm.InstallTimeout(i.timeout), 333 helm.InstallWait(i.wait), 334 helm.InstallDescription(i.description)) 335 if err != nil { 336 if i.atomic { 337 fmt.Fprintf(os.Stdout, "INSTALL FAILED\nPURGING CHART\nError: %v\n", prettyError(err)) 338 deleteSideEffects := &deleteCmd{ 339 name: i.name, 340 disableHooks: i.disableHooks, 341 purge: true, 342 timeout: i.timeout, 343 description: "", 344 dryRun: i.dryRun, 345 out: i.out, 346 client: i.client, 347 } 348 if err := deleteSideEffects.run(); err != nil { 349 return err 350 } 351 fmt.Fprintf(os.Stdout, "Successfully purged a chart!\n") 352 } 353 return prettyError(err) 354 } 355 356 // 首先获取Release信息,在上步发送install请求后,Tiller的response信息内就已经含有了这些信息。 357 rel := res.GetRelease() 358 if rel == nil { 359 return nil 360 } 361 362 if outputFormat(i.output) == outputTable { 363 i.printRelease(rel) 364 } 365 366 // If this is a dry run, we can't display status. 367 if i.dryRun { 368 // This is special casing to avoid breaking backward compatibility: 369 if res.Release.Info.Description != "Dry run complete" { 370 fmt.Fprintf(os.Stdout, "WARNING: %s\n", res.Release.Info.Description) 371 } 372 return nil 373 } 374 375 // Print the status like status command does 376 // 和install一样, 也是向Tiller发送请求,这个API的URL /hapi.services.tiller.ReleaseService/GetReleaseStatus 377 status, err := i.client.ReleaseStatus(rel.Name) 378 if err != nil { 379 return prettyError(err) 380 } 381 382 return write(i.out, &statusWriter{status}, outputFormat(i.output)) 383 } 384 385 // Merges source and destination map, preferring values from the source map 386 func mergeValues(dest map[string]interface{}, src map[string]interface{}) map[string]interface{} { 387 for k, v := range src { 388 // If the key doesn't exist already, then just set the key to that value 389 if _, exists := dest[k]; !exists { 390 dest[k] = v 391 continue 392 } 393 nextMap, ok := v.(map[string]interface{}) 394 // If it isn't another map, overwrite the value 395 if !ok { 396 dest[k] = v 397 continue 398 } 399 // Edge case: If the key exists in the destination, but isn't a map 400 destMap, isMap := dest[k].(map[string]interface{}) 401 // If the source map has a map for this key, prefer it 402 if !isMap { 403 dest[k] = v 404 continue 405 } 406 // If we got to this point, it is a map in both, so merge them 407 dest[k] = mergeValues(destMap, nextMap) 408 } 409 return dest 410 } 411 412 // vals merges values from files specified via -f/--values and 413 // directly via --set or --set-string or --set-file, marshaling them to YAML 414 func vals(valueFiles valueFiles, values []string, stringValues []string, fileValues []string, CertFile, KeyFile, CAFile string) ([]byte, error) { 415 base := map[string]interface{}{} 416 417 // User specified a values files via -f/--values 418 for _, filePath := range valueFiles { 419 currentMap := map[string]interface{}{} 420 421 var bytes []byte 422 var err error 423 if strings.TrimSpace(filePath) == "-" { 424 bytes, err = ioutil.ReadAll(os.Stdin) 425 } else { 426 bytes, err = readFile(filePath, CertFile, KeyFile, CAFile) 427 } 428 429 if err != nil { 430 return []byte{}, err 431 } 432 433 if err := yaml.Unmarshal(bytes, ¤tMap); err != nil { 434 return []byte{}, fmt.Errorf("failed to parse %s: %s", filePath, err) 435 } 436 // Merge with the previous map 437 base = mergeValues(base, currentMap) 438 } 439 440 // User specified a value via --set 441 for _, value := range values { 442 if err := strvals.ParseInto(value, base); err != nil { 443 return []byte{}, fmt.Errorf("failed parsing --set data: %s", err) 444 } 445 } 446 447 // User specified a value via --set-string 448 for _, value := range stringValues { 449 if err := strvals.ParseIntoString(value, base); err != nil { 450 return []byte{}, fmt.Errorf("failed parsing --set-string data: %s", err) 451 } 452 } 453 454 // User specified a value via --set-file 455 for _, value := range fileValues { 456 reader := func(rs []rune) (interface{}, error) { 457 bytes, err := readFile(string(rs), CertFile, KeyFile, CAFile) 458 return string(bytes), err 459 } 460 if err := strvals.ParseIntoFile(value, base, reader); err != nil { 461 return []byte{}, fmt.Errorf("failed parsing --set-file data: %s", err) 462 } 463 } 464 465 return yaml.Marshal(base) 466 } 467 468 // printRelease prints info about a release if the Debug is true. 469 func (i *installCmd) printRelease(rel *release.Release) { 470 if rel == nil { 471 return 472 } 473 // TODO: Switch to text/template like everything else. 474 fmt.Fprintf(i.out, "NAME: %s\n", rel.Name) 475 if settings.Debug { 476 printRelease(i.out, rel) 477 } 478 } 479 480 // locateChartPath looks for a chart directory in known places, and returns either the full path or an error. 481 // 482 // This does not ensure that the chart is well-formed; only that the requested filename exists. 483 // 484 // Order of resolution: 485 // - current working directory 486 // - if path is absolute or begins with '.', error out here 487 // - chart repos in $HELM_HOME 488 // - URL 489 // 490 // If 'verify' is true, this will attempt to also verify the chart. 491 func locateChartPath(repoURL, username, password, name, version string, verify bool, keyring, 492 certFile, keyFile, caFile string) (string, error) { 493 // 第一优先级是当前目录,对传入的目录去掉左右空行后,通过库函数判断当前文件夹是否存在,如果存在,则返回当前文件夹的全局绝对路径 494 name = strings.TrimSpace(name) 495 version = strings.TrimSpace(version) 496 if fi, err := os.Stat(name); err == nil { 497 abs, err := filepath.Abs(name) 498 if err != nil { 499 return abs, err 500 } 501 if verify { 502 if fi.IsDir() { 503 return "", errors.New("cannot verify a directory") 504 } 505 if _, err := downloader.VerifyChart(abs, keyring); err != nil { 506 return "", err 507 } 508 } 509 return abs, nil 510 } 511 // 如果传入的是绝对路径或者是以 . 开头的路径,则直接返回失败 512 if filepath.IsAbs(name) || strings.HasPrefix(name, ".") { 513 return name, fmt.Errorf("path %q not found", name) 514 } 515 516 // 如果前面两种都不是,则从 HELM_HOME 中寻找对应的文件,并返回对应的绝对路径 517 crepo := filepath.Join(settings.Home.Repository(), name) 518 if _, err := os.Stat(crepo); err == nil { 519 return filepath.Abs(crepo) 520 } 521 522 // 除了如上3种情况外,下面提供一个URL,需要从URL下载 523 dl := downloader.ChartDownloader{ 524 HelmHome: settings.Home, 525 Out: os.Stdout, 526 Keyring: keyring, 527 Getters: getter.All(settings), 528 Username: username, 529 Password: password, 530 } 531 if verify { 532 dl.Verify = downloader.VerifyAlways 533 } 534 // 如果设置了Chart repo,就会从对应的chart repo处查找Chart、 535 if repoURL != "" { 536 chartURL, err := repo.FindChartInAuthRepoURL(repoURL, username, password, name, version, 537 certFile, keyFile, caFile, getter.All(settings)) 538 if err != nil { 539 return "", err 540 } 541 name = chartURL 542 } 543 544 // 如果默认设置的下载路径没有对应的文件夹,那么先创建对应的文件夹。 545 if _, err := os.Stat(settings.Home.Archive()); os.IsNotExist(err) { 546 os.MkdirAll(settings.Home.Archive(), 0744) 547 } 548 549 // 将前面拼接好的带有URL路径的文件,或者用户直接提供的全路径http文件下载到对应的文件夹中,最后返回这个下载到本地路径的文件。 550 filename, _, err := dl.DownloadTo(name, version, settings.Home.Archive()) 551 if err == nil { 552 lname, err := filepath.Abs(filename) 553 if err != nil { 554 return filename, err 555 } 556 debug("Fetched %s to %s\n", name, filename) 557 return lname, nil 558 } else if settings.Debug { 559 return filename, err 560 } 561 562 return filename, fmt.Errorf("failed to download %q (hint: running `helm repo update` may help)", name) 563 } 564 565 func generateName(nameTemplate string) (string, error) { 566 t, err := template.New("name-template").Funcs(sprig.TxtFuncMap()).Parse(nameTemplate) 567 if err != nil { 568 return "", err 569 } 570 var b bytes.Buffer 571 err = t.Execute(&b, nil) 572 if err != nil { 573 return "", err 574 } 575 return b.String(), nil 576 } 577 578 func defaultNamespace() string { 579 if ns, _, err := kube.GetConfig(settings.KubeContext, settings.KubeConfig).Namespace(); err == nil { 580 return ns 581 } 582 return "default" 583 } 584 585 //readFile load a file from the local directory or a remote file with a url. 586 func readFile(filePath, CertFile, KeyFile, CAFile string) ([]byte, error) { 587 u, _ := url.Parse(filePath) 588 p := getter.All(settings) 589 590 // FIXME: maybe someone handle other protocols like ftp. 591 getterConstructor, err := p.ByScheme(u.Scheme) 592 593 if err != nil { 594 return ioutil.ReadFile(filePath) 595 } 596 597 getter, err := getterConstructor(filePath, CertFile, KeyFile, CAFile) 598 if err != nil { 599 return []byte{}, err 600 } 601 data, err := getter.Get(filePath) 602 return data.Bytes(), err 603 }