第三方应用如何调用我们 kubebuilder 生成的自定义资源?
注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用
注:本文所有示例代码都可以在 blog-code 仓库中找到
kubebuilder 能否生成类似 client-go 的 sdk?
在去年写的系列文章中,我们完整的实现了 operator 开发过程中涉及到的绝大部分要素,但是在实际的生产应用中我们定义的 CR(CustomResource) 就像 k8s 自带的 deployment、pod 等资源一样,会存在其他服务直接调用 api-server 接口进行创建更新的需求,而不仅仅只是通过 kubectl 编辑yaml
那么 k8s 自带的对象我们可以通过 client-go 进行调用,我们自己设计的 CR 能否直接生成类似的 SDK 呢?
这个问题在 kubebuilder 社区从 v1 - v2 版本都有用户在提,但是 kubebuilder 官方似乎不太赞同生成 sdk 的这种做法
- https://github.com/kubernetes-sigs/kubebuilder/issues/403
- https://github.com/kubernetes-sigs/kubebuilder/issues/1152
目前找到以下几种方案
方案 | 优点 | 缺点 |
---|---|---|
通过 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 项目中找到
通过 client-go 调用
如下所示可以看到,代码整体来说相对比较复杂,dynamic
包生成的 client 是一个通用的 client,所以他只能获取到 k8s 的一些通用的 metadata 数据,如果想要获取到 CR 的结构化数据就只能通过 json 来进行转换
1 |
|
执行代码可以获取到正确的结果
1 |
|
简单看一下源码,可以看到实际上 Resource
方法就是返回了 NamespaceableResourceInterface
接口,这个接口支持了 Namespace 以及非 Namespace 级别的资源的 CURD
等访问方法
1 |
|
上面的这些方法返回的都是 *unstructured.Unstructured
类型的数据,这个类型本质上就是把 object 通过 map 保存了下来,然后提供了 GetNamespace
等便捷的方法给用户使用
1 |
|
通过 controller-runtime 调用
如下所示,可以发现 controller-runtime 的代码明显要比上一种方式要简洁一些,不需要手动去 json 编码解码了,基础的 scheme 数据也可以直接使用生成好的数据
1 |
|
执行测试一下
1 |
|
同样简单看下接口,controller-runtime 的 client 是多个接口组合而来的,合并在一起之后其实和上面 client-go 的接口大差不差
1 |
|
生成 clientset 调用
生成 clientset
我们使用 code-generator 的 client-gen 子项目来生成客户端的调用,使用这个方法我们需要对代码做很多的调整
项目结构调整,kubebuilder 生成的 api 目录是
api/v1
,但是 client-gen 要求的目录结构是api/${group}/${version}
。所以我们需要将目录结构调整为
api/job/v1
,调整后记得修改原有代码的依赖路径修改
PROJECT
文件,这个文件用于 kubebuilder 记录,修改里面的 path 路径1
2
3
4
5
6resources:
# ... 删除掉不需要关注的部分
- path: github.com/mohuishou/blog-code/02-k8s-operator/operator-kubebuilder-clientset/api/v1
+ path: github.com/mohuishou/blog-code/02-k8s-operator/operator-kubebuilder-clientset/api/job/v1
version: v1
version: "3"
给需要生成 sdk 的资源加上
// +genclient
注释,如下所示,放在//+kubebuilder:object:root=true
前面即可1
2
3
4
5
6
7
8
9
10
11
12
13
//+genclient
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// Test is the Schema for the tests API
type Test struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec TestSpec `json:"spec,omitempty"`
Status TestStatus `json:"status,omitempty"`
}api 新增
SchemeGroupVersion
全局变量,修改api/job/v1/groupversion_info.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14var (
// GroupVersion is group version used to register these objects
GroupVersion = schema.GroupVersion{Group: "job.lailin.xyz", Version: "v1"}
// SchemeGroupVersion for clien-gen
SchemeGroupVersion = GroupVersion
// SchemeBuilder is used to add go types to the GroupVersionKind scheme
SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
// AddToScheme adds the types in this group-version to the given scheme.
AddToScheme = SchemeBuilder.AddToScheme
)添加
code-generator
依赖,注意code-generator
版本一定要和你的client-go
版本一致例如在我们的测试项目里面
client-go
的版本是 v0.25.0 那我们执行1
go get k8s.io/code-generator@v0.25.0
由于我们的项目内实际上并没有依赖
code-generator
,所以我们需要添加一个文件依赖这个项目,我们新建一个hack/code_generator.go
文件,我们加上go:build tools
标签确保在编译应用的时候不会将这个依赖编译进去1
2
3
4
5
6//go:build tools
// +build tools
package hack
import _ "k8s.io/code-generator"然后我们执行
go mod tidy
编写代码生成脚本,会将 clientset 放到 pkg 目录下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41#!/bin/bash
set -e
set -x
# 生成 clientset 代码
# 获取 go module name
go_module=$(go list -m)
# crd group
group=${GROUP:-"job"}
# api 版本
api_version=${API_VERSION:-"v1"}
project_dir=$(cd $(dirname ${BASH_SOURCE[0]})/..; pwd) # 项目根目录
# check generate-groups.sh is exist
# 直接下载 generate-groups.sh 脚本,这个脚本还可以生成其他类型的代码,但是我们这里只用来生成 client 的代码
if [ ! -f "$project_dir/hack/generate-groups.sh" ]; then
echo "hack/generate-groups.sh is not exist, download"
wget -O "$project_dir/hack/generate-groups.sh" https://raw.githubusercontent.com/kubernetes/code-generator/master/generate-groups.sh
chmod +x $project_dir/hack/generate-groups.sh
fi
# 生成 clientset
# 脚本文档可以查看 https://raw.githubusercontent.com/kubernetes/code-generator/master/generate-groups.sh
CLIENTSET_NAME_VERSIONED="$api_version" \
$project_dir/hack/generate-groups.sh client \
$go_module/pkg $go_module/api "$group:$api_version" --output-base $project_dir/
if [ ! -d "$project_dir/pkg" ];then
mkdir $project_dir/pkg
fi
# 生成的 clientset 的文件夹路径会包含 $go_module/pkg 所以我们需要把这个文件夹复制出来
rm -rf $project_dir/pkg/clientset
mv -f $project_dir/$go_module/pkg/* $project_dir/pkg/
# 删除不需要的目录
rm -rf $project_dir/$(echo $go_module | cut -d '/' -f 1)执行
bash hack/gen-client.sh
生成代码,生成的目录结构如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23❯ tree pkg/clientset
pkg/clientset
└── v1
├── clientset.go
├── doc.go
├── fake
│ ├── clientset_generated.go
│ ├── doc.go
│ └── register.go
├── scheme
│ ├── doc.go
│ └── register.go
└── typed
└── job
└── v1
├── doc.go
├── fake
│ ├── doc.go
│ ├── fake_job_client.go
│ └── fake_test.go
├── generated_expansion.go
├── job_client.go
└── test.go生成的客户端接口如下所示,我们可以看到和上面两种方式的主要区别就是指定了类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19// TestsGetter has a method to return a TestInterface.
// A group's client should implement this interface.
type TestsGetter interface {
Tests(namespace string) TestInterface
}
// TestInterface has methods to work with Test resources.
type TestInterface interface {
Create(ctx context.Context, test *v1.Test, opts metav1.CreateOptions) (*v1.Test, error)
Update(ctx context.Context, test *v1.Test, opts metav1.UpdateOptions) (*v1.Test, error)
UpdateStatus(ctx context.Context, test *v1.Test, opts metav1.UpdateOptions) (*v1.Test, error)
Delete(ctx context.Context, name string, opts metav1.DeleteOptions) error
DeleteCollection(ctx context.Context, opts metav1.DeleteOptions, listOpts metav1.ListOptions) error
Get(ctx context.Context, name string, opts metav1.GetOptions) (*v1.Test, error)
List(ctx context.Context, opts metav1.ListOptions) (*v1.TestList, error)
Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions, subresources ...string) (result *v1.Test, err error)
TestExpansion
}
调用 clientset
可以看到 clientset 的代码是最简洁的
1 |
|
执行结果如下
1 |
|
总结
这三种调用方式其实各有优劣,kubebuilder 官方比较推荐直接使用 controller-runtime,但是另外两种方式也有各自的使用场景,client-go 这种方式通用性最强,不用依赖 operator 开发者的代码,clientset 的定制性最强,对于使用方来说也最方便
对于我而言其实最开始只了解到 client-go 和 clientset 这两种方式,所以之前一直都是使用的 clientset 这种方式,这次这篇文章的初衷其实也只是为了记录一下 clientset 的最小化配置方法,但是在资料汇总的过程中发现了 controller-runtime 这种方法,作为 operator 的开发者最后选择使用 controller-runtime,因为生成 clientset 需要改动的东西实在是太多了,而且很容易出错。controller-runtime 在易用性和通用性都有不错的表现
参考文献
- https://github.com/kubernetes-sigs/kubebuilder/issues/403
- https://github.com/kubernetes-sigs/kubebuilder/issues/1152
- controller-runtime/pkg/client
- client-go/dynamic
- client-gen
- 结合Kubebuilder与code-generator开发Operator
- 在不生成 crd client 代码的情况下通过 client-go 增删改查 k8s crd 资源
关注我获取更新
猜你喜欢
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处,禁止全文转载