akua / examples / 08-pkg-compose
08-pkg-compose
Package composition via `pkg.render` — an outer Package calls a reusable inner Package twice with different inputs and concatenates the results. Renders end-to-end today (pure KCL; no helm needed).
Package composition via pkg.render — an outer Package calls a reusable inner Package twice with different inputs and concatenates the results. Renders end-to-end today (pure KCL; no helm needed).
What's here
| file | purpose |
|---|---|
package.k | Outer Package; calls pkg.render(pkg.Render { package = "shared", ... }) twice with distinct inputs. |
shared/package.k | Inner Package; emits one ConfigMap parameterized by name + payload. |
shared/akua.toml | Inner manifest — marks shared/ as an Akua package. |
akua.toml | Outer manifest — declares shared as a workspace-local path dep. |
inputs.example.yaml | Per-component inputs, auto-discovered by akua render. |
Render
cargo run -q -p akua-cli -- render --package examples/08-pkg-compose/package.k --out ./rendered
Or, from the example directory:
akua render --out ./rendered
Two ConfigMaps land in ./rendered/ (checked in as reference output):
rendered/
├── 000-configmap-frontend.yaml
└── 001-configmap-backend.yaml
How pkg.render works
pkg.render is a synchronous KCL host plugin: the handler resolves the package alias against the calling Package's akua.toml [dependencies] (no filesystem paths in user code), calls PackageK::load(...).render(inputs) inline, and returns the inner resources list directly to the caller. Because the return is a real list, list-comprehension patches and filter expressions on the result work natively — no post-eval rewrite step.
Reentrancy works because akua's KCL fork copies the plugin handler fn pointer out of its mutex before invoking the callback (see cnap-tech/kcl#akua-wasm32); upstream KCL holds the mutex across the call and would deadlock here.
Cycle detection uses akua-core's thread-local render-stack: a Package referring to itself, directly or transitively (A → B → A), is rejected before infinite recursion. Nested pkg.render calls (A → B → C) recurse through the same handler, with each inner Package's render pushing/popping the stack.
package.k
# Outer Package: composes two ConfigMaps from a reusable nested
# Package via `pkg.render`. Exercises akua's Package-of-Packages
# composition — the inner Package is declared as the `shared` dep in
# akua.toml and loaded + rendered fresh for each `pkg.render` call.
#
# Render:
#
# akua render --package examples/08-pkg-compose/package.k --out ./rendered
#
# Produces two ConfigMaps: `config-frontend` and `config-backend`,
# each with its own data payload from the outer's inputs.
import akua.ctx
import akua.pkg
schema ComponentInput:
"""Per-component inputs — name + payload passed through to the
shared ConfigMap schema in `./shared/`."""
name: str
data: {str:str} = {}
schema Input:
frontend: ComponentInput = ComponentInput { name = "config-frontend" }
backend: ComponentInput = ComponentInput { name = "config-backend" }
input: Input = ctx.input()
# Each call loads + renders the `shared` dep synchronously with the
# supplied inputs and returns the inner resources list. `package` is
# the dep alias from akua.toml — no filesystem path in the program.
_frontend = pkg.render(pkg.Render {
package = "shared"
inputs = {
name = input.frontend.name
payload = input.frontend.data
}
})
_backend = pkg.render(pkg.Render {
package = "shared"
inputs = {
name = input.backend.name
payload = input.backend.data
}
})
resources = _frontend + _backend
Rendered output
000-configmap-frontend.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: frontend
labels:
app.kubernetes.io/name: frontend
app.kubernetes.io/component: shared
data:
PUBLIC_API_URL: https://api.example.com
THEME: dark
001-configmap-backend.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: backend
labels:
app.kubernetes.io/name: backend
app.kubernetes.io/component: shared
data:
DATABASE_URL: postgres://db.internal:5432/app
LOG_LEVEL: info
Source: examples/08-pkg-compose/