Recursive AI

Read Kubernetes manifests and multi-document YAML

All oneyaml Go recipes

A Kubernetes manifest often holds several documents separated by ---. Read each one into a struct with a Decoder, or edit one of them in a Document and write the whole file back with its comments.

The file:

YAML
# Web frontend
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  template:
    spec:
      containers:
        - name: web
          image: registry.example.com/web:1.4.2 # pinned by release tooling
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  ports:
    - port: 80
      targetPort: 8080

The program:

GO
package main
 
import (
	"bytes"
	"errors"
	"fmt"
	"io"
	"log"
	"os"
 
	oneyaml "oneyaml.dev/go"
)
 
type Object struct {
	APIVersion string `yaml:"apiVersion"`
	Kind       string `yaml:"kind"`
	Metadata   struct {
		Name string `yaml:"name"`
	} `yaml:"metadata"`
}
 
func main() {
	src, err := os.ReadFile("app.yaml")
	if err != nil {
		log.Fatal(err)
	}
 
	// Typed: a Decoder reads one document per call, until io.EOF.
	dec := oneyaml.NewDecoder(bytes.NewReader(src))
	for {
		var obj Object
		err := dec.Decode(&obj)
		if errors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			log.Fatal(err)
		}
		fmt.Printf("%s %s (%s)\n", obj.Kind, obj.Metadata.Name, obj.APIVersion)
	}
 
	// Lossless: edit the Deployment, keep everything else byte for byte.
	stream, err := oneyaml.Parse(src)
	if err != nil {
		log.Fatal(err)
	}
	for i := 0; i < stream.Len(); i++ {
		doc := stream.Doc(i)
		var kind string
		if doc.Get("/kind", &kind) == nil && kind == "Deployment" {
			if err := doc.Set("/spec/template/spec/containers/0/image", "registry.example.com/web:1.5.0"); err != nil {
				log.Fatal(err)
			}
		}
	}
	fmt.Print(stream)
}
Text
Deployment web (apps/v1)
Service web (v1)
# Web frontend
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
spec:
  template:
    spec:
      containers:
        - name: web
          image: registry.example.com/web:1.5.0 # pinned by release tooling
---
apiVersion: v1
kind: Service
metadata:
  name: web
spec:
  ports:
    - port: 80
      targetPort: 8080

Good to know

  • Decoder.Decode reads one document per call and returns io.EOF after the last. Unmarshal reads only the first document.
  • A parsed Document holds the whole stream. Len() counts its documents, and Doc(i) is a view of one that reads and edits only that document.
  • For manifests from outside, use Untrusted(): it also bounds input size, documents per stream and scalar length (read untrusted YAML safely).
  • Kubernetes code built on sigs.k8s.io/yaml can switch to oneyaml.dev/go/compat/k8syaml with one import change (migration guide).