如何使用 Go 调用 Kubernetes API - 类型和常用机制

语言: CN / TW / HK

C T O

    分享

    视频

  解答

Go Rust Python Istio containerd CoreDNS Envoy etcd Fluentd Harbor Helm Jaeger Kubernetes Open Policy Agent Prometheus Rook TiKV TUF Vitess Argo Buildpacks CloudEvents CNI Contour Cortex CRI-O Falco Flux gRPC KubeEdge Linkerd NATS Notary OpenTracing Operator Framework SPIFFE SPIRE     Thanos

如何使用 Go 调用 Kubernetes API - 类型和常用机制

官方的 Kubernetes Go 客户端加载了高级抽象 - Clientset、Informers、Cache、Scheme、Discovery ,哦!当我尝试在不先学习运行组件的情况下使用它时,我遇到了大量的新概念。这是一次不愉快的经历,但更重要的是,它降低了我在代码中做出明智决策的能力。

因此,我决定彻底了解它的组成部分,为自己解开谜团。 client-go

但是从哪里开始呢?在剖析 client-go 自身之前,了解它的两个主要依赖项 k8s.io/api k8s.io/apimachinery 模块可能是一个好主意。它会简化主要任务,但这不是唯一的好处。这两个模块被排除在外是有原因的——它们不仅可以由客户端使用,还可以在服务器端或任何其他处理 Kubernetes 对象的软件中使用。

API 资源、种类和对象

首先,快速回顾一下。熟悉以下概念对于进一步讨论的成功至关重要:

  • 资源类型- 由 Kubernetes API 端点提供服务的实体: pods、deployments、configmaps 等。
  • API Group -资源类型被组织成版本化的逻辑组: apps/v1 batch/v1 storage.k8s.io/v1beta1
  • Object - 一个资源实例 - 每个 API 端点都处理某种资源类型的对象。
  • Kind - API 返回或接受的每个对象都必须符合对象模式- 由其种类定义的特定属性组合: Pod Deployment , ConfigMap 等。

区分广义上的对象和 Kubernetes “一流”对象也很重要——持久实体,如 Pod Service Secret 。虽然为了序列化和反序列化,每个 API 对象都必须具有 API 版本和种类属性,但并非每个 API 对象都是  Kubernetes 对象。

模块 k8s.io/api

Go 是一种静态类型的编程语言。那么,与 Pods ConfigMaps Secrets 和其他的 Kubernetes 对象对应的所有结构都在哪里?对,在 .k8s.io/api

尽管命名松散,但该 k8s.io/api 模块似乎仅用于 API 类型定义。它与我们都知道的 YAML 清单非常相似

package main

import (
  "fmt"

  appsv1 "k8s.io/api/apps/v1"
  corev1 "k8s.io/api/core/v1"
)

func main() {
  deployment := appsv1.Deployment{
    Spec: appsv1.DeploymentSpec{
      Template: corev1.PodTemplateSpec{
        Spec: corev1.PodSpec{
          Containers: []corev1.Container{
            { Name:  "web", Image: "nginx:1.21" },
          },
        },
      },
    },
  }

  fmt.Printf("%#v", &deployment)
}

该模块不仅定义了像上面这样的顶级 Kubernetes 对象 Deployment ,还为它们的内部属性定义了许多辅助类型:

// PodSpec is a description of a pod.
type PodSpec struct {
  Volumes []Volume `json:"volumes,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name" protobuf:"bytes,1,rep,name=volumes"`
  
  InitContainers []Container `json:"initContainers,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,20,rep,name=initContainers"`
    
  Containers []Container `json:"containers" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=containers"`

  EphemeralContainers []EphemeralContainer `json:"ephemeralContainers,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,34,rep,name=ephemeralContainers"`

  RestartPolicy RestartPolicy `json:"restartPolicy,omitempty" protobuf:"bytes,3,opt,name=restartPolicy,casttype=RestartPolicy"`
    
  ...
}

模块中定义的所有结构 k8s.io/api 都带有注释。不过要小心: jsonprotobuf

  • 支持编组为 JSON
  • 不鼓励 Protobuf 序列化- 生成的结果可能与现有的 API 服务器不兼容(请参阅 README 了解更多信息)。
http://github.com/kubernetes/api/tree/35d41aaac2bf55a353ccade31b852d466b2495c2#recommended-use

总结一下,这个 k8s.io/api 模块是:

  • Huge - 1000 多个描述 Kubernetes API 对象的结构。
  • 简单——几乎没有算法,只有“愚蠢”的数据结构。

  • 有用 - 它的数据类型被客户端、服务器、控制器等使用。

模块 k8s.io/apimachinery

与范围狭窄的 k8s.io/api 模块不同,该 k8s.io/apimachinery 模块相当多。自述文件将其目的描述为:

该库是服务器和客户端使用 Kubernetes API 基础设施的共享依赖项,无需直接类型依赖项。它的第一个消费者是 k8s.io/kubernetes , k8s.io/client-go k8s.io/apiserver

要在一篇文章中涵盖 apimachery 模块的所有职责是很困难的。因此,我将讨论这个模块中最常见的包、类型和功能。

有用的结构和接口

虽然该 k8s.io/api 模块专注于具体的高级类型,如 Deployments Secrets Pods ,但 k8s.io/apimachinery 它是低级但更通用的数据结构的家园。

例如, Kubernetes 对象的所有这些常见属性: apiVersion kind name uid ownerReferences creationTimestamp 等。如果我要构建自己的 Kubernetes 自定义资源,我不需要自己为这些属性定义数据类型——这要归功于 apimachinery 模块。

k8s.io/apimachinery/pkg/apis/meta 包定义了两个方便的结构 - TypeMeta 可以 ObjectMeta 嵌入到用户定义的结构中,使其看起来很像任何其他 Kubernetes 对象。

此外, TypeMetaandObjectMeta 结构实现 meta.Type meta.Object 接口,可用于以通用方式指向任何兼容对象。

模块中定义的另一个方便的类型 apimachinery interface runtime.Object 。由于其简单的定义,它可能看起来毫无用处:

// pkg/runtime

type Object interface {
  GetObjectKind() schema.ObjectKind
  DeepCopyObject() Object
}

但在现实中,它被大量使用! Kubernetes 代码早在 Go 获得真正的泛型支持之前就已经编写好了。所以,这 runtime.Object 很像传统的 interface{} 解决方法——它是一个通用接口,在代码库中被广泛的类型断言和类型切换。并且可以通过检查 kind 底层对象的来获得实际类型。

:bulb: runtime.Object 实例可以指向具有该 kind 属性的任何对象——成熟的 Kubernetes 对象、不携带元数据的更简单的 API 资源,或具有明确定义的对象方案的任何其他类型的对象。

:warning: 请注意,虽然看起来相似,但由于结构偏移量非零, meta.Object 因此无法安全地向下转换为相应的 Kubernetes 对象。

更有用的 apimachinery 类型:

  • PartialObjectMetadata 结构-元数据的组合。 TypeMeta 和元数据。·ObjectMeta·作为一种通用的方法来表示任何具有元数据的对象。
  • APIVersions , APIGroupList , APIGroup 结构体-还记得 kubectl get API 探索练习吗-原始 /API ? 这些和类似的结构用于 Kubernetes API 资源的类型,但不是 Kubernetes 对象(例如,它们有 kind apiVersion 属性,但没有真正的 Object 元数据)。
  • GetOptions , ListOptions , UpdateOptions 等等——这些结构体代表了客户端对资源的相应动作的参数。
  • GroupKind GroupVersionKind GroupResource GroupVersionResource 等——简单的数据传输对象——包含组、版本、类型或资源字符串的元组。

:bulb:在 Scheme RESTMapperGroupVersionKind 讨论之前请记住——他们的知识会派上用场。 GroupVersionResource

非结构化结构

是的,你没听错。撇开玩笑不谈,它是另一种重要且广泛使用的数据类型。

使用具体类型处理 Kubernetes 对象 k8s.io/api 很方便,但如果:

  • 您需要以通用方式使用 Kubernetes 对象吗?
  • 您不想或不能依赖该 api 模块?
  • api 您需要使用模块中未定义的自定义资源吗?

非结构化。用于救援的非结构化结构!这个结构体允许没有注册 Go 结构体的对象被操作为通用的 json 类对象:

type Unstructured struct {
  // Object is a JSON compatible map with
  // string, float, int, bool, []interface{}, or
  // map[string]interface{} children.
  Object map[string]interface{}
}

// And for the list of objects you can 
// use the UnstructuredList struct.
type UnstructuredList struct {
  Object map[string]interface{}

  Items []Unstructured
}

这两个结构只是 map[string]interface{} . 但是,它们带有一堆方便的方法来简化嵌套属性访问和 JSON 编组/解组。

如何在 Go 代码中使用非结构化 Kubernetes 对象: http://github.com/iximiuz/client-go-examples/blob/5b220c4572d65ea8bf0ad68e369e015902e7521c/crud-dynamic-simple/main.go#L36

类型转换 - 非结构化到类型化,反之亦然

自然地,可能需要将非结构化对象转换为具体 k8s.io/api 类型的结构(反之亦然)。该 runtime.UnstructuredConverter 接口及其默认实现 DefaultUnstructuredConverter 可以帮助您:

type UnstructuredConverter interface {
  ToUnstructured(obj interface{}) (map[string]interface{}, error)
  FromUnstructured(u map[string]interface{}, obj interface{}) error
}

如何将非结构化对象转换为类型化对象并返回: http://github.com/iximiuz/client-go-examples/tree/main/convert-unstructured-typed

对象序列化为 JSON、YAML 或 Protobuf

使用静态类型语言的 API 时,另一个乏味的任务是将数据结构编组和解组到它们的有线表示中和从它们的有线表示中解组。

大量 apimachinery 代码专门用于此任务:

// pkg/runtime

// Encoder writes objects to a serialized form
type Encoder interface {
  Encode(obj Object, w io.Writer) error
  Identifier() Identifier
}

// Decoder attempts to load an object from data.
type Decoder interface {
  Decode(
    data []byte,
    defaults *schema.GroupVersionKind,
    into Object
  ) (Object, *schema.GroupVersionKind, error)
}

type Serializer interface {
  Encoder
  Decoder
}

注意到上面的代码片段中的这些对象了吗?是的,这些是运行时。对象,也就是 Kind-able 接口{}实例。

例子:

如何将类型化的 Kubernetes 对象序列化为 JSON

http://github.com/iximiuz/client-go-examples/tree/main/serialize-typed-json

如何将类型化的 Kubernetes 对象序列化为 YAML

http://github.com/iximiuz/client-go-examples/tree/main/serialize-typed-yaml

如何将非结构化 Kubernetes 对象序列化为 JSON

http://github.com/iximiuz/client-go-examples/tree/main/serialize-unstructured-json

如何将非结构化 Kubernetes 对象序列化为 YAML

http://github.com/iximiuz/client-go-examples/tree/main/serialize-unstructured-yaml

Scheme 和 RESTMapper

运行时。在使用 client-go 时,方案概念随处出现,特别是在编写处理自定义资源的控制器(或 operator )时。

我花了一段时间才明白它的目的。然而,按照正确的顺序处理事情会有所帮助。

想想非结构化到类型化转换的潜在实现:有一个类似 JSON 的对象, k8s.io/api 需要从它创建一些具体类型的对应对象。第一步可能是弄清楚如何使用 kind string 创建类型化对象的空实例。

一种天真的方法可能看起来像是 switch 对所有可能类型(实际上是 API 组)的巨大声明:

import (
  appsv1 "k8s.io/api/apps/v1"
  corev1 "k8s.io/api/core/v1"
)

func New(apiVersion, kind string) runtime.Object {
  switch (apiVersion + "/" + kind) {  
  case: "v1/Pod":
    return &corev1.Pod{}
  case: "apps/v1/Deployment":
    return &appsv1.Deployment{}
  }
  ...
}

更聪明的方法是使用反射。可以为所有已注册的类型维护 switcha ,而不是: map[string]reflect.Type

type Registry struct {
  map[string]reflect.Type types
}

func (r *Registry) Register(apiVersion, kind string, typ reflect.Type) {
  r.types[apiVersion + "/" + kind] = typ
}

func (r *Registry) New(apiVersion, kind string) runtime.Object {
  return r.types[apiVersion + "/" + kind].New().(runtime.Object)
}

这种方法的优点是它不需要代码生成,并且可以在运行时添加新的类型映射。

现在,考虑一个反序列化问题:需要将一段 YAML JSON 转换为 Typed object 。第一步 - 对象创建 - 将非常相似。

事实证明,通过 API 组和种类创建空对象是一项如此频繁的任务,以至于它在模块中拥有了自己的组件 apimachinery- runtime.Scheme

// Scheme defines methods for serializing and deserializing API objects, a type
// registry for converting group, version, and kind information to and from Go
// schemas, and mappings between Go schemas of different versions. 
type Scheme struct {
  gvkToType map[schema.GroupVersionKind]reflect.Type
  
  typeToGVK map[reflect.Type][]schema.GroupVersionKind
  
  unversionedTypes map[reflect.Type]schema.GroupVersionKind
  
  unversionedKinds map[string]reflect.Type

  ...
}

runtime.Schemestruct 就是这样一个注册表,其中包含各种 Kubernetes 对象的类型到类型和类型到类型的映射。

请记住, GroupVersionKind 它只是一个元组,即一个 DTO 结构,对吗?:wink:

runtime.Scheme 结构实际上非常强大 - 它有一大堆方法并实现了一些基础接口,例如:

// ObjectTyper contains methods for extracting 
// the APIVersion and Kind of objects.
type ObjectTyper interface {
  ObjectKinds(runtime.Object) ([]schema.GroupVersionKind, bool, error)
  Recognizes(gvk schema.GroupVersionKind) bool
}

// ObjectCreater contains methods for instantiating
// an object by kind and version.
type ObjectCreater interface {
  New(kind schema.GroupVersionKind) (out Object, err error)
}

不过, runtime.Scheme 也不是万能的。它有从 kind types 的映射,但如果不是 kind ,只知道资源名称怎么办?

这就是 RESTMapper 开始的地方:

type RESTMapper interface {
  // KindFor takes a partial resource and returns the single match.  Returns an error if there are multiple matches
  KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error)

  // KindsFor takes a partial resource and returns the list of potential kinds in priority order
  KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error)

  ...
  
  ResourceSingularizer(resource string) (singular string, err error)
}

RESTMapper 也是某种注册表。但是,它维护资源到种类的映射。因此,向映射器提供一个类似的字符串 apps/v1/deployments 会给出 API Groupapps/v1 kind Deployment RESTMapper 也可以处理资源快捷方式和单数化: po, pod, 并且 pods 可以注册为同一资源的别名。

通常情况下,会有一个全局的单例运行时。然而,似乎 apimachery 模块本身试图避免状态——它定义了 RESTMapper Scheme 结构,但没有实例化它们。

与运行时。该方案被 apimachery 模块本身广泛使用, RESTMapper 在内部没有使用,至少目前没有。

字段和标签选择器

字段和标签的类型、创建和匹配逻辑也存在于 apimachinery 模块中。例如,以下是可以使用 k8s.io/apimachinery/pkg/labels 包执行的操作:

lbl := labels.Set{"foo": "bar"}
sel, _ = labels.Parse("foo==bar")
if sel.Matches(lbl) {
  fmt.Printf("Selector %v matched label set %v\n", sel, lbl)
}

:book:例子:

如何使用标签集和标签选择器

http://github.com/iximiuz/client-go-examples/tree/main/label-selectors

如何使用字段集和字段选择器

http://github.com/iximiuz/client-go-examples/tree/main/field-selectors

API 错误处理

如果不正确处理其错误,就不可能在代码中使用 Kubernetes API API 服务器可能完全消失,请求可能未经授权,对象可能丢失,并发更新可能发生冲突。幸运的是,该 k8s.io/apimachinery/pkg/api/errors 包定义了一些方便的实用程序函数来处理 API 错误。这是一个例子:

_, err = client.
  CoreV1().
  ConfigMaps("default").
  Get(
    context.Background(),
    "this_name_definitely_does_not_exist",
    metav1.GetOptions{},
  )
if !errors.IsNotFound(err) {
  panic(err.Error())
}

:book:示例-如何处理 Kubernetes API 错误。http://github.com/iximiuz/client-go-examples/tree/main/error-handling

util实用程序

最后但并非最不重要的一点是, apimachinery/pkg/util 包装中充满了有用的东西。这里有些例子:

  • util/wait 包通过重试和适当的退避/抖动实现来简化等待资源出现或消失的任务。
  • util/yaml 有助于解组 YAML 或将其转换为 JSON

总结

k8s.io/api k8s.io/apimachinery 包是学习如何在 Go 中使用 Kubernetes 对象的一个很好的起点。如果你需要编写你的第一个控制器,直接跳到 client-go ,甚至是控制器运行时或 kubebuilder 可能会使学习体验过于粗糙——可能存在太多的知识空白。但是,先看看和玩一下 api apimachinery 包将帮助您在接下来的旅程中保持安心

参考地址 [1]

参考资料

[1]

参考地址: http://iximiuz.com/en/posts/kubernetes-api-go-types-and-common-machinery/