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.