注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
在去年写的系列文章中,我们完整的实现了 operator 开发过程中涉及到的绝大部分要素,但是在实际的生产应用中我们定义的 CR(CustomResource) 就像 k8s 自带的 deployment、pod 等资源一样,会存在其他服务直接调用 api-server 接口进行创建更新的需求,而不仅仅只是通过 kubectl 编辑yaml
那么 k8s 自带的对象我们可以通过 client-go 进行调用,我们自己设计的 CR 能否直接生成类似的 SDK 呢?
这个问题在 kubebuilder 社区从 v1 - v2 版本都有用户在提,但是 kubebuilder 官方似乎不太赞同生成 sdk 的这种做法
目前找到以下几种方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| 通过 client-gen 生成对应的 sdk | 调用方使用起来会更加的方便,毕竟是静态代码,不容易出错 | 对于 operator 的开发者来说比较麻烦,因为要通过这个工具生成对应的代码还需要做很多其他的事情,甚至需要调整 kubebuiler 生成的代码结构 客制化较强,通用性较弱,每个 CR 都需要单独生成 |
| controller-runtime/pkg/client | 调用也比较方便 通用性强,只需要将 kubebuilder 生成好的 CR 定义暴露出去即可 | 相对于通过 client-gen 来说静态代码检查的能力相对较弱 |
| client-go/dynamic | 通用性极强,甚至可以不用 Operator 开发中提供对应的 CR 定义代码 | 调用方来说极其不方便,需要自定义很多东西,并且需要反复进行序列化操作 |
接下来我们就自定义一个简单的 CR,这个 CR 没有任何的逻辑,只是为了用来验证客户端调用,关于 kubebuilder 生成 CR 如果不是特别清楚,可以阅读之前的这篇文章: kubebuilder 简明教程
1 | |
如上所示这个 CR 只有一个 foo 字段,也就是 kubebuilder 初始化的一个字段,除此之外什么也没有
接下来我都以 get 数据为例来分别说明这三种方式的基本使用方法,下面的示例代码可以在 operator-kubebuilder-clientset 项目中找到
如下所示可以看到,代码整体来说相对比较复杂,dynamic 包生成的 client 是一个通用的 client,所以他只能获取到 k8s 的一些通用的 metadata 数据,如果想要获取到 CR 的结构化数据就只能通过 json 来进行转换
1 | |
执行代码可以获取到正确的结果
1 | |
简单看一下源码,可以看到实际上 Resource 方法就是返回了 NamespaceableResourceInterface 接口,这个接口支持了 Namespace 以及非 Namespace 级别的资源的 CURD 等访问方法
1 | |
上面的这些方法返回的都是 *unstructured.Unstructured 类型的数据,这个类型本质上就是把 object 通过 map 保存了下来,然后提供了 GetNamespace 等便捷的方法给用户使用
1 | |
如下所示,可以发现 controller-runtime 的代码明显要比上一种方式要简洁一些,不需要手动去 json 编码解码了,基础的 scheme 数据也可以直接使用生成好的数据
1 | |
执行测试一下
1 | |
同样简单看下接口,controller-runtime 的 client 是多个接口组合而来的,合并在一起之后其实和上面 client-go 的接口大差不差
1 | |
我们使用 code-generator 的 client-gen 子项目来生成客户端的调用,使用这个方法我们需要对代码做很多的调整
项目结构调整,kubebuilder 生成的 api 目录是 api/v1,但是 client-gen 要求的目录结构是 api/${group}/${version} 。
所以我们需要将目录结构调整为 api/job/v1,调整后记得修改原有代码的依赖路径
修改 PROJECT 文件,这个文件用于 kubebuilder 记录,修改里面的 path 路径
1 | |
给需要生成 sdk 的资源加上 // +genclient 注释,如下所示,放在 //+kubebuilder:object:root=true 前面即可
1 | |
api 新增 SchemeGroupVersion 全局变量,修改 api/job/v1/groupversion_info.go
1 | |
添加 code-generator 依赖,注意 code-generator 版本一定要和你的 client-go 版本一致
例如在我们的测试项目里面 client-go 的版本是 v0.25.0 那我们执行
1 | |
由于我们的项目内实际上并没有依赖 code-generator ,所以我们需要添加一个文件依赖这个项目,我们新建一个 hack/code_generator.go 文件,我们加上 go:build tools 标签确保在编译应用的时候不会将这个依赖编译进去
1 | |
然后我们执行 go mod tidy
编写代码生成脚本,会将 clientset 放到 pkg 目录下
1 | |
执行 bash hack/gen-client.sh 生成代码,生成的目录结构如下
1 | |
生成的客户端接口如下所示,我们可以看到和上面两种方式的主要区别就是指定了类型
1 | |
可以看到 clientset 的代码是最简洁的
1 | |
执行结果如下
1 | |
这三种调用方式其实各有优劣,kubebuilder 官方比较推荐直接使用 controller-runtime,但是另外两种方式也有各自的使用场景,client-go 这种方式通用性最强,不用依赖 operator 开发者的代码,clientset 的定制性最强,对于使用方来说也最方便
对于我而言其实最开始只了解到 client-go 和 clientset 这两种方式,所以之前一直都是使用的 clientset 这种方式,这次这篇文章的初衷其实也只是为了记录一下 clientset 的最小化配置方法,但是在资料汇总的过程中发现了 controller-runtime 这种方法,作为 operator 的开发者最后选择使用 controller-runtime,因为生成 clientset 需要改动的东西实在是太多了,而且很容易出错。controller-runtime 在易用性和通用性都有不错的表现
注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
在去年写的系列文章中,我们完整的实现了 operator 开发过程中涉及到的绝大部分要素,但是在实际的生产应用中我们定义的 CR(CustomResource) 就像 k8s 自带的 deployment、pod 等资源一样,会存在其他服务直接调用 api-server 接口进行创建更新的需求,而不仅仅只是通过 kubectl 编辑yaml
那么 k8s 自带的对象我们可以通过 client-go 进行调用,我们自己设计的 CR 能否直接生成类似的 SDK 呢?
这个问题在 kubebuilder 社区从 v1 - v2 版本都有用户在提,但是 kubebuilder 官方似乎不太赞同生成 sdk 的这种做法
目前找到以下几种方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| 通过 client-gen 生成对应的 sdk | 调用方使用起来会更加的方便,毕竟是静态代码,不容易出错 | 对于 operator 的开发者来说比较麻烦,因为要通过这个工具生成对应的代码还需要做很多其他的事情,甚至需要调整 kubebuiler 生成的代码结构 客制化较强,通用性较弱,每个 CR 都需要单独生成 |
| controller-runtime/pkg/client | 调用也比较方便 通用性强,只需要将 kubebuilder 生成好的 CR 定义暴露出去即可 | 相对于通过 client-gen 来说静态代码检查的能力相对较弱 |
| client-go/dynamic | 通用性极强,甚至可以不用 Operator 开发中提供对应的 CR 定义代码 | 调用方来说极其不方便,需要自定义很多东西,并且需要反复进行序列化操作 |
接下来我们就自定义一个简单的 CR,这个 CR 没有任何的逻辑,只是为了用来验证客户端调用,关于 kubebuilder 生成 CR 如果不是特别清楚,可以阅读之前的这篇文章: kubebuilder 简明教程
1 | |
如上所示这个 CR 只有一个 foo 字段,也就是 kubebuilder 初始化的一个字段,除此之外什么也没有
接下来我都以 get 数据为例来分别说明这三种方式的基本使用方法,下面的示例代码可以在 operator-kubebuilder-clientset 项目中找到
如下所示可以看到,代码整体来说相对比较复杂,dynamic 包生成的 client 是一个通用的 client,所以他只能获取到 k8s 的一些通用的 metadata 数据,如果想要获取到 CR 的结构化数据就只能通过 json 来进行转换
1 | |
执行代码可以获取到正确的结果
1 | |
简单看一下源码,可以看到实际上 Resource 方法就是返回了 NamespaceableResourceInterface 接口,这个接口支持了 Namespace 以及非 Namespace 级别的资源的 CURD 等访问方法
1 | |
上面的这些方法返回的都是 *unstructured.Unstructured 类型的数据,这个类型本质上就是把 object 通过 map 保存了下来,然后提供了 GetNamespace 等便捷的方法给用户使用
1 | |
如下所示,可以发现 controller-runtime 的代码明显要比上一种方式要简洁一些,不需要手动去 json 编码解码了,基础的 scheme 数据也可以直接使用生成好的数据
1 | |
执行测试一下
1 | |
同样简单看下接口,controller-runtime 的 client 是多个接口组合而来的,合并在一起之后其实和上面 client-go 的接口大差不差
1 | |
我们使用 code-generator 的 client-gen 子项目来生成客户端的调用,使用这个方法我们需要对代码做很多的调整
项目结构调整,kubebuilder 生成的 api 目录是 api/v1,但是 client-gen 要求的目录结构是 api/${group}/${version} 。
所以我们需要将目录结构调整为 api/job/v1,调整后记得修改原有代码的依赖路径
修改 PROJECT 文件,这个文件用于 kubebuilder 记录,修改里面的 path 路径
1 | |
给需要生成 sdk 的资源加上 // +genclient 注释,如下所示,放在 //+kubebuilder:object:root=true 前面即可
1 | |
api 新增 SchemeGroupVersion 全局变量,修改 api/job/v1/groupversion_info.go
1 | |
添加 code-generator 依赖,注意 code-generator 版本一定要和你的 client-go 版本一致
例如在我们的测试项目里面 client-go 的版本是 v0.25.0 那我们执行
1 | |
由于我们的项目内实际上并没有依赖 code-generator ,所以我们需要添加一个文件依赖这个项目,我们新建一个 hack/code_generator.go 文件,我们加上 go:build tools 标签确保在编译应用的时候不会将这个依赖编译进去
1 | |
然后我们执行 go mod tidy
编写代码生成脚本,会将 clientset 放到 pkg 目录下
1 | |
执行 bash hack/gen-client.sh 生成代码,生成的目录结构如下
1 | |
生成的客户端接口如下所示,我们可以看到和上面两种方式的主要区别就是指定了类型
1 | |
可以看到 clientset 的代码是最简洁的
1 | |
执行结果如下
1 | |
这三种调用方式其实各有优劣,kubebuilder 官方比较推荐直接使用 controller-runtime,但是另外两种方式也有各自的使用场景,client-go 这种方式通用性最强,不用依赖 operator 开发者的代码,clientset 的定制性最强,对于使用方来说也最方便
对于我而言其实最开始只了解到 client-go 和 clientset 这两种方式,所以之前一直都是使用的 clientset 这种方式,这次这篇文章的初衷其实也只是为了记录一下 clientset 的最小化配置方法,但是在资料汇总的过程中发现了 controller-runtime 这种方法,作为 operator 的开发者最后选择使用 controller-runtime,因为生成 clientset 需要改动的东西实在是太多了,而且很容易出错。controller-runtime 在易用性和通用性都有不错的表现