暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

Kubernetes API 清单转换工具 kubectl convert

k8s技术圈 2023-04-28
818

王金鑫,中国移动磐基 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 等命令结合重定向符号实现。

参考资料

[1]

安装 kubectl-convert: https://kubernetes.io/docs/tasks/tools/install-kubectl-linux/#install-kubectl-convert-plugin

文章转载自k8s技术圈,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论