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

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

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

场景

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

{{- 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 是什么

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

// 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 包含三个值:

// KubeVersion is the Kubernetes version.
type KubeVersion struct {
	Version string // Kubernetes version
	Major   string // Kubernetes major version
	Minor   string // Kubernetes minor version
}

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

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 字段:

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

同时可以

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

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

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

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

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

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 结构体的初始化调用的查看,可以看到有一处初始化默认版本的代码,如下:

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

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

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 命令初始化的时候,可以看到

client.ClientOnly = !validate

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

集群中读取 KubeVersion

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

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

正确的姿势

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

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

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

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

Reference

Read more

容器镜像(4):镜像的常用工具箱

容器镜像(4):镜像的常用工具箱

前几篇在讲多架构镜像时已经用过 skopeo 和 crane 做镜像复制,这篇系统整理这两个工具的完整能力,同时介绍几个日常操作镜像时同样好用的工具。 一、skopeo:不依赖 Daemon 的镜像瑞士军刀 skopeo 的核心价值是绕过 Docker daemon,直接与 Registry API 交互。上一篇用它做镜像复制和离线传输,但它的能力远不止于此。 1.1 安装 # Ubuntu / Debian sudo apt install -y skopeo skopeo --version # skopeo version 1.15.1 1.2 inspect:免拉取检查镜像元数据 docker inspect 需要先把镜像拉到本地,skopeo inspect 直接向 Registry

容器镜像(3):多架构镜像构建

容器镜像(3):多架构镜像构建

一、什么是多架构镜像 1.1 OCI Image Index 上一篇介绍了单平台镜像的结构:一个 Manifest 指向 Config 和若干 Layer blob。多架构镜像在此之上多了一层——OCI Image Index(也叫 Manifest List),是一个轻量的索引文件,把多个单平台 Manifest 组织在一起: $ docker manifest inspect golang:1.22-alpine { "schemaVersion": 2, "mediaType": "application/vnd.oci.image.index.v1+json", "manifests&

容器镜像(2):containerd 视角下的镜像

容器镜像(2):containerd 视角下的镜像

一、为什么需要了解 containerd 如果你只用 docker run 跑容器,从来不关心底层,那可以不了解 containerd。但如果你在用 Kubernetes,或者想真正理解"容器运行时"是什么,containerd 是绕不开的。 事实上,当你执行 docker run 的时候,containerd 早就在后台悄悄工作了——Docker 从 1.11 版本开始,就把核心运行时剥离出来交给 containerd 负责。 1.1 Docker 的架构演变 早期的 Docker(1.10 及之前)是一个"大一统"的单体程序:一个 dockerd