k8s client-go 程序实现kubernetes Controller Operator 使用CRD 学习总结
k8s client-go 程序实现kubernetes Controller & Operator 使用CRD 学习总结
大纲
- 1 定义CRD
- 2 client-go自动代码生成
- 3 client-go操作CR
- 4 创建镜像
- 5 配置权限
- 6 部署到k8s
基础流程
这里使用client-go实现编写,相对于kubebuiler这些工具生成脚手架工程要麻烦一些,但是可以理解完整的原理。
k8s 自定义operator 基本流程
- 1 定义crd
- 2 使用code-generator 生成自定义clientset
- 3 编写代码
- 4 配置权限
- 5 发布到k8s集群
定义CRD
此例子中使用的CRD自定义资源定义基本和 《k8s java程序实现kubernetes Controller & Operator 使用CRD 学习总结》 文章中使用的CRD一致
CRD自定义资源定义yaml文件内容如下 (yaml/crd-liuyijiang.yaml )
# 定义自定义的 MyCrdGolangTest 资源
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:# 名字必需与下面的 spec 字段匹配,并且格式为 '<名称的复数形式>.<组名>'name: mycrdgolangtests.liuyjiang.comspec: # 组名称,用于 REST API: /apis/<组>/<版本>group: liuyjiang.comnames:# 名称的复数形式,用于 URL:/apis/<组>/<版本>/<名称的复数形式>plural: mycrdgolangtests# 名称的单数形式,作为命令行使用时和显示时的别名singular: mycrdgolangtest# kind 通常是单数形式的驼峰命名(CamelCased)形式。你的资源清单会使用这一形式。kind: MyCrdGolangTest# shortNames 允许你在命令行使用较短的字符串来匹配资源shortNames:- mcgt# 可以是 Namespaced 或 Cluster scope: Namespaced versions:- name: v1# 每个版本都可以通过服务标志启用/禁用。served: true# 必须将一个且只有一个版本标记为存储版本。storage: trueschema:openAPIV3Schema:type: objectproperties: #自定义CRD中的specspec:type: objectproperties:# 自定义的资源spec 中的属性 mymsgmymsg: type: string# 自定义的资源spec 中的属性 myarray myarray: type: arrayitems:type: string # 自定义的资源spec 中的属性 mynumber mynumber: type: integer#自定义CRD中的status status:type: object properties: mystatus: type: stringmyip: type: string
资源定义完成后使用 kubectl apply -f crd-liuyijiang.yaml 在集群内部先创建好CRD
kubectl apply -f crd-liuyijiang.yaml
kubectl get crd mycrdgolangtests.liuyjiang.com
crd 创建成功
kubectl describe crd mycrdgolangtests.liuyjiang.com 查看crd内容
client-go code-generator 自动代码生成
如果不选择使用自动代码生成,可以直接使用client-go 提供的 dynamicClient实现操作CR,但是使用起来相对麻烦
例如需要使用 unstructured.Unstructured实现创建资源
需要使用 watch方法实现监听,无法使用Informer
使用 code-generator 可以生成对应的clientset, Informer, lister等编码时更方便
code-generator地址:
https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/code-generator
测试时发现code-generator 生成代码需要在linux环境下才能正常运行,在window环境下生成会有如下错误 unknown escape sequence(and 3 more errors) 或者 illegal character U+005C ‘’ (and 14 more errors)
errors in package "crdtest\\\\pkg\\\\client\\\\clientset\\\\versioned":
unable to format file "..\\\\crdtest\\\\pkg\\\\client\\\\clientset\\\\versioned\\\\clientset.go" (23:24: unknown escape sequence (and 3 more errors)).errors in package "crdtest\\\\pkg\\\\client\\\\versioned\\\\typed\\\\liuyijiang.com\\\\v1":
unable to format file "..\\\\crdtest\\\\pkg\\\\client\\\\versioned\\\\typed\\\\liuyijiang.com\\\\v1\\\\mycrdgolangtest.go" (27:9: illegal character U+005C '\\' (and 14 more errors)).
使用gitbash 报错
使用 cygwin 也报错
看生成的代码,是文件分割符号有问题 目前还未解决此问题!所以只有在linux环境下执行code-generator 脚本生成代码
client-go code-generator 生成代码流程
基本流程:
- 1 需要一台配置了go环境的linux系统机器
- 2 安装code-generator 代码生成工具
- 3 创建一个最简单的go项目
- 4 go项目需要使用git管理 (否则报错 fatal: not a git repository (or any of the parent directories): .git)
- 5 编写 doc.go types.go register.go 文件用于生成代码
linux搭建go环境
golang 下载地址 https://golang.google.cn/dl/
本次测试使用golang 版本 go1.19.3.linux-amd64.tar.gz
例如在把 golang 安装到 /ops/go
修改环境变量 path
vi /etc/profile把go命令和gopath 添加到环境变量中
export PATH=/ops/go/go/bin:$PATH
export GOPATH=/root/gosource /etc/profile
配置代理和开启go mod
go env -w GO111MODULE=on
go env -w GOPROXY=https://goproxy.cn,direct
进入到/root 文件夹 目前还没有go文件夹 手动创建
mkdir -p ./go/pkg
此时 linux go环境搭建完成
安装 code-generator
code-generator
官方地址 https://github.com/kubernetes/code-generator
在 $GOPTAH/pkg (即/root/go/pkg)文件夹下拉取code-generator
git clone https://github.com/kubernetes/code-generator
进入code-generator安装代码生成需要的工具
go install ./cmd/{applyconfiguration-gen,client-gen,deepcopy-gen,informer-gen,lister-gen}
当执行generate-groups.sh all 时会分别调用applyconfiguration-gen,client-gen,deepcopy-gen,informer-gen,lister-gen生成工具
工具说明
- client-gen 用于生成clientset相关代码
- deepcopy-gen 用于实现对象deepcopy
- informer-gen 用于生成Informer相关代码
- lister-gen 用于生成Lister相关代码
- applyconfiguration-gen 生成配置相关的的代码 generate-groups.sh all 时会调用
此时code-generator 代码生产需要的环境已经完成
go项目搭建
在开发环境 window机器上的创建项目
注意:项目需要使用git管理 否则使用 deepcopy-gen生成深拷贝代码时会出现以下异常
fatal: not a git repository (or any of the parent directories): .git
step1 在gitee上创建一个项目 方便等下在linux环境下 pull push 生成后的代码
例如已经在gitee上创建好一个空的项目crdtest。并在 D:\\giteecode 拉取项目
git clone https://gitee.com/liuyijiang/crdtest.git
cd crdtest
go mod init crdtest
在crdtest文件夹中创建 pkg/apis/liuyijiang.com/v1 文件夹用于存放代码生成需要的模板文件
其中 liuyijiang.com文件夹和自己CRM配置文件中的组名称一致
step2 添加模板代码
需要三个模板文件 doc.go types.go register.go
doc.go 内容如下
// +k8s:deepcopy-gen=package// +groupName=liuyjiang.com
package v1
+k8s:deepcopy-gen=package用来告诉生成器来生成我们自定义资源类型的deepcopy方法,+groupName=liuyjiang.com是指定我们的group名称
types.go 内容如下
package v1import (metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)// +genclient
// +genclient:noStatus
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
type MyCrdGolangTest struct {metav1.TypeMeta `json:",inline"`metav1.ObjectMeta `json:"metadata,omitempty"`Spec MyCrdGolangTestSpec `json:"spec"`Status MyCrdGolangTestStatus `json:"status"`
}type MyCrdGolangTestSpec struct {Mymsg string `json:"mymsg"`Mynumber int64 `json:"mynumber"`Myarray []string `json:"myarray"`
}type MyCrdGolangTestStatus struct {Mystatus string `json:"mystatus"`Myip string `json:"myip"`
}// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object// StudentList is a list of Student resources
type MyCrdGolangTestList struct {metav1.TypeMeta `json:",inline"`metav1.ListMeta `json:"metadata"`Items []MyCrdGolangTest `json:"items"`
}
types.go主要就是编写自定义CRD结构体Spec Status等
register.go 内容如下
package v1import (metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/runtime""k8s.io/apimachinery/pkg/runtime/schema"
)var SchemeGroupVersion = schema.GroupVersion{Group: "liuyjiang.com",Version: "v1",
}var (SchemeBuilder = runtime.NewSchemeBuilder(addKnownTypes)AddToScheme = SchemeBuilder.AddToScheme
)func Resource(resource string) schema.GroupResource {return SchemeGroupVersion.WithResource(resource).GroupResource()
}func Kind(kind string) schema.GroupKind {return SchemeGroupVersion.WithKind(kind).GroupKind()
}func addKnownTypes(scheme *runtime.Scheme) error {scheme.AddKnownTypes(SchemeGroupVersion,&MyCrdGolangTest{},&MyCrdGolangTestList{},)// register the type in the schememetav1.AddToGroupVersion(scheme, SchemeGroupVersion)return nil
}
register.go 就是一个模板代码,只需要注意SchemeGroupVersion 使用自己的组名和版本名称,addKnownTypes中添加自己创建的CRD结构体指针
step3 最后执行 go mod tidy 添加依赖
使用 go mod tidy 添加依赖
此时项目结构如下 注意此时是 未生成代码前的项目结构
到此 go项目搭建完成 使用git push 到远端服务上
到linux机器上生成代码
拉取刚才提交的代码 注意此时在linux机器上,任意文件夹下都可以
进入go项目内执行生成代码脚本
/root/go/pkg/code-generator/generate-groups.sh all crdtest/pkg/client crdtest/pkg/apis liuyjiang.com:v1 --go-header-file=/root/go/pkg/code-generator/examples/hack/boilerplate.go.txt --output-base ../
生成命令说明:
- /root/go/pkg/code-generator/generate-groups.sh
这里使用code-generator项目中的generate-groups.sh
- all
是使用 applyconfiguration,client,deepcopy,informer,lister 的简写
- crdtest/pkg/client
是生成的文件的包名,简单说就是会在 crdtest/pkg/client文件下创建生成的文件,并且包名以crdtest/pkg/client为前缀
注意这里要配合--output-base 指定输出的根路径
例如在crdtest文件夹内执行命令 那么--output-base需要指定为 ../ 即输出根路径是当前文件夹的父文件夹
- crdtest/pkg/apis
是指定模板文件的位置,简单说就是doc.go types.go register.go这几个文件放置的最上层文件夹名称
- liuyjiang.com:v1
指定组名和版本与CRD配置中的组名和版本一致即可
- –go-header-file
直接使用code-generator项目中的现有的boilerplate.go.txt文件
/root/go/pkg/code-generator/examples/hack/boilerplate.go.txt
- –output-base
这里使用 ../ 即输出根路径是当前文件夹的父文件夹 ,这样输出文件就可以在pkg文件夹下了
generate-groups.sh 脚本参数与使用方式如下
此时代码生成完成 git push后回到window环境上查看
如有报错再执行一下 go mod tidy
client-go操作CR
有了生成代码后 就可以操作自定义的资源了,现在编写一个简单的web服务,可以创建并查询自定义的CRD,同时启动一个线程监听CRD
注意: 集群内部需要使用 rest.InClusterConfig() 获取权限信息
config, err := rest.InClusterConfig()if err != nil {panic(err.Error())}
相关代码如下
web.go
package crdwebimport ("context"crdv1 "crdtest/pkg/apis/liuyijiang.com/v1"crdclientset "crdtest/pkg/client/clientset/versioned""fmt""net/http""time"crdexternalversions "crdtest/pkg/client/informers/externalversions"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/util/wait""k8s.io/client-go/rest""k8s.io/client-go/tools/cache"
)func StartWeb() {go watchMyCrdGolangTest()fmt.Println("start")http.HandleFunc("/", index)http.HandleFunc("/get", get)http.HandleFunc("/list", list)http.HandleFunc("/create", create)//http.HandleFunc("/update", index)//http.HandleFunc("/delete", index)http.ListenAndServe(":8000", nil)}// 对应首页
func index(w http.ResponseWriter, r *http.Request) {w.Write([]byte("hello this is MyCrdGolangTest \\n"))}func get(w http.ResponseWriter, r *http.Request) {crdName := r.FormValue("name")w.Write([]byte(getMyCrdGolangTestStr(crdName)))}func list(w http.ResponseWriter, r *http.Request) {//crdName := r.FormValue("name")w.Write([]byte(listMyCrdGolangTest()))}func create(w http.ResponseWriter, r *http.Request) {crdName := r.FormValue("name")createMyCrdGolangTest(crdName)w.Write([]byte("createMyCrdGolangTest success \\n"))}func getClientSet() *crdclientset.Clientset {//外部访问集群的方式// config, err := clientcmd.BuildConfigFromFlags("", "./config")// if err != nil {// fmt.Println(err)// }//使用集群内部访问k8s的方式config, err := rest.InClusterConfig()if err != nil {panic(err.Error())}clientset, err := crdclientset.NewForConfig(config)if err != nil {fmt.Println(err)}return clientset
}/*
创建自定义资源 MyCrdGolangTest
*/
func createMyCrdGolangTest(name string) {clientset := getClientSet()namespace := "crd-golang-test"typeMeta := metav1.TypeMeta{Kind: "MyCrdGolangTest",APIVersion: "v1",}labelMap := make(map[string]string)labelMap["app"] = "this-is-my-crd"//可以配置pod的名字 Labels 等objectMeta := metav1.ObjectMeta{Name: name,Labels: labelMap,}spec := crdv1.MyCrdGolangTestSpec{Mymsg: "hello liuyijiang222",Mynumber: 123,Myarray: []string{"AFFF", "BCCC", "CEEE"},}myCrdGolangTest := crdv1.MyCrdGolangTest{TypeMeta: typeMeta,ObjectMeta: objectMeta,Spec: spec,}crd, _ := clientset.LiuyjiangV1().MyCrdGolangTests(namespace).Create(context.TODO(), &myCrdGolangTest, metav1.CreateOptions{})fmt.Println(crd.GetName())
}/*
查询所有自定义资源 MyCrdGolangTest
*/
func listMyCrdGolangTest() string {clientset := getClientSet()namespace := "crd-golang-test"info := ""list, _ := clientset.LiuyjiangV1().MyCrdGolangTests(namespace).List(context.TODO(), metav1.ListOptions{})for _, cr := range list.Items {fmt.Println(cr.GetName())info = info + cr.GetName() + "\\n "}return info
}/*
查询单个自定义资源 MyCrdGolangTest
*/
func getMyCrdGolangTest(name string) *crdv1.MyCrdGolangTest {clientset := getClientSet()namespace := "crd-golang-test"cr, _ := clientset.LiuyjiangV1().MyCrdGolangTests(namespace).Get(context.TODO(), name, metav1.GetOptions{})//返回的cr 不会为空 只能根据名字是否有值去判断fmt.Println(cr.GetName())fmt.Println(cr.Labels)fmt.Println("=============Spec===============")fmt.Println("Mymsg: ", cr.Spec.Mymsg)fmt.Println("Mynumber: ", cr.Spec.Mynumber)fmt.Println("Myarray: ", cr.Spec.Myarray)fmt.Println("=============Status===============")fmt.Println("Mystatus: ", cr.Status.Mystatus)fmt.Println("Myip: ", cr.Status.Myip)return cr
}func getMyCrdGolangTestStr(name string) string {clientset := getClientSet()namespace := "crd-golang-test"cr, _ := clientset.LiuyjiangV1().MyCrdGolangTests(namespace).Get(context.TODO(), name, metav1.GetOptions{})info := " " + cr.GetName() + " \\n "info = info + "=============Spec===============\\n Mymsg:" + cr.Spec.Mymsg + "\\n Mynumber: " + fmt.Sprintf("%d", cr.Spec.Mynumber)info = info + "\\n=============Status===============\\n Mystatus: " + cr.Status.Mystatus + " \\n"return info
}/*
更新自定义资源 MyCrdGolangTest
*/
func updateMyCrdGolangTest(name string) {fmt.Printf("=== before update === \\n\\n")cr := getMyCrdGolangTest(name)clientset := getClientSet()namespace := "default"cr.Spec.Mymsg = "ffffff"cr.Spec.Mynumber = 456cr.Spec.Myarray = []string{"TTTT", "GGGGG"}cr.Status.Myip = "127.0.0.1"cr.Status.Mystatus = "runing"clientset.LiuyjiangV1().MyCrdGolangTests(namespace).Update(context.TODO(), cr, metav1.UpdateOptions{})fmt.Printf("=== after update === \\n\\n")getMyCrdGolangTest(name)
}/*
监听资源
*/
func watchMyCrdGolangTest() {// 生成clientSetclientSet := getClientSet()/*使用Informer 实现watch操作*///使用NewSharedInformerFactory无法过滤 命名空间 无法根细粒度的选择哪个Crd//crdiformerFactory := crdexternalversions.NewSharedInformerFactory(clientSet, time.Second*30)/*NewFilteredSharedInformerFactory 可以过滤命名空间 和 Crd粒度*/namespace := "crd-golang-test"tweakListOptions := func(opt *metav1.ListOptions) {fmt.Println("=======tweakListOptions========")/这里还可以对pod进行筛选app=my-quarkus-demo*///opt.LabelSelector = "app=my-quarkus-demo"}crdiformerFactory := crdexternalversions.NewFilteredSharedInformerFactory(clientSet, time.Second*30, namespace, tweakListOptions)crdInformer := crdiformerFactory.Liuyjiang().V1().MyCrdGolangTests()crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{AddFunc: func(obj interface{}) {//fmt.Println(obj)crd := obj.(*crdv1.MyCrdGolangTest)fmt.Println("======AddFunc=====", crd.GetName())},UpdateFunc: func(oldObj, newObj interface{}) {//fmt.Println(oldObj)//fmt.Println(newObj)crd := newObj.(*crdv1.MyCrdGolangTest)fmt.Println("======UpdateFunc=====", crd.GetName())},DeleteFunc: func(obj interface{}) {crd := obj.(*crdv1.MyCrdGolangTest)fmt.Println("======DeleteFunc=====", crd.GetName())},})crdiformerFactory.Start(wait.NeverStop)crdiformerFactory.WaitForCacheSync(wait.NeverStop)//time.Sleep(time.Hour * 3)}
main.go
package mainimport ("context"crdweb "crdtest/pkg"crdv1 "crdtest/pkg/apis/liuyijiang.com/v1"crdclientset "crdtest/pkg/client/clientset/versioned"crdexternalversions "crdtest/pkg/client/informers/externalversions""fmt""time"metav1 "k8s.io/apimachinery/pkg/apis/meta/v1""k8s.io/apimachinery/pkg/util/wait""k8s.io/client-go/tools/cache""k8s.io/client-go/tools/clientcmd"
)//crdinformers "crdtest/pkg/client/informers/externalversions/liuyijiang.com/v1"
//crdexternalversions "crdtest/pkg/client/informers/externalversions"func main() {crdweb.StartWeb()
}
代码编写完成后使用交叉编译的方式,生成linux环境下可执行文件
GOOS=linux GOARCH=amd64 go build main.go
镜像创建
编写一个Dockerfile内容如下
FROM ubuntu
VOLUME ["/data/service/logs","/data/service/tmp"]
WORKDIR "/data/service"
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
COPY main main
RUN chmod 711 main
ENTRYPOINT ["./main"]
使用 ubuntu作为基础镜像
然后创建镜像并推送到私库
docker build -t crd-go .
docker tag crd-go registry.cn-hangzhou.aliyuncs.com/jimliu/crd-go
docker push registry.cn-hangzhou.aliyuncs.com/jimliu/crd-go
此时得到可以使用的crd-go 版本镜像
权限配置与发布配置
权限和发布参考 《k8s java程序实现kubernetes Controller & Operator 使用CRD 学习总结》 文章中使用的CRD一致
直接贴出deploy.yml内容
# 创建命名空间
apiVersion: v1
kind: Namespace
metadata:name: crd-golang-testlabels:liuyijiang.com: crd-golang-test---# 创建阿里云私库秘钥
apiVersion: v1
kind: Secret
metadata:name: myaliyunsecret-crd-golang-testnamespace: crd-golang-testlabels:liuyijiang.com: crd-golang-test
data:.dockerconfigjson: eyJhdXRocyI6eyJyZWd-省略
type: kubernetes.io/dockerconfigjson---# 创建ServiceAccount 用于 程序中访问自定义资源
apiVersion: v1
kind: ServiceAccount
metadata:name: crd-golang-test-serviceaccountnamespace: crd-golang-testlabels:liuyijiang.com: crd-golang-test
imagePullSecrets:- name: myaliyunsecret-crd-golang-test---# 需要操作自定义的 CRD MyCrdGolangTest 需要配置对MyCrdGolangTest资源的操作权限
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRole
metadata:name: crd-golang-test-clusterrolelabels:liuyijiang.com: crd-golang-test
rules:- apiGroups:- "liuyjiang.com" #apiGroups crd-liuyijiang.yaml中定义的 groupresources: - mycrdgolangtests #MyCrdGolangTest 注意为crd-liuyijiang.yaml中配置的复数名称verbs: #可以操作的类型- list- watch- get- create- delete - update - edit - exec---# 让ServiceAccount 与 ClusterRole绑定
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: ClusterRoleBinding
metadata:name: crdtest-cluster-role-bindinglabels:liuyijiang.com: crd-golang-test
roleRef:apiGroup: rbac.authorization.k8s.iokind: ClusterRolename: crd-golang-test-clusterrole
subjects:- kind: ServiceAccountname: crd-golang-test-serviceaccountnamespace: crd-golang-test ---# 创建项目容器pod
apiVersion: v1
kind: Pod
metadata: name: crd-gonamespace: crd-golang-test labels: app: crd-goliuyijiang.com: crd-golang-testspec: # 注意指定serviceAccountserviceAccountName: crd-golang-test-serviceaccountrestartPolicy: Alwayscontainers: - image: registry.cn-hangzhou.aliyuncs.com/jimliu/crd-go:latestname: crd-go-runtime ---# 创建service
apiVersion: v1
kind: Service
metadata:name: crd-go-servicenamespace: crd-golang-test
spec:ports:- protocol: TCPport: 8000targetPort: 8000nodePort: 18000name: httpselector:app: crd-gotype: NodePort
部署与测试
kubectl apply -f deploy.yml 部署程序
调用创建接口 http://192.168.0.160:18000/create?name=jimtest
调用查询接口 http://192.168.0.160:18000/get?name=jimtest
手动修改一下cr
kubectl -n crd-golang-test edit mcgt jimtest
删除cr