IO
IO - YAML Serialization and Resource Printing
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 dereferenceA 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>Listof a registered kind (DeploymentList,PriorityClassList) yields typed items in both modes. An item may leaveapiVersionandkindout, 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 itsItemsas a slice. One whoseItemsit 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 noclient.Object. - A generic
v1Listis read item by item, each as a document of its own: a registered kind comes back typed, an unregistered one followsAllowUnstructured, and a list inside the list is flattened where it stands. - A
<Kind>Listof a kind that is not registered is refused by a strict parse. WithAllowUnstructuredit is a list when it statesitems, and is read item by item like thev1List: 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 leavesapiVersionorkindout is given the list’s: itsapiVersion, and its kind without theList.
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
v1Listand the list of an unregistered kind; one nested deeper is refused, whatever its kind. - Its own
metadatacarries labels or annotations. The items are all a parse returns of a list, and they cannot keep what the list says about itself; ahelm.sh/hookannotation 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 statesmetadata,labelsorannotationsmore 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/hookA 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 itOn 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:
| Format | Constant | Description |
|---|---|---|
| YAML | OutputFormatYAML | Full YAML output |
| JSON | OutputFormatJSON | Full JSON output |
| Table | OutputFormatTable | Columnar table view |
| Wide | OutputFormatWide | Extended table with extra columns |
| Name | OutputFormatName | kind[.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)
}Related Packages
- errors - Error types for parse failures
- kubernetes - Scheme registration for type-aware parsing