惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
C
Cybersecurity and Infrastructure Security Agency CISA
S
Secure Thoughts
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
Attack and Defense Labs
Attack and Defense Labs
N
News and Events Feed by Topic
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
A
Arctic Wolf
www.infosecurity-magazine.com
www.infosecurity-magazine.com
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
The GitHub Blog
The GitHub Blog
W
WeLiveSecurity
Simon Willison's Weblog
Simon Willison's Weblog
WordPress大学
WordPress大学
Y
Y Combinator Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
Forbes - Security
Forbes - Security
NISL@THU
NISL@THU
博客园 - 聂微东
G
Google Developers Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
Schneier on Security
Schneier on Security
V
Vulnerabilities – Threatpost
V
V2EX
I
Intezer
S
Schneier on Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
雷峰网
雷峰网
L
LangChain Blog
小众软件
小众软件
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
L
LINUX DO - 热门话题
P
Proofpoint News Feed
Microsoft Security Blog
Microsoft Security Blog
Project Zero
Project Zero
V
Visual Studio Blog
Engineering at Meta
Engineering at Meta
爱范儿
爱范儿
H
Hacker News: Front Page
B
Blog
T
Threatpost
Spread Privacy
Spread Privacy
H
Heimdal Security Blog
F
Fortinet All Blogs
TaoSecurity Blog
TaoSecurity Blog
T
Threat Research - Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
The Cloudflare Blog

Mohuishou

如何实现支持多集群的 Kubernetes Operator? Kubernetes 简明教程 k8s job 为何迟迟不能结束? Go 工程化(十一) 如何优雅的写出 repo 层代码 Go 工程化(十) 如何在整洁架构中使用事务? 给博客添加章节目录 使用 Notion Database 管理静态博客文章 一个普通 Go 开发的三年 4. localhost 就一定是 localhost 么? Go可用性(七) 总结: 一张图串联可用性知识点 Go可用性(六) 熔断 10. 总结 9. kubebuilder 进阶: 源码分析 8. kubebuilder 进阶: webhook 7. kubebuilder 进阶: 测试 6. kubebuilder 实战: status & event 5. kubebuilder 实战: CRUD 4. kustomize 简明教程 3. KubeBuilder 简明教程 2. Kind: 如何快速搭建本地 K8s 开发环境? 1. Operator概述: 如何对 Kubernetes 进行扩展 Go可用性(五) 自适应限流 Go可用性(四) 漏桶算法 Go可用性(三) 令牌桶的实现 rate/limt Go可用性(二) 令牌桶原理及使用 Go可用性(一) 隔离设计 Go并发编程(十二) Singleflight Go工程化(九) 项目重构实践 Go工程化(八) 单元测试 Go工程化(七) Go Module Go工程化(六) 配置管理 Go工程化(五) API 设计下: 基于 protobuf 自动生成 gin 代码 Go工程化(四) API 设计上: 项目结构 & 设计 Go工程化(三) 依赖注入框架 wire Go工程化(二) 项目目录结构 Go工程化(一) 架构整洁之道阅读笔记 Go并发编程(十一) 总结 Go并发编程(十) 深入理解 Channel Go并发编程(九) 深入理解 Context Go并发编程(八) 深入理解 sync.Once Go并发编程(七) 深入理解 errgroup Go并发编程(六) 深入理解 WaitGroup Go并发编程(五) 深入理解 sync/atomic Go并发编程(四) 深入理解 Mutex Go并发编程(三) data race Go并发编程(二) Go 内存模型 Go并发编程(一) goroutine Go错误处理最佳实践 微服务(二) 服务发现&多租户 微服务(一) 微服务概览 5. 栈下: 深入理解 defer 4. 栈上: 如何实现一个计算器 Go Struct 初始化风格的抉择 3. 数组下: 使用 GDB 调试 Golang 代码 2. 数组上: 深入理解 slice 1. 链表: 深入理解container/list&LRU缓存的实现 Go设计模式24-总结(更新完毕) Go设计模式23-中介模式 Go设计模式22-解释器模式 Go设计模式21-命令模式 Go设计模式20-备忘录模式 Go设计模式19-访问者模式 Go设计模式18-迭代器模式 Go设计模式17-状态模式 Go设计模式16-职责链模式(Gin的中间件实现) Go设计模式15-策略模式 Go模板模式14-模板模式 Go设计模式13-观察者模式(实现简单的EventBus) Go设计模式12-享元模式 Go设计模式11-组合模式 Go设计模式10-门面模式 Go设计模式09-适配器模式 Go设计模式08-装饰器模式 Go设计模式07-桥接模式 Go设计模式06-代理模式(generate实现类似动态代理) Go设计模式05-创建型模式总结 Go设计模式04-原型模式 Go设计模式03-建造者模式 Go设计模式02-工厂模式&DI容器 笔记-让你最快速地改善代码质量的20条编程规范 Go设计模式01-单例模式 一点拙见-如何写好一个技术预研报告? Go Web小技巧(四)在单个仓库中支持多个 go mod 模块 Go Web 小技巧(三)Gin 参数绑定 Go Web 小技巧(二)GORM 使用自定义类型 Go Web 小技巧(一)简化Gin接口代码 善用工具之postman高级用法概述 go generate and ast hexo-next-algolia-search全文搜索 docker镜像瘦身&优化 GORM避坑指南之含关联关系的更新 Github Actions介绍&自动构建Github Pages博客 在blog中内嵌在线PPT 记一次net http内存泄漏 使用TravisCI自动部署Blog 使用Goland调试Go程序 一个十分边缘的gorm的bug Httprouter介绍及源码阅读 Gin源码阅读 从0.1开始
第三方应用如何调用我们 kubebuilder 生成的自定义资源?
Mohuishou · 2022-11-16 · via Mohuishou

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

kubebuilder 能否生成类似 client-go 的 sdk?

在去年写的系列文章中,我们完整的实现了 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
2
3
4
5
6
7
8
9
10
11
12
13
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 项目中找到

通过 client-go 调用

如下所示可以看到,代码整体来说相对比较复杂,dynamic​ 包生成的 client 是一个通用的 client,所以他只能获取到 k8s 的一些通用的 metadata 数据,如果想要获取到 CR 的结构化数据就只能通过 json 来进行转换

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
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)
}

执行代码可以获取到正确的结果

1
2
go run client-example/client-go/main.go
2022/11/15 23:16:23 foo: test

简单看一下源码,可以看到实际上 Resource​ 方法就是返回了 NamespaceableResourceInterface​ 接口,这个接口支持了 Namespace 以及非 Namespace 级别的资源的 CURD​ 等访问方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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​ 等便捷的方法给用户使用

1
2
3
4
5
6
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 调用

如下所示,可以发现 controller-runtime 的代码明显要比上一种方式要简洁一些,不需要手动去 json 编码解码了,基础的 scheme 数据也可以直接使用生成好的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
cfg, err := config.GetConfigWithContext("kind-kind")
fatalf(err, "get config fail")

scheme := runtime.NewScheme()
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(v2.AddToScheme(scheme))

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)
}

执行测试一下

1
2
go run client-example/controller-runtime/main.go
2022/11/15 23:34:45 foo: test

同样简单看下接口,controller-runtime 的 client 是多个接口组合而来的,合并在一起之后其实和上面 client-go 的接口大差不差

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 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 调用

生成 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
      6
      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​​ 前面即可

    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
    14
    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 那我们执行

      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
2
3
4
5
6
7
8
9
10
11
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)
}

执行结果如下

1
2
❯ 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 在易用性和通用性都有不错的表现

参考文献

关注我获取更新

猜你喜欢