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


【编者的话】本文带你打开Kubernetes API的探索之旅的正确姿势,快来一睹为快吧!

官方的Kubernetes Go客户端装载了高级抽象——ClientsetInformersCacheSchemeDiscovery,哦,天哪!当我尝试在没有学习移动部分的情况下使用它时,我遇到了大量的新概念。这是一次不愉快的经历,但更重要的是,它削弱了我在代码中做出明智决定的能力。

因此,我决定通过对客户端组件的彻底研究来解开这个谜。

但是从哪里开始呢?在剖析client-go本身之前,了解它的两个主要依赖项可能是一个好主意,k8s.io/apik8s.io/apimachinery模块。这将简化主要任务,但这不是唯一的好处。这两个模块被分离出来是有原因的——它们不仅可以被客户端使用,也可以被服务器端使用,或者被处理Kubernetes对象的任何其他软件使用。
01.png

API资源、类和对象

首先,快速回顾一下。熟悉以下概念对进一步讨论的成功至关重要:
  • 资源类型——一个由Kubernetes API端点服务的实体:Pod、Deployment、ConfigMap等。
  • API组——资源类型被组织成版本化的逻辑组:apps/v1、batch/v1、storage.k8s.io/v1beta1等等。
  • 对象——一个资源实例——每个API端点都处理特定资源类型的对象。
  • 类——API返回或接受的每个对象都必须符合一个对象模式——由其类型定义的属性的特定组合Pod、Deployment、ConfigMap等。


同样重要的是要区分广义对象和Kubernetes的“一级”对象——像Pod、Service或Secret这样的持久实体,它们作为集群的意图的记录。虽然为了序列化和反序列化,每个API对象都必须有一个API版本和类型属性,但并不是每个API对象都是“一级”Kubernetes对象。
02.png

k8s.io/api模块

Go是一种静态类型的编程语言。那么,与Pod、ConfigMap、Secret和其他一级Kubernetes对象对应的所有结构在哪里呢?对,在k8s.io/api。

尽管命名松散,k8.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对象,就像上面的部署一样,还为它们的内部属性定义了许多辅助类型:
// 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"`

...


Kubernetes中定义的所有结构k8s.io/api模块自带JSON和Protobuf注解。但要注意:
  • 支持将数据编组成JSON。
  • Protobuf序列化是不被鼓励的——产生的结果可能与现有的API服务器不兼容(更多信息请参阅README)。



专业提示:如果你去阅读源码,你会看到k8s.io/apimachery通过对提供的对象调用标准的json.marshal()来实现JSON的序列化。因此,不要害怕,只要需要转储API对象,就使用json.Marshal()。
总结一下,k8s.io/api模块:
  • 巨大——1000个以上的结构描述Kubernetes API对象。
  • 简单——几乎没有算法,只有“哑”的数据结构。
  • 有用——它的数据类型被客户端、服务器、控制器等使用。


k8s.io/apimachinery模块

不像简单的k8s.io/api模块,k8s.io/apimachery模块是相当复杂的。README将其目的描述为:

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

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

有用的结构和接口

k8s.io/api模块专注于具体的高级类型,如Deployment、Secret、Pod,k8s.io/apimachery是低层但更通用的数据结构。

例如,Kubernetes对象的所有这些公共属性:apiVersion、kind、name、uid、ownerReferences、creationTimestamp等。如果我要构建自己的Kubernetes自定义资源,我就不需要自己为这些属性定义数据类型——这要感谢apimachery模块。

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

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

在apimachery模块中定义的另一个方便的类型是接口runtime.Object。由于其简单的定义,它可能看起来毫无用处:
// pkg/runtime

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


但实际上,它被用得很多!Kubernetes的代码是在Go获得真正泛型支持之前很久编写的。因此,runtime.Object很像传统的接口——它是一个泛型接口,在代码库中广泛地进行类型断言和类型切换。而实际的类型可以通过检查底层对象的类型来获得。


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


注意,虽然看起来相似,但meta.Object不能安全地向下转换到相应的Kubernetes对象,因为它的结构偏移量不为零。
更有用的apimachery类型:
  • PartialObjectMetadata结构——meta.TypeMeta和meta.ObjectMeta作为一种通用的方法来表示任何具有元数据的对象。
  • APIVersions、APIGroupList、APIGroup结构体——还记得kubectl get的API探索练习吗?原始API这些和类似的结构用于Kubernetes,API资源的类型,但不是Kubernetes对象(例如,它们有kind和apiVersion属性,但没有真正的Object元数据)。
  • GetOptions、ListOptions、UpdateOptions等等——这些结构体代表了客户端对资源的相应动作的参数。
  • GroupKind、GroupVersionKind、GroupResource、GroupVersionResource等——简单的数据传输对象,包含组、版本、类型或资源字符串的元组。


在讨论Scheme和RESTMapper之前,请记住GroupVersionKind和GroupVersionResource——他们的知识将派上用场。

非结构化的结构

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

使用固定k8s.io/api类型处理Kubernetes对象很方便,但如果:
  • 你需要以通用的方式使用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序列化/反序列化。

示例:https://github.com/iximiuz/cli ... 23L36

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

自然的,需要将非结构化对象转换为具体k8s.io/api类型(反之亦然)。runtime.UnstructuredConverter接口及其默认实现DefaultUnstructuredConverter可以帮助你:
type UnstructuredConverter interface {
ToUnstructured(obj interface{}) (map[string]interface{}, error)
FromUnstructured(u map[string]interface{}, obj interface{}) error


示例:https://github.com/iximiuz/cli ... typed

对象序列化为JSON、YAML或Protobuf

在处理来自静态类型语言的API时,另一项乏味的任务是将数据结构编组和解组到它们的连线表示中。

大量的apimachery代码都用于此任务:
// 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


注意到上面的代码片段中的这些对象了吗?是的,这些是runtime.Object,也就是Kind-able接口实例。

例子:
  1. https://github.com/iximiuz/cli ... -json
  2. https://github.com/iximiuz/cli ... -yaml
  3. https://github.com/iximiuz/cli ... -json
  4. https://github.com/iximiuz/cli ... -yaml


模式和RESTMapper

runtime.Schema在使用client-go时,模式概念随处出现,特别是在编写处理自定义资源的控制器(或操作符)时。

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

考虑一下非结构化到类型化转换的潜在实现:有一个类似json的对象,以及一些具体k8s.io/api类型需要从它创建。也许,第一步就是要弄清楚如何使用kind字符串创建一个空的类型化对象实例。

一个简单的方法可能看起来像一个巨大的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{}
}
...


更聪明的方法是使用反射。不是开关,而是映射[字符串]反射。类型可以为所有注册类型维护:
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转换为一个类型化对象。第一步——对象创建——将非常类似。

事实证明,通过API组和类型创建空对象是一项非常频繁的任务,以至于它在apimachery模块——运行时中获得了自己的模块——runtime.Schema:
// 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.Scheme结构就是这样一个注册表,它包含了所有Kubernetes对象的kind到type和type到kind的映射。


记住,GroupVersionKind只是一个元组,即DTO结构,对吗?
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.Schema不是万能的。它有从kind到type的映射,但是如果不是只有资源名已知而不是类型呢?

这就是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/Deployment这样的字符串,就会得到API Group apps/v1和部署类型。RESTMapper还可以处理资源快捷方式和奇点化: po、pod和pods可以注册为相同资源的别名。
04.png

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

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

字段和标签选择器

字段和标签的类型、创建和匹配逻辑也存在于apimachery模块中。例如,这里是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)


例子:
  1. https://github.com/iximiuz/cli ... ctors
  2. https://github.com/iximiuz/cli ... ctors


API错误处理

在代码中使用Kubernetes API是不可能的,除非正确处理它的错误。API服务器可能完全消失,请求可能未经授权,对象可能丢失,并发更新可能发生冲突。幸运的是,k8s.io/apimachery /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())


示例:https://github.com/iximiuz/cli ... dling

其他

最后但并非最不重要的是,apimachery/pkg/util包充满了有用的东西。下面是一些例子:
  • util/wait包通过重试和适当的backoff/jitter实现,减轻了等待资源出现或消失的任务。
  • util/yaml有助于对yaml进行反序列化或将其转换为JSON。


总结

k8s.io/api和k8s.io/apimachery包是学习如何在Go中使用Kubernetes对象的一个很好的起点。如果你需要编写你的第一个控制器,直接跳到client-go,甚至跳到controller-runtime或kubebuilder可能会让你的学习经历变得太复杂——可能会有太多的知识缺口。不过,先看看API和apimachery包,然后再尝试一下,这将帮助你在接下来的旅程中保持平和的心态。

请继续关注

已经有三篇文章了,我还没接触过客户端。下次,我保证会是一篇关于客户端的文章!

原文链接:How To Call Kubernetes API using Go - Types and Common Machinery

译者:Mr.lzc,高级工程师、DevOpsDays、HDZ深圳核心组织者,目前供职于华为,从事云计算工作,专注于Kubernetes、微服务领域。

0 个评论

要回复文章请先登录注册