
第三方应用如何调用我们 kubebuilder 生成的自定义资源?
作者:lailinxyz 2022-11-16 11:53:38
云计算
云原生 这次这篇文章的初衷其实也只是为了记录一下 clientset 的最小化配置方法,但是在资料汇总的过程中发现了 controller-runtime 这种方法,作为 operator 的开发者最后选择使用 controller-runtime,因为生成 clientset 需要改动的东西实在是太多了,而且很容易出错。controller-runtime 在易用性和通用性都有不错的表现。
kubebuilder 能否生成类似 clie
在去年写的系列文章[1]中,我们完整的实现了 operator 开发过程中涉及到的绝大部分要素,但是在实际的生产应用中我们定义的 CR(CustomResource[2]) 就像 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[3]。
- https://github.com/kubernetes-sigs/kubebuilder/issues/1152[4]
目前找到以下几种方案。
方案 | 优点 | 缺点 |
通过 client-gen[5] 生成对应的 sdk | 调用方使用起来会更加的方便,毕竟是静态代码,不容易出错 | 对于 operator 的开发者来说比较麻烦,因为要通过这个工具生成对应的代码还需要做很多其他的事情,甚至需要调整 kubebuiler 生成的代码结构 客制化较强,通用性较弱,每个 CR 都需要单独生成 |
controller-runtime/pkg/client[6] | 调用也比较方便 通用性强,只需要将 kubebuilder 生成好的 CR 定义暴露出去即可 | 相对于通过 client-gen 来说静态代码检查的能力相对较弱 |
client-go/dynamic[7] | 通用性极强,甚至可以不用 Operator 开发中提供对应的 CR 定义代码 | 调用方来说极其不方便,需要自定义很多东西,并且需要反复进行序列化操作 |
接下来我们就自定义一个简单的 CR,这个 CR 没有任何的逻辑,只是为了用来验证客户端调用,关于 kubebuilder 生成 CR 如果不是特别清楚,可以阅读之前的这篇文章: kubebuilder 简明教程[8]。
apiVersion: job.lailin.xyz/v1
kind: Test
metadata:
labels:
app.kuberentes.io/managed-by: kustomize
app.kubernetes.io/created-by: operator-kubebuilder-clientset
app.kubernetes.io/instance: test-sample
app.kubernetes.io/name: test
app.kubernetes.io/part-of: operator-kubebuilder-clientset
name: test-sample
namespace: default
spec:
foo: test
如上所示这个 CR 只有一个 foo 字段,也就是 kubebuilder 初始化的一个字段,除此之外什么也没有。
接下来我都以 get 数据为例来分别说明这三种方式的基本使用方法,下面的示例代码可以在 operator-kubebuilder-clientset[9] 项目中找到。
通过 client-go 调用
如下所示可以看到,代码整体来说相对比较复杂,dynamic 包生成的 client 是一个通用的 client,所以他只能获取到 k8s 的一些通用的 metadata 数据,如果想要获取到 CR 的结构化数据就只能通过 json 来进行转换。
funcmain() {
cfg, err :=clientcmd.BuildConfigFromFlags("", os.Getenv("HOME")+"/.kube/config")
fatalf(err, "get kube config fail")
// 获取 client
gvr :=schema.GroupVersionResource{
Group: jobv1.GroupVersion.Group,
Version: jobv1.GroupVersion.Version,
Resource: "tests",
}
client :=dynamic.NewForConfigOrDie(cfg).Resource(gvr)
ctx :=context.Background()
res, err :=client.Namespace("default").Get(ctx, "test-sample", v1.GetOptions{})
fatalf(err, "get resource fail")
b, err :=res.MarshalJSON()
fatalf(err, "get json byte fail")
test :=jobv1.Test{}
err=json.Unmarshal(b, &test)
fatalf(err, "get json byte fail")
log.Printf("foo: %s", test.Spec.Foo)
}
执行代码可以获取到正确的结果。
go run client-example/client-go/main.go
2022/11/15 23:16:23 foo: test
简单看一下源码,可以看到实际上 Resource 方法就是返回了 NamespaceableResourceInterface 接口,这个接口支持了 Namespace 以及非 Namespace 级别的资源的 CURD 等访问方法。
typeResourceInterfaceinterface {
Create(ctxcontext.Context, obj*unstructured.Unstructured, optionsmetav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error)
Update(ctxcontext.Context, obj*unstructured.Unstructured, optionsmetav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error)
UpdateStatus(ctxcontext.Context, obj*unstructured.Unstructured, optionsmetav1.UpdateOptions) (*unstructured.Unstructured, error)
Delete(ctxcontext.Context, namestring, optionsmetav1.DeleteOptions, subresources ...string) error
DeleteCollection(ctxcontext.Context, optionsmetav1.DeleteOptions, listOptionsmetav1.ListOptions) error
Get(ctxcontext.Context, namestring, optionsmetav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error)
List(ctxcontext.Context, optsmetav1.ListOptions) (*unstructured.UnstructuredList, error)
Watch(ctxcontext.Context, optsmetav1.ListOptions) (watch.Interface, error)
Patch(ctxcontext.Context, namestring, pttypes.PatchType, data []byte, optionsmetav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error)
Apply(ctxcontext.Context, namestring, obj*unstructured.Unstructured, optionsmetav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error)
ApplyStatus(ctxcontext.Context, namestring, obj*unstructured.Unstructured, optionsmetav1.ApplyOptions) (*unstructured.Unstructured, error)
}
// dynamic.NewForConfigOrDie(cfg).Resource(gvr) 返回的接口
typeNamespaceableResourceInterfaceinterface {
Namespace(string) ResourceInterface
ResourceInterface
}
上面的这些方法返回的都是 *unstructured.Unstructured 类型的数据,这个类型本质上就是把 object 通过 map 保存了下来,然后提供了 GetNamespace 等便捷的方法给用户使用。
typeUnstructuredstruct {
// Object is a JSON compatible map with string, float, int, bool, []interface{}, or
// map[string]interface{}
// children.
Objectmap[string]interface{}
}
通过 controller-runtime 调用
如下所示,可以发现 controller-runtime 的代码明显要比上一种方式要简洁一些,不需要手动去 json 编码解码了,基础的 scheme 数据也可以直接使用生成好的数据。
funcmain() {
cfg, err :=config.GetConfigWithContext("kind-kind")
fatalf(err, "get config fail")
scheme, err :=v1.SchemeBuilder.Build()
fatalf(err, "get scheme fail")
c, err :=client.New(cfg, client.Options{Scheme: scheme})
fatalf(err, "new client fail")
test :=v1.Test{}
err=c.Get(context.Background(), types.NamespacedName{
Namespace: "default",
Name: "test-sample",
}, &test)
fatalf(err, "get resource fail")
log.Printf("foo: %s", test.Spec.Foo)
}
执行测试一下。
gorunclient-example/controller-runtime/main.go
2022/11/1523:34:45foo: test
同样简单看下接口,controller-runtime 的 client 是多个接口组合而来的,合并在一起之后其实和上面 client-go 的接口大差不差。
// Client knows how to perform CRUD operations on Kubernetes objects.
typeClientinterface {
Reader
Writer
StatusClient
Scheme() *runtime.Scheme
RESTMapper() meta.RESTMapper
}
typeReaderinterface {
Get(ctxcontext.Context, keyObjectKey, objObject, opts ...GetOption) error
List(ctxcontext.Context, listObjectList, opts ...ListOption) error
}
typeWriterinterface {
Create(ctxcontext.Context, objObject, opts ...CreateOption) error
Delete(ctxcontext.Context, objObject, opts ...DeleteOption) error
Update(ctxcontext.Context, objObject, opts ...UpdateOption) error
Patch(ctxcontext.Context, objObject, patchPatch, opts ...PatchOption) error
DeleteAllOf(ctxcontext.Context, objObject, opts ...DeleteAllOfOption) error
}
生成 clientset 调用
生成 clientset
我们使用 code-generator[10] 的 client-gen 子项目来生成客户端的调用,使用这个方法我们需要对代码做很多的调整。
- 项目结构调整,kubebuilder 生成的 api 目录是api/v1,但是 client-gen 要求的目录结构是 api/${group}/${version} 。
- 所以我们需要将目录结构调整为api/job/v1,调整后记得修改原有代码的依赖路径。
- 修改PROJECT 文件,这个文件用于 kubebuilder 记录,修改里面的 path 路径。
resources:
# ... 删除掉不需要关注的部分
-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 前面即可。
//+genclient
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// Test is the Schema for the tests API
typeTeststruct {
metav1.TypeMeta`json:",inline"`
metav1.ObjectMeta`json:"metadata,omitempty"`
SpecTestSpec`json:"spec,omitempty"`
StatusTestStatus`json:"status,omitempty"`
}
- api 新增SchemeGroupVersion 全局变量,修改 api/job/v1/groupversion_info.go。
var (
// 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 那我们执行。
gogetk8s.io/code-generator@v0.25.0
- 由于我们的项目内实际上并没有依赖 code-generator ,所以我们需要添加一个文件依赖这个项目,我们新建一个 hack/code_generator.go 文件,我们加上 go:build tools 标签确保在编译应用的时候不会将这个依赖编译进去。
//go:build tools
// +build tools
packagehack
import_"k8s.io/code-generator"
- 然后我们执行 go mod tidy。
- 编写代码生成脚本,会将 clientset 放到 pkg 目录下。
#!/bin/bash
set-e
set-x
#生成clientset代码
#获取gomodulename
go_module=$(golist-m)
#crdgroup
group=${GROUP:-"job"}
#api版本
api_version=${API_VERSION:-"v1"}
project_dir=$(cd$(dirname${BASH_SOURCE[0]})/..; pwd) #项目根目录
#checkgenerate-groups.shisexist
#直接下载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.shclient\
$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 生成代码,生成的目录结构如下:
treepkg/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
- 生成的客户端接口如下所示,我们可以看到和上面两种方式的主要区别就是指定了类型。
// TestsGetter has a method to return a TestInterface.
// A group's client should implement this interface.
typeTestsGetterinterface {
Tests(namespacestring) TestInterface
}
// TestInterface has methods to work with Test resources.
typeTestInterfaceinterface {
Create(ctxcontext.Context, test*v1.Test, optsmetav1.CreateOptions) (*v1.Test, error)
Update(ctxcontext.Context, test*v1.Test, optsmetav1.UpdateOptions) (*v1.Test, error)
UpdateStatus(ctxcontext.Context, test*v1.Test, optsmetav1.UpdateOptions) (*v1.Test, error)
Delete(ctxcontext.Context, namestring, optsmetav1.DeleteOptions) error
DeleteCollection(ctxcontext.Context, optsmetav1.DeleteOptions, listOptsmetav1.ListOptions) error
Get(ctxcontext.Context, namestring, optsmetav1.GetOptions) (*v1.Test, error)
List(ctxcontext.Context, optsmetav1.ListOptions) (*v1.TestList, error)
Watch(ctxcontext.Context, optsmetav1.ListOptions) (watch.Interface, error)
Patch(ctxcontext.Context, namestring, pttypes.PatchType, data []byte, optsmetav1.PatchOptions, subresources ...string) (result*v1.Test, errerror)
TestExpansion
}
调用 clientset
可以看到 clientset 的代码是最简洁的。
funcmain() {
cfg, err :=config.GetConfigWithContext("kind-kind")
fatalf(err, "get config fail")
client :=clientv1.NewForConfigOrDie(cfg)
test, err :=client.Tests("default").Get(context.Background(), "test-sample", v1.GetOptions{})
fatalf(err, "new client fail")
log.Printf("foo: %s", test.Spec.Foo)
}
执行:
gorunclient-example/clientset/main.go
2022/11/1610:26:50foo: test
总结
这三种调用方式其实各有优劣,kubebuilder 官方比较推荐直接使用 controller-runtime,但是另外两种方式也有各自的使用场景,client-go 这种方式通用性最强,不用依赖 operator 开发者的代码,clientset 的定制性最强,对于使用方来说也最方便。
对于我而言其实最开始只了解到 client-go 和 clientset 这两种方式,所以之前一直都是使用的 clientset 这种方式,这次这篇文章的初衷其实也只是为了记录一下 clientset 的最小化配置方法,但是在资料汇总的过程中发现了 controller-runtime 这种方法,作为 operator 的开发者最后选择使用 controller-runtime,因为生成 clientset 需要改动的东西实在是太多了,而且很容易出错。controller-runtime 在易用性和通用性都有不错的表现。
参考资料
[1]系列文章: https://lailin.xyz/post/operator-11-summary.html。
[2]CustomResource: https://kubernetes.io/zh-cn/docs/concepts/extend-kubernetes/api-extension/custom-resources/。
[3]https://github.com/kubernetes-sigs/kubebuilder/issues/403: https://github.com/kubernetes-sigs/kubebuilder/issues/403。
[4]https://github.com/kubernetes-sigs/kubebuilder/issues/1152: https://github.com/kubernetes-sigs/kubebuilder/issues/1152。
[5]client-gen: https://github.com/kubernetes/community/blob/master/contributors/devel/sig-api-machinery/generating-clientset.md。
[6]controller-runtime/pkg/client: https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/client?utm_source=godoc#example-Client-Update。
[7]client-go/dynamic: https://pkg.go.dev/k8s.io/[email protected]/dynamic。
[8]kubebuilder 简明教程: https://lailin.xyz/post/operator-03-kubebuilder-tutorial.html。
[9]operator-kubebuilder-clientset: https://github.com/mohuishou/blog-code/tree/main/02-k8s-operator/operator-kubebuilder-clientset/client-example。
[10]code-generator: https://github.com/kubernetes/code-generator。
本文转载自微信公众号「mohuishou」,可以通过以下二维码关注。转载本文请联系mohuishou公众号。
文章来源网络,作者:运维,如若转载,请注明出处:https://shuyeidc.com/wp/243129.html<