如何使用 Go 呼叫 Kubernetes API - 型別和常用機制

語言: CN / TW / HK

C T O

    分享

    影片

  解答

Go Rust Python Istio containerd CoreDNS Envoy etcd Fluentd Harbor Helm Jaeger Kubernetes Open Policy Agent Prometheus Rook TiKV TUF Vitess Argo Buildpacks CloudEvents CNI Contour Cortex CRI-O Falco Flux gRPC KubeEdge Linkerd NATS Notary OpenTracing Operator Framework SPIFFE SPIRE     Thanos

如何使用 Go 呼叫 Kubernetes API - 型別和常用機制

官方的 Kubernetes Go 客戶端載入了高階抽象 - Clientset、Informers、Cache、Scheme、Discovery ,哦!當我嘗試在不先學習執行元件的情況下使用它時,我遇到了大量的新概念。這是一次不愉快的經歷,但更重要的是,它降低了我在程式碼中做出明智決策的能力。

因此,我決定徹底瞭解它的組成部分,為自己解開謎團。 client-go

但是從哪裡開始呢?在剖析 client-go 自身之前,瞭解它的兩個主要依賴項 k8s.io/api k8s.io/apimachinery 模組可能是一個好主意。它會簡化主要任務,但這不是唯一的好處。這兩個模組被排除在外是有原因的——它們不僅可以由客戶端使用,還可以在伺服器端或任何其他處理 Kubernetes 物件的軟體中使用。

API 資源、種類和物件

首先,快速回顧一下。熟悉以下概念對於進一步討論的成功至關重要:

  • 資源型別- 由 Kubernetes API 端點提供服務的實體: pods、deployments、configmaps 等。
  • API Group -資源型別被組織成版本化的邏輯組: apps/v1 batch/v1 storage.k8s.io/v1beta1
  • Object - 一個資源例項 - 每個 API 端點都處理某種資源型別的物件。
  • Kind - API 返回或接受的每個物件都必須符合物件模式- 由其種類定義的特定屬性組合: Pod Deployment , ConfigMap 等。

區分廣義上的物件和 Kubernetes “一流”物件也很重要——持久實體,如 Pod Service Secret 。雖然為了序列化和反序列化,每個 API 物件都必須具有 API 版本和種類屬性,但並非每個 API 物件都是  Kubernetes 物件。

模組 k8s.io/api

Go 是一種靜態型別的程式語言。那麼,與 Pods ConfigMaps Secrets 和其他的 Kubernetes 物件對應的所有結構都在哪裡?對,在 .k8s.io/api

儘管命名鬆散,但該 k8s.io/api 模組似乎僅用於 API 型別定義。它與我們都知道的 YAML 清單非常相似

package main

import (
  "fmt"

  appsv1 "k8s.io/api/apps/v1"
  corev1 "k8s.io/api/core/v1"
)

func main() {
  deployment := appsv1.Deployment{
    Spec: appsv1.DeploymentSpec{
      Template: corev1.PodTemplateSpec{
        Spec: corev1.PodSpec{
          Containers: []corev1.Container{
            { Name:  "web", Image: "nginx:1.21" },
          },
        },
      },
    },
  }

  fmt.Printf("%#v", &deployment)
}

該模組不僅定義了像上面這樣的頂級 Kubernetes 物件 Deployment ,還為它們的內部屬性定義了許多輔助型別:

// PodSpec is a description of a pod.
type PodSpec struct {
  Volumes []Volume `json:"volumes,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name" protobuf:"bytes,1,rep,name=volumes"`
  
  InitContainers []Container `json:"initContainers,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,20,rep,name=initContainers"`
    
  Containers []Container `json:"containers" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=containers"`

  EphemeralContainers []EphemeralContainer `json:"ephemeralContainers,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,34,rep,name=ephemeralContainers"`

  RestartPolicy RestartPolicy `json:"restartPolicy,omitempty" protobuf:"bytes,3,opt,name=restartPolicy,casttype=RestartPolicy"`
    
  ...
}

模組中定義的所有結構 k8s.io/api 都帶有註釋。不過要小心: jsonprotobuf

  • 支援編組為 JSON
  • 不鼓勵 Protobuf 序列化- 生成的結果可能與現有的 API 伺服器不相容(請參閱 README 瞭解更多資訊)。
https://github.com/kubernetes/api/tree/35d41aaac2bf55a353ccade31b852d466b2495c2#recommended-use

總結一下,這個 k8s.io/api 模組是:

  • Huge - 1000 多個描述 Kubernetes API 物件的結構。
  • 簡單——幾乎沒有演算法,只有“愚蠢”的資料結構。

  • 有用 - 它的資料型別被客戶端、伺服器、控制器等使用。

模組 k8s.io/apimachinery

與範圍狹窄的 k8s.io/api 模組不同,該 k8s.io/apimachinery 模組相當多。自述檔案將其目的描述為:

該庫是伺服器和客戶端使用 Kubernetes API 基礎設施的共享依賴項,無需直接型別依賴項。它的第一個消費者是 k8s.io/kubernetes , k8s.io/client-go k8s.io/apiserver

要在一篇文章中涵蓋 apimachery 模組的所有職責是很困難的。因此,我將討論這個模組中最常見的包、型別和功能。

有用的結構和介面

雖然該 k8s.io/api 模組專注於具體的高階型別,如 Deployments Secrets Pods ,但 k8s.io/apimachinery 它是低階但更通用的資料結構的家園。

例如, Kubernetes 物件的所有這些常見屬性: apiVersion kind name uid ownerReferences creationTimestamp 等。如果我要構建自己的 Kubernetes 自定義資源,我不需要自己為這些屬性定義資料型別——這要歸功於 apimachinery 模組。

k8s.io/apimachinery/pkg/apis/meta 包定義了兩個方便的結構 - TypeMeta 可以 ObjectMeta 嵌入到使用者定義的結構中,使其看起來很像任何其他 Kubernetes 物件。

此外, TypeMetaandObjectMeta 結構實現 meta.Type meta.Object 介面,可用於以通用方式指向任何相容物件。

模組中定義的另一個方便的型別 apimachinery interface runtime.Object 。由於其簡單的定義,它可能看起來毫無用處:

// pkg/runtime

type Object interface {
  GetObjectKind() schema.ObjectKind
  DeepCopyObject() Object
}

但在現實中,它被大量使用! Kubernetes 程式碼早在 Go 獲得真正的泛型支援之前就已經編寫好了。所以,這 runtime.Object 很像傳統的 interface{} 解決方法——它是一個通用介面,在程式碼庫中被廣泛的型別斷言和型別切換。並且可以通過檢查 kind 底層物件的來獲得實際型別。

:bulb: runtime.Object 例項可以指向具有該 kind 屬性的任何物件——成熟的 Kubernetes 物件、不攜帶元資料的更簡單的 API 資源,或具有明確定義的物件方案的任何其他型別的物件。

:warning: 請注意,雖然看起來相似,但由於結構偏移量非零, meta.Object 因此無法安全地向下轉換為相應的 Kubernetes 物件。

更有用的 apimachinery 型別:

  • PartialObjectMetadata 結構-元資料的組合。 TypeMeta 和元資料。·ObjectMeta·作為一種通用的方法來表示任何具有元資料的物件。
  • APIVersions , APIGroupList , APIGroup 結構體-還記得 kubectl get API 探索練習嗎-原始 /API ? 這些和類似的結構用於 Kubernetes API 資源的型別,但不是 Kubernetes 物件(例如,它們有 kind apiVersion 屬性,但沒有真正的 Object 元資料)。
  • GetOptions , ListOptions , UpdateOptions 等等——這些結構體代表了客戶端對資源的相應動作的引數。
  • GroupKind GroupVersionKind GroupResource GroupVersionResource 等——簡單的資料傳輸物件——包含組、版本、型別或資源字串的元組。

:bulb:在 Scheme RESTMapperGroupVersionKind 討論之前請記住——他們的知識會派上用場。 GroupVersionResource

非結構化結構

是的,你沒聽錯。撇開玩笑不談,它是另一種重要且廣泛使用的資料型別。

使用具體型別處理 Kubernetes 物件 k8s.io/api 很方便,但如果:

  • 您需要以通用方式使用 Kubernetes 物件嗎?
  • 您不想或不能依賴該 api 模組?
  • api 您需要使用模組中未定義的自定義資源嗎?

非結構化。用於救援的非結構化結構!這個結構體允許沒有註冊 Go 結構體的物件被操作為通用的 json 類物件:

type Unstructured struct {
  // Object is a JSON compatible map with
  // string, float, int, bool, []interface{}, or
  // map[string]interface{} children.
  Object map[string]interface{}
}

// And for the list of objects you can 
// use the UnstructuredList struct.
type UnstructuredList struct {
  Object map[string]interface{}

  Items []Unstructured
}

這兩個結構只是 map[string]interface{} . 但是,它們帶有一堆方便的方法來簡化巢狀屬性訪問和 JSON 編組/解組。

如何在 Go 程式碼中使用非結構化 Kubernetes 物件: https://github.com/iximiuz/client-go-examples/blob/5b220c4572d65ea8bf0ad68e369e015902e7521c/crud-dynamic-simple/main.go#L36

型別轉換 - 非結構化到型別化,反之亦然

自然地,可能需要將非結構化物件轉換為具體 k8s.io/api 型別的結構(反之亦然)。該 runtime.UnstructuredConverter 介面及其預設實現 DefaultUnstructuredConverter 可以幫助您:

type UnstructuredConverter interface {
  ToUnstructured(obj interface{}) (map[string]interface{}, error)
  FromUnstructured(u map[string]interface{}, obj interface{}) error
}

如何將非結構化物件轉換為型別化物件並返回: https://github.com/iximiuz/client-go-examples/tree/main/convert-unstructured-typed

物件序列化為 JSON、YAML 或 Protobuf

使用靜態型別語言的 API 時,另一個乏味的任務是將資料結構編組和解組到它們的有線表示中和從它們的有線表示中解組。

大量 apimachinery 程式碼專門用於此任務:

// pkg/runtime

// Encoder writes objects to a serialized form
type Encoder interface {
  Encode(obj Object, w io.Writer) error
  Identifier() Identifier
}

// Decoder attempts to load an object from data.
type Decoder interface {
  Decode(
    data []byte,
    defaults *schema.GroupVersionKind,
    into Object
  ) (Object, *schema.GroupVersionKind, error)
}

type Serializer interface {
  Encoder
  Decoder
}

注意到上面的程式碼片段中的這些物件了嗎?是的,這些是執行時。物件,也就是 Kind-able 介面{}例項。

例子:

如何將型別化的 Kubernetes 物件序列化為 JSON

https://github.com/iximiuz/client-go-examples/tree/main/serialize-typed-json

如何將型別化的 Kubernetes 物件序列化為 YAML

https://github.com/iximiuz/client-go-examples/tree/main/serialize-typed-yaml

如何將非結構化 Kubernetes 物件序列化為 JSON

https://github.com/iximiuz/client-go-examples/tree/main/serialize-unstructured-json

如何將非結構化 Kubernetes 物件序列化為 YAML

https://github.com/iximiuz/client-go-examples/tree/main/serialize-unstructured-yaml

Scheme 和 RESTMapper

執行時。在使用 client-go 時,方案概念隨處出現,特別是在編寫處理自定義資源的控制器(或 operator )時。

我花了一段時間才明白它的目的。然而,按照正確的順序處理事情會有所幫助。

想想非結構化到型別化轉換的潛在實現:有一個類似 JSON 的物件, k8s.io/api 需要從它建立一些具體型別的對應物件。第一步可能是弄清楚如何使用 kind string 建立型別化物件的空例項。

一種天真的方法可能看起來像是 switch 對所有可能型別(實際上是 API 組)的巨大宣告:

import (
  appsv1 "k8s.io/api/apps/v1"
  corev1 "k8s.io/api/core/v1"
)

func New(apiVersion, kind string) runtime.Object {
  switch (apiVersion + "/" + kind) {  
  case: "v1/Pod":
    return &corev1.Pod{}
  case: "apps/v1/Deployment":
    return &appsv1.Deployment{}
  }
  ...
}

更聰明的方法是使用反射。可以為所有已註冊的型別維護 switcha ,而不是: map[string]reflect.Type

type Registry struct {
  map[string]reflect.Type types
}

func (r *Registry) Register(apiVersion, kind string, typ reflect.Type) {
  r.types[apiVersion + "/" + kind] = typ
}

func (r *Registry) New(apiVersion, kind string) runtime.Object {
  return r.types[apiVersion + "/" + kind].New().(runtime.Object)
}

這種方法的優點是它不需要程式碼生成,並且可以在執行時新增新的型別對映。

現在,考慮一個反序列化問題:需要將一段 YAML JSON 轉換為 Typed object 。第一步 - 物件建立 - 將非常相似。

事實證明,通過 API 組和種類建立空物件是一項如此頻繁的任務,以至於它在模組中擁有了自己的元件 apimachinery- runtime.Scheme

// Scheme defines methods for serializing and deserializing API objects, a type
// registry for converting group, version, and kind information to and from Go
// schemas, and mappings between Go schemas of different versions. 
type Scheme struct {
  gvkToType map[schema.GroupVersionKind]reflect.Type
  
  typeToGVK map[reflect.Type][]schema.GroupVersionKind
  
  unversionedTypes map[reflect.Type]schema.GroupVersionKind
  
  unversionedKinds map[string]reflect.Type

  ...
}

runtime.Schemestruct 就是這樣一個登錄檔,其中包含各種 Kubernetes 物件的型別到型別和型別到型別的對映。

請記住, GroupVersionKind 它只是一個元組,即一個 DTO 結構,對嗎?:wink:

runtime.Scheme 結構實際上非常強大 - 它有一大堆方法並實現了一些基礎介面,例如:

// ObjectTyper contains methods for extracting 
// the APIVersion and Kind of objects.
type ObjectTyper interface {
  ObjectKinds(runtime.Object) ([]schema.GroupVersionKind, bool, error)
  Recognizes(gvk schema.GroupVersionKind) bool
}

// ObjectCreater contains methods for instantiating
// an object by kind and version.
type ObjectCreater interface {
  New(kind schema.GroupVersionKind) (out Object, err error)
}

不過, runtime.Scheme 也不是萬能的。它有從 kind types 的對映,但如果不是 kind ,只知道資源名稱怎麼辦?

這就是 RESTMapper 開始的地方:

type RESTMapper interface {
  // KindFor takes a partial resource and returns the single match.  Returns an error if there are multiple matches
  KindFor(resource schema.GroupVersionResource) (schema.GroupVersionKind, error)

  // KindsFor takes a partial resource and returns the list of potential kinds in priority order
  KindsFor(resource schema.GroupVersionResource) ([]schema.GroupVersionKind, error)

  ...
  
  ResourceSingularizer(resource string) (singular string, err error)
}

RESTMapper 也是某種登錄檔。但是,它維護資源到種類的對映。因此,向對映器提供一個類似的字串 apps/v1/deployments 會給出 API Groupapps/v1 kind Deployment RESTMapper 也可以處理資源快捷方式和單數化: po, pod, 並且 pods 可以註冊為同一資源的別名。

通常情況下,會有一個全域性的單例執行時。然而,似乎 apimachery 模組本身試圖避免狀態——它定義了 RESTMapper Scheme 結構,但沒有例項化它們。

與執行時。該方案被 apimachery 模組本身廣泛使用, RESTMapper 在內部沒有使用,至少目前沒有。

欄位和標籤選擇器

欄位和標籤的型別、建立和匹配邏輯也存在於 apimachinery 模組中。例如,以下是可以使用 k8s.io/apimachinery/pkg/labels 包執行的操作:

lbl := labels.Set{"foo": "bar"}
sel, _ = labels.Parse("foo==bar")
if sel.Matches(lbl) {
  fmt.Printf("Selector %v matched label set %v\n", sel, lbl)
}

:book:例子:

如何使用標籤集和標籤選擇器

https://github.com/iximiuz/client-go-examples/tree/main/label-selectors

如何使用欄位集和欄位選擇器

https://github.com/iximiuz/client-go-examples/tree/main/field-selectors

API 錯誤處理

如果不正確處理其錯誤,就不可能在程式碼中使用 Kubernetes API API 伺服器可能完全消失,請求可能未經授權,物件可能丟失,併發更新可能發生衝突。幸運的是,該 k8s.io/apimachinery/pkg/api/errors 包定義了一些方便的實用程式函式來處理 API 錯誤。這是一個例子:

_, err = client.
  CoreV1().
  ConfigMaps("default").
  Get(
    context.Background(),
    "this_name_definitely_does_not_exist",
    metav1.GetOptions{},
  )
if !errors.IsNotFound(err) {
  panic(err.Error())
}

:book:示例-如何處理 Kubernetes API 錯誤。https://github.com/iximiuz/client-go-examples/tree/main/error-handling

util實用程式

最後但並非最不重要的一點是, apimachinery/pkg/util 包裝中充滿了有用的東西。這裡有些例子:

  • util/wait 包通過重試和適當的退避/抖動實現來簡化等待資源出現或消失的任務。
  • util/yaml 有助於解組 YAML 或將其轉換為 JSON

總結

k8s.io/api k8s.io/apimachinery 包是學習如何在 Go 中使用 Kubernetes 物件的一個很好的起點。如果你需要編寫你的第一個控制器,直接跳到 client-go ,甚至是控制器執行時或 kubebuilder 可能會使學習體驗過於粗糙——可能存在太多的知識空白。但是,先看看和玩一下 api apimachinery 包將幫助您在接下來的旅程中保持安心

參考地址 [1]

參考資料

[1]

參考地址: https://iximiuz.com/en/posts/kubernetes-api-go-types-and-common-machinery/