跳过正文

代码生成器

·2154 字·11 分钟
目录
K8s-Codegen - 这篇文章属于一个选集。
§ 3: 本文

代码生成器
#

deepcopy-gen 代码生成器
#

deepcopy-gen 是一个自动生成 DeepCopy 相关函数的代码生成器。给定一个包的目录路径作为输入源,它可以为其生成 DeepCopy 相关函数,这些函数可以有效地执行每种类型的深复制操作。

为整个包生成 DeepCopy 相关函数时,其 Tags 形式如下。

// +k8s:deepcopy-gen=package

为单个类型生成 DeepCopy 相关函数时,其 Tags 形式如下。

// +k8s:deepcopy-gen=true

为整个包生成 DeepCopy 相关函数时,可以忽略单个类型,其 Tags 形式如下。

// +k8s:deepcopy-gen=false

在 Kubernetes 源码中,会看到 deepcopy-gen 的 Tag 被定义为 runtime.Object,deepcopy-gen 会为该类型生成返回值为 runtime.Object 类型的 DeepCopyObject 函数,代码示例如下。

代码路径:pkg/apis/abac/types.go

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// Policy contains a single ABAC policy rule
type Policy struct {
    metav1.TypeMeta `json:",inline"`
    Spec PolicySpec `json:"spec"`
}

生成以下代码。

代码路径:pkg/apis/abac/zz_generated.deepcopy.go

func (in *Policy) DeepCopyObject() runtime.Object {
    if c := in.DeepCopy(); c != nil {
        return c
    }
    return nil
}

作用:

  1. 满足 runtime.Object 接口契约;

    Kubernetes 核心系统(API Server、各种 Controller 等)需要统一处理成百上千种不同的资源(Pod、Node、Deployment 以及你自定义的 CRD)。为了实现这种统一,K8s 将所有 API 资源抽象为 runtime.Object 接口。

  2. 提供底层的“泛型”克隆能力;

1. deepcopy-gen 使用示例
#

构建 deepcopy-gen 二进制文件,并且执行 deepcopy-gen 代码生成器为 k8s.io/kubernetes/pkg/apis/abac/v1beta1 包生成 zz_generated.deepcopy.go 代码文件。

$ hack/make-rules/build.sh k8s.io/code-generator/cmd/deepcopy-gen
$ ./hack/run-in-gopath.sh _output/bin/deepcopy-gen \
  --v 1 \
  --logtostderr \
  -i "k8s.io/kubernetes/pkg/apis/abac/v1beta1" \
  --bounding-dirs k8s.io/kubernetes,"k8s.io/api" \
  -O zz_generated.deepcopy

2. deepcopy-gen 生成规则逻辑
#

deepcopy-gen 会遍历包中的所有类型,如果类型为 types.Struct,则为该类型生成 DeepCopy 相关函数。

代码路径:vendor/k8s.io/gengo/examples/deepcopy-gen/generators/deepcopy.go

func copyableType(t *types.Type) bool {
    if t.Kind != types.Struct {
        return false
    }
    return true
}

小结
#

deepcopy-gen 为每个 Struct 类型的资源生成 DeepCopy() 方法和 DeepCopyObject() 方法。

  • DeepCopy() 方法:返回同类型的深拷贝副本,方法签名如 func (in *Policy) DeepCopy() *Policy。内部将每个字段逐个复制,切片、Map、指针等复杂类型会创建新副本而非共享引用。
  • DeepCopyObject() 方法:实现 runtime.Object 接口,返回 runtime.Object 类型,使资源对象可被 K8s 核心系统统一处理。

这两个函数使得任意资源对象可以被完整复制一份独立的副本,从而在并发操作中避免共享对象产生的竞态条件。例如在 Controller 收到同一个对象的多次更新时,可以复制一份再修改而不影响原始数据。

在 K8s 架构中,deepcopy-gen 是整个代码生成体系的基石。几乎所有其他代码生成器(client-gen、lister-gen、informer-gen 等)都依赖 deepcopy-gen 生成的深拷贝能力。没有它,K8s 无法安全地实现 Controller 模式下的并发处理,也无法在客户端和缓存层之间传递独立的对象副本。

defaulter-gen 代码生成器
#

defaulter-gen 是一个自动生成 Defaulter 相关函数的代码生成器。给定一个包的目录路径作为输入源,它可以为其生成 Defaulter 相关函数,这些函数可以为资源对象生成默认的值

为拥有 TypeMeta 属性的类型生成 Defaulter 相关函数时,其 Tags 形式如下:

// +k8s:defaulter-gen=TypeMeta

为拥有 ListMeta 属性的类型生成 Defaulter 相关函数时,其 Tags 形式如下:

// +k8s:defaulter-gen=ListMeta

为拥有 ObjectMeta 属性的类型生成 Defaulter 相关函数时,其 Tags 形式如下:

// +k8s:defaulter-gen=ObjectMeta

defaulter-gen 的每一个 Tag 都是全局 Tag,没有局部 Tag。其值可以为 TypeMeta、ListMeta、ObjectMeta,其中最常用的是 TypeMeta,代码示例如下。

代码路径:pkg/apis/apps/v1/doc.go

// +k8s:defaulter-gen=TypeMeta
// +k8s:defaulter-gen-input=k8s.io/api/apps/v1

package v1 // import "k8s.io/kubernetes/pkg/apis/apps/v1"

1. defaulter-gen 使用示例
#

构建 defaulter-gen 二进制文件,并且执行 defaulter-gen 代码生成器为 k8s.io/kubernetes/pkg/apis/rbac/v1 包生成 zz_generated.defaults.go 代码文件。

$ hack/make-rules/build.sh k8s.io/code-generator/cmd/defaulter-gen
$ ./hack/run-in-gopath.sh _output/bin/defaulter-gen \
  --v 1 \
  --logtostderr \
  -i k8s.io/kubernetes/pkg/apis/rbac/v1 \
  --extra-peer-dirs k8s.io/kubernetes/pkg/apis/rbac/v1 \
  -O zz_generated.defaults

2. defaulter-gen 生成规则逻辑
#

代码路径:vendor/k8s.io/gengo/examples/defaulter-gen/generators/defaulter.go

if t.Kind == types.Struct && len(typesWith) > 0 {
    for _, field := range t.Members {
        for _, s := range typesWith {
            if field.Name == s {
                return true
            }
        }
    }
}

defaulter-gen 会遍历包中的所有类型,如果该类型拥有 +k8s:defaulter-gen=<Type> 指定的类型属性字段 (如 TypeMeta、ListMeta、ObjectMeta),则为该类型生成 Defaulter 函数和 RegisterDefaults 注册函数,代码示例如下。

代码路径:pkg/apis/rbac/v1/doc.go

// +k8s:defaulter-gen=TypeMeta
// +k8s:defaulter-gen-input=k8s.io/api/rbac/v1
package v1 // import "k8s.io/kubernetes/pkg/apis/rbac/v1"

代码路径:vendor/k8s.io/api/rbac/v1/types.go

type ClusterRoleBinding struct {
    metav1.TypeMeta `json:",inline"`
    // ...
}

生成以下代码。

代码路径:pkg/apis/rbac/v1/zz_generated.defaults.go

func SetObjectDefaults_ClusterRoleBinding(in *v1.ClusterRoleBinding) {
    // ...
}

ClusterRoleBinding 类型拥有类型为 TypeMeta 的属性字段,defaulter-gen 自动为其生成 SetObjectDefaults_ClusterRoleBinding 函数。

RegisterDefaults 会将资源对象的 Defaulter 函数注册到 Scheme 注册表,可以通过 Scheme.Default 显式地为空资源对象设置默认值,代码示例如下。

代码路径:cmd/kube-controller-manager/app/options/options.go

versioned := kubectrlmgrconfigv1alpha1.KubeControllerManagerConfiguration{}
kubectrlmgrconfigscheme.Scheme.Default(&versioned)

小结
#

defaulter-gen 为包含特定字段类型(TypeMeta、ListMeta、ObjectMeta)的资源生成 SetDefaults_*()RegisterDefaults() 函数。

  • SetDefaults_*() 函数:如 SetDefaults_ClusterRoleBinding(in *v1.ClusterRoleBinding),为对象的字段填充默认值(如 APIVersion、Kind 等)。
  • RegisterDefaults() 函数:将 Defaulter 注册到 Scheme 注册表,随后可通过 Scheme.Default(&obj) 统一调用。

这些函数使得空的资源对象在创建时就具备符合规范的默认值,无需人工逐字段设置。例如创建一个未指定任何字段的 Deployment 时,Scheme.Default 会自动为其填充 replicas、selector 等必要字段的默认值。

在 K8s 架构中,defaulter-gen 提供了声明式的默认值语义。用户只需关注自己设置的字段,其余字段由系统自动补全,降低了 API 使用成本,也保证了资源对象的规范性和一致性。

conversion-gen 代码生成器
#

conversion-gen 是一个自动生成 Convert 相关函数的代码生成器。给定一个包的目录路径作为输入源,它可以为其生成 Convert 相关函数,这些函数可以为对象在内部类型和外部类型之间提供转换函数。

为整个包生成 Convert 相关函数时,其 Tags 形式如下:

// +k8s:conversion-gen=<peer-pkg>

<peer-pkg> 定义包的导入路径,例如 k8s.io/kubernetes/pkg/apis/abac

为整个包生成 Convert 相关函数依赖额外的包时,其 Tags 形式如下:

// +k8s:conversion-gen-external-types=<type-pkg>

<type-pkg> 定义额外的包的路径,例如 k8s.io/api/autoscaling/v1

排除某个属性为其生成 Convert 相关函数时,其 Tags 形式如下:

// +k8s:conversion-gen=false

1. conversion-gen 使用示例
#

构建 conversion-gen 二进制文件,并且执行 conversion-gen 代码生成器为 k8s.io/kubernetes/pkg/apis/abac/v1beta1 包生成 zz_generated.conversion.go 代码文件。

$ hack/make-rules/build.sh k8s.io/code-generator/cmd/conversion-gen
$ ./hack/run-in-gopath.sh _output/bin/conversion-gen \
  --v 1 \
  --logtostderr \
  -i k8s.io/kubernetes/pkg/apis/abac/v1beta1 \
  --extra-peer-dirs k8s.io/kubernetes/pkg/apis/core,k8s.io/kubernetes/pkg/apis/core/v1,k8s.io/api/core/v1 \
  -O zz_generated.conversion

2. conversion-gen 生成规则逻辑
#

代码路径:vendor/k8s.io/code-generator/cmd/conversion-gen/generators/conversion.go

func (g genConversion) convertibleOnlyWithinPackage(inType, outType *types.Type) bool {
    // ...
    if t.Kind != types.Struct {
        return false
    }
    if namer.IsPrivateGoName(other.Name.Name) {
        return false
    }
    return true
}

conversion-gen 会遍历包中的所有类型,如果类型为 types.Struct 且过滤了私有的 Struct 类型,则为该类型生成 Convert 相关函数,并且为该类型同时生成 RegisterConversions 注册函数,代码示例如下。

代码路径:pkg/apis/abac/v1beta1/types.go

type Policy struct {
    metav1.TypeMeta `json:",inline"`
    Spec PolicySpec `json:"spec"`
}

生成 Convert 相关函数的代码示例如下。

代码路径:pkg/apis/abac/v1beta1/zz_generated.conversion.go

func Convert_v1beta1_Policy_To_abac_Policy(in *Policy, out *abac.Policy, s conversion.Scope) error {
    return autoConvert_v1beta1_Policy_To_abac_Policy(in, out, s)
}

func Convert_abac_Policy_To_v1beta1_Policy(in *abac.Policy, out *Policy, s conversion.Scope) error {
    return autoConvert_abac_Policy_To_v1beta1_Policy(in, out, s)
}

types.go 中定义了 Policy 类型,conversion-gen 为该类型生成 Convert 函数。例如,从 v1beta1 版本转换到 internal 内部版本,从 internal 内部版本转换到 v1 版本。定义 old 变量为 v1beta1 资源版本,通过 Convert 函数将 v1beta1 版本转换为内部版本 (即 internal 变量),代码示例如下。

old := &v1beta1.Policy{Spec: v1beta1.PolicySpec{User: "bob"}}
internal := &abac.Policy{}
abac.Scheme.Convert(old, internal, nil)

小结
#

conversion-gen 为每个 Struct 类型生成 Convert_*_To_*()RegisterConversions() 函数。

  • Convert_*_To_*() 函数:如 Convert_v1beta1_Policy_To_abac_Policy(in *Policy, out *abac.Policy, s conversion.Scope) error,负责将两个不同版本的结构体之间做字段映射和转换,去除或添加 JSON tag、版本元数据等。
  • RegisterConversions() 函数:将 Convert 函数注册到 Scheme,随后可通过 Scheme.Convert(src, dst, nil) 统一调用。

这些函数使得同一个资源对象可以在不同版本之间互相转换。例如 kube-apiserver 收到 v1beta1 版本的 Policy 后,转换为内部版本存储到 etcd;客户端查询时,再从内部版本转换回 v1beta1 返回。

在 K8s 架构中,conversion-gen 支撑了多版本 API 共存的设计。每个资源都有内部版本(用于存储)和外部版本(用于 API 传输),conversion-gen 负责在两条路径上做正确的字段转换,实现了 API 的版本解耦——用户始终使用稳定的外部版本,而存储层始终使用统一的内部版本。

openapi-gen 代码生成器
#

openapi-gen 是一个自动生成 OpenAPI 定义文件的代码生成器。给定一个包的目录路径作为输入源,它可以为其生成 OpenAPI 定义文件,该定义文件用于 kube-apiserver 上 OpenAPI 规范的生成。

为特定类型或包生成 OpenAPI 定义时,其 Tags 形式如下:

// +k8s:openapi-gen=true

排除为特定类型或包生成 OpenAPI 定义时,其 Tags 形式如下:

// +k8s:openapi-gen=false

1. openapi-gen 使用示例
#

构建 openapi-gen 二进制文件,并且执行 openapi-gen 代码生成器为 k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1 包生成 zz_generated.openapi.go 代码文件并将其存放在 k8s.io/kubernetes/pkg/generated/openapi 目录下。

$hack/make-rules/build.sh k8s.io/code-generator/cmd/openapi-gen$ ./hack/run-in-gopath.sh _output/bin/openapi-gen \
  --v 1 \
  --logtostderr \
  -i k8s.io/kubernetes/vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1 \
  -p k8s.io/kubernetes/pkg/generated/openapi \
  -O zz_generated.openapi \
  -h vendor/k8s.io/code-generator/hack/boilerplate.go.txt \
  -r _output/KUBE_violations.report

2. openapi-gen 生成规则逻辑
#

代码路径:vendor/k8s.io/kube-openapi/pkg/generators/openapi.go

func (g openAPITypeWriter) generate(t *types.Type) error {
    switch t.Kind {
    case types.Struct:
        if hasOpenAPIDefinitionMethod(t) {
            return nil
        }
    }
    return nil
}

openapi-gen 会遍历包中的所有类型,如果类型为 types.Struct 且忽略其他类型,则为 types.Struct 类型生成 OpenAPI 定义文件,代码示例如下。

代码路径:vendor/k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1/types.go

type CustomResourceDefinitionSpec struct {
    Group string `json:"group" protobuf:"bytes,1,opt,name=group"`
    Version string `json:"version,omitempty" protobuf:"bytes,2,opt,name=version"`
}

types.go 中定义了 CustomResourceDefinitionSpec 类型,openapi-gen 为该类型生成 OpenAPIDefinition。生成 OpenAPIDefinition 的代码示例如下。

代码路径:pkg/generated/openapi/zz_generated.openapi.go

func GetOpenAPIDefinitions(ref common.ReferenceCallback) map[string]common.OpenAPIDefinition {
    return map[string]common.OpenAPIDefinition{
        "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1.CustomResourceDefinitionSpec": schema_pkg_apis_apiextensions_v1beta1_CustomResourceDefinitionSpec(ref),
    }
}

小结
#

openapi-gen 为每个 Struct 类型生成 GetOpenAPIDefinitions() 函数,返回该包中所有类型的 OpenAPI 规范定义。

GetOpenAPIDefinitions() 返回一个 map,key 为类型的完全限定名(如 CustomResourceDefinitionSpec),value 为该类型的 OpenAPI 定义,描述了每个字段的名称、类型、描述、是否必填、默认值等元信息。

这些定义使得 kube-apiserver 能够提供完整的 OpenAPI 接口。kubectl 可以通过 kubectl explain 查看资源字段说明,客户端工具可以自动生成对应语言的 SDK,API 文档站点也可以动态生成所有资源的 API Reference。

在 K8s 架构中,openapi-gen 提供了元编程级别的自描述能力。K8s 本身就是一个"用代码生成代码"的系统,OpenAPI 定义使得所有资源的 schema 信息对外可见,从而支撑了 kubectl、client-gen、自动生成 SDK 等一系列上游工具链的存在。

prerelease-lifecycle-gen 代码生成器
#

prerelease-lifecycle-gen 是一个为 beta 资源自动生成 APILifecycle 相关函数的代码生成器。给定一个包的目录路径作为输入源,它可以为其生成 APILifecycle 相关函数,这些函数可以有效地管理每种资源的当前版本、弃用版本、删除版本及替换版本。

为整个包生成 APILifecycle 相关函数时,可以忽略单个类型,其 Tags 形式如下:

// +k8s:prerelease-lifecycle-gen=true

为单个类型生成 APILifecycle 相关函数时,指定当前版本、弃用版本、删除版本,其 Tags 形式如下:

// +k8s:prerelease-lifecycle-gen:introduced=<version>
// +k8s:prerelease-lifecycle-gen:deprecated=<version>
// +k8s:prerelease-lifecycle-gen:removed=<version>

为单个类型生成 APILifecycle 相关函数时,指定替换版本,其 Tags 形式如下:

// +k8s:prerelease-lifecycle-gen:replacement=<group>,<version>,<Kind> (例如: rbac.authorization.k8s.io,v1,Role)

1. prerelease-lifecycle-gen 使用示例
#

构建 prerelease-lifecycle-gen 二进制文件,并且执行 prerelease-lifecycle-gen 代码生成器为 k8s.io/kubernetes/vendor/k8s.io/api/rbac/v1beta1 包生成 zz_generated.prerelease-lifecycle.go 代码文件。

$hack/make-rules/build.sh k8s.io/code-generator/cmd/prerelease-lifecycle-gen$ ./hack/run-in-gopath.sh _output/bin/prerelease-lifecycle-gen \
  --v 1 \
  --logtostderr \
  -i k8s.io/kubernetes/vendor/k8s.io/api/rbac/v1beta1 \
  -O zz_generated.prerelease-lifecycle

2. prerelease-lifecycle-gen 生成规则逻辑
#

prerelease-lifecycle-gen 会遍历包中的所有类型,如果类型为 types.Struct,则为该类型生成 APILifecycle 相关函数。

代码路径:vendor/k8s.io/code-generator/cmd/prerelease-lifecycle-gen/prerelease-lifecycle-generators/status.go

func isAPIType(t *types.Type) bool {
    if t.Kind != types.Struct {
        return false
    }
    return true
}

生成的 APILifecycle 相关函数的代码示例如下。

代码路径:vendor/k8s.io/api/rbac/v1beta1/zz_generated.prerelease-lifecycle.go

func (in RoleList) APILifecycleRemoved() (major, minor int) {
    return 1, 22
}

小结
#

prerelease-lifecycle-gen 为每个 beta 资源生成 APILifecycleIntroduced()APILifecycleDeprecated()APILifecycleRemoved()APILifecycleReplacement() 函数。

这些函数返回具体的版本号,例如 APILifecycleRemoved() (major, minor int) { return 1, 22 } 表示该 API 在 v1.22 被移除。kube-apiserver 在处理请求时会调用这些函数,当请求使用已弃用或已移除的 API 版本时,会返回警告或拒绝。

这些函数使得 API 版本生命周期可被程序化检测和处理,帮助集群管理员和开发者及时将应用迁移到新版本的 API,避免在 K8s 升级后突然失效。

在 K8s 架构中,prerelease-lifecycle-gen 将 API 演进的策略从运行时契约变成了编译时可见的代码。这使得 K8s 能够安全地淘汰旧版本 API——通过弃用周期告知用户即将移除的 API,同时在代码层面提供精确的版本判断逻辑。

client-gen 代码生成器
#

client-gen 代码生成器是一种为资源生成 ClientSet 客户端的工具。ClientSet 对资源组、资源版本、资源进行了封装,可以针对资源执行生产资源操作方法 (Create、Update、Delete、Get 等),这些方法由 client-gen 代码生成器自动生成。

client-gen 代码生成器的代码生成策略与其他代码生成器的类似,都是通过 Tags 来识别一个包是否需要生成代码及确定生成代码的方式。

与其他代码生成器相比,client-gen 代码生成器支持的 Tags 更多,可扩展性非常高。除了生成基本的资源操作方法 (Verbs),例如 Create、Update、Delete、Get、List、Patch、Watch 等,如果存在 Status 字段,还能生成 UpdateStatus 函数。

生成基本的资源操作方法 (Verbs),其 Tags 形式如下:

// +genclient

生成基本的资源操作方法,不生成 Namespaced 函数,其 Tags 形式如下:

// +genclient:nonNamespaced

即便存在 Status 字段,也不生成 UpdateStatus 函数,其 Tags 形式如下:

// +genclient:noStatus

不生成基本的资源操作方法 (Verbs),其 Tags 形式如下:

// +genclient:noVerbs

仅生成指定的基本操作方法,如 Create、Get,其 Tags 形式如下:

// +genclient:onlyVerbs=create,get

生成基本的资源操作方法 (Verbs),但排除 Get、Update 方法,其 Tags 形式如下:

// +genclient:skipVerbs=get,update

生成相关扩展 (Extensions) 函数,如生成 UpdateScale 函数。在 UpdateScale 函数中会对 scale 子资源进行更新操作。input 和 result 是用于设置输入和输出的参数,其 Tags 形式如下:

// +genclient:method=UpdateScale,verb=update,subresource=scale,input=Scale,result=Scale

1. client-gen 使用示例
#

构建 client-gen 二进制文件,执行 client-gen 二进制文件并将给定资源的目录路径作为输入源,解析输入源中的 Tags,生成 ClientSet 代码。构建 client-gen 二进制文件的命令如下。

$ make all WHAT=vendor/k8s.io/code-generator/cmd/client-gen

构建完成后,将 client-gen 二进制文件存放在 _output/bin/client-gen 下。将 k8s.io/kubernetes/vendor/k8s.io/api 下的资源目录作为输入源,使用 --input 指定需要生成 ClientSet 的资源组和资源版本列表,执行命令如下。

$ ./_output/bin/client-gen \
  --input-base=k8s.io/kubernetes/vendor/k8s.io/api \
  --input=core/v1,apps/v1 \
  --output-base=$GOPATH/src/k8s.io/kubernetes/vendor \
  --output-package=k8s.io/client-go \
  --clientset-name=kubernetes \
  --go-header-file=./hack/boilerplate/boilerplate.generatego.txt

执行以上命令后,会在 $GOPATH/src/k8s.io/kubernetes/vendor/k8s.io/client-go/kubernetes/typed 下生成 core/v1 和 apps/v1 两个资源组与资源版本的 ClientSet 代码。

除了手动构建 client-gen 代码生成器并生成代码,也可以执行 Kubernetes 提供的代码生成脚本,后者的生成代码的过程与上述过程完全相同。Kubernetes 代码生成脚本的路径为 hack/update-codegen.sh

client-gen 二进制文件常用的输入参数如表所示。

参数说明
--input-baseapi 资源组的基本路径,默认为 k8s.io/kubernetes/pkg/apis
--input用于生成客户端的资源组和资源版本列表,每个资源组可以指定最多一个资源版本,多个资源组和资源版本之间使用逗号分隔,例如 group1/version1,group2/version2…
--output-base生成文件的基本路径(默认值为 $GOPATH/src,如果未设置 $GOPATH 环境变量,则默认值为 ./)
--output-package输出的基本包路径,默认值为 k8s.io/kubernetes/pkg/client/clientset_generated/
--clientset-name生成的 ClientSet 包名称,默认值为 internalclientset
--go-header-file指定许可证样式(License Boilerplate)文件。该文件用于存放开源软件作者及开源协议等信息(默认值为 vendor/k8s.io/code-generator/hack/boilerplate.go.txt)

2. client-gen 生成规则
#

以 Pod 资源对象为例,Pod 资源对象定义 //+genclient 标签,该标签负责生成 Pod 资源对象基本的资源操作方法 (Verbs),如 Create、Update、Delete、Get、List、Patch、Watch 等,代码示例如下。

代码路径:vendor/k8s.io/api/core/v1/types.go

// +genclient
// +genclient:method=UpdateEphemeralContainers,verb=update,subresource=ephemeralcontainers

type Pod struct {
    metav1.TypeMeta
    metav1.ObjectMeta
    Spec PodSpec
    Status PodStatus
}

每一个代码生成器都提供了一个 GenerateType 函数,它实现了代码生成器的核心逻辑。在 client-gen 中用于生成资源客户端类 (如 vendor/k8s.io/client-go/kubernetes/typed/core/v1/pod.go) 的代码生成器 (genClientForType) 函数中,util.ParseClientGenTags 会解析并验证 Tags,当 Tags 中含有 get 代码标记时,为资源对象生成 Get 方法,代码示例如下。

代码路径:vendor/k8s.io/code-generators/cmd/client-gen/generators/generator_for_type.go

func (g *genClientForType) GenerateType(c *generator.Context, t *types.Type, w io.Writer) error {
    sw := generator.NewSnippetWriter(w, c, "$", "$")
    pkg := filepath.Base(t.Name.Package)
    tags, err := util.ParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
    // ...
    if tags.HasVerb("get") {
        sw.Do(getTemplate, m)
    }
    // ...
}

tags.HasVerb("get") 含有 get 代码标记时 (示例中的 Pod 资源对象未使用 noVerbs、onlyVerbs、skipVerbs 指定任何操作方法,因此会包含 get 代码标记),为资源对象生成 Get 方法,通过 Go 语言标准库的 text/template 模板将资源信息渲染到 getTemplate 模板中,该模板代码进行变量替换生成 Get 方法的示例如图 13-1 所示。

image-20260516013812201

小结
#

client-gen 为每个资源生成对应的 ClientSet 客户端类,包含 Get()List()Create()Update()Delete()Patch()Watch() 等方法。

以 Pod 为例,生成的 pods.Get() 方法封装了 GET /api/v1/namespaces/{namespace}/pods/{name} 的 HTTP 请求,pods.List() 封装了 GET /api/v1/pods 的请求。这些方法返回强类型的资源对象(如 *v1.Pod),而非 runtime.Object 静态类型。

这些函数使得开发者可以通过强类型接口操作 K8s 资源,无需手动构造 HTTP 请求和解析响应。例如 client.CoreV1().Pods("default").Get("my-pod", metav1.GetOptions{}) 直接返回 *v1.Pod 对象。

在 K8s 架构中,client-gen 将 RESTful API 封装成了人类可用的强类型SDK。所有 K8s 的客户端工具(kubectl、controller-manager、operator 框架等)都构建在 client-gen 生成的客户端之上,使得与集群交互的开发工作从 HTTP 协议层面提升到了应用层。

lister-gen 代码生成器
#

lister-gen 代码生成器是一种为资源生成 Lister 的工具。Lister 为每一个 Kubernetes 资源提供 Lister 功能 (即提供 Get 和 List 方法)。Get 和 List 方法为客户端提供只读的本地缓存数据。以上功能由 lister-gen 代码生成器提供。

lister-gen 代码生成器的代码生成策略与其他代码生成器的类似,都是通过 Tags 来识别一个包是否需要生成代码及确定生成代码的方式。

与其他代码生成器相比,lister-gen 代码生成器并没有独立的 Tags,它依赖于 client-gen 代码生成器的 //+genclient 标签。

1. lister-gen 使用示例
#

构建 lister-gen 二进制文件,执行 lister-gen 二进制文件并将给定资源的目录路径作为输入源。构建 lister-gen 二进制文件的命令如下。

$ make all WHAT=vendor/k8s.io/code-generator/cmd/lister-gen

构建完成后,将 lister-gen 二进制文件存放在 _output/bin/lister-gen 下。生成代码过程的执行命令如下。

$ ./_output/bin/lister-gen \
  --output-base=$GOPATH/src/k8s.io/kubernetes/vendor \
  --output-package=k8s.io/client-go/listers \
  --input-dirs=k8s.io/api/core/v1,k8s.io/api/apps/v1

执行以上命令后,会在 $GOPATH/src/k8s.io/kubernetes/vendor/k8s.io/client-go/listers 下生成 core/v1 和 apps/v1 资源组和资源版本的 Listers 代码。

除了手动构建 lister-gen 代码生成器并生成代码,也可以执行 Kubernetes 提供的代码生成脚本,后者的生成代码的过程与上述过程完全相同。Kubernetes 代码生成脚本的路径为 hack/update-codegen.sh

lister-gen 二进制文件常用的输入参数如表所示。

参数说明
--output-base生成文件的基本路径(默认值为 $GOPATH/src,如果未设置 $GOPATH 环境变量,则默认值为 ./)
--output-package输出的基本包路径,默认值为 k8s.io/kubernetes/pkg/client/clientset_generated/
--input-dirs输入源,以逗号分隔的导入路径
--go-header-file指定许可证样式(License Boilerplate)文件。该文件用于存放开源软件作者及开源协议等信息(默认值为 vendor/k8s.io/code-generator/hack/boilerplate.go.txt)

2. lister-gen 生成规则
#

lister-gen 代码生成器并没有独立的 Tags,它依赖于 client-gen 代码生成器的 //+genclient 标签。当判断是否需要生成某资源类型时,有 3 个过滤条件:资源类型拥有 //+genclient 标签,资源类型拥有 list 字段,资源类型拥有 get 字段。满足过滤条件以后,为该资源类型生成 List 和 Get 方法。代码示例如下。

代码路径:vendor/k8s.io/code-generators/cmd/lister-gen/generators/lister.go

func Packages(context generator.Context, arguments *args.GeneratorArgs) generator.Packages {
    var typesToGenerate []*types.Type
    for _, t := range p.Types {
        tags := util.MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
        if !tags.GenerateClient || !tags.HasVerb("list") || !tags.HasVerb("get") {
            continue
        }
        typesToGenerate = append(typesToGenerate, t)
    }
    // ...
}

在生成资源的 Lister 时,lister-gen 会使用两个主要的代码生成器,分别为 expansionGenerator 和 listerGenerator。

expansionGenerator 用于为每一个资源生成 Lister 扩展接口,并且放在资源组和资源版本目录下的 expansion_generated.go 文件中。以为 Pod 生成资源扩展接口的 PodListerExpansion 为例,代码示例如下。

代码路径:$GOPATH/src/k8s.io/kubernetes/vendor/k8s.io/client-go/listers/core/v1/expansion_generated.go

package v1
// PodListerExpansion allows custom methods to be added to
// PodLister.
type PodListerExpansion interface {}

listerGenerator 用于为每个资源生成对应的 Lister 接口和实现类。以 Pod 资源对象为例,listerGenerator 会使用 Go 语言标准库的 text/template 模板描述对应的 List 方法,经过渲染并生成最终的 List 方法,如图 13-2 所示。

image-20260516013838556

小结
#

lister-gen 为每个资源生成 *Lister 接口和实现类,提供 Get(name string)List() 两个只读方法。

以 PodLister 为例,podLister.Get("my-pod") 从本地缓存中查找指定名称的 Pod(而非每次请求 kube-apiserver),podLister.List() 返回缓存中所有的 Pod 列表。Lister 的数据来源于 Informer 的本地缓存(DeltaFIFO 队列),通过 Indexer 建立索引。

这些函数使得 Controller 可以在不发送网络请求的情况下读取资源数据,大幅降低了 kube-apiserver 的负载。例如在 Deployment Controller 的 reconcile 循环中,每次检查 ReplicaSet 数量时直接从 Lister 读取,而非发起一次 List 请求。

在 K8s 架构中,lister-gen 是 Controller 性能优化的关键一环。Informers 预先同步并缓存了集群状态,Lister 提供了只读的随机访问接口,共同构成了"旁路缓存"模式——Controller 始终读取本地副本而非直接查询 API Server,使得即使在数千个 Controller 同时运行的集群中,kube-apiserver 也不会过载。

informer-gen 代码生成器
#

informer-gen 代码生成器为资源生成 Informer。Informer 为 client-go 提供了与 kube-apiserver 通信的机制,这些功能由 informer-gen 代码生成器自动生成。

informer-gen 代码生成器的代码生成策略与其他代码生成器的类似,都是通过 Tags 来识别一个包是否需要生成代码及确定生成代码的方式。

与其他代码生成器相比,informer-gen 代码生成器并没有可用的 Tags,它依赖于 client-gen 代码生成器的 //+genclient 标签,类似于 lister-gen 代码生成器。

1. informer-gen 使用示例
#

构建 informer-gen 二进制文件,执行 informer-gen 二进制文件并将给定资源的目录路径作为输入源。构建 informer-gen 二进制文件的命令如下。

$ make all WHAT=vendor/k8s.io/code-generator/cmd/informer-gen

构建完成后,informer-gen 二进制文件存放在 _output/bin/informer-gen 下。生成代码过程的执行命令如下。

$ ./_output/bin/informer-gen \
  --output-base=$GOPATH/src/k8s.io/kubernetes/vendor \
  --output-package=k8s.io/client-go/informers \
  --single-directory \
  --listers-package=k8s.io/client-go/listers \
  --input-dirs=k8s.io/api/core/v1,k8s.io/api/apps/v1

执行以上命令后,会在 $GOPATH/src/k8s.io/kubernetes/vendor/k8s.io/client-go/informers 下生成 core/v1 和 apps/v1 资源组和资源版本的 Informer。

除了手动构建 informer-gen 代码生成器并生成代码,也可以执行 Kubernetes 提供的代码生成脚本,后者的生成代码的过程与上述过程完全相同。Kubernetes 代码生成脚本的路径为 hack/update-codegen.sh

informer-gen 二进制文件常用的输入参数如表所示。

参数说明
--output-base生成文件的基本路径(默认值为 $GOPATH/src,如果 $GOPATH 环境变量未设置,则默认值为 ./)
--output-package输出的基本包路径,默认值为 k8s.io/kubernetes/pkg/client/informer_generated/
--single-directory如果为 true,则忽略 internalversion 和 externalversions 子目录
--input-dirs输入源,以逗号分隔的导入路径
--versioned-clientset-package依赖的 ClientSet 完整包路径(默认值为 k8s.io/kubernetes/pkg/client/clientset_generated/clientset)
--listers-package依赖的 Listers 完整包路径(默认值为 k8s.io/kubernetes/pkg/client/listers)
--go-header-file指定许可证样式(License Boilerplate)文件。该文件用于存放开源软件作者及开源协议等信息(默认值为 vendor/k8s.io/code-generator/hack/boilerplate.go.txt)

2. informer-gen 生成规则
#

informer-gen 代码生成器本身并没有可用的 Tags,它依赖于 client-gen 代码生成器的 //+genclient 标签。当判断是否需要生成某资源类型时,有 4 个过滤条件:资源类型拥有 //+genclient 标签,资源类型不能拥有 //+genclient:noVerbs 标签,资源类型拥有 list 字段,资源类型拥有 watch 字段。满足过滤条件以后,为该资源类型生成 Informer 相关函数。代码示例如下。

代码路径:vendor/k8s.io/code-generators/cmd/informer-gen/generators/packages.go

func Packages(...) generator.Packages {
    var typesToGenerate []*types.Type
    for _, t := range p.Types {
        tags := util.MustParseClientGenTags(append(t.SecondClosestCommentLines, t.CommentLines...))
        if !tags.GenerateClient || tags.NoVerbs || !tags.HasVerb("list") || !tags.HasVerb("watch") {
            continue
        }
        typesToGenerate = append(typesToGenerate, t)
    }
    // ...
}

小结
#

informer-gen 为每个资源生成完整的 Informer 组件,包括 *Informer(事件源)、SharedIndexInformer(带索引的事件监听器)、SharedInformerFactory(工厂类)以及 ResourceEventHandler(事件处理接口)。

Informer 通过 Watch API 实时监听 kube-apiserver 的资源变化,当创建/修改/删除资源时,将变化写入 DeltaFIFO 队列。开发者通过注册 ResourceEventHandler 的回调函数(OnAdd/OnUpdate/OnDelete)在资源变化时获得通知。Informer 还会驱动 Lister 和 Indexer 的数据更新。

这些函数使得 Controller 可以实时响应集群状态的变化,而无需自己实现一套 Watch + 重试 + 队列的逻辑。例如 Deployment Controller 注册了 Pod 的 Informer 后,当有任何 Pod 创建或删除时,Informer 会自动回调 Controller 的处理函数,触发一次 reconcile。

在 K8s 架构中,informer-gen 是 client-go 的核心支柱,也是整个 K8s Controller 模式的基础设施。所有 K8s 内置 Controller(Deployment、ReplicaSet、StatefulSet 等)和绝大多数 Operator 框架都构建在 Informer 之上。Informers 将"与 kube-apiserver 保持长连接 + 本地缓存 + 事件回调"这一套通用模式封装成了可复用的代码生成产物,大幅降低了 Controller 的开发门槛。

applyconfiguration-gen 代码生成器
#

applyconfiguration-gen 代码生成器用于为资源的 Apply 和 ApplyStatus 函数生成专属的 ApplyConfiguration 资源配置应用类型。

client-go 中的 Apply 和 ApplyStatus 函数支持使用直接的、类型安全的方式对每一类资源执行服务端应用 (Server-side Apply) 操作,该操作使用 ApplyConfiguration 类型作为资源专属的强类型请求参数。以 HPA 为例,在 client-go 中执行 Apply 操作的使用示例如下。

import (
    ...
    v1ac "k8s.io/client-go/applyconfigurations/autoscaling/v1"
)

hpaApplyConfig := v1ac.HorizontalPodAutoscaler(autoscalerName, ns).
    WithSpec(v1ac.HorizontalPodAutoscalerSpec().
        WithMinReplicas(0))

return hpav1client.Apply(ctx, hpaApplyConfig, metav1.ApplyOptions{FieldManager: "mycontroller", Force: true})

HPA 专属的 ApplyConfiguration 资源配置应用类型为 v1ac.HorizontalPodAutoscaler,它提供了一系列的 WithXXX 工具方法以支持设置当前操作关注的资源对象属性值。在使用 ClientSet 中的 hpav1client 执行 Apply 函数进行服务端应用操作时,可以将 hpaApplyConfig 资源配置应用对象作为请求参数,并且将该对象提交到 kube-apiserver 实现资源属性的变更和应用。

applyconfiguration-gen 代码生成器的代码生成策略与其他代码生成器的类似,都是通过 Tags 来识别一个包是否需要生成代码及确定生成代码的方式。

与其他代码生成器相比,applyconfiguration-gen 代码生成器并没有可用的 Tags,它依赖于 client-gen 代码生成器的 //+genclient 标签,类似于 lister-gen 代码生成器和 informer-gen 代码生成器。

下面介绍 applyconfiguration-gen 的使用示例和生成规则。

1. applyconfiguration-gen 使用示例
#

构建 applyconfiguration-gen 二进制文件,执行 applyconfiguration-gen 二进制文件并将给定资源的目录路径作为输入源。构建 applyconfiguration-gen 二进制文件的命令如下。

$ make all WHAT=vendor/k8s.io/code-generator/cmd/applyconfiguration-gen

构建完成后,applyconfiguration-gen 二进制文件存放在 _output/bin/applyconfiguration-gen 下 (注:原书内容此处存在笔误,写成了 informer-gen)。生成代码过程的执行命令如下。

$ ./_output/bin/applyconfiguration-gen \
  --output-base=$GOPATH/src/k8s.io/kubernetes/vendor \
  --output-package=k8s.io/client-go/applyconfigurations \
  --input-dirs=k8s.io/api/core/v1,k8s.io/api/apps/v1

执行以上命令,会在 $GOPATH/src/k8s.io/kubernetes/vendor/k8s.io/client-go/applyconfigurations 下生成 core/v1 和 apps/v1 资源组和资源版本的 ApplyConfigurations。

除了手动构建 applyconfiguration-gen 代码生成器并生成代码,也可以执行 Kubernetes 提供的代码生成脚本,后者的生成代码的过程与上述过程完全相同。Kubernetes 代码生成脚本的路径为 hack/update-codegen.sh

applyconfiguration-gen 二进制文件常用的输入参数,如表所示。

参数说明
--output-base生成文件的基本路径(默认值为 $GOPATH/src,如果 $GOPATH 环境变量未设置,则默认值为 ./)
--output-package输出的基本包路径,默认值为 k8s.io/kubernetes/pkg/client/informer_generated/
--input-dirs输入源,以逗号分隔的导入路径
--openapi-schemaopenapi 注册表的路径,在 openapi 注册表中存储了所有期望生成对应 ApplyConfiguration 的资源类型
--go-header-file指定许可证样式(License Boilerplate)文件。该文件用于存放开源软件作者及开源协议等信息(默认值为 vendor/k8s.io/code-generator/hack/boilerplate.go.txt)

2. applyconfiguration-gen 生成规则
#

applyconfiguration-gen 代码生成器并没有本身可用的 Tags,它依赖于 client-gen 代码生成器的 //+genclient 标签。当判断是否需要生成某资源类型时,Packages 函数调用的 refGraphForReachableTypes 函数会使用两个过滤条件:资源类型拥有 //+genclient 标签,资源类型拥有 apply 或 applyStatus 字段。

满足过滤条件后,递归查找到与该资源类型相关的所有类型,查找的资源类型范围包括:集合的元素类型,使用 type alias 方式定义关联的底层类型,Map 类型的主键类型,结构体类型的所有字段类型。

在这些类型中,会进一步筛选出包含 JSON 标签的 Struct 结构体类型,作为最终需要生成 ApplyConfiguration 代码的资源类型。

代码示例如下。

代码路径:vendor/k8s.io/code-generators/cmd/applyconfiguration-gen/generators/refgraph.go

func refGraphForReachableTypes(universe types.Universe, pkgTypes map[string]*types.Package, initialTypes map[types.Name]string) refGraph {
    var refs refGraph = initialTypes
    reachableTypes := map[types.Name]*types.Type{}

    for _, p := range pkgTypes {
        for _, t := range p.Types {
            tags := genclientTags(t)
            hasApply := tags.HasVerb("apply") || tags.HasVerb("applyStatus")
            if tags.GenerateClient && hasApply {
                findReachableTypes(t, reachableTypes)
            }
            ...
        }
    }

    for pkg, p := range pkgTypes {
        for _, t := range p.Types {
            if _, ok := reachableTypes[t.Name]; !ok {
                continue
            }
            if requiresApplyConfiguration(t) {
                refs[t.Name] = pkg
            }
        }
    }
    return refs
}

如上述代码所示,findReachableTypes 函数用于找到所有可达的类型列表,requiresApplyConfiguration 函数用于从找到的类型列表中过滤出满足要求的类型。

小结
#

applyconfiguration-gen 为每个资源生成链式构造器类型(ApplyConfiguration),包含 With*() 系列方法和 Apply() 方法。

以 HPA 为例,生成的 HorizontalPodAutoscalerApplyConfiguration 可以通过链式调用构建请求体:HorizontalPodAutoscaler(name, ns).WithSpec(HorizontalPodAutoscalerSpec().WithMinReplicas(0))。构造完成后传给 Apply() 方法,以服务端应用模式向 kube-apiserver 提交。

这些函数使得强类型的 Apply 操作成为可能。不同于 JSON Patch 的脆弱(字段名拼写错误无法被编译器检测),ApplyConfiguration 通过 WithMinReplicas() 等编译期安全的方法构造请求体,同时记录字段管理者,解决了多 Manager 同时管理同一资源时的冲突问题。

在 K8s 架构中,applyconfiguration-gen 补全了 K8s 1.16+ 引入的 Server-side Apply 所需的基础代码生成。与 client-gen 生成的 Update/Patch 方法相比,ApplyConfiguration 提供了声明式的、字段级别的更新语义,使得 Operator 开发者可以精确地声明自己管理的字段,而不会意外覆盖其他管理器的配置。这是 K8s 从命令式到声明式转变的最后一块拼图。

凉柠
作者
凉柠
专注于 Kubernetes、分布式系统与 AI Agent 架构探索。
K8s-Codegen - 这篇文章属于一个选集。
§ 3: 本文