k8s编程operator系列:
在K8S系统扩展中,开发者可以通过CRD(CustomResourceDefinition)来扩展K8S API,其功能主要由APIExtensionServer负责。使用CRD扩展资源分为三步:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# 名字必须与下面的spec字段匹配,并且格式为: <名称的复数形式>.<组名>
name: demos.example.com
spec:
# 组名,用于 REST API: /apis/<组>/<版本>
group: example.com
names:
# 名称的复数形式,用于URL: /apis/<组>/<版本>/<名称的复数形式>
plural: demos
# 名称的单数形式,作为命令行使用时和显示时的别名
singular: demo
# kind通常是单数形式的帕斯卡编码形式。你的资源清单会使用这一形式
kind: Demo
# shortNames 允许你在命令行使用较短的字符串来匹配资源
shortNames:
- dm
# 可以是Namespaced 或 Cluster
scope: Namespaced
# 列举此CRD所支持的版本
versions:
- name: v1
# 每个版本都可以通过served标准来独立启用或禁止
served: true
# 其中一个且只有一个版本必须被标记为存储版本
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
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-finalizer
finalizers:
- 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 main
import (
"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)
}
// 指定GV
config.GroupVersion = &schema.GroupVersion{
Group: "example.com",
Version: "v1",
}
// 指定解码器
config.NegotiatedSerializer = scheme.Codecs.WithoutConversion()
// 指定APIPath APIPath为通过http访问的路径前缀
config.APIPath = "/apis/"
// 创建restClient
restClient, 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 main
import (
"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 main
import (
"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、构建config
config, err := clientcmd.BuildConfigFromFlags("", "./conf/config")
if err != nil {
panic(err)
}
// 2、创建dynamicClient,也可以使用restClient
client, 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、启动informer
informer.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/pkg
git 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 v1
import (
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 v1
import (
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 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,
&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.com
versions:
- name: v1
served: true
storage: true
schema:
# schema used for validation
openAPIV3Schema:
type: object
properties:
spec:
type: object
properties:
deploymentName:
type: string
replicas:
type: integer
minimum: 1
maximum: 10
status:
type: object
properties:
availableReplicas:
type: integer
names:
kind: Foo
plural: foos
scope: 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-foo
replicas: 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 main
import (
"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.创建clientset
clientset, err := clientset.NewForConfig(config)
if err != nil {
panic(err)
}
// 3.创建informerFactory
factory := externalversions.NewSharedInformerFactory(clientset, 0)
// 4.获取FooInformer
fooInformer := factory.Crd().V1().Foos()
// 获取SharedIndexInformer
informer := fooInformer.Informer()
// 获取lister
lister := 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查询foo
foo, 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 v1
import (
"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 v1
import 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脚手架,使用起来非常简单方便。