文章目录

  • 一、CRD是什么?
  • 二、自动生成代码
  • code-generator探讨
  • 开始实战
  • 三、编写controller



一、CRD是什么?

我们在学习一个新的东西的时候,一定要弄明白1件事就是什么是什么?
在学习CRD的时候,我们也应该明白CRD是什么?这时候我们就要去官网了,因为官网往往是最全的。

  • CRD官网 那么CRD到底是什么呢?
    他的英文是CustomResourceDefinition,其实就是让让开发者去自定义资源(如Deployment,StatefulSet等)的一种方法,从而为Kubernetes提高可扩展性。
    其实CRD仅仅是资源的定义,而Controller可以去监听CRD的CRUD事件来添加自定义业务逻辑。
    我们主要就是完成Controller的业务逻辑。下面将会介绍

二、自动生成代码

k8s容器修改crontab文件 k8s 自定义crd_json


从上图可以发现整个逻辑还是比较复杂的,为了简化我们的自定义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 请求提供只读缓存层。

开始实战

  1. 在$GOPATH/src/目录下创建一个文件夹crd_controller
  2. 进入文件夹crd_controller,执行如下命令创建三层目录:
mkdir -p pkg/apis/example
  1. 在新建的example目录下创建文件register.go,内容如下:
[root@ecs-431f-0001 example]# vi register.go 
package example
  
const (
        GroupName = "example.k8s.io"
        Version   = "v1"
)
  1. 在新建的example目录下创建名为v1的文件夹;
  2. 在新建的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

  1. 在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对象的时候内容就要和这里匹配了;
  1. 在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
}
  1. 目录结构如下:

k8s容器修改crontab文件 k8s 自定义crd_json_02

  1. 执行以下命令,会先下载依赖包,再下载代码生成工具,再执行代码生成工作:
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

k8s容器修改crontab文件 k8s 自定义crd_kubernetes_03


执行下面的命令,要在文件的根目录crd_controller下执行:

./../k8s.io/code-generator/generate-groups.sh all \
crd_controller/pkg/client \
crd_controller/pkg/apis \
example:v1

k8s容器修改crontab文件 k8s 自定义crd_k8s容器修改crontab文件_04


三、编写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)
}
  1. 接下来可以写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
}
  1. 接下来可以编写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 . 编译构建和启动

现在的文件是这些。

k8s容器修改crontab文件 k8s 自定义crd_k8s容器修改crontab文件_05

go mod tidy # 检查modules,并下载需要的依赖

k8s容器修改crontab文件 k8s 自定义crd_代码生成_06

go mod vendor # 将依赖下载到vendor中

解决了包依赖问题,现在可以在crd_controller运行代码

go build -o crd_controller . # 得到crd_controller 可执行文件 记着有个点

k8s容器修改crontab文件 k8s 自定义crd_k8s_07


启动命令:

./crd_controller -kubeconfig=$HOME/.kube/config -alsologtostderr=true

启动成功:

k8s容器修改crontab文件 k8s 自定义crd_代码生成_08


测试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创建成功,在另外一个窗口被监听到

k8s容器修改crontab文件 k8s 自定义crd_json_09

创建实例:

#创建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

同样被监听到。

k8s容器修改crontab文件 k8s 自定义crd_json_10


对创建的实例进行修改:

[root@ecs-431f-0001 crd]# kubectl edit stu object-name
student.example.k8s.io/object-name edited

k8s容器修改crontab文件 k8s 自定义crd_代码生成_11


同样监听到了修改之后的变化:

k8s容器修改crontab文件 k8s 自定义crd_json_12


将创建的实例删除:

[root@ecs-431f-0001 crd]# kubectl delete -f student-instance.yaml 
student.example.k8s.io "object-name" deleted

监视到了删除之后的变化:

k8s容器修改crontab文件 k8s 自定义crd_kubernetes_13


到此CRD和controller的编写就完成了。

参考文章