作者:lailinxyz 2022-11-16 11:53:38
云计算
云原生 这次这篇文章的初衷其实也只是为了记录一下 clientset 的最小化配置方法,但是在资料汇总的过程中发现了 controller-runtime 这种方法,作为 operator 的开发者最后选择使用 controller-runtime,因为生成 clientset 需要改动的东西实在是太多了,而且很容易出错。controller-runtime 在易用性和通用性都有不错的表现。
网站建设哪家好,找创新互联!专注于网页设计、网站建设、微信开发、小程序开发、集团企业网站建设等服务项目。为回馈新老客户创新互联还提供了鄞州免费建站欢迎大家使用!
在去年写的系列文章[1]中,我们完整的实现了 operator 开发过程中涉及到的绝大部分要素,但是在实际的生产应用中我们定义的 CR(CustomResource[2]) 就像 k8s 自带的 deployment、pod 等资源一样,会存在其他服务直接调用 api-server 接口进行创建更新的需求,而不仅仅只是通过 kubectl 编辑yaml。
那么 k8s 自带的对象我们可以通过 client-go 进行调用,我们自己设计的 CR 能否直接生成类似的 SDK 呢?
这个问题在 kubebuilder 社区从 v1 - v2 版本都有用户在提,但是 kubebuilder 官方似乎不太赞同生成 sdk 的这种做法。
目前找到以下几种方案。
方案 |
优点 |
缺点 |
通过 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] 项目中找到。
如下所示可以看到,代码整体来说相对比较复杂,dynamic 包生成的 client 是一个通用的 client,所以他只能获取到 k8s 的一些通用的 metadata 数据,如果想要获取到 CR 的结构化数据就只能通过 json 来进行转换。
func main() {
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 等访问方法。
type ResourceInterface interface {
Create(ctx context.Context, obj *unstructured.Unstructured, options metav1.CreateOptions, subresources ...string) (*unstructured.Unstructured, error)
Update(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions, subresources ...string) (*unstructured.Unstructured, error)
UpdateStatus(ctx context.Context, obj *unstructured.Unstructured, options metav1.UpdateOptions) (*unstructured.Unstructured, error)
Delete(ctx context.Context, name string, options metav1.DeleteOptions, subresources ...string) error
DeleteCollection(ctx context.Context, options metav1.DeleteOptions, listOptions metav1.ListOptions) error
Get(ctx context.Context, name string, options metav1.GetOptions, subresources ...string) (*unstructured.Unstructured, error)
List(ctx context.Context, opts metav1.ListOptions) (*unstructured.UnstructuredList, error)
Watch(ctx context.Context, opts metav1.ListOptions) (watch.Interface, error)
Patch(ctx context.Context, name string, pt types.PatchType, data []byte, options metav1.PatchOptions, subresources ...string) (*unstructured.Unstructured, error)
Apply(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions, subresources ...string) (*unstructured.Unstructured, error)
ApplyStatus(ctx context.Context, name string, obj *unstructured.Unstructured, options metav1.ApplyOptions) (*unstructured.Unstructured, error)
}
// dynamic.NewForConfigOrDie(cfg).Resource(gvr) 返回的接口
type NamespaceableResourceInterface interface {
Namespace(string) ResourceInterface
ResourceInterface
}
上面的这些方法返回的都是 *unstructured.Unstructured 类型的数据,这个类型本质上就是把 object 通过 map 保存了下来,然后提供了 GetNamespace 等便捷的方法给用户使用。
type Unstructured struct {
// Object is a JSON compatible map with string, float, int, bool, []interface{}, or
// map[string]interface{}
// children.
Object map[string]interface{}
}
如下所示,可以发现 controller-runtime 的代码明显要比上一种方式要简洁一些,不需要手动去 json 编码解码了,基础的 scheme 数据也可以直接使用生成好的数据。
func main() {
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)
}
执行测试一下。
go run client-example/controller-runtime/main.go
2022/11/15 23:34:45 foo: test
同样简单看下接口,controller-runtime 的 client 是多个接口组合而来的,合并在一起之后其实和上面 client-go 的接口大差不差。
// Client knows how to perform CRUD operations on Kubernetes objects.
type Client interface {
Reader
Writer
StatusClient
Scheme() *runtime.Scheme
RESTMapper() meta.RESTMapper
}
type Reader interface {
Get(ctx context.Context, key ObjectKey, obj Object, opts ...GetOption) error
List(ctx context.Context, list ObjectList, opts ...ListOption) error
}
type Writer interface {
Create(ctx context.Context, obj Object, opts ...CreateOption) error
Delete(ctx context.Context, obj Object, opts ...DeleteOption) error
Update(ctx context.Context, obj Object, opts ...UpdateOption) error
Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error
DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error
}
我们使用 code-generator[10] 的 client-gen 子项目来生成客户端的调用,使用这个方法我们需要对代码做很多的调整。
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"
//+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"`
}
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
)
go get k8s.io/code-generator@v0.25.0
//go:build tools
// +build tools
package hack
import _ "k8s.io/code-generator"
#!/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)
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
// 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 的代码是最简洁的。
func main() {
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)
}
执行:
go run client-example/clientset/main.go
2022/11/16 10:26:50 foo: 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/client-go@v0.25.4/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公众号。
分享文章:第三方应用如何调用我们kubebuilder生成的自定义资源?
转载注明:http://www.gawzjz.com/qtweb2/news14/1564.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联