您现在的位置是:首页 >技术交流 >k8s client-go 程序实现kubernetes Controller & Operator 使用CRD 学习总结网站首页技术交流
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.com
spec:
# 组名称,用于 REST API: /apis/<组>/<版本>
group: liuyjiang.com
names:
# 名称的复数形式,用于 URL:/apis/<组>/<版本>/<名称的复数形式>
plural: mycrdgolangtests
# 名称的单数形式,作为命令行使用时和显示时的别名
singular: mycrdgolangtest
# kind 通常是单数形式的驼峰命名(CamelCased)形式。你的资源清单会使用这一形式。
kind: MyCrdGolangTest
# shortNames 允许你在命令行使用较短的字符串来匹配资源
shortNames:
- mcgt
# 可以是 Namespaced 或 Cluster
scope: Namespaced
versions:
- name: v1
# 每个版本都可以通过服务标志启用/禁用。
served: true
# 必须将一个且只有一个版本标记为存储版本。
storage: true
schema:
openAPIV3Schema:
type: object
properties:
#自定义CRD中的spec
spec:
type: object
properties:
# 自定义的资源spec 中的属性 mymsg
mymsg:
type: string
# 自定义的资源spec 中的属性 myarray
myarray:
type: array
items:
type: string
# 自定义的资源spec 中的属性 mynumber
mynumber:
type: integer
#自定义CRD中的status
status:
type: object
properties:
mystatus:
type: string
myip:
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/go
source /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 v1
import (
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 v1
import (
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 scheme
metav1.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 crdweb
import (
"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
"))
}
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
"))
}
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() + "
"
}
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() + "
"
info = info + "=============Spec===============
Mymsg:" + cr.Spec.Mymsg + "
Mynumber: " + fmt.Sprintf("%d", cr.Spec.Mynumber)
info = info + "
=============Status===============
Mystatus: " + cr.Status.Mystatus + "
"
return info
}
/*
更新自定义资源 MyCrdGolangTest
*/
func updateMyCrdGolangTest(name string) {
fmt.Printf("=== before update ===
")
cr := getMyCrdGolangTest(name)
clientset := getClientSet()
namespace := "default"
cr.Spec.Mymsg = "ffffff"
cr.Spec.Mynumber = 456
cr.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 ===
")
getMyCrdGolangTest(name)
}
/*
监听资源
*/
func watchMyCrdGolangTest() {
// 生成clientSet
clientSet := 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 main
import (
"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-test
labels:
liuyijiang.com: crd-golang-test
---
# 创建阿里云私库秘钥
apiVersion: v1
kind: Secret
metadata:
name: myaliyunsecret-crd-golang-test
namespace: crd-golang-test
labels:
liuyijiang.com: crd-golang-test
data:
.dockerconfigjson: eyJhdXRocyI6eyJyZWd-省略
type: kubernetes.io/dockerconfigjson
---
# 创建ServiceAccount 用于 程序中访问自定义资源
apiVersion: v1
kind: ServiceAccount
metadata:
name: crd-golang-test-serviceaccount
namespace: crd-golang-test
labels:
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-clusterrole
labels:
liuyijiang.com: crd-golang-test
rules:
- apiGroups:
- "liuyjiang.com" #apiGroups crd-liuyijiang.yaml中定义的 group
resources:
- 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-binding
labels:
liuyijiang.com: crd-golang-test
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: crd-golang-test-clusterrole
subjects:
- kind: ServiceAccount
name: crd-golang-test-serviceaccount
namespace: crd-golang-test
---
# 创建项目容器pod
apiVersion: v1
kind: Pod
metadata:
name: crd-go
namespace: crd-golang-test
labels:
app: crd-go
liuyijiang.com: crd-golang-test
spec:
# 注意指定serviceAccount
serviceAccountName: crd-golang-test-serviceaccount
restartPolicy: Always
containers:
- image: registry.cn-hangzhou.aliyuncs.com/jimliu/crd-go:latest
name: crd-go-runtime
---
# 创建service
apiVersion: v1
kind: Service
metadata:
name: crd-go-service
namespace: crd-golang-test
spec:
ports:
- protocol: TCP
port: 8000
targetPort: 8000
nodePort: 18000
name: http
selector:
app: crd-go
type: 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 **