Skip to content

Commit ca9cd64

Browse files
committed
docs: add KRM function developer guide
Add a developer guide for writing KRM functions with the Go SDK, covering a tutorial, interface selection (fn.Runner vs fn.ResourceListProcessor), testing, and containerizing. Sourced from the krm-functions-sdk docs and adapted into a Hugo subsection under guides/. Refs #4725 Signed-off-by: Fiachra Corcoran <fiachra.corcoran@est.tech>
1 parent 18dff10 commit ca9cd64

6 files changed

Lines changed: 824 additions & 0 deletions

File tree

documentation/content/en/guides/_index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ menu:
1515
- [Value Propagation Pattern]({{% relref "/guides/value-propagation" %}})
1616
- [Tenant Onboarding]({{% relref "/guides/tenant-onboarding" %}})
1717
- [Understanding 3-Way Merge in kpt]({{% relref "/guides/3-way-merge" %}})
18+
- [KRM Function Developer Guide]({{% relref "/guides/krm-functions" %}})
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
---
2+
title: KRM Function Developer Guide
3+
linkTitle: KRM Function Developer Guide
4+
description: Write your own KRM functions with the Go SDK.
5+
toc_hide: false
6+
menu:
7+
main:
8+
parent: "Guides"
9+
---
10+
This guide walks through writing KRM functions with the
11+
[Go SDK](https://github.com/kptdev/krm-functions-sdk). Start with the tutorial,
12+
then dig into the topic guides as needed.
13+
14+
- [Tutorial]({{% relref "/guides/krm-functions/tutorial" %}}) — build a working
15+
function end to end, with embedded documentation, golden tests, and support for
16+
`--help`, `--doc`, and standalone file mode.
17+
- [Interfaces]({{% relref "/guides/krm-functions/interfaces" %}}) — choose between
18+
`fn.Runner` (transformers, validators) and `fn.ResourceListProcessor`
19+
(generators, complex functions).
20+
- [Testing]({{% relref "/guides/krm-functions/testing" %}}) — golden test patterns
21+
and unit testing in depth.
22+
- [Containerizing]({{% relref "/guides/krm-functions/containerizing" %}}) — package
23+
your function as a container image.
24+
25+
For a complete working example, see
26+
[`go/get-started/`](https://github.com/kptdev/krm-functions-sdk/tree/main/go/get-started).
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
title: Containerizing
3+
linkTitle: Containerizing
4+
description: Package a KRM function as a container image.
5+
toc_hide: false
6+
menu:
7+
main:
8+
parent: "KRM Function Developer Guide"
9+
weight: 40
10+
---
11+
KRM functions are distributed as container images. This guide covers building
12+
and running containerized functions.
13+
14+
## Dockerfile
15+
16+
The [krm-functions-catalog](https://github.com/kptdev/krm-functions-catalog)
17+
provides a shared Dockerfile at `build/docker/go/Dockerfile` that all the catalog
18+
functions use. It accepts `BUILDER_IMAGE` and `BASE_IMAGE` as build args.
19+
20+
For standalone functions or local development, use a multi-stage build with a
21+
minimal base image. The function binary should be statically linked (no CGO), so
22+
it can run on `scratch` or `distroless`:
23+
24+
```dockerfile
25+
FROM golang:1.26-alpine AS builder
26+
ENV CGO_ENABLED=0
27+
WORKDIR /go/src/
28+
COPY go.mod go.sum ./
29+
RUN go mod download
30+
COPY . .
31+
RUN go build -o /usr/local/bin/function ./
32+
33+
FROM scratch
34+
COPY --from=builder /usr/local/bin/function /usr/local/bin/function
35+
ENTRYPOINT ["function"]
36+
```
37+
38+
Key points:
39+
- `CGO_ENABLED=0` produces a static binary that runs on `scratch`.
40+
- The `scratch` base image has zero overhead — no shell, no OS packages.
41+
- If you need TLS certificates (e.g., for network calls), use `gcr.io/distroless/static` instead of `scratch`.
42+
- Copy only the binary to the final image to minimize size.
43+
44+
### Alternative with distroless
45+
46+
```dockerfile
47+
FROM golang:1.26-alpine AS builder
48+
ENV CGO_ENABLED=0
49+
WORKDIR /go/src/
50+
COPY go.mod go.sum ./
51+
RUN go mod download
52+
COPY . .
53+
RUN go build -o /usr/local/bin/function ./
54+
55+
FROM gcr.io/distroless/static:nonroot
56+
COPY --from=builder /usr/local/bin/function /usr/local/bin/function
57+
ENTRYPOINT ["function"]
58+
```
59+
60+
## Building
61+
62+
```bash
63+
docker build -t ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 .
64+
```
65+
66+
### Image Naming Convention
67+
68+
Follow this pattern for function images:
69+
70+
```
71+
ghcr.io/kptdev/krm-functions-catalog/{function-name}:{version}
72+
```
73+
74+
Examples:
75+
- `ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.1`
76+
- `ghcr.io/kptdev/krm-functions-catalog/enforce-namespace:v1.0`
77+
- `ghcr.io/kptdev/krm-functions-catalog/generate-configmap:v0.3`
78+
79+
Use semantic versioning for tags. Avoid `latest` in production pipelines.
80+
81+
## Running
82+
83+
KRM functions read from STDIN and write to STDOUT:
84+
85+
```bash
86+
docker run --rm -i ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 < input.yaml > output.yaml
87+
```
88+
89+
### With file mode
90+
91+
```bash
92+
docker run --rm -v $(pwd):/data ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 /data/deployment.yaml
93+
```
94+
95+
### Help and doc flags
96+
97+
```bash
98+
docker run --rm ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 --help
99+
docker run --rm ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1 --doc
100+
```
101+
102+
## Using with kpt
103+
104+
In a `Kptfile` pipeline, `kpt fn render` will pull the image from the registry
105+
and run it against your package resources:
106+
107+
```yaml
108+
apiVersion: kpt.dev/v1
109+
kind: Kptfile
110+
metadata:
111+
name: my-package
112+
pipeline:
113+
mutators:
114+
- image: ghcr.io/kptdev/krm-functions-catalog/set-labels:v0.1
115+
configMap:
116+
app: my-app
117+
validators:
118+
- image: ghcr.io/kptdev/krm-functions-catalog/enforce-namespace:v1.0
119+
configMap:
120+
namespace: production
121+
```
122+
123+
Note: the image must be published and accessible from the machine running
124+
`kpt fn render`. For local development, build the image locally first. It
125+
will be used from the local Docker cache without pulling.
126+
127+
## Tips
128+
129+
- Keep images small — a typical Go KRM function image is 5–15 MB with `scratch`.
130+
- Pin dependency versions in `go.mod` for reproducible builds.
131+
- Use `.dockerignore` to exclude test data, docs, and other non-build files.
132+
- Test the container locally before publishing:
133+
```bash
134+
echo '{"apiVersion":"config.kubernetes.io/v1","kind":"ResourceList","items":[]}' | \
135+
docker run --rm -i ghcr.io/kptdev/krm-functions-catalog/my-function:v0.1
136+
```
137+
138+
## Publishing
139+
140+
Publishing function images to a registry is handled by the
141+
[krm-functions-catalog](https://github.com/kptdev/krm-functions-catalog)
142+
CI pipeline. See the catalog's
143+
[CONTRIBUTING.md](https://github.com/kptdev/krm-functions-catalog/blob/main/CONTRIBUTING.md)
144+
for the release workflow.
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
---
2+
title: Interfaces
3+
linkTitle: Interfaces
4+
description: Choose between fn.Runner and fn.ResourceListProcessor.
5+
toc_hide: false
6+
menu:
7+
main:
8+
parent: "KRM Function Developer Guide"
9+
weight: 20
10+
---
11+
The SDK provides two interfaces for implementing KRM functions. Choose according
12+
to your function requirements.
13+
14+
> The `main` functions below call `fn.AsMain` without `fn.WithDocs` to keep the
15+
> interface examples focused. Production functions should embed documentation and
16+
> pass `fn.WithDocs(readme, metadata)` — see the
17+
> [tutorial]({{% relref "/guides/krm-functions/tutorial" %}}).
18+
19+
## fn.Runner
20+
21+
Use `fn.Runner` for **transformers** (mutators) and **validators**. This is the
22+
recommended interface for most functions.
23+
24+
```go
25+
type Runner interface {
26+
Run(context *Context, functionConfig *KubeObject, items KubeObjects, results *Results) bool
27+
}
28+
```
29+
30+
Characteristics:
31+
- The SDK automatically parses `functionConfig` into your struct's exported fields (via JSON tags).
32+
- You can **modify** existing items, but you cannot add or remove items from the slice.
33+
- Return `true` for success, `false` for failure.
34+
- Use `results` to report structured info/warning/error messages.
35+
36+
### Example: Validator
37+
38+
```go
39+
var _ fn.Runner = &EnforceNamespace{}
40+
41+
type EnforceNamespace struct {
42+
Namespace string `json:"namespace"`
43+
}
44+
45+
func (r *EnforceNamespace) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
46+
for _, obj := range items {
47+
if obj.GetNamespace() != r.Namespace {
48+
results.Errorf("resource %s/%s has namespace %q, expected %q",
49+
obj.GetKind(), obj.GetName(), obj.GetNamespace(), r.Namespace)
50+
}
51+
}
52+
return results.ExitCode() == 0
53+
}
54+
55+
func main() {
56+
runner := fn.WithContext(context.Background(), &EnforceNamespace{})
57+
if err := fn.AsMain(runner); err != nil {
58+
os.Exit(1)
59+
}
60+
}
61+
```
62+
63+
### Example: Transformer (Mutator)
64+
65+
```go
66+
var _ fn.Runner = &SetAnnotations{}
67+
68+
type SetAnnotations struct {
69+
Annotations map[string]string `json:"annotations,omitempty"`
70+
}
71+
72+
func (r *SetAnnotations) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
73+
for _, obj := range items {
74+
for k, v := range r.Annotations {
75+
if err := obj.SetAnnotation(k, v); err != nil {
76+
results.ErrorE(err)
77+
}
78+
}
79+
}
80+
return results.ExitCode() == 0
81+
}
82+
```
83+
84+
## fn.ResourceListProcessor
85+
86+
Use `fn.ResourceListProcessor` for **generators** and **complex functions** that
87+
need full control over the ResourceList.
88+
89+
```go
90+
type ResourceListProcessor interface {
91+
Process(rl *ResourceList) (bool, error)
92+
}
93+
```
94+
95+
Characteristics:
96+
- Full access to `ResourceList.Items` — you can add, remove, or modify items.
97+
- You must parse `functionConfig` manually from `rl.FunctionConfig`.
98+
- You can modify `rl.Results` directly.
99+
- Return `(true, nil)` for success, `(false, err)` for failure.
100+
101+
### Example: Generator
102+
103+
```go
104+
type ConfigMapGenerator struct{}
105+
106+
func (g *ConfigMapGenerator) Process(rl *fn.ResourceList) (bool, error) {
107+
// Parse functionConfig manually
108+
name, _, _ := rl.FunctionConfig.NestedString("metadata", "name")
109+
110+
// Generate a new ConfigMap
111+
cm := fn.NewEmptyKubeObject()
112+
if err := cm.SetAPIVersion("v1"); err != nil {
113+
return false, err
114+
}
115+
if err := cm.SetKind("ConfigMap"); err != nil {
116+
return false, err
117+
}
118+
if err := cm.SetName(name + "-generated"); err != nil {
119+
return false, err
120+
}
121+
if err := cm.SetNamespace("default"); err != nil {
122+
return false, err
123+
}
124+
125+
// Add to items
126+
rl.Items = append(rl.Items, cm)
127+
return true, nil
128+
}
129+
130+
func main() {
131+
if err := fn.AsMain(&ConfigMapGenerator{}); err != nil {
132+
os.Exit(1)
133+
}
134+
}
135+
```
136+
137+
### ResourceListProcessorFunc
138+
139+
For simple cases, use the function adapter instead of defining a struct:
140+
141+
```go
142+
type ResourceListProcessorFunc func(rl *ResourceList) (bool, error)
143+
```
144+
145+
Example:
146+
147+
```go
148+
func main() {
149+
processor := fn.ResourceListProcessorFunc(func(rl *fn.ResourceList) (bool, error) {
150+
for _, obj := range rl.Items {
151+
if err := obj.SetLabel("managed-by", "my-function"); err != nil {
152+
return false, err
153+
}
154+
}
155+
return true, nil
156+
})
157+
if err := fn.AsMain(processor); err != nil {
158+
os.Exit(1)
159+
}
160+
}
161+
```
162+
163+
## Choosing Between Interfaces
164+
165+
| Capability | fn.Runner | fn.ResourceListProcessor |
166+
|---|---|---|
167+
| Auto-parse functionConfig || ❌ (manual) |
168+
| Modify existing items |||
169+
| Add new items |||
170+
| Remove items |||
171+
| Access full ResourceList |||
172+
| Best for | Transformers, Validators | Generators, Complex functions |
173+
174+
## Wrapping a Runner
175+
176+
`fn.Runner` is wrapped into a `ResourceListProcessor` internally using
177+
`fn.WithContext`:
178+
179+
```go
180+
runner := fn.WithContext(context.Background(), &MyFunction{})
181+
// runner implements ResourceListProcessor and can be passed to fn.AsMain
182+
```
183+
184+
This wrapper handles the following:
185+
1. Parsing `functionConfig` into your struct fields
186+
2. Calling your `Run` method with the parsed context
187+
3. Collecting results and determining success/failure
188+
189+
---
190+
191+
Next: [Testing]({{% relref "/guides/krm-functions/testing" %}}) — golden test patterns for verifying your function.

0 commit comments

Comments
 (0)