本文为 K8s API 和控制器 系列文章之一

☸️ Kubernetes code-generator

K8s 中大量使用了代码生成,尤其是 API、控制器和客户端。Generators 统一放置在子项目 kubernetes/code-generator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
~/go/src/k8s.io/kubernetes $ tree staging/src/k8s.io/code-generator/cmd -L 1
staging/src/k8s.io/code-generator/cmd
├── applyconfiguration-gen
├── client-gen
├── conversion-gen
├── deepcopy-gen
├── defaulter-gen
├── go-to-protobuf
├── informer-gen
├── lister-gen
├── openapi-gen
├── register-gen

K8s 之上的三方项目,也可以(且通常)会利用这些 generators,生成控制器相关代码和客户端代码。

如果项目基于 CustomResourceDefinitions (CRD) 做开发,经常用到的是 deepcopy-genregister-gen

如果要开发 custom apiserver,则还会经常用到 defaulter-gen conversion-gen openapi-gen go-to-protobuf

无论通过 CRD 还是 custom apiserver,都可能要为 API 生成 Go 客户端,也就会用到 applyconfiguration-gen client-gen lister-gen informer-gen

多语言客户端,可以利用 openapi-gen 生成 OpenAPI Specification JSON 文件,再利用 OpenAPI Specification 生成任意语言客户端代码。

✍️ Prepare API Structs

K8s 相关项目对外 API 都会包含 3 文件:types.go doc.goregister.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
api                           # external APIs     
└── hello.zeng.dev
    ├── v1
       ├── defaults.go       # 提供用户默认函数 func SetDefaults_TYPE
       ├── doc.go            # packge 级别生成声明
       ├── register.go       # 注册 API Structs, generated funcs 到 K8s Schema,提供通用的序列化/反序列化能力
       ├── types.go          # 提供 API Structs
    └── v2
        ├── doc.go
        ├── register.go
        └── types.go

doc.go 在代码生成中最为重要。它声明了按照 package 维度,为所有 structs 提供生成声明

1
2
3
4
5
6
7
// +k8s:openapi-gen=true ➡️ 生成 OpenAPI 相关
// +k8s:deepcopy-gen=package ➡️ 生成 deepcopy funcs
// +k8s:protobuf-gen=package ➡️ 生成 protobuf 定义和并从 protobuf 定义生成 protobuf funcs
// +k8s:defaulter-gen=TypeMeta ➡️ 生成 default funcs,如果 package 下存在 defaults.go 且存在函数签名 `func SetDefaults_TYPE(obj *TYPE)`
// +groupName=hello.zeng.dev // API Group,可供 register-gen 读取并使用

package v1

types.go 包含了 kubernetes/code-generator 注释 tag

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
// +genclient ➡️ 生成客户端相关 client informer lister 以及 applyconfiguration
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object ➡️ 声明生成 
//   func (in *Foo) DeepCopyObject() runtime.Object (实现 interface k8s.io/apimachinery/pkg/runtime.Object

type Foo struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	Spec FooSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
}
...

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object ➡️ 声明生成 func (in *FooList) DeepCopyObject() runtime.Object

type FooList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

	Items []Foo `json:"items" protobuf:"bytes,2,rep,name=items"`
}

register.go 提供注册到 runtime.Scheme 的函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package v2

const GroupName = "hello.zeng.dev"

// SchemeGroupVersion is group version used to register these objects
var SchemeGroupVersion = schema.GroupVersion{Group: GroupName, Version: "v2"}

var (
	SchemeBuilder      = runtime.NewSchemeBuilder(addKnownTypes)
	// AddToScheme adds this group to a scheme.
	AddToScheme = localSchemeBuilder.AddToScheme
)

// Adds the list of known types to the given scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&Foo{},
		&FooList{},
	)
	// add common meta types (i.e WatchEvent, ListOptions, ...) to the SchemeGroupVersion
	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
	return nil
}

观察一下官方项目,可以注意到大部分 register.go 都是重复,因此也可以自动生成。kubernetes/code-generator 提供的 register-gen 可以读取 doc.go // +groupName 并生成 register 文件。rancher/wrangler controller-gen 是这样干的。kubernetes-sigs/kubebuilder 会生成一份 groupversion_info.go。

💪 Helper Generators

register-gen

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# pwd 
# github.com/phosae/x-kubernetes/api
inputs=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1")
inputs+=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v2")

register-gen \
    -O zz_generated.register \
    --go-header-file /tmp/fake-boilerplate.txt \
    --output-base /go/src \
    "${inputs[@]}"

# generated outputs:
# - github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1/zz_generated.register.go
# - github.com/phosae/x-kubernetes/api/hello.zeng.dev/v2/zz_generated.register.go

deepcopy-gen

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# pwd 
# github.com/phosae/x-kubernetes/api
inputs=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1")
inputs+=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v2")

deepcopy-gen \
    -O zz_generated.deepcopy \
    --go-header-file /tmp/fake-boilerplate.txt \
    --output-base /go/src \
    "${inputs[@]}"

# generated outputs:
# - github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1/zz_generated.deepcopy.go
# - github.com/phosae/x-kubernetes/api/hello.zeng.dev/v2/zz_generated.deepcopy.go

常见的两种注释式声明

  • package level: // +k8s:deepcopy-gen=package
  • struct level: // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

凡要集成 API 到 K8s 编解码库,对应的 Go struct 都需要实现 apimachinery/interface runtime.Object

1
2
3
4
5
6
7
8
// Object interface must be supported by all API types registered with Scheme. Since objects in a scheme are
// expected to be serialized to the wire, the interface an Object must provide to the Scheme allows
// serializers to set the kind, version, and group the object is represented as. An Object may choose
// to return a no-op ObjectKindAccessor in cases where it is not expected to be serialized.
type Object interface {
	GetObjectKind() schema.ObjectKind
	DeepCopyObject() Object
}

API struct 一般会直接内嵌 TypeMeta(这样就继承了 func GetObjectKind() schema.ObjectKind),所以通常还缺少 func DeepCopyObject() Object

注释 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object 就是告诉 deepcopy-gen,这个 struct 需要实现 apimachinery/interface runtime.Object

1
2
3
4
5
6
7
8
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type Foo struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`
	Spec FooSpec `json:"spec"`
}

生成文件一般和 types.go 同级,名字默认是 zz_generated.deepcopy.go

同时,注意到 doc.go 中有 package 级别声明

1
2
// +k8s:deepcopy-gen=package
package v1

这个声明告诉 deepcopy-gen 生成基础的深度拷贝函数,生成时按照结构体字段递归为所有复合 struct 生成 DeepCopyInto 和 DeepCopy

前面的 func DeepCopyObject() Object 就会调用到 func (in *Foo) DeepCopy() *Foo

在 controller、apiserver 编写中,也会经常用到 deepcopy funcs

1
2
3
4
5
6
7
func (in *Foo) DeepCopyInto(out *Foo) 

func (in *Foo) DeepCopy() *Foo

func (in *FooSpec) DeepCopyInto(out *FooSpec)

func (in *FooSpec) DeepCopy() *FooSpec 

defaulter-gen

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# pwd 
# github.com/phosae/x-kubernetes/api
inputs=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1")
inputs+=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v2")

defaulter-gen \
    -v "${v}" \
    -O zz_generated.defaults \
    --go-header-file /tmp/fake-boilerplate.txt \
    --output-base /go/src\
    "${inputs[@]}"

# generated outputs:
# - github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1/zz_generated.defaults.go
#
# v2 没有提供 defaults 实现,所以没有生成相应文件

defaulter-gen 较为有意思,只要在 package 下存在函数 func SetDefaults_TYPE(obj *TYPE),且提供了 packge 级声明

// +k8s:defaulter-gen=TypeMeta

注:// +k8s:defaulter-gen=TypeMeta 表示只要 API structs 存在 TypeMeta 字段(也可以是 ObjectMeta、ListMeta),就帮它们生成 Defaults 函数

举例说明,我在 hello.zeng.dev/v1 defaults.go 中提供了符合规范命名的实现

func SetDefaults_Foo(obj *Foo) {/*... defaulting codes */}

会生成文件 hello.zeng.dev/v1 zz_generated.defaults.go,包含这些内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// 生成的注册函数
func RegisterDefaults(scheme *runtime.Scheme) error {
	scheme.AddTypeDefaultingFunc(&Foo{}, func(obj interface{}) { SetObjectDefaults_Foo(obj.(*Foo)) })
	scheme.AddTypeDefaultingFunc(&FooList{}, func(obj interface{}) { SetObjectDefaults_FooList(obj.(*FooList)) })
	return nil
}

func SetObjectDefaults_Foo(in *Foo) {
	SetDefaults_Foo(in) // 调用 defaults.go 实现
}

func SetObjectDefaults_FooList(in *FooList) {
	for i := range in.Items {
		a := &in.Items[i]
		SetObjectDefaults_Foo(a)
	}
}

习惯性地,我们应该在 hello.zeng.dev/v1 defaults.go 包装 func RegisterDefaults(scheme *runtime.Scheme) error 暴露给其他函数调用,避免直接依赖生成函数

func AddDefaultingFuncs(scheme *runtime.Scheme) error {
    return RegisterDefaults(scheme)
}

此外,也可以给外部 module 提供的 public API 生成 default func,只需在 doc.go 提供声明 +k8s:defaulter-gen-input 引入外部 package

// +k8s:defaulter-gen=TypeMeta
// +k8s:defaulter-gen-input=github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1
package v1

具体可以参考 api-aggregation-lib pkg/api/hello.zeng.dev/v1

使用时将外部 module 类型和生成的 default funcs 一起注册到 Schema 即可

conversion-gen

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# pwd 
# github.com/phosae/x-kubernetes/api
inputs=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1")
inputs+=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v2")

conversion-gen \
    -O zz_generated.conversion \
    --go-header-file /tmp/fake-boilerplate.txt \
    --output-base /go/src \
    "${inputs[@]}"

三种注释声明

  • struct level: // +k8s:conversion-gen:explicit-from=url.Values
  • package level: // +k8s:conversion-gen=k8s.io/kubernetes/pkg/apis/apps
  • package level: // +k8s:conversion-gen-external-types=k8s.io/api/apps/v1

conversion-gen 作用是解决多版本 API 类型到统一内部类型转换,它会自动比对外部 struct 和内部 struct 字段,尝试自动生成转换函数。

以 1.16 之前的官方资源 Deployment 为例,对外存在两个版本 API v1beta1 (k8s.io/api/apps/v1beta1/types.go) 和 v1 (k8s.io/api/apps/v1/types.go),但是在内存和 etcd 它们都表现为统一的内部 Deployment (k8s.io/kubernetes/pkg/apis/apps/types.go)

k8s.io/api/apps/v1beta1/types.go Deployment  <---+
                                                 +---> internal version k8s.io/kubernetes/pkg/apis/apps/types.go Deployment
k8s.io/api/apps/v1/types.go      Deployment  <---+

转换函数全部放置在 zz_generated.conversion.go。文件头提供一个注册函数 func RegisterConversions

1
2
3
4
5
6
7
8
9
// file: zz_generated.conversion.go


// Convert_v1_DeploymentSpec_To_apps_DeploymentSpec is an autogenerated conversion function.
func Convert_v1_DeploymentSpec_To_apps_DeploymentSpec(in *v1.DeploymentSpec, out *apps.DeploymentSpec, s conversion.Scope) error {...}

func autoConvert_apps_DeploymentSpec_To_v1_DeploymentSpec(in *apps.DeploymentSpec, out *v1.DeploymentSpec, s conversion.Scope) error {...}

func autoConvert_apps_DeploymentSpec_To_v1_DeploymentSpec(in *apps.DeploymentSpec, out *v1.DeploymentSpec, s conversion.Scope) error {...}

需要手动转换的部分,在同目录下 conversion.go 提供 func Convert_<pkg1>_<type>_To_<pkg2>_<type> 并调用 autoConvert_<pkg1>_<type>_To_<pkg2>_<type> 即可

1
2
3
4
5
6
7
8
9
// file: conversion.go

func Convert_apps_DeploymentSpec_To_v1_DeploymentSpec(in *apps.DeploymentSpec, out *appsv1.DeploymentSpec, s conversion.Scope) error {
  if err := autoConvert_apps_DeploymentSpec_To_v1_DeploymentSpec(in, out, s); err != nil {
      return err
    }
    // conversion code here
    return nil
}

点击这里查看 Deployment v1 conversionhello.zeng.dev/v1 conversion 提供了易于理解的例子。

如果内部类型和某个外部类型表示完全一致,则不需要写任何手工代码,由 conversion-gen 生成所有转换代码即可,hello.zeng.dev/v2 conversion 就是一例。

// +k8s:conversion-gen:explicit-from=<package.type, i.e url.Values> 提供 structs 级别声明,支持指明 structs 一对一转换,Kubernetes 的一个典型场景是 转换 URL 参数到各种 CRUD options struct

go-to-protobuf

1
2
3
4
5
6
7
8
9
# pwd 
# github.com/phosae/x-kubernetes/api

go-to-protobuf \
    --proto-import=/go/src \
    --proto-import=/go/src/k8s.io/kubernetes/third_party/protobuf \
    --packages github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1,github.com/phosae/x-kubernetes/api/hello.zeng.dev/v2 \
    --output-base /go/src/ \
    --go-header-file /tmp/fake-boilerplate.txt

doc.go 提供 protobuf 生成声明

// +k8s:protobuf-gen=package

执行 go-to-protobuf 即可生成两个文件: generated.proto 和 generated.pb.go

generated.proto 是根据 go package 和 go struct 生成的 protobuf (proto 2) 定义

示例 API structs 内容,注意 field tags protobuf 为自动生成

 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
package v1

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

type Foo struct {
        metav1.TypeMeta   `json:",inline"`
        metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

        Spec FooSpec `json:"spec" protobuf:"bytes,2,opt,name=spec"`
}

type FooSpec struct {
        // Msg says hello world!
        Msg string `json:"msg" protobuf:"bytes,1,opt,name=msg"`
        // Msg1 provides some verbose information
        // +optional
        Msg1 string `json:"msg1" protobuf:"bytes,2,opt,name=msg1"`
}

type FooList struct {
        metav1.TypeMeta `json:",inline"`
        metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`

        Items []Foo `json:"items" protobuf:"bytes,2,rep,name=items"`
}

示例 generated.proto 内容

 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
syntax = "proto2";

package github.com.phosae.x_kubernetes.api.hello.zeng.dev.v1;

import "k8s.io/apimachinery/pkg/apis/meta/v1/generated.proto";
import "k8s.io/apimachinery/pkg/runtime/generated.proto";
import "k8s.io/apimachinery/pkg/runtime/schema/generated.proto";

// Package-wide variables from generator "generated".
option go_package = "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1";

message Foo {
  optional k8s.io.apimachinery.pkg.apis.meta.v1.ObjectMeta metadata = 1;

  optional FooSpec spec = 2;
}

message FooList {
  optional k8s.io.apimachinery.pkg.apis.meta.v1.ListMeta metadata = 1;

  repeated Foo items = 2;
}

message FooSpec {
  // Msg says hello world!
  optional string msg = 1;

  // Msg1 provides some verbose information
  // +optional
  optional string msg1 = 2;
}

generated.pb.go 是根据 generated.proto 生成的 Go 函数集,不再赘述

有了这两份文件,自定义 apiserver 等实现即可使用 protobuf 与官方 kube-apiserver 传递数据,也可以在 etcd 以 protobuf 格式存储对象。

🔥 OpenAPI Generator

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# project:        github.com/phosae/x-kubernetes
# API go module:  github.com/phosae/x-kubernetes/api
inputs=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v1")
inputs+=("--input-dirs" "github.com/phosae/x-kubernetes/api/hello.zeng.dev/v2")

openapi-gen" \
    -O zz_generated.openapi \
    --go-header-file ${PROJECT_ROOT}/hack/boilerplate.go.txt" \
    --output-package github.com/phosae/x-kubernetes/api/generated/openapi" \
    --report-filename "${PROJECT_ROOT}/api/api-rules/violation_exceptions.list" \
    --input-dirs "k8s.io/apimachinery/pkg/apis/meta/v1" \
    --input-dirs "k8s.io/apimachinery/pkg/runtime" \
    --input-dirs "k8s.io/apimachinery/pkg/version" \
    "${inputs[@]}"

result

./github.com/phosae/x-kubernetes/api/generated/openapi/zz_generated.openapi.go

zz_generated.openapi.go 可以被 k8s.io/apiserver 库使用,作为 /openapi/v2 和 /openapi/v3 来源

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
package cmd

import(
	openapinamer "k8s.io/apiserver/pkg/endpoints/openapi"
	genericapiserver "k8s.io/apiserver/pkg/server"
	utilfeature "k8s.io/apiserver/pkg/util/feature"
)

func ApiserverConfig() *genericapiserver.RecommendedConfig {
	serverConfig := genericapiserver.NewRecommendedConfig(myapiserver.Codecs)
	// enable OpenAPI schemas
	namer := openapinamer.NewDefinitionNamer(myapiserver.Scheme)
	serverConfig.OpenAPIConfig = genericapiserver.DefaultOpenAPIConfig(generatedopenapi.GetOpenAPIDefinitions, namer)
	serverConfig.OpenAPIConfig.Info.Title = "hello.zeng.dev-server"
	serverConfig.OpenAPIConfig.Info.Version = "0.1"

	if utilfeature.DefaultFeatureGate.Enabled(features.OpenAPIV3) {
		serverConfig.OpenAPIV3Config = genericapiserver.DefaultOpenAPIV3Config(generatedopenapi.GetOpenAPIDefinitions, namer)
		serverConfig.OpenAPIV3Config.Info.Title = "hello.zeng.dev-server"
		serverConfig.OpenAPIV3Config.Info.Version = "0.1"
	}
}

🔮 Client Generators

kubernetes/client-go 即是根据 API structs 自动生成,摘录 hack/update-codegen.sh 如下

 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
applyconfiguration-gen \
    --openapi-schema <("${modelsschema}") \
    --go-header-file "hack/boilerplate/boilerplate.generatego.tx" \
    --output-base "${KUBE_ROOT}/vendor" \
    --output-package k8s.io/client-go/applyconfigurations \
    $(printf -- " --input-dirs %s" "${ext_apis[@]}")
---
client-gen \
    --go-header-file "hack/boilerplate/boilerplate.generatego.tx" \
    --output-base "${KUBE_ROOT}/vendor" \
    --output-package="k8s.io/client-go" \
    --clientset-name="kubernetes" \
    --input-base="k8s.io/api" \
    --apply-configuration-package k8s.io/client-go/applyconfigurations \
    $(printf -- " --input %s" "${gv_dirs[@]}")
---
lister-gen \
    --go-header-file "hack/boilerplate/boilerplate.generatego.tx" \
    --output-base "${KUBE_ROOT}/vendor" \
    --output-package "k8s.io/client-go/listers" \
    $(printf -- " --input-dirs %s" "${ext_apis[@]}")
---
informer-gen \
    --go-header-file "hack/boilerplate/boilerplate.generatego.tx" \
    --output-base "${KUBE_ROOT}/vendor" \
    --output-package "k8s.io/client-go/informers" \
    --single-directory \
    --versioned-clientset-package k8s.io/client-go/kubernetes \
    --listers-package k8s.io/client-go/listers \
    $(printf -- " --input-dirs %s" "${ext_apis[@]}")

注: applyconfiguration-gen 参数 –openapi-schema 为可选。送了会生成额外的 ExtractTYPE 函数。如

func ExtractPod(pod *apicorev1.Pod, fieldManager string) (*PodApplyConfiguration, error)  ----------------+
                                                                                                          |
func ExtractPodStatus(pod *apicorev1.Pod, fieldManager string) (*PodApplyConfiguration, error) -----------+
                                                                                                          ⬇️ 
func extractPod(pod *apicorev1.Pod, fieldManager string, subresource string) (*PodApplyConfiguration, error)

生成结果即是 kubernetes/client-go 中的熟悉结构

tree -L 1 client-go
client-go
├── applyconfigurations  # by applyconfiguration-gen
├── informers            # by informer-gen
├── kubernetes           # by client-gen
├── listers              # by lister-gen

client-gen 注释参数较多,可以查阅 K8s community generating-clientset.md

client example

 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
42
43
44
45
46
47
48
49
50
51
52
53
package main

import (
	"context"
	"fmt"

	apiv1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	applyv1 "k8s.io/client-go/applyconfigurations/core/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

func main() {
	config, _ := clientcmd.BuildConfigFromFlags("", homedir.HomeDir()+"/.kube/config")
	clientset, _ := kubernetes.NewForConfig(config)

	podClient := clientset.CoreV1().Pods(apiv1.NamespaceDefault)

	pod := &apiv1.Pod{
		ObjectMeta: metav1.ObjectMeta{Name: "testpd", Labels: map[string]string{"app": "test"}},
		Spec: apiv1.PodSpec{
			Containers: []apiv1.Container{
				{Name: "busybox", Image: "busybox:1.36"},
			},
		},
	}

	result, err := podClient.Create(context.TODO(), pod, metav1.CreateOptions{})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Created pod %q.\n", result.GetObjectMeta().GetName())
	defer func() { podClient.Delete(context.TODO(), pod.Name, metav1.DeleteOptions{}) }()

	// similar to: kubectl patch pod/testpd -p '{"metadata": {"labels": {"hello": "world"}}}'
	podApplyCfg := applyv1.Pod("testpd", apiv1.NamespaceDefault).WithLabels(map[string]string{"hello": "world"})
	applyRet, err := podClient.Apply(context.Background(), podApplyCfg, metav1.ApplyOptions{FieldManager: "example", Force: false})
	if err != nil {
		panic(err)
	}
	fmt.Printf("Pod/%s labels:\n", applyRet.Name)
	for ak, av := range applyRet.Labels {
		fmt.Printf("\t%s: %v\n", ak, av)
	}
} ///~ ouput:
/*
Created pod "testpd".
Pod/testpd labels:
        app: test
        hello: world
*/

watch example

 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
42
43
44
45
46
47
48
49
50
51
package main

import (
	"context"
	"fmt"
	"time"

	apiv1 "k8s.io/api/core/v1"
	coreinformers "k8s.io/client-go/informers/core/v1"
	"k8s.io/client-go/kubernetes"
	corelisters "k8s.io/client-go/listers/core/v1"
	"k8s.io/client-go/tools/cache"
	"k8s.io/client-go/tools/clientcmd"
	"k8s.io/client-go/util/homedir"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	config, _ := clientcmd.BuildConfigFromFlags("", homedir.HomeDir()+"/.kube/config")
	clientset, _ := kubernetes.NewForConfig(config)

	// normally K8s Objects are KV based
	// client cache+indexers address this limit to some extent
	// for example we can group pods by nodeName
	var podInformer = coreinformers.NewFilteredPodInformer(clientset, apiv1.NamespaceDefault, 0,
		cache.Indexers{"spec.NodeName": func(obj interface{}) ([]string, error) {
			return []string{obj.(*apiv1.Pod).Spec.NodeName}, nil
		}}, nil)
	// pods, _ := podInformer.GetIndexer().ByIndex("spec.NodeName", "node-1")

	podInformer.Run(ctx.Done())

	for !podInformer.HasSynced() {
	}

	var podLister = corelisters.NewPodLister(podInformer.GetIndexer())
	result, err := podLister.Pods(apiv1.NamespaceDefault).Get("testpd")
	if err != nil {
		panic(err)
	}
	fmt.Printf("Get pod/%q from cache .\n", result.GetObjectMeta().GetName())

	// using event handlers do business when Pod Object changes
	podInformer.AddEventHandler(cache.ResourceEventHandlerFuncs{
		AddFunc:    func(obj interface{}) { /**/ },
		UpdateFunc: func(oldObj, newObj interface{}) { /**/ },
		DeleteFunc: func(obj interface{}) { /**/ },
	})
}

🥷 All in One Script

上述 Generators 中许多参数存在重复,针对这个问题 kubernetes/code-generator 早先提供了 generate-groups.sh 和 /generate-internal-groups.sh 脚本批量生成。1.28 alpha 之后则提供了表述更清晰更好维护的脚本 kube_codegen.sh。

另外,上述 Generator 均假设项目放置在 GOPATH 下,如 github.com/phosae/x-kubernetes/api 路径应为 ${GOPATH}/github.com/phosae/x-kubernetes/api。 之后在对应项目路径下执行生成命令,Generator 才能正常工作,造成了一定不便。

所以最好的方式是在容器环境生成代码,事先准备好容器镜像,运行时将项目挂载到容器 GOPATH 下,一键生成即可。 x-kubernetes/apix-kubernetes/api-aggregation-lib 即利用了这种方式生成代码。

Public API module: github.com/phosae/x-kubernetes/api 运行 ./hack/update-codegen-docker.sh 结果如下

 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
tree -L 2 hello.zeng.dev generated/
hello.zeng.dev
├── install.go
├── register.go
├── v1
│   ├── defaults.go
│   ├── doc.go
│   ├── generated.pb.go
│   ├── generated.proto
│   ├── register.go
│   ├── types.go
│   ├── zz_generated.deepcopy.go
│   └── zz_generated.defaults.go
└── v2
    ├── doc.go
    ├── generated.pb.go
    ├── generated.proto
    ├── register.go
    ├── types.go
    └── zz_generated.deepcopy.go

generated/
├── applyconfiguration
│   ├── hello.zeng.dev
│   ├── internal
│   └── utils.go
├── clientset
│   └── versioned
├── examples
│   ├── client
│   ├── README.md
│   └── watch
├── informers
│   └── externalversions
├── listers
│   └── hello.zeng.dev
└── openapi
    └── zz_generated.openapi.go

apiserver module: github.com/phosae/x-kubernetes/api-aggregation-lib 运行 ./hack/update-codegen-docker.sh 结果如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pkg                            # internal APIs   
├── api
│   └── hello.zeng.dev
│       ├── install                
│       │   └── install.go
│       ├── v1
│       │   ├── conversion.go  # conversion funcs v1 ---> internal type
│       │   ├── defaults.go    # default funcs v1
│       │   ├── doc.go         # packge 级别生成声明
│       │   ├── register.go    # 注册 API Structs
│       │   ├── zz_generated.conversion.go
│       │   └── zz_generated.defaults.go
│       ├── v2
│       │   ├── defaults.go    # default funcs v2
│       │   ├── doc.go         # packge 级别生成声明
│       │   ├── register.go    # 注册 API Structs     
│       │   ├── zz_generated.conversion.go
│       │   └── zz_generated.defaults.go
│       ├── doc.go
│       ├── register.go
│       ├── types.go           # internal type all api versions, include v1 and v2
│       └── zz_generated.deepcopy.go
├── apiserver
..

Public API 生成前后 PR 对比

Internal API 生成前后 PR 对比

👋👋👋 容器一键生成镜像代码库在 phosae/kube-code-generator,欢迎 PR