helm 三、制作Chart(下)

大番茄 2020年02月24日 1,329次浏览

helm version: v3.1.0

一、流程控制

https://helm.sh/docs/chart_template_guide/control_structures/

Helm 模板语言提供以下流程控制语句:

  • if/else 判断
  • with 作用域
  • range 循环

1、if/else 判断

基本结构
{{ if PIPELINE }}  
 # Do something  
{{ else if OTHER PIPELINE }}  
 # Do something else  
{{ else }}  
 # Default case  
{{ end }}  
运算符

值为以下几种情况,则为假。

  • 布尔值 false
  • 数字
  • 一个 空字符串
  • 一个 nil(empty or null)
  • 一个空的集合(map, slice, tuple, dict, array)

所有其他条件下,条件为真。

运算符:
eq, ne, lt, gt, and, or
可以使用()括号。

例子

看一个官方的例子:

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.favorite.drink 等于 "coffee" , 则添加 mug: true
添加从语句到{{ end }} 之间的所有内容。

例子:

 containers:  
 - image: "{{ .Values.image.name }}:{{ .Chart.AppVersion }}"  
 name: {{ .Values.image.name }}  
 env:  
 {{ if .Values.env.rest }}  
 - name: host  
 value: localhost  
 - name: user  
 value: test  
 {{ end }}  
 - name: hello  
 value: world  

values.yaml 里定义了:

env:  
 rest: False  

渲染结果:

 containers:  
 - image: "nginx:1.16.0"  
 name: nginx  
 env:  
  
 - name: hello  
 value: world  
  

values.yaml:

env:  
 rest: True  

再次测试:

 containers:  
 - image: "nginx:1.16.0"  
 name: nginx  
 env:  
  
 - name: host  
 value: localhost  
 - name: user  
 value: test  
  
 - name: hello  
 value: world  

if 判断就这样。 但是发现多了空行。

处理空行

为什么会有空行出现,因为模板引擎渲染时,会删除{{ }}之间的内容,但却会完全保留其余的空白。
可以使用特殊字符修改花括号语法:
{{- (破折号与空格): 表示删除左边的空白。
-}} : 表示删除右边空白,注意:换行符也是空白。

修改一下刚才的例子:

 containers:  
 - image: "{{ .Values.image.name }}:{{ .Chart.AppVersion }}"  
 name: {{ .Values.image.name }}  
 env:  
 {{- if .Values.env.rest }}  
 - name: host  
 value: localhost  
 - name: user  
 value: test  
 {{- end }}  
 - name: hello  
 value: world  

没有空格了:

 containers:  
 - image: "nginx:1.16.0"  
 name: nginx  
 env:  
 - name: host  
 value: localhost  
 - name: user  
 value: test  
 - name: hello  
 value: world  

2、with 变量作用域

with 控制变量作用域。
作用域就是变量的引用范围。
上面的变量使用都是 .Values, 都是 . 开头,这个点其实就是一个当前范围的引用。with 就是修改这个范围,比如,设置到一个字典上。
先看一下语法,具体怎么回事下面举例子就明白了。

with 的语法类似简单的 if:

{{ with PIPELINE }}
  # restricted scope
{{ end }}
没有用 with 的例子:

values.yaml:

env:
  host: localhost
  user: test
  hello: world

deployment.yaml 的引用:

       {{- if .Values.env }}
        env:
        - name: host
          value: {{ .Values.env.host }}
        - name: user
          value: {{ .Values.env.user }}
        - name: hello
          value: {{ .Values.env.hello }}
        {{- end }}

上面的变量引用都需要从.Values开始, 有点繁琐。

with 的例子:

deployment.yaml 添加 with 以后:

       {{- with .Values.env }}
        env:
        - name: host
          value: {{ .host }}
        - name: user
          value: {{ .user }}
        - name: hello
          value: {{ .hello }}
        {{- end }}

with 语句块里, 把当前范围定义在了.Values.env这个变量上了。
查看一下这个 . 的内容
deployment.yaml 添加 . 的值。

       {{- with .Values.env }}
        env:
        - name: host
          value: {{ .host }}
        - name: user
          value: {{ .user }}
        - name: hello
          value: {{ .hello }}
        - name: test
          value: {{ . }}
        {{- end }}

结果:

       env:
        - name: host
          value: localhost
        - name: user
          value: test
        - name: hello
          value: world
        - name: test
          value: map[enable:true hello:world host:localhost user:test]

是一个 map 映射(字典)。跟不使用 with 的 .Values.env 结果一样。

但是有一个缺点,因为 with 修改了 . 的范围, 所以语句块里不能再引用 .Chart,.Release,.Values 等变量。

解决办法有两个:

  1. 使用模板变量。注意,不是上面那些变量,是模板内定义的变量。
  2. 使用 $ 符号,$ 始终指向根上下文。

3、模板变量

https://helm.sh/docs/chart_template_guide/variables/
变量的定义:
$relname := .Release.Name
定义变量跟别的语言不一样, 名称也需要 $ 符号, 赋值运算符是 :=
with 里面不能调用其他变量,那么就在 with 之外把其他变量重新定义为模板变量。
例子:

        {{- $relname := .Release.Name }}
        {{- with .Values.env }}
        env:
        - name: host
          value: {{ .host }}
        - name: user
          value: {{ .user }}
        - name: hello
          value: {{ .hello }}
        - name: releaseName
          value: {{ $relname }}
        {{- end }}

4、始终指向根上下文的 $ 变量

模板变量通常都不是全局的,作用域取决于定义它的语句块。
上面的 relname 是全局的,因为他没有在其他的语句块里。
但是有一个变量始终是全局的,就是 $ 变量。
$ 始终指向根上下文。 上面的 with 里面想要调用 .Release.Name,使用 $.Release.Name 就可以。
如:

 {{- with .Values.env }}  
 env:  
 - name: host  
 value: {{ .host }}  
 - name: user  
 value: {{ .user }}  
 - name: hello  
 value: {{ .hello }}  
 - name: releaseName  
 value: {{ $.Release.Name }}  
 {{- end }}  

5、range 循环

类似于 for 循环。直接看例子
官方的例子:
先在 values.yaml 里添加数据。
favorite 是上个例子的,主要关注 pizzaToppings。

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

创建 configmap.yaml 模板:

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

关注 range 那里。
看一下渲染结果:

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

range 遍历 .Values.pizzaToppings 列表。
而且也跟 with 一样,语句块里的 . 的范围变了,这里直接表示的每一次循环遍历出来的对应列表项。 . 直接就是表示可用的值了,后面的函数只是修改了数据的头字母大写以及字符串化。

例子

之前 deployment.yamlenv 字段的变量是手动添加的,有点傻对不对。可以使用 range 添加。
但是 values.yaml 里的数据是字典,循环出的东西会有问题,先来看一下。

        {{- if .Values.env.enable }}
        env:
          {{- range .Values.env }}
          - name: {{ . }}
            value: {{ . }}
          {{ end }}
        {{- end }}

其实一看就知道有问题, namevalue 的值都是一样的。
结果:

        env:
          - name: true
            value: true

          - name: world
            value: world

          - name: localhost
            value: localhost

          - name: test
            value: test

对比一下 yaml.yaml 的值:

env:
  enable: True
  host: localhost
  user: test
  hello: world

只取出了 value 部分,没有取出 key。
办法也是有, 使用模板变量。

取出字典 key 与 value

如:

        {{- if .Values.env.enable }}
        env:
          {{- range $key,$value := .Values.env }}
          - name: {{ $key }}
            value: {{ $value }}
          {{- end }}
        {{- end }}

结果:

        env:
          - name: enable
            value: true
          - name: hello
            value: world
          - name: host
            value: localhost
          - name: user
            value: test

这样就可以了。
对列表也可以取出索引和值。

取出列表索引与值

上面 pizza 的例子改一下:

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

执行结果:

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

二、一些骚操作

1、资源开关

if 判断可以直接放在模板文件的头和尾。从而实现关闭和开启某个资源的部署。
如:
service.yaml:

{{ if .Values.service -}}
apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx
spec:
  ports:
  {{- range .Values.service }}
  -
  {{- range $key,$value := . }}
    {{ $key }}: {{ $value }}
  {{- end }}
  {{- end }}
  selector:
    app: nginx
  type: ClusterIP
{{ end }}

values.yaml:

service:
#  - name: work
#    port: 80
#    protocol: TCP
#    targetPort: 80
#  - name: status
#    port: 88
#    protocol: TCP
#    targetPort: 88

这样的 values.yaml,因为 service 是空的。不会渲染 service.yaml 这个文件。
values.yaml 改成这样:

service:
  - name: work
    port: 80
    protocol: TCP
    targetPort: 80
  - name: status
    port: 88
    protocol: TCP
    targetPort: 88

结果:

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx
spec:
  ports:
  -
    name: work
    port: 80
    protocol: TCP
    targetPort: 80
  -
    name: status
    port: 88
    protocol: TCP
    targetPort: 88
  selector:
    app: nginx
  type: ClusterIP

2、values.yaml 变量的值原样添加到模板中

上面的 service port 是通过 range 循环实现。我们 values.yaml 里 service 的值字段与 k8s 的一致,可以原模原样的添加到模板中。
就可以这样:

apiVersion: v1
kind: Service
metadata:
  creationTimestamp: null
  labels:
    app: nginx
  name: nginx
spec:
  ports:
    {{- toYaml .Values.service | nindent 4 }}
  selector:
    app: nginx
  type: ClusterIP

.Values.service 的内容直接添加到这个位置。
直接添加到这里,数据会从行首开始,导致因为缩进问题出问题。
所以使用 nindent 函数添加缩进。
nindent 函数会对每行的内容都做缩进。
indent 函数只对单行数据做缩进。

三、注释

模板中使用{{/* */}}注释。如:

          env:
            - name: ttt
              value: yyy
{{/*
            {{- include "variables" . | nindent 12 -}}
*/}}
            {{- range $x, $y := .Values.dev }}
            - name: {{ $x }}
              value: {{ $y }}
            {{- end }}

注释的内容不会被渲染出来。

非模板的文件,如 values.yaml。 就是yaml文件的注释方式#

三、命名模板

https://helm.sh/docs/chart_template_guide/named_templates/

命名模板有时候也被称为 部分或子模板。
叫部分模板的原因估计是因为,这个模板只是为了定义部分内容。
相对于 deployment.yaml 这种主模板,命名模板只是定义部分通用内容,然后在各个主模板中调用。

要注意的一个细节

命名模板的名称是全局的,需要全局唯一。 而且有时候 chart 还依赖子 chart, 为了防止两个 chart 的模板名称重复。
一种流行的命名约定是在每个定义的模板前添加chart名称:{{ define "mychart.labels" }}

定义模板

刚创建的chart templates目录下有个_helpers.tpl文件。
公共的命名模板都放在这个文件里。 但不是必须的,起个别的名字也可以用,甚至放到主模板也可以。
这只是一种规定,为了容易维护都放到这个文件里,一些非公共的模板也会放到以tpl结尾的文件里。

命名模板使用 define 来定义。
如,这里先简单定义一个只包含字符串的模板,用作资源名称。
按照惯例,定义的模板上面都需要简单的文本描述他的作用。

[root@k8s-master3 templates]# cat _helpers.tpl
{{/* 定义资源名称 */}}
{{ define "mytest.name" -}}
mytest-name
{{ end }}

这里的定义语句也会出现空行,所以使用-}} 删除后面换行符。
里面的内容最好也是从行首开始,防止导致一些缩进问题。

template引用模板

使用template 引用。

metadata:
  labels:
    app: {{ .Values.name }}
  name: {{ template "mytest.name" }}
spec:

结果:

metadata:
  labels:
    app: web1
  name: mytest-name

spec:

命名模板正确引用了, 但是还是多出来一个空行。

处理空行

这个因为定义模板的语句块,字符串mytest-name 后面有一个换行符。
name: {{ template "mytest.name" }} 后面很显然也有一个换行符。
所以就有三个办法解决了:

1、删除定义模板里的回车符:
{{ define "mytest.name" -}}
mytest-name{{ end }}
2、不清楚为什么这样也行

{{- 官方的解释是只删除左侧的空白,可这看起来没这么简单。

{{ define "mytest.name" -}}
mytest-name
{{- end }}

3、删除引用模板语句后面的回车符:
metadata:
  labels:
    app: {{ .Values.name }}
  name: {{ template "mytest.name" -}}
spec:
模板引用变量的范围

记得之前的with有一个变量作用域,变量引用范围的东西。这里也是。
还是刚才的模板,改一下:

{{/* 定义资源名称 */}}
{{ define "mytest.name" -}}
{{ .Chart.Name }}-{{ .Release.Name }}
{{ end }}

为了方便看到效果,引用模板这里加点东西test-, 不加的话会报错的:

metadata:
  labels:
    app: {{ .Values.name }}
  name: test-{{ template "mytest.name" -}}
spec:

结果,下面这个只有test--,如果没有test-,结果只有一个 -, 当然报错啦:

metadata:
  labels:
    app: web1
  name: test--
spec:

发现定义的模板里, .Chart.Name.Release.Name是空的。
这是因为里面的.Chart.Name.Release.Name没有找到。

原因是:
define需要接收template传递变量范围的参数。
我们只需要把范围传递就可以了:

metadata:
  labels:
    app: {{ .Values.name }}
  name: test-{{ template "mytest.name" . -}}
spec:

这里是把当前范围.传递过去了。这里的当前范围是根范围,模板里的变量当然都可以引用。

include引用模板

template引用模板只是一个动作,它不是函数,没有返回值,不能通过管道传递给其他函数处理。
include 就是解决这个问题的。

看一下这个例子, 定义label:

{{/* 定义label */}}
{{ define "mytest.label" -}}
app: {{ .Release.Name }}
release: stable
env: qa
{{ end }}

引用的话就是这样:

metadata:
  labels:
    {{ template "mytest.label" . }}
  name: test-{{ template "mytest.name" . -}}
spec:

报缩进问题:

[root@k8s-master3 ~]# helm lint mytest
==> Linting mytest
[INFO] Chart.yaml: icon is recommended
[ERROR] templates/deployment.yaml: unable to parse YAML: error converting YAML to JSON: yaml: line 9: mapping values are not allowed in this context

Error: 1 chart(s) linted, 1 chart(s) failed

提示不允许在这个上下文。就是缩进有问题。 需要使用函数nindent添加缩进。

metadata:
  labels:
    {{ include "mytest.label" . | nindent 4 }}
  name: test-{{ template "mytest.name" . -}}
spec:

会有空行, 加-慢慢调试。 最终:

metadata:
  labels:
    {{- include "mytest.label" . | nindent 4 }}
  name: test-{{ template "mytest.name" . -}}
spec:
{{/* 定义label */}}
{{- define "mytest.label" -}}
app: {{ .Release.Name }}
release: stable
env: qa
{{- end }}

四、上传harbor

不影响harbor做镜像仓库的功能。

1、harbor支持

首先,启动harbor的时候添加参数--with-chartmuseum

[root@lvs harbor]# ./install.sh --help

Note: Please set hostname and other necessary attributes in harbor.yml first. DO NOT use localhost or 127.0.0.1 for hostname, because Harbor needs to be accessed by external clients.
Please set --with-notary if needs enable Notary in Harbor, and set ui_url_protocol/ssl_cert/ssl_cert_key in harbor.yml bacause notary must run under https.
Please set --with-clair if needs enable Clair in Harbor
Please set --with-chartmuseum if needs enable Chartmuseum in Harbor
[root@lvs harbor]# ./install.sh --with-chartmuseum

[Step 0]: checking installation environment ...

Note: docker version: 19.03.6

Note: docker-compose version: 1.25.4

[Step 1]: loading Harbor images ...
.....省略......

启动以后,在项目里就可以看到多了一个Helm Charts。
添加用于上传项目的用户。

2、helm支持

https://github.com/chartmuseum/helm-push
需要安装插件。

[root@k8s-master3 ~]# helm plugin install https://github.com/chartmuseum/helm-push
Downloading and installing helm-push v0.8.1 ...
https://github.com/chartmuseum/helm-push/releases/download/v0.8.1/helm-push_0.8.1_linux_amd64.tar.gz
Installed plugin: push
[root@k8s-master3 ~]#

3、添加仓库

把harbor添加成仓库。

[root@k8s-master3 ~]# helm repo add harbor http://172.100.102.91/chartrepo/mycharts --username sst --password Abcdefg123
"harbor" has been added to your repositories

里面的chartrepo是固定的。 mycharts是harbor上创建的项目名称。

也可以不添加,只是需要在上传的时候需要指定url。

4、helm push

使用helm push命令推送。
有几种方式:
1、直接推送目录,自动打包
2、helm package 打包以后推送。

[root@k8s-master3 ~]# helm push mytest harbor
Pushing mytest-0.1.0.tgz to harbor...
Done.

push完成,就会发现harbor的对应项目的Helm Charts标签下多了一个上传的chart。

5、从仓库下载

更新仓库信息:

[root@k8s-master3 ~]# helm repo update
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "harbor" chart repository
...Successfully got an update from the "choerodon" chart repository
...Successfully got an update from the "ali" chart repository
...Successfully got an update from the "stable" chart repository
Update Complete. ⎈ Happy Helming!⎈

发现已经多了harbor。

搜索一下上传的那个chart:

[root@k8s-master3 ~]# helm search repo mytest
NAME         	CHART VERSION	APP VERSION	DESCRIPTION
harbor/mytest	0.1.0        	1.16.0     	A Helm chart for Kubernetes

安装都一样:

[root@k8s-master3 ~]# helm install webtest harbor/mytest
Error: Deployment in version "v1" cannot be handled as a Deployment: v1.Deployment.Spec: v1.DeploymentSpec.Template: v1.PodTemplateSpec.Spec: v1.PodSpec.Containers: []v1.Container: v1.Container.Env: []v1.EnvVar: v1.EnvVar.Value: ReadString: expects " or n, but found t, error found in #10 byte of ...|,"value":true},{"nam|..., bigger context ...|:{"containers":[{"env":[{"name":"enable","value":true},{"name":"hello","value":"world"},{"name":"hos|...
[root@k8s-master3 ~]# ls

我这里安装报错了, 是因为chart本来就有问题,正常情况下肯定需要详细的测试,然后才能上传。