helm源码分析-install命令


| 阅读 |,阅读约 11 分钟
| 复制链接:

Overview

helm源码分析-install命令

  • helm install命令的代码入口:cmd/helm/install.go

使用介绍

  • 第一个参数为部署后的应用名称、第二个参数为chart包信息

  • helm install 支持多种文件格式

     1# 通过配置远程仓库安装 helm repo add xxx
     2helm install mymaria example/mariadb
     3# 通过本地压缩包
     4helm install mynginx ./nginx-1.2.3.tgz
     5# 通过本地文件夹
     6helm install mynginx ./nginx
     7# 通过绝对路径安装
     8helm install mynginx https://example.com/charts/nginx-1.2.3.tgz
     9# 通过名称+仓库地址
    10helm install --repo https://example.com/charts/ mynginx nginx
    
  • 默认安装chat仓库中最新最稳定的分支,可以通过 –version 指定版本

  • 参数覆盖:

    • 支持配置文件覆盖:-f values.yaml
    • 支持单个参数覆盖:–set key=value,–set-file key=filename

原理说明

核心实现包括两步:

  • 加载文件为Chart对象
  • 调用action.Run方法,部署这个Chart对象
    • 调用engine模块,渲染模板文件为k8s编排文件
    • 调用k8s的接口,部署生成的完整编排文件

源码分析

函数入口:main

cmd/helm/helm.go: main -> newRootCmd

 1func main() {
 2	actionConfig := new(action.Configuration)
 3  // 注册命令
 4	cmd, err := newRootCmd(actionConfig, os.Stdout, os.Args[1:])
 5  ...
 6  cobra.OnInitialize(func() {
 7    // 读取HELM_DRIVER配置
 8    // helm driver是存储helm数据的驱动,目前支持secret(默认)、configmap、memory、sql(目前只支持postgresql)
 9		helmDriver := os.Getenv("HELM_DRIVER")
10    // 初始化配置
11		if err := actionConfig.Init(settings.RESTClientGetter(), settings.Namespace(), helmDriver, debug); err != nil {
12			log.Fatal(err)
13		}
14		if helmDriver == "memory" {
15			loadReleasesInMemory(actionConfig)
16		}
17	})
18  ...
19}

注册命令:newRootCmd

cmd/helm/root.go:newRootCmd -> newInstallCmd

 1func newRootCmd(actionConfig *action.Configuration, out io.Writer, args []string) (*cobra.Command, error) {
 2	cmd := &cobra.Command{
 3		Use:          "helm",
 4		Short:        "The Helm package manager for Kubernetes.",
 5		Long:         globalUsage,
 6		SilenceUsage: true,
 7	}
 8  ...
 9  // Add subcommands
10	cmd.AddCommand(
11    ...
12    newInstallCmd(actionConfig, out),
13    ...
14  )
15}  

安装命令入口:newInstallCmd

cmd/helm/install.go:newInstallCmd -> runInstall

 1func newInstallCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
 2  // 创建一个用于执行install命令的action对象
 3  // helm中的操作都是类似的,cmd调用pkg/action包下的代码执行真正的操作
 4	client := action.NewInstall(cfg)
 5  ...
 6	cmd := &cobra.Command{
 7		Use:   "install [NAME] [CHART]",
 8		Short: "install a chart",
 9    ...
10		RunE: func(_ *cobra.Command, args []string) error {
11      // 真正执行install命令的核心代码
12      // 安装完成后返回值是一个 *release.Release 对象,用于后面打印到控制台
13      // 这个函数的第一个参数是args,用于获取生成的helm应用的名称和chart包
14      // 第二个参数是前面创建的Install对象
15			rel, err := runInstall(args, client, valueOpts, out)
16	    ...
17		},
18	}
19  ...
20	return cmd
21}

runInstall函数

 1func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) {
 2	...
 3  // 解析参数中的release名称、chart名称。(release名称没有就自动生成一个)
 4	name, chart, err := client.NameAndChart(args)
 5	...
 6	client.ReleaseName = name
 7
 8  // 查找指定chart名字的chart包路径,并下载到本地缓存中
 9  // 这一步不能确保chart包符合规范(只有ChartPathOptions中设置了verify才会校验)
10	cp, err := client.ChartPathOptions.LocateChart(chart, settings)
11	
12  ...
13  // 合并所有的参数值,包括-f xx.yaml和--set 设置的值
14  // 内部会读取yaml文件为map对象,并和基础的map合并。最后将--set设置的属性合并
15	vals, err := valueOpts.MergeValues(p)
16  ...
17
18	// 将给定路径的文件夹加载为内部的 *chart.Chart 对象
19  // 该过程会校验里面所有的dependencies
20	chartRequested, err := loader.Load(cp)
21	
22  // 校验这个chart对象是否被安装过
23	if err := checkIfInstallable(chartRequested); err != nil {
24		return nil, err
25	}
26
27  // 校验依赖项是否下载了
28	if req := chartRequested.Metadata.Dependencies; req != nil {
29		
30		if err := action.CheckDependencies(chartRequested, req); err != nil {
31			if client.DependencyUpdate {
32        // 依赖项通过downloaderManager下载
33				man := &downloader.Manager{
34					Out:              out,
35					ChartPath:        cp,
36					Keyring:          client.ChartPathOptions.Keyring,
37					SkipUpdate:       false,
38					Getters:          p,
39					RepositoryConfig: settings.RepositoryConfig,
40					RepositoryCache:  settings.RepositoryCache,
41					Debug:            settings.Debug,
42				}
43				if err := man.Update(); err != nil {
44					return nil, err
45				}
46				// Reload the chart with the updated Chart.lock file.
47				if chartRequested, err = loader.Load(cp); err != nil {
48					return nil, errors.Wrap(err, "failed reloading chart after repo update")
49				}
50			} else {
51				return nil, err
52			}
53		}
54	}
55
56  // 设置待安装的命名空间
57	client.Namespace = settings.Namespace()
58  
59  // 通过执行action中的Run命令,真正执行安装
60  // 这个函数的入参为 *chart.Chart对象
61  // 返回值为 *release.Release对象
62	return client.Run(chartRequested, vals)
63}

获取chart包地址:LocateChart函数

  • 判断当前路径下是否存在对应的chart包文件
  • 判断当前路径下是否存在对应的chart包文件夹,并用download模块校验包的合法性(如果配置了需要校验)
  • 通过远程仓库下载:用到download模块下载功能,这部分逻辑参考downloader篇的源码解析
  • 如果是远程仓库,先下载到本地并返回下载后的路径(下载到配置的缓存地址中)

pkg/action/install.go

 1func (c *ChartPathOptions) LocateChart(name string, settings *cli.EnvSettings) (string, error) {
 2	name = strings.TrimSpace(name)
 3	version := strings.TrimSpace(c.Version)
 4
 5  // chart包是当前路径下的文件的场景
 6  // 首先判断当前路径下是否存在以name命名的文件,以此作为chart包(tgz的场景)
 7	if _, err := os.Stat(name); err == nil {
 8		abs, err := filepath.Abs(name)
 9    // 如果存在这个目录,就返回这个文件夹的绝对路径
10		if err != nil {
11			return abs, err
12		}
13    // 如果设置了Verify参数,校验chart包
14		if c.Verify {
15			if _, err := downloader.VerifyChart(abs, c.Keyring); err != nil {
16				return "", err
17			}
18		}
19    // 如果设置校验,直接返回绝对路径
20		return abs, nil
21	}
22  
23  // chart包是当前路径下的文件夹的场景
24	if filepath.IsAbs(name) || strings.HasPrefix(name, ".") {
25		return name, errors.Errorf("path %q not found", name)
26	}
27
28  // 尝试通过远程仓库下载
29  // 首先构造下载器对象
30	dl := downloader.ChartDownloader{
31		Out:     os.Stdout,
32		Keyring: c.Keyring,
33		Getters: getter.All(settings),
34		Options: []getter.Option{
35			getter.WithBasicAuth(c.Username, c.Password),
36			getter.WithTLSClientConfig(c.CertFile, c.KeyFile, c.CaFile),
37			getter.WithInsecureSkipVerifyTLS(c.InsecureSkipTLSverify),
38		},
39		RepositoryConfig: settings.RepositoryConfig,
40		RepositoryCache:  settings.RepositoryCache,
41	}
42  ...
43  // 从配置了的仓库地址中寻找chart包
44	if c.RepoURL != "" {
45    // 内部实现就是先找到仓库的索引
46    // 根据索引查找chart包是否存在
47    // 如果找到就返回这个chart包的url地址
48		chartURL, err := repo.FindChartInAuthAndTLSRepoURL(c.RepoURL, c.Username, c.Password, name, version,
49			c.CertFile, c.KeyFile, c.CaFile, c.InsecureSkipTLSverify, getter.All(settings))
50		if err != nil {
51			return "", err
52		}
53    // 保存url地址
54		name = chartURL
55	}
56  ...
57  // 将资源下载到缓存目录下
58	filename, _, err := dl.DownloadTo(name, version, settings.RepositoryCache)
59	...
60}

chart包加载为Chart对象:loader.Load函数

  • 参数是上一步得到的chart包本地路径(前一个函数介绍过,如果是远程仓库,会先下载到本地)
  • 解析过程是读取chart包中约定的文件,并存储到Chart这个对象中

pkg/loader/load.go

 1func Load(name string) (*chart.Chart, error) {
 2  // 构造Loader对象,实现看后面
 3	l, err := Loader(name)
 4	if err != nil {
 5		return nil, err
 6	}
 7  // 调用Loader对象的load方法
 8  // 不管是文件还是文件夹,最终都会调用到 pkg/chart/loader/load.go中的LoadFiles()方法
 9	return l.Load()
10}
11
12func Loader(name string) (ChartLoader, error) {
13	fi, err := os.Stat(name)
14  // 如果文件不存在,返回错误
15	if err != nil {
16		return nil, err
17	}
18  // 如果是文件夹,返回操作文件夹的Loader,即DirLoader
19	if fi.IsDir() {
20    // 最终调用 pkg/chart/loader/directory.go中的LoadDir()方法,内部调用pkg/chart/loader/load.go中的LoadFiles()方法
21		return DirLoader(name), nil
22	}
23  // 如果是文件,返回操作文件的Loader,即FileLoader
24  // 最终调用 pkg/chart/loader/archive.go中的LoadFile()方法。内部调用pkg/chart/loader/load.go中的LoadFiles()方法
25	return FileLoader(name), nil
26}
27
28func LoadFiles(files []*BufferedFile) (*chart.Chart, error) {
29	c := new(chart.Chart)
30	subcharts := make(map[string][]*BufferedFile)
31
32	// Chart.yaml文件解析为Chart对象的Metadata字段
33	for _, f := range files {
34		c.Raw = append(c.Raw, &chart.File{Name: f.Name, Data: f.Data})
35		if f.Name == "Chart.yaml" {
36			if c.Metadata == nil {
37				c.Metadata = new(chart.Metadata)
38			}
39			if err := yaml.Unmarshal(f.Data, c.Metadata); err != nil {
40				return c, errors.Wrap(err, "cannot load Chart.yaml")
41			}
42      ...
43		}
44	}
45  // 解析chart包中的各种文件:Chart.yaml、values.yaml、requirements.yaml、templates、charts
46	for _, f := range files {
47		switch {
48		case f.Name == "Chart.yaml":
49			// already processed
50			continue
51		case f.Name == "Chart.lock":
52			...
53		case f.Name == "values.yaml":
54			...
55		case f.Name == "values.schema.json":
56			...
57		case f.Name == "requirements.yaml":
58			...
59		case f.Name == "requirements.lock":
60			...
61		case strings.HasPrefix(f.Name, "templates/"):
62			c.Templates = append(c.Templates, &chart.File{Name: f.Name, Data: f.Data})
63		case strings.HasPrefix(f.Name, "charts/"):
64			...
65		default:
66			c.Files = append(c.Files, &chart.File{Name: f.Name, Data: f.Data})
67		}
68	}
69  ...
70  // 处理依赖项
71	for n, files := range subcharts {
72		...
73		c.AddDependency(sc)
74	}
75
76	return c, nil
77}

部署Chart资源:client.Run函数

源码位置:pkg/action/install.go

  1func (i *Install) Run(chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
  2	...
  3  // 校验名字是否可用,不可用的情况包括:空名称、名称太长(超过53字符)、已经被占用、
  4	if err := i.availableName(); err != nil {
  5		return nil, err
  6	}
  7
  8  // 优先安装crd目录下的资源文件,这些文件在最开始执行
  9	if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
 10		if i.DryRun {
 11			...
 12      // 安装crd
 13		} else if err := i.installCRDs(crds); err != nil {
 14			return nil, err
 15		}
 16	}
 17	...
 18  // 处理依赖
 19	if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
 20		return nil, err
 21	}
 22  ...
 23	// 渲染values
 24	valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)
 25	if err != nil {
 26		return nil, err
 27	}
 28
 29  // 通过Chart对象生成Release对象
 30  // Release.Manifest存放了helm渲染后的原始k8s编排文件
 31	rel := i.createRelease(chrt, vals)
 32
 33	var manifestDoc *bytes.Buffer
 34  // 渲染资源文件,包括hooks、doc等
 35  // 这里调用engine模块的函数
 36	rel.Hooks, manifestDoc, rel.Info.Notes, err = i.cfg.renderResources(chrt, valuesToRender, i.ReleaseName, i.OutputDir, i.SubNotes, i.UseReleaseName, i.IncludeCRDs, i.PostRenderer, i.DryRun)
 37	...
 38  // 取出Release中渲染成功后的Manifest资源,通过Manifest构造出k8s中资源对象
 39	resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
 40	if err != nil {
 41		return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
 42	}
 43
 44	// It is safe to use "force" here because these are resources currently rendered by the chart.
 45	err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true))
 46	...
 47  // 自动创建namespace,就生成一个namespace的yaml文件并执行
 48	if i.CreateNamespace {
 49		ns := &v1.Namespace{
 50			TypeMeta: metav1.TypeMeta{
 51				APIVersion: "v1",
 52				Kind:       "Namespace",
 53			},
 54			ObjectMeta: metav1.ObjectMeta{
 55				Name: i.Namespace,
 56				Labels: map[string]string{
 57					"name": i.Namespace,
 58				},
 59			},
 60		}
 61		buf, err := yaml.Marshal(ns)
 62		if err != nil {
 63			return nil, err
 64		}
 65		resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true)
 66		if err != nil {
 67			return nil, err
 68		}
 69		if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) {
 70			return nil, err
 71		}
 72	}
 73
 74  // 执行replace操作
 75	if i.Replace {
 76		if err := i.replaceRelease(rel); err != nil {
 77			return nil, err
 78		}
 79	}
 80
 81  // 保存发布历史
 82  // 调用 pkg/storage/storage.go模块
 83  // storage模块也是定了了接口和多种实现,默认使用secret实现
 84  
 85	// Store the release in history before continuing (new in Helm 3). We always know
 86	// that this is a create operation.
 87	if err := i.cfg.Releases.Create(rel); err != nil {
 88		// We could try to recover gracefully here, but since nothing has been installed
 89		// yet, this is probably safer than trying to continue when we know storage is
 90		// not working.
 91		return rel, err
 92	}
 93
 94	// pre-install hooks
 95	if !i.DisableHooks {
 96		if err := i.cfg.execHook(rel, release.HookPreInstall, i.Timeout); err != nil {
 97			return i.failRelease(rel, fmt.Errorf("failed pre-install: %s", err))
 98		}
 99	}
100
101	// At this point, we can do the install. Note that before we were detecting whether to
102	// do an update, but it's not clear whether we WANT to do an update if the re-use is set
103	// to true, since that is basically an upgrade operation.
104  
105  // 调用k8s原生api执行资源创建或者更新操作
106	if len(toBeAdopted) == 0 && len(resources) > 0 {
107		if _, err := i.cfg.KubeClient.Create(resources); err != nil {
108			return i.failRelease(rel, err)
109		}
110	} else if len(resources) > 0 {
111		if _, err := i.cfg.KubeClient.Update(toBeAdopted, resources, false); err != nil {
112			return i.failRelease(rel, err)
113		}
114	}
115
116  // 等待执行返回
117	if i.Wait {
118		if i.WaitForJobs {
119			if err := i.cfg.KubeClient.WaitWithJobs(resources, i.Timeout); err != nil {
120				return i.failRelease(rel, err)
121			}
122		} else {
123			if err := i.cfg.KubeClient.Wait(resources, i.Timeout); err != nil {
124				return i.failRelease(rel, err)
125			}
126		}
127	}
128
129	if !i.DisableHooks {
130		if err := i.cfg.execHook(rel, release.HookPostInstall, i.Timeout); err != nil {
131			return i.failRelease(rel, fmt.Errorf("failed post-install: %s", err))
132		}
133	}
134
135	if len(i.Description) > 0 {
136		rel.SetStatus(release.StatusDeployed, i.Description)
137	} else {
138		rel.SetStatus(release.StatusDeployed, "Install complete")
139	}
140
141	// This is a tricky case. The release has been created, but the result
142	// cannot be recorded. The truest thing to tell the user is that the
143	// release was created. However, the user will not be able to do anything
144	// further with this release.
145	//
146	// One possible strategy would be to do a timed retry to see if we can get
147	// this stored in the future.
148  
149  // 记录发布历史,调用driver模块实现
150	if err := i.recordRelease(rel); err != nil {
151		i.cfg.Log("failed to record the release: %s", err)
152	}
153
154	return rel, nil
155}

Chart生成Release函数:createRelease

 1func (i *Install) createRelease(chrt *chart.Chart, rawVals map[string]interface{}) *release.Release {
 2	ts := i.cfg.Now()
 3  // 用chart信息初始化一个release对象 
 4	return &release.Release{
 5		Name:      i.ReleaseName,
 6		Namespace: i.Namespace,
 7		Chart:     chrt,
 8		Config:    rawVals,
 9		Info: &release.Info{
10			FirstDeployed: ts,
11			LastDeployed:  ts,
12			Status:        release.StatusUnknown,
13		},
14		Version: 1,
15	}
16}

模板渲染函数:renderResources

  • 该函数的核心是渲染模板文件,最终生成k8s的元素编排文件(后续的步骤就是调用k8s的接口部署该编排文件)

这个函数前面写着注释:写的很糟糕,需要重构

  1func (c *Configuration) renderResources(ch *chart.Chart, values chartutil.Values, releaseName, outputDir string, subNotes, useReleaseName, includeCrds bool, pr postrender.PostRenderer, dryRun bool) ([]*release.Hook, *bytes.Buffer, string, error) {
  2	hs := []*release.Hook{}
  3	b := bytes.NewBuffer(nil)
  4
  5	caps, err := c.getCapabilities()
  6	if err != nil {
  7		return hs, b, "", err
  8	}
  9
 10	if ch.Metadata.KubeVersion != "" {
 11		if !chartutil.IsCompatibleRange(ch.Metadata.KubeVersion, caps.KubeVersion.String()) {
 12			return hs, b, "", errors.Errorf("chart requires kubeVersion: %s which is incompatible with Kubernetes %s", ch.Metadata.KubeVersion, caps.KubeVersion.String())
 13		}
 14	}
 15
 16	var files map[string]string
 17	var err2 error
 18
 19	// A `helm template` or `helm install --dry-run` should not talk to the remote cluster.
 20	// It will break in interesting and exotic ways because other data (e.g. discovery)
 21	// is mocked. It is not up to the template author to decide when the user wants to
 22	// connect to the cluster. So when the user says to dry run, respect the user's
 23	// wishes and do not connect to the cluster.
 24  
 25  // 调用engine模块去渲染
 26	if !dryRun && c.RESTClientGetter != nil {
 27		rest, err := c.RESTClientGetter.ToRESTConfig()
 28		if err != nil {
 29			return hs, b, "", err
 30		}
 31		files, err2 = engine.RenderWithClient(ch, values, rest)
 32	} else {
 33		files, err2 = engine.Render(ch, values)
 34	}
 35
 36	if err2 != nil {
 37		return hs, b, "", err2
 38	}
 39
 40	// NOTES.txt gets rendered like all the other files, but because it's not a hook nor a resource,
 41	// pull it out of here into a separate file so that we can actually use the output of the rendered
 42	// text file. We have to spin through this map because the file contains path information, so we
 43	// look for terminating NOTES.txt. We also remove it from the files so that we don't have to skip
 44	// it in the sortHooks.
 45	var notesBuffer bytes.Buffer
 46	for k, v := range files {
 47		if strings.HasSuffix(k, notesFileSuffix) {
 48			if subNotes || (k == path.Join(ch.Name(), "templates", notesFileSuffix)) {
 49				// If buffer contains data, add newline before adding more
 50				if notesBuffer.Len() > 0 {
 51					notesBuffer.WriteString("\n")
 52				}
 53				notesBuffer.WriteString(v)
 54			}
 55			delete(files, k)
 56		}
 57	}
 58	notes := notesBuffer.String()
 59
 60	// Sort hooks, manifests, and partials. Only hooks and manifests are returned,
 61	// as partials are not used after renderer.Render. Empty manifests are also
 62	// removed here.
 63  
 64  // 将渲染后的编排文件排序
 65	hs, manifests, err := releaseutil.SortManifests(files, caps.APIVersions, releaseutil.InstallOrder)
 66	if err != nil {
 67		// By catching parse errors here, we can prevent bogus releases from going
 68		// to Kubernetes.
 69		//
 70		// We return the files as a big blob of data to help the user debug parser
 71		// errors.
 72		for name, content := range files {
 73			if strings.TrimSpace(content) == "" {
 74				continue
 75			}
 76			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", name, content)
 77		}
 78		return hs, b, "", err
 79	}
 80
 81	// Aggregate all valid manifests into one big doc.
 82	fileWritten := make(map[string]bool)
 83
 84	if includeCrds {
 85		for _, crd := range ch.CRDObjects() {
 86			if outputDir == "" {
 87				fmt.Fprintf(b, "---\n# Source: %s\n%s\n", crd.Name, string(crd.File.Data[:]))
 88			} else {
 89				err = writeToFile(outputDir, crd.Filename, string(crd.File.Data[:]), fileWritten[crd.Name])
 90				if err != nil {
 91					return hs, b, "", err
 92				}
 93				fileWritten[crd.Name] = true
 94			}
 95		}
 96	}
 97
 98	for _, m := range manifests {
 99		if outputDir == "" {
100			fmt.Fprintf(b, "---\n# Source: %s\n%s\n", m.Name, m.Content)
101		} else {
102			newDir := outputDir
103			if useReleaseName {
104				newDir = filepath.Join(outputDir, releaseName)
105			}
106			// NOTE: We do not have to worry about the post-renderer because
107			// output dir is only used by `helm template`. In the next major
108			// release, we should move this logic to template only as it is not
109			// used by install or upgrade
110			err = writeToFile(newDir, m.Name, m.Content, fileWritten[m.Name])
111			if err != nil {
112				return hs, b, "", err
113			}
114			fileWritten[m.Name] = true
115		}
116	}
117
118	if pr != nil {
119		b, err = pr.Run(b)
120		if err != nil {
121			return hs, b, notes, errors.Wrap(err, "error while running post render on files")
122		}
123	}
124
125	return hs, b, notes, nil
126}