Reading and writing k8s resource as YAML in golang

I am writing this article because none other is available on this topic. The information is very scattered and often requires digging into the code of k8s libraries. I am going to use a Q/A style for this article.

How to read a k8s resource from a file? The intuitive and easy way is to try to use interface{} and unmarshal YAML into it.

import (
 corev1 "k8s.io/api/core/v1"
 "io/ioutil"
)
var obj interface{}
stream, _ :=ioutils.ReadFile("pod.yaml")
yaml.Unmarshal(stream, &obj)
obj.(*corev1.Pod)

The last step will not work. obj is of type interface{}, and conversion to a complex type such as pod is not possible. Other than that, we haven’t defined a function to convert interface{} to pod or another k8s resource.

Since the above approach doesn’t work, let’s look at how k8s deals with this. Refer https://github.com/kubernetes/client-go/issues/193

import (
 "k8s.io/client-go/kubernetes/scheme"
 corev1 "k8s.io/api/core/v1"
)
decode := scheme.Codecs.UniversalDeserializer().Decode
stream, _ :=ioutils.ReadFile("pod.yaml")
obj, gKV, _ := decode(stream, nil, nil)
if gKV.Kind == "Pod" {
 pod := obj.(*corev1.Pod)
}

Boom! This works, as it uses the codec scheme defined for k8s resources.

But this only works for native k8s resources. What if you are working with CRDs? Their codec is not directly available, so we have to manually add that to the scheme.

import (
     "k8s.io/client-go/kubernetes/scheme"
      corev1 "k8s.io/api/core/v1"
      apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
     "k8s.io/apimachinery/pkg/runtime/serializer"
)
sch := runtime.NewScheme()
_ = scheme.AddToScheme(sch)
_ = apiextv1beta1.AddToScheme(sch)
decode := serializer.NewCodecFactory(sch).UniversalDeserializer().Decode
stream, _ :=ioutils.ReadFile("crd.yaml")
obj, gKV, _ := decode(stream, nil, nil)
if gKV.Kind == "CustomResourceDefinition" {
           pod := obj.(*apiextv1beta1.CustomResourceDefinition)
}

Now that we are able to unmarshal k8s objects into proper types, how do we write them back as YAML? An easy but wrong way is to directly marshal as YAML and write to a file.

import (
        "io/ioutil"
        "gopkg.in/yaml.v2"
)
w := ioutils.WriteFile("pod.yaml)
podByte, e := yaml.Marshal(&pod)
w.Write(podByte)

This will save the k8s resource as YAML. But the output will have runtime fields that the k8s cluster uses to manage resources. Applying it to the cluster results in an error. It will look like below having typemeta, objectmeta and selflink, which are runtime properties.

typemeta: # defines runtime property
  kind: Pod
  apiversion: v1
objectmeta: # defines runtime property
  selflink: ""

**Solution: **nothing direct. So, let’s look at how kubectl prints the resource. Those aren’t declaration of runtime objects.

So, after digging in code, I found that k8s.io/kubectl uses k8s.io/cli-runtime which has the printers. Printers output resources in pre-apply format.

import (
	"k8s.io/cli-runtime/pkg/printers"
        "io"
)
newFile, err := os.Create("pod.yaml")
y := printers.YAMLPrinter{}
defer newFile.Close()
y.PrintObj(pod, newFile) // PrintObj (runtime.Object, *io.Writer)