In this blog post, we will see how we can encode/decode Kubernetes Go API types to/from Kubernetes YAML.

When we work with Kubernetes Go API types (custom types or core types), we often need to convert the Go API types into YAML manifest or vice versa. For example, A CLI application

  • Which output Kubernetes objects in YAML

  • Which takes Kubernetes objects YAML as input

For demonstration purposes, we will be creating a simple application that creates a Kubernetes ConfigMap object and encodes/decodes it.

Using gopkg.in/yaml Link to heading

gopkg.in/yaml is Go package for working with YAML values in Go.

package main

import (
	"fmt"

	"gopkg.in/yaml.v3"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func main() {
	// Marshal a ConfigMap object to YAML.
	cm1 := corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-configmap",
			Namespace: "test-namespace",
		},
		Data: map[string]string{
			"key": "value",
		},
	}

	y, err := yaml.Marshal(cm1)
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	fmt.Println("Encoded YAML:")
	fmt.Println(string(y))
	y1 := `
kind: ConfigMap
apiVersion: v1
metadata:
  name: test-configmap
  namespace: test-namespace
data:
  key: value
`

	// Unmarshal the YAML back into another ConfigMap object.
	var cm2 corev1.ConfigMap
	err = yaml.Unmarshal([]byte(y1), &cm2)
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	fmt.Println("Decoded Object from YAML:")
	fmt.Println(cm2)
}

If you run this program, it will give below output

$ go run main.go
Encoded YAML:
typemeta:
    kind: ""
    apiversion: ""
objectmeta:
    name: test-configmap
    generatename: ""
    namespace: test-namespace
    selflink: ""
    uid: ""
    resourceversion: ""
    generation: 0
    creationtimestamp: "0001-01-01T00:00:00Z"
    deletiontimestamp: null
    deletiongraceperiodseconds: null
    labels: {}
    annotations: {}
    ownerreferences: []
    finalizers: []
    managedfields: []
immutable: null
data:
    key: value
binarydata: {}
Decoded Object from YAML:
{{ } {      0 0001-01-01 00:00:00 +0000 UTC <nil> <nil> map[] map[] [] [] []} <nil> map[key:value] map[]}

First section of the output is Kubernetes ConfigMap object encoded in YAML. It is clearly not in the Kubernetes YAML manifest format . It has also added all empty fields which are not usually required in a Kubernetes YAML manifest and generally used by controllers. typemeta field should not be there as it is an inline struct field. Also, the value for apiVersion and kind are empty.

Second section is decoded YAML object in Kubernetes ConfigMap type. The decoded object is missing most of the fields from metadata and type section.

Using sigs.k8s.io/yaml Link to heading

It is a permanent fork of ghodss/yaml which handles YAML marshalling to and from struct in a better way. It is a wrapper around gopkg.in/yaml that reuses the JSON tags for YAML as it first coverts YAML to JSON using go-yaml.

package main

import (
	"fmt"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/yaml"
)

func main() {
	// Marshal a ConfigMap object to YAML.
	cm1 := corev1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-configmap",
			Namespace: "test-namespace",
		},
		Data: map[string]string{
			"key": "value",
		},
	}

	y, err := yaml.Marshal(cm1)
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	fmt.Println("Encoded YAML:")
	fmt.Println(string(y))
	y1 := `
kind: ConfigMap
apiVersion: v1
metadata:
  name: test-configmap
  namespace: test-namespace
data:
  key: value
`

	// Unmarshal the YAML back into another ConfigMap object.
	var cm2 corev1.ConfigMap
	err = yaml.Unmarshal([]byte(y1), &cm2)
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	fmt.Println("Decoded Object from YAML:")
	fmt.Println(cm2)
}

Output: Link to heading

$ go run main.go
Encoded YAML:
data:
  key: value
metadata:
  creationTimestamp: null
  name: test-configmap
  namespace: test-namespace
Decoded Object from YAML:
{{ConfigMap v1} {test-configmap  test-namespace    0 0001-01-01 00:00:00 +0000 UTC <nil> <nil> map[] map[] [] [] []} <nil> map[key:value] map[]}

This yaml package works well and can handle encoding and decoding except one case. It can not automatically detect the apiVersion and kind while encoding the go types into YAML.

We can fix that by specifying the TypeMeta field for Kubernetes API types when creating the object. package main

import (
	"fmt"

	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"sigs.k8s.io/yaml"
)

func main() {
	// Marshal a ConfigMap object to YAML.
	cm1 := corev1.ConfigMap{
		TypeMeta: metav1.TypeMeta{
			Kind:       "ConfigMap",
			APIVersion: "v1",
		},
		ObjectMeta: metav1.ObjectMeta{
			Name:      "test-configmap",
			Namespace: "test-namespace",
		},
		Data: map[string]string{
			"key": "value",
		},
	}

	y, err := yaml.Marshal(cm1)
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	fmt.Println("Encoded YAML:")
	fmt.Println(string(y))
	y1 := `
kind: ConfigMap
apiVersion: v1
metadata:
  name: test-configmap
  namespace: test-namespace
data:
  key: value
`

	// Unmarshal the YAML back into another ConfigMap object.
	var cm2 corev1.ConfigMap
	err = yaml.Unmarshal([]byte(y1), &cm2)
	if err != nil {
		fmt.Printf("err: %v\n", err)
		return
	}
	fmt.Println("Decoded Object from YAML:")
	fmt.Println(cm2)
}

Output: Link to heading

$ go run main.go
Encoded YAML:
apiVersion: v1
data:
  key: value
kind: ConfigMap
metadata:
  creationTimestamp: null
  name: test-configmap
  namespace: test-namespace
Decoded Object from YAML:
{{ConfigMap v1} {test-configmap  test-namespace    0 0001-01-01 00:00:00 +0000 UTC <nil> <nil> map[] map[] [] [] []} <nil> map[key:value] map[]}

Thanks for reading !! Follow me on LinkedIn for such content.