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:
- Create Dynamic Kubernetes client
- Create Discovery REST mapper
- Decode YAML documents
- Discover GroupVersionResource
- 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" }