在上一篇 GVK 和 GVR 中,我演示了如何通过 HTTP API 来访问操作 Kubernetes 的资源,如果我们愿意,完全可以用代码来做这些操作,从而将 Kubernetes CRD 的操作集成进我们的项目中。但是,事实上,这种工作繁琐且复杂,所以,不妨先看看是否有现成的工具可以利用。
在 Go 编程语言中,官方钦定了两个 Library,分别是 Client-Go 和 Api Machinery,其中 Client-Go 可以用来 CRUD Kubernetes 的资源,而 Api Machinery 的功能更多是提供 Object 辅助,因为它定义了很多 Kubernetes 中的对象结构。后面我就以 Client Go 作为客户端,进行 Kubernetes 的操作演示。
为了让大家了解一下其实 Client Go 中有有类型和无类型两种访问方式,这里会提一下所谓的 Dynamic Client,也就是获取资源,返回的不是我们期望中可以直接操作属性的资源对象,而是一个 Map[string]interface{}
,这样就需要我们自己去定义资源结构,并且进行转换。
当然,既然有不那么友好的,也有友好的 Typed Client
,也就是资源的结构都是可以通过属性进行访问的,这个是我要着重介绍的部分。
在 k8s.io/client-go/dynamic 中的客户端对于 GVK 一无所知,它要做的事情就是将 Kubernetes 中的资源转换成 unstructured.Unstructured 结构,而这个结构只提供一个 json.Unmarshal 和它的输出。
这里我就简单演示一下它的效果,首先需要说明的是,一般使用 Kubernetes SDK 都分为两步:
1. 构造 Client
2. 操作资源
这里因为是 Dynamic Client,所以获取的对象没法直接使用,如果想要获取资源的一些属性的话,得通过 unstructured
进行:
从这个例子中可以看出,通过 Dynamic Client 来访问 Kubernetes 的资源还是非常麻烦的。所以,更多的时候,我们使用的都是 Typed Client。
Typed Client 和 Dynamic Client 就不一样了,Typed Client 提供了各种资源的 Golang 的数据结构,这让我们对资源的操作和访问变得很简单,当然,代码的可读性也就更高了。但是,这并不意味着使用 Typed Client 就是什么都是好的,例如,为什么这个 Client 就知道 Kubernetes 的资源类型的 Golang 数据结构呢?内置资源可以理解,毕竟就是固定的东西,那么对于我们关注的 CRD 呢?怎么才能让 Client-Go 也能够理解我们的 CRD,并且支持 Typed 操作呢,这比较复杂,同时是后面的重点内容。
基础知识
在开始使用 Client Go 的正式代码之前,先来点基础知识,在使用 Kubernetes 的过程中,会发现,各种不同的资源的 YAML 描述文件都有很多相似的地方,事实上,他们是有隐含着一个标准结构,首先是他们在 Client Go 中的代码包结构,资源都放置在 group/version.Kind
的包结构中,例如:
这是 Client-Go 中关于内置资源的目录结构,在平时开发的 CRD 中,一般的放置结构也类似。这是包层次的目录结构,接着是看资源级别的结构,就以 daemonset 为例看下它的 Go 代码结构:
这里有 4 个部分,分别是:
TypeMeta
:这一部分就是 GVK 部分了,用于映射 YAML 中的头两行;ObjectMeta
:这一部分是 Resource 的元数据,例如资源名称,ID 之类的;Spec
:这一部分就是每个资源独有的部分,需要资源自定义具体的格式;Status
:这个是在 Kubernetes 运行时使用的字段其中,前两个字段都是各种资源通用的,无论是内置资源还是 CRD,都是这样的。而 Spec
是每个资源自己特定的部分,表示该资源的特色的业务,而 Status
用于在运行时保存资源的状态用的。
了解了这些之后,通过 SDK 访问 Kubernetes 的资源也就心中有底了。
Typed Client 的使用
其实使用 Typed Client 和 Dynamic Client 的过程差不多,基本上也是两步:
1. 构造 Client
这里标红的部分就是和 Dynamic Client 不一样的地方,这里构造的是两种不同的 Client,其他部分都是类似的。
2. 操作资源
这里操作资源就变得很简单了,直接通过各种对象一路调用下去,最后拿到资源之后可以直接使用(标红部分就是直接使用的例子)。
从 Typed Client 中的例子可以看到,核心资源都被包含在 SDK 中了,但是,对于我们在第一篇中创建的 CRD 就只写了一个 YAML 的声明描述文件,也没有写代码,那么,在代码层面该如何使用它呢?
对于 CRD,如果想通过 Client-Go Typed Client 的方式来操作的话,那么第一步,是得我们先定义 CRD 的 Go 数据结构,也就是得根据我们在前面看到得 Deployment 的 Struct 那样,先写一个自己的 CRD 的 Struct:
这里因为后续需要用一个工具来自动生成代码,所以需要写的代码还复杂一些,整个目录结构应该是这样的:
[root@liqiang.io]# tree
├── apis
│ └── admin
│ └── v1
│ ├── doc.go
│ ├── register.go
│ └── typed.go
除了定义 CRD 的 Go Struct 之外,还编写 doc.go 和 register.go,他们的作用分别是:
doc.go
:为所有的类型进行一个 Go Gen 的全局设置register.go
:对 Kubernetes 的 API Server 注册自己,让 API Server 能够理解这个 CRD你按照我示例中的 register.go 写应该会报错,但是,没关系,那是因为还没有生成完整的代码的问题,后面生成之后就可以了:
[root@liqiang.io]# cat register.go
// Define your schema name and the version
var SchemeGroupVersion = schema.GroupVersion{
Group: "admin.liqiang.io",
Version: "v1",
}
var (
SchemeBuilder runtime.SchemeBuilder
localSchemeBuilder = &SchemeBuilder
AddToScheme = localSchemeBuilder.AddToScheme
)
func init() {
// We only register manually written functions here. The registration of the
// generated functions takes place in the generated files. The separation
// makes the code compile even when the generated files are missing.
localSchemeBuilder.Register(addKnownTypes)
}
// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
return SchemeGroupVersion.WithResource(resource).GroupResource()
}
// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
scheme.AddKnownTypes(
SchemeGroupVersion,
&Admin{},
&AdminList{},
)
scheme.AddKnownTypes(
SchemeGroupVersion,
&metav1.Status{},
)
metav1.AddToGroupVersion(
scheme,
SchemeGroupVersion,
)
return nil
}
编写完这三个文件之后,就可以通过 Kubernetes 的官方工具:Code Generator 来生成可以直接使用的 ClientSet(可以以 Typed Client 和 API Server 交互的 Client) 了。因为我的代码项目使用了 Go Module,所以我是通过 Docker 来生成的:
[root@liqiang.io]# cat gen.sh
...
cmd="./generate-groups.sh all \
"$PROJECT_MODULE/pkg/client" \
"$PROJECT_MODULE/pkg/apis" \
$CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"
...
这就是创建的命令,当成功运行完之后,就可以看到新的目录结构:
[root@liqiang.io]# tree
├── apis
│ └── admin
│ └── v1
│ ├── doc.go
│ ├── register.go
│ ├── typed.go
│ └── zz_generated.deepcopy.go
├── client
│ ├── clientset
│ │ └── ... ...
│ ├── informers
│ │ └── ... ....
│ └── listers
│ └── ... ...
然后使用起来也是老套路了,关键在于初始化客户端的时候千万别忘了要使用自己的客户端:
注意,这里的代码的 import 是:
[root@liqiang.io]# head main.go
...
import (
"github.com/liuliqiang/blog-demos/kubernetes/crds/chap03/code-gen/client/clientset/versioned"
...
然后使用起来就非常舒服了,下面就尝试获取 CRD 的属性看看:
然后执行一下代码:
[root@liqiang.io]# go run main.go
2019/11/25 00:10:41 [INFO]get a admin password is: password
可以发现一切工作正常。好的,这就是关于如何使用 Go 来操作 CRD 的简单示例,可以从这里看出我们的操作轨迹是先定义 CRD 的描述文件,然后再写 Go 的 Struct,以及一系列的代码,不是很方便,这个问题在后面写其他工具的时候会得到良好的解决。