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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// create the dynamic client with standard rest.Config
client, err := dynamic.NewForConfig(config)
// create the dynamic client with standard rest.Config client, err := dynamic.NewForConfig(config)
// 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
Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// 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)
// 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)
// 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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
# 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)
...
}
# 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) ... }
# 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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
// get GroupVersionResource to invoke the dynamic client
gvk := obj.GroupVersionKind()
restMapping, err := k.discoveryMapper.RESTMapping(gvk.GroupKind(), gvk.Version)
gvr := restMapping.Resource
// get GroupVersionResource to invoke the dynamic client gvk := obj.GroupVersionKind() restMapping, err := k.discoveryMapper.RESTMapping(gvk.GroupKind(), gvk.Version) gvr := restMapping.Resource
// 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.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
applyOpts := metav1.ApplyOptions{FieldManager: "k8s-dynamic-client"}
_, err = k.client.Resource(gvr).Namespace(namespace).Apply(context.TODO(), obj.GetName(), obj, applyOpts)
applyOpts := metav1.ApplyOptions{FieldManager: "k8s-dynamic-client"} _, err = k.client.Resource(gvr).Namespace(namespace).Apply(context.TODO(), obj.GetName(), obj, applyOpts)
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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
/* 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
}
/* 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 }
/* 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

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
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"
}
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" }
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