實現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鉤子:
翻譯一下,如果資源對象被直接刪除,就無法再讀取任何被刪除對象的信息,這就會導致後續的清理工作因爲信息不足無法進行,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現有的各種生命週期管理機制。