apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:# 名字必须与下面的spec字段匹配,并且格式为: <名称的复数形式>.<组名>name: demos.example.com
spec:# 组名,用于 REST API: /apis/<组>/<版本>group: example.comnames:# 名称的复数形式,用于URL: /apis/<组>/<版本>/<名称的复数形式>plural: demos# 名称的单数形式,作为命令行使用时和显示时的别名singular: demo# kind通常是单数形式的帕斯卡编码形式。你的资源清单会使用这一形式kind: Demo# shortNames 允许你在命令行使用较短的字符串来匹配资源shortNames:- dm# 可以是Namespaced 或 Cluster scope: Namespaced# 列举此CRD所支持的版本versions:- name: v1# 每个版本都可以通过served标准来独立启用或禁止served: true# 其中一个且只有一个版本必须被标记为存储版本storage: trueschema:openAPIV3Schema:type: objectproperties:spec:type: objectproperties:name:type: string
执行下面的命令来注册我们的CRD:
# 将上面内容复制到一个crd-demo.yaml文件中# 注册我们的CRD
[root@master demo-test]# kubectl create -f crd-demo.yaml
customresourcedefinition.apiextensions.k8s.io/demos.example.com created# 查看我们注册的CRD
[root@master demo-test]# kubectl get crd
NAME CREATED AT
demos.example.com 2022-11-24T07:16:38Z# 查看自定义资源CR,目前还没有,因为我们还没有创建
[root@master demo-test]# kubectl get demos
No resources found in default namespace.
待CRD创建完成后,我们就可以使用它来创建我们的自定义资源了,其创建方式跟内置的资源如Pod这些是一样的,只需要将kind、apiVersion指定为我们CRD中声明的值,比如使用上面的例子中的CRD定义资源:
apiVersion: "example.com/v1"
kind: Demo
metadata:name: crd-demo
spec:name: test
创建Demo:
# 将上面yaml内容复制到demo.yaml中# 创建demo
[root@master demo-test]# kubectl create -f demo.yaml
demo.example.com/crd-demo created# 查看demos
[root@master demo-test]# kubectl get dm
NAME AGE
crd-demo 5s
虽然我们注册了CRD并且创建了一个CR,但是此时是没有任何效果的。要实现效果的话,就需要我们来实现一个controller来监听我们的CR。
Finalizers能够让控制器实现异步的删除前(Pre-delete)回调。与内置对象类似,定制对象也支持Finalizer
给我们的CR添加Finalizer:
apiVersion: "example.com/v1"
kind: Demo
metadata:name: demo-finalizerfinalizers:- example.com/finalizer
自定义 Finalizer 的标识符包含一个域名、一个正向斜线和 finalizer 的名称。 任何控制器都可以在任何对象的 finalizer 列表中添加新的 finalizer。
对带有 Finalizer 的对象的第一个删除请求会为其 metadata.deletionTimestamp 设置一个值,但不会真的删除对象。一旦此值被设置,finalizers 列表中的表项只能被移除。 在列表中仍然包含 finalizer 时,无法强制删除对应的对象。
当 metadata.deletionTimestamp 字段被设置时,监视该对象的各个控制器会执行它们所能处理的 finalizer,并在完成处理之后将其从列表中移除。 每个控制器负责将其 finalizer 从列表中删除。
metadata.deletionGracePeriodSeconds 的取值控制对更新的轮询周期。
一旦 finalizers 列表为空时,就意味着所有 finalizer 都被执行过, Kubernetes 会最终删除该资源
下面进行一个测试:
创建CR:
# 将上面yaml内容复制到cr-finalizer.yaml# 创建cr
[root@master demo-test]# kubectl create -f cr-finalizer.yaml
demo.example.com/demo-finalizer created# 查看cr
[root@master demo-test]# kubectl get demos
NAME AGE
crd-demo 9m48s
demo-finalizer 19s# 删除cr
kubectl delete demo demo-finalizer
当我们删除时,可以看到会在命令执行后一直卡住,等待我们的controller来完成资源清理:

下面我们来模拟一下清理资源:
# 启动另一个终端# 编辑我们的CR
kubectl edit demo demo-finalizer
将下面图片中红框中的内容删除

保存退出后,可以看到另一个终端已经OK了

在CRD中定义了我们的CR的一些字段,我们可以对字段进行合法性校验,比如我们使用正则表达式来限制name必须为test开头:

# 1.在我们的crd-demo.yaml中添加上图的pattern# 2.将之前的cr删除
kubectl delete dm crd-demo# 3. 更新我们的crd
[root@master demo-test]# kubectl apply -f crd-demo.yaml
customresourcedefinition.apiextensions.k8s.io/demos.example.com configured
将我们的demo.yaml中的spec.name修改

创建cr,可以看到name不合法,创建失败了。如果将name改为test开头的字符串就可以创建成功了。

client-go为每种K8S内置资源提供对应的clientset和informer。那么如果我们要监听和操作自定义资源对象,应该如何操作呢?这里有两种方式:
方式一:使用client-go提供的dynamicClient来操作自定义资源对象,当然由于dynamicClient是基于RESTClient实现的,所以我们也可以使用RESTClient来达到同样目的。方式二:使用code-generator来帮助我们生成我们需要的代码,这样我们就可以像使用client-go为K8S内置资源提供的方式监听和操作自定义资源了。我们主要使用code-generator来编写我们的控制器。
下面将使用RestClient和DynamicClient来操作我们的自定义资源demo
使用RestClient操作自定义资源对象:
使用restClient时,需要我们在config中指定GV以及解码器同时还要配置APIPath
package mainimport ("context""encoding/json""fmt"v1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/apis/meta/v1/unstructured""k8s.io/apimachinery/pkg/runtime/schema""k8s.io/client-go/kubernetes/scheme""k8s.io/client-go/rest""k8s.io/client-go/tools/clientcmd""k8s.io/klog/v2"
)func main() {// 获取配置 将/root/.kube/config拷贝到项目的conf目录下config, err := clientcmd.BuildConfigFromFlags("", "./conf/config")if err != nil {panic(err)}// 指定GVconfig.GroupVersion = &schema.GroupVersion{Group: "example.com",Version: "v1",}// 指定解码器config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()// 指定APIPath APIPath为通过http访问的路径前缀config.APIPath = "/apis/"// 创建restClientrestClient, err := rest.RESTClientFor(config)if err != nil {panic(err)}// 将获取的数据保存到Unstructured类型的对象中obj := unstructured.Unstructured{}// 获取资源为demos,deafult命名空间下,名称为crd-demo的资源对象err = restClient.Get().Resource("demos").Name("crd-demo").Namespace(v1.NamespaceDefault).Do(context.Background()).Into(&obj)if err != nil {klog.Errorf("get demo error:%v", err)return}// 序列化为json后打印,看的更清晰bytes, err := json.Marshal(obj.Object)if err != nil {klog.Errorf("json marshal error:%v", err)return}fmt.Println(string(bytes))
}
对于K8S内建的资源对象例如Pod、Deployment来说,有对应的golang 结构体类型,我们可以直接使用。但是我们自定义的资源是没有的,所以数据的接收需要使用unstructured.Unstructured{}类型:
这个类型中就是一个map
type Unstructured struct {// Object is a JSON compatible map with string, float, int, bool, []interface{}, or// map[string]interface{}// children.Object map[string]interface{}
}
运行并使用json_pp格式化后的结果如下:

使用DynamicClient操作自定义资源对象:
在使用dynamicClient操作自定义资源对象时,需要传入自定义资源的GVR,然后就可以像使用内置资源对象一样来操作了。
package mainimport ("context""encoding/json""fmt"v1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/runtime/schema""k8s.io/client-go/dynamic""k8s.io/client-go/tools/clientcmd""k8s.io/klog/v2"
)func main() {config, err := clientcmd.BuildConfigFromFlags("", "./conf/config")if err != nil {panic(err)}client, err := dynamic.NewForConfig(config)gvr := schema.GroupVersionResource{Group: "example.com",Version: "v1",Resource: "demos",}resourceInterface := client.Resource(gvr)obj, err := resourceInterface.Namespace(v1.NamespaceDefault).Get(context.Background(), "crd-demo", v1.GetOptions{})if err != nil {klog.Errorf("get error:%v", err)return}bytes, err := json.Marshal(obj.Object)if err != nil {klog.Errorf("json marshal error:%v", err)return}fmt.Println(string(bytes))}
运行并使用json_pp格式化后的运行结果如下:

K8S的内建资源都有对应的informer的实现,比如PodInformer、DeploymentInformer。对于我们的自定义资源来说,并没有这样的informer,但是我们可以使用shredIndexInformer。在下一节的代码生成中,可以使用代码生成器来生成特定的informer,比如DemoInfomer和DemoLister等工具。
这节主要将sharedIndexInformer的使用,代码如下:
package mainimport ("context""encoding/json""fmt"v1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/apis/meta/v1/unstructured""k8s.io/apimachinery/pkg/runtime""k8s.io/apimachinery/pkg/runtime/schema""k8s.io/apimachinery/pkg/watch""k8s.io/client-go/dynamic""k8s.io/client-go/tools/cache""k8s.io/client-go/tools/clientcmd""k8s.io/klog/v2""time"
)func main() {// 1、构建configconfig, err := clientcmd.BuildConfigFromFlags("", "./conf/config")if err != nil {panic(err)}// 2、创建dynamicClient,也可以使用restClientclient, err := dynamic.NewForConfig(config)gvr := schema.GroupVersionResource{Group: "example.com",Version: "v1",Resource: "demos",}resourceInterface := client.Resource(gvr)// 使用sharedIndexInformer需要一个ListWatch对象,该对象可以从apiServer获取数据listwatch := cache.ListWatch{ListFunc: func(options v1.ListOptions) (runtime.Object, error) {return resourceInterface.List(context.Background(), options)},WatchFunc: func(options v1.ListOptions) (watch.Interface, error) {return resourceInterface.Watch(context.Background(), options)},DisableChunking: false,}// 示例对象,unstructured.Unstructured实现了runtime.Object接口obj := unstructured.Unstructured{}// 3、创建sharedIndexInformer,使用Namespace索引器informer := cache.NewSharedIndexInformer(&listwatch, &obj, time.Minute, cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc})// 4、添加资源事件处理方法informer.AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: PrintObj,UpdateFunc: func(oldObj, newObj interface{}) {PrintObj(newObj)},DeleteFunc: PrintObj,})stopCh := make(chan struct{})// 5、启动informerinformer.Run(stopCh)<-stopCh
}func PrintObj(obj interface{}) {demo := obj.(*unstructured.Unstructured)bytes, err := json.Marshal(demo.Object)if err != nil {klog.Errorf("json marshal error:%v", err)return}fmt.Println(string(bytes))
}
在使用sharedIndexInformer时需要我们传入ListWatch、示例对象和索引器。ListWatch用于用apiServer获取数据;由于我们没有自定义资源的go类型,因此只能使用unstructured.Unstructured类型。
code-generator是K8S官网提供的一组代码生成工具。当我们为CRD编写自定义controller时,可以使用它来生成我们需要的versioned client、informer、lister以及其它工具方法。
github地址:https://github.com/kubernetes/code-generator
# 将code-generator克隆到$GOPATH/pkg中
cd $GOPATH/pkggit clone https://github.com/kubernetes/code-generator# 安装需要的组件
# 进入code-generator目录中
cd code-generator$ go install ./cmd/{client-gen,deepcopy-gen,informer-gen,lister-gen}# 这些组件被安装到了$GOPATH/bin目录下, 我们可以将它们添加到PATH中,这样就可以在任意地方使用了
如果一个个使用这些组件也是很麻烦的,我们可以使用code-generator目录下的generate-groups.sh脚本文件来生成我们的代码。
接下来我们自定义一个CRD,然后使用code-generator来生成代码来实现对自定义资源的操作。在https://github.com/kubernetes/sample-controller中有一个样例,我们就根据这个样例来。
1、创建一个工程文件,然后使用我们的ide打开,我用的是Goland:
mkdir -p github.com/operator-crd
cd github.com/operator-crd
touch main.go
go mod init github.com/operator-crd
2、根据样例中的目录结构来创建出我们的目录结构
目录结构:pkg/apis/

创建出如下的目录:

3、在样例的v1alpha1中有四个文件,其中doc.go types.go 以及 register.go都是需要我们自己写的,然后其余的代码根据这三个文件来生成。

创建出这些文件
types.go:在这个文件中需要定义我们的自定义资源的go结构体类型。register.go:用来注册我们的类型doc.go:在其中添加全局的标记我们需要在这些文件中添加标记,然后代码生成器就可以根据这些标记来生成代码,比如在doc.go中添加下面两个标记,// +k8s:deepcopy-gen=package用来告诉生成器来生成我们自定义资源类型的deepcopy方法,+groupName=samplecontroller.k8s.io是指定我们的group名称

(1)我们需要在我们的doc.go中添加标记,内容如下:
doc.go
// +k8s:deepcopy-gen=package
// +groupName=crd.example.com
package v1
(2)然后在types.go中声明类型:
types.go
package v1import (metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)// +genclient
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// Foo is a specification for a Foo resource
type Foo struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec FooSpec `json:"spec"`Status FooStatus `json:"status"`
}// FooSpec is the spec for a Foo resource
type FooSpec struct {DeploymentName string `json:"deploymentName"`Replicas *int32 `json:"replicas"`
}// FooStatus is the status for a Foo resource
type FooStatus struct {AvailableReplicas int32 `json:"availableReplicas"`
}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// FooList is a list of Foo resources
type FooList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata"`Items []Foo `json:"items"`
}
我们声明的Foo类型跟K8S内建的资源类型是差不多的,都包含了TypeMeta和ObjectMeta以及Spec等。
下面两个标记分别用来告诉代码生成器生成自定义资源的clientset和Foo类型要实现的deepcopy的interface

(3)在register.go中注册我们的类型
register.go:
package v1import (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: "crd.example.com", Version: "v1"}// 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 builderSchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)// AddToScheme is a global function that registers this API group & version to a schemeAddToScheme = SchemeBuilder.AddToScheme
)// Adds the list of known types to Scheme.
func addKnownTypes(scheme *runtime.Scheme) error {scheme.AddKnownTypes(SchemeGroupVersion,&Foo{},&FooList{},)metav1.AddToGroupVersion(scheme, SchemeGroupVersion)return nil
}
上面三个步骤完成后,在下面会报红,是因为我们还没有为其生成相应的GetObjectKind和DeepCopyObject方法

4、生成其余代码
我们可以使用code-generator中的generate-groups.sh来生成代码
我们可以直接执行来查看它的使用方法,Examples中的第一个用来使用所有的组件,第二个我们可以单独使用组件:

生成的命令如下:
$GOPATH/pkg/code-generator/generate-groups.sh all github.com/operator-crd/pkg/generated github.com/operator-crd/pkg/apis crd.example.com:v1 --go-header-file=$GOPATH/pkg/code-generator/hack/boilerplate.go.txt --output-base ../../
github.com/operator-crd/pkg/generated:是我们生成的代码的位置github.com/operator-crd/pkg/apis:我们的代码的位置,要根据我们的三个代码文件来生成其它代码crd.example.com:v1:组名和版本注意:在windows的gitbash上使用generate-groups.sh不成功,还没有找到解决办法,建议在linux中生成,其实这个code-generator不会也无所谓,后面有更好用的工具,主要看生成的步骤即可。可以根据上面的步骤在linux中安装code-generator来生成代码
最终在linux上生成了对应的代码:

最终生成了我们的deepcopy、clientset、informer以及listers

然后我们就可以像使用K8S的内建资源一样来操作我们的自定义资源了。
5、创建自定义资源
crd.yaml
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:name: foos.crd.example.com
spec:group: crd.example.comversions:- name: v1served: truestorage: trueschema:# schema used for validationopenAPIV3Schema:type: objectproperties:spec:type: objectproperties:deploymentName:type: stringreplicas:type: integerminimum: 1maximum: 10status:type: objectproperties:availableReplicas:type: integernames:kind: Fooplural: foosscope: Namespaced
注册CRD:
# 将上面的内容黏贴到crd.yaml中# 注册crd
[root@master manifests]# kubectl create -f crd.yaml
customresourcedefinition.apiextensions.k8s.io/foos.crd.example.com created
创建一个CR
example-foo.yaml
apiVersion: crd.example.com/v1
kind: Foo
metadata:name: example-foo
spec:deploymentName: example-fooreplicas: 1
# 创建cr
[root@master manifests]# kubectl create -f example-foo.yaml
foo.crd.example.com/example-foo created# 查看cr
[root@master manifests]# kubectl get foos
NAME AGE
example-foo 27s
6、在代码中操作我们的自定义资源
main.go
package mainimport ("fmt"v1 "github.com/operator-crd/pkg/apis/crd.example.com/v1"clientset "github.com/operator-crd/pkg/generated/clientset/versioned""github.com/operator-crd/pkg/generated/informers/externalversions""k8s.io/client-go/tools/cache""k8s.io/client-go/tools/clientcmd""k8s.io/klog/v2"
)func main() {// 1.创建配置config, err := clientcmd.BuildConfigFromFlags("", "./conf/config")if err != nil {panic(config)}// 2.创建clientsetclientset, err := clientset.NewForConfig(config)if err != nil {panic(err)}// 3.创建informerFactoryfactory := externalversions.NewSharedInformerFactory(clientset, 0)// 4.获取FooInformerfooInformer := factory.Crd().V1().Foos()// 获取SharedIndexInformerinformer := fooInformer.Informer()// 获取listerlister := fooInformer.Lister()// 注册资源事件处理其informer.AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: func(obj interface{}) {foo := obj.(*v1.Foo)fmt.Printf("[Add Event] %s\n", foo.Name)},UpdateFunc: func(oldObj, newObj interface{}) {foo := newObj.(*v1.Foo)fmt.Printf("[Add Event] %s\n", foo.Name)},DeleteFunc: func(obj interface{}) {foo := obj.(*v1.Foo)fmt.Printf("[Add Event] %s\n", foo.Name)},})stopCh := make(chan struct{})factory.Start(stopCh)factory.WaitForCacheSync(stopCh)// 使用lister查询foofoo, err := lister.Foos("default").Get("example-foo")if err != nil {klog.Errorf("get foo error:%v", err)} else {fmt.Println("foo name:", foo.Name)}<-stopCh
}
运行结果如下:

在上节中,使用code-generate可以帮助我们生成types文件以及informer、lister等工具方法。但是它不能为我们生成types文件以及CRD。但是使用controller-tools中的工具就可以来生成types文件以及CRD、RBAC等文件。
github地址:https://github.com/kubernetes-sigs/controller-tools
# 将代码克隆到下来
git clone https://github.com/kubernetes-sigs/controller-tools# 安装
cd controller-tools
go install ./cmd/{controller-gen,type-scaffold}
安装后,两个程序就被安装到了$GOPATH/bin目录下。
type-scaffold可以为我们生成自定义资源的go类型。使用时需要指定Kind以及resource(也可以不指定resource,会根据kind来生成),使用如下:
type-scaffold.exe --kind Foo --resource Foos
但是直接这样使用并不会为我们生成文件,而是直接打印出了生成的代码,因此我们可以使用重定向来生成文件:
type-scaffold.exe --kind Foo --resource Foos > pkg/apis/crd.example.com/v1/types.go
然后我们可以在FooSpec中添加需要的字段:

controller-gen可以生成deepcopy方法实现、CRD、RBAC等:

生成这些文件也要依赖于注释标记,比如在生成CRD时候,我们可以在types文件中添加标记来设置数据的校验:

生成deepcopy方法:
controller-gen object paths=pkg/apis/crd.example.com/v1/types.go
生成CRD
资源定义好了,我们需要将其注册到client-go中。在v1目录下创建register.go,在package上面添加groupName的标记,然后定义GV:`
// register.go
// +groupName=example.com
package v1import ("k8s.io/apimachinery/pkg/runtime""k8s.io/apimachinery/pkg/runtime/schema""k8s.io/apimachinery/pkg/runtime/serializer"
)var (Scheme = runtime.NewScheme()GroupVersion = schema.GroupVersion{Group: "example.com",Version: "v1",}Codec = serializer.NewCodecFactory(Scheme)
)
在types.go文件中调用Scheme.AddKnownTypes的方法来注册我们的类型:
// types.go
package v1import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"// FooSpec defines the desired state of Foo
type FooSpec struct {// INSERT ADDITIONAL SPEC FIELDS -- desired state of cluster
}// FooStatus defines the observed state of Foo.
// It should always be reconstructable from the state of the cluster and/or outside world.
type FooStatus struct {// INSERT ADDITIONAL STATUS FIELDS -- observed state of cluster
}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// Foo is the Schema for the foos API
// +k8s:openapi-gen=true
type Foo struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec FooSpec `json:"spec,omitempty"`Status FooStatus `json:"status,omitempty"`
}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// FooList contains a list of Foo
type FooList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata,omitempty"`Items []Foo `json:"items"`
}func init() {Scheme.AddKnownTypes(GroupVersion, &Foo{}, &FooList{})
}
# 生成CRD
controller-gen crd paths=./... output:crd:dir=config/crd
但是不知为何,在这生成之后,没有任何文件,半天找不到原因。先不管了,这些东西知道一个流程就行了,反正后面也不会用。后面有kubebuilder脚手架,使用起来非常简单方便。