helm源码分析-install命令
| 阅读 | 共 5149 字,阅读约
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}