新聞中心
第三方應(yīng)用如何調(diào)用我們 kubebuilder 生成的自定義資源?
作者:lailinxyz 2022-11-16 11:53:38
云計(jì)算
云原生 這次這篇文章的初衷其實(shí)也只是為了記錄一下 clientset 的最小化配置方法,但是在資料匯總的過(guò)程中發(fā)現(xiàn)了 controller-runtime 這種方法,作為 operator 的開(kāi)發(fā)者最后選擇使用 controller-runtime,因?yàn)樯?clientset 需要改動(dòng)的東西實(shí)在是太多了,而且很容易出錯(cuò)。controller-runtime 在易用性和通用性都有不錯(cuò)的表現(xiàn)。

網(wǎng)站建設(shè)哪家好,找創(chuàng)新互聯(lián)!專(zhuān)注于網(wǎng)頁(yè)設(shè)計(jì)、網(wǎng)站建設(shè)、微信開(kāi)發(fā)、小程序開(kāi)發(fā)、集團(tuán)企業(yè)網(wǎng)站建設(shè)等服務(wù)項(xiàng)目。為回饋新老客戶(hù)創(chuàng)新互聯(lián)還提供了鄞州免費(fèi)建站歡迎大家使用!
kubebuilder 能否生成類(lèi)似 clie
在去年寫(xiě)的系列文章[1]中,我們完整的實(shí)現(xiàn)了 operator 開(kāi)發(fā)過(guò)程中涉及到的絕大部分要素,但是在實(shí)際的生產(chǎn)應(yīng)用中我們定義的 CR(CustomResource[2]) 就像 k8s 自帶的 deployment、pod 等資源一樣,會(huì)存在其他服務(wù)直接調(diào)用 api-server 接口進(jìn)行創(chuàng)建更新的需求,而不僅僅只是通過(guò) kubectl 編輯yaml。
那么 k8s 自帶的對(duì)象我們可以通過(guò) client-go 進(jìn)行調(diào)用,我們自己設(shè)計(jì)的 CR 能否直接生成類(lèi)似的 SDK 呢?
這個(gè)問(wèn)題在 kubebuilder 社區(qū)從 v1 - v2 版本都有用戶(hù)在提,但是 kubebuilder 官方似乎不太贊同生成 sdk 的這種做法。
- https://github.com/kubernetes-sigs/kubebuilder/issues/403[3]。
- https://github.com/kubernetes-sigs/kubebuilder/issues/1152[4]
目前找到以下幾種方案。
|
方案 |
優(yōu)點(diǎn) |
缺點(diǎn) |
|
通過(guò) client-gen[5] 生成對(duì)應(yīng)的 sdk |
調(diào)用方使用起來(lái)會(huì)更加的方便,畢竟是靜態(tài)代碼,不容易出錯(cuò) |
對(duì)于 operator 的開(kāi)發(fā)者來(lái)說(shuō)比較麻煩,因?yàn)橐ㄟ^(guò)這個(gè)工具生成對(duì)應(yīng)的代碼還需要做很多其他的事情,甚至需要調(diào)整 kubebuiler 生成的代碼結(jié)構(gòu) 客制化較強(qiáng),通用性較弱,每個(gè) CR 都需要單獨(dú)生成 |
|
controller-runtime/pkg/client[6] |
調(diào)用也比較方便 通用性強(qiáng),只需要將 kubebuilder 生成好的 CR 定義暴露出去即可 |
相對(duì)于通過(guò) client-gen 來(lái)說(shuō)靜態(tài)代碼檢查的能力相對(duì)較弱 |
|
client-go/dynamic[7] |
通用性極強(qiáng),甚至可以不用 Operator 開(kāi)發(fā)中提供對(duì)應(yīng)的 CR 定義代碼 |
調(diào)用方來(lái)說(shuō)極其不方便,需要自定義很多東西,并且需要反復(fù)進(jìn)行序列化操作 |
接下來(lái)我們就自定義一個(gè)簡(jiǎn)單的 CR,這個(gè) CR 沒(méi)有任何的邏輯,只是為了用來(lái)驗(yàn)證客戶(hù)端調(diào)用,關(guān)于 kubebuilder 生成 CR 如果不是特別清楚,可以閱讀之前的這篇文章: kubebuilder 簡(jiǎn)明教程[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
如上所示這個(gè) CR 只有一個(gè) foo 字段,也就是 kubebuilder 初始化的一個(gè)字段,除此之外什么也沒(méi)有。
接下來(lái)我都以 get 數(shù)據(jù)為例來(lái)分別說(shuō)明這三種方式的基本使用方法,下面的示例代碼可以在 operator-kubebuilder-clientset[9] 項(xiàng)目中找到。
通過(guò) client-go 調(diào)用
如下所示可以看到,代碼整體來(lái)說(shuō)相對(duì)比較復(fù)雜,dynamic 包生成的 client 是一個(gè)通用的 client,所以他只能獲取到 k8s 的一些通用的 metadata 數(shù)據(jù),如果想要獲取到 CR 的結(jié)構(gòu)化數(shù)據(jù)就只能通過(guò) json 來(lái)進(jìn)行轉(zhuǎn)換。
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)
}執(zhí)行代碼可以獲取到正確的結(jié)果。
go run client-example/client-go/main.go
2022/11/15 23:16:23 foo: test
簡(jiǎn)單看一下源碼,可以看到實(shí)際上 Resource 方法就是返回了 NamespaceableResourceInterface 接口,這個(gè)接口支持了 Namespace 以及非 Namespace 級(jí)別的資源的 CURD 等訪問(wèn)方法。
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 類(lèi)型的數(shù)據(jù),這個(gè)類(lèi)型本質(zhì)上就是把 object 通過(guò) map 保存了下來(lái),然后提供了 GetNamespace 等便捷的方法給用戶(hù)使用。
type Unstructured struct {
// Object is a JSON compatible map with string, float, int, bool, []interface{}, or
// map[string]interface{}
// children.
Object map[string]interface{}
}通過(guò) controller-runtime 調(diào)用
如下所示,可以發(fā)現(xiàn) controller-runtime 的代碼明顯要比上一種方式要簡(jiǎn)潔一些,不需要手動(dòng)去 json 編碼解碼了,基礎(chǔ)的 scheme 數(shù)據(jù)也可以直接使用生成好的數(shù)據(jù)。
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)
}執(zhí)行測(cè)試一下。
go run client-example/controller-runtime/main.go
2022/11/15 23:34:45 foo: test
同樣簡(jiǎn)單看下接口,controller-runtime 的 client 是多個(gè)接口組合而來(lái)的,合并在一起之后其實(shí)和上面 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
}
生成 clientset 調(diào)用
生成 clientset
我們使用 code-generator[10] 的 client-gen 子項(xiàng)目來(lái)生成客戶(hù)端的調(diào)用,使用這個(gè)方法我們需要對(duì)代碼做很多的調(diào)整。
- 項(xiàng)目結(jié)構(gòu)調(diào)整,kubebuilder 生成的 api 目錄是api/v1,但是 client-gen 要求的目錄結(jié)構(gòu)是 api/${group}/${version} 。
- 所以我們需要將目錄結(jié)構(gòu)調(diào)整為api/job/v1,調(diào)整后記得修改原有代碼的依賴(lài)路徑。
- 修改PROJECT 文件,這個(gè)文件用于 kubebuilder 記錄,修改里面的 path 路徑。
resources:
# ... 刪除掉不需要關(guān)注的部分
- 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 注釋?zhuān)缦滤?,放?nbsp;//+kubebuilder:object:root=true 前面即可。
//+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。
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 依賴(lài),注意 code-generator 版本一定要和你的 client-go 版本一致。
- 例如在我們的測(cè)試項(xiàng)目里面 client-go 的版本是 v0.25.0 那我們執(zhí)行。
go get k8s.io/[email protected]
- 由于我們的項(xiàng)目?jī)?nèi)實(shí)際上并沒(méi)有依賴(lài) code-generator ,所以我們需要添加一個(gè)文件依賴(lài)這個(gè)項(xiàng)目,我們新建一個(gè) hack/code_generator.go 文件,我們加上 go:build tools 標(biāo)簽確保在編譯應(yīng)用的時(shí)候不會(huì)將這個(gè)依賴(lài)編譯進(jìn)去。
//go:build tools
// +build tools
package hack
import _ "k8s.io/code-generator"
- ?然后我們執(zhí)行 go mod tidy。
- 編寫(xiě)代碼生成腳本,會(huì)將 clientset 放到 pkg 目錄下。
#!/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) # 項(xiàng)目根目錄
# check generate-groups.sh is exist
# 直接下載 generate-groups.sh 腳本,這個(gè)腳本還可以生成其他類(lèi)型的代碼,但是我們這里只用來(lái)生成 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 的文件夾路徑會(huì)包含 $go_module/pkg 所以我們需要把這個(gè)文件夾復(fù)制出來(lái)
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)
- 執(zhí)行 bash hack/gen-client.sh 生成代碼,生成的目錄結(jié)構(gòu)如下:
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
- 生成的客戶(hù)端接口如下所示,我們可以看到和上面兩種方式的主要區(qū)別就是指定了類(lèi)型。
// 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
}
調(diào)用 clientset
可以看到 clientset 的代碼是最簡(jiǎn)潔的。
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)
}執(zhí)行:
go run client-example/clientset/main.go
2022/11/16 10:26:50 foo: test
總結(jié)
這三種調(diào)用方式其實(shí)各有優(yōu)劣,kubebuilder 官方比較推薦直接使用 controller-runtime,但是另外兩種方式也有各自的使用場(chǎng)景,client-go 這種方式通用性最強(qiáng),不用依賴(lài) operator 開(kāi)發(fā)者的代碼,clientset 的定制性最強(qiáng),對(duì)于使用方來(lái)說(shuō)也最方便。
對(duì)于我而言其實(shí)最開(kāi)始只了解到 client-go 和 clientset 這兩種方式,所以之前一直都是使用的 clientset 這種方式,這次這篇文章的初衷其實(shí)也只是為了記錄一下 clientset 的最小化配置方法,但是在資料匯總的過(guò)程中發(fā)現(xiàn)了 controller-runtime 這種方法,作為 operator 的開(kāi)發(fā)者最后選擇使用 controller-runtime,因?yàn)樯?clientset 需要改動(dòng)的東西實(shí)在是太多了,而且很容易出錯(cuò)。controller-runtime 在易用性和通用性都有不錯(cuò)的表現(xiàn)。
參考資料
[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 簡(jiǎn)明教程: 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。
本文轉(zhuǎn)載自微信公眾號(hào)「mohuishou」,可以通過(guò)以下二維碼關(guān)注。轉(zhuǎn)載本文請(qǐng)聯(lián)系mohuishou公眾號(hào)。
標(biāo)題名稱(chēng):第三方應(yīng)用如何調(diào)用我們kubebuilder生成的自定義資源?
網(wǎng)站路徑:http://fisionsoft.com.cn/article/dpgcpgi.html


咨詢(xún)
建站咨詢(xún)
