Helm3进行template时如何处理Capabilities.KubeVersion字段

在 helm3 中有一个 Capabilities.KubeVersion 字段,可以用来标识目标 Kubernetes 集群的版本,同时在 helm 中,可以通过模板语言,使用这个值来达到兼容性处理的目标。
那么这个值,该怎么操作,才能跟随目标集群变动呢?

场景

使用到 Capabilities.KubeVersion 内置变量的场景非常简单,通过 helm create sample 即可在 sample 文件夹中,创建一个默认的 helm chart。在 templates/ingress.yaml 文件中,可以看到一段使用它的代码,如下:

1
2
3
4
5
6
7
{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1
{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
apiVersion: networking.k8s.io/v1beta1
{{- else -}}
apiVersion: extensions/v1beta1
{{- end }}

Capabilities.KubeVersion 发生变更时,即所部署的目标集群的版本发生变化时,会为此 Ingress 资源渲染成不同的 apiVersion,达到兼容处理的目的。

历史变更

当前最新的版本是 3.8.2

在 helm 的 3.6.0 版本的 CHANGELOG 中,可以看到在 template 子命令中添加了 --kube-version 字段以及相关的测试代码

也就是说从 3.6.0 版本之后,可以在命令行中直接通过 --kube-version 来设置 .Capabilities.KubeVersion 值。

Capabilities 是什么

它的数据结构很简单,是一个简单的结构体,只包含三个属性:

1
2
3
4
5
6
7
8
9
// Capabilities describes the capabilities of the Kubernetes cluster.
type Capabilities struct {
// KubeVersion is the Kubernetes version.
KubeVersion KubeVersion
// APIversions are supported Kubernetes API versions.
APIVersions VersionSet
// HelmVersion is the build information for this helm version
HelmVersion helmversion.BuildInfo
}

APIVersions 是一个字符串数组:type VersionSet []string
KubeVersion 包含三个值:

1
2
3
4
5
6
// KubeVersion is the Kubernetes version.
type KubeVersion struct {
Version string // Kubernetes version
Major string // Kubernetes major version
Minor string // Kubernetes minor version
}

它的这三个值之间的联系,可以通过解析输入版本是的逻辑来进行区分:

1
2
3
4
5
6
7
8
9
10
11
func ParseKubeVersion(version string) (*KubeVersion, error) {
sv, err := semver.NewVersion(version)
if err != nil {
return nil, err
}
return &KubeVersion{
Version: "v" + sv.String(),
Major: strconv.FormatUint(sv.Major(), 10),
Minor: strconv.FormatUint(sv.Minor(), 10),
}, nil
}

还可以看到 KubeVersion.String()KubeVersion.GitCommit() 都是返回 KubeVersion.Version 字段:

1
2
func (kv *KubeVersion) String() string { return kv.Version }
func (kv *KubeVersion) GitVersion() string { return kv.Version }

同时可以

添加--kubeconfig是否会自动去集群查询?

按照我们潜意识里面的认知,添加了 --kubeconfig 之后,与集群相关的数据,会以集群中的实际数据为准,也就是我们觉得它会去集群中先获得集群的版本,然后再进行渲染,但是结果却不是这样的

通过 minikube 创建一个版本为 1.18.20 的 Kubernetes 集群:

1
minikube start --driver=hyperkit --kubernetes-version=v1.18.20

这个时候,如果指定刚创建集群的 kubeconfig,Ingress 的 apiVersion 应当是 networking.k8s.io/v1beta1,但却看到了 networking.k8s.io/v1

1
2
3
4
5
helm template . --kubeconfig=/Users/yangyu/.kube/config
---
# Source: sample/templates/ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress

也就是说指定了 kubeconfig 之后,并没有按照我们预期的那样去执行。这是为什么?

通过在 pkg/action/action.go:renderResources() 方法中的断点调试
EJ5Qbx

我们可以看到,渲染时 .Capabilities.KubeVersion 的值是 v1.20.0
CA6MCt

这个值不是目标集群的值,同时我们也没有指定 --kube-version。那这个值是哪来的?通过对 KubeVersion 结构体的初始化调用的查看,可以看到有一处初始化默认版本的代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var (
// The Kubernetes version can be set by LDFLAGS. In order to do that the value
// must be a string.
k8sVersionMajor = "1"
k8sVersionMinor = "20"

// DefaultVersionSet is the default version set, which includes only Core V1 ("v1").
DefaultVersionSet = allKnownVersions()

// DefaultCapabilities is the default set of capabilities.
DefaultCapabilities = &Capabilities{
KubeVersion: KubeVersion{
Version: fmt.Sprintf("v%s.%s.0", k8sVersionMajor, k8sVersionMinor),
Major: k8sVersionMajor,
Minor: k8sVersionMinor,
},
APIVersions: DefaultVersionSet,
HelmVersion: helmversion.Get(),
}
)

所以,v1.20.0 这个版本是默认的 KubeVersion。从而可以从侧面印证:即使指定了 kubeconfig,也不会从集群中获取集群的版本。

Helm template 时 Capabilities 的加载流程

总共分为3个步骤,主要看 --validate 是否为 true

初始化时读取 --kube-version

template 子命令最开始运行时,会读取 --kube-version 的输入(前提是有设置 --kube-version

1
2
3
4
5
6
7
if kubeVersion != "" {
parsedKubeVersion, err := chartutil.ParseKubeVersion(kubeVersion)
if err != nil {
return fmt.Errorf("invalid kube version '%s': %s", kubeVersion, err)
}
client.KubeVersion = parsedKubeVersion
}

设置默认 Capabilities 或已读取的 --kube-version

在快要进行渲染操作前,若为 ClientOnly 模式,则使用 DefaultCapabilities 或已读取的 --kube-version

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if i.ClientOnly {
// Add mock objects in here so it doesn't use Kube API server
// NOTE(bacongobbler): used for `helm template`
i.cfg.Capabilities = chartutil.DefaultCapabilities.Copy()
if i.KubeVersion != nil {
i.cfg.Capabilities.KubeVersion = *i.KubeVersion
}
i.cfg.Capabilities.APIVersions = append(i.cfg.Capabilities.APIVersions, i.APIVersions...)
i.cfg.KubeClient = &kubefake.PrintingKubeClient{Out: ioutil.Discard}

mem := driver.NewMemory()
mem.SetNamespace(i.Namespace)
i.cfg.Releases = storage.Init(mem)
} else if !i.ClientOnly && len(i.APIVersions) > 0 {
i.cfg.Log("API Version list given outside of client only mode, this list will be ignored")
}

那什么是 ClientOnly 模式?在 template 命令初始化的时候,可以看到

1
client.ClientOnly = !validate

其中 validate 来自 f.BoolVar(&validate, "validate", false, "xxx")。也就是说,可以通过指定 --validate=true 来避免被设置成 DefaultCapabilities

集群中读取 KubeVersion

如果非 ClientOnly 模式,它是从 k8s 集群中获取。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
caps, err := i.cfg.getCapabilities()

// getCapabilities()
if cfg.Capabilities != nil {
return cfg.Capabilities, nil
}

dc, err := cfg.RESTClientGetter.ToDiscoveryClient()
kubeVersion, err := dc.ServerVersion()
apiVersions, err := GetVersionSet(dc)
cfg.Capabilities = &chartutil.Capabilities{
APIVersions: apiVersions,
KubeVersion: chartutil.KubeVersion{
Version: kubeVersion.GitVersion,
Major: kubeVersion.Major,
Minor: kubeVersion.Minor,
},
HelmVersion: chartutil.DefaultCapabilities.HelmVersion,
}
return cfg.Capabilities, nil

综上所述,Capablities 的初始化过程的脑图如下:
WPSbLR

正确的姿势

经过上面的分析,正确的处理方式只有两种,如下:

1
helm template . --kubeconfig=/Users/yangyu/.kube/config --validate=true

1
helm template . --kube-version=v1.15.0

所以,--kubeconfig 只是作为 --validate=true 时才会生效的一个选项。

Reference

Helm3进行template时如何处理Capabilities.KubeVersion字段

https://eucham.me/2022/05/01/573f5a05f677.html

作者

遇寻

发布于

2022-05-01

更新于

2022-05-05

许可协议

评论