避坑指南 | 我非要把這個bug優雅的解決掉

  • 2019 年 11 月 14 日
  • 筆記

騰訊雲服務器http://t.cn/AirXAQ1X

之前寫過手把手教你編寫一個operator在中間件容器化中的實踐,以及自定義代碼生成:

https://liabio.blog.csdn.net/article/details/93620155

記錄了編寫redis-operator的過程,達到自動操作redis集群,達到自動化運維的目的。

最近搞正在搞給redis operator加記錄events事件功能時,報錯了(可以說每次加功能都沒有順順利利的,但這又如何,奈何它也擋不住洒家探索的腳步)報錯如下:

Could not construct reference to ***due to: 『no kind is registered for the type *** in scheme... 』

記錄事件events代碼如下:

可以一路跟蹤代碼到client-go/tools/reference/ref.go中:

可以看到45行,其實強轉為v1.ObjectReference,也是不會報錯的。所以我們可以構建如下的結構體:

代碼像這樣子:

ref := &v1.ObjectReference{              APIVersion: controllerKind.Group + "/" + controllerKind.Version,              Kind: controllerKind.Kind,              Name: redisCluster.Name,              Namespace: redisCluster.Namespace,              UID:redisCluster.UID,              ResourceVersion: redisCluster.ResourceVersion,          }  rco.eventRecorder.Eventf(ref, v1.EventTypeNormal, "CreateCluster", "create cluster start")

但每次記錄events前,都寫這麼一坨,顯然是逼格很低的,況且縱觀k8s源碼,到處都是直接傳pod、svc等內置類型(其實現了runtime.Object),隨手一找就看到下面這個樣子,將&svc直接傳入,優雅很多。

那麼該怎麼搞才能直接把redisCluster結構體傳進去不報錯呢?更接近原生代碼風格,更優雅呢?我們先分析下面的源碼:

// GetReference returns an ObjectReference which refers to the given  // object, or an error if the object doesn't follow the conventions  // that would allow this.  // TODO: should take a meta.Interface see http://issue.k8s.io/7127  func GetReference(scheme *runtime.Scheme, obj runtime.Object) (*v1.ObjectReference, error) {      if obj == nil {          return nil, ErrNilObject      }      if ref, ok := obj.(*v1.ObjectReference); ok {          // Don't make a reference to a reference.          return ref, nil      }        gvk := obj.GetObjectKind().GroupVersionKind()        // if the object referenced is actually persisted, we can just get kind from meta      // if we are building an object reference to something not yet persisted, we should fallback to scheme      kind := gvk.Kind      if len(kind) == 0 {          // TODO: this is wrong          gvks, _, err := scheme.ObjectKinds(obj)          if err != nil {              return nil, err          }          kind = gvks[0].Kind      }        // An object that implements only List has enough metadata to build a reference      var listMeta metav1.Common      objectMeta, err := meta.Accessor(obj)      if err != nil {          listMeta, err = meta.CommonAccessor(obj)          if err != nil {              return nil, err          }      } else {          listMeta = objectMeta      }        // if the object referenced is actually persisted, we can also get version from meta      version := gvk.GroupVersion().String()      if len(version) == 0 {          selfLink := listMeta.GetSelfLink()          if len(selfLink) == 0 {              return nil, ErrNoSelfLink          }          selfLinkUrl, err := url.Parse(selfLink)          if err != nil {              return nil, err          }          // example paths: /<prefix>/<version>/*          parts := strings.Split(selfLinkUrl.Path, "/")          if len(parts) < 4 {              return nil, fmt.Errorf("unexpected self link format: '%v'; got version '%v'", selfLink, version)          }          if parts[1] == "api" {              version = parts[2]          } else {              version = parts[2] + "/" + parts[3]          }      }        // only has list metadata      if objectMeta == nil {          return &v1.ObjectReference{              Kind:            kind,              APIVersion:      version,              ResourceVersion: listMeta.GetResourceVersion(),          }, nil      }        return &v1.ObjectReference{          Kind:            kind,          APIVersion:      version,          Name:            objectMeta.GetName(),          Namespace:       objectMeta.GetNamespace(),          UID:             objectMeta.GetUID(),          ResourceVersion: objectMeta.GetResourceVersion(),      }, nil  }

看到最後其實會幫助我們構造v1.ObjectReference對象,但前提是能正確獲取到從傳入CRD的objectMeta、kind、version,一開始的報錯就是因為 scheme.ObjectKinds(obj)獲取kind時返回error導致。

怎麼才能正確獲取到kind、version、objectMeta呢?可以查看官方提供的sample-controller,這也是最簡單的通過CRD擴展的控制器示例:

// Create event broadcaster  // Add sample-controller types to the default Kubernetes Scheme so Events can be  // logged for sample-controller types.  utilruntime.Must(samplescheme.AddToScheme(scheme.Scheme))

創建事件廣播器,將樣本控制器類型添加到默認的Kubernetes方案,以便可以記錄事件以獲取樣本控制器類型。

samplescheme.AddToScheme(scheme.Scheme)會去註冊一些GroupVersion、kind等信息到Scheme,這樣在記錄events時才可以正確獲取到。對應到我們的CRD,redisCluster對象,就應該是

utilruntime.Must(redisscheme.AddToScheme(scheme.Scheme))

這裡的redisscheme是指向code-generator生成的clientset包下的versioned/scheme,register.go中

根據golang代碼執行順序可知,被調用時,init函數會被執行,從而調用 v1.AddToGroupVersion進行註冊流程:

這樣分析,順便把代碼修改一番後,重新調試,可以看到正確記錄了events事件: