helm charts 模板编程

模板函数与管道

模板函数

比如我们需要从.Values中读取的值变成字符串的时候就可以通过调用quote模板函数来实现:(templates/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  k8s: {
    
    {
    
     quote .Values.course.k8s }}
  python: {
    
    {
    
     .Values.course.python }}

模板函数遵循调用的语法为:functionName arg1 arg2…。在上面的模板文件中,quote .Values.course.k8s调用quote函数并将后面的值作为一个参数传递给它。最终被渲染为:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '39405'
......
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: masked-saola-configmap
data:
  myvalue: "Hello World"
  k8s: "devops"
  python: django

我们可以看到.Values.course.k8s被渲染成了字符串devops。如果需要双引号",则需要添加\来进行转义,而squote函数的用途则是用双引号将字符串括起来,而不会对内容进行转义。


辅助模板

有时你想在图表中创建一些可重复使用的部分,无论它们是块还是模板部分。通常,将它们保存在自己的文件中会更干净。

在templates/目录中,任何以下划线 ( _) 开头的文件都不会输出 Kubernetes 清单文件。所以按照惯例,辅助模板和部分被放置在一个_helpers.tpl文件中。


管道

比如我们用管道来重写上面的 ConfigMap 模板:(templates/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  k8s: {
    
    {
    
     .Values.course.k8s | quote }}
  python: {
    
    {
    
     .Values.course.python }}

使用管道我们可以将几个功能顺序的连接在一起,比如我们希望上面的 ConfigMap 模板中的 k8s 的 value 值被渲染后是大写的字符串,则我们就可以使用管道来修改:(templates/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  k8s: {
    
    {
    
     .Values.course.k8s | upper | quote }}
  python: {
    
    {
    
     .Values.course.python }}

然后我们用debug模式来查看下上面的模板最终会被渲染成什么样子:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '46651'
......
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: maudlin-labradoodle-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: django

我们可以看到之前我们的 devops 已经被渲染成了 “DEVOPS” 了,要注意的是使用管道操作的时候,前面的操作结果会作为参数传递给后面的模板函数,比如我们这里希望将上面模板中的 python 的值渲染为重复出现3次的字符串,则我们就可以使用到 repeat 函数,不过该函数需要传入一个参数repeat COUNT STRING表示重复的次数:(templates/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  k8s: {
    
    {
    
     .Values.course.k8s | upper | quote }}
  python: {
    
    {
    
     .Values.course.python | quote | repeat 3 }}

该repeat函数会将给定的字符串重复3次返回,所以我们将得到这个输出:

helm install --dry-run --debug .
[debug] Created tunnel using local port: '39712'

......

Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 7: did not find expected key

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: piquant-butterfly-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: "django""django""django"

我们可以看到上面的输出中 python 对应的值变成了3个相同的字符串,这显然是不符合我们预期的,我们的预期是形成一个字符串,而现在是3个字符串了,而且上面还有错误信息,根据管道处理的顺序,我们将quote函数放到repeat函数后面去是不是就可以解决这个问题了:(templates/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  k8s: {
    
    {
    
     .Values.course.k8s | upper | quote }}
  python: {
    
    {
    
     .Values.course.python | repeat 3 | quote }}

现在是不是就是先重复3次.Values.course.python的值,然后调用quote函数:

helm install --dry-run --debug .
[debug] Created tunnel using local port: '33837'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: braided-manatee-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: "djangodjangodjango"

现在是不是就正常了,也得到了我们的预期结果,所以我们在使用管道操作的时候一定要注意是按照从前到后一步一步顺序处理的。


default 函数

另外一个我们会经常使用的一个函数是default 函数:default DEFAULT_VALUE GIVEN_VALUE。该函数允许我们在模板内部指定默认值,以防止该值被忽略掉了。比如我们来修改上面的 ConfigMap 的模板:(templates/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: {
    
    {
    
     .Values.hello | default  "Hello World" | quote }}
  k8s: {
    
    {
    
     .Values.course.k8s | upper | quote }}
  python: {
    
    {
    
     .Values.course.python | repeat 5 | quote }}

由于我们的values.yaml文件中只定义了 course 结构的信息,并没有定义 hello 的值,所以如果没有设置默认值的话是得不到{ { .Values.hello }}的值的,这里我们为该值定义了一个默认值:Hello World,所以现在如果在values.yaml文件中没有定义这个值,则我们也可以得到默认值:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '42670'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: orbiting-hog-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: "djangodjangodjangodjangodjango"

lookup 函数

该 lookup 函数可用于查找正在运行的集群中的资源。查找函数的概要是

lookup apiVersion, kind, namespace, name -> resource or resource list
parameter apiVersion kind namespace name
type string string string string

name和都是namespace可选的,可以作为空字符串 ( “”) 传递。

以下参数组合是可能的:

行为 查找功能
Kubectl Get Pod Mypod -N Mynamespace Lookup “v1” “Pod” “mynamespace” “mypod”
Kubectl Get Pods -N Mynamespace Lookup “v1” “Pod” “mynamespace” “”
Kubectl Get Pods —all-Namespaces Lookup “v1” “Pod” “” “”
Kubectl Get Namespace Mynamespace Lookup “v1” “Namespace” “” “mynamespace”
Kubectl Get Namespaces Lookup “v1” “Namespace” “” “”

当lookup返回一个对象时,它会返回一个字典。可以进一步导航此字典以提取特定值。

以下示例将返回mynamespace对象的注释:

(lookup "v1" "Namespace" "" "mynamespace").metadata.annotations

返回对象列表时lookup,可以通过以下items字段访问对象列表:

{
    
    {
    
     range $index, $service := (lookup "v1" "Service" "mynamespace" "").items }}
    {
    
    {
    
    /* do something with each service */}}
{
    
    {
    
     end }}

当没有找到对象时,返回一个空值。这可用于检查对象是否存在。

该lookup函数使用 Helm 现有的 Kubernetes 连接配置来查询 Kubernetes。如果与调用 API 服务器交互时返回任何错误(例如由于缺少访问资源的权限),则 helm 的模板处理将失败。


控制流程:判断、循环

模板函数和管道是通过转换信息并将其插入到YAML文件中的强大方法。但有时候需要添加一些比插入字符串更复杂一些的模板逻辑。这就需要使用到模板语言中提供的控制结构了。

控制流程为我们提供了控制模板生成流程的一种能力,Helm 的模板语言提供了以下几种流程控制:

if/else 条件块
with 指定范围
range 循环块

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

define在模板中声明一个新的命名模板
template导入一个命名模板
block声明了一种特殊的可填写的模板区域

if/else 条件

if/else块是用于在模板中有条件地包含文本块的方法,条件块的基本结构如下:

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

当然要使用条件块就得判断条件是否为真,如果值为下面的几种情况,则管道的结果为 false:

一个布尔类型的假
一个数字零
一个空的字符串
一个nil(空或null)
一个空的集合(map、slice、tuple、dict、array)

除了上面的这些情况外,其他所有条件都为真。

同样还是以上面的 ConfigMap 模板文件为例,添加一个简单的条件判断,如果 python 被设置为 django,则添加一个web: true:(tempaltes/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: {
    
    {
    
     .Values.hello | default  "Hello World" | quote }}
  k8s: {
    
    {
    
     .Values.course.k8s | upper | quote }}
  python: {
    
    {
    
     .Values.course.python | repeat 3 | quote }}
  {
    
    {
    
     if eq .Values.course.python "django" }}web: true{
    
    {
    
     end }}

在上面的模板文件中我们增加了一个条件语句判断{ { if eq .Values.course.python “django” }}web: true{ { end }},其中运算符eq是判断是否相等的操作,除此之外,还有ne、lt、gt、and、or等运算符都是 Helm 模板已经实现了的,直接使用即可。这里我们{ { .Values.course.python }}的值在values.yaml文件中默认被设置为了django,所以正常来说下面的条件语句判断为真,所以模板文件最终被渲染后会有web: true这样的的一个条目:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '40143'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: fallacious-prawn-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: "djangodjangodjangodjangodjango"
  web: true

可以看到上面模板被渲染后出现了web: true的条目,如果我们在安装的时候覆盖下 python 的值呢,比如我们改成 ai:

helm install --dry-run --debug --set course.python=ai .
[debug] Created tunnel using local port: '42802'

......
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dull-mite-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: "aiaiai"

根据我们模板文件中的定义,如果{ { .Values.course.python }}的值为django的话就会新增web: true这样的一个条目,但是现在我们是不是通过参数–set将值设置为了 ai,所以这里条件判断为假,正常来说就不应该出现这个条目了,上面我们通过 debug 模式查看最终被渲染的值也没有出现这个条目,证明条件判断是正确的。


空格控制

上面我们的条件判断语句是在一整行中的,如果平时经常写代码的同学可能非常不习惯了,我们一般会将其格式化为更容易阅读的形式,比如:

{
    
    {
    
     if eq .Values.course.python "django" }}
web: true
{
    
    {
    
     end }}

这样的话看上去比之前要清晰很多了,但是我们通过模板引擎来渲染一下,会得到如下结果:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '44537'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: bald-narwhal-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: "djangodjangodjango"

  web: true

我们可以看到渲染出来会有多余的空行,这是因为当模板引擎运行时,它将一些值渲染过后,之前的指令被删除,但它之前所占的位置完全按原样保留剩余的空白了,所以就出现了多余的空行。YAML文件中的空格是非常严格的,所以对于空格的管理非常重要,一不小心就会导致你的YAML文件格式错误。

我们可以通过使用在模板标识{ { 后面添加破折号和空格{ {-来表示将空白左移,而在}}前面添加一个空格和破折号-}}表示应该删除右边的空格,另外需要注意的是换行符也是空格!

使用这个语法,我们来修改我们上面的模板文件去掉多余的空格:(templates/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: {
    
    {
    
     .Values.hello | default  "Hello World" | quote }}
  k8s: {
    
    {
    
     .Values.course.k8s | upper | quote }}
  python: {
    
    {
    
     .Values.course.python | repeat 3 | quote }}
  {
    
    {
    
    - if eq .Values.course.python "django" }}
  web: true
  {
    
    {
    
    - end }}

现在我们来查看上面模板渲染过后的样子:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '34702'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: mangy-olm-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: "djangodjangodjango"
  web: true

现在是不是没有多余的空格了,另外我们需要谨慎使用-}},比如上面模板文件中:

python: {
    
    {
    
     .Values.course.python | repeat 3 | quote }}
{
    
    {
    
    - if eq .Values.course.python "django" -}}
web: true
{
    
    {
    
    - end }}

如果我们在if条件后面增加-}},这会渲染成:

python: "djangodjangodjango"web: true

因为-}}它删除了双方的换行符,显然这是不正确的。


使用 with 修改范围

接下来我们来看下with关键词的使用,它用来控制变量作用域。还记得之前我们的{ { .Release.xxx }}或者{ { .Values.xxx }}吗?其中的.就是表示对当前范围的引用,.Values就是告诉模板在当前范围中查找Values对象的值。而with语句就可以来控制变量的作用域范围,其语法和一个简单的if语句比较类似:

{
    
    {
    
     with PIPELINE }}
  #  restricted scope
{
    
    {
    
     end }}

with语句可以允许将当前范围.设置为特定的对象,比如我们前面一直使用的.Values.course,我们可以使用with来将.范围指向.Values.course:(templates/configmap.yaml)

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: {
    
    {
    
     .Values.hello | default  "Hello World" | quote }}
  {
    
    {
    
    - with .Values.course }}
  k8s: {
    
    {
    
     .k8s | upper | quote }}
  python: {
    
    {
    
     .python | repeat 3 | quote }}
  {
    
    {
    
    - if eq .python "django" }}
  web: true
  {
    
    {
    
    - end }}
  {
    
    {
    
    - end }}

可以看到上面我们增加了一个{ {- with .Values.course }}xxx{ {- end }}的一个块,这样的话我们就可以在当前的块里面直接引用.python.k8s了,而不需要进行限定了,这是因为该with声明将.指向了.Values.course,在{ {- end }}.就会复原其之前的作用范围了,我们可以使用模板引擎来渲染上面的模板查看是否符合预期结果。

不过需要注意的是在with声明的范围内,此时将无法从父范围访问到其他对象了,比如下面的模板渲染的时候将会报错,因为显然.Release根本就不在当前的.范围内,当然如果我们最后两行交换下位置就正常了,因为{ {- end }}之后范围就被重置了:

{
    
    {
    
    - with .Values.course }}
k8s: {
    
    {
    
     .k8s | upper | quote }}
python: {
    
    {
    
     .python | repeat 3 | quote }}
release: {
    
    {
    
     .Release.Name }}
{
    
    {
    
    - end }}

range 循环

如果大家对编程语言熟悉的话,几乎所有的编程语言都支持类似于for、foreach或者类似功能的循环机制,在 Helm 模板语言中,是使用range关键字来进行循环操作。

我们在values.yaml文件中添加上一个课程列表:

course:
  k8s: devops
  python: django
courselist:
- k8s
- python
- search
- golang

现在我们有一个课程列表,修改 ConfigMap 模板文件来循环打印出该列表:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  myvalue: {
    
    {
    
     .Values.hello | default  "Hello World" | quote }}
  {
    
    {
    
    - with .Values.course }}
  k8s: {
    
    {
    
     .k8s | upper | quote }}
  python: {
    
    {
    
     .python | repeat 3 | quote }}
  {
    
    {
    
    - if eq .python "django" }}
  web: true
  {
    
    {
    
    - end }}
  {
    
    {
    
    - end }}
  courselist:
  {
    
    {
    
    - range .Values.courselist }}
  - {
    
    {
    
     . | title | quote }}
  {
    
    {
    
    - end }}

可以看到最下面我们使用了一个range函数,该函数将会遍历{ { .Values.courselist }}列表,循环内部我们使用的是一个.,这是因为当前的作用域就在当前循环内,这个.从列表的第一个元素一直遍历到最后一个元素,然后在遍历过程中使用了title和quote这两个函数,前面这个函数是将字符串首字母变成大写,后面就是加上双引号变成字符串,所以按照上面这个模板被渲染过后的结果为:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '34626'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: dining-terrier-configmap
data:
  myvalue: "Hello World"
  k8s: "DEVOPS"
  python: "djangodjangodjango"
  web: true
  courselist:
  - "K8s"
  - "Python"
  - "Search"
  - "Golang"

我们可以看到courselist按照我们的要求循环出来了。除了 list 或者 tuple,range 还可以用于遍历具有键和值的集合(如map 或 dict),这个就需要用到变量的概念了。


变量

前面我们已经学习了函数、管理以及控制流程的使用方法,我们知道编程语言中还有一个很重要的概念叫:变量,在 Helm 模板中,使用变量的场合不是特别多,但是在合适的时候使用变量可以很好的解决我们的问题。如下面的模板:

{
    
    {
    
    - with .Values.course }}
k8s: {
    
    {
    
     .k8s | upper | quote }}
python: {
    
    {
    
     .python | repeat 3 | quote }}
release: {
    
    {
    
     .Release.Name }}
{
    
    {
    
    - end }}

我们在with语句块内添加了一个.Release.Name对象,但这个模板是错误的,编译的时候会失败,这是因为.Release.Name不在该with语句块限制的作用范围之内,我们可以将该对象赋值给一个变量可以来解决这个问题:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  {
    
    {
    
    - $releaseName := .Release.Name -}}
  {
    
    {
    
    - with .Values.course }}
  k8s: {
    
    {
    
     .k8s | upper | quote }}
  python: {
    
    {
    
     .python | repeat 3 | quote }}
  release: {
    
    {
    
     $releaseName }}
  {
    
    {
    
    - end }}

我们可以看到我们在with语句上面增加了一句{ {- $releaseName := .Release.Name -}},其中$releaseName就是后面的对象的一个引用变量,它的形式就是$name,赋值操作使用:=,这样with语句块内部的$releaseName变量仍然指向的是.Release.Name,同样,我们 DEBUG 下查看结果:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '45474'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: nosy-seagull-configmap
data:
  k8s: "DEVOPS"
  python: "djangodjangodjango"
  release: nosy-seagull

可以看到已经正常了,另外变量在range循环中也非常有用,我们可以在循环中用变量来同时捕获索引的值:

courselist:
{
    
    {
    
    - range $index, $course := .Values.courselist }}
- {
    
    {
    
     $index }}: {
    
    {
    
     $course | title | quote }}
{
    
    {
    
    - end }}

例如上面的这个列表,我们在range循环中使用$index$course两个变量来接收后面列表循环的索引和对应的值,最终可以得到如下结果:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '38876'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: vetoed-anaconda-configmap
data:
  courselist:
  - 0: "K8s"
  - 1: "Python"
  - 2: "Search"
  - 3: "Golang"

我们可以看到 courselist 下面将索引和对应的值都打印出来了,实际上具有键和值的数据结构我们都可以使用range来循环获得二者的值,比如我们可以对.Values.course这个字典来进行循环:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
data:
  {
    
    {
    
    - range $key, $value := .Values.course }}
  {
    
    {
    
     $key }}: {
    
    {
    
     $value | quote }}
  {
    
    {
    
    - end }}

直接使用range循环,用变量$key$value来接收字段.Values.course的键和值。这就是变量在 Helm 模板中的使用方法。


命名模板

前面我们学习了一些 Helm 模板中的一些常用使用方法,但是我们都是操作的一个模板文件,在实际的应用中,很多都是相对比较复杂的,往往会超过一个模板,如果有多个应用模板,我们应该如何进行处理呢?这就需要用到新的概念:命名模板。

命名模板我们也可以称为子模板,是限定在一个文件内部的模板,然后给一个名称。在使用命名模板的时候有一个需要特别注意的是:模板名称是全局的,如果我们声明了两个相同名称的模板,最后加载的一个模板会覆盖掉另外的模板,由于子 chart 中的模板也是和顶层的模板一起编译的,所以在命名的时候一定要注意,不要重名了。

为了避免重名,有个通用的约定就是为每个定义的模板添加上 chart 名称:{ {define "mychart.labels"}},define关键字就是用来声明命名模板的,加上 chart 名称就可以避免不同 chart 间的模板出现冲突的情况。


声明define和使用命名template

使用define关键字就可以允许我们在模板文件内部创建一个命名模板,它的语法格式如下:

{
    
    {
    
     define "ChartName.TplName" }}
# 模板内容区域
{
    
    {
    
     end }}

比如,现在我们可以定义一个模板来封装一个 label 标签:

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

然后我们可以将该模板嵌入到现有的 ConfigMap 中,然后使用 template 关键字在需要的地方包含进来即可:

{
    
    {
    
    - define "mychart.labels" }}
  labels:
    from: helm
    date: {
    
    {
    
     now | htmlDate }}
{
    
    {
    
    - end }}
apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
  {
    
    {
    
    - template "mychart.labels" }}
data:
  {
    
    {
    
    - range $key, $value := .Values.course }}
  {
    
    {
    
     $key }}: {
    
    {
    
     $value | quote }}
  {
    
    {
    
    - end }}

我们这个模板文件被渲染过后的结果如下所示:

$ helm install --dry-run --debug .

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '42058'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ardent-bunny-configmap
  labels:
    from: helm
    date: 2018-09-22
data:
  k8s: "devops"
  python: "django"

我们可以看到define区域定义的命名模板被嵌入到了template所在的区域,但是如果我们将命名模板全都写入到一个模板文件中的话无疑也会增大模板的复杂性。

还记得我们在创建 chart 包的时候,templates 目录下面默认会生成一个_helpers.tpl文件吗?我们前面也提到过 templates 目录下面除了NOTES.txt文件和以下划线_开头命令的文件之外,都会被当做 kubernetes 的资源清单文件,而这个下划线开头的文件不会被当做资源清单外,还可以被其他 chart 模板中调用,这个就是 Helm 中的partials文件,所以其实我们完全就可以将命名模板定义在这些partials文件中,默认就是_helpers.tpl文件了。

现在我们将上面定义的命名模板移动到 templates/_helpers.tpl 文件中去:

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

一般情况下面,我们都会在命名模板头部加一个简单的文档块,用/**/包裹起来,用来描述我们这个命名模板的用途的。

现在我们讲命名模板从模板文件 templates/configmap.yaml 中移除,当然还是需要保留 template 来嵌入命名模板内容,名称还是之前的 mychart.lables,这是因为模板名称是全局的,所以我们可以能够直接获取到。我们再用 DEBUG 模式来调试下是否符合预期?
示例:Creating Image Pull Secrets

假设凭证是在values.yaml文件中定义的,如下所示:

imageCredentials:
  registry: quay.io
  username: someone
  password: sillyness
  email: [email protected]

然后我们定义我们的帮助模板如下:

{
    
    {
    
    - define "imagePullSecret" }}
{
    
    {
    
    - with .Values.imageCredentials }}
{
    
    {
    
    - printf "{
    
    \"auths\":{
    
    \"%s\":{
    
    \"username\":\"%s\",\"password\":\"%s\",\"email\":\"%s\",\"auth\":\"%s\"}}}" .registry .username .password .email (printf "%s:%s" .username .password | b64enc) | b64enc }}
{
    
    {
    
    - end }}
{
    
    {
    
    - end }}

最后,我们在更大的模板中使用辅助模板来创建 Secret 清单:

apiVersion: v1
kind: Secret
metadata:
  name: myregistrykey
type: kubernetes.io/dockerconfigjson
data:
  .dockerconfigjson: {
    
    {
    
     template "imagePullSecret" . }}

模板范围

上面我们定义的命名模板中,没有使用任何对象,只是使用了一个简单的函数,如果我们在里面来使用 chart 对象相关信息呢:

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

如果这样的直接进行渲染测试的话,是不会得到我们的预期结果的:

$ $ helm install --dry-run --debug .
[debug] Created tunnel using local port: '42058'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: peeking-zorse-configmap
  labels:
    from: helm
    date: 2018-09-22
    chart:
    version:
data:
  k8s: "devops"
  python: "django"

chart 的名称和版本都没有正确被渲染,这是因为他们不在我们定义的模板范围内,当命名模板被渲染时,它会接收由 template 调用时传入的作用域,由于我们这里并没有传入对应的作用域,因此模板中我们无法调用到 .Chart 对象,要解决也非常简单,我们只需要在 template 后面加上作用域范围即可:

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

我们在 template 末尾传递了.,表示当前的最顶层的作用范围,如果我们想要在命名模板中使用.Values范围内的数据,当然也是可以的,现在我们再来渲染下我们的模板:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '32768'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: oldfashioned-mule-configmap
  labels:
    from: helm
    date: 2018-09-22
    chart: mychart
    version: 0.1.0
data:
  k8s: "devops"
  python: "django"

我们可以看到 chart 的名称和版本号都已经被正常渲染出来了。


include 函数

假如现在我们将上面的定义的 labels 单独提取出来放置到 _helpers.tpl 文件中:

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

现在我们将该命名模板插入到 configmap 模板文件的 labels 部分和 data 部分:

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

然后同样的查看下渲染的结果:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '42652'

......

Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: mapping values are not allowed in this context

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: altered-wombat-configmap
  labels:
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0
data:
  k8s: "devops"
  python: "django"
from: helm
date: 2018-09-22
chart: mychart
version: 0.1.0

我们可以看到渲染结果是有问题的,不是一个正常的 YAML 文件格式,这是因为template只是表示一个嵌入动作而已,不是一个函数,所以原本命名模板中是怎样的格式就是怎样的格式被嵌入进来了。

为了解决这个问题,Helm 提供了另外一个方案来代替template,那就是使用include函数,在需要控制空格的地方使用indent管道函数来自己控制,比如上面的例子我们替换成include函数:

apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap
  labels:
{
    
    {
    
    - include "mychart.labels" . | indent 4 }}
data:
  {
    
    {
    
    - range $key, $value := .Values.course }}
  {
    
    {
    
     $key }}: {
    
    {
    
     $value | quote }}
  {
    
    {
    
    - end }}
{
    
    {
    
    - include "mychart.labels" . | indent 2 }}

在 labels 区域我们需要4个空格,所以在管道函数indent中,传入参数4就可以,而在 data 区域我们只需要2个空格,所以我们传入参数2即可以,现在我们来渲染下我们这个模板看看是否符合预期呢:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '38481'

......

---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: torpid-bobcat-configmap
  labels:
    from: helm
    date: 2018-09-22
    chart: mychart
    version: 0.1.0
data:
  k8s: "devops"
  python: "django"
  from: helm
  date: 2018-09-22
  chart: mychart
  version: 0.1.0

可以看到是符合我们的预期,所以在 Helm 模板中我们使用 include 函数要比 template 更好,可以更好地处理 YAML 文件输出格式。


required 函数

该required函数允许您根据模板渲染的需要声明一个特定的值条目。如果该值为空,则模板渲染将失败并显示用户提交的错误消息。

以下required函数示例声明了一个条目 for.Values.who是必需的,并且在缺少该条目时将打印一条错误消息:

value: {
    
    {
    
     required "A valid .Values.who entry required!" .Values.who }}

tpl 函数

该tpl函数允许开发人员将字符串评估为模板内的模板。这对于将模板字符串作为值传递给图表或呈现外部配置文件很有用。句法:{ { tpl TEMPLATE_STRING VALUES }}

# values
template: "{
    
    { .Values.name }}"
name: "Tom"
# template
{
    
    {
    
     tpl .Values.template . }}
# output
Tom

渲染外部配置文件:

# external configuration file conf/app.conf
firstName={
    
    {
    
     .Values.firstName }}
lastName={
    
    {
    
     .Values.lastName }}
# values
firstName: Peter
lastName: Parker
# template
{
    
    {
    
     tpl (.Files.Get "conf/app.conf") . }}
# output
firstName=Peter
lastName=Parker

(这个属实没太明白干嘛用的,调试?)


注意事项

到这里我们基本上就把 Helm 模板中经常使用到的一些知识点和大家介绍完了。但是仍然还是有一些在开发中值得我们注意的一些知识点,比如 NOTES.txt 文件的使用、子 Chart 的使用、全局值的使用。

子 chart 包

我们到目前为止都只用了一个 chart,但是 chart 也可以有 子 chart 的依赖关系,它们也有自己的值和模板,在学习子 chart 之前,我们需要了解几点关于子 chart 的说明:

子 chart 是独立的,所以子 chart 不能明确依赖于其父 chart
子 chart 无法访问其父 chart 的值
父 chart 可以覆盖子 chart 的值
Helm 中有全局值的概念,可以被所有的 chart 访问

创建子 chart

现在我们就来创建一个子 chart,还记得我们在创建 mychart 包的时候,在根目录下面有一个空文件夹 charts 目录吗?这就是我们的子 chart 所在的目录,在该目录下面添加一个新的 chart:

$ cd mychart/charts
$ helm create mysubchart
Creating mysubchart
$ rm -rf mysubchart/templates/*.*
$ tree ..
..
├── charts
│   └── mysubchart
│       ├── charts
│       ├── Chart.yaml
│       ├── templates
│       └── values.yaml
├── Chart.yaml
├── templates
│   ├── configmap.yaml
│   ├── _helpers.tpl
│   └── NOTES.txt
└── values.yaml

5 directories, 7 files

同样的,我们将子 chart 模板中的文件全部删除了,接下来,我们为子 chart 创建一个简单的模板和 values 文件了。

$ cat > mysubchart/values.yaml <<EOF
in: mysub
EOF
$ cat > mysubchart/templates/configmap.yaml <<EOF
apiVersion: v1
kind: ConfigMap
metadata:
  name: {
    
    {
    
     .Release.Name }}-configmap2
data:
  in: {
    
    {
    
     .Values.in }}
EOF

我们上面已经提到过每个子 chart 都是独立的 chart,所以我们可以单独给 mysubchart 进行测试:

$ helm install --dry-run --debug ./mysubchart
[debug] Created tunnel using local port: '33568'

......

---
# Source: mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: washed-indri-configmap2
data:
  in: mysub

我们可以看到正常渲染出了结果。

值覆盖

现在 mysubchart 这个子 chart 就属于 mychart 这个父 chart 了,由于 mychart 是父级,所以我们可以在 mychart 的 values.yaml 文件中直接配置子 chart 中的值,比如我们可以在 mychart/values.yaml 文件中添加上子 chart 的值:

course:
  k8s: devops
  python: django
courselist:
- k8s
- python
- search
- golang

mysubchart:
  in: parent

注意最后两行,mysubchart 部分内的任何指令都会传递到 mysubchart 这个子 chart 中去的,现在我们在 mychart 根目录中执行调试命令,可以查看到子 chart 也被一起渲染了:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '44798'

......

---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: ideal-ostrich-configmap2
data:
  in: parent
---
# Source: mychart/templates/configmap.yaml
......

我们可以看到子 chart 中的值已经被顶层的值给覆盖了。但是在某些场景下面我们还是希望某些值在所有模板中都可以使用,这就需要用到全局 chart 值了。

全局值

全局值可以从任何 chart 或者子 chart中进行访问使用,values 对象中有一个保留的属性是Values.global,就可以被用来设置全局值,比如我们在父 chart 的 values.yaml 文件中添加一个全局值:

course:
  k8s: devops
  python: django
courselist:
- k8s
- python
- search
- golang

mysubchart:
  in: parent

global:
  allin: helm

我们在 values.yaml 文件中添加了一个 global 的属性,这样的话无论在父 chart 中还是在子 chart 中我们都可以通过{ { .Values.global.allin }}来访问这个全局值了。比如我们在 mychart/templates/configmap.yaml 和 mychart/charts/mysubchart/templates/configmap.yaml 文件的 data 区域下面都添加上如下内容:

...
data:
  allin: {
    
    {
    
     .Values.global.allin }}
...

现在我们在 mychart 根目录下面执行 debug 调试模式:

$ helm install --dry-run --debug .
[debug] Created tunnel using local port: '32775'

......

MANIFEST:

---
# Source: mychart/charts/mysubchart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: wistful-spaniel-configmap2
data:
  allin: helm
  in: parent
---
# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: wistful-spaniel-configmap
......
data:
  allin: helm
......

我们可以看到两个模板中都输出了allin: helm这样的值,全局变量对于传递这样的信息非常有用,不过也要注意我们不能滥用全局值。

另外值得注意的是我们在学习命名模板的时候就提到过父 chart 和子 chart 可以共享模板。任何 chart 中的任何定义块都可用于其他 chart,所以我们在给命名模板定义名称的时候添加了 chart 名称这样的前缀,避免冲突。

猜你喜欢

转载自blog.csdn.net/qq_43762191/article/details/124877893