When we write resource manifest files with Kubernetes, we often use tools like Helm or Kustomize for templating, both to improve the flexibility of resource manifests and to really lower the threshold for installing complex Kubernetes applications. In this article we try to implement a YAML resource manifest file templating solution ourselves using Golang.

Golang’s Templating

Golang has a standard library text/template that supports templated text files. This library allows us to run functions, assignments, and other operations, and can perform some logic to replace some of the template values in the source text, either by reading the text from the file or by parsing it from a string. Since we want to templatize the YAML file, we will read from the file, so we can use the code shown below to do this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
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
}

The above code reads a file located in filePath and uses it as a template, using the functions in availableFunctions and the data in availableData to populate all the template values. For example, we are reading a YAML file of ConfigMap.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-configmap
  namespace: {{ .Namespace }}
  labels:
    app: myapp
data:
  USER: admin
  PASSWORD: {{ GeneratePassword }}

Then we define availableData and availableFunctions as the code shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var availableData = map[string]string{
    "Namespace": "my-namespace",
}
var availableFunctions = template.FuncMap{
    "GeneratePassword": GeneratePasswordFunc,
}

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

The output of the above defined Read function call is shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
apiVersion: v1
kind: ConfigMap
metadata:
  name: my-configmap
  namespace: my-namespace
  labels:
    app: myapp
data:
  USER: admin
  PASSWORD: s0m3p455w0rd # 依赖你的 GeneratePassword 函数

Using YAML in your application

When we use a CLI tool like kubectl, using YAML in Kubernetes is very simple:

1
kubectl create -f myfile.yaml

However, if we were to write our own code to apply the YAML file, we would normally use the client-go client toolkit, but client-go is for static types, and there is no corresponding information in the YAML file, but we can solve this problem with the following two options.

  • Use Kind and Version in YAML to deserialize to static types, and then use its typed REST client to communicate.
  • Using the Discovery feature, Discovery allows us to dynamically find the REST client of a given type instead of accessing it via static types, as we demonstrate below using this approach.

First we need to communicate with the APIServer as usual to create a ClientSet object. If we execute the code from a system that can use kubectl, this means that there is a kubeconfig file available, which is usually the $HOME/.kube/config file, as follows shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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)
}

The ClientSet acts as a gateway to communicate with the K8S cluster, using it we can fetch objects to give us the discovery interface. For the functionality we want to implement, we need to be able to query the type of a given resource and communicate with the REST client of that type, so we need a Discovery REST mapper and a Dynamic REST interface respectively, with the code shown below.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)
}

Next we look up the type of object represented in the YAML file and get a REST client that supports it so we can manipulate the resource object?

First call the previous Read function to read and execute a template.

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

In order to use our Discovery REST mapper and Dynamic REST interface, we need to decode the contents of the YAML file into a runtime.Objects object.

The contents of the YAML file are first split according to -- (there may be multiple resource objects in a single YAML file).

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

The deserialization function of k8s.io is then used on each fragment to output the runtime.Object object and a structure holding the Group, Version and Kind information.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
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)
    }
...

Now we can go back and use our RESTMapper to get a mapping with the GVK we got above.

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

With the resource type in place, we can use the previous dynamic REST interface to obtain the client of a specific resource object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
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)
}

At this point we can use the resulting client objects in Kubernetes to perform operations such as creation and deletion!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
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)
}

At this point we’ve completed a lightweight YAML template processing tool using Golang.