概述

在上一篇 GVK 和 GVR 中,我演示了如何通过 HTTP API 来访问操作 Kubernetes 的资源,如果我们愿意,完全可以用代码来做这些操作,从而将 Kubernetes CRD 的操作集成进我们的项目中。但是,事实上,这种工作繁琐且复杂,所以,不妨先看看是否有现成的工具可以利用。

在 Go 编程语言中,官方钦定了两个 Library,分别是 Client-GoApi Machinery,其中 Client-Go 可以用来 CRUD Kubernetes 的资源,而 Api Machinery 的功能更多是提供 Object 辅助,因为它定义了很多 Kubernetes 中的对象结构。后面我就以 Client Go 作为客户端,进行 Kubernetes 的操作演示。

为了让大家了解一下其实 Client Go 中有有类型和无类型两种访问方式,这里会提一下所谓的 Dynamic Client,也就是获取资源,返回的不是我们期望中可以直接操作属性的资源对象,而是一个 Map[string]interface{},这样就需要我们自己去定义资源结构,并且进行转换。

当然,既然有不那么友好的,也有友好的 Typed Client,也就是资源的结构都是可以通过属性进行访问的,这个是我要着重介绍的部分。

Dynamic 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 Clients

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 个部分,分别是:

其中,前两个字段都是各种资源通用的,无论是内置资源还是 CRD,都是这样的。而 Spec 是每个资源自己特定的部分,表示该资源的特色的业务,而 Status 用于在运行时保存资源的状态用的。

了解了这些之后,通过 SDK 访问 Kubernetes 的资源也就心中有底了。

Typed Client 的使用

其实使用 Typed Client 和 Dynamic Client 的过程差不多,基本上也是两步:

1. 构造 Client

这里标红的部分就是和 Dynamic Client 不一样的地方,这里构造的是两种不同的 Client,其他部分都是类似的。

2. 操作资源

这里操作资源就变得很简单了,直接通过各种对象一路调用下去,最后拿到资源之后可以直接使用(标红部分就是直接使用的例子)。

CRD 如何使用 Typed Client

从 Typed Client 中的例子可以看到,核心资源都被包含在 SDK 中了,但是,对于我们在第一篇中创建的 CRD 就只写了一个 YAML 的声明描述文件,也没有写代码,那么,在代码层面该如何使用它呢?

对于 CRD,如果想通过 Client-Go Typed Client 的方式来操作的话,那么第一步,是得我们先定义 CRD 的 Go 数据结构,也就是得根据我们在前面看到得 Deployment 的 Struct 那样,先写一个自己的 CRD 的 Struct:

这里因为后续需要用一个工具来自动生成代码,所以需要写的代码还复杂一些,整个目录结构应该是这样的:

  1. [[email protected].io]# tree
  2. ├── apis
  3. └── admin
  4. └── v1
  5. ├── doc.go
  6. ├── register.go
  7. └── typed.go

除了定义 CRD 的 Go Struct 之外,还编写 doc.go 和 register.go,他们的作用分别是:

你按照我示例中的 register.go 写应该会报错,但是,没关系,那是因为还没有生成完整的代码的问题,后面生成之后就可以了:

  1. [[email protected].io]# cat register.go
  2. // Define your schema name and the version
  3. var SchemeGroupVersion = schema.GroupVersion{
  4. Group: "admin.liqiang.io",
  5. Version: "v1",
  6. }
  7. var (
  8. SchemeBuilder runtime.SchemeBuilder
  9. localSchemeBuilder = &SchemeBuilder
  10. AddToScheme = localSchemeBuilder.AddToScheme
  11. )
  12. func init() {
  13. // We only register manually written functions here. The registration of the
  14. // generated functions takes place in the generated files. The separation
  15. // makes the code compile even when the generated files are missing.
  16. localSchemeBuilder.Register(addKnownTypes)
  17. }
  18. // Resource takes an unqualified resource and returns a Group qualified GroupResource
  19. func Resource(resource string) schema.GroupResource {
  20. return SchemeGroupVersion.WithResource(resource).GroupResource()
  21. }
  22. // Adds the list of known types to the given scheme.
  23. func addKnownTypes(scheme *runtime.Scheme) error {
  24. scheme.AddKnownTypes(
  25. SchemeGroupVersion,
  26. &Admin{},
  27. &AdminList{},
  28. )
  29. scheme.AddKnownTypes(
  30. SchemeGroupVersion,
  31. &metav1.Status{},
  32. )
  33. metav1.AddToGroupVersion(
  34. scheme,
  35. SchemeGroupVersion,
  36. )
  37. return nil
  38. }

编写完这三个文件之后,就可以通过 Kubernetes 的官方工具:Code Generator 来生成可以直接使用的 ClientSet(可以以 Typed Client 和 API Server 交互的 Client) 了。因为我的代码项目使用了 Go Module,所以我是通过 Docker 来生成的:

  1. [[email protected].io]# cat gen.sh
  2. ...
  3. cmd="./generate-groups.sh all \
  4. "$PROJECT_MODULE/pkg/client" \
  5. "$PROJECT_MODULE/pkg/apis" \
  6. $CUSTOM_RESOURCE_NAME:$CUSTOM_RESOURCE_VERSION"
  7. ...

这就是创建的命令,当成功运行完之后,就可以看到新的目录结构:

  1. [[email protected].io]# tree
  2. ├── apis
  3. └── admin
  4. └── v1
  5. ├── doc.go
  6. ├── register.go
  7. ├── typed.go
  8. └── zz_generated.deepcopy.go
  9. ├── client
  10. ├── clientset
  11. └── ... ...
  12. ├── informers
  13. └── ... ....
  14. └── listers
  15. └── ... ...

然后使用起来也是老套路了,关键在于初始化客户端的时候千万别忘了要使用自己的客户端:

注意,这里的代码的 import 是:

  1. [[email protected].io]# head main.go
  2. ...
  3. import (
  4. "github.com/liuliqiang/blog-demos/kubernetes/crds/chap03/code-gen/client/clientset/versioned"
  5. ...

然后使用起来就非常舒服了,下面就尝试获取 CRD 的属性看看:

然后执行一下代码:

  1. [[email protected].io]# go run main.go
  2. 2019/11/25 00:10:41 [INFO]get a admin password is: password

可以发现一切工作正常。好的,这就是关于如何使用 Go 来操作 CRD 的简单示例,可以从这里看出我们的操作轨迹是先定义 CRD 的描述文件,然后再写 Go 的 Struct,以及一系列的代码,不是很方便,这个问题在后面写其他工具的时候会得到良好的解决。

Reference