1、code-generator

1.1、code-gen 是什么

在 k8s 的开发中,会用到客户端来和 API Server 进行交互,客户端有4种,其中 DynamicClient 是为操作所有资源而编写的通用Client,我们写的CRD就可以直接用这种客户端。但是 DynamicClient 没有某种资源对应的 Informer、Lister,所以使用 DynamicClient,就无法像使用 ClientSet 那样,操作某一个特定资源的 Informer、Lister 等。

为此我们想,能否为 CRD 资源生成和内建资源一样的 Informer、Lister 代码,这样的话,开发控制器的时候,我们就可以像使用内建资源一样,为 CRD 的 informer 注册事件方法,并使用 Lister 从缓存中获取数据。

kubernetes 的开发中,有很多代码是相似而重复的,开发起来耗时耗力,因此就希望制作成了自动化工具,提高代码的可维护性和一致性。code-generator 应运而生。

code-generator 是一个代码生成工具集合,内部包含很多gen工具,用于自动生成与 k8s CRD 相关的 clientset、lister、informer、deepcopy 方法等。

code-gen 的代码位于:https://github.com/kubernetes/code-generator

code-generator是使用注释标记工作的,不同的gen工具,有不同的注释标记

  • deepcopy-gen:为每个 T 类型生成 func (t* T) DeepCopy() *T 方法,API 类型都需要实现深拷贝。

  • client-gen:生成类型化客户端集合(typed client sets),即与 Kubernetes API 服务器通信的客户端代码。

  • informer-gen:用于生成基于 Kubernetes API 资源的 Informer,提供事件机制来响应资源的事件,方便开发者执行资源监视操作。

  • lister-gen:用于生成 Kubernetes API 资源的 Lister,为 get 和 list 请求提供只读缓存层(通过 indexer 获取),可以实现高效的资源列表查询。

  • register-gen:自动生成 API 资源类型的注册表代码

  • conversion-gen:用于生成 Kubernetes 对象之间的转换器(Converter),方便在不同版本之间进行对象转换。

  • openapi-gen:用于生成 Kubernetes API 的 OpenAPI 规范,以便进行文档化和验证。

1.2、code-generator 标记 Tag

  • code-generator 将 tag 分为了两种:

    • Global tags: 全局的 tag,放在具体版本的 doc.go 文件中

    • Local tags: 本地的 tag,放在 types.go 文件中的具体的 struct 上

  • 使用语法:

// +tag-name 
或
// +tag-name=value

1.2.1、deepcopy-gen 的常用标记

  • 在要生成 client 代码的类型 type 上方使用

告诉代码生成器不生成该对象的深拷贝方法

// +k8s:deepcopy-gen=false

告诉代码生成器生成该对象的深拷贝方法

// +k8s:deepcopy-gen=true

指定生成的代码中实现的接口,这里指定了实现 k8s.io/apimachinery/pkg/runtime.Object 接口的代码

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
  • 在doc.go文件中使用

告诉代码生成器生成整个包的对象深拷贝方法。这意味着生成的代码将包含用于深拷贝包中所有对象的相关代码

// +k8s:deepcopy-gen=package

指定自定义 API 组的名称。这个注释用于定义自定义 API 资源的 API 组,使其能够与其他资源进行区分

// +groupName=foo.example.com

package v1

1.2.2、client-gen 的常用标记

  • 在要生成 client 代码的类型 type 上方使用

指示代码生成器生成用于客户端库的代码
// +genclient

不要为自定义资源生成状态字段相关的代码
// +genclient:noStatus
 
表示这是一个集群级别的资源
// +genclient:nonNamespaced

不要生成默认的 HTTP 动词(verbs)相关的代码,这可能是因为该资源的操作方式与常规的 RESTful 操作并不完全相同
// +genclient:noVerbs

指示代码生成器只生成用于创建和删除操作的代码
// +genclient:onlyVerbs=create,delete

告诉代码生成器跳过指定的动词相关的代码生成,这表明生成的客户端库将不包含与这些操作相关的代码
// +genclient:skipVerbs=get,list,create,update,patch,delete,deleteCollection,watch

为特定的方法指定生成代码的细节,这里指定了当执行创建操作时所使用的方法以及其返回结果
// +genclient:method=Create,verb=create,result=k8s.io/apimachinery/pkg/apis/meta/v1.Status

1.2.3、informer-gen 的常用标记

在要生成 client 代码的类型type上方使用

使用informer-gen必须要写这一行
// +k8s:informers

指定生成的 Informer 代码对应的 API 组和版本
// +k8s:informers:groupVersion=${group}/${version}

指定生成的 Informer 代码对应的内部 API 版本
// +k8s:informers:internalVersion=internal/version

指定生成的 Informer 代码是否需要为版本化的客户端集(Versioned ClientSet)生成代码
// +k8s:informers:versionedClientSet=false

指定生成的 Informer 代码是否需要使用缓存
// +k8s:informers:cache

1.3、k8s CRD 项目的结构

K8s 相关项目对外 API 都会包含 3 文件:

  • types.go:编写crd的资源结构,code-generator 的注释 tag 一般写在这边里面

  • doc.go:声明了按照 package 维度,为所有 structs 提供生成的声明

  • register.go:提供注册到 runtime.Scheme 的函数。code-generator 的 register-gen 可以读取 doc.go 中的 // +groupName,并生成 register.go

1.4、实战:为 CRD 生成代码

1、初始化目录

# 创建并进入项目目录
mkdir code-gen-demo && cd code-gen-demo

# 初始化项目
go mod init code-gen-demo

# 获取依赖
go get k8s.io/apimachinery@v0.0.0-20190425132440-17f84483f500
go get k8s.io/client-go@v0.0.0-20190425172711-65184652c889
go get k8s.io/code-generator@v0.0.0-20190419212335-ff26e7842f9d

2、创建 CRD 相关文件

我们希望创建的资源,groupName 为appcontroller.k8s.io,版本为 v1alpha1

3、编写文件

  • doc.go

// +k8s:deepcopy-gen=package
// +groupName=appcontroller.k8s.io

// Package v1alpha1 v1alpha1版本的api包
package v1alpha1

  • types.go

package v1alpha1

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

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

type Application struct {
	metav1.TypeMeta   `json:",inline"`
	metav1.ObjectMeta `json:"metadata,omitempty"`

	Spec   ApplicationSpec   `json:"spec"`
	Status ApplicationStatus `json:"status"`
}

type ApplicationSpec struct {
	DeploymentName string `json:"deploymentName"`
	Replicas       *int32 `json:"replicas"`
}

type ApplicationStatus struct {
	AvailableReplicas int32 `json:"availableReplicas"`
}

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

type ApplicationList struct {
	metav1.TypeMeta `json:",inline"`
	metav1.ListMeta `json:"metadata"`

	Items []Application `json:"items"`
}

  • register.go

注意,这是 appcontroller 目录下的 register.go,并非某个版本目录下的。

版本目录下的,在使用 code-generator 脚本后,再进行编写

package appcontroller

const (
	GroupName = "appcontroller.k8s.io"
)

  • boilerplate.go.txt

该文件是文件开头统一的注释,会在使用 code-generator 脚本时,指定 boilerplate.go.txt 文件的所在目录

/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@Time : 2024/8
@Author : wuyq
*/

  • tools.go

要使用 code-generator,可代码中还没有任何位置导入 code-generator 的包,所以我们需要一个类,专门用于将 code-generator 的包导入。一般使用 tools.go 来做这件事

//go:build tools
// +build tools

/*
Copyright 2019 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

// This package imports things required by build scripts, to force `go mod` to see them as dependencies
package tools

import _ "k8s.io/code-generator"

  • update-codegen.sh

#!/usr/bin/env bash

# Copyright 2017 The Kubernetes Authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

# 设置脚本在执行过程中遇到任何错误时立即退出
set -o errexit
# 设置脚本在使用未定义的变量时立即退出
set -o nounset
# 设置脚本在管道命令中任意一条命令失败时立即退出
set -o pipefail

# 对generate-groups.sh 脚本的调用
../vendor/k8s.io/code-generator/generate-groups.sh \
  "deepcopy,client,informer,lister" \
  code-gen-demo/pkg/generated \
  code-gen-demo/pkg/apis \
  appcontroller:v1alpha1 \
  --go-header-file $(pwd)/boilerplate.go.txt \
  --output-base $(pwd)/../../
  • "deepcopy,client,informer,lister":指定要生成的代码类型,这里包括深拷贝方法、客户端、informer 和 lister。

  • code-generator-demo/pkg/generated:指定生成的代码的输出目录。

  • code-generator-demo/pkg/apis:指定包含 API 定义的目录。

  • appcontroller:v1alpha1:指定要生成的 API 版本和组。

  • --go-header-file $(pwd)/boilerplate.go.txt:指定用于生成的代码文件头部的文本文件,这里使用了当前目录下的 boilerplate.go.txt 文件。

  • --output-base $(pwd)/../../:指定生成的代码的基础目录,即输出目录的上一级目录。

该命令的作用是调用 Kubernetes 代码生成器脚本,根据指定的 API 定义和选项生成深拷贝方法、客户端、informer 和 lister 等代码,并将生成的代码输出到指定的目录中。

4、执行脚本生成代码

go mod tidy

# 生成vendor文件夹
go mod vendor

# 为vendor中的code-generator赋予权限
chmod -R 777 vendor

# 为hack中的update-codegen.sh脚本赋予权限
chmod -R 777 hack

# 调用脚本生成代码
cd hack && ./update-codegen.sh

现在的目录变为:

5、手动注册版本 v1alpha1 的 CRD 资源

k8s 里面的资源需要注册到资源注册表 scheme 中才可以使用

在生成了客户端代码后,我们还需要手动注册版本 v1alpha1 的 CRD 资源,才能真正使用这个 client,不然在编译时会出现 undefined: v1alpha1.AddToScheme 错误、undefined: v1alpha1.Resource 错误。

v1alpha1.AddToScheme、v1alpha1.Resource 这两个是用于 client 注册的

编写 v1alpha1/register.go

package v1alpha1

import (
	"code-gen-demo/pkg/apis/appcontroller"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/runtime"
	"k8s.io/apimachinery/pkg/runtime/schema"
)

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

// Kind takes an unqualified kind and returns back a Group qualified GroupKind
func Kind(kind string) schema.GroupKind {
	return SchemeGroupVersion.WithKind(kind).GroupKind()
}

// Resource takes an unqualified resource and returns a Group qualified GroupResource
func Resource(resource string) schema.GroupResource {
	return SchemeGroupVersion.WithResource(resource).GroupResource()
}

var (
	// SchemeBuilder initializes a scheme builder
	SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)
	// AddToScheme is a global function that registers this API group & version to a scheme
	AddToScheme = SchemeBuilder.AddToScheme
)

// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {
	scheme.AddKnownTypes(SchemeGroupVersion,
		&Application{},
		&ApplicationList{},
	)
	metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
	return nil
}

在上面的项目,存在的问题:

  • 问题一:如果要在kubernetes集群中,运行上面编写的项目,还需要手动编写CRD的yaml,这个 yaml 文件会特别长,不好手动编写。

  • 问题二:types.go文件全部内容都需要我们手写,无法自动生成框架

解决方法:

有一个工具,controller-tools,可以解决这些问题,自动生成 crd+controller 框架。

2、controller-tools

controller-tools 其实是一个由 Kubernetes 社区维护的项目,用于简化 Kubernetes 控制器的开发。其中提供了一组工具来生成和更新 Kubernetes API 对象的代码,以及构建自定义控制器所需的代码框架。

controller-tools 的 github 地址:https://github.com/kubernetes-sigs/controller-tools

  • controller-gen:用于生成 deepcopy.go 文件以及 CRD 文件【kubebuilder也是通过这个工具生成 CRD 的相关框架的】

  • type-scaffold:用于生成所需的 types.go 文件

  • helpgen:用于生成针对 Kubernetes API 对象的代码文档,可以包括 API 对象的字段、标签和注释等信息

通过 code-gen 生成 CRD 的 clientset、lister、informer、deepcopy,通过 controller-tools 生成 CRD 的 yaml 文件。

1、安装controller-gen

go install sigs.k8s.io/controller-tools/cmd/controller-gen@v0.12.1

2、创建 Kustomization 文件:

在项目根目录下创建一个 config 目录,并在其中创建 kustomization.yaml 文件。

# config/kustomization.yaml
resources:
  - crd/bases

3、生成 CRD Yaml

在项目的根目录下controller-gen crd:crdVersions=v1 output:crd:dir=config/crd/bases paths=./...

可以发现在crd/bases下生成了appcontroller.k8s.io_applications.yaml

---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  annotations:
    controller-gen.kubebuilder.io/version: v0.12.1
  name: applications.appcontroller.k8s.io
spec:
  group: appcontroller.k8s.io
  names:
    kind: Application
    listKind: ApplicationList
    plural: applications
    singular: application
  scope: Namespaced
  versions:
  - name: v1alpha1
    schema:
      openAPIV3Schema:
        properties:
          apiVersion:
            description: 'APIVersion defines the versioned schema of this representation
              of an object. Servers should convert recognized schemas to the latest
              internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
            type: string
          kind:
            description: 'Kind is a string value representing the REST resource this
              object represents. Servers may infer this from the endpoint the client
              submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
            type: string
          metadata:
            type: object
          spec:
            properties:
              deploymentName:
                type: string
              replicas:
                format: int32
                type: integer
            required:
            - deploymentName
            - replicas
            type: object
          status:
            properties:
              availableReplicas:
                format: int32
                type: integer
            required:
            - availableReplicas
            type: object
        required:
        - spec
        - status
        type: object
    served: true
    storage: true

有了这个 Yaml 文件,就可以用 kubectl apply -f 直接部署