Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 73 additions & 28 deletions documentation/content/en/book/05-developing-functions/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,14 @@ for writing functions that manipulate KRM. Go provides:

### Quickstart

In this quickstart, we will write a function called "set-annotation" that adds an annotation
`config.kubernetes.io/managed-by=kpt` to all `Deployment` resources.
In this quickstart, we will start from the get-started scaffold — a small
"hello world" function that stamps a greeting annotation on every resource — and
adapt it into a function that adds `config.kubernetes.io/managed-by=kpt` to all
`Deployment` resources.

For a deeper treatment of function development — choosing an interface, testing
with golden files, and containerizing — see the
[KRM Function Developer Guide]({{% relref "/guides/krm-functions" %}}).

#### Set up your project

Expand Down Expand Up @@ -181,6 +187,7 @@ package main
import (
"context"
_ "embed"
"fmt"
"os"

"github.com/kptdev/krm-functions-sdk/go/fn"
Expand All @@ -192,26 +199,48 @@ var readme []byte
//go:embed metadata.yaml
var metadata []byte

var _ fn.Runner = &YourFunction{}
// greetingAnnotation is the annotation this example stamps onto every resource.
const greetingAnnotation = "example.kpt.dev/greeting"

// TODO: Change to your functionConfig "Kind" name.
type YourFunction struct {
FnConfigBool bool
FnConfigInt int
FnConfigFoo string
var _ fn.Runner = &HelloWorld{}
Comment thread
efiacor marked this conversation as resolved.

// HelloWorld is the functionConfig for this example. The struct name is used as
// the functionConfig `kind`, and each exported field is populated from the
// matching functionConfig key via its JSON tag.
//
// TODO: Rename this struct to your functionConfig "kind" and replace the fields
// with the configuration your function needs.
type HelloWorld struct {
Comment thread
efiacor marked this conversation as resolved.
Greeting string `json:"greeting,omitempty"`
Name string `json:"name,omitempty"`
}

// Run is the main function logic.
// `items` is parsed from the STDIN "ResourceList.Items".
// `functionConfig` is from the STDIN "ResourceList.FunctionConfig". The value has been assigned to the r attributes
// `functionConfig` is from the STDIN "ResourceList.FunctionConfig". Its values
// have already been unmarshaled into the receiver's fields.
// `results` is the "ResourceList.Results" that you can write result info to.
func (r *YourFunction) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
// TODO: Write your code.
return true
func (r *HelloWorld) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
Comment thread
efiacor marked this conversation as resolved.
greeting := r.Greeting
if greeting == "" {
greeting = "Hello"
}
name := r.Name
if name == "" {
name = "world"
}
message := fmt.Sprintf("%s, %s!", greeting, name)
for _, obj := range items {
if err := obj.SetAnnotation(greetingAnnotation, message); err != nil {
results.ErrorE(err)
}
}
results.Infof("greeted %d resource(s) with %q", len(items), message)
return results.ExitCode() == 0
}

func main() {
runner := fn.WithContext(context.Background(), &YourFunction{})
runner := fn.WithContext(context.Background(), &HelloWorld{})
if err := fn.AsMain(runner, fn.WithDocs(readme, metadata)); err != nil {
os.Exit(1)
}
Expand All @@ -225,19 +254,21 @@ Basically, the KRM resource `ResourceList.FunctionConfig` and KRM resources `Res
`KubeObject` objects. You can use `KubeObject` in a similar manner to
[`unstructured.Unstructured`](https://pkg.go.dev/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured).

The set-annotation function (see below) iterates the `ResourceList.Items`, finds out the `Deployment` resources and
adds the annotation. After the iteration, it adds some user message to the `ResourceList.Results`
The set-annotation function (see below) iterates the `ResourceList.Items`, finds the `Deployment` resources and
adds the annotation. After the iteration, it reports a user message to the `ResourceList.Results` via `results.Infof`.

```go
func (r *YourFunction) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
Comment thread
efiacor marked this conversation as resolved.
for _, kubeObject := range items {
if kubeObject.IsGVK("apps", "v1", "Deployment") {
kubeObject.SetAnnotation("config.kubernetes.io/managed-by", "kpt")
if kubeObject.GetKind() == "Deployment" {
if err := kubeObject.SetAnnotation("config.kubernetes.io/managed-by", "kpt"); err != nil {
results.ErrorE(err)
}
}
}
// This result message will be displayed in the function evaluation time.
*results = append(*results, fn.GeneralResult("Add config.kubernetes.io/managed-by=kpt to all `Deployment` resources", fn.Info))
return true
// This result message will be displayed at function evaluation time.
results.Infof("added config.kubernetes.io/managed-by=kpt to all Deployment resources")
return results.ExitCode() == 0
}
```

Expand All @@ -248,16 +279,16 @@ Learn more about the `KubeObject` from the [go documentation](https://pkg.go.dev
The "get-started" package contains a `./testdata` directory. You can use this to test out your functions.

```shell
# Edit the `testdata/noop-passthrough/resources.yaml` with your KRM resources.
# resources.yaml already has a `Deployment` and `Service` as test data.
vim testdata/noop-passthrough/resources.yaml
# Edit `testdata/hello-world/resources.yaml` with your KRM resources.
# Add a `Deployment` so the set-annotation logic above has something to match.
vim testdata/hello-world/resources.yaml

# Convert the KRM resources and FunctionConfig resource to `ResourceList`, and
# then pipe the ResourceList as StdIn to your function
kpt fn source testdata | go run main.go
```

Verify the KRM function behavior in the StdOutput `ResourceList` by looking for the new annotation on the "nginx-deplyment":
Verify the KRM function behavior in the StdOutput `ResourceList` by looking for the new annotation on the `Deployment`:

```yaml
apiVersion: apps/v1
Expand All @@ -281,10 +312,19 @@ kubeObject.SetAnnotation("config.kubernetes.io/managed-by", "kpt")
to

```shell
kubeObject.SetAnnotation("config.kubernetes.io/managed-by", r.FnConfigFoo)
kubeObject.SetAnnotation("config.kubernetes.io/managed-by", r.ManagedBy)
Comment thread
efiacor marked this conversation as resolved.
```

Add a `ManagedBy` field to your struct so the value can be read from the
functionConfig:

```go
type YourFunction struct {
ManagedBy string `json:"managedBy,omitempty"`
}
```

The annotation value will be set from the value of the `FnConfigFoo` field.
The annotation value will be set from the value of the `managedBy` field.

Create the configuration information so that we can concatenate it onto the ResourceList generated by the `kpt fn source` command. This
configuration specifies that the "config.kubernetes.io/managed-by" annotation should be set to a value of "bar".
Expand All @@ -298,7 +338,7 @@ functionConfig:
name: test
annotations:
internal.kpt.dev/upstream-identifier: 'fn.kpt.dev|YourFunction|default|test'
fnConfigFoo: bar
managedBy: bar
EOF
```

Expand Down Expand Up @@ -363,11 +403,16 @@ docker build . -t ${FN_CONTAINER_REGISTRY}/${FUNCTION_NAME}:${TAG}

To verify the image using the same `./testdata` resources
```shell
kpt fn eval ./testdata/noop-passthrough/resources.yaml --image ${FN_CONTAINER_REGISTRY}/${FUNCTION_NAME}:${TAG}
kpt fn eval ./testdata/hello-world/resources.yaml --image ${FN_CONTAINER_REGISTRY}/${FUNCTION_NAME}:${TAG}
```

### Next Steps

- Read the [KRM Function Developer Guide]({{% relref "/guides/krm-functions" %}})
for choosing an [interface]({{% relref "/guides/krm-functions/interfaces" %}})
(`fn.Runner` vs `fn.ResourceListProcessor`),
[testing]({{% relref "/guides/krm-functions/testing" %}}) with golden files, and
[containerizing]({{% relref "/guides/krm-functions/containerizing" %}}) your function.
- See other [go documentation examples](https://pkg.go.dev/github.com/kptdev/krm-functions-sdk/go/fn/examples) to use KubeObject.
- To contribute to KRM catalog functions, please follow the [contributor guide](https://github.com/kptdev/krm-functions-catalog/blob/main/CONTRIBUTING.md)
- For the `metadata.yaml` schema reference (required fields, allowed tags), see the [metadata schema documentation](https://catalog.kpt.dev/metadata-schema/)
1 change: 1 addition & 0 deletions documentation/content/en/guides/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ menu:
- [Value Propagation Pattern]({{% relref "/guides/value-propagation" %}})
- [Tenant Onboarding]({{% relref "/guides/tenant-onboarding" %}})
- [Understanding 3-Way Merge in kpt]({{% relref "/guides/3-way-merge" %}})
- [KRM Function Developer Guide]({{% relref "/guides/krm-functions" %}})
26 changes: 26 additions & 0 deletions documentation/content/en/guides/krm-functions/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
title: KRM Function Developer Guide
linkTitle: KRM Function Developer Guide
description: Write your own KRM functions with the Go SDK.
toc_hide: false
menu:
main:
parent: "Guides"
---
This guide walks through writing KRM functions with the
[Go SDK](https://github.com/kptdev/krm-functions-sdk). Start with the tutorial,
then dig into the topic guides as needed.

- [Tutorial]({{% relref "/guides/krm-functions/tutorial" %}}) — build a working
function end to end, with embedded documentation, golden tests, and support for
`--help`, `--doc`, and standalone file mode.
- [Interfaces]({{% relref "/guides/krm-functions/interfaces" %}}) — choose between
`fn.Runner` (transformers, validators) and `fn.ResourceListProcessor`
(generators, complex functions).
- [Testing]({{% relref "/guides/krm-functions/testing" %}}) — golden test patterns
and unit testing in depth.
- [Containerizing]({{% relref "/guides/krm-functions/containerizing" %}}) — package
your function as a container image.

For a complete working example, see
[`go/get-started/`](https://github.com/kptdev/krm-functions-sdk/tree/main/go/get-started).
148 changes: 148 additions & 0 deletions documentation/content/en/guides/krm-functions/containerizing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
---
title: Containerizing
linkTitle: Containerizing
description: Package a KRM function as a container image.
toc_hide: false
menu:
main:
parent: "KRM Function Developer Guide"
weight: 40
---
KRM functions are distributed as container images. This guide covers building
and running containerized functions.

## Dockerfile

The [krm-functions-catalog](https://github.com/kptdev/krm-functions-catalog)
provides a shared Dockerfile at `build/docker/go/Dockerfile` that all the catalog
functions use. It accepts `BUILDER_IMAGE` and `BASE_IMAGE` as build args.

For standalone functions or local development, use a multi-stage build with a
minimal base image. The function binary should be statically linked (no CGO), so
it can run on `scratch` or `distroless`:

```dockerfile
FROM golang:1.26-alpine AS builder
ENV CGO_ENABLED=0
WORKDIR /go/src/
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /usr/local/bin/function ./

FROM scratch
COPY --from=builder /usr/local/bin/function /usr/local/bin/function
ENTRYPOINT ["function"]
```

Key points:
- `CGO_ENABLED=0` produces a static binary that runs on `scratch`.
- The `scratch` base image has zero overhead — no shell, no OS packages.
- If you need TLS certificates (e.g., for network calls), use `gcr.io/distroless/static` instead of `scratch`.
- Copy only the binary to the final image to minimize size.

### Alternative with distroless

```dockerfile
FROM golang:1.26-alpine AS builder
ENV CGO_ENABLED=0
WORKDIR /go/src/
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o /usr/local/bin/function ./

FROM gcr.io/distroless/static:nonroot
COPY --from=builder /usr/local/bin/function /usr/local/bin/function
ENTRYPOINT ["function"]
```

## Building

```bash
docker build -t ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 .
```

### Image Naming Convention

Follow this pattern for function images:

```
ghcr.io/kptdev/krm-functions-catalog/{function-name}:{version}
```

Examples:
- `ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.1`
- `ghcr.io/kptdev/krm-functions-catalog/enforce-namespace:v1.0`
- `ghcr.io/kptdev/krm-functions-catalog/generate-configmap:v0.3`

Use semantic versioning for tags. Avoid `latest` in production pipelines.

## Running

KRM functions read from STDIN and write to STDOUT:

```bash
docker run --rm -i ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 < input.yaml > output.yaml
```

### With file mode

```bash
docker run --rm -v $(pwd):/data ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 /data/deployment.yaml
```

Note: file mode assembles the given files into a ResourceList with an **empty
functionConfig**. Functions that require configuration should be run via STDIN
(or a `kpt` pipeline) so the functionConfig is provided.

### Help and doc flags

```bash
docker run --rm ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 --help
docker run --rm ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 --doc
```

## Using with kpt

In a `Kptfile` pipeline, `kpt fn render` will pull the image from the registry
and run it against your package resources:

```yaml
apiVersion: kpt.dev/v1
kind: Kptfile
metadata:
name: my-package
pipeline:
mutators:
- image: ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.1
configMap:
app: my-app
validators:
- image: ghcr.io/kptdev/krm-functions-catalog/enforce-namespace:v1.0
configMap:
namespace: production
```

Note: the image must be published and accessible from the machine running
`kpt fn render`. For local development, build the image locally first. It
will be used from the local Docker cache without pulling.

## Tips

- Keep images small — a typical Go KRM function image is 5–15 MB with `scratch`.
- Pin dependency versions in `go.mod` for reproducible builds.
- Use `.dockerignore` to exclude test data, docs, and other non-build files.
- Test the container locally before publishing:
```bash
echo '{"apiVersion":"config.kubernetes.io/v1","kind":"ResourceList","items":[]}' | \
docker run --rm -i ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1
```

## Publishing

Publishing function images to a registry is handled by the
[krm-functions-catalog](https://github.com/kptdev/krm-functions-catalog)
CI pipeline. See the catalog's
[CONTRIBUTING.md](https://github.com/kptdev/krm-functions-catalog/blob/main/CONTRIBUTING.md)
for the release workflow.
Loading