《 Kubebuilder v2 使用指南 》-P5-實現CRD控制邏輯

實現CRD控制邏輯

前言

上一篇已經設定了Unit所要實現的目標,完成了Unit結構體各子字段、ownResource字段的填充,爲控制邏輯的實現做了基礎鋪墊。

本篇主要解決和實現的控制邏輯:

  • 如何管理Unit下屬的own Resources
  • 如何使Unit和own Resources生命週期綁定
  • 刪除Unit資源前能否做一些自定義操作

逐一來實現。

管理own Resources

如前文所說,一共設計了5種ownResource分別對應StatefulSet/Deployment/Ingress/Service/Ingress這5種資源,每一種的管理無一例外需要進行增刪改查,管理方式大同小異,因此,提煉了一個通用的接口:

controllers/unit_controller.go:34

type OwnResource interface {
	// 根據Unit的指定,生成相應的各類own build-in資源對象,用作創建或更新
	MakeOwnResource(instance *extensionsv1beta1.Mycrd, logger logr.Logger, scheme *runtime.Scheme) (interface{}, error)

	// 判斷此ownResource資源是否已存在
	OwnResourceExist(instance *extensionsv1beta1.Mycrd, client client.Client, logger logr.Logger) (bool, interface{}, error)

	// 獲取對應的own build-in資源的狀態,用來填充Mycrd的status字段
	UpdateOwnResourceStatus(instance *extensionsv1beta1.Mycrd, client client.Client, logger logr.Logger) (*extensionsv1beta1.Mycrd, error)

	// 創建/更新 Mycrd對應的own build-in資源
	ApplyOwnResource(instance *extensionsv1beta1.Mycrd, client client.Client, logger logr.Logger, scheme *runtime.Scheme) error
}

對應有4個接口方法,分別應用於:

  • MakeOwnResource() 根據Unit的指定,生成相應的各類own build-in資源對象,用作創建或更新
  • OwnResourceExist() 判斷此ownResource資源是否已存在
  • UpdateOwnResourceStatus() 獲取對應的own build-in資源的狀態,用來填充Mycrd的status字段
  • ApplyOwnResource() 創建/更新 Mycrd對應的own build-in資源

也即是說,每種ownResource結構體都要實現這4個方法,同時要注意,這4種方法被用作CUR(不包括D),要求是冪等性的,多次執行結果一致。

篇幅有限,這裏只列舉ownStatefulSet的實現方法,其他的幾種資源的實現可直接去github庫裏查看。

ownStatefulSet 的接口方法實現

####MakeOwnResource

api/v1/own_statefulSet.go:22

type OwnStatefulSet struct {
	Spec appsv1.StatefulSetSpec
}

func (ownStatefulSet *OwnStatefulSet) MakeOwnResource(instance *Unit, logger logr.Logger,
	scheme *runtime.Scheme) (interface{}, error) {

	// new a StatefulSet object
	sts := &appsv1.StatefulSet{
		// metadata field inherited from owner Unit
		ObjectMeta: metav1.ObjectMeta{Name: instance.Name, Namespace:instance.Namespace, Labels: instance.Labels},
		Spec:       ownStatefulSet.Spec,
	}

	// add some customize envs, ignore this step if you don't need it
	customizeEnvs := []v1.EnvVar{
		{
			Name: "POD_NAME",
			ValueFrom: &v1.EnvVarSource{
				FieldRef: &v1.ObjectFieldSelector{
					APIVersion: "v1",
					FieldPath:  "metadata.name",
				},
			},
		},
		{
			Name:  "APPNAME",
			Value: instance.Name,
		},
	}

	var specEnvs []v1.EnvVar
	templateEnvs := sts.Spec.Template.Spec.Containers[0].Env
	for index := range templateEnvs {
		if templateEnvs[index].Name != "POD_NAME" && templateEnvs[index].Name != "APPNAME" {
			specEnvs = append(specEnvs, templateEnvs[index])
		}
	}

	sts.Spec.Template.Spec.Containers[0].Env = append(specEnvs, customizeEnvs...)

	// add ControllerReference for sts,the owner is Unit object
  // 這一步在下方會解釋它的作用
	if err := controllerutil.SetControllerReference(instance, sts, scheme); err != nil {
		msg := fmt.Sprintf("set controllerReference for StatefulSet %s/%s failed", instance.Namespace, instance.Name)
		logger.Error(err, msg)
		return nil, err
	}

	return sts, nil
}

OwnResourceExist

// Check if the StatefulSet already exists
func (ownStatefulSet *OwnStatefulSet) OwnResourceExist(instance *Unit, client client.Client,
	logger logr.Logger) (bool, interface{}, error) {

	found := &appsv1.StatefulSet{}
	err := client.Get(context.TODO(), types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, found)
	if err != nil {
		if errors.IsNotFound(err) {
			return false, nil, nil
		}
		msg := fmt.Sprintf("StatefulSet %s/%s found, but with error: %s  \n", instance.Namespace, instance.Name)
		logger.Error(err, msg)
		return true, found, err
	}
	return true, found, nil
}

ApplyOwnResource

// apply this own resource, create or update
func (ownStatefulSet *OwnStatefulSet) ApplyOwnResource(instance *Unit, client client.Client,
	logger logr.Logger, scheme *runtime.Scheme) error {

	// assert if StatefulSet exist
	exist, found, err := ownStatefulSet.OwnResourceExist(instance, client, logger)
	if err != nil {
		return err
	}

	// make StatefulSet object
	sts, err := ownStatefulSet.MakeOwnResource(instance, logger, scheme)
	if err != nil {
		return err
	}
	newStatefulSet := sts.(*appsv1.StatefulSet)

	// apply the StatefulSet object just make
	if !exist {
		// if StatefulSet not exist,then create it
		msg := fmt.Sprintf("StatefulSet %s/%s not found, create it!", newStatefulSet.Namespace, newStatefulSet.Name)
		logger.Info(msg)
		if err := client.Create(context.TODO(), newStatefulSet); err != nil {
			return err
		}
		return nil

	} else {
		foundStatefulSet := found.(*appsv1.StatefulSet)

		// if StatefulSet exist with change,then try to update it
		if !reflect.DeepEqual(newStatefulSet.Spec, foundStatefulSet.Spec) {
			msg := fmt.Sprintf("Updating StatefulSet %s/%s", newStatefulSet.Namespace, newStatefulSet.Name)
			logger.Info(msg)
			return client.Update(context.TODO(), newStatefulSet)
		}
		return nil
	}
}

UpdateOwnResourceStatus

func (ownStatefulSet *OwnStatefulSet) UpdateOwnResourceStatus(instance *Unit, client client.Client,
	logger logr.Logger) (*Unit, error) {

	found := &appsv1.StatefulSet{}
	err := client.Get(context.TODO(), types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, found)
	if err != nil {
		return instance, err
	}
	instance.Status.BaseStatefulSet = found.Status
	instance.Status.LastUpdateTime = metav1.Now()
	return instance, nil
}	

接口方法實現後,就要在Reconcile內調用了。

Apply OwnResources

getOwnResources

apply之前首先要生成所有的OwnResources對象,這個方法用來生成所有的ownResource對象放到一個array裏。詳情看註釋

controllers/unit_controller.go:220

// 根據Unit.Spec生成其所有的own resource
func (r *UnitReconciler) getOwnResources(instance *customv1.Unit) ([]OwnResource, error) {
	var ownResources []OwnResource

	// Deployment 和StatefulSet 二者只能存在其一。由於可以動態選擇,所以ownDeployment或ownStatefulSet在後端生成,不由前端指定
	if instance.Spec.Category == "Deployment" {
		ownDeployment := customv1.OwnDeployment{
			Spec: appsv1.DeploymentSpec{
				Replicas: instance.Spec.Replicas,
				Selector: instance.Spec.Selector,
				Template: instance.Spec.Template,
			},
		}
		ownDeployment.Spec.Template.Labels = instance.Spec.Selector.MatchLabels
		ownResources = append(ownResources, &ownDeployment)

	} else {
		ownStatefulSet := &customv1.OwnStatefulSet{
			Spec: appsv1.StatefulSetSpec{
				Replicas:    instance.Spec.Replicas,
				Selector:    instance.Spec.Selector,
				Template:    instance.Spec.Template,
				ServiceName: instance.Name,
			},
		}

		ownResources = append(ownResources, ownStatefulSet)
	}

	// 將關聯的資源(svc/ing/pvc)加入ownResources中
	if instance.Spec.RelationResource.Service != nil {
		ownResources = append(ownResources, instance.Spec.RelationResource.Service)
	}
	if instance.Spec.RelationResource.Ingress != nil {
		ownResources = append(ownResources, instance.Spec.RelationResource.Ingress)
	}
	if instance.Spec.RelationResource.PVC != nil {
		ownResources = append(ownResources, instance.Spec.RelationResource.PVC)
	}
	return ownResources, nil
}

apply

	// 3. 創建或更新操作
	// 3.1 根據Unit.spec 生成Unit關聯的所有own build-in resource
	ownResources, err := r.getOwnResources(instance)
	if err != nil {
		msg := fmt.Sprintf("%s %s Reconciler.getOwnResource() function error", instance.Namespace, instance.Name)
		r.Log.Error(err, msg)
		return ctrl.Result{}, err
	}

	// 3.2 判斷各own resource 是否存在,不存在則創建,存在則判斷spec是否有變化,有變化則更新
	success := true
	for _, ownResource := range ownResources {
		if err = ownResource.ApplyOwnResource(instance, r.Client, r.Log, r.Scheme); err != nil {
			fmt.Println("apply resource err:", err)
			success = false
		}
	}

這裏就用到了ApplyOwnResource接口方法了。Apply完成後,下一步就需要更新狀態了

Update OwnResourceStatus

controllers/unit_controller.go:152

	// 4. update Unit.status
	// 4.1 更新實例Unit.Status 字段
	updateInstance := instance.DeepCopy()
	for _, ownResource := range ownResources {
		updateInstance, err = ownResource.UpdateOwnResourceStatus(updateInstance, r.Client, r.Log)
		if err != nil {
			//fmt.Println("update Unit ownresource status error:", err)
			success = false
		}
	}

	// 4.2 apply update to apiServer if status changed
	if updateInstance != nil && !reflect.DeepEqual(updateInstance.Status, instance.Status) {
		if err := r.Status().Update(context.Background(), updateInstance); err != nil {
			r.Log.Error(err, "unable to update Unit status")
		}
	}

由於Status Update可能比較頻繁,因此有兩點值得一提:

  • 讀請求走的是informer local storage的cache,而寫請求是發起APIServer的直連,爲了減輕APIServer的壓力,要儘可能的減少寫請求,所以這裏設計爲所有的OwnResouces更新完畢後才發起一次Update請求。
  • 爲了與Spec的寫操作區分開,這裏使用的是r.Status().Update()的status局部更新方法,而不是r.Update()整體更新的方法,這樣可以儘量避免寫操作的併發衝突。
  • 如果連續多次Update,每次Update後Resource Object的Revision會更新,因此每次Update完成後,需要重新Get後才能再次Update,否則會報錯。

生命週期綁定

上面描述了創建/更新Unit時如何同步Apply更新到own resources,那如何保證Unit生命週期結束時(刪除),own resources跟隨一起結束呢?

實現的方式非常簡單,K8s已經替我們實現了,只需要給own resource加上一組特殊的標記即可。

還是以StatefulSet舉例,回顧上面的OwnStatefulSet.MakeOwnResource()方法,你會看到這麼幾行代碼:

func (ownStatefulSet *OwnStatefulSet) MakeOwnResource(instance *Unit, logger logr.Logger,
	scheme *runtime.Scheme) (interface{}, error) {
  ...
  
	// add ControllerReference for sts,the owner is Unit object
	if err := controllerutil.SetControllerReference(instance, sts, scheme); err != nil {
		msg := fmt.Sprintf("set controllerReference for StatefulSet %s/%s failed", instance.Namespace, instance.Name)
		logger.Error(err, msg)
		return nil, err
	}

	...
}

controllerutil.SetControllerReference(OwnerObj, OwnObj, scheme)方法,可以爲被管理的own resource實例加上控制來源描述,這樣,它便可與所屬對象實現生命週期的綁定了。

這個方法在K8s build-in資源中也多處使用,例如StatefulSet用來管理Pod。找一個被Sts管理的pod實例來看看:

加上這個ownerReferences描述之後,owner 刪除前,也會同步刪除own resources.

當然,如果在刪除owner時希望非級聯刪除,可以在kubectl命令末尾追加--cascade=false參數。

PreDelete操作

在上面添加了SetControllerReference的步驟後,默認的PreDelete策略是: 在刪除Owner之前,先確保刪除所有的Own resources,如果放任刪除乾淨了,即使是local cache也找不到資源的信息。如果需要在此之前實現自定義的刪除前操作,添加額外的自定義PreDelete鉤子,可以按下面的方式實現。

###PreDelete鉤子

使用controller GC的Finalizer終結器機制,可以在Delete之前加入PreDelete鉤子:

Advanced topics-Finalizer

翻譯一下,如果資源對象被直接刪除,就無法再讀取任何被刪除對象的信息,這就會導致後續的清理工作因爲信息不足無法進行,Finalizer字段設計來處理這種情況:

  • Finalizer本身只是一串隨機字符串標識,controller負責添加它和刪除它。
  • 添加了Finalizer之後,delete操作就會變成update操作,即爲對象加上deletionTimestamp時間戳
  • Finalizer已主動清空(視爲清理後續的任務已處理完成)之後,當前時間大於deletionTimestamp,就會開始執行gc

顯而易見,PreDelete鉤子要放在主動清除Finalizer之前。來看代碼:

controllers/unit_controller.go:98

	// 2. 刪除操作
	// 如果資源對象被直接刪除,就無法再讀取任何被刪除對象的信息,這就會導致後續的清理工作因爲信息不足無法進行,Finalizer字段設計來處理這種情況:
	// 2.1 當資源對象 Finalizer字段不爲空時,delete操作就會變成update操作,即爲對象加上deletionTimestamp時間戳
	// 2.2 當 當前時間在deletionTimestamp時間之後,且Finalizer已清空(視爲清理後續的任務已處理完成)的情況下,就會gc此對象了

	myFinalizerName := "storage.finalizers.tutorial.kubebuilder.io"
	//orphanFinalizerName := "orphan"

	// 2.1 DeletionTimestamp 時間戳爲空,代表着當前對象不處於被刪除的狀態,爲了開啓Finalizer機制,先給它加上一段Finalizers,內容隨機非空字符串即可
	if instance.ObjectMeta.DeletionTimestamp.IsZero() {
		// The object is not being deleted, so if it does not have our finalizer,
		// then lets add the finalizer and update the object. This is equivalent
		// registering our finalizer.
		if !containsString(instance.ObjectMeta.Finalizers, myFinalizerName) {
			instance.ObjectMeta.Finalizers = append(instance.ObjectMeta.Finalizers, myFinalizerName)
			if err := r.Update(ctx, instance); err != nil {
				r.Log.Error(err, "Add Finalizers error", instance.Namespace, instance.Name)
				return ctrl.Result{}, err
			}
		}
	} else {
		// 2.2  DeletionTimestamp不爲空,說明對象已經開始進入刪除狀態了,執行自己的刪除步驟後續的邏輯,並清除掉自己的finalizer字段,等待自動gc
		if containsString(instance.ObjectMeta.Finalizers, myFinalizerName) {

			// 在刪除owner resource之前,先執行自定義的預刪除步驟,例如刪除owner resource
			if err := r.PreDelete(instance); err != nil {
				// if fail to delete the external dependency here, return with error
				// so that it can be retried
				return ctrl.Result{}, err
			}

			// 移出掉自定義的Finalizers,這樣當Finalizers爲空時,gc就會正式開始了
			instance.ObjectMeta.Finalizers = removeString(instance.ObjectMeta.Finalizers, myFinalizerName)
			if err := r.Update(ctx, instance); err != nil {
				return ctrl.Result{}, err
			}
		}

		// Stop reconciliation as the item is being deleted
		return ctrl.Result{}, nil
	}


// Unit pre delete logic
func (r *UnitReconciler) PreDelete(instance *customv1.Unit) error {
	// 特別說明,own resource加上了ControllerReference之後,owner resource gc刪除前,會先自動刪除它的所有
	// own resources,因此綁定ControllerReference後無需再特別處理刪除own resource。

	// 這裏留空出來,是爲了如果有自定義的pre delete邏輯的需要,可在這裏實現。

	return nil
}

// Helper functions to check and remove string from a slice of strings.
func containsString(slice []string, s string) bool {
	for _, item := range slice {
		if item == s {
			return true
		}
	}
	return false
}

func removeString(slice []string, s string) (result []string) {
	for _, item := range slice {
		if item == s {
			continue
		}
		result = append(result, item)
	}
	return
}

如此,在PreDelete()方法內部,就可以實現自定義的PreDelete鉤子的邏輯了。

總結

控制器邏輯的核心,還是圍繞着owner和own resources之間來進行的,在這個過程中,儘量減少寫請求的頻率,同時可以充分利用k8s現有的各種生命週期管理機制。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章