Kurel Packages
Design: Kurel Package Spec
Status: Final | Issue: #36
| Version | Date | Summary |
|---|---|---|
| 1.1 | 2026-05-14 | Complete §6 (parameter syntax — Option A); fix GVK references; remove backup from Phase 1 trait table; fix §5 diagram label |
| 1.0 | 2026-04-19 | Initial draft — parameter syntax section omitted pending decision |
1. Purpose
A kurel package is a distributable, reusable OAM application pattern. It bundles:
- an OAM Application document (
app.yaml) describing what workloads to run and what platform capabilities they need - a package metadata file (
kurel.yaml) with identity and parameter declarations - optionally, example value files for common deployment scenarios
Packages are designed to be shared: a team defines a webservice-with-ingress package
once and any project instantiates it by supplying their image, domain, and values.
2. Package Directory Layout
The OAM Application format replaces the prototype’s parameters.yaml + resources/ + patches/
layout. No coexistence or backward-compatible bridging is required.
3. kurel.yaml
kurel.yaml declares the package identity and the parameter schema.
4. app.yaml — OAM Application
app.yaml is a launcher Application document (launcher.gokure.dev/v1alpha1, kind
Application). It contains ${var} parameter placeholders that the resolver substitutes
using the values supplied at build time. The component and trait types must match the
handler registry that the runtime is configured with. See docs/oam/design-gvk.md for
the GVK rationale and docs/oam/options-param-syntax.md for the parameter syntax spec.
4.1 Basic structure
4.2 Supported component types (Phase 1)
Migrated from crane. Each type maps to a ComponentHandler implementation in
pkg/oam/builtin/:
| type | description |
|---|---|
webservice | Long-running HTTP service: Deployment + Service |
worker | Long-running background worker: Deployment (no Service) |
cronjob | Scheduled task: CronJob |
postgresql | PostgreSQL instance (CNPG) |
helmchart | FluxCD HelmRelease for third-party charts. Supports inline source creation (source.url) and reference to existing source CRs (source.name). Multiple components sharing the same source key share a single source CR (first component wins). For HelmRepository the key is the URL; for OCIRepository the key is URL+version, so two OCI components with the same URL but different versions each get their own source CR. |
daemonset | DaemonSet for node-level agents. Optional port: <N> generates a ClusterIP Service exposing port N; required when the daemonset acts as an implicit backend for ingress, httproute, or expose traits. |
statefulset | StatefulSet for ordered, persistent workloads |
passthrough | Generic escape hatch (launcher-native, not migrated from crane): emits an arbitrary Kubernetes object — CRD or non-standard type — declared inline under object:. Set clusterScoped: true for cluster-scoped resources (no namespace injected). object.metadata.name defaults to the component name. For namespaced objects object.metadata.namespace defaults to the build namespace when unset, but an inline value is respected (intentional cross-namespace). No standard trait/port integration or auto health check. |
crd | Emits CustomResourceDefinition manifests from a multi-doc YAML source — inline: (offline) or url: (http/https only; oci:// not yet supported). Rejects any non-CRD document. Emitted CRDs are auto-staged early by stack-compile’s CRD inference. URL hosts are constrained by the policy registry allowlist (AllowedRegistries), re-checked on every redirect. |
manifests | Emits arbitrary Kubernetes manifests from the same sources as crd (inline / url). Each object’s scope is resolved (built-in kinds plus any CRD in the same source); namespaced objects that omit metadata.namespace are stamped with the build namespace, cluster-scoped objects are left untouched, and an unknown-scope object with no namespace fails closed. Optional scopeOverrides: [{apiVersion, kind, scope: Cluster|Namespaced}] supplies an explicit scope for a kind whose scope is otherwise unknown — e.g. a cluster-scoped custom resource (a namespace-less ClusterIssuer) whose CRD is installed out of band; overrides apply only to unknown-scope objects, so they cannot contradict a known scope, and the fail-closed default is kept for unknown kinds with no override. Same URL allowlist as crd. |
oci | Reconciles an OCI artifact: emits an OCIRepository source CR plus a per-component Flux Kustomization. Properties: source.url (required, oci://), version (required; a tag, or sha256:<digest>), path (default ./), prune (default true), interval (default 60m), targetNamespace (optional). The OCIRepository participates in source dedup keyed on URL+version (shared with helmchart OCI sources, first component wins); the Kustomization is always emitted, one per component. Both land in the Flux namespace. OCI registry host is constrained by the policy registry allowlist (AllowedRegistries). |
4.3 Supported trait types (Phase 1)
Migrated from crane. Each type maps to a TraitHandler in pkg/oam/builtin/:
| type | requires capability | description |
|---|---|---|
expose | yes — controllerType | Dispatches to ingress or httproute based on platform |
ingress | no | Kubernetes Ingress |
httproute | no | Gateway API HTTPRoute |
certificate | yes — issuerRef | cert-manager Certificate |
external-secret | yes — secretStoreRef | ExternalSecrets ExternalSecret |
configmap | no | ConfigMap with optional volume mount |
scaler | no | HPA + optional PDB |
Traits that remain in crane (not migrated to launcher): backup, fluxcd-postbuild,
fluxcd-patches, prune-protection, rbac. These depend on crane’s delivery pipeline
and have no meaning in a static manifest build.
4.4 OAM policies
OAM Application policies are parsed and passed to the runtime unchanged. The runtime
does not interpret any policy type in Phase 1 (policy application via Enforceable is
wired in Phase 1 but uses NoopPolicy by default). Policy handling is activated in
Phase 1 via the --policy flag.
5. Two-Parameter-Set Model
Every kurel build receives exactly two parameter sets:
Set 1 — Platform profile (--profile cluster.yaml)
Describes how the platform implements each trait. This is an environment-level input,
supplied by the platform operator and shared across all applications on a cluster.
Represented as a ClusterProfile document. See docs/oam/design-cluster-profile.md.
Set 2 — Application values
Describes what this specific deployment needs: image, replica count, domain names, etc. This is a per-deployment input, supplied by the application team at build time.
The two sets are merged at different stages:
- Platform profile rendering is merged into trait properties before handler invocation (capability resolution, see ClusterProfile design)
- Application values are merged into component and trait properties
(
${var}placeholder substitution — see §6)
Separation of concerns
6. Parameter Syntax
Application values are expressed as ${name} placeholders in app.yaml. The resolver
substitutes all placeholders using the values supplied at build time before the Application
is parsed or dispatched to handlers. For the full design rationale see
docs/oam/options-param-syntax.md.
6.1 Parameter declarations in kurel.yaml
Each parameter has a name, type, required flag, optional default, and optional description.
Supported types: string, integer, boolean.
6.2 Placeholder syntax in app.yaml
6.3 Resolver behaviour
Scalar substitution — when ${name} is the entire value of a YAML field, the resolver
replaces it with the typed value from the parameter declaration:
image: "${image}"→image: "myregistry/app:v1.2.3"(string)replicas: ${replicas}→replicas: 3(integer, not string"3")
Inline string embedding — when ${name} is embedded inside a larger string value:
secretName: "${name}-tls"→secretName: "webservice-tls"(always a string)
6.4 Supplying values at build time
6.5 Validation
- Missing required parameter → build error naming the parameter before any resolution
- Optional parameter with no value → default value used
defaultmay itself contain${name}references to other parameters; these are resolved in declaration orderdefaultvalues are type-checked atParsePackagetime; a string default that is not parseable as the declared type (e.g.default: foofortype: integer) is rejected immediately
7. Build Invocation
Output is static Kubernetes manifests on stdout (YAML, multi-document). Pipe into
kubectl apply, a GitOps repo, or a CI artifact store.
The --profile flag is required. Without a profile, capability-aware traits will fail
with ErrMissingCapability.