⚠️ Work in Progress
You are viewing development documentation built from the latest commit on main. APIs and features are subject to change.

IO

IO - YAML Serialization and Resource Printing

Go Reference Go Reference

The io package provides utilities for parsing, serializing, and printing Kubernetes resources. It supports multiple output formats including YAML, JSON, and kubectl-compatible table views.

Overview

This package handles the I/O boundary of Kure: reading Kubernetes manifests from files, serializing resources to YAML/JSON, and printing resources in human-readable formats. It integrates with Kure’s registered scheme for type-aware parsing.

Each Go block on this page is the body of an Example function in example_test.go, which go test runs: it imports this package as io, github.com/go-kure/kure/pkg/kubernetes for the objects it encodes, appsv1 "k8s.io/api/apps/v1", corev1 "k8s.io/api/core/v1", sigs.k8s.io/controller-runtime/pkg/client, bytes, os, path/filepath, strings, and fmt for the lines that print what the example read or wrote. testdata/manifests.yaml is a Deployment and a Service.

Parsing

Parse YAML Files

// Parse a multi-document YAML file into typed Kubernetes objects
objects, err := io.ParseFile("testdata/manifests.yaml")
if err != nil {
    panic(err)
}

// Parse YAML bytes directly
yamlData := []byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n  name: app-config\n")
more, err := io.ParseYAML(yamlData)
if err != nil {
    panic(err)
}

for _, obj := range append(objects, more...) {
    fmt.Printf("%T %s\n", obj, obj.GetName())
}

Parse Errors

A document that fails to decode into a Kubernetes object is an error in the ParseErrors value the parse returns, a type of the errors package. Such a failure does not end the parse: the documents before and after it are decoded all the same, and their objects are returned beside the error.

A place where the stream itself cannot be read is an error as well. Where the decoder can go on from it, the parse does: a YAML document that does not parse costs only itself, and the documents after it are read. Where the decoder cannot, the parse ends there. It returns one error for the place beside the objects decoded before it, and what follows in the stream is not read. A stream whose first character is { and whose first two documents are JSON objects, one after the other with no --- line between them, is read as JSON only from there on, and the first thing in it that is not JSON is such a place: malformed JSON, or a --- line. JSON documents with a --- line between them are read one by one, as the documents of a YAML stream are: one that does not parse costs only itself.

A registered kind is decoded into its upstream Go type, so that type’s own UnmarshalJSON runs inside the parse. One that panics does not take the caller down. The document, or the list item, becomes a parse error that names the object and carries the panic’s value, and the documents and items beside it are still returned:

parse error in Kubernetes object: failed to decode object: the decoder panicked on CiliumNetworkPolicy "default/p": runtime error: invalid memory address or nil pointer dereference

A panic whose value is an error keeps it in the chain, for errors.As. The panic’s stack trace is not kept. A known case is a CiliumNetworkPolicy whose ICMP field has no type, or type: null: the upstream type dereferences a nil pointer.

Unstructured Fallback

By default, only GVKs registered in the kure scheme are accepted. To parse arbitrary Kubernetes YAML (CRDs, custom operators, etc.) use ParseYAMLWithOptions or ParseFileWithOptions with AllowUnstructured.

Which kinds are typed follows the scheme, so it widens when kure registers more. PriorityClass, EndpointSlice, Lease, RuntimeClass, the two webhook configurations and the four admission policy kinds are registered and come back as their k8s.io/api types in both modes; a caller that matched them as *unstructured.Unstructured must match the typed object instead. The same holds for APIService, typed as its k8s.io/kube-aggregator type, and for the Flux ImageRepository and ImagePolicy. The MetalLB BGPPeer at metallb.io/v1beta2 is registered beside metallb.io/v1beta1 and comes back as its typed object too; a caller that matched it as *unstructured.Unstructured must match the typed object instead. VerticalPodAutoscaler is not registered and still needs AllowUnstructured. The full list is the generated table .

A list document is flattened into its items; the list itself is never returned. The items come back in the list’s order and take the list’s place in a multi-document stream, and an empty list yields nothing.

  • A <Kind>List of a registered kind (DeploymentList, PriorityClassList) yields typed items in both modes. An item may leave apiVersion and kind out, as the API server’s own list responses do; an item that states them must state the kind the list holds. A registered type is such a list when apimachinery reads its Items as a slice. One whose Items it cannot read (a nil field of interface type, or one behind a nil embedded pointer) is not flattened: it is decoded as one object of that type and refused with an error, never a panic, since it either fails to decode or is no client.Object.
  • A generic v1 List is read item by item, each as a document of its own: a registered kind comes back typed, an unregistered one follows AllowUnstructured, and a list inside the list is flattened where it stands.
  • A <Kind>List of a kind that is not registered is refused by a strict parse. With AllowUnstructured it is a list when it states items, and is read item by item like the v1 List: an item of a registered kind comes back typed, one of an unregistered kind unstructured, and a list among the items is flattened where it stands. An item that leaves apiVersion or kind out is given the list’s: its apiVersion, and its kind without the List.

A kind that is not registered and does not end in List is one object, whatever fields it has: with AllowUnstructured, a field named items stays part of its content. A kind that ends in List and states no items is one object too, unless it states them under a key in another case (Items): that document is refused, see below.

Each item is decoded by itself, in every list shape. An item that cannot be decoded, a null among them, is reported with its position (item 1 of List, item 1 of DeploymentList), and the items beside it are still returned, as the documents of a stream are.

A list is refused as a whole, with no item returned, in three cases:

  • It is nested too deep. A list may sit inside eight lists whose items are documents of their own, the v1 List and the list of an unregistered kind; one nested deeper is refused, whatever its kind.
  • Its own metadata carries labels or annotations. The items are all a parse returns of a list, and they cannot keep what the list says about itself; a helm.sh/hook annotation on a list is the known case. The error names what the list carries. Metadata that cannot be read is refused as well, and a list that states metadata, labels or annotations more than once carries what any of them states. What describes the response a list came in (resourceVersion, continue, selfLink, remainingItemCount) is ignored, and so is a name on a list.
  • It states its items under a key in another case (Items); see below.
parse error in Kubernetes object: List has metadata of its own that its items cannot keep: annotations helm.sh/hook

A document has one reading. The Kubernetes decoder finds apiVersion and kind under those keys in any case, while a list is recognised, and its items are read, under the exact keys. A top-level key that equals apiVersion or kind only after case folding (Kind, APIVersion) is therefore refused, on every document and on every item of a list. One that equals items is refused on every list of a registered kind and, with AllowUnstructured, on every unregistered kind that ends in List, whether or not it states items as well. The error names the key:

parse error in Kubernetes object: the key "Kind" equals "kind" only after case folding; write it "kind" or remove it

On any other document, Items is a field like any other.

The fallback in use:

yamlData := []byte(`apiVersion: v1
kind: Pod
metadata:
  name: web
---
apiVersion: example.com/v1
kind: Widget
metadata:
  name: my-widget
`)

opts := io.ParseOptions{AllowUnstructured: true}
objects, err := io.ParseYAMLWithOptions(yamlData, opts)
if err != nil {
    panic(err)
}
// Known types are returned as typed objects (e.g. *corev1.Pod).
// Unknown types are returned as *unstructured.Unstructured.
for _, obj := range objects {
    fmt.Printf("%T\n", obj)
}

Load and Save

SaveFile writes one object as YAML; LoadFile reads YAML into an object you pass:

dir, err := os.MkdirTemp("", "kure-io-example")
if err != nil {
    panic(err)
}
defer func() { _ = os.RemoveAll(dir) }()
path := filepath.Join(dir, "service.yaml")

// Save an object to file
service := kubernetes.CreateService("web", "default")
if err := io.SaveFile(path, service); err != nil {
    panic(err)
}

// Load a single object from file
var loaded corev1.Service
if err := io.LoadFile(path, &loaded); err != nil {
    panic(err)
}
fmt.Println(loaded.Kind, loaded.Namespace+"/"+loaded.Name)

Serialization

Marshal and Unmarshal

Marshal writes YAML to an io.Writer, and Unmarshal reads it from an io.Reader:

deployment := kubernetes.CreateDeployment("web", "default")

// Serialize to YAML
var buf bytes.Buffer
if err := io.Marshal(&buf, deployment); err != nil {
    panic(err)
}

// Deserialize from YAML
var obj appsv1.Deployment
if err := io.Unmarshal(&buf, &obj); err != nil {
    panic(err)
}
fmt.Println(obj.Kind, obj.Namespace+"/"+obj.Name)

Encode Multiple Objects

The encoders take []*client.Object, where the parsers return []client.Object. The YAML encoder separates documents with ---. The JSON encoder writes one object per line and follows each, the last included, with a --- line, so its output is not a JSON array.

var configMap client.Object = kubernetes.CreateConfigMap("app-config", "default")
var service client.Object = kubernetes.CreateService("web", "default")
objects := []*client.Object{&configMap, &service}

// Encode as multi-document YAML
yamlData, err := io.EncodeObjectsToYAML(objects)
if err != nil {
    panic(err)
}

// Encode as JSON: one object per line, each followed by a --- separator
jsonData, err := io.EncodeObjectsToJSON(objects)
if err != nil {
    panic(err)
}

fmt.Print(string(yamlData))
fmt.Print(string(jsonData))

Numbers

The YAML encoders write an integer exactly when the object holds it as an integer: an integer field of a typed object, an int64 in an unstructured one. 9007199254740993 is written as 9007199254740993, in both field orders, and so is every integer up to the largest uint64.

A float64 is not an integer to the encoders, also when its value is one. A float64 that holds an integer between -2^63 and 2^64, both excluded, is written with the digits of its JSON form, the shortest that read back as the same float64, in both field orders. Above 2^53 those are not always its exact expansion: the float64 2^62 is written 4611686018427388000, not 4611686018427387904. Hold a large integer as an integer to have it written exactly.

Any other finite number, the float64 -2^63 among them, passes through a float64 and is written with that precision.

Deterministic Field Ordering

configMap := kubernetes.CreateConfigMap("app-config", "default")
configMap.Data = map[string]string{"LOG_LEVEL": "info"}
var obj client.Object = configMap
objects := []*client.Object{&obj}

// Encode with Kubernetes-conventional field ordering:
// apiVersion, kind, metadata, spec, ... status (last)
opts := io.EncodeOptions{KubernetesFieldOrder: true}
yamlData, err := io.EncodeObjectsToYAMLWithOptions(objects, opts)
if err != nil {
    panic(err)
}
fmt.Print(string(yamlData))

Server-Set Field Stripping

By default, YAML encoding (EncodeObjectsToYAML, EncodeObjectsToYAMLWithOptions) strips server-managed metadata fields that should not appear in client-generated manifests: managedFields, resourceVersion, uid, generation, selfLink, the kubectl.kubernetes.io/last-applied-configuration annotation, null creationTimestamp, and empty status.

The example encodes one Service at each stripping level and reports whether its resourceVersion and its empty status survived:

// A Service as read back from a cluster: a resourceVersion, and an empty status
service := kubernetes.CreateService("web", "default")
service.ResourceVersion = "12345"
var obj client.Object = service
objects := []*client.Object{&obj}

for _, opts := range []io.EncodeOptions{
    // Default behavior — full stripping (the zero value, which EncodeObjectsToYAML uses)
    {},
    // Explicit full stripping with field ordering
    {KubernetesFieldOrder: true, ServerFieldStripping: io.StripServerFieldsFull},
    // Basic stripping (only null creationTimestamp and empty status)
    {ServerFieldStripping: io.StripServerFieldsBasic},
    // No stripping — preserve all fields as-is
    {ServerFieldStripping: io.StripServerFieldsNone},
} {
    yamlData, err := io.EncodeObjectsToYAMLWithOptions(objects, opts)
    if err != nil {
        panic(err)
    }
    out := string(yamlData)
    fmt.Println(strings.Contains(out, "resourceVersion:"), strings.Contains(out, "status:"))
}

Printing

Output Formats

The package supports these output formats, named as kubectl names them:

FormatConstantDescription
YAMLOutputFormatYAMLFull YAML output
JSONOutputFormatJSONFull JSON output
TableOutputFormatTableColumnar table view
WideOutputFormatWideExtended table with extra columns
NameOutputFormatNamekind[.group]/name, with (namespace: <ns>) appended for a namespaced object

Usage

The example prints one ConfigMap three ways: as YAML, as a table through a ResourcePrinter (ShowLabels adds the LABELS column), and in the format a user-supplied string names. ValidateOutputFormat turns that string into an OutputFormat, and fails on one it does not know. PrintObjectsAsTable prints a table directly, without a ResourcePrinter.

configMap := kubernetes.CreateConfigMap("app-config", "default")
configMap.Labels = map[string]string{"app": "web"}
var obj client.Object = configMap
objects := []*client.Object{&obj}

// Print as YAML to stdout
if err := io.PrintObjectsAsYAML(objects, os.Stdout); err != nil {
    panic(err)
}

// Use ResourcePrinter for configurable output
printer := io.NewResourcePrinter(io.PrintOptions{
    OutputFormat: io.OutputFormatTable,
    ShowLabels:   true,
})
if err := printer.Print(objects, os.Stdout); err != nil {
    panic(err)
}

// Validate a format string before use
format, err := io.ValidateOutputFormat("name")
if err != nil {
    panic(err)
}

// Format-agnostic printing (selects the printer by format)
if err := io.PrintObjects(objects, format, io.PrintOptions{}, os.Stdout); err != nil {
    panic(err)
}
  • errors - Error types for parse failures
  • kubernetes - Scheme registration for type-aware parsing