Skip to content

External Functions

Functions are how ConfigHub operates on configuration data: they validate it, change it, and extract values from it, on demand or automatically through Triggers. ConfigHub comes with a library of functions built in. When that library cannot express what you need, you write a function of your own, host it in your own environment, and ConfigHub invokes it like any other.

This page explains what the built-in functions cover, why and when to run your own, what you can write them in, and how to build and run one with the Go SDK.

Built-in functions

ConfigHub ships functions for Kubernetes resources and for the other configuration formats it supports: set-image and set-env-var to change a workload, vet-schemas and vet-cel to validate it, get-container-image to read from it, and many more. They run inside the ConfigHub server, need no setup, and cub function list shows them all.

cub function list
cub function do --space my-space --unit my-deployment set-replicas 3

Using functions covers invoking them, bulk operations, and attributes. Validating configuration and enforcing policies covers running them in Triggers.

Running your own functions

An external function is one you write and run yourself, in a process you operate, which ConfigHub calls the same way it calls a built-in function. You would run a function of your own instead of using a built-in one when:

  • The logic is yours. Your organization's naming rules, a derived value that takes real computation, a check that reads three resources at once. Built-in functions are general; yours can be as specific as you need.
  • It has to reach something only you can reach. A policy engine running in your cluster, an internal registry, an inventory system, a secrets manager. The function runs in your environment with your credentials and your network access. ConfigHub never holds those credentials, and the call never leaves your boundary except for the configuration going in and the result coming out.
  • It needs a library or a tool. Built-in functions are hermetic. Yours can link any library or shell out to any binary, which is how the examples wrap kube-score, kube-linter and Kyverno. The one constraint is that a function returns synchronously, so it has to finish without long delays.

How an external function is invoked

Your process authenticates to ConfigHub as a Worker. A Worker is an identity in ConfigHub, with its own credentials and permissions; a process connected with that identity is where ConfigHub sends invocations of the functions it registered. The connection is outbound from your process over HTTP long-polling on the normal API port, so it works from behind a firewall and needs no inbound access.

On connecting, the process tells ConfigHub which functions it serves. From then on, an invocation that names the Worker is routed over that connection: ConfigHub sends the Unit's configuration data and the arguments, the process runs the function, and ConfigHub receives the result. A mutating function's changes are recorded as a Revision, a validating function's verdict becomes a Validation Error if it fails, exactly as for a built-in function. Triggers can run external functions, and a Trigger whose Worker has been unavailable for long enough is automatically disabled; connection status describes the conditions ConfigHub reports.

Languages

A Worker talks to ConfigHub over HTTP with JSON bodies, so a function can be written in any language. Today the Go SDK is the only supported client, and the wire protocol is not yet documented for independent implementations. The rest of this page uses Go.

Build a function in Go

A function has three parts: a signature that tells ConfigHub its name, parameters, output and properties; an implementation; and a main that registers it and connects to ConfigHub. The hello-world-function example is the complete version of what follows.

The signature:

import "github.com/confighub/sdk/core/function/api"

func GetHelloWorldFunctionSignature() api.FunctionSignature {
    return api.FunctionSignature{
        FunctionName: "hello-world",
        Parameters: []api.FunctionParameter{{
            ParameterName: "greeting",
            Description:   "The greeting message to add to the configuration",
            Required:      true,
            DataType:      api.DataTypeString,
        }},
        RequiredParameters: 1,
        OutputInfo: &api.FunctionOutput{
            ResultName:  "modified-config",
            Description: "Configuration with greeting annotation added",
            OutputType:  api.OutputTypeYAML,
        },
        Mutating:              true,
        Validating:            false,
        Hermetic:              true,
        Idempotent:            true,
        Description:           "Adds a greeting annotation to every Kubernetes resource",
        FunctionType:          api.FunctionTypeCustom,
        AffectedResourceTypes: []api.ResourceType{api.ResourceTypeAny},
    }
}

Mutating, Validating, Hermetic and Idempotent tell ConfigHub what the function does, which decides how its result is recorded and where it may run. Names of functions, parameters and results are kebab-case, like the CLI's.

The implementation receives the parsed configuration data and the arguments, and returns the data, an optional output value, and an error. The SDK handles argument count and type checking before it is called:

import (
    "github.com/confighub/sdk/configkit/k8skit"
    "github.com/confighub/sdk/core/configkit/yamlkit"
    "github.com/confighub/sdk/core/function/api"
    "github.com/confighub/sdk/core/function/handler"
    "github.com/confighub/sdk/core/third_party/gaby"
)

func HelloWorldFunction(fArgs handler.FunctionImplementationArguments) (gaby.Container, any, error) {
    greeting := fArgs.Arguments[0].Value.(string)
    visitor := func(doc *gaby.YamlDoc, output any, index int, resourceInfo *api.ResourceInfo) (any, []error) {
        if _, err := doc.SetP(greeting, "metadata.annotations.confighub-example/hello-world-greeting"); err != nil {
            return nil, []error{err}
        }
        return output, nil
    }
    _, err := yamlkit.VisitResources(fArgs.ParsedData, nil, k8skit.NewK8sResourceProvider(), visitor)
    return fArgs.ParsedData, nil, err
}

A validating function returns false and a ValidationResult as its output instead of mutating the data; a read-only function returns the extracted value. The function executor README in the SDK covers traversing resources and paths, the function API in full, and the other configuration formats. The built-in functions in function-impl are written against the same API and are the largest set of examples.

main creates an executor, registers the function for the toolchain it applies to, and starts the connector with the Worker's credentials from the environment:

import (
    "log"
    "os"

    "github.com/confighub/sdk/configkit/k8skit"
    "github.com/confighub/sdk/core/function/executor"
    "github.com/confighub/sdk/core/function/handler"
    "github.com/confighub/sdk/core/worker"
    "github.com/confighub/sdk/core/workerapi"
)

func main() {
    exec := executor.NewEmptyExecutor()
    exec.RegisterToolchain(k8skit.NewK8sResourceProvider(), true)
    exec.RegisterFunction(workerapi.ToolchainKubernetesYAML, handler.FunctionRegistration{
        FunctionSignature: GetHelloWorldFunctionSignature(),
        Function:          HelloWorldFunction,
    })

    connector, err := worker.NewConnector(worker.ConnectorOptions{
        WorkerID:         os.Getenv("CONFIGHUB_WORKER_ID"),
        WorkerSecret:     os.Getenv("CONFIGHUB_WORKER_SECRET"),
        ConfigHubURL:     os.Getenv("CONFIGHUB_URL"),
        FunctionExecutor: exec,
    })
    if err != nil {
        log.Fatal(err)
    }
    log.Fatal(connector.Start())
}

A function that needs one-time setup, such as opening a client to a policy engine, supplies a FunctionInit in its registration; validating with admission webhooks shows one.

Run the process

The process reads three variables from its environment, plus whatever your own functions need:

Variable Value
CONFIGHUB_URL The ConfigHub server, such as https://hub.confighub.com
CONFIGHUB_WORKER_ID The Worker's ID
CONFIGHUB_WORKER_SECRET The Worker's secret

Locally

Create the Worker, put its credentials in your shell, name the server, and start the binary:

cub worker create --space $SPACE my-worker
eval "$(cub worker get-envs --space $SPACE my-worker)"
export CONFIGHUB_URL=https://hub.confighub.com
go build -o my-worker . && ./my-worker

It connects as the Worker and reports the functions it serves, and the Worker shows as Ready. Invocations name the Worker with --worker:

cub worker list --space $SPACE
cub worker list-function --space $SPACE my-worker
cub function do --space $SPACE --unit my-unit --worker $SPACE/my-worker hello-world "Hello from ConfigHub!"

In Kubernetes

Build the binary into an image, as each example's Dockerfile does. Give the cluster the credentials as a Secret, then run the image with a Deployment that reads them. --no-export prints plain KEY=value lines, which kubectl create secret accepts as an env file:

kubectl create namespace my-worker
cub worker get-envs --no-export --space $SPACE my-worker \
  | kubectl -n my-worker create secret generic my-worker-credentials --from-env-file=/dev/stdin

If you manage secrets some other way, such as with the external secrets operator, the two keys the Secret needs are CONFIGHUB_WORKER_ID and CONFIGHUB_WORKER_SECRET.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-worker
  namespace: my-worker
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-worker
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-worker
    spec:
      terminationGracePeriodSeconds: 60
      containers:
        - name: worker
          image: my-registry/my-worker:latest
          env:
            - name: CONFIGHUB_URL
              value: https://hub.confighub.com
          envFrom:
            - secretRef:
                name: my-worker-credentials
kubectl apply -f my-worker.yaml
kubectl -n my-worker rollout status deployment/my-worker --timeout=120s
cub worker list --space $SPACE

Add your functions' own variables under env. The rollout strategy starts the replacement replica before the running one is terminated, so an upgrade hands over without a gap; replicas and high availability describes how ConfigHub treats two connections with the same credentials. A function that calls the Kubernetes API also needs a ServiceAccount with the right permissions; the admission webhook guide shows the RBAC for one such case.

The Deployment is ordinary Kubernetes configuration, so it can live in a Unit and reach the cluster by publishing a Release like anything else. Keep the Secret out of the Unit and create it in the cluster as above:

cub unit create --space $SPACE my-worker my-worker.yaml
cub release publish $SPACE

In Triggers

Once the Worker shows Ready, the functions your process serves can run automatically. A Trigger names the Worker the same way an invocation does:

cub trigger create --space app-dev --worker $SPACE/my-worker \
  greet Mutation Kubernetes/YAML hello-world "Hello from ConfigHub!"

The validation guide covers Trigger behaviour in full, including what happens to a Trigger when its Worker goes away.

Examples

Every directory in examples/custom-workers is a complete, buildable process with a Dockerfile and a demo script: