k8s(16)-- 模板开发

模板开发

1. 内置对象

前面我们介绍了 Helm Chart 的一些基本概念和使用,接下来我们重点介绍下 Chart 模板的编写。模板会渲染成 Kubernetes 的资源清单文件。

前面提到过我们可以在模板中使用 {{ .Release.Name }} 获取 release 的名称,Release 是我们可以在模板中访问的几个顶级对象之一:

  • Release:该对象描述了 release 本身的相关信息,它内部有几个对象:

    • Release.Name:release 名称

    • Release.Namespace:release 安装到的命名空间

    • Release.IsUpgrade:如果当前操作是升级或回滚,则该值为 true

    • Release.IsInstall:如果当前操作是安装,则将其设置为 true

    • Release.Revision:release 的 revision 版本号,在安装的时候,值为1,每次升级或回滚都会增加

    • Reelase.Service:渲染当前模板的服务,在 Helm 上,实际上该值始终为 Helm

  • Values:从 values.yaml 文件和用户提供的 values 文件传递到模板的 Values 值,默认情况下,Values 是空的。

  • Chart:获取 Chart.yaml 文件的内容,该文件中的任何数据都可以访问,例如 {{ .Chart.Name }}-{{ .Chart.Version}} 可以渲染成 mychart-0.1.0,该对象下面可用的字段前面我们已经提到过了。

  • Files:可以访问 chart 中的所有非特殊文件,虽然无法使用它来访问模板文件,但是可以来访问 chart 中的其他文件。

    • Files.Get:用于根据名称获取文件(比如 .Files.Get config.ini

    • Files.GetBytes:用于以 bytes 数组而不是字符串的形式来获取文件内容的函数,这对于类似于图片之类的东西很有用

    • Files.Glob:用于返回名称于给定的 shell glob 模式匹配的文件列表

    • Files.Lines:可以逐行读取文件的函数,对于遍历文件中的每行内容很有用

    • Files.AsSecrets:将文件内容以 Base64 编码的字符串返回的函数

    • Files.AsConfig:将文件正文作为 YAML 字典返回的函数

  • Capabilities:提供了获取有关 Kubernetes 集群支持功能的信息的对象

    • Capabilities.APIVersions:支持的版本集合

    • Capabilities.APIVersions.Has $version:判断一个版本(比如 batch/v1)或资源(比如 apps/v1/Deployment)是否可用

    • Capabilities.Kube.Version:Kubernetes 的版本

    • Capabilities.Kube:是 Kubernetes 版本的缩写

    • Capabilities.Kube.Major:Kubernetes 主版本

    • Capabilities.Kube.Minor:Kubernetes 的次版本

  • Template:包含当前正在执行的模板的相关信息

    • Name:当前模板的命名空间文件路径(比如 mychart/templates/mytemplate.yaml

    • BaePath:当前 chart 的模板目录的命名空间路径(比如 mychart/templates

需要注意的是内置的对象始终是以大写字母开头的,这也是符合 Go 的命名约定的,创建自己的名称的时候,可以自由使用以适合你团队的约定,一些团队,比如 Kubernetes Charts 团队,选择仅使用首字母小写,以区分本地名称和内置名称,这里我们也会遵循该约定。

2. Values 文件

前面我们介绍了 Helm 模板提供的内置对象,其中就有一个内置对象 Values,该对象提供对传递到 chart 中的 values 值的访问,其内容主要有4个来源:

  • chart 文件中的 values.yaml 文件

  • 如果这是子 chart,父 chart 的 values.yaml 文件

  • -f 参数传递给 helm installhelm upgrade 的 values 值文件(例如 helm install -f myvals.yaml ./mychart

  • --set 传递的各个参数(例如 helm install --set foo=bar ./mychart

values.yaml 文件是默认值,可以被父 chart 的 values.yaml 文件覆盖,而后者又可以由用户提供的 values 值文件覆盖,而该文件又可以被 --set 参数覆盖。

values 值文件是纯 YAML 文件,我们可以来编辑 mychart/values.yaml 文件然后编辑 ConfigMap 模板。删除 values.yaml 中的默认设置后,我们将只设置一个参数:

favoriteDrink: coffee

现在我们可以在模板中直接使用它:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favoriteDrink }}

可以看到在最后一行我们将 favoriteDrink 作为 Values 的属性进行访问:{{ .Values.favoriteDrink }}。我们可以来看看是如何渲染的:

[root@master helm]# helm install --generate-name --dry-run --debug ./mychart
install.go:178: [debug] Original chart version: ""
install.go:195: [debug] CHART PATH: /mnt/k8s/helm/mychart

NAME: mychart-1663567115
LAST DEPLOYED: Mon Sep 19 13:58:35 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
affinity: {}
HOOKS:
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1663567115-configmap
data:
  myvalue: "Hello World"
  drink: coffee

由于在默认的 values.yaml 文件中将 favoriteDrink 设置为了 coffee,所以这就是模板中显示的值,我们可以通过在调用 helm install 的过程中添加 --set 参数来覆盖它:

[root@master helm]# helm install --generate-name --dry-run --debug --set favoriteDrink=slurm ./mychart
install.go:178: [debug] Original chart version: ""
install.go:195: [debug] CHART PATH: /mnt/k8s/helm/mychart

NAME: mychart-1663567182
LAST DEPLOYED: Mon Sep 19 13:59:42 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
USER-SUPPLIED VALUES:
favoriteDrink: slurm

COMPUTED VALUES:
affinity: {}
autoscaling:
  enabled: false
  maxReplicas: 100
  minReplicas: 1
  targetCPUUtilizationPercentage: 80
favoriteDrink: slurm
fullnameOverride: ""
image:
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1663567182-configmap
data:
  myvalue: "Hello World"
  drink: slurm

因为 --set 的优先级高于默认的 values.yaml 文件,所以我们的模板会生成 drink: slurm。Values 值文件也可以包含更多结构化的内容,例如我们可以在 values.yaml 文件中创建一个 favorite 的部分,然后在其中添加几个 keys:

favorite:
  drink: coffee
  food: pizza

现在我们再去修改下我们的模板:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink }}
  food: {{ .Values.favorite.food }}

2.1 删除默认 KEY

如果你需要从默认值中删除 key,则可以将该 key 的值覆盖为 null,在这种情况下,Helm 将从覆盖的 values 中删除该 key。例如,在 Drupal chart 中配置一个 liveness 探针:

livenessProbe:
  httpGet:
    path: /user/login
    port: http
  initialDelaySeconds: 120

如果你想使用 --set livenessProbe.exec.command=[cat, docroot/CHANGELOG.txt] 将 livenessProbe 的处理程序覆盖为 exec 而不是 httpGet,则 Helm 会将默认键和覆盖键合并在一起,如下所示:

livenessProbe:
  httpGet:
    path: /user/login
    port: http
  exec:
    command:
    - cat
    - docroot/CHANGELOG.txt
  initialDelaySeconds: 120

但是,这样却有一个问题,因为你不能声明多个 livenessProbe 处理程序,为了解决这个问题,你可以让 Helm 通过将 livenessProbe.httpGet 设置为 null 来删除它:

$ helm install stable/drupal --set image=my-registry/drupal:0.1.0 --set livenessProbe.exec.command=[cat, docroot/CHANGELOG.txt] --set livenessProbe.httpGet=null

到这里我们已经了解到了几个内置对象,并利用它们将信息注入到了模板中,现在我们来看看模板引擎的另外方面:函数和管道。

3. 函数和管道

现在我们已经了解了如何将信息加入到模板中,但是这些信息都是直接原样的放置过去的,有时候,我们希望以一种对我们更有用的方式来转换提供的数据。

下面让我们从一个最佳实践开始:将 .Values 对象中的字符串注入模板时,我们应该引用这些字符串,我们可以通过在 template 指令中调用 quote 函数来实现,比如:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ quote .Values.favorite.drink }}
  food: {{ quote .Values.favorite.food }}

模板函数遵循的语法规则是 functionName arg1 arg2...,在上面的代码片段中,quote .Values.favorite.drink 会调用 quote 函数并传递一个单个参数。

Helm 有60多种可用的函数,其中一些是由 Go 模板语言本身定义的,其他大多数都是 Sprig 模板库提供的,接下来我们会通过部分示例来逐步介绍其中的一些功能函数。

当我们谈论 Helm 模板语言 的时候,就好像是特定于 Helm 一样,但实际上它是 Go 模板语言加上一些额外的函数以及各种封装程序的组合,以将某些对象暴露给模板。当我们需要学习模板的时候,Go 模板上有许多资源会对我们有所帮助的。

3.1 管道

模板语言有一个强大的功能就是**管道(Pipeline)**概念,管道利用 UNIX 的概念,将一系列模板命令链接在一起,一起对外提供服务,换句话说,管道是按顺序完成多项工作的有效方式,我们来使用管道重写上面的示例模板:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | quote }}
  food: {{ .Values.favorite.food | quote }}

在这里我们没有调用 quote ARGUMENT 函数,而是颠倒了下顺序,我们使用管道符(|)将参数发送给函数:.Values.favorite.drink | quote,使用管道,我们可以将多个功能链接在一起:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | quote }}
  food: {{ .Values.favorite.food | upper | quote }}

反转顺序是模板中常见的做法,我们会看到 .val | quotequote .val 用法更多,虽然两种方法都是可以的。

最后,模板渲染后,会产生如下所示的结果:

[root@master helm]# helm install --generate-name --dry-run --debug ./mychart
install.go:178: [debug] Original chart version: ""
install.go:195: [debug] CHART PATH: /mnt/k8s/helm/mychart

NAME: mychart-1663567467
LAST DEPLOYED: Mon Sep 19 14:04:27 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
affinity: {}
autoscaling:
  enabled: false
  maxReplicas: 100
  minReplicas: 1
  targetCPUUtilizationPercentage: 80
favorite:
  drink: coffee
  food: pizza
favoriteDrink: coffee
fullnameOverride: ""
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1663567467-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"

我们可以看到 values 中的 pizza 值已经被转换成了 "PIZZA"。当这样传递参数的时候,第一个求值结果(.Values.favorite.drink)会作为一个参数发送给函数,我们可以修改上面的 drink 示例,用一个带有两个参数的函数进行说明:repeat COUNT STRING

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | repeat 5 | quote }}
  food: {{ .Values.favorite.food | upper | quote }}

repeat 函数将重复字符串给定的次数,渲染后我们可以得到如下的输出结果:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575966939-configmap
data:
  myvalue: "Hello World"
  drink: "coffeecoffeecoffeecoffeecoffee"
  food: "PIZZA"

3.2 default 函数

在模板中经常会使用到的一个函数是 default 函数:default DEFAULT_VALUE GIVEN_VALUE,该函数允许你在模板内部指定默认值,我们来修改上面示例中的模板:

food: {{ .Values.favorite.food | default "rice" | upper | quote }}

正常运行,我们还是可以得到 values.yaml 文件中定义的 pizza:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575966939-configmap
data:
  myvalue: "Hello World"
  drink: "coffeecoffeecoffeecoffeecoffee"
  food: "PIZZA"

现在我们从 values.yaml 文件中移除 food 的定义:

favorite:
  drink: coffee
  # food: pizza

现在我们重新运行 helm install --generate-name --dry-run --debug ./mychart 将渲染成如下的 YAML 文件:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575967394-configmap
data:
  myvalue: "Hello World"
  drink: "coffeecoffeecoffeecoffeecoffee"
  food: "RICE"

在一个真实的 chart 模板中,所有的静态默认值都应位于 values.yaml 文件中,并且不应该重复使用 default 函数,但是,默认命令非常适合计算不能在 values.yaml 文件中声明的 values 值,例如:

food: {{ .Values.favorite.food | default (printf "%s-rice" (include "mychart.fullname" .)) }}
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1663569147-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: mychart-1663569147-rice

不过在有些地方,if 条件语句可能比 default 函数更合适,我们会在后面了解到。

模板函数和管道是将数据转换后然后将其插入到 YAML 文件中的一种强大方法,但是有的时候有必要添加一些模板逻辑,这些逻辑比仅仅插入字符串要复杂得多,下面我们将来了解模板语言中提供的控制流程。

3.3 运算符函数

另外需要注意的是在模板中,运算符(eq、ne、lt、gt、and、or 等等)均实现为函数,在管道中,运算符可以用括号()进行分割。

4. 流程控制

控制流程为模板作者提供了控制模板生成流程的功能,Helm 的模板语言提供了以下一些流程控制:

  • if/else 条件语句

  • with 指定一个作用域范围

  • range 提供类似于 for each 这样的循环样式

除此之外,还提供了一些声明和使用命名模板的操作:

  • define 在模板内部声明一个新的命名模板

  • template 导入一个命名模板

  • block 声明了一种特殊的可填充模板区域。

这里我们先来了解 ifwithrange 语句的使用,其他将在后面的命名模板部分介绍。

4.1 if/else

首先我们先来了解下有条件地在模板中包含一个文本区域,就是 if/else ,这个条件判断的基本结构如下所示:

{{ if PIPELINE }}
  # Do something
{{ else if OTHER PIPELINE }}
  # Do something else
{{ else }}
  # Default case
{{ end }}

可以看到我们这里判断的是管道而不是一个 values 值,这是因为控制结构可以执行整个管道,而不仅仅是判断值。如果值为以下的一些内容,则将管道判断为 false:

  • 布尔 false

  • 数字零

  • 一个空字符串

  • nil(empty 或者 null)

  • 一个空集合(map、slice、tuple、dict、array)

在其他条件下,条件都为真。

现在我们在上面的示例模板 ConfigMap 中添加一个简单的条件,如果 drink 设置为 coffee,我们就添加另外一个设置:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | default "tea" | quote }}
  food: {{ .Values.favorite.food | upper | quote }}
  {{ if eq .Values.favorite.drink "coffee" }}mug: true{{ end }}

我们把 values.yaml 文件内容设置成下面的样子:

favorite:
  # drink: coffee
  food: pizza

由于我们注释掉了 drink: coffee,所以渲染后输出不会包含 mug: true 的标志,但是如果我们把注释取消掉,则应该输出如下所示的内容:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575970308-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  mug: true

这是因为上面模板中我们添加了 if eq .Values.favorite.drink "coffee" 这样的条件判断,相当于是判断 .Values.favorite.drink 值是否等于 "coffee",如果相等则渲染 mug: true

4.2 空格控制

还有一个非常重要的功能点就是关于空格的控制,因为空格对于 YAML 文件非常重要的,不是说任意缩进就可以,依然还是以前面的例子为例,我们来格式化下模板格式以更易于阅读:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | default "tea" | quote }}
  food: {{ .Values.favorite.food | upper | quote }}
  {{ if eq .Values.favorite.drink "coffee" }}
    mug: true
  {{ end }}

现在我们的模板看上去更易于阅读了,但是我们通过模板引擎来渲染下,却会得到如下的错误信息:

[root@master helm]# helm install --generate-name --dry-run --debug ./mychart
install.go:178: [debug] Original chart version: ""
install.go:195: [debug] CHART PATH: /mnt/k8s/helm/mychart

Error: INSTALLATION FAILED: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: did not find expected key
helm.go:84: [debug] error converting YAML to JSON: yaml: line 9: did not find expected key

这是因为我们在模板中添加了空格,生成了不正确的 YAML 文件:

apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575970308-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
    mug: true

我们可以看到 mug: true 的缩进是有问题的,不符合 YAML 文件格式,现在我们讲缩进去掉试看看:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | default "tea" | quote }}
  food: {{ .Values.favorite.food | upper | quote }}
  {{ if eq .Values.favorite.drink "coffee" }}
  mug: true
  {{ end }}

重新渲染模板,然后可以发现已经可以正常通过了,但是渲染出来的 YAML 文件格式看上去还是有点奇怪:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1663569522-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  
  mug: true

我们可以看到得到的 YAML 文件中多了一些空行,这是因为模板引擎渲染的时候它会删除 {{}} 之间的内容,但是会完全保留其余的空格。我们知道在 YAML 文件中空格是有意义的,所以管理空格就变得非常重要了,不过 Helm 模板也提供了一些工具来帮助我们管理空格。

首先可以使用特殊字符修改模板声明的花括号语法,以告诉模板引擎去掉空格。{{- 添加了破折号和空格表示应将左边的空格移除,-}}表示将右边的空格移除,另外也需要注意的是,换行符也是空格

需要注意的时候要确保 - 和指令的其余部分之间要有空格,{{- 3 }} 表示删除左边的空格并打印3,但是 {{-3 }}表示打印-3

使用这个语法,我们可以修改上面的模板来移除多余的空行:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | default "tea" | quote }}
  food: {{ .Values.favorite.food | upper | quote }}
  {{- if eq .Values.favorite.drink "coffee" }}
  mug: true
  {{- end }}

渲染后可以看到空行被移除掉了:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1663569580-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  mug: true

为了更加清楚地说明这个问题,我们用*来代替将要删除的每个空格,行尾的*表示被删除的换行符:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | default "tea" | quote }}
  food: {{ .Values.favorite.food | upper | quote }}*
**{{- if eq .Values.favorite.drink "coffee" }}
  mug: true*
**{{- end }}

所以我们这里用 {{- 表示的就是删除本行开头的两个空格以及上一行的换行符,这样是不是就将空行都删除了啊。

在使用移除空格的时候还需要小心,比如下面的操作:

food: {{ .Values.favorite.food | upper | quote }}
{{- if eq .Values.favorite.drink "coffee" -}}
mug: true
{{- end -}}

我们依然还是可以用 * 来代替空格进行分析,如下所示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  drink: {{ .Values.favorite.drink | default "tea" | quote }}
  food: {{ .Values.favorite.food | upper | quote }}*
**{{- if eq .Values.favorite.drink "coffee" -}}*
  mug: true*
**{{- end -}}

第一个 {{- 会删除前面的空格和前面的换行符,然后后面的 -}} 会删除当前行的换行符,这样就会把 mug: true 移动到 food: "PIZZA" 后面去了,最终渲染过后就会变成:food: "PIZZA"mug: true,因为在两侧都去掉换行符。

有关模板中空格控制的详细信息,可以查看 Go 模板官方文档介绍。

也可以使用indent 函数

4.3 使用 with 修改作用域

接下来需要了解的是 with 操作,它可以控制变量的作用域,然后重新用 . 调用就表示对当前作用域的引用,所以,.Values 是告诉模板引擎在当前作用域下内查找 Values 对象。

with 语句的语法和 if 语句比较类似:

{{ with PIPELINE }}
  # 限制范围
{{ end }}

范围可以更改,可以让你将当前范围 . 设置为特定的对象,例如,我们一直在使用 .Values.favorites,让我们重写下模板文件 ConfigMap 来更改 . 的范围指向 .Values.favorites

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- with .Values.favorite }}
  drink: {{ .drink | default "tea" | quote }}
  food: {{ .food | upper | quote }}
  {{- end }}

我们这里将前面练习的 if 条件语句删除了,在模板中我们添加了一个 {{- with .Values.favorite }} 的语句,意思就是说在 with 语句的作用范围内可以用 . 表示 .Values.favorite 了,所以我们可以引用 .drink.food 了,但是在 {{ end }} 之后就会重置为之前的作用域了。

不过需要注意得是,在受限的作用域范围内,你无法从父级范围访问到其他对象,比如,下面得模板会失败:

{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
release: {{ .Release.Name }}
{{- end }}

因为 Release.Name 并不在 . 的限制范围内,所以会产生错误,但是,如果我们交换最后两行,则就可以正常工作了,因为 {{ end }} 之后会重置作用域。

{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
{{- end }}
release: {{ .Release.Name }}

下面我先来了解下 range,然后我们再去了解下模板变量,它可以为上面得这个范围问题提供一种解决方案。

4.4 range 循环操作

我们知道许多编程语言都支持使用 for 循环、foreach 循环或者类似功能机制进行循环迭代,在 Helm 得模板语言中,迭代集合得方法是使用 range 运算符。

比如首先我们在 values.yaml 文件中添加一份 pizza 馅料列表:

favorite:
  drink: coffee
  food: pizza
pizzaToppings:
  - mushrooms
  - cheese
  - peppers
  - onions

现在我们有了 pizzaToppings 列表(在模板中称为切片),我们可以来修改下模板将列表打印到 ConfigMap 中:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- with .Values.favorite }}
  drink: {{ .drink | default "tea" | quote }}
  food: {{ .food | upper | quote }}
  {{- end }}
  toppings: |-
    {{- range .Values.pizzaToppings }}
    - {{ . | title | quote }}
    {{- end }}

我们仔细观察下模板中的 toppings: 列表,range 函数将遍历 Values.pizzaToppings 列表,我们看到里面使用了一个 .,类似于上面我们用 with 设置范围一样,运算符也是这样的,每次循环,. 都会被设置为当前的 pizzaTopping,也就是说第一次设置为mushrooms,第二次迭代设置为cheese,依次类推。

我们可以直接传递 . 这个值到管道上,所以我们这里 {{ . | title | quote }} 就相当于发送 .title(标题大小写函数)函数,然后发送给 quote 函数,我们渲染这个模板,会输出如下的内容:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575975849-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  toppings: |-
    - "Mushrooms"
    - "Cheese"
    - "Peppers"
    - "Onions"

在上面模板中,我们做了一些小小的特殊处理,toppings: |- 行表示声明一个多行字符串,所以其实我们的 toppings 列表不是一个 YAML 列表,而是一个比较大的字符串,这是因为 ConfigMap 中的数据由 key/value 对组成,所有 key 和 value 都是简单的字符串,要了解为什么是这样的,可以查看 Kubernetes ConfigMap 文档,不过这个细节对我们这里不重要。

有时候,在模板中快速创建一个列表,然后遍历该列表很有用,Helm 模板具有简化该功能的函数:tuple。元组是固定大小的列表集合,但是具有任意数据类型,下面是元组的大概使用方法:

sizes: |-
  {{- range tuple "small" "medium" "large" }}
  - {{ . }}
  {{- end }}

上面的模板最终会被渲染成如下的 YAML:

sizes: |-
  - small
  - medium
  - large

5. 变量

有了函数、管道、对象以及控制结构,我们可以想象下大多数编程语言中更基本的思想之一:变量。在模板中,变量的使用频率较低,但是,我们还是可以使用他们来简化代码,以及更好地使用 withrange

在前面的示例中,我们知道下面的模板渲染会出错:

{{- with .Values.favorite }}
drink: {{ .drink | default "tea" | quote }}
food: {{ .food | upper | quote }}
release: {{ .Release.Name }}
{{- end }}

因为 Release.Name 不在 with 语句块限制的范围之内,解决作用域问题的一种方法是将对象分配给在不考虑当前作用域情况下访问的变量。

在 Helm 模板中,变量是对另外一个对象的命名引用。它遵循 $name 格式,变量使用特殊的赋值运算符进行赋值 :=,我们可以修改上面的模板,为 Release.Name 声明一个变量:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- $relname := .Release.Name -}}
  {{- with .Values.favorite }}
  drink: {{ .drink | default "tea" | quote }}
  food: {{ .food | upper | quote }}
  release: {{ $relname }}
  {{- end }}

注意在 with 语句之前,我们先分配了 $relname := .Release.Name,然后在 with 语句块中,$relname 变量仍然表示 release 的名称,我们渲染该模板,可以得到如下的正确结果:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575982655-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "PIZZA"
  release: mychart-1575982655

变量在 range 循环里面非常有用,它们可以用于类似于列表的对象来捕获索引和 value 值:

toppings: |-
  {{- range $index, $topping := .Values.pizzaToppings }}
    {{ $index }}: {{ $topping }}
  {{- end }}

注意 range 在前面,然后是变量,然后是赋值运算符,然后才是列表,这会将整数索引(从0开始)分配给 $index,并将 value 值分配给 $topping,上面的内容会被渲染成如下内容:

toppings: |-
  0: mushrooms
  1: cheese
  2: peppers
  3: onions

对于同时具有 key 和 value 的数据结构,我们也可以使用 range 来获得 key、value 的值,比如,我们可以像这样循环遍历 .Values.favorite

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}

在第一次迭代中,$keydrink$valcoffee,在第二次迭代中,$keyfood$valpizza。运行上面的命令将生成下面的内容:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1575983119-configmap
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"

一般来说变量不是全局的,它们的作用域是声明它们的块区域,之前,我们在模板的顶层分配了 $relname,该变量将在整个模板的范围内,但是在我们上面的示例中,$key$val 作用域只在 {{ range... }}{{ end }} 区域内。

但是,有一个始终是全局变量的 始终指向顶层根上下文,当我们在 range 循环内需要知道 chart 包的 release 名称的时候,该功能就非常有用了,比如下面的模板文件:

{{- range .Values.tlsSecrets }}
apiVersion: v1
kind: Secret
metadata:
  name: {{ .name }}
  labels:
    # helm 模板经常使用 `.`,但是这里是无效的,用 `$` 是可以生效的。
    app.kubernetes.io/name: {{ template "fullname" $ }}
    # 这里不能引用 `.Chart.Name`,但是可用使用 `$.Chart.Name`
    helm.sh/chart: "{{ $.Chart.Name }}-{{ $.Chart.Version }}"
    app.kubernetes.io/instance: "{{ $.Release.Name }}"
    # 值来自于 Chart.yaml 文件中的 appVersion
    app.kubernetes.io/version: "{{ $.Chart.AppVersion }}"
    app.kubernetes.io/managed-by: "{{ $.Release.Service }}"
type: kubernetes.io/tls
data:
  tls.crt: {{ .certificate }}
  tls.key: {{ .key }}
---
{{- end }}

到现在为止,我们只研究了在一个文件中声明的一个模板,但是,Helm 模板语言的强大功能之一是它能够声明多个模板并将其一起使用。

6. 命名模板

前面我们都是只操作的一个模板,现在我们来尝试使用多个模板文件。在本节中,我们可以了解到如何在一个文件中定义命名模板,然后在其他地方使用它们。命名模板(有时也叫子模板)只是在文件内部定义的有名称的模板。主要有两种创建方式以及几种不同的使用方式。

当使用命名模板的时候有几个重要细节:模板名称是全局的,如果声明两个具有相同名称的模板,则会使用最后被加载的模板。由于子 chart 中的模板是与顶级模板一起编译的,所以需要谨慎命名。

一种流行的命名约定是在每个定义的模板前添加 chart 名称:{{ define "mychart.labels" }},通过使用特定的 chart 名作为前缀,我们可以避免由于两个不同的 chart 实现了相同名称的模板而引起的冲突。

6.1 partials 和 _ 文件

到目前为止,我们只使用了一个模板文件,但是 Helm 的模板语言允许我们创建命名的嵌入式模板,可以在其他位置进行访问。在编写这些模板之前,有一些值得一提的命名约定:

  • templates/ 中的大多数文件都被视为 Kubernetes 资源清单文件(NOTES.txt 除外)

  • _ 开头命名的文件也不会被当做 Kubernetes 资源清单文件

  • 下划线开头的文件不会被当做资源清单之外,还可以被其他 chart 模板调用

_ 开头的这些文件其实就是 Helm 中的 partials 文件,所以其实我们完全可以将命名模板定义在这些 partials 文件中,默认就是 _helpers.tpl 文件,其实在前面我们创建的 mychart 包中也可以找到这个文件。

6.2 define和 template

define 关键字可以让我们在模板文件中创建命名模板,它的语法如下所示:

{{ define "MY.NAME" }}
  # 模板内容区域
{{ end }}

比如我们可以定义一个模板来封装下 Kubernetes 的 labels 标签:

{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}

现在我们可以将该模板嵌入到前面的 ConfigMap 模板中,然后将其包含在模板中:

{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  {{- template "mychart.labels" }}
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}

当模板引擎读取这个文件的时候,它会存储 mychart.labels 的引用,直到该模板被调用,然后会内联渲染该模板。我们渲染这个模板可以都到如下所示的结果(记得先删掉默认生成的 _helpers.tpl 文件):

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576034036-configmap
  labels:
    generator: helm
    date: 2022-9-11
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"

一般来说,Helm 中约定将这些模板统一放到一个 partials 文件中,通常就是 _helpers.tpl 文件中,我们将上面的命名模板移动到该文件(templates/_helpers.tpl)中去:

{{/* 生成基本的 Label 标签 */}}
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
{{- end }}

一般来说,我们也会用一个简单的块({{/*...*/}})来注释这个命名模板的作用。

现在虽然我们把命名模板放到了 _helpers.tpl 文件中,但是我们在 configmap.yaml 模板中还是可以访问,因为命名模板是全局的:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  {{- template "mychart.labels" }}
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}

因为上面我们提到过命名模板是全局的,我们可以再渲染下上面的模板可以得到正确的结果。

6.3 设置模板范围

上面我们定义的模板中,还没有使用到任何对象,只使用了函数,现在我们来修改下定义的命名模板,包含 chart 的名称和版本:

{{/* 生成基本的 Label 标签 */}}
{{- define "mychart.labels" }}
  labels:
    generator: helm
    date: {{ now | htmlDate }}
    chart: {{ .Chart.Name }}
    version: {{ .Chart.Version }}
{{- end }}

现在我们来渲染下模板,会出现下面的错误:

$ helm install --generate-name --dry-run --debug ./mychart

我们可以看到提示 labels.chartnil,这是因为我们使用的 .Chart.Name 不在定义的这个模板的作用域范围内,当渲染命名模板(使用 define 定义)的时候,它将接收模板调用传递的作用域。在我们这个示例中,我们是这样引用这个模板的:

{{- template "mychart.labels" }}

没有传入任何作用域,所以在模板内我们无法访问 . 中的任何内容,当然要解决很简单,我们只需要把作用域范围传递给模板即可:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  {{- template "mychart.labels" . }}
......

我们这里在使用 template 调用模板的时候传递了 .,我们可以很容易传递 .Values 或者 .Values.favorite 或者我们想要的任何范围,但是这里我们想要的是顶级作用域,所以我们传递的是 .

现在我们再来重新渲染我们的模板,可以得到如下所示的结果:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576035668-configmap
  labels:
    generator: helm
    date: 2022-9-11
    chart: mychart
    version: 0.1.0
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"

现在 {{ .Chart.Name }} 解析为了 mychart,而 {{ .Chart.Version }} 解析为了 0.1.0

6.4 include 函数

假设我们定义了一个如下所示的简单模板:

{{- define "mychart.app" -}}
app_name: {{ .Chart.Name }}
app_version: "{{ .Chart.Version }}"
{{- end -}}

现在我们想把上面的内容插入到模板的 labels 部分,在 data 部分也想要这个内容:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  labels:
    {{ template "mychart.app" . }}
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}
{{ template "mychart.app" . }}

但是我们直接渲染上面的模板还是会有错误:

$ helm install --generate-name --dry-run --debug ./mychart

Error: unable to build kubernetes objects from release manifest: error validati
ng "": error validating data: [ValidationError(ConfigMap): unknown field "app_n
ame" in io.k8s.api.core.v1.ConfigMap, ValidationError(ConfigMap): unknown field
 "app_version" in io.k8s.api.core.v1.ConfigMap]
helm.go:76: [debug] error validating "": error validating data: [ValidationErro
r(ConfigMap): unknown field "app_name" in io.k8s.api.core.v1.ConfigMap, Validat
ionError(ConfigMap): unknown field "app_version" in io.k8s.api.core.v1.ConfigMap]
......

因为 template 只是一个动作,而不是一个函数,所以无法将模板调用的输出传递给其他函数,只是内联插入,相当于渲染的结果是这样的:

apiVersion: v1
kind: ConfigMap
metadata:
  name: measly-whippet-configmap
  labels:
    app_name: mychart
app_version: "0.1.0+1478129847"
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"
  app_name: mychart
app_version: "0.1.0+1478129847"

很明显上面的 YAML 文件是不符合 ConfigMap 资源对象的格式要求的,所以报错了。为解决这个问题,Helm 提供了代替 template 的函数 include,可以将模板的内容导入到当前的管道中,这样就可以在管道中传递给其他函数进行处理了,如下所示:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
  labels:
{{ include "mychart.app" . | indent 4 }}
data:
  myvalue: "Hello World"
  {{- range $key, $val := .Values.favorite }}
  {{ $key }}: {{ $val | quote }}
  {{- end }}
{{ include "mychart.app" . | indent 2 }}

现在我们重新渲染就可以得到正确的结果了,这是因为我们用 include 函数得到模板内容后通过管道传给了后面的 indent 函数来保证了缩进:

 Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576036671-configmap
  labels:
    app_name: mychart
    app_version: "0.1.0"
data:
  myvalue: "Hello World"
  drink: "coffee"
  food: "pizza"
  app_name: mychart
  app_version: "0.1.0"

在 Helm 模板中最好使用 include 而不是 template,这样可以更好地处理 YAML 文档的输出格式。

7. 访问文件

在上一节中我们介绍了几种创建和访问命名模板的方法,这使得从另一个模板中导入一个模板变得很容易,但是有时候需要导入一个不是模板的文件并注入其内容,而不通过模板渲染器获得内容。

Helm 提供了一个 .Files 对象对文件的访问,但是在模板中使用这个对象之前,还有几个需要注意的事项值得一提:

  • 可以在 Helm chart 中添加额外的文件,这些文件也会被打包,不过需要注意,由于 Kubernetes 对象的存储限制,Charts 必须小于 1M

  • 由于一些安全原因,通过 .Files 对象无法访问某些文件

    • 无法访问 templates/ 下面的文件

    • 无法访问使用 .helmignore 排除的文件

  • Chart 不会保留 UNIX 模式的信息,所以,当使用 .Files 对象时,文件级别的权限不会对文件的可用性产生影响。

7.1 基本示例

现在我们来编写一个模板,将3个文件读入到 ConfigMap 模板中,首先我们在 chart 中添加3个文件,将3个文件都直接放置在 mychart/ 目录中。

config1.toml:

message = Hello from config 1

config2.toml:

message = This is config 2

config3.toml:

message = Goodbye from config 3

3个文件都是简单的 TOML 文件,我们知道这些文件的名称,所以我们可以使用 range 函数来遍历它们,并将其内容注入到 ConfigMap 中去。

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  {{- $files := .Files }}
  {{- range tuple "config1.toml" "config2.toml" "config3.toml" }}
  {{ . }}: |-
    {{ $files.Get . }}
  {{- end }}

这里我们声明了一个 $files 的变量来保存 .Files 对象的引用,还使用了 tuple 函数来循环文件列表,然后我们打印每个文件夹 {{ . }}: |-,后面使用 {{ $files.Get . }} 获取文件内容。

现在我们渲染这个模板会产生包含3个文件内容的单个 ConfigMap:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576046462-configmap
data:
  config1.toml: |-
    message = Hello from config 1

  config2.toml: |-
    message = This is config 2

  config3.toml: |-
    message = Goodbye from config 3

另外在处理文件的时候,对文件路径本身执行一些标准操作可能非常有用,为了解决这个问题,Helm 从 Go 的路径包中导入了许多功能供你使用,它们都可以使用与 Go 包中相同的相同名称来访问,但是首字母需要小写,比如 Base 需要变成 base,导入的函数有:- Base - Dir - Ext - IsAbs - Clean。

7.2 Glob 模式

随着 chart 的增长,你可能需要更多地组织文件,因此 Helm 提供了 Files.Glob 的方法来帮助我们获取具有 glob 模式的文件。

.Glob 返回 Files 类型,所以你可以在返回的对象上调用任何 Files 方法。比如,我们的文件目录结构如下所示:

foo/:
  foo.txt foo.yaml

bar/:
  bar.go bar.conf bar.yaml

我们可以用 Glob 进行多种选择:

{{$root := .}}
{{range $path, $bytes := .Files.Glob "**.yaml"}}
{{$path}}: |-
{{$root.Files.Get $path}}
{{end}}

或者

{{$root := .}}
{{range $path, $bytes := .Files.Glob "foo/*"}}
{{$path}}: '{{ $root.Files.Get $path | b64enc }}'
{{end}}

7.3 ConfigMap 和 Secrets

想要将文件内容同时放入 ConfigMap 和 Secrets 中,以便在运行时安装到 Pod 中,这种需求很常见,为了解决这个问题,Helm 在 Files 类型上添加了一个实用的方法。

根据上面的目录结构,我们可以按照如下的方式进行处理:

apiVersion: v1
kind: ConfigMap
metadata:
  name: conf
data:
{{ (.Files.Glob "foo/*").AsConfig | indent 2 }}
---
apiVersion: v1
kind: Secret
metadata:
  name: very-secret
type: Opaque
data:
{{ (.Files.Glob "bar/*").AsSecrets | indent 2 }}

7.4 编码

我们也可以导入一个文件并用 base64 编码进行编码:

apiVersion: v1
kind: Secret
metadata:
  name: {{ .Release.Name }}-secret
type: Opaque
data:
  token: |-
    {{ .Files.Get "config1.toml" | b64enc }}

上面将采用我们上面的 config1.toml 文件并对其内容进行 base64 编码,渲染会得到如下所示的结果:

# Source: mychart1/templates/secert.yaml
apiVersion: v1
kind: Secret
metadata:
  name: mychart1-1663578612-secret
type: Opaque
data:
  token: |-
    bWVzc2FnZSA9IEhlbGxvIGZyb20gY29uZmlnIDEK

7.5 Lines

有时,需要访问模板中文件的每一行内容,Helm 也提供了方法的 Lines 方法,我们可以使用 range 函数遍历每行内容:

data:
  some-file.txt: {{ range .Files.Lines "foo/bar.txt" }}
    {{ . }}{{ end }}

在 Helm 安装的时候无法将文件传递到 chart 外部,所以,如果你要求用户提供数据的话,则必须使用 helm install -f 或者 helm install --set 来获取。

8. NOTES.txt 文件

在本节中我们将来了解为 chart 用户提供说明的一个 NOTES.txt 文件,在 chart 安装或者升级结束时,Helm 可以为用户打印出一些有用的信息,使用模板也可以自定义这些信息。

要将安装说明添加到 chart 中,只需要创建一个 templates/NOTES.txt 文件,该文件纯文本的,但是可以像模板一样进行处理,并具有所有常规模板的功能和可用对象。

现在让我们来创建一个简单的 NOTES.txt 文件:

Thank you for installing {{ .Chart.Name }}.

Your release is named {{ .Release.Name }}.

To learn more about the release, try:

  $ helm status {{ .Release.Name }}
  $ helm get {{ .Release.Name }}

现在我们运行 helm install ./mychart,我们就可以在底部看到这样的消息:


NOTES:
Thank you for installing mychart.

Your release is named rude-cardinal.

To learn more about the release, try:

  $ helm status rude-cardinal
  $ helm get rude-cardinal

用这种方式可以向用户提供一个有关如何使用其新安装的 chart 的详细信息,强烈建议创建 NOTES.txt 文件,虽然这不是必须的。

9. Subcharts 和 Global Values

到现在为止,我们从单一模板,到多个模板文件,但是都仅仅是处理的一个 chart 包,但是 charts 可能具有一些依赖项,我们称为 subcharts(子 chart),接下来我们将创建一个子 chart。

同样在深入了解之前,我们需要了解下子 chart 相关的一些信息。

  • 子 chart 是独立的,这意味着子 chart 不能显示依赖其父 chart

  • 所以子 chart 无法访问其父级的值

  • 父 chart 可以覆盖子 chart 的值

  • Helm 中有可以被所有 charts 访问的全局值的概念

9.1 创建子chart

同样还是在之前操作的 mychart/ 这个 chart 包中,我们来尝试添加一些新的子 chart:

$ cd mychart/charts
$ helm create mysubchart
Creating mysubchart
$ rm -rf mysubchart/templates/*.*

我们删除了所有的基本模板,这样我们可以从头开始。

9.2 添加 values 和 模板

接下来我们为 mysubchart 这个子 chart 创建一个简单的模板和 values 值文件,mychart/charts/mysubchart 中已经有一个 values.yaml 文件了,在文件中添加下面的 values:

dessert: cake

下面我们再创建一个新的 ConfigMap 模板 mychart/charts/mysubchart/templates/configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-cfgmap2
data:
  dessert: {{ .Values.dessert }}

因为每个子 chart 都是独立的 chart,所以我们可以单独测试 mysubchart

[root@master helm]# helm install --generate-name --dry-run --debug mychart1/charts/subchart  
install.go:178: [debug] Original chart version: ""
install.go:195: [debug] CHART PATH: /mnt/k8s/helm/mychart1/charts/subchart

NAME: subchart-1663581452
LAST DEPLOYED: Mon Sep 19 17:57:33 2022
NAMESPACE: default
STATUS: pending-install
REVISION: 1
TEST SUITE: None
USER-SUPPLIED VALUES:
{}

COMPUTED VALUES:
affinity: {}
autoscaling:
  enabled: false
  maxReplicas: 100
  minReplicas: 1
  targetCPUUtilizationPercentage: 80
dessert: cake
fullnameOverride: ""
image:
  pullPolicy: IfNotPresent
  repository: nginx
  tag: ""
imagePullSecrets: []
ingress:
  annotations: {}
  className: ""
  enabled: false
  hosts:
  - host: chart-example.local
    paths:
    - path: /
      pathType: ImplementationSpecific
  tls: []
nameOverride: ""
nodeSelector: {}
podAnnotations: {}
podSecurityContext: {}
replicaCount: 1
resources: {}
securityContext: {}
service:
  port: 80
  type: ClusterIP
serviceAccount:
  annotations: {}
  create: true
  name: ""
tolerations: []

HOOKS:
MANIFEST:
---
# Source: subchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: subchart-1663581452-cfgmap2
data:
  dessert: cake

9.3 从父 chart 覆盖 values

我们原来的 chart - mychart 现在是 mysubchart 的父级 chart 了。由于 mychart 是父级,所以我们可以在 mychart 中指定配置,并将该配置发送到 mysubchart 中去,比如,我们可以这样修改 mychart/values.yaml

favorite:
  drink: coffee
  food: pizza
pizzaToppings:
  - mushrooms
  - cheese
  - peppers
  - onions

mysubchart:
  dessert: ice cream

最后两行,mysubchart 部分中的所有指令都回被发送到 mysubchart 子 chart 中,所以,如果我们现在渲染模板,我们可以看到 mysubchart 的 ConfigMap 会被渲染成如下的内容:

# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576051914-cfgmap2
data:
  dessert: ice cream

我们可以看到顶层的 values 值覆盖了子 chart 中的值。这里有一个细节需要注意,我们没有将 mychart/charts/mysubchart/templates/configmap.yaml 模板更改为指向 .Values.mysubchart.dessert,因为从该模板的角度来看,该值仍然位于 .Values.dessert,当模板引擎传递 values 值的时候,它会设置这个作用域,所以,对于 mysubchart 模板,.Values 中仅仅提供用于该子 chart 的值。

但是有时候如果我们确实希望某些值可以用于所有模板,这个时候就可以使用全局 chart values 值来完成了。

9.4 全局值

全局值是可以从任何 chart 或子 chart 中都可以访问的值,全局值需要显示的声明,不能将现有的非全局对象当作全局对象使用。

Values 数据类型具有一个名为 Values.global 的保留部分,可以在其中设置全局值,我们在 mychart/values.yaml 文件中添加一个全局值:

favorite:
  drink: coffee
  food: pizza
pizzaToppings:
  - mushrooms
  - cheese
  - peppers
  - onions

mysubchart:
  dessert: ice cream
global:
  salad: caesar

由于全局值的原因,在 mychart/templates/configmap.yamlmysubchart/templates/configmap.yaml 下面都应该可以以 {{ .Values.global.salad }} 的形式来访问这个值。

mychart/templates/configmap.yaml

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  salad: {{ .Values.global.salad }}

mysubchart/templates/configmap.yaml:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-cfgmap2
data:
  dessert: {{ .Values.dessert }}
  salad: {{ .Values.global.salad }}

然后我们渲染这个模板,可以得到如下所示的内容:

---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576053485-cfgmap2
data:
  dessert: ice cream
  salad: caesar
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mychart-1576053485-configmap
data:
  salad: caesar

全局值对于传递这样的数据比较有用。

9.5 共享模板

父级 chart 和子 chart 可以共享模板,任何 chart 中已定义的块都可以用于其他 chart。比如,我们可以定义一个简单的模板,如下所示:

{{- define "labels" }}from: mychart{{ end }}

前面我们提到过可以使用在模板中使用 includetemplate,但是使用 include 的一个优点是可以动态引入模板的内容:

{{ include $mytemplate }}

10. 模板调试

调试模板可能比较麻烦,因为渲染的模板会发送到 Kubernetes API server,而 API server 可能会因为格式以外的一些原因而拒绝 YAML 文件。

下面这些命令可以帮助你调试一些问题:

  • helm lint 是验证 chart 是否遵循最佳实践的首选工具

  • helm install --dry-run --debug 或者 helm template --debug:前面我们已经使用了这个技巧,这个是让服务器渲染模板,然后返回生成的资源清单文件的好方法,而且不会真正的去安装这些资源

  • helm get manifest:这是查看服务器上安装了哪些模板的好方法

当你的 YAML 文件无法解析的时候,但你想要查看生成的内容的时候,检索 YAML 的一种简单方法是注释掉模板中的问题部分,然后重新运行 helm install --dry-run --debug

apiVersion: v2
# some: problem section
# {{ .Values.foo | quote }}

上面的内容将呈现并返回完整的注释:

apiVersion: v2
# some: problem section
#  "bar"

这提供了一种查看生成的内容的快速方法。


k8s(16)-- 模板开发
http://47.123.5.226:8090//archives/k8s-16----mo-ban-kai-fa
作者
pony
发布于
2024年04月28日
许可协议