王金鑫,中国移动磐基 PaaS 平台中间件专业服务项目组成员

前言
kubectl convert 原本是 Kubernetes 命令行工具 kubectl 的子命令之一,但在 1.17 版本之后,它变成了一个插件,我姑且猜一猜是使用不频繁也或许是其他原因。但是这插件使你能够将清单转换为不同的 API 版本,尤其是在将清单迁移到新的 Kubernetes 发行版上未被废弃的 API 版本时,特别有帮助。
让我们从 kubectl convert 的源码角度去看看这个命令行工具,如何让能够将 Kubernetes API 清单转换为其他 API 版本,将清单转换为不同的 API 版本,并确保在转换过程中正确处理了依赖项和上下文?
安装 kubectl-convert[1]
#下载最新版发行版
curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl-convert"
#验证该可执行文件(可选步骤)
curl -LO "https://dl.k8s.io/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl-convert.sha256"
#基于校验和,验证 kubectl-convert 的可执行文件
echo "$(cat kubectl-convert.sha256) kubectl-convert" | sha256sum --check
#安装 kubectl-convert
sudo install -o root -g root -m 0755 kubectl-convert /usr/local/bin/kubectl-convert
#安装插件后,清理安装文件
rm kubectl-convert kubectl-convert.sha256
kubectl convert 转换版本信息
创建一个在 1.22 版本中,extensions/v1beta1 版本已经被废弃的 ingress
cat <<'EOF' > example-ingress.yml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: example-ingress
spec:
rules:
- http:
paths:
- backend:
serviceName: example
servicePort: 8080
path: /*
EOF
报错直接提示无法识别了,在这种情况下,如果我们面临大量的 YAML 文件需要转换,那一定推荐去试用一下 kubectl-convert。
kubectl apply -f example-ingress.yml
error: unable to recognize "example-ingress.yml": no matches for kind "Ingress" in version "extensions/v1beta1"
经过 kubectl convert 工具的转化,最新输出的 yml 与旧版本相比产生了显著的变化,具体内容如下:
# kubectl convert -f ./example-ingress.yml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
creationTimestamp: null
name: example-ingress
spec:
rules:
- http:
paths:
- backend:
service:
name: example
port:
number: 8080
path: /*
pathType: ImplementationSpecific
status:
loadBalancer: {}
解读调用 convert,它干了些什么?
convert 命令核心源码路径:
https://github.com/kubernetes/kubernetes/blob/master/pkg/kubectl/cmd/convert/convert.go
ConvertOptions
在代码中,ConvertOptions 结构体定义了转换命令所需的各种参数和选项。例如 PrintFlags 是用来控制输出格式和内容的打印参数,Namespace 用于指定目标对象所处的 Namespace,builder 和 validator 分别是资源构建器和资源校验器。
NewCmdConvert
下面这段代码定义了一个名为 NewCmdConvert 的函数,用于创建并返回一个 Cobra 命令行工具实例。
在函数中,首先使用 NewConvertOptions 函数创建一个 ConvertOptions 对象,该对象用于配置命令选项。然后,使用 &cobra.Command 构造函数创建一个 cobra.Command 实例,其中使用指定的 Use、Short、Long 和 Example 字段来定义命令的外观和行为。
在 cobra.Command 实例中,使用 cmdutil.CheckErr 函数检查 ConvertOptions 对象的 Complete 和 RunConvert 方法是否成功。
在命令运行时,将调用 ConvertOptions 对象的 Complete 方法以初始化命令选项,然后调用 ConvertOptions 对象的 RunConvert 方法来运行转换操作。
也就是说我们调用 convert 命令后,所有的转换逻辑都是 RunConvert 方法来帮我们做的。
func NewCmdConvert(f cmdutil.Factory, ioStreams genericiooptions.IOStreams) *cobra.Command {
o := NewConvertOptions(ioStreams)
......
cmd := &cobra.Command{
Run: func(cmd *cobra.Command, args []string) {
cmdutil.CheckErr(o.Complete(f, cmd))
cmdutil.CheckErr(o.RunConvert())
},
}
.....
}
Complete
Complete 方法主要是完成收集从命令行运行 convert 命令所需的参数。
首先获取了命令中的文件名,并根据 Factory 接口创建了资源构建器 builder。之后通过 ToRawKubeConfigLoader 获取了当前的 Namespace。最后通过 Validator 方法获取了资源校验器 validator。
func (o *ConvertOptions) Complete(f cmdutil.Factory, cmd *cobra.Command) (err error) {
err = o.FilenameOptions.RequireFilenameOrKustomize()
if err != nil {
return err
}
o.builder = f.NewBuilder
o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace()
if err != nil {
return err
}
o.validator = func() (validation.Schema, error) {
directive, err := cmdutil.GetValidationDirective(cmd)
if err != nil {
return nil, err
}
return f.Validator(directive)
}
// build the printer
o.Printer, err = o.PrintFlags.ToPrinter()
return err
}
RunConvert
RunConvert 方法则实现了文件转换的核心逻辑。它首先使用 builder 构建 resource.Builder 对象,并通过 FilenameParam 方法指定需要转换的文件名和其它选项。然后解析出每个文件对应的所有的 API 对象 Resource.Info,并调用 asVersionedObject 将得到的资源对象转换成指定版本的 API 对象。最后,使用 printers.ResourcePrinter 将 API 对象打印到标准输出或指定文件中。
func (o *ConvertOptions) RunConvert() error {
b := o.builder().
WithScheme(scheme.Scheme).
LocalParam(o.local)
if !o.local {
schema, err := o.validator()
if err != nil {
return err
}
b.Schema(schema)
}
r := b.NamespaceParam(o.Namespace).
ContinueOnError().
FilenameParam(false, &o.FilenameOptions).
Flatten().
Do()
err := r.Err()
if err != nil {
return err
}
singleItemImplied := false
infos, err := r.IntoSingleItemImplied(&singleItemImplied).Infos()
if err != nil {
return err
}
if len(infos) == 0 {
return fmt.Errorf("no objects passed to convert")
}
var specifiedOutputVersion schema.GroupVersion
if len(o.OutputVersion) > 0 {
specifiedOutputVersion, err = schema.ParseGroupVersion(o.OutputVersion)
if err != nil {
return err
}
}
internalEncoder := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...)
internalVersionJSONEncoder := unstructured.NewJSONFallbackEncoder(internalEncoder)
objects, err := asVersionedObject(infos, !singleItemImplied, specifiedOutputVersion, internalVersionJSONEncoder, o.IOStreams)
if err != nil {
return err
}
return o.Printer.PrintObj(objects, o.Out)
}
asVersionedObject
asVersionedObject 函数将 resources.Info 对象转换成 API 对象。首先将所有得到的 resources.Info 对象合并为一个 API 对象 List,如果只有一个资源对象,则表示该资源对象就是要转换的对象。然后根据指定的版本号,将 List 或单个对象切换为指定版本的 API 对象,并返回最终的结果。如果指定的版本不可用,则转换成默认版本并打印错误信息。
func asVersionedObject(infos []*resource.Info, forceList bool, specifiedOutputVersion schema.GroupVersion, encoder runtime.Encoder, iostream genericclioptions.IOStreams) (runtime.Object, error) {
objects, err := asVersionedObjects(infos, specifiedOutputVersion, encoder, iostream)
if err != nil {
return nil, err
}
var object runtime.Object
if len(objects) == 1 && !forceList {
object = objects[0]
} else {
object = &api.List{Items: objects}
targetVersions := []schema.GroupVersion{}
if !specifiedOutputVersion.Empty() {
targetVersions = append(targetVersions, specifiedOutputVersion)
}
targetVersions = append(targetVersions, schema.GroupVersion{Group: "", Version: "v1"})
converted, err := tryConvert(scheme.Scheme, object, targetVersions...)
if err != nil {
return nil, err
}
object = converted
}
actualVersion := object.GetObjectKind().GroupVersionKind()
if actualVersion.Version != specifiedOutputVersion.Version {
defaultVersionInfo := ""
if len(actualVersion.Version) > 0 {
defaultVersionInfo = fmt.Sprintf("Defaulting to %q", actualVersion.Version)
}
klog.V(1).Infof("info: the output version specified is invalid. %s\n", defaultVersionInfo)
}
return object, nil
}
asVersionedObjects
asVersionedObjects 函数将多个 resources.Info 对象转换成多个 API 对象,并将其放到一个数组中返回。对于未注册的 API 对象,将它们转换成 JSON 字符串,并使用 runtime.Unknown 类型封装起来。最后再尝试将转换后的对象和指定的版本号进行匹配和转换。
func asVersionedObjects(infos []*resource.Info, specifiedOutputVersion schema.GroupVersion, encoder runtime.Encoder, iostream genericclioptions.IOStreams) ([]runtime.Object, error) {
objects := []runtime.Object{}
for _, info := range infos {
if info.Object == nil {
continue
}
targetVersions := []schema.GroupVersion{}
// objects that are not part of api.Scheme must be converted to JSON
// TODO: convert to map[string]interface{}, attach to runtime.Unknown?
if !specifiedOutputVersion.Empty() {
if _, _, err := scheme.Scheme.ObjectKinds(info.Object); runtime.IsNotRegisteredError(err) {
// TODO: ideally this would encode to version, but we don't expose multiple codecs here.
data, err := runtime.Encode(encoder, info.Object)
if err != nil {
return nil, err
}
// TODO: Set ContentEncoding and ContentType.
objects = append(objects, &runtime.Unknown{Raw: data})
continue
}
targetVersions = append(targetVersions, specifiedOutputVersion)
} else {
gvks, _, err := scheme.Scheme.ObjectKinds(info.Object)
if err == nil {
for _, gvk := range gvks {
targetVersions = append(targetVersions, scheme.Scheme.PrioritizedVersionsForGroup(gvk.Group)...)
}
}
}
converted, err := tryConvert(scheme.Scheme, info.Object, targetVersions...)
if err != nil {
// Dont fail on not registered error converting objects.
// Simply warn the user with the error returned from api-machinery and continue with the rest of the file
// fail on all other errors
if runtime.IsNotRegisteredError(err) {
fmt.Fprintln(iostream.ErrOut, err.Error())
continue
}
return nil, err
}
objects = append(objects, converted)
}
return objects, nil
}
tryConvert
tryConvert 函数尝试将给定的 API 对象转换成指定的 API 版本。如果尝试的所有版本都无法转换成功,则返回最后一个错误。
func tryConvert(converter runtime.ObjectConvertor, object runtime.Object, versions ...schema.GroupVersion) (runtime.Object, error) {
var last error
for _, version := range versions {
if version.Empty() {
return object, nil
}
obj, err := converter.ConvertToVersion(object, version)
if err != nil {
last = err
continue
}
return obj, nil
}
return nil, last
}
补充
在 asVersionedObjects 函数中,如果转换后的对象是 runtime.Unknown 类型,则会将该对象的原始 JSON 字符串打印到终端中,以提示用户该 API 对象尚未注册。这种方式可以帮助用户及时发现和解决 API 对象版本不匹配的问题。
最后需要注意的一点是,代码中的转换命令只支持将 YAML 或 JSON 文件转换成指定版本的 API 对象,并将其打印到标准输出或指定文件中。如果用户需要将 API 对象转换为 YAML 或 JSON 格式的文件,则需要使用 kubectl 中的其他命令,例如 kubectl get、kubectl describe 等命令结合重定向符号实现。
参考资料
安装 kubectl-convert: https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/#install-kubectl-convert-plugin




