manifestival

package module
v0.0.0-...-66ab05d Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 22, 2026 License: Apache-2.0 Imports: 21 Imported by: 0

README

Manifestival

Build Status PkgGoDev

Manifestival is a library for manipulating a set of unstructured Kubernetes resources. Essentially, it enables you to toss a "bag of YAML" at a k8s cluster.

It's sort of like embedding a simplified kubectl in your Go application. You can load a manifest of resources from a variety of sources, optionally transform/filter those resources, and then apply/delete them to/from your k8s cluster.

See CHANGELOG.md

Creating Manifests

Manifests may be constructed from a variety of sources. Once created, they are immutable, but new instances may be created from them using their Append, Filter, Sort and Transform functions.

The typical way to create a manifest is by calling NewManifest

manifest, err := NewManifest("/path/to/file.yaml")

But NewManifest is just a convenience function that calls ManifestFrom with a Path, an implementation of Source.

Sources

A manifest is created by passing an implementation of the Source interface to the ManifestFrom function. Here are the built-in types that implement Source:

  • Path
  • Recursive
  • Slice
  • Reader

The Path source is the most versatile. It's a string representing the location of some YAML content in many possible forms: a file, a directory of files, a URL, or a comma-delimited list of any of those things, all of which will be combined into a single manifest.

// Single file
m, err := ManifestFrom(Path("/path/to/file.yaml"))

// All files in a directory
m, err := ManifestFrom(Path("/path/to/dir"))

// A remote URL
m, err := ManifestFrom(Path("http://site.com/manifest.yaml"))

// All of the above
m, err := ManifestFrom(Path("/path/to/file.yaml,/path/to/dir,http://site.com/manifest.yaml"))

Recursive works exactly like Path except that directories are searched recursively.

The Slice source enables the creation of a manifest from an existing slice of []unstructured.Unstructured. This is helpful for testing and, combined with the Resources accessor, facilitates more sophisticated combinations of manifests, e.g. a crude form of Append:

m3, _ := ManifestFrom(Slice(append(m1.Resources(), m2.Resources()...)))

And Reader is a function that takes an io.Reader and returns a Source from which valid YAML is expected.

Append

The Append function enables the creation of new manifests from the concatenation of others. The resulting manifest retains the options, e.g. client and logger, of the receiver. For example,

core, _ := NewManifest(path, UseLogger(logger), UseClient(client))
istio, _ := NewManifest(pathToIstio)
kafka, _ := NewManifest(pathToKafka)

manifest := core.Append(istio, kafka)
Filter

Filter returns a new Manifest containing only the resources for which all passed predicates return true. A Predicate is a function that takes an Unstructured resource and returns a bool indicating whether the resource should be included in the filtered results.

There are a few built-in predicates and some helper functions for creating your own:

  • All returns a Predicate that returns true unless any of its arguments returns false
  • Everything is equivalent to All()
  • Any returns a Predicate that returns false unless any of its arguments returns true
  • Nothing is equivalent to Any()
  • Not negates its argument, returning false if its argument returns true
  • ByName, ByKind, ByLabel, ByAnnotation, and ByGVK filter resources by their respective attributes.
  • CRDs and its complement NoCRDs are handy filters for CustomResourceDefinitions
  • In can be used to find the "intersection" of two manifests
clusterRBAC := Any(ByKind("ClusterRole"), ByKind("ClusterRoleBinding"))
namespaceRBAC := Any(ByKind("Role"), ByKind("RoleBinding"))
rbac := Any(clusterRBAC, namespaceRBAC)

theRBAC := manifest.Filter(rbac)
theRest := manifest.Filter(Not(rbac))

// Find all resources named "controller" w/label 'foo=bar' that aren't CRD's
m := manifest.Filter(ByLabel("foo", "bar"), ByName("controller"), NoCRDs)

The Predicate receives a deep copy of each resource, so no modifications made to any resource will be reflected in the returned Manifest, which is immutable. The only way to alter resources in a Manifest is with its Transform method.

Sort

Sort enables you to reorder the resources in a manifest. This is particularly useful for ensuring Kubernetes resources are applied in the correct dependency order, such as applying Namespaces before namespaced resources, or ServiceAccounts before Deployments use them.

For Kubernetes dependency-aware sorting, use the provided ByKindPriority() function:

// Sort by Kubernetes deployment dependencies
// (Namespaces first, then ServiceAccounts, Secrets, ConfigMaps, etc.)
sorted := manifest.Sort(ByKindPriority())

The ByKindPriority() function orders resources according to standard Kubernetes best practices, ensuring that foundational resources like Namespaces and CustomResourceDefinitions are deployed before resources that depend on them.

Transform

Transform will apply some function to every resource in your manifest, and return a new Manifest with the results. It's common for a Transformer function to include a guard that simply returns if the unstructured resource isn't of interest.

There are a few useful transformers provided, including InjectNamespace and InjectOwner. An example should help to clarify:

func updateDeployment(resource *unstructured.Unstructured) error {
    if resource.GetKind() != "Deployment" {
        return nil
    }
    // Either manipulate the Unstructured resource directly or...
    // convert it to a structured type...
    var deployment = &appsv1.Deployment{}
    if err := scheme.Scheme.Convert(resource, deployment, nil); err != nil {
        return err
    }

    // Now update the deployment!
    
    // If you converted it, convert it back, otherwise return nil
    return scheme.Scheme.Convert(deployment, resource, nil)
}

m, err := manifest.Transform(updateDeployment, InjectOwner(parent), InjectNamespace("foo"))

Applying Manifests

Persisting manifests is accomplished via the Apply and Delete functions of the Manifestival interface, and though DryRun doesn't change anything, it does query the API Server. Therefore all of these functions require a Client.

Client

Manifests require a Client implementation to interact with a k8s API server. There are currently two alternatives:

To apply your manifest, you'll need to provide a client when you create it with the UseClient functional option, like so:

manifest, err := NewManifest("/path/to/file.yaml", UseClient(client))
if err != nil {
    panic("Failed to load manifest")
}

It's the Client that enables you to persist the resources in your manifest using Apply, remove them using Delete, compute differences using DryRun, and occasionally it's even helpful to invoke the manifest's Client directly...

manifest.Apply(ctx)
manifest.Filter(NoCRDs).Delete(ctx)

u := manifest.Resources()[0]
u.SetName("foo")
manifest.Client.Create(ctx, &u)

Functions that access the k8s API require a Context object.

fake.Client

The fake package includes a fake Client with stubs you can easily override in your unit tests. For example,

func verifySomething(t *testing.T, expected *unstructured.Unstructured) {
    client := fake.Client{
        fake.Stubs{
            Create: func(ctx context.Context, u *unstructured.Unstructured) error {
                if !reflect.DeepEqual(u, expected) {
                    t.Error("You did it wrong!")
                }
                return nil
            },
        },
    }
    manifest, _ := NewManifest("testdata/some.yaml", UseClient(client))
    ctx := context.Background()
    callSomethingThatUltimatelyAppliesThis(ctx, manifest)
}

There is also a convenient New function that returns a fully-functioning fake Client by overriding the stubs to persist the resources in a map. Here's an example using it to test the DryRun function:

client := fake.New()
current, _ := NewManifest("testdata/current.yaml", UseClient(client))
current.Apply(ctx)
modified, _ := NewManifest("testdata/modified.yaml", UseClient(client))
diffs, err := modified.DryRun(ctx)

The default fake Client returns an error if the provided context is cancelled or its deadline is exceeded. This can be used to simulate network timeouts or graceful termination scenarios:

ctx, cancel := context.WithCancel(context.Background())
client := fake.New()
current, _ := NewManifest("testdata/current.yaml", UseClient(client))
current.Apply(ctx)
cancel()
modified, _ := NewManifest("testdata/modified.yaml", UseClient(client))
diffs, err := modified.DryRun(ctx)
if err != context.Canceled {
    t.Errorf("Expected to receive context canceled, got %v")
}
Logging

By default, Manifestival logs nothing, but it will happily log its actions if you pass it a logr.Logger via its UseLogger functional option, like so:

m, _ := NewManifest(path, UseLogger(log.WithName("manifestival")), UseClient(c))
Last Applied Configuration Annotation

Upon creation or modification, Manifestival records the contents of the object in an annotation. This allows future updates to remove obsolete fields and values automatically. By default, the name of this annotation is kubectl.kubernetes.io/last-applied-configuration which is the same name used by kubectl and applications which use kubectl to apply object values. If desired, the annotation name can be changed so that Manifestival-applied fields won't interfere with non-conflicting, kubectl-applied fields (or vice-versa). The annotation name is set like this:

m, _ := NewManifest(path, UseLastAppliedConfigAnnotation("myapp.example.com/last-applied-configuration"))
Apply

Apply will persist every resource in the manifest to the cluster. It will invoke either Create or Update depending on whether the resource already exists. And if it does exist, the same 3-way strategic merge patch used by kubectl apply will be applied. And the same annotation used by kubectl to record the resource's previous configuration will be updated, too.

The following functional options are supported, all of which map to either the k8s metav1.CreateOptions and metav1.UpdateOptions fields or kubectl apply flags:

  • Overwrite [true] resolve any conflicts in favor of the manifest
  • FieldManager the name of the actor applying changes
  • DryRunAll if present, changes won't persist
Delete

Delete attempts to delete all the manifest's resources in reverse order. Depending on the resources' owner references, race conditions with the k8s garbage collector may occur, and by default NotFound errors are silently ignored.

The following functional options are supported, all except IgnoreNotFound mirror the k8s metav1.DeleteOptions:

  • IgnoreNotFound [true] silently ignore any NotFound errors
  • GracePeriodSeconds the number of seconds before the object should be deleted
  • Preconditions must be fulfilled before a deletion is carried out
  • PropagationPolicy whether and how garbage collection will be performed
DryRun

DryRun returns a list of JSON merge patches that show the effects of applying the manifest without modifying the live system. Each item in the returned list is valid content for the kubectl patch command.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	Everything = All()
	Nothing    = Any()
)
View Source
var CRDs = ByKind("CustomResourceDefinition")

CRDs returns only CustomResourceDefinitions

View Source
var DryRunAll = dryRunAll{}

Indicates that changes should not be persisted

View Source
var NoCRDs = Not(CRDs)

NoCRDs returns no CustomResourceDefinitions

Functions

This section is empty.

Types

type ApplyOption

type ApplyOption interface {
	ApplyWith(*ApplyOptions)
}

Functional options pattern

type ApplyOptions

type ApplyOptions struct {
	ForCreate *metav1.CreateOptions
	ForUpdate *metav1.UpdateOptions
	Overwrite bool
}

func ApplyWith

func ApplyWith(options []ApplyOption) *ApplyOptions

type Client

type Client interface {
	Create(ctx context.Context, obj *unstructured.Unstructured, options ...ApplyOption) error
	Update(ctx context.Context, obj *unstructured.Unstructured, options ...ApplyOption) error
	Delete(ctx context.Context, obj *unstructured.Unstructured, options ...DeleteOption) error
	Get(ctx context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error)
}

Client includes the operations required by the Manifestival interface

type DeleteOption

type DeleteOption interface {
	DeleteWith(*DeleteOptions)
}

type DeleteOptions

type DeleteOptions struct {
	ForDelete      *metav1.DeleteOptions
	IgnoreNotFound bool // default to true in DeleteWith()
}

func DeleteWith

func DeleteWith(options []DeleteOption) *DeleteOptions

type FieldManager

type FieldManager string

FieldManager is the name of the actor applying changes

func (FieldManager) ApplyWith

func (f FieldManager) ApplyWith(opts *ApplyOptions)

type GracePeriodSeconds

type GracePeriodSeconds int64

The duration in seconds before the object should be deleted

func (GracePeriodSeconds) DeleteWith

func (g GracePeriodSeconds) DeleteWith(opts *DeleteOptions)

type IgnoreNotFound

type IgnoreNotFound bool

Whether to error when deleting a non-existent resource [true]

func (IgnoreNotFound) DeleteWith

func (i IgnoreNotFound) DeleteWith(opts *DeleteOptions)

type LessFunc

type LessFunc func(a, b unstructured.Unstructured) bool

LessFunc is a comparison function that returns true if resource a should be ordered before resource b. It follows the same convention as Go's sort package comparison functions.

func ByKindPriority

func ByKindPriority() LessFunc

type Manifest

type Manifest struct {
	Client Client
	// contains filtered or unexported fields
}

Manifest tracks a set of concrete resources which should be managed as a group using a Kubernetes client

func ManifestFrom

func ManifestFrom(src Source, opts ...Option) (m Manifest, err error)

ManifestFrom creates a Manifest from any Source implementation

func NewManifest

func NewManifest(pathname string, opts ...Option) (Manifest, error)

NewManifest creates a Manifest from a comma-separated set of YAML files, directories, or URLs. It's equivalent to `ManifestFrom(Path(pathname))`

func (Manifest) Append

func (m Manifest) Append(mfs ...Manifest) Manifest

Append creates a new Manifest by appending the resources from other Manifests onto this one. No equality checking is done, so for any resources sharing the same GVK+name, the last one will "win".

func (Manifest) Apply

func (m Manifest) Apply(ctx context.Context, opts ...ApplyOption) error

Apply updates or creates all resources in the manifest.

func (Manifest) Delete

func (m Manifest) Delete(ctx context.Context, opts ...DeleteOption) error

Delete removes all resources in the Manifest

func (Manifest) DryRun

func (m Manifest) DryRun(ctx context.Context) ([]MergePatch, error)

DryRun returns a list of merge patches, either strategic or RFC-7386 for unregistered types, that show the effects of applying the manifest.

func (Manifest) Filter

func (m Manifest) Filter(preds ...Predicate) Manifest

Filter returns a Manifest containing only those resources for which *no* Predicate returns false. Any changes callers make to the resources passed to their Predicate[s] will only be reflected in the returned Manifest.

func (Manifest) Resources

func (m Manifest) Resources() []unstructured.Unstructured

Resources returns a deep copy of the Manifest resources

func (Manifest) Sort

func (m Manifest) Sort(lessFunc LessFunc) Manifest

func (Manifest) Transform

func (m Manifest) Transform(fns ...Transformer) (Manifest, error)

Transform applies an ordered set of Transformer functions to the `Resources` in this Manifest. If an error occurs, no resources are transformed.

type Manifestival

type Manifestival interface {
	// Either updates or creates all resources in the manifest
	Apply(ctx context.Context, opts ...ApplyOption) error
	// Deletes all resources in the manifest
	Delete(ctx context.Context, opts ...DeleteOption) error
	// Transforms the resources within a Manifest
	Transform(fns ...Transformer) (Manifest, error)
	// Filters resources in a Manifest; Predicates are AND'd
	Filter(fns ...Predicate) Manifest
	// Append the resources from other Manifests to create a new one
	Append(mfs ...Manifest) Manifest
	// Sort the resources in a Manifest
	Sort(lessFunc LessFunc) Manifest
	// Show how applying the manifest would change the cluster
	DryRun(ctx context.Context) ([]MergePatch, error)
}

Manifestival defines the operations allowed on a set of Kubernetes resources (typically, a set of YAML files, aka a manifest)

type MergePatch

type MergePatch map[string]interface{}

type Option

type Option func(*Manifest)

Option follows the "functional object" idiom

func UseClient

func UseClient(client Client) Option

UseClient enables interaction with the k8s API server

func UseLastAppliedConfigAnnotation

func UseLastAppliedConfigAnnotation(annotationName string) Option

UseLastAppliedConfigAnnotation sets an alternate name for the annotation used to track the last applied configuration (defaults to kubectl.kubernetes.io/last-applied-configuration) annotationName is a value specific to your application such as myapp.example.com/last-applied-configuration

func UseLogger

func UseLogger(log logr.Logger) Option

UseLogger will cause manifestival to log its actions

type Overwrite

type Overwrite bool

Resolve conflicts by using values from the manifest values

func (Overwrite) ApplyWith

func (i Overwrite) ApplyWith(opts *ApplyOptions)

type Owner

type Owner interface {
	v1.Object
	schema.ObjectKind
}

Owner is a partial Kubernetes metadata schema.

type Path

type Path string

Path is a Source represented as a comma-delimited list of files, directories, and URL's

func (Path) Parse

func (p Path) Parse() ([]unstructured.Unstructured, error)

type Preconditions

type Preconditions metav1.Preconditions

Must be fulfilled before a deletion is carried out

func (Preconditions) DeleteWith

func (p Preconditions) DeleteWith(opts *DeleteOptions)

type Predicate

type Predicate func(u *unstructured.Unstructured) bool

Predicate returns true if u should be included in result

func All

func All(preds ...Predicate) Predicate

All returns a predicate that returns true unless any of its passed predicates return false.

func Any

func Any(preds ...Predicate) Predicate

Any returns a predicate that returns false unless any of its passed predicates return true.

func ByAnnotation

func ByAnnotation(annotation, value string) Predicate

ByAnnotation returns resources that contain a particular annotation and value. A value of "" denotes *ANY* value

func ByGVK

ByGVK returns resources of a particular GroupVersionKind

func ByKind

func ByKind(kind string) Predicate

ByKind returns resources matching a particular kind

func ByLabel

func ByLabel(label, value string) Predicate

ByLabel returns resources that contain a particular label and value. A value of "" denotes *ANY* value

func ByLabels

func ByLabels(labels map[string]string) Predicate

ByLabels returns true when the resource contains any of the labels.

func ByName

func ByName(name string) Predicate

ByName returns resources with a specifc name

func In

func In(manifest Manifest) Predicate

In(m) returns a Predicate that tests for membership in m, using "gk|ns/name" as a unique identifier

func Not

func Not(pred Predicate) Predicate

Not returns the complement of a given predicate.

type PropagationPolicy

type PropagationPolicy metav1.DeletionPropagation

Whether and how garbage collection will be performed.

func (PropagationPolicy) DeleteWith

func (p PropagationPolicy) DeleteWith(opts *DeleteOptions)

type Recursive

type Recursive string

Recursive is identical to Path, but dirs are searched recursively

func (Recursive) Parse

func (r Recursive) Parse() ([]unstructured.Unstructured, error)

type Slice

type Slice []unstructured.Unstructured

Slice is a Source comprised of existing objects

func (Slice) Parse

func (s Slice) Parse() ([]unstructured.Unstructured, error)

type Source

type Source interface {
	Parse() ([]unstructured.Unstructured, error)
}

Source is the interface through which all Manifests are created.

func Reader

func Reader(r io.Reader) Source

Reader takes an io.Reader from which YAML content is expected

type Transformer

type Transformer func(u *unstructured.Unstructured) error

Transformer transforms a resource from the manifest in place.

func InjectNamespace

func InjectNamespace(ns string) Transformer

InjectNamespace creates a Transformer which adds a namespace to existing resources if appropriate. We assume all resources in the manifest live in the same namespace.

func InjectOwner

func InjectOwner(owner Owner) Transformer

InjectOwner creates a Transformer which adds an OwnerReference pointing to `owner`

Directories

Path Synopsis
internal

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL