0x0034's Blog.

helm template 源码分析

字数统计: 2.8k阅读时长: 16 min
2021/12/16

入口文件helm/cmd/template.go
样例命令helm template {{chart_name}}

源码分析

template 也是复用 helm/install.gorunInstall()方法 , 而templateinstall 方法最大的不同是,设置了client.DryRun = true, 这个会让 RunWithContext 这个直接return,并且将manifest也就是最终渲染出来的Yaml输出到os.Stdout,也就是输出到屏幕.

1
2
3
4
if i.DryRun {
rel.Info.Description = "Dry run complete"
return rel, nil
}

流程分析

流程分析中并不会涉及到每一行的方法调用, 需要深入到每一行的同学可以自行去解读.

template.go 入口文件

template.go文件:

  1. 设置DryRun参数为true
  2. 设置Replace参数为true
  3. 运行runInstall方法渲染Yaml
  4. 将存放Yaml的manifest输出给os.Stdout

runInstall 方法

流程如下:

  1. name, chart, err := client.NameAndChart(args)

    • 分离参数,返回自定义资源名称name 和 chart路径chart
  2. cp, err := client.ChartPathOptions.LocateChart(chart, settings)

    • 转换chart路径为绝对路径cp
  3. chartRequested, err := loader.Load(cp)

    • 将绝对路径cptemplate目录文件转化为*chart.Chart对象.
    • chart.Chart结构体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    type Chart struct {
// Raw 包含最初包含在图表存档中的文件的原始内容。
// 这不应该被使用,除非在像“helm show values”这样的特殊情况下,我们想要显示原始值、注释等。
Raw []*File `json:"-"`
// Metadata 是chartfile的内容
Metadata *Metadata `json:"metadata"`
// Lock 是 Chart.lock的内容
Lock *Lock `json:"lock"`
// 对应chart 的 Templates
Templates []*File `json:"templates"`
// Values 是 chart 的默认配置
Values map[string]interface{} `json:"values"`
// Schema 是一个可选的 JSON 模式,用于在Values上强加结构
Schema []byte `json:"schema"`
// Files 是 chart 中的杂项文件
// 例如: README, LICENSE, etc.
Files []*File `json:"files"`

parent *Chart
dependencies []*Chart
}
  1. return client.RunWithContext(ctx, chartRequested, vals)

RunWithContext 方法

  1. valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)

    • value.yaml内容转化为map[string]interface{}
  2. 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)

    • valuesToRender 中的内容渲染到chrt.template文件中,最终输出为manifestDoc
  3. 判断DryRun,直接将文件返回到os.Stdout

涉及文件及内容

newTemplateCmd方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
func newTemplateCmd(cfg *action.Configuration, out io.Writer) *cobra.Command {
var validate bool
var includeCrds bool
var skipTests bool
client := action.NewInstall(cfg)
valueOpts := &values.Options{}
var kubeVersion string
var extraAPIs []string
var showFiles []string

cmd := &cobra.Command{
Use: "template [NAME] [CHART]",
Short: "locally render templates",
Long: templateDesc,
Args: require.MinimumNArgs(1),
ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
return compInstall(args, toComplete, client)
},
RunE: func(_ *cobra.Command, args []string) error {
if kubeVersion != "" {
parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion)
if err != nil {
return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
}
client.KubeVersion = parsedKubeVersion
}

client.DryRun = true
client.ReleaseName = "RELEASE-NAME"
client.Replace = true // Skip the name check
client.ClientOnly = !validate
client.APIVersions = chartutil.VersionSet(extraAPIs)
client.IncludeCRDs = includeCrds
rel, err := runInstall(args, client, valueOpts, out)

if err != nil && !settings.Debug {
if rel != nil {
return fmt.Errorf("%w\n\nUse --debug flag to render out invalid YAML", err)
}
return err
}

// We ignore a potential error here because, when the --debug flag was specified,
// we always want to print the YAML, even if it is not valid. The error is still returned afterwards.
if rel != nil {
var manifests bytes.Buffer
fmt.Fprintln(&manifests, strings.TrimSpace(rel.Manifest))
if !client.DisableHooks {
fileWritten := make(map[string]bool)
for _, m := range rel.Hooks {
if skipTests && isTestHook(m) {
continue
}
if client.OutputDir == "" {
fmt.Fprintf(&manifests, "---\n# Source: %s\n%s\n", m.Path, m.Manifest)
} else {
newDir := client.OutputDir
if client.UseReleaseName {
newDir = filepath.Join(client.OutputDir, client.ReleaseName)
}
err = writeToFile(newDir, m.Path, m.Manifest, fileWritten[m.Path])
if err != nil {
return err
}
fileWritten[m.Path] = true
}

}
}

// if we have a list of files to render, then check that each of the
// provided files exists in the chart.
if len(showFiles) > 0 {
// This is necessary to ensure consistent manifest ordering when using --show-only
// with globs or directory names.
splitManifests := releaseutil.SplitManifests(manifests.String())
manifestsKeys := make([]string, 0, len(splitManifests))
for k := range splitManifests {
manifestsKeys = append(manifestsKeys, k)
}
sort.Sort(releaseutil.BySplitManifestsOrder(manifestsKeys))

manifestNameRegex := regexp.MustCompile("# Source: [^/]+/(.+)")
var manifestsToRender []string
for _, f := range showFiles {
missing := true
// Use linux-style filepath separators to unify user's input path
f = filepath.ToSlash(f)
for _, manifestKey := range manifestsKeys {
manifest := splitManifests[manifestKey]
submatch := manifestNameRegex.FindStringSubmatch(manifest)
if len(submatch) == 0 {
continue
}
manifestName := submatch[1]
// manifest.Name is rendered using linux-style filepath separators on Windows as
// well as macOS/linux.
manifestPathSplit := strings.Split(manifestName, "/")
// manifest.Path is connected using linux-style filepath separators on Windows as
// well as macOS/linux
manifestPath := strings.Join(manifestPathSplit, "/")

// if the filepath provided matches a manifest path in the
// chart, render that manifest
if matched, _ := filepath.Match(f, manifestPath); !matched {
continue
}
manifestsToRender = append(manifestsToRender, manifest)
missing = false
}
if missing {
return fmt.Errorf("could not find template %s in chart", f)
}
}
for _, m := range manifestsToRender {
fmt.Fprintf(out, "---\n%s\n", m)
}
} else {
fmt.Fprintf(out, "%s", manifests.String())
}
}

return err
},
}

f := cmd.Flags()
addInstallFlags(cmd, f, client, valueOpts)
f.StringArrayVarP(&showFiles, "show-only", "s", []string{}, "only show manifests rendered from the given templates")
f.StringVar(&client.OutputDir, "output-dir", "", "writes the executed templates to files in output-dir instead of stdout")
f.BoolVar(&validate, "validate", false, "validate your manifests against the Kubernetes cluster you are currently pointing at. This is the same validation performed on an install")
f.BoolVar(&includeCrds, "include-crds", false, "include CRDs in the templated output")
f.BoolVar(&skipTests, "skip-tests", false, "skip tests from templated output")
f.BoolVar(&client.IsUpgrade, "is-upgrade", false, "set .Release.IsUpgrade instead of .Release.IsInstall")
f.StringVar(&kubeVersion, "kube-version", "", "Kubernetes version used for Capabilities.KubeVersion")
f.StringArrayVarP(&extraAPIs, "api-versions", "a", []string{}, "Kubernetes api versions used for Capabilities.APIVersions")
f.BoolVar(&client.UseReleaseName, "release-name", false, "use release name in the output-dir path.")
bindPostRenderFlag(cmd, &client.PostRenderer)

return cmd
}

runInstall 方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
func runInstall(args []string, client *action.Install, valueOpts *values.Options, out io.Writer) (*release.Release, error) {
debug("Original chart version: %q", client.Version)
if client.Version == "" && client.Devel {
debug("setting version to >0.0.0-0")
client.Version = ">0.0.0-0"
}
// name: 自定义资源名称
// chart: charts路径,
name, chart, err := client.NameAndChart(args)
if err != nil {
return nil, err
}
client.ReleaseName = name

if err := checkOCI(chart); err != nil {
return nil, err
}

// 转换chart路径为绝对路径
cp, err := client.ChartPathOptions.LocateChart(chart, settings)

if err != nil {
return nil, err
}

debug("CHART PATH: %s\n", cp)

p := getter.All(settings)
// 判断 -f values.yaml 并序列化values.yaml 文件
vals, err := valueOpts.MergeValues(p)
if err != nil {
return nil, err
}

// Check chart dependencies to make sure all are present in /charts
// 序列化charts中文件
chartRequested, err := loader.Load(cp)
// loader.Load 返回类型 *chart.Chart
// Chart结构体 包含 Templates []*File `json:"templates"`
// 其中包含了template目录下的文件名和文件内容
if err != nil {
return nil, err
}

// 校验chart 是否可安装
if err := checkIfInstallable(chartRequested); err != nil {
return nil, err
}

if chartRequested.Metadata.Deprecated {
warning("This chart is deprecated")
}

if req := chartRequested.Metadata.Dependencies; req != nil {
// If CheckDependencies returns an error, we have unfulfilled dependencies.
// As of Helm 2.4.0, this is treated as a stopping condition:
// https://github.com/helm/helm/issues/2209
if err := action.CheckDependencies(chartRequested, req); err != nil {
err = errors.Wrap(err, "An error occurred while checking for chart dependencies. You may need to run `helm dependency build` to fetch missing dependencies")
if client.DependencyUpdate {
man := &downloader.Manager{
Out: out,
ChartPath: cp,
Keyring: client.ChartPathOptions.Keyring,
SkipUpdate: false,
Getters: p,
RepositoryConfig: settings.RepositoryConfig,
RepositoryCache: settings.RepositoryCache,
Debug: settings.Debug,
}
if err := man.Update(); err != nil {
return nil, err
}
// Reload the chart with the updated Chart.lock file.
if chartRequested, err = loader.Load(cp); err != nil {
return nil, errors.Wrap(err, "failed reloading chart after repo update")
}
} else {
return nil, err
}
}
}

client.Namespace = settings.Namespace()

// Create context and prepare the handle of SIGTERM
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)

// Set up channel on which to send signal notifications.
// We must use a buffered channel or risk missing the signal
// if we're not ready to receive when the signal is sent.
cSignal := make(chan os.Signal, 2)
signal.Notify(cSignal, os.Interrupt, syscall.SIGTERM)
go func() {
<-cSignal
fmt.Fprintf(out, "Release %s has been cancelled.\n", args[0])
cancel()
}()
// 安装k8s资源
return client.RunWithContext(ctx, chartRequested, vals)
}

RunWithContext 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
func (i *Install)  RunWithContext(ctx context.Context, chrt *chart.Chart, vals map[string]interface{}) (*release.Release, error) {
// Check reachability of cluster unless in client-only mode (e.g. `helm template` without `--validate`)
// 检查apiserver是否可访问
if !i.ClientOnly {
if err := i.cfg.KubeClient.IsReachable(); err != nil {
return nil, err
}
}
// 名称是否可用
if err := i.availableName(); err != nil {
return nil, err
}

// 处理依赖
if err := chartutil.ProcessDependencies(chrt, vals); err != nil {
return nil, err
}

// Pre-install anything in the crd/ directory. We do this before Helm
// contacts the upstream server and builds the capabilities object.
if crds := chrt.CRDObjects(); !i.ClientOnly && !i.SkipCRDs && len(crds) > 0 {
// On dry run, bail here
if i.DryRun {
i.cfg.Log("WARNING: This chart or one of its subcharts contains CRDs. Rendering may fail or contain inaccuracies.")
} else if err := i.installCRDs(crds); err != nil {
return nil, err
}
}

if i.ClientOnly {
// Add mock objects in here so it doesn't use Kube API server
// NOTE(bacongobbler): used for `helm template`
i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy()
if i.KubeVersion != nil {
i.cfg.Capabilities.KubeVersion = *i.KubeVersion
}
i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...)
i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}

mem := driver.NewMemory()
mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem)
} else if !i.ClientOnly && len(i.APIVersions) > 0 {
i.cfg.Log("API Version list given outside of client only mode, this list will be ignored")
}

// Make sure if Atomic is set, that wait is set as well. This makes it so
// the user doesn't have to specify both
i.Wait = i.Wait || i.Atomic


// 获取Kube version 和 APIVersion
caps, err := i.cfg.getCapabilities()
if err != nil {
return nil, err
}

// special case for helm template --is-upgrade
isUpgrade := i.IsUpgrade && i.DryRun
options := chartutil.ReleaseOptions{
Name: i.ReleaseName,
Namespace: i.Namespace,
Revision: 1,
IsInstall: !isUpgrade,
IsUpgrade: isUpgrade,
}

// 组合go template对象
valuesToRender, err := chartutil.ToRenderValues(chrt, vals, options, caps)
fmt.Println(valuesToRender)
if err != nil {
return nil, err
}

// 创建release, 向kubernetes 写入一个secret 或者 configmap
rel := i.createRelease(chrt, vals)

var manifestDoc *bytes.Buffer
// 渲染资源
// Hooks: 钩子
// mainfestDoc: 具体的Yaml
// Note: note文件
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)
// Even for errors, attach this if available
if manifestDoc != nil {
rel.Manifest = manifestDoc.String()
}
// Check error from render
if err != nil {
rel.SetStatus(release.StatusFailed, fmt.Sprintf("failed to render resource: %s", err.Error()))
// Return a release with partial data so that the client can show debugging information.
return rel, err
}

// Mark this release as in-progress
rel.SetStatus(release.StatusPendingInstall, "Initial install underway")

var toBeAdopted kube.ResourceList
// 使用mainfest 创建resource资源
resources, err := i.cfg.KubeClient.Build(bytes.NewBufferString(rel.Manifest), !i.DisableOpenAPIValidation)
if err != nil {
return nil, errors.Wrap(err, "unable to build kubernetes objects from release manifest")
}

// It is safe to use "force" here because these are resources currently rendered by the chart.
err = resources.Visit(setMetadataVisitor(rel.Name, rel.Namespace, true))
if err != nil {
return nil, err
}

// Install requires an extra validation step of checking that resources
// don't already exist before we actually create resources. If we continue
// forward and create the release object with resources that already exist,
// we'll end up in a state where we will delete those resources upon
// deleting the release because the manifest will be pointing at that
// resource
if !i.ClientOnly && !isUpgrade && len(resources) > 0 {
// 判断资源是否冲突
toBeAdopted, err = existingResourceConflict(resources, rel.Name, rel.Namespace)
if err != nil {
return nil, errors.Wrap(err, "rendered manifests contain a resource that already exists. Unable to continue with install")
}
}

// Bail out here if it is a dry run
if i.DryRun {
rel.Info.Description = "Dry run complete"
return rel, nil
}

if i.CreateNamespace {
ns := &v1.Namespace{
TypeMeta: metav1.TypeMeta{
APIVersion: "v1",
Kind: "Namespace",
},
ObjectMeta: metav1.ObjectMeta{
Name: i.Namespace,
Labels: map[string]string{
"name": i.Namespace,
},
},
}
buf, err := yaml.Marshal(ns)
if err != nil {
return nil, err
}
resourceList, err := i.cfg.KubeClient.Build(bytes.NewBuffer(buf), true)
if err != nil {
return nil, err
}
if _, err := i.cfg.KubeClient.Create(resourceList); err != nil && !apierrors.IsAlreadyExists(err) {
return nil, err
}
}

// If Replace is true, we need to supercede the last release.
if i.Replace {
if err := i.replaceRelease(rel); err != nil {
return nil, err
}
}

// Store the release in history before continuing (new in Helm 3). We always know
// that this is a create operation.
if err := i.cfg.Releases.Create(rel); err != nil {
// We could try to recover gracefully here, but since nothing has been installed
// yet, this is probably safer than trying to continue when we know storage is
// not working.
return rel, err
}
rChan := make(chan resultMessage)
go i.performInstall(rChan, rel, toBeAdopted, resources)
go i.handleContext(ctx, rChan, rel)
result := <-rChan
//start preformInstall go routine
return result.r, result.e
}
CATALOG
  1. 1. 源码分析
  2. 2. 流程分析
    1. 2.1. template.go 入口文件
    2. 2.2. runInstall 方法
    3. 2.3. RunWithContext 方法
  3. 3. 涉及文件及内容