Kubernetes

Apply YAML manifests to Kubernetes using Go client

We use the kubectl apply command to apply resource configurations in YAML format to Kubernetes. But, how do we do this programmatically? Go programmers can use the client-go library and achieve similar results. This article can provide the idea for mimicking the kubectl apply command in Go.

The Go example involves the following steps:

  1. Create Dynamic Kubernetes client
  2. Create Discovery REST mapper
  3. Decode YAML documents
  4. Discover GroupVersionResource
  5. Apply configuration to Kubernetes

1. Create Dynamic Kubernetes client

The client-go library provides the package ‘dynamic‘ to implement the generic functionalities across different resources.

// create the dynamic client with standard rest.Config
client, err := dynamic.NewForConfig(config)

2. Create Discovery REST mapper

Next, create an instance of Discovery REST mapper to work with the dynamic resources. Kubernetes REST APIs are organized as Group, Version, and Resources. So, we need an instance of schema.GroupVersionResource to access the dynamic APIs. The input YAML manifests do not contain this information. Discovery REST mapper discovers the API resources from Kubernetes and its associated Group/Versions.

  • Group – APIs are grouped with related functionalities. Example: apps, networking.k8s.io
  • Version – The API version allows future modifications to the API behavior while providing backward compatibility. Example: v1, v1beta1
  • Resource – Use of a kind in API endpoint. Example: replicasets, deployments
// create a discovery client to map dynamic API resources
clientset, err := kubernetes.NewForConfig(config)
if err != nil {
	log.Fatalf("failed to create discovery client: %w", err)
}
discoveryClient := memory.NewMemCacheClient(clientset.Discovery())
discoveryMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)

3. Decode YAML documents

The input file can have multiple YAML documents with the separator “—“. Let’s decode the documents individually and apply the configurations to Kubernetes.

Package unstructured.Unstructured is the companion for dynamic client that can hold any resource. So, we can parse the YAML document and store it as an Unstructured object.

# Decode multiple YAML documents from the same file
dec := yaml.NewDecoder(r)
for {
	// parse the YAML doc
	obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
	err := dec.Decode(obj.Object)
	...
}

4. Discover GroupVersionResource

Once the YAML document is decoded into an Unstructured object, we will get the object Kind and discover the associated GroupVersionResource as shown below.

// get GroupVersionResource to invoke the dynamic client
gvk := obj.GroupVersionKind()
restMapping, err := k.discoveryMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
gvr := restMapping.Resource

5. Apply the configuration to Kubernetes

Finally, we can apply the configuration stored in the Unstructured object with the GroupVersionResource.

applyOpts := metav1.ApplyOptions{FieldManager: "k8s-dynamic-client"}
_, err = k.client.Resource(gvr).Namespace(namespace).Apply(context.TODO(), obj.GetName(), obj, applyOpts)

6. The Complete Example

kubeclient.go

/* kubeapply */
package main

import (
	"context"
	"errors"
	"fmt"
	"io"
	"log"

	"gopkg.in/yaml.v3"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
	"k8s.io/client-go/discovery/cached/memory"
	"k8s.io/client-go/dynamic"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/restmapper"
	"k8s.io/client-go/tools/clientcmd"
)

// KubeClient is a dynamic type of client for kubernetes
type KubeClient struct {
	client          *dynamic.DynamicClient
	discoveryMapper *restmapper.DeferredDiscoveryRESTMapper
}

// NewKubeClient creates an instance of KubeClient
func NewKubeClient(kubeConfigFile string) *KubeClient {
	config, err := clientcmd.BuildConfigFromFlags("", kubeConfigFile)
	if err != nil {
		log.Fatal(err)
	}
	// create the dynamic client
	client, err := dynamic.NewForConfig(config)
	if err != nil {
		log.Fatalf("failed to create dynamic client: %w", err)
	}
	// create a discovery client to map dynamic API resources
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		log.Fatalf("failed to create discovery client: %w", err)
	}
	discoveryClient := memory.NewMemCacheClient(clientset.Discovery())
	discoveryMapper := restmapper.NewDeferredDiscoveryRESTMapper(discoveryClient)
	return &KubeClient{client: client, discoveryMapper: discoveryMapper}
}

// Apply applies the given YAML manifests to kubernetes
func (k *KubeClient) Apply(r io.Reader) error {
	dec := yaml.NewDecoder(r)
	for {
		// parse the YAML doc
		obj := &unstructured.Unstructured{Object: map[string]interface{}{}}
		err := dec.Decode(obj.Object)
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			panic(err)
		}
		if obj.Object == nil {
			log.Print("skipping empty document")
			continue
		}

		// get GroupVersionResource to invoke the dynamic client
		gvk := obj.GroupVersionKind()
		restMapping, err := k.discoveryMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
		if err != nil {
			return err
		}
		gvr := restMapping.Resource

		// apply the YAML doc
		namespace := obj.GetNamespace()
		if len(namespace) == 0 {
			namespace = "default"
		}
		applyOpts := metav1.ApplyOptions{FieldManager: "kube-apply"}
		_, err = k.client.Resource(gvr).Namespace(namespace).Apply(context.TODO(), obj.GetName(), obj, applyOpts)
		if err != nil {
			return fmt.Errorf("apply error: %w", err)
		}
		log.Printf("applied YAML for %s %q", obj.GetKind(), obj.GetName())
	}
	return nil
}

main.go

package main

import (
	"fmt"
	"log"
	"os"
	"os/user"
)

func main() {
	// get the manifests file from command line
	if len(os.Args) != 2 {
		fmt.Printf("Usage: %s <manifests.yaml>\n", os.Args[0])
		os.Exit(1)
	}
	manifestsFile := os.Args[1]

	// create the dynamic client
	kubeConfigFile := defaultKubeConfigFile()
	log.Printf("creating dynamic client with config: %s\n", kubeConfigFile)
	client := NewKubeClient(kubeConfigFile)

	// open the manifests file
	file, err := os.Open(manifestsFile)
	if err != nil {
		log.Fatalf("failed to open: %s: %s", manifestsFile, err)
	}
	defer file.Close()

	// apply the YAML to create/update a deployment
	err = client.Apply(file)
	if err != nil {
		log.Fatalf("failed to apply yaml: %s", err)
	}
}

// defaultKubeConfigFile determines the default location of kube config (~/.kube/config)
func defaultKubeConfigFile() string {
	usr, err := user.Current()
	if err != nil {
		log.Fatalf("unable to find the kube config: %s", err)
	}
	return usr.HomeDir + "/.kube/config"
}

Leave a Reply

Your email address will not be published. Required fields are marked *

Copyright copyright 2024 Simplerize