Go Function Libraries

Writing exec and container functions in Golang.

Function developers can write exec and container functions in Golang using the following libraries:

Library Purpose
sigs.k8s.io/kustomize/kyaml/fn/framework Setup function command
sigs.k8s.io/kustomize/kyaml/yaml Modify resources

Develop using the two examples of writing functions in Go or consult the kyaml libraries’ reference below.

Hello World Go Function

Create the go module

go mod init github.com/user/repo
go get sigs.k8s.io/kustomize/kyaml

Create the main.go

// main.go
package main

import (
 "os"

 "sigs.k8s.io/kustomize/kyaml/fn/framework"
 "sigs.k8s.io/kustomize/kyaml/yaml"
)

var value string

func main() {
    resourceList := &framework.ResourceList{}
 cmd := framework.Command(resourceList, func() error {
        // cmd.Execute() will parse the ResourceList.functionConfig into
        // cmd.Flags from the ResourceList.functionConfig.data field.
  for i := range resourceList.Items {
            // modify the resources using the kyaml/yaml library:
            // https://pkg.go.dev/sigs.k8s.io/kustomize/kyaml/yaml
   if err := resourceList.Items[i].PipeE(yaml.SetAnnotation("value", value)); err != nil {
    return err
   }
  }
  return nil
 })
 cmd.Flags().StringVar(&value, "value", "", "flag value")
 if err := cmd.Execute(); err != nil {
  os.Exit(1)
 }
}

Build and test the function

Build the go binary:

go build -o my-fn .

Test it by running imperatively as an executable function:

# run the my-fn function against the configuration in PACKAGE_DIR/
kpt fn run PACKAGE_DIR/ --enable-exec --exec-path ./my-fn -- value=foo

Or declaratively as an executable function:

# PACKAGE_DIR/example.yaml
apiVersion: example.com/v1alpha1
kind: ConfigMap
metadata:
  name: foo
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: /path/to/my-fn
data:
  value: foo
kpt fn run PACKAGE_DIR/ --enable-exec

Publish the function

Build the function into a container image:

# optional: generate a Dockerfile to contain the function
go run ./main.go gen ./
# build the function into an image
docker build . -t gcr.io/project/fn-name:tag
# optional: push the image to a container registry
docker push gcr.io/project/fn-name:tag

Run the function imperatively as a container function:

kpt fn run PACKAGE_DIR/ --image gcr.io/project/fn-name:tag -- value=foo

Or run declaratively as a container function:

# PACKAGE_DIR/example.yaml
apiVersion: example.com/v1alpha1
kind: ConfigMap
metadata:
  name: foo
  annotations:
    config.kubernetes.io/function: |
      container:
        image: gcr.io/project/fn-name:tag
data:
  value: foo
kpt fn run PACKAGE_DIR/

Advanced Go Function

Functions may alternatively be written using a struct for parsing the functionConfig rather than flags. The example shown below explicitly implements what the preceding example implements implicitly.

package main

import (
 "os"

 "sigs.k8s.io/kustomize/kyaml/fn/framework"
 "sigs.k8s.io/kustomize/kyaml/yaml"
)

func main() {
 type Data struct {
        Value string `yaml:"value,omitempty"`
 }
 type Example struct {
        // Data contains the function configuration (e.g. client-side CRD).
        // Using "data" as the field name to contain key-value pairs enables
        // the function to be invoked imperatively via
        // `kpt fn run DIR/ --image img:tag -- key=value` and the
        // key=value arguments will be parsed into the functionConfig.data
        // field.
        // If the function does not need to be invoked imperatively, other
        // field names may be used.
        Data Data `yaml:"data,omitempty"`
 }
 functionConfig := &Example{}
    resourceList := &framework.ResourceList{FunctionConfig: functionConfig}

 cmd := framework.Command(resourceList, func() error {
  for i := range resourceList.Items {
            // use the kyaml libraries to modify each resource by applying
            // transformations
   err := resourceList.Items[i].PipeE(
                yaml.SetAnnotation("value", functionConfig.Data.Value),
            )
            if err != nil {
    return nil, err
   }
  }
  return items, nil
 })

 if err := cmd.Execute(); err != nil {
  os.Exit(1)
 }
}

Note: functionConfig need not read from the data field if it is not going to be run imperatively with kpt fn run DIR/ --image gcr.io/some/image -- foo=bar or kpt fn run DIR/ --exec-path /some/bin --enable-exec -- foo=bar. This is more appropriate for functions implementing abstractions (e.g. client-side CRD equivalents).

...
 type NestedValue struct {
  Value string `yaml:"value,omitempty"`
 }
 type Spec struct {
        NestedValue string `yaml:"nestedValue,omitempty"`
        MapValues map[string]string  `yaml:"mapValues,omitempty"`
        ListItems []string  `yaml:"listItems,omitempty"`
 }
 type Example struct {
        Spec Spec `yaml:"spec,omitempty"`
 }
 functionConfig := &Example{}
...
# PACKAGE_DIR/example.yaml
apiVersion: example.com/v1alpha1
kind: Example
metadata:
  name: foo
  annotations:
    config.kubernetes.io/function: |
      exec:
        path: /path/to/my-fn
spec:
  nestedValue:
    value: something
  mapValues:
    key: value
  listItems:
    - a
    - b

kyaml Libraries Reference

Functions written in go should use the sigs.k8s.io/kustomize/kyaml libraries for modifying resource configuration.

The sigs.k8s.io/kustomize/kyaml/yaml library offers utilities for reading and modifying yaml configuration, while retaining comments and structure.

To use the kyaml/yaml library, become familiar with:

  • The *yaml.RNode type, which represents a configuration object or field
  • The Pipe and PipeE functions, which apply a series of pipelined operations to the *RNode.

Workflow

To modify a *yaml.RNode call PipeE() on the *RNode, passing in the operations to be performed.

// Set the spec.replicas field to 3 if it exists
var node *yaml.RNode
...
err := node.PipeE(yaml.Lookup("spec", "replicas"), yaml.FieldSetter{StringValue: "3"})
// Set the spec.replicas field to 3, creating it if it doesn't exist
var node *yaml.RNode
...
// pass in the type of the node to create if it doesn't exist
// (e.g. Sequence, Map, Scalar)
err := node.PipeE(yaml.LookupCreate(yaml.ScalarNode, "spec", "replicas"), yaml.FieldSetter{StringValue: "3"})

To read a value from a *yaml.RNode call Pipe() on the RNode, passing in the operations to lookup a field.

// Read the spec.replicas field
var node *yaml.RNode
...
replicas, err := node.Pipe(yaml.Lookup("spec", "replicas"))

Operations are any types implementing the yaml.Filter interface, so it is simple to define custom operations and provide them to Pipe, combining them with the built-in operations.

Visiting Fields and Elements

Maps (i.e. Objects) and Sequences (i.e. Lists) support functions for visiting their fields and elements.

// Visit each of the elements in a Sequence (i.e. a List)
err := node.VisitElements(func(elem *yaml.RNode) error {
    // do something with each element in the list
    return nil
})
// Visit each of the fields in a Map (i.e. an Object)
err := node.VisitFields(func(n *yaml.MapNode) error {
    // do something with each field in the map / object
    return nil
})

Validation Results

Go functions can implement high fidelity validation results by setting a framework.Result on the ResourceList.

If run using kpt fn run --results-dir SOME_DIR/, the result will be written to a file in the specified directory.

If the result is returned and contains an item with severity of framework.Error, the function will exit non-0. Otherwise it will exit 0.

cmd := framework.Command(resourceList, func() error {
    ...
    if ... {
        // return validation results to be written under the results dir
     resourceList.Result = framework.Result{...}

        // return the results as an error if desireds
        return resourceList.Result
    }
    ...
})

Next Steps


Last modified November 9, 2020: docs: fix broken links for fn (a863150c)