天天看點

自己動手寫一個 K8S YAML 模闆化工具

自己動手寫一個 K8S YAML 模闆化工具

我們在使用 Kubernetes 編寫資源清單檔案的時候,往往會使用類似于

Helm

或者

Kustomize

這樣的工具來進行模闆化處理,一來是提高了資源清單的靈活性,另一方面也确實降低了我們安裝複雜的 Kubernetes 應用的門檻。本文我們嘗試自己使用 Golang 來實作一個 YAML 資源清單檔案模闆化的方工具。

Golang 的模闆化

Golang 中有一個支援模闆文本檔案的标準庫

text/template

,這個庫允許我們運作函數、指派等操作,并可以執行一些邏輯來替換一些源文本中的模闆值,我們可以從檔案中讀取這些文本,也可以從一個字元串去進行解析。由于我們想要模闆化 YAML 檔案,是以會從檔案中去讀取,這樣我們就可以用如下所示的代碼來進行處理:

package templates

import (
    "bytes"
    "path/filepath"
    "text/template"
    ...
)

func Read(filePath string) ([]byte, error) {
    tmpl, err := template.New(filepath.Base(filePath)).
    Funcs(availableFunctions).
    ParseFiles(filePath)
    if err != nil {
        return nil, err
    }
    var buf bytes.Buffer
    if err := tmpl.Execute(&buf, availableData); err != nil {
        return nil, err
    }
    return buf.Bytes(), nil
}
           

複制

上面的代碼讀取一個位于 filePath 的檔案,并将其作為模闆,使用

availableFunctions

中的函數和

availableData

中的資料來填充所有的模闆值。比如我們讀取的是一個

ConfigMap

的 YAML 檔案。

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-configmap
  namespace: {{ .Namespace }}
  labels:
    app: myapp
data:
  USER: admin
  PASSWORD: {{ GeneratePassword }}
           

複制

然後我們把

availableData

availableFunctions

定義成如下所示的代碼。

var availableData = map[string]string{
    "Namespace": "my-namespace",
}
var availableFunctions = template.FuncMap{
    "GeneratePassword": GeneratePasswordFunc,
}

func GeneratePasswordFunc() (string, error) {
...
}
           

複制

這樣上面定義的 Read 函數調用後的輸出結果如下所示。

apiVersion: v1
kind: ConfigMap
metadata:
  name: my-configmap
  namespace: my-namespace
  labels:
    app: myapp
data:
  USER: admin
  PASSWORD: s0m3p455w0rd # 依賴你的 GeneratePassword 函數
           

複制

在程式中使用 YAML

當我們使用 kubectl 這樣的 CLI 工具的時候,在 Kubernetes 中使用 YAML 非常簡單:

kubectl create -f myfile.yaml
           

複制

但是如果要我們自己去編寫代碼來應用 YAML 檔案的話,一般情況下會去使用

client-go

這個用戶端工具包,但是 client-go 是針對靜态類型的,而 YAML 檔案中是沒有對應的資訊的,但是我們還可以通過下面兩種方案來解決這個問題。

  • 使用 YAML 中的 Kind 和 Version 反序列化為靜态類型,然後使用它的類型化 REST 用戶端進行通信。
  • 使用 Discovery 功能,Discovery 允許我們動态地查找給定類型的 REST 用戶端,而不是通過靜态類型去通路,下面我們就使用這種方式來進行示範。

首先我們需要像往常一樣與 APIServer 通信建立一個 ClientSet 對象,如果我們從一個可以使用 kubectl 的系統執行代碼,就意味着有一個可用的

kubeconfig

檔案可以使用,通常這個檔案為

$HOME/.kube/config

檔案,如下所示:

import (
    "k8s.io/client-go/tools/clientcmd"
    "k8s.io/client-go/kubernetes"
)
...
// 使用本地 ~/.kube/config 建立配置
kubeConfigPath := os.ExpandEnv("$HOME/.kube/config")
config, err := clientcmd.BuildConfigFromFlags("", kubeConfigPath)
if err != nil {
    log.Fatal(err)
}
// 使用上面的配置擷取連接配接
c, err := kubernetes.NewForConfig(config)
if err != nil {
    log.Fatal(err)
}
           

複制

ClientSet 相當于和 K8S 叢集通信的網關,使用它我們可以擷取對象來給我們提供發現接口。對于我們想要實作的功能,需要能夠查詢給定資源的類型,并與該類型的 REST 用戶端進行通信,是以我們分别需要一個 Discovery REST mapper 和一個動态的 REST 接口,代碼如下所示:

import (
    "k8s.io/client-go/restmapper"
    "k8s.io/client-go/dynamic"
)
...
// 擷取支援的資源類型清單
resources, err := restmapper.GetAPIGroupResources(c.Discovery())
if err != nil {
    log.Fatal(err)
}
// 建立 'Discovery REST Mapper',擷取查詢的資源的類型
mapper:= restmapper.NewDiscoveryRESTMapper(resourcesAvailable)
// 擷取 'Dynamic REST Interface',擷取一個指定資源類型的 REST 接口
dynamicREST, err := dynamic.NewForConfig(config)
if err != nil {
    log.Fatal(err)
}
           

複制

接下來我們去查找 YAML 檔案中所代表的對象類型,并得到一個支援它的 REST 用戶端是不是就可以去操作這個資源對象了?

首先調用前面的 Read 函數讀取并執行一個模闆:

finalYAML, err := templates.Read(myFilePath)
if err != nil {
    log.Fatal(err)
}
           

複制

為了使用我們的 Discovery REST mapper 和動态 REST 接口,我們需要将 YAML 檔案的内容 decode 成一個

runtime.Objects

對象。

首先将 YAML 檔案内容根據

---

進行分割(一個 YAML 檔案中可能有多個資源對象):

objectsInYAML := bytes.Split(yamlBytes, []byte("---"))
if len(objectsInYAML) == 0 {
    return nil, nil
}
           

複制

然後在每個片段上使用 k8s.io 的反序列化功能輸出得到 runtime.Object 對象,以及一個持有 Group、Version 和 Kind 資訊的結構體。

import(
    "k8s.io/apimachinery/pkg/runtime/serializer/yaml"
)
...
for _, objectInYAML := range objectsInYAML {
    runtimeObject, groupVersionAndKind, err := 
    yaml.
        NewDecodingSerializer(unstructured.UnstructuredJSONScheme).
        Decode(objectInYAML.Raw, nil, nil)
    if err != nil {
        log.Fatal(err)
    }
...
           

複制

現在我們可以回頭去使用我們的 RESTMapper,通過上面得到的 GVK 來擷取一個映射:

// 查找 Group/Version/Kind 的 REST 映射
mapping, err := d.mapper.RESTMapping(groupVersionAndKind.GroupKind(), groupVersionAndKind.Version)
if err != nil {
    log.Fatal(err)
}
           

複制

有了資源類型,我們就可以使用前面的動态 REST 接口擷取特定資源對象的用戶端了:

unstructuredObj := runtimeObject.(*unstructured.Unstructured)
var resourceREST dynamic.ResourceInterface
// 需要為 namespace 範圍内的資源提供不同的接口
if mapping.Scope.Name() == meta.RESTScopeNameNamespace {
    if unstructuredObj.GetNamespace() == "" {
        unstructuredObj.SetNamespace("default")
    }
    resourceREST = 
    d.
      dynamicREST.
      Resource(mapping.Resource).
      Namespace(unstructuredObj.GetNamespace())
} else {
    resourceREST = d.dynamicREST.Resource(mapping.Resource)
}
           

複制

到這裡我們就可以在 Kubernetes 中使用得到的 client 對象來執行建立删除等操作了!

import (
    metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// 建立對象
_, err = resourceREST.Create(unstructuredObj, metav1.CreateOptions{})
if err != nil {
    log.Fatal(err)
}
// 删除對象
prop := metav1.DeletePropagationForeground
err = resourceREST.Delete(unstructuredObj.GetName(),
    &metav1.DeleteOptions{
       PropagationPolicy: &prop,
    })
if err != nil {
   log.Fatal(err)
}
           

複制

到這裡我們就使用 Golang 完成了一個輕量級的 YAML 模闆處理工具了。