Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DynamicClient 操作 CRD:无需代码生成的动态操作

Kubernetes 编程 / Operator 专题【左扬精讲】—— Client-go 源代码分析:DynamicClient 操作 CRD:无需代码生成的动态操作

当我们需要操作 Kubernetes 内置资源(如 Pod、Deployment)时,可以使用 typed client(Clientset),因为这些资源的 Go 类型已经定义好了。

但当我们需要操作自定义资源(CRD)时,如果每次都等代码生成,会非常麻烦。DynamicClient 就是来解决这个问题的:它可以动态地操作任意 Kubernetes 资源,包括 CRD,无需提前知道资源的具体类型。这一篇文章,我们来深入理解 DynamicClient 的设计和使用。

Kubernetes DynamicClient CRD Unstructured v1.36.1

🔓 学习重点提示  — 建议先通读全文,再重点回顾标注内容

★ 重点掌握(必须)
   • DynamicClient vs Clientset:什么时候用哪个,为什么
   • Unstructured 对象:如何用 map[string]interface{} 表示任意资源
   • GroupVersionResource:如何用 GVR 定位任意资源

☆ 次重点(了解即可)
   • DynamicInformer 与 DynamicClient 的配合


一、为什么需要 DynamicClient?

假设我们开发了一个通用工具,需要操作用户的 CRD,但用户可能有任意类型的 CRD,不可能每次都等 CRD 定义好之后重新生成代码。

另一个场景是 Operator Framework 的早期版本,它们需要在运行时动态发现和操作 CRD。

DynamicClient 的核心思想:用通用的数据结构(map/list)来表示资源,而不是用编译时已知的 Go struct。这样就可以操作任意类型的资源了。

二、DynamicClient 的数据结构

DynamicClient 的结构非常简单:

// staging/src/k8s.io/client-go/dynamic/simple.go(行 34-36)

// DynamicClient 是一个动态客户端,可以操作任意 Kubernetes 资源
type DynamicClient struct {
    client rest.Interface  // 通用的 REST 客户端
}

var _ Interface = &DynamicClient{}  // 实现了 Interface 接口

DynamicClient 只有一个字段:rest.Interface。所有对 APIServer 的请求都通过这个通用接口完成。创建 DynamicClient 的方式也很简单:

// 创建 DynamicClient

import "k8s.io/client-go/dynamic"

func main() {
    // 方式一:从 config 创建
    config, _ := clientcmd.BuildConfigFromFlags("", "")
    dynamicClient, _ := dynamic.NewForConfig(config)

    // 方式二:从已有的 rest.Config 创建
    dynamicClient, _ := dynamic.NewForConfigAndClient(config, httpClient)
}

三、GVR:定位任意资源的关键

typed client 通过 Clientset 直接访问特定的资源,比如 `clientset.AppsV1().Deployments()`。DynamicClient 不一样,我们需要用 GroupVersionResource(GVR)来定位资源。

import "k8s.io/apimachinery/pkg/runtime/schema"

// GVR 示例
podGVR := schema.GroupVersionResource{
    Group:    "",           // core 组,Group 为空
    Version:  "v1",        // API 版本
    Resource: "pods",      // 资源名(复数形式)
}

// 自定义资源 GVR 示例(假设有一个 Foo CRD)
fooGVR := schema.GroupVersionResource{
    Group:    "example.com",    // CRD 的 API Group
    Version:  "v1",             // CRD 的版本
    Resource: "foos",           // CRD 的复数资源名
}

有了 GVR 之后,就可以通过 DynamicClient 获取资源了:

// 获取某个 namespace 下的资源
resourceClient := dynamicClient.Resource(fooGVR).Namespace("default")

// 获取所有 namespace 下的资源
resourceClient := dynamicClient.Resource(fooGVR)  // 不调用 Namespace()

四、Unstructured:动态表示任意资源

DynamicClient 的核心是 Unstructured 对象。它是一个用 `map[string]interface{}` 表示的动态对象,可以装任意 JSON 数据。

// Unstructured 对象示例

import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"

// 创建一个 Unstructured 对象
obj := &unstructured.Unstructured{
    Object: map[string]interface{}{
        "apiVersion": "example.com/v1",
        "kind":       "Foo",
        "metadata": map[string]interface{}{
            "name":      "my-foo",
            "namespace": "default",
        },
        "spec": map[string]interface{}{
            "replicas": 3,
            "selector": map[string]interface{}{
                "matchLabels": map[string]interface{}{
                    "app": "myapp",
                },
            },
        },
    },
}

Unstructured 提供了便捷的方法来读写嵌套字段:

// 设置嵌套字段
unstructured.SetNestedField(obj.Object, int64(3), "spec", "replicas")
unstructured.SetNestedMap(obj.Object, map[string]interface{}{
    "matchLabels": map[string]interface{}{
        "app": "myapp",
    },
}, "spec", "selector")

// 读取嵌套字段
replicas, found, err := unstructured.NestedInt64(obj.Object, "spec", "replicas")
name, found, err := unstructured.NestedString(obj.Object, "metadata", "name")

五、完整使用示例

下面是一个完整的 DynamicClient 使用示例,演示如何操作一个名为 Foo 的 CRD:

// 完整的 DynamicClient 使用示例

package main

import (
    "context"
    "fmt"

    "k8s.io/apimachinery/pkg/api/meta"
    "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    "k8s.io/apimachinery/pkg/runtime/schema"
    "k8s.io/client-go/dynamic"
    "k8s.io/client-go/tools/clientcmd"
)

func main() {
    // 第一步:创建 DynamicClient
    config, _ := clientcmd.BuildConfigFromFlags("", "")
    dynamicClient, _ := dynamic.NewForConfig(config)

    // 第二步:定义 CRD 的 GVR
    fooGVR := schema.GroupVersionResource{
        Group:    "example.com",
        Version:  "v1",
        Resource: "foos",
    }

    // 第三步:创建资源
    foo := &unstructured.Unstructured{
        Object: map[string]interface{}{
            "apiVersion": "example.com/v1",
            "kind":       "Foo",
            "metadata": map[string]interface{}{
                "name":      "my-foo",
                "namespace": "default",
            },
            "spec": map[string]interface{}{
                "replicas": 3,
            },
        },
    }

    // 创建资源
    created, err := dynamicClient.Resource(fooGVR).Namespace("default").Create(
        context.Background(),
        foo,
        metav1.CreateOptions{},
    )
    if err != nil {
        panic(err)
    }
    fmt.Printf("Created: %s/%s\n", created.GetNamespace(), created.GetName())

    // 第四步:读取资源
    fetched, err := dynamicClient.Resource(fooGVR).Namespace("default").Get(
        context.Background(),
        "my-foo",
        metav1.GetOptions{},
    )
    if err != nil {
        panic(err)
    }
    fmt.Printf("Fetched: %+v\n", fetched.Object)

    // 第五步:修改资源(使用 Unstructured 的便捷方法)
    unstructured.SetNestedField(fetched.Object, int64(5), "spec", "replicas")
    
    updated, err := dynamicClient.Resource(fooGVR).Namespace("default").Update(
        context.Background(),
        fetched,
        metav1.UpdateOptions{},
    )
    if err != nil {
        panic(err)
    }
    fmt.Printf("Updated replicas to: %d\n", 
        updated.Object["spec"].(map[string]interface{})["replicas"])

    // 第六步:列表资源
    list, err := dynamicClient.Resource(fooGVR).Namespace("default").List(
        context.Background(),
        metav1.ListOptions{},
    )
    if err != nil {
        panic(err)
    }
    fmt.Printf("Found %d Foo resources\n", len(list.Items))

    // 第七步:删除资源
    err = dynamicClient.Resource(fooGVR).Namespace("default").Delete(
        context.Background(),
        "my-foo",
        metav1.DeleteOptions{},
    )
    if err != nil {
        panic(err)
    }
    fmt.Println("Deleted my-foo")
}

六、DynamicClient 的限制

DynamicClient 虽然灵活,但也有一些限制:

限制说明解决方案
无类型安全 map[string]interface{} 没有编译时检查,容易写错字段名 使用 Unstructured 便捷方法,设置/读取嵌套字段
无代码补全 IDE 无法提供字段补全 用 typed client 处理已知的资源类型
性能稍差 每次都需要序列化/反序列化 map[string]interface{} 对于频繁操作的资源,使用 typed client

七、DynamicInformer:监听 CRD 变化

和 typed Informer 类似,DynamicClient 也有对应的 DynamicInformer,用于监听 CRD 的变化:

import "k8s.io/client-go/dynamic/dynamicinformer"
import "k8s.io/client-go/tools/cache"

func main() {
    // 创建 DynamicInformerFactory
    factory := dynamicinformer.NewFilteredDynamicSharedInformerFactory(
        dynamicClient,
        30*time.Second,      // resync 周期
        "default",           // namespace
        nil,                 // tweakListOptions
    )

    // 为 Foo CRD 创建 Informer
    fooInformer := factory.ForResource(fooGVR)
    
    // 注册事件处理函数
    fooInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
        AddFunc: func(obj interface{}) {
            u := obj.(*unstructured.Unstructured)
            fmt.Printf("Foo Added: %s\n", u.GetName())
        },
        UpdateFunc: func(old, new interface{}) {
            u := new.(*unstructured.Unstructured)
            fmt.Printf("Foo Updated: %s\n", u.GetName())
        },
        DeleteFunc: func(obj interface{}) {
            u := obj.(*unstructured.Unstructured)
            fmt.Printf("Foo Deleted: %s\n", u.GetName())
        },
    })

    // 启动 Informer
    factory.Start(ctx.Done())
    factory.WaitForCacheSync(ctx.Done())
}

八、总结

这一节我们深入理解了 DynamicClient:

  • 为什么需要 DynamicClient:操作未知类型的 CRD,无需代码生成
  • GVR 定位资源:用 GroupVersionResource 唯一标识任意资源
  • Unstructured 对象:用 map[string]interface{} 动态表示任意 JSON 数据
  • 限制:无类型安全、无代码补全、性能稍差
  • DynamicInformer:配合 DynamicInformer 监听 CRD 变化

下一节我们将学习 工具与调试方法,了解如何排查 Controller 的问题。敬请期待!


Kubernetes 编程 / Operator 专题【左扬精讲】—— DynamicClient 操作 CRD · 来源:Kubernetes v1.36.1 client-go 源码分析

posted @ 2026-06-13 18:20  左扬  阅读(2)  评论(0)    收藏  举报