如何使用Go调用Kubernetes API-类型和普通机制
【编者的话】本文带你打开Kubernetes API的探索之旅的正确姿势,快来一睹为快吧!
官方的Kubernetes Go客户端装载了高级抽象——
Clientset
、Informers
、Cache
、Scheme
、Discovery
,哦,天哪!当我尝试在没有学习移动部分的情况下使用它时,我遇到了大量的新概念。这是一次不愉快的经历,但更重要的是,它削弱了我在代码中做出明智决定的能力。因此,我决定通过对客户端组件的彻底研究来解开这个谜。
但是从哪里开始呢?在剖析
client-go
本身之前,了解它的两个主要依赖项可能是一个好主意,k8s.io/api
和k8s.io/apimachinery
模块。这将简化主要任务,但这不是唯一的好处。这两个模块被分离出来是有原因的——它们不仅可以被客户端使用,也可以被服务器端使用,或者被处理Kubernetes对象的任何其他软件使用。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对象。
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/api模块:
专业提示:如果你去阅读源码,你会看到k8s.io/apimachery通过对提供的对象调用标准的json.marshal()来实现JSON的序列化。因此,不要害怕,只要需要转储API对象,就使用json.Marshal()。
- 巨大——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接口,可用于以通用方式指向任何兼容对象。
在apimachery模块中定义的另一个方便的类型是接口runtime.Object。由于其简单的定义,它可能看起来毫无用处:
// pkg/runtime
type Object interface {
GetObjectKind() schema.ObjectKind
DeepCopyObject() Object
}
但实际上,它被用得很多!Kubernetes的代码是在Go获得真正泛型支持之前很久编写的。因此,runtime.Object很像传统的接口——它是一个泛型接口,在代码库中广泛地进行类型断言和类型切换。而实际的类型可以通过检查底层对象的类型来获得。
更有用的apimachery类型:
runtime.Object实例可以指向任何具有kind属性的对象——成熟的Kubernetes对象、不携带元数据的更简单的API资源,或者具有定义良好的对象方案的任何其他类型的对象。
注意,虽然看起来相似,但meta.Object不能安全地向下转换到相应的Kubernetes对象,因为它的结构偏移量不为零。
- 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接口实例。
例子:
- https://github.com/iximiuz/cli ... -json
- https://github.com/iximiuz/cli ... -yaml
- https://github.com/iximiuz/cli ... -json
- 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的映射。
runtime.Scheme结构实际上是非常强大的,它有一大堆方法和实现一些基本的接口,如:
记住,GroupVersionKind只是一个元组,即DTO结构,对吗?
// 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可以注册为相同资源的别名。
通常情况下,会有一个全局的单例运行时。然而,似乎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)
}
例子:
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、微服务领域。