akua / concepts / package-format

Package format

Package.k authoring shape — imports, schemas, body, and the `resources` output.

The canonical shape of an akua Package. A Package is a reusable definition authored in KCL and published as a signed OCI artifact. Package.k is the only shape akua itself specifies; higher-level workspace concepts (App / Environment / Cluster / PolicySet / etc.) are user-defined KCL schemas in the consumer's workspace, not akua-owned kinds.

This document specifies what a package.k file may contain. Companion references: lockfile-format.md for akua.toml / akua.lock, policy-format.md for Rego.


1. Anatomy

Every Package is one KCL program with three typed regions:

# package.k

# (1) imports — engines, ambient context, schemas, reusable modules
import akua.ctx
import akua.helm
import charts.cnpg    as cnpg
import charts.webapp  as webapp

# (2) schema — the public input contract
schema Input:
    appName:  str
    hostname: str
    replicas: int = 3

input: Input = ctx.input()

# (3) body — source-engine calls + transforms + aggregation
_pg  = helm.template(helm.Template { chart = cnpg.Chart,   values = ... })
_app = helm.template(helm.Template { chart = webapp.Chart, values = ... })

resources = _pg + _app

That's it. akua render writes resources as raw YAML files under --out. Other distribution shapes (Helm charts, OCI bundles, kro RGDs) come from either (a) transformation functions invoked in the body that produce more K8s resources (kro.rgd(...), crossplane.composition(...)), or (b) future akua publish --as <format> at distribution time. The Package itself never pre-commits to an emit format — resources is the single canonical thing it produces.


2. Imports

An import brings one of four things into scope:

import formpurposepinned by
import akua.<engine>a source-engine callable (helm, rgd, kustomize, oci)the akua CLI version
import charts.<name>a typed Helm chart dep previously added via akua add (synthetic wrapper that exposes the chart path + a pre-bound template callable)akua.toml
import pkgs.<name>a typed Akua-package dep (synthetic stub re-exporting the upstream's schemas + a pre-bound render lambda — pkgs.<name>.render(pkgs.<name>.Input{...}))akua.toml
import <name>an upstream KCL ecosystem package (e.g. import k8s.api.apps.v1 against oci://ghcr.io/kcl-lang/k8s)akua.toml
import <local/path>a local KCL module within this packagethe filesystem

Imports are resolved at build time against akua.toml (declared deps) and verified against akua.lock (digest + signature). Failed verification is a compile error. A missing pin is a compile error.

Helm-chart deps and KCL-package deps both land in [dependencies]; akua tells them apart from the manifest media type + org.kcllang.package. annotations and routes them to the right consumer (Helm via the synthetic charts. wrapper, KCL packages as direct ExternalPkg entries inside the render sandbox). Four dependency source forms are supported:

sourceakua.toml shapeuse when
OCI{ oci = "oci://ghcr.io/.../foo", version = "1.2.3" }published signed artifact (most common)
Git{ git = "https://github.com/foo/bar", tag = "v1.2.3" }non-OCI-distributed sources
Path{ path = "../shared" }workspace-local, dev-only
Helm repo{ repo = "https://go.temporal.io/helm-charts", chart = "temporal", version = "0.62.0" }classic HTTPS Helm repository

Helm-repo deps resolve against the repo's index.yaml at akua add / lock time, content-pinned by .tgz sha256 in akua.lock, and rendered deterministically offline. Add one with:

akua add temporal --repo https://go.temporal.io/helm-charts --chart temporal --version 0.62.0

For Helm charts and Akua-package deps, use the alias method on the import — the synthesized stub owns the engine call so the consumer just states the typed args:

import charts.webapp as webapp
import pkgs.upstream as upstream

resources = webapp.template(webapp.TemplateOpts {
    values = webapp.Values { replicaCount = 3 }
})
_up = upstream.render(upstream.Input { appName = "x", replicas = 3 })

The engine import (akua.helm, akua.pkg) does not appear at the call site; the stub handles dispatch.

**Engine-direct callables remain at akua.* for engines whose input is not a registered typed dep** — kustomize bases are local-to-Package file organization, not external deps, so the alias-method form doesn't apply:

Every engine callable returns [Resource], a typed list of Kubernetes-shaped resource dicts.


3. Schema — the public input contract

The Input schema declares what customers (or App resources) must provide to render this Package.

Rules:

```python import akua.ctx

input: Input = ctx.input() ```

ctx.input() is a thin wrapper around KCL's option("input") that hides the plumbing string — a typo becomes a parse error instead of a silent pass-through. input: Input = ... triggers KCL's structural coercion: the returned dict is validated against the Input schema at this binding site, defaults fill in, check: blocks run.

Under the hood ctx.input() is just option("input") or {}, so the Package remains standalone-valid KCLkcl fmt / kcl lint / IDE LSPs all work once import akua.ctx resolves (akua materializes the stdlib at render time and exposes it to KCL via ExecProgramArgs.external_pkgs).

Example with all shapes:

schema Input:
    """Public inputs for this package."""

    # Required, primitive
    appName: str

    # Required, nested schema
    routing: RoutingInput

    # Optional with default
    replicas: int = 3

    # Optional union
    tier: "startup" | "production" = "startup"

    # Optional list of nested schemas
    additional_hosts: [HostInput] = []

    # Optional dict
    labels: {str: str} = {}

    # Cross-field constraint
    check:
        replicas >= 1, "replicas must be at least 1"
        len(additional_hosts) < 10, "at most 10 additional hosts"

schema RoutingInput:
    hostname: str
    tls:      bool = True
    issuer:   str  = "letsencrypt-prod"

schema HostInput:
    hostname: str
    priority: int = 0

UI hints (optional) ✅

When a Package is consumed through a UI (merchant install form, Package Studio, generated Swagger form), renderers benefit from hints about field ordering, labels, placeholders, grouping. akua reads UI hints from two sources, both projected into the JSON Schema / OpenAPI output of akua export.

KCL docstrings — the field's """…""" docstring becomes the schema property's description:

schema Input:
    """Public inputs for this package."""

    appName: str
    """Application name. Lowercase, hyphen-separated."""

    hostname: str
    """Public hostname. Must be a valid RFC 1123 DNS name."""

    replicas: int = 3
    """Number of replicas. Minimum 1 in production."""

@ui(...) decorators — for ordering / grouping / widget hints docstrings can't carry. The decorator's keyword arguments are projected onto the JSON Schema property as the OpenAPI-3.1-compliant x-ui extension; renderers that recognise it (rjsf, custom form UIs) consume the hints, renderers that don't, ignore them.

schema Input:
    @ui(order=10, group="Identity")
    appName: str

    @ui(order=20, group="Identity", placeholder="app.example.com")
    hostname: str

    @ui(order=30, group="Capacity", widget="slider", min=1, max=20)
    replicas: int = 3

@ui(...) is an akua-specific authoring hint, not a registered KCL decorator — akua render strips it before handing the source to KCL's resolver, while akua export extracts it from the parsed AST.

Exporting a view vs rendering ✅

The canonical Package is KCL. akua ships two different verbs producing different outputs from it:

verbpurposeneeds inputs?output
akua exportconvert the Package's Input schema to a standard interchange formatnoJSON Schema 2020-12 or OpenAPI 3.1
akua renderexecute the Package's full pipeline and produce deploy-ready Kubernetes manifestsyesrendered YAML the reconciler applies

For install UIs, API docs, rjsf / JSONForms, admission webhook schemas, and client SDK generators — akua export skips engine invocation and customer inputs:

akua export --package package.k > inputs.schema.json              # JSON Schema 2020-12
akua export --package package.k --format=openapi > package.openapi.json

For actual deployment rendering — use akua render with customer inputs (covered in §9).

akua export output is pure, spec-compliant JSON Schema 2020-12 / OpenAPI 3.1. Docstrings become description; @ui(...) decorators become x-ui metadata. Consumers that speak these standards — including every JSON Schema tool in the ecosystem — work unchanged.

No x-user-input or x-input markers. Previous versions of akua layered custom extensions on JSON Schema to mark user-configurable fields and embed transforms. With KCL as the authoring substrate, both are redundant: the Input schema IS the customer-configurable contract by definition, and transforms live as KCL code in the package body. The eventual exported JSON Schema is standards-pure; UI renderers in the broader ecosystem don't need to learn akua-specific vocabulary.


4. Body — engine calls + transforms

The body composes resources by calling engine functions and optionally transforming their output.

Engine calls return typed resource lists. Common patterns:

# Helm with per-source input mapping (no chart fork needed to rename values)
_pg = helm.template(cnpg.Chart {
    values = cnpg.Values {
        cluster.name      = "${input.appName}-pg"
        cluster.instances = 3
        bootstrap.initdb.database = input.appName
    }
})

# Helm with postRenderer — per-resource transformation
_app = helm.template(webapp.Chart {
    values = webapp.Values { replicaCount = input.replicas }
    postRenderer = lambda r: dict -> dict {
        r.metadata.labels |= {"team": input.team}
        r
    }
})

# kro RGD — compile-time instantiation (offline, deterministic)
_glue = rgd.instantiate(platform_glue.RGD, {
    metadata.name: input.appName
    spec.domain:   input.routing.hostname
})

# Kustomize base
_addons = kustomize.build("./overlays/monitoring")

Aggregating results: concatenate with [*a, *b, ...]. Add extra resources declared in KCL:

_servicemonitor = {
    apiVersion: "monitoring.coreos.com/v1"
    kind:       "ServiceMonitor"
    metadata.name: input.appName
    spec.selector.matchLabels.app: input.appName
}

resources = [*_pg, *_app, *_glue, *_addons, _servicemonitor]

Schema-level validation via check: blocks — this is KCL's role in the two-layer validation model (schema → Rego for cross-resource policy, see policy-format.md):

schema Deployment:
    spec: DeploymentSpec
    check:
        spec.replicas >= 1, "must have at least one replica"
        "app.kubernetes.io/name" in spec.template.metadata.labels,
            "every deployment must carry the app.kubernetes.io/name label"

KCL check: blocks evaluate at render time against each resource; failures surface as lint errors with line + field context.


5. The render output

akua render --out ./deploy writes every entry in resources as its own YAML file in ./deploy/. Filenames are deterministic (<NNN>-<kind>-<name>.yaml), ordered by resource-list position.

deploy/
├── 000-configmap-hello.yaml
├── 001-service-hello.yaml
└── 002-deployment-hello.yaml

Raw manifests are akua's single render shape. Downstream systems that want a different shape use one of:

consumes resources and returns more K8s resources. kro.rgd(...), crossplane.composition(...), kyverno.policy(...) all fit this mould: they produce CRDs + composite resources that go into resources alongside everything else, and ship as plain YAML.

wraps rendered manifests into a Helm chart at distribution time; akua publish --as oci-bundle signs and packages them. These are distribution concerns, not render concerns — the Package's resources are the input, not a pre-declared output list.

This keeps the Package shape trivially uniform: one resources list, one render target. Authors reason about what exists; the CLI decides how it ships.


6. Metadata

Optional top-level metadata that akua tooling surfaces in inspect, diff, and publishing:

metadata = {
    name:        "payments-api"
    version:     "3.2.0"
    description: "Checkout + payments processing with managed Postgres"
    publisher:   "github.com/acme/payments"
    license:     "Apache-2.0"
    homepage:    "https://github.com/acme/payments"

    # Machine-readable keyword list for catalog discovery
    keywords: ["postgres", "webapp", "payments"]

    # Minimum akua version required to render this package
    requires: {
        akua:    ">=0.2.0"
        engines: { helm: ">=4.0", kcl: ">=0.12" }
    }
}

All fields optional; missing fields default to package-name-and-version derived from akua.toml. Publishing may require a license field depending on registry rules.


7. What's disallowed

Because determinism and the WASI sandbox are load-bearing:

Violation of any of these is a compile error with a clear message.


8. Rendering model

akua render:

  1. Parses package.k and type-checks the program.
  2. Loads input from inputs file (YAML or KCL). Validates against the Input schema.
  3. Resolves dependencies via akua.toml / akua.lock. Pulls and verifies signed artifacts.
  4. Evaluates the KCL program. Every engine call happens here (in-process, sandboxed).
  5. Collects the resources list — pkg.render calls have already resolved inline.
  6. Writes each resource as its own YAML file under --out.
  7. (Future) Writes attestation.json (SLSA v1 predicate) alongside the manifests.

Every step is deterministic: same inputs + same akua.lock + same akua version → byte-identical output.


9. Minimal example

The smallest possible Package:

# package.k
import akua.ctx
import akua.helm
import charts.nginx as nginx

schema Input:
    hostname: str

input: Input = ctx.input()

_nginx = helm.template(helm.Template {
    chart  = nginx.Chart
    values = nginx.Values {
        ingress.hostname = input.hostname
    }
})

resources = _nginx

See examples/01-hello-webapp for the fully runnable version.


10. Testing Packages

Packages ship with tests. The test runner is built into akua test; no separate framework required.

Test file conventions

Example — schema defaults

# test_schema.k
import package as pkg

# Required fields are satisfied by sample inputs
_default_sample = pkg.Input {
    appName:  "test"
    hostname: "test.example.com"
}

# Assert default values
assert _default_sample.replicas == 3, "default replicas should be 3"
assert _default_sample.database.user == "app", "default db user should be 'app'"

Example — render-output test

# test_rendered.k
import akua.test as test
import package as pkg

# Render the package with specific inputs
_rendered = test.render(pkg, {
    appName:  "checkout"
    hostname: "checkout.example.com"
    replicas: 5
})

# Find the Deployment and assert its shape
_deployment = [r for r in _rendered if r.kind == "Deployment" and r.metadata.name == "checkout"][0]
assert _deployment.spec.replicas == 5, "replicas should flow through"
assert _deployment.spec.template.metadata.labels["team"] == "payments"

Golden-output tests

When you want to pin the exact rendered YAML (catching unintended changes from dep bumps), add a test.golden.yaml alongside inputs:

tests/
├── basic/
│   ├── inputs.yaml
│   └── expected.golden.yaml
└── production/
    ├── inputs.yaml
    └── expected.golden.yaml
akua test --golden              # regenerate goldens if they drifted intentionally
akua test --golden=verify       # fail CI if goldens don't match (default in CI)

Running

akua test                       # runs everything, including Rego tests
akua test --watch               # re-runs on file change (ideal for TDD)
akua test --coverage            # report per-schema / per-source coverage
akua test --filter=default      # only tests matching 'default'

Tests run via the embedded KCL engine (see embedded-engines.md) — fast, sandboxed, deterministic.

What to test

Packages without tests ship with a lint warning; platform teams can enforce a policy rule requiring tests for production-tier packages.


11. Relationship to other docs