# 45. Helm 模板之管道与控制结构
上节课我们学习了如何将信息渲染到模板之中，但是这些信息都是直接传入模板引擎中进行渲染的，有的时候我们想要转换一下这些数据才进行渲染，这就需要使用到 Go 模板语言中的一些其他用法。

## 模板函数
比如我们需要从`.Values`中读取的值变成字符串的时候就可以通过调用`quote`模板函数来实现：(templates/configmap.yaml)
```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`函数并将后面的值作为一个参数传递给它。最终被渲染为：
```shell
$ 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`。上节课我们也提到过 Helm 是一种 Go 模板语言，拥有超过60多种可用的内置函数，一部分是由[Go 模板语言](https://godoc.org/text/template)本身定义的，其他大部分都是[`Sprig`模板库](https://godoc.org/github.com/Masterminds/sprig)提供的一部分，我们可以前往这两个文档中查看这些函数的用法。

比如我们这里使用的`quote`函数就是`Sprig 模板库`提供的一种字符串函数，用途就是用双引号将字符串括起来，如果需要双引号`"`，则需要添加`\`来进行转义，而`squote`函数的用途则是用双引号将字符串括起来，而不会对内容进行转义。

所以在我们遇到一些需求的时候，首先要想到的是去查看下上面的两个模板文档中是否提供了对应的模板函数，这些模板函数可以很好的解决我们的需求。


## 管道
模板语言除了提供了丰富的内置函数之外，其另一个强大的功能就是**管道**的概念。和`UNIX`中一样，管道我们通常称为`Pipeline`，是一个链在一起的一系列模板命令的工具，以紧凑地表达一系列转换。简单来说，管道是可以按顺序完成一系列事情的一种方法。比如我们用管道来重写上面的 ConfigMap 模板：（templates/configmap.yaml）
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  k8s: {{ .Values.course.k8s | quote }}
  python: {{ .Values.course.python }}
```

这里我们直接调用`quote`函数，而是调换了一个顺序，使用一个管道符`|`将前面的**参数**发送给后面的**模板函数**：`{{ .Values.course.k8s | quote }}`，使用管道我们可以将几个功能顺序的连接在一起，比如我们希望上面的 ConfigMap 模板中的 k8s 的 value 值被渲染后是大写的字符串，则我们就可以使用管道来修改：（templates/configmap.yaml）
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ .Release.Name }}-configmap
data:
  myvalue: "Hello World"
  k8s: {{ .Values.course.k8s | upper | quote }}
  python: {{ .Values.course.python }}
```

这里我们在管道中增加了一个`upper`函数，该函数同样是[`Sprig 模板库`](https://godoc.org/github.com/Masterminds/sprig)提供的，表示将字符串每一个字母都变成大写。然后我们用`debug`模式来查看下上面的模板最终会被渲染成什么样子：
```shell
$ 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次的字符串，则我们就可以使用到[`Sprig 模板库`](https://godoc.org/github.com/Masterminds/sprig)提供的`repeat`函数，不过该函数需要传入一个参数`repeat COUNT STRING`表示重复的次数：（templates/configmap.yaml）
```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次返回，所以我们将得到这个输出：
```shell
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）
```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`函数：
```shell
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）
```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`文件中没有定义这个值，则我们也可以得到默认值：
```shell
$ 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"
```

我们可以看到`myvalue`值被渲染成了**Hello World**，证明我们的默认值生效了。


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

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

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

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

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

关于`命名模板`的相关知识点，我们会在后面的课程中和大家接触到，这里我们暂时和大家介绍`if/else`、`with`、`range`这3中控制流程的用法。

### if/else 条件
`if/else`块是用于在模板中有条件地包含文本块的方法，条件块的基本结构如下：
```yaml
{{ 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）
```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`这样的的一个条目：
```shell
$ 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:
```shell
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 模式查看最终被渲染的值也没有出现这个条目，证明条件判断是正确的。

### 空格控制
上面我们的条件判断语句是在一整行中的，如果平时经常写代码的同学可能非常不习惯了，我们一般会将其格式化为更容易阅读的形式，比如：
```yaml
{{ if eq .Values.course.python "django" }}
web: true
{{ end }}
```

这样的话看上去比之前要清晰很多了，但是我们通过模板引擎来渲染一下，会得到如下结果：
```shell
$ 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）
```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 }}
```

现在我们来查看上面模板渲染过后的样子：
```shell
$ 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
```

现在是不是没有多余的空格了，另外我们需要谨慎使用` -}}`，比如上面模板文件中：
```yaml
python: {{ .Values.course.python | repeat 3 | quote }}
{{- if eq .Values.course.python "django" -}}
web: true
{{- end }}
```

如果我们在`if`条件后面增加` -}}`，这会渲染成：
```yaml
python: "djangodjangodjango"web: true
```

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

> 有关模板中空格控制的详细信息，请参阅官方 Go 模板文档[Official Go template documentation](https://godoc.org/text/template)


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

`with`语句可以允许将当前范围`.`设置为特定的对象，比如我们前面一直使用的`.Values.course`，我们可以使用`with`来将`.`范围指向`.Values.course`：(templates/configmap.yaml)
```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 }}`之后范围就被重置了：
```yaml
{{- with .Values.course }}
k8s: {{ .k8s | upper | quote }}
python: {{ .python | repeat 3 | quote }}
release: {{ .Release.Name }}
{{- end }}
```

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

todo
