Skip to content
Draft
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
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.
197 changes: 197 additions & 0 deletions documentation/content/en/guides/krm-functions/interfaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
---
title: Interfaces
linkTitle: Interfaces
description: Choose between fn.Runner and fn.ResourceListProcessor.
toc_hide: false
menu:
main:
parent: "KRM Function Developer Guide"
weight: 20
---
The SDK provides two interfaces for implementing KRM functions. Choose according
to your function requirements.

> The `main` functions below call `fn.AsMain` without `fn.WithDocs` to keep the
> interface examples focused. Production functions should embed documentation and
> pass `fn.WithDocs(readme, metadata)` — see the
> [tutorial]({{% relref "/guides/krm-functions/tutorial" %}}).

## fn.Runner

Use `fn.Runner` for **transformers** (mutators) and **validators**. This is the
recommended interface for most functions.

```go
type Runner interface {
Run(context *Context, functionConfig *KubeObject, items KubeObjects, results *Results) bool
}
```

Characteristics:
- The SDK automatically parses `functionConfig` into your struct's exported fields.
A typed functionConfig (its `kind` matching your struct name) is unmarshaled via
JSON tags; alternatively, a `ConfigMap` functionConfig has its `.data` map assigned
to a `map[string]string` field on your struct.
- You can **modify** existing items, but adding or removing items is not supported.
This is a convention, not a compile-time restriction: the SDK does not read back
items appended inside `Run`, so adds and removes are effectively dropped. Use
`fn.ResourceListProcessor` when you need to add or remove items.
- Return `true` for success, `false` for failure.
- Use `results` to report structured info/warning/error messages.

### Example: Validator

```go
var _ fn.Runner = &EnforceNamespace{}

type EnforceNamespace struct {
Namespace string `json:"namespace"`
}

func (r *EnforceNamespace) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
for _, obj := range items {
if obj.GetNamespace() != r.Namespace {
results.Errorf("resource %s/%s has namespace %q, expected %q",
obj.GetKind(), obj.GetName(), obj.GetNamespace(), r.Namespace)
}
}
return results.ExitCode() == 0
}

func main() {
runner := fn.WithContext(context.Background(), &EnforceNamespace{})
if err := fn.AsMain(runner); err != nil {
os.Exit(1)
}
}
```

### Example: Transformer (Mutator)

```go
var _ fn.Runner = &SetAnnotations{}

type SetAnnotations struct {
Annotations map[string]string `json:"annotations,omitempty"`
}

func (r *SetAnnotations) Run(ctx *fn.Context, functionConfig *fn.KubeObject, items fn.KubeObjects, results *fn.Results) bool {
for _, obj := range items {
for k, v := range r.Annotations {
if err := obj.SetAnnotation(k, v); err != nil {
results.ErrorE(err)
}
}
}
return results.ExitCode() == 0
}
```

## fn.ResourceListProcessor

Use `fn.ResourceListProcessor` for **generators** and **complex functions** that
need full control over the ResourceList.

```go
type ResourceListProcessor interface {
Process(rl *ResourceList) (bool, error)
}
```

Characteristics:
- Full access to `ResourceList.Items` — you can add, remove, or modify items.
- You must parse `functionConfig` manually from `rl.FunctionConfig`.
- You can modify `rl.Results` directly.
- Return `(true, nil)` for success, `(false, err)` for failure.

### Example: Generator

```go
type ConfigMapGenerator struct{}

func (g *ConfigMapGenerator) Process(rl *fn.ResourceList) (bool, error) {
// Parse functionConfig manually
name, _, _ := rl.FunctionConfig.NestedString("metadata", "name")

// Generate a new ConfigMap
cm := fn.NewEmptyKubeObject()
if err := cm.SetAPIVersion("v1"); err != nil {
return false, err
}
if err := cm.SetKind("ConfigMap"); err != nil {
return false, err
}
if err := cm.SetName(name + "-generated"); err != nil {
return false, err
}
if err := cm.SetNamespace("default"); err != nil {
return false, err
}

// Add to items
rl.Items = append(rl.Items, cm)
return true, nil
}

func main() {
if err := fn.AsMain(&ConfigMapGenerator{}); err != nil {
os.Exit(1)
}
}
```

### ResourceListProcessorFunc

For simple cases, use the function adapter instead of defining a struct:

```go
type ResourceListProcessorFunc func(rl *ResourceList) (bool, error)
```

Example:

```go
func main() {
processor := fn.ResourceListProcessorFunc(func(rl *fn.ResourceList) (bool, error) {
for _, obj := range rl.Items {
if err := obj.SetLabel("managed-by", "my-function"); err != nil {
return false, err
}
}
return true, nil
})
if err := fn.AsMain(processor); err != nil {
os.Exit(1)
}
}
```

## Choosing Between Interfaces

| Capability | fn.Runner | fn.ResourceListProcessor |
|---|---|---|
| Auto-parse functionConfig | ✅ | ❌ (manual) |
| Modify existing items | ✅ | ✅ |
| Add new items | ❌ | ✅ |
| Remove items | ❌ | ✅ |
| Access full ResourceList | ❌ | ✅ |
| Best for | Transformers, Validators | Generators, Complex functions |

## Wrapping a Runner

`fn.Runner` is wrapped into a `ResourceListProcessor` internally using
`fn.WithContext`:

```go
runner := fn.WithContext(context.Background(), &MyFunction{})
// runner implements ResourceListProcessor and can be passed to fn.AsMain
```

This wrapper handles the following:
1. Parsing `functionConfig` into your struct fields
2. Calling your `Run` method with the parsed context
3. Collecting results and determining success/failure

---

Next: [Testing]({{% relref "/guides/krm-functions/testing" %}}) — golden test patterns for verifying your function.
Loading