k8s自定义controller,创建CRD并验证
文章目录一、CRD是什么?二、创建CRD1.三、自动生成代码code-generator探讨开始实战四、编写controller一、CRD是什么?二、创建CRD1.三、自动生成代码从上图可以发现整个逻辑还是比较复杂的,为了简化我们的自定义controller开发,k8s的大师们利用自动代码生成工具将controller之外的事情都做好了,我们只要专注于controller的开发就好。github地
一、CRD是什么?
我们在学习一个新的东西的时候,一定要弄明白1件事就是什么是什么?
在学习CRD的时候,我们也应该明白CRD是什么?这时候我们就要去官网了,因为官网往往是最全的。
- CRD官网
那么CRD到底是什么呢?
他的英文是CustomResourceDefinition
,其实就是让让开发者去自定义资源(如Deployment,StatefulSet等)的一种方法,从而为Kubernetes提高可扩展性。
其实CRD仅仅是资源的定义,而Controller可以去监听CRD的CRUD事件来添加自定义业务逻辑。
我们主要就是完成Controller的业务逻辑。下面将会介绍
二、自动生成代码
从上图可以发现整个逻辑还是比较复杂的,为了简化我们的自定义controller开发,k8s的大师们利用自动代码生成工具将controller之外的事情都做好了,我们只要专注于controller的开发就好。
github地址:code-generator
参考资料:kubernetes深入探讨:自定义资源的代码生成
code-generator探讨
在 Kubernetes 1.8 中,需要生成 client-go 代码。更具体地说,client-go 要求runtime.Object类型(golang 中的 CustomResources 必须实现runtime.Object interface
)必须具有 DeepCopy
方法。这里代码生成通过 deepcopy-gen 生成器发挥作用,可以在k8s.io/code-generator存储库中找到。
除了 deepcopy-gen 之外,还有一些 CustomResources 用户想要使用的代码生成器:
- deepcopy-gen—
func (t* T) DeepCopy() *T
为每个类型 T创建一个方法 - client-gen—为 CustomResource APIGroups 创建类型化的客户端集
- Informer-gen——为 CustomResources 创建 Informers,它提供一个基于事件的界面来对服务器上 CustomResources 的变化做出反应
- lister-gen—为 CustomResources 创建列表,为 GET 和 LIST 请求提供只读缓存层。
开始实战
- 在$GOPATH/src/目录下创建一个文件夹crd_controller:
- 进入文件夹crd_controller,执行如下命令创建三层目录:
mkdir -p pkg/apis/example
- 在新建的example目录下创建文件register.go,内容如下:
[root@ecs-431f-0001 example]# vi register.go
package example
const (
GroupName = "example.k8s.io"
Version = "v1"
)
- 在新建的example目录下创建名为v1的文件夹;
- 在新建的v1文件夹下创建文件doc.go,内容如下:
[root@ecs-431f-0001 v1]# vi doc.go
// +k8s:deepcopy-gen=package
// +groupName=example.k8s.io
package v1
// +k8s:deepcopy-gen=package
是全局标签。
它告诉 deepcopy-gen 默认为该包中的每种类型创建 deepcopy 方法。如果您有不需要或不需要 deepcopy 的类型,您可以选择退出具有本地标签的此类类型// +k8s:deepcopy-gen=false。如果您不启用包范围的深度复制,则必须通过 为每种所需类型选择深度复制// +k8s:deepcopy-gen=true。
// +groupName=example.com
定义了完全限定的 API 组名称。如果你弄错了,client-gen 将产生错误的代码,所以必须和CRD的组名一样。
请注意,此标签必须位于正上方的注释块中package
- 在v1文件夹下创建文件types.go,里面定义了Student对象的具体内容:
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 Student struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec StudentSpec `json:"spec"`
}
type StudentSpec struct {
name string `json:"name"`
school string `json:"school"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// StudentList is a list of Student resources
type StudentList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata"`
Items []Student `json:"items"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
:
- 客户端标签
// +genclient
:告诉 client-gen 为这种类型创建一个客户端.
// +genclient:noStatus
:告诉client-gen 这种类型没有通过/status子资源使用规范状态分离。生成的客户端将没有该UpdateStatus方法(client-gen 会盲目地生成该方法,知道Status在你的结构中找到一个字段)。 - 上述源码中,Student对象的内容已经被设定好,主要有name和school这两个字段,表示学生的名字和所在学校,因此创建Student对象的时候内容就要和这里匹配了;
- 在v1目录下创建register.go文件,此文件的作用是通过addKnownTypes方法使得client可以知道Student类型的API对象:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"crd_controller/pkg/apis/example"
)
var SchemeGroupVersion = schema.GroupVersion{
Group: example.GroupName,
Version: example.Version,
}
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,
&Student{},
&StudentList{},
)
// register the type in the scheme
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil
}
- 目录结构如下:
- 执行以下命令,会先下载依赖包,再下载代码生成工具,再执行代码生成工作:
cd $GOPATH/src #进入到src目录下
mkdir -p k8s.io #创建k8s.io目录
下载code-generator到k8s.io目录中。
#去Git上下载code-generator-master.zip 访问速度比较慢,直接下载.zip包即可
unzip code-generator-master.zip # 解压在k8s.io目录中
mv code-generator-master code-generator
下载apimachinery到k8s.io目录中
#去Git上下载apimachinery.zip 访问速度比较慢,直接下载.zip包即可
unzip apimachinery-master.zip # 解压在k8s.io目录中
mv apimachinery-master apimachinery
执行下面的命令,要在文件的根目录crd_controller下执行:
./../k8s.io/code-generator/generate-groups.sh all \
crd_controller/pkg/client \
crd_controller/pkg/apis \
example:v1
如果到这里没有生成informers,listers,请参考博客:
使用code-generator代码生成器没有生成informers,listers的处理办法
三、编写controller
1、在crd_controller目录下创建 controller.go,代码内容如下:
vi controller.go
package main
import (
"fmt"
"time"
"github.com/golang/glog"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/util/runtime"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/scheme"
typedcorev1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/cache"
"k8s.io/client-go/tools/record"
"k8s.io/client-go/util/workqueue"
example"crd_controller/pkg/apis/example/v1"
clientset "crd_controller/pkg/client/clientset/versioned"
studentscheme "crd_controller/pkg/client/clientset/versioned/scheme"
informers "crd_controller/pkg/client/informers/externalversions/example/v1"
listers "crd_controller/pkg/client/listers/example/v1"
)
const controllerAgentName = "student-controller"
const (
SuccessSynced = "Synced"
MessageResourceSynced = "Student synced successfully"
)
// Controller is the controller implementation for Student resources
type Controller struct {
// kubeclientset is a standard kubernetes clientset
kubeclientset kubernetes.Interface
// studentclientset is a clientset for our own API group
studentclientset clientset.Interface
studentsLister listers.StudentLister
studentsSynced cache.InformerSynced
workqueue workqueue.RateLimitingInterface
recorder record.EventRecorder
}
// NewController returns a new student controller
func NewController(
kubeclientset kubernetes.Interface,
studentclientset clientset.Interface,
studentInformer informers.StudentInformer) *Controller {
utilruntime.Must(studentscheme.AddToScheme(scheme.Scheme))
glog.V(4).Info("Creating event broadcaster")
eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartLogging(glog.Infof)
eventBroadcaster.StartRecordingToSink(&typedcorev1.EventSinkImpl{Interface: kubeclientset.CoreV1().Events("")})
recorder := eventBroadcaster.NewRecorder(scheme.Scheme, corev1.EventSource{Component: controllerAgentName})
controller := &Controller{
kubeclientset: kubeclientset,
studentclientset: studentclientset,
studentsLister: studentInformer.Lister(),
studentsSynced: studentInformer.Informer().HasSynced,
workqueue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), "Students"),
recorder: recorder,
}
glog.Info("Setting up event handlers")
// Set up an event handler for when Student resources change
studentInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: controller.enqueueStudent,
UpdateFunc: func(old, new interface{}) {
oldStudent := old.(*example.Student)
newStudent := new.(*example.Student)
if oldStudent.ResourceVersion == newStudent.ResourceVersion {
//版本一致,就表示没有实际更新的操作,立即返回
return
}
controller.enqueueStudent(new)
},
DeleteFunc: controller.enqueueStudentForDelete,
})
return controller
}
//在此处开始controller的业务
func (c *Controller) Run(threadiness int, stopCh <-chan struct{}) error {
defer runtime.HandleCrash()
defer c.workqueue.ShutDown()
glog.Info("开始controller业务,开始一次缓存数据同步")
if ok := cache.WaitForCacheSync(stopCh, c.studentsSynced); !ok {
return fmt.Errorf("failed to wait for caches to sync")
}
glog.Info("worker启动")
for i := 0; i < threadiness; i++ {
go wait.Until(c.runWorker, time.Second, stopCh)
}
glog.Info("worker已经启动")
<-stopCh
glog.Info("worker已经结束")
return nil
}
func (c *Controller) runWorker() {
for c.processNextWorkItem() {
}
}
// 取数据处理
func (c *Controller) processNextWorkItem() bool {
obj, shutdown := c.workqueue.Get()
if shutdown {
return false
}
// We wrap this block in a func so we can defer c.workqueue.Done.
err := func(obj interface{}) error {
defer c.workqueue.Done(obj)
var key string
var ok bool
if key, ok = obj.(string); !ok {
c.workqueue.Forget(obj)
runtime.HandleError(fmt.Errorf("expected string in workqueue but got %#v", obj))
return nil
}
// 在syncHandler中处理业务
if err := c.syncHandler(key); err != nil {
return fmt.Errorf("error syncing '%s': %s", key, err.Error())
}
c.workqueue.Forget(obj)
glog.Infof("Successfully synced '%s'", key)
return nil
}(obj)
if err != nil {
runtime.HandleError(err)
return true
}
return true
}
// 处理
func (c *Controller) syncHandler(key string) error {
// Convert the namespace/name string into a distinct namespace and name
namespace, name, err := cache.SplitMetaNamespaceKey(key)
if err != nil {
runtime.HandleError(fmt.Errorf("invalid resource key: %s", key))
return nil
}
// 从缓存中取对象
student, err := c.studentsLister.Students(namespace).Get(name)
if err != nil {
// 如果Student对象被删除了,就会走到这里,所以应该在这里加入执行
if errors.IsNotFound(err) {
glog.Infof("Student对象被删除,请在这里执行实际的删除业务: %s/%s ...", namespace, name)
return nil
}
runtime.HandleError(fmt.Errorf("failed to list student by: %s/%s", namespace, name))
return err
}
glog.Infof("这里是student对象的期望状态: %#v ...", student)
glog.Infof("实际状态是从业务层面得到的,此处应该去的实际状态,与期望状态做对比,并根据差异做出响应(新增或者删除)")
c.recorder.Event(student, corev1.EventTypeNormal, SuccessSynced, MessageResourceSynced)
return nil
}
// 数据先放入缓存,再入队列
func (c *Controller) enqueueStudent(obj interface{}) {
var key string
var err error
// 将对象放入缓存
if key, err = cache.MetaNamespaceKeyFunc(obj); err != nil {
runtime.HandleError(err)
return
}
// 将key放入队列
c.workqueue.AddRateLimited(key)
}
// 删除操作
func (c *Controller) enqueueStudentForDelete(obj interface{}) {
var key string
var err error
// 从缓存中删除指定对象
key, err = cache.DeletionHandlingMetaNamespaceKeyFunc(obj)
if err != nil {
runtime.HandleError(err)
return
}
//再将key放入队列
c.workqueue.AddRateLimited(key)
}
- 接下来可以写main.go了,不过在此之前把处理系统信号量的辅助类先写好,然后在main.go中会用到(处理例如ctrl+c的退出),在
$GOPATH/src/crd_controller/pkg
目录下新建目录signals:
在signals目录下新建文件signal_posix.go
:
// +build !windows
package signals
import (
"os"
"syscall"
)
var shutdownSignals = []os.Signal{os.Interrupt, syscall.SIGTERM}
在signals目录下新建文件 signal_windows.go
:
package signals
import (
"os"
)
var shutdownSignals = []os.Signal{os.Interrupt}
在signals目录下新建文件 signal.go
:
package signals
import (
"os"
"os/signal"
)
var onlyOneSignalHandler = make(chan struct{})
// SetupSignalHandler registered for SIGTERM and SIGINT. A stop channel is returned
// which is closed on one of these signals. If a second signal is caught, the program
// is terminated with exit code 1.
func SetupSignalHandler() (stopCh <-chan struct{}) {
close(onlyOneSignalHandler) // panics when called twice
stop := make(chan struct{})
c := make(chan os.Signal, 2)
signal.Notify(c, shutdownSignals...)
go func() {
<-c
close(stop)
<-c
os.Exit(1) // second signal. Exit directly.
}()
return stop
}
- 接下来可以编写main.go了,在crd_controller目录下创建main.go文件,内容如下:
package main
import (
"flag"
"k8s.io/client-go/tools/clientcmd"
"time"
"github.com/golang/glog"
"k8s.io/client-go/kubernetes"
//"k8s.io/client-go/tools/clientcmd"
// Uncomment the following line to load the gcp plugin (only required to authenticate against GKE clusters).
// _ "k8s.io/client-go/plugin/pkg/client/auth/gcp"
clientset "crd_controller/pkg/client/clientset/versioned"
informers "crd_controller/pkg/client/informers/externalversions"
"crd_controller/pkg/signals"
)
var (
masterURL string
kubeconfig string
)
func main() {
flag.Parse()
// 处理信号量
stopCh := signals.SetupSignalHandler()
// 处理入参
cfg, err := clientcmd.BuildConfigFromFlags(masterURL, kubeconfig)
if err != nil {
glog.Fatalf("Error building kubeconfig: %s", err.Error())
}
kubeClient, err := kubernetes.NewForConfig(cfg)
if err != nil {
glog.Fatalf("Error building kubernetes clientset: %s", err.Error())
}
studentClient, err := clientset.NewForConfig(cfg)
if err != nil {
glog.Fatalf("Error building example clientset: %s", err.Error())
}
studentInformerFactory := informers.NewSharedInformerFactory(studentClient, time.Second*30)
//得到controller
controller := NewController(kubeClient, studentClient,
studentInformerFactory.Example().V1().Students())
//启动informer
go studentInformerFactory.Start(stopCh)
//controller开始处理消息
if err = controller.Run(2, stopCh); err != nil {
glog.Fatalf("Error running controller: %s", err.Error())
}
}
func init() {
flag.StringVar(&kubeconfig, "kubeconfig", "", "Path to a kubeconfig. Only required if out-of-cluster.")
flag.StringVar(&masterURL, "master", "", "The address of the Kubernetes API server. Overrides any value in kubeconfig. Only required if out-of-cluster.")
}
到目前为止,前期的编写就已经结束了下面需要编译和运行了。
4 . 编译构建和启动
现在的文件是这些。
go mod tidy # 检查modules,并下载需要的依赖
go mod vendor # 将依赖下载到vendor中
解决了包依赖问题,现在可以在crd_controller运行代码
go build -o crd_controller . # 得到crd_controller 可执行文件 记着有个点
启动命令:
./crd_controller -kubeconfig=$HOME/.kube/config -alsologtostderr=true
启动成功:
测试controller:
创建crd,命名为student.yaml
apiVersion: apiextensions.k8s.io/v1beta1
kind: CustomResourceDefinition
metadata:
# metadata.name的内容是由"复数名.分组名"构成,如下,students是复数名,example.k8s.io是分组名
name: students.example.k8s.io
spec:
# 分组名,在REST API中也会用到的,格式是: /apis/分组名/CRD版本
group: example.k8s.io
# list of versions supported by this CustomResourceDefinition
versions:
- name: v1
# 是否有效的开关.
served: true
# 只有一个版本能被标注为storage
storage: true
# 范围是属于namespace的
scope: Namespaced
names:
# 复数名
plural: students
# 单数名
singular: student
# 类型名
kind: Student
# 简称,就像service的简称是svc
shortNames:
- stu
crd创建成功,在另外一个窗口被监听到
创建实例:
#创建student对象。k8s可以识别Kind类型为Student
apiVersion: example.k8s.io/v1
kind: Student
metadata:
name: object-name
spec:
name: "wang"
school: "china"
[root@ecs-431f-0001 crd]# kubectl create -f student-instance.yaml
student.example.k8s.io/object-name created
[root@ecs-431f-0001 crd]# kubectl get stu
NAME AGE
object-name 92s
同样被监听到。
对创建的实例进行修改:
[root@ecs-431f-0001 crd]# kubectl edit stu object-name
student.example.k8s.io/object-name edited
同样监听到了修改之后的变化:
将创建的实例删除:
[root@ecs-431f-0001 crd]# kubectl delete -f student-instance.yaml
student.example.k8s.io "object-name" deleted[添加链接描述](https://blog.csdn.net/boling_cavalry/article/details/88934063)
监视到了删除之后的变化:
到此CRD和controller的编写就完成了。
参考文章:
更多推荐
所有评论(0)