manifestival

package module
v0.7.2 Latest Latest
Warning

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

Go to latest
Published: Nov 8, 2022 License: Apache-2.0 Imports: 19 Imported by: 115

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 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.

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()
manifest.Filter(NoCRDs).Delete()

u := manifest.Resources()[0]
u.SetName("foo")
manifest.Client.Create(&u)
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(u *unstructured.Unstructured) error {
                if !reflect.DeepEqual(u, expected) {
                    t.Error("You did it wrong!")
                }
                return nil
            },
        },
    }
    manifest, _ := NewManifest("testdata/some.yaml", UseClient(client))
    callSomethingThatUltimatelyAppliesThis(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()
modified, _ := NewManifest("testdata/modified.yaml", UseClient(client))
diffs, err := modified.DryRun()
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))
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 added in v0.2.0

type ApplyOption interface {
	ApplyWith(*ApplyOptions)
}

Functional options pattern

type ApplyOptions added in v0.2.0

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

func ApplyWith added in v0.2.0

func ApplyWith(options []ApplyOption) *ApplyOptions

type Client added in v0.1.0

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

Client includes the operations required by the Manifestival interface

type DeleteOption added in v0.2.0

type DeleteOption interface {
	DeleteWith(*DeleteOptions)
}

type DeleteOptions added in v0.2.0

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

func DeleteWith added in v0.2.0

func DeleteWith(options []DeleteOption) *DeleteOptions

type FieldManager added in v0.1.0

type FieldManager string

FieldManager is the name of the actor applying changes

func (FieldManager) ApplyWith added in v0.2.0

func (f FieldManager) ApplyWith(opts *ApplyOptions)

type GracePeriodSeconds added in v0.1.0

type GracePeriodSeconds int64

The duration in seconds before the object should be deleted

func (GracePeriodSeconds) DeleteWith added in v0.2.0

func (g GracePeriodSeconds) DeleteWith(opts *DeleteOptions)

type IgnoreNotFound added in v0.2.0

type IgnoreNotFound bool

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

func (IgnoreNotFound) DeleteWith added in v0.2.0

func (i IgnoreNotFound) DeleteWith(opts *DeleteOptions)

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 added in v0.2.0

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 added in v0.6.0

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(opts ...ApplyOption) error

Apply updates or creates all resources in the manifest.

func (Manifest) Delete

func (m Manifest) Delete(opts ...DeleteOption) error

Delete removes all resources in the Manifest

func (Manifest) DryRun added in v0.4.0

func (m Manifest) DryRun() ([]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 added in v0.2.0

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) 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(opts ...ApplyOption) error
	// Deletes all resources in the manifest
	Delete(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
	// Show how applying the manifest would change the cluster
	DryRun() ([]MergePatch, error)
}

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

type MergePatch added in v0.4.0

type MergePatch map[string]interface{}

type Option added in v0.1.0

type Option func(*Manifest)

Option follows the "functional object" idiom

func UseClient added in v0.1.0

func UseClient(client Client) Option

UseClient enables interaction with the k8s API server

func UseLogger added in v0.1.0

func UseLogger(log logr.Logger) Option

UseLogger will cause manifestival to log its actions

type Overwrite added in v0.5.0

type Overwrite bool

Resolve conflicts by using values from the manifest values

func (Overwrite) ApplyWith added in v0.5.0

func (i Overwrite) ApplyWith(opts *ApplyOptions)

type Owner

type Owner interface {
	v1.Object
	schema.ObjectKind
}

Owner is a partial Kubernetes metadata schema.

type Path added in v0.2.0

type Path string

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

func (Path) Parse added in v0.2.0

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

type Preconditions added in v0.1.0

type Preconditions metav1.Preconditions

Must be fulfilled before a deletion is carried out

func (Preconditions) DeleteWith added in v0.2.0

func (p Preconditions) DeleteWith(opts *DeleteOptions)

type Predicate added in v0.2.0

type Predicate func(u *unstructured.Unstructured) bool

Predicate returns true if u should be included in result

func All added in v0.3.0

func All(preds ...Predicate) Predicate

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

func Any added in v0.3.0

func Any(preds ...Predicate) Predicate

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

func ByAnnotation added in v0.6.0

func ByAnnotation(annotation, value string) Predicate

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

func ByGVK added in v0.2.0

ByGVK returns resources of a particular GroupVersionKind

func ByKind added in v0.2.0

func ByKind(kind string) Predicate

ByKind returns resources matching a particular kind

func ByLabel added in v0.2.0

func ByLabel(label, value string) Predicate

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

func ByLabels added in v0.4.0

func ByLabels(labels map[string]string) Predicate

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

func ByName added in v0.2.0

func ByName(name string) Predicate

ByName returns resources with a specifc name

func In added in v0.6.0

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 added in v0.6.0

func Not(pred Predicate) Predicate

Not returns the complement of a given predicate.

type PropagationPolicy added in v0.1.0

type PropagationPolicy metav1.DeletionPropagation

Whether and how garbage collection will be performed.

func (PropagationPolicy) DeleteWith added in v0.2.0

func (p PropagationPolicy) DeleteWith(opts *DeleteOptions)

type Recursive added in v0.1.0

type Recursive string

Recursive is identical to Path, but dirs are searched recursively

func (Recursive) Parse added in v0.2.0

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

type Slice added in v0.2.0

type Slice []unstructured.Unstructured

Slice is a Source comprised of existing objects

func (Slice) Parse added in v0.2.0

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

type Source added in v0.2.0

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

Source is the interface through which all Manifests are created.

func Reader added in v0.2.0

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