流控制

控制结构(在模板语言中称为"actions")提供给你和模板作者控制模板迭代流的能力。 Helm的模板语言提供了以下控制结构:

  • if/else, 用来创建条件语句
  • with, 用来指定范围
  • range, 提供"for each"类型的循环

除了这些之外,还提供了一些声明和使用命名模板的关键字:

  • define 在模板中声明一个新的命名模板
  • template 导入一个命名模板
  • block 声明一种特殊的可填充的模板块

该部分,我们会讨论关于ifwith,和 range。其他部门会在该指南的“命名模板”部分说明。

If/Else

第一个控制结构是在按照条件在一个模板中包含一个块文本。即if/else块。

基本的条件结构看起来像这样:

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

注意我们讨论的是 管道 而不是值。这样做的原因是要清楚地说明控制结构可以执行整个管道,而不仅仅是计算一个值。

如果是以下值时,管道会被设置为 false

  • 布尔false
  • 数字0
  • 空字符串
  • nil (空或null)
  • 空集合(map, slice, tuple, dict, array)

在所有其他条件下,条件都为true。

让我们先在配置映射中添加一个简单的条件。如果饮品是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 }}

由于我们在最后一个例子中注释了drink: coffee,输出中就不会包含mug: true标识。但如果将这行添加到values.yaml 文件中,输入就会是这样:

# Source: mychart/templates/configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: eyewitness-elk-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 }}

初始情况下,看起来没问题。但是如果通过模板引擎运行时,我们将得到一个不幸的结果:

$ helm install --dry-run --debug ./mychart
SERVER: "localhost:44134"
CHART PATH: /Users/mattbutcher/Code/Go/src/helm.sh/helm/_scratch/mychart
Error: YAML parse error on mychart/templates/configmap.yaml: error converting YAML to JSON: yaml: line 9: did not find expected key

发生了啥?因为空格导致生成了错误的YAML。

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

mug的缩进是不对的。取消缩进重新执行一下:

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: telling-chimp-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 }}

只是为了把这一点搞清楚,我们来调整上述内容,用一个*来代替每个遵循此规则被删除的空白, 在行尾的*表示删除新行的字符:

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 }}

记住这一点,我们可以通过Helm运行模板并查看结果:

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

要注意这个删除字符的更改,很容易意外地出现情况:

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

这样会变成food: "PIZZA"mug:true,因为这把两边的新行都删除了。

关于模板中的空白控制,请查看 官方Go模板文档

最终,有时这更容易告诉模板系统如何缩进,而不是试图控制模板指令间的间距。因此,您有时会发现使用indent方法({{ indent 2 "mug:true" }})会很有用。

修改使用with的范围

下一个控制结构是with操作。这个用来控制变量范围。回想一下,.是对 当前作用域 的引用。因此 .Values就是告诉模板在当前作用域查找Values对象。

with的语法与if语句类似:

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

作用域可以被改变。with允许你为特定对象设定当前作用域(.)。比如,我们已经在使用.Values.favorite。 修改配置映射中的.的作用域指向.Values.favorite

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条件) 注意现在我们可以引用.drink.food了,而不必限定他们。因为with语句设置了.指向.Values.favorite.被重置为{{ 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 }}

或者,我们可以使用$从父作用域中访问Release.Name对象。当模板开始执行后$会被映射到根作用域,且执行过程中不会更改。 下面这种方式也可以正常工作:

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

在介绍了range之后,我们会看看模板变量,提供了上述作用域问题的另一种解决方案。

使用range操作循环

很多编程语言支持使用for循环,foreach循环,或者类似的方法机制。 在Helm的模板语言中,在一个集合中迭代的方式是使用range操作符。

开始之前,我们先在values.yaml文件添加一个披萨的配料列表:

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

现在我们有了一个pizzaToppings列表(模板中称为切片)。修改模板把这个列表打印到配置映射中:

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 }}

我可以使用$从父作用域访问Values.pizzaToppings列表。当模板开始执行后$会被映射到根作用域, 且执行过程中不会更改。下面这种方式也可以正常工作:

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 }}
  toppings: |-
    {{- range $.Values.pizzaToppings }}
    - {{ . | title | quote }}
    {{- end }}
  {{- end }}

让我们仔细看看toppings:列表。range方法“涵盖”(迭代)pizzaToppings列表。但现在发生了有意思的事情。 就像with设置了.的作用域,range操作符也做了同样的事。每一次循环,.都会设置为当前的披萨配料。 也就是说,第一次.设置成了mushrooms,第二次迭代设置成了cheese,等等。

我们可以直接发送.的值给管道,因此当我们执行{{ . | title | quote }}时,它会发送.title然后发送到quote。 如果执行这个模板,输出是这样的:

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

现在,我们已经处理了一些棘手的事情。toppings: |-行是声明的多行字符串。所以这个配料列表实际上不是YAML列表, 是个大字符串。为什么要这样做?因为在配置映射data中的数据是由键值对组成,key和value都是简单的字符串。 要理解这个示例,请查看 Kubernetes ConfigMap 文档。 但对于我们来说,这个细节并不重要。

正如例子中所示,|-标识在YAML中是指多行字符串。这在清单列表中嵌入大块数据是很有用的技术。

有时能在模板中快速创建列表然后迭代很有用,Helm模板的tuple可以很容易实现该功能。在计算机科学中, 元组表示一个有固定大小的类似列表的集合,但可以是任意数据类型。这大致表达了tuple的用法。

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

上述模板会生成以下内容:

  sizes: |-
    - small
    - medium
    - large

除了列表和元组,range可被用于迭代有键值对的集合(像mapdict)。我们会在下一部分介绍模板变量是看到它是如何应用的。