自己動(dòng)手寫(xiě)一個(gè) K8S YAML 模板化工具

我們?cè)谑褂?Kubernetes 編寫(xiě)資源清單文件的時(shí)候,往往會(huì)使用類似于 Helm 或者 Kustomize 這樣的工具來(lái)進(jìn)行模板化處理,一來(lái)是提高了資源清單的靈活性,另一方面也確實(shí)降低了我們安裝復(fù)雜的 Kubernetes 應(yīng)用的門(mén)檻。本文我們嘗試自己使用 Golang 來(lái)實(shí)現(xiàn)一個(gè) YAML 資源清單文件模板化的方工具。
Golang 的模板化
Golang 中有一個(gè)支持模板文本文件的標(biāo)準(zhǔn)庫(kù) text/template,這個(gè)庫(kù)允許我們運(yùn)行函數(shù)、賦值等操作,并可以執(zhí)行一些邏輯來(lái)替換一些源文本中的模板值,我們可以從文件中讀取這些文本,也可以從一個(gè)字符串去進(jìn)行解析。由于我們想要模板化 YAML 文件,所以會(huì)從文件中去讀取,這樣我們就可以用如下所示的代碼來(lái)進(jìn)行處理:
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
}
上面的代碼讀取一個(gè)位于 filePath 的文件,并將其作為模板,使用 availableFunctions 中的函數(shù)和 availableData 中的數(shù)據(jù)來(lái)填充所有的模板值。比如我們讀取的是一個(gè) 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 函數(shù)調(diào)用后的輸出結(jié)果如下所示。
apiVersion:?v1
kind:?ConfigMap
metadata:
??name:?my-configmap
??namespace:?my-namespace
??labels:
????app:?myapp
data:
??USER:?admin
??PASSWORD:?s0m3p455w0rd?#?依賴你的?GeneratePassword?函數(shù)
在程序中使用 YAML
當(dāng)我們使用 kubectl 這樣的 CLI 工具的時(shí)候,在 Kubernetes 中使用 YAML 非常簡(jiǎn)單:
kubectl?create?-f?myfile.yaml
但是如果要我們自己去編寫(xiě)代碼來(lái)應(yīng)用 YAML 文件的話,一般情況下會(huì)去使用 client-go 這個(gè)客戶端工具包,但是 client-go 是針對(duì)靜態(tài)類型的,而 YAML 文件中是沒(méi)有對(duì)應(yīng)的信息的,但是我們還可以通過(guò)下面兩種方案來(lái)解決這個(gè)問(wèn)題。
使用 YAML 中的 Kind 和 Version 反序列化為靜態(tài)類型,然后使用它的類型化 REST 客戶端進(jìn)行通信。 使用 Discovery 功能,Discovery 允許我們動(dòng)態(tài)地查找給定類型的 REST 客戶端,而不是通過(guò)靜態(tài)類型去訪問(wèn),下面我們就使用這種方式來(lái)進(jìn)行演示。
首先我們需要像往常一樣與 APIServer 通信創(chuàng)建一個(gè) ClientSet 對(duì)象,如果我們從一個(gè)可以使用 kubectl 的系統(tǒng)執(zhí)行代碼,就意味著有一個(gè)可用的 kubeconfig 文件可以使用,通常這個(gè)文件為 $HOME/.kube/config 文件,如下所示:
import?(
????"k8s.io/client-go/tools/clientcmd"
????"k8s.io/client-go/kubernetes"
)
...
//?使用本地?~/.kube/config?創(chuàng)建配置
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 相當(dāng)于和 K8S 集群通信的網(wǎng)關(guān),使用它我們可以獲取對(duì)象來(lái)給我們提供發(fā)現(xiàn)接口。對(duì)于我們想要實(shí)現(xiàn)的功能,需要能夠查詢給定資源的類型,并與該類型的 REST 客戶端進(jìn)行通信,所以我們分別需要一個(gè) Discovery REST mapper 和一個(gè)動(dòng)態(tài)的 REST 接口,代碼如下所示:
import?(
????"k8s.io/client-go/restmapper"
????"k8s.io/client-go/dynamic"
)
...
//?獲取支持的資源類型列表
resources,?err?:=?restmapper.GetAPIGroupResources(c.Discovery())
if?err?!=?nil?{
????log.Fatal(err)
}
//?創(chuàng)建?'Discovery?REST?Mapper',獲取查詢的資源的類型
mapper:=?restmapper.NewDiscoveryRESTMapper(resourcesAvailable)
//?獲取?'Dynamic?REST?Interface',獲取一個(gè)指定資源類型的?REST?接口
dynamicREST,?err?:=?dynamic.NewForConfig(config)
if?err?!=?nil?{
????log.Fatal(err)
}
接下來(lái)我們?nèi)ゲ檎?YAML 文件中所代表的對(duì)象類型,并得到一個(gè)支持它的 REST 客戶端是不是就可以去操作這個(gè)資源對(duì)象了?
首先調(diào)用前面的 Read 函數(shù)讀取并執(zhí)行一個(gè)模板:
finalYAML,?err?:=?templates.Read(myFilePath)
if?err?!=?nil?{
????log.Fatal(err)
}
為了使用我們的 Discovery REST mapper 和動(dòng)態(tài) REST 接口,我們需要將 YAML 文件的內(nèi)容 decode 成一個(gè) runtime.Objects 對(duì)象。
首先將 YAML 文件內(nèi)容根據(jù) --- 進(jìn)行分割(一個(gè) YAML 文件中可能有多個(gè)資源對(duì)象):
objectsInYAML?:=?bytes.Split(yamlBytes,?[]byte("---"))
if?len(objectsInYAML)?==?0?{
????return?nil,?nil
}
然后在每個(gè)片段上使用 k8s.io 的反序列化功能輸出得到 runtime.Object 對(duì)象,以及一個(gè)持有 Group、Version 和 Kind 信息的結(jié)構(gòu)體。
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)
????}
...
現(xiàn)在我們可以回頭去使用我們的 RESTMapper,通過(guò)上面得到的 GVK 來(lái)獲取一個(gè)映射:
//?查找?Group/Version/Kind?的?REST?映射
mapping,?err?:=?d.mapper.RESTMapping(groupVersionAndKind.GroupKind(),?groupVersionAndKind.Version)
if?err?!=?nil?{
????log.Fatal(err)
}
有了資源類型,我們就可以使用前面的動(dòng)態(tài) REST 接口獲取特定資源對(duì)象的客戶端了:
unstructuredObj?:=?runtimeObject.(*unstructured.Unstructured)
var?resourceREST?dynamic.ResourceInterface
//?需要為?namespace?范圍內(nèi)的資源提供不同的接口
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 對(duì)象來(lái)執(zhí)行創(chuàng)建刪除等操作了!
import?(
????metav1?"k8s.io/apimachinery/pkg/apis/meta/v1"
)
//?創(chuàng)建對(duì)象
_,?err?=?resourceREST.Create(unstructuredObj,?metav1.CreateOptions{})
if?err?!=?nil?{
????log.Fatal(err)
}
//?刪除對(duì)象
prop?:=?metav1.DeletePropagationForeground
err?=?resourceREST.Delete(unstructuredObj.GetName(),
????&metav1.DeleteOptions{
???????PropagationPolicy:?&prop,
????})
if?err?!=?nil?{
???log.Fatal(err)
}
到這里我們就使用 Golang 完成了一個(gè)輕量級(jí)的 YAML 模板處理工具了。
雙11課程優(yōu)惠活動(dòng)~

?點(diǎn)擊屏末?|?閱讀原文?|?即刻學(xué)習(xí)
