Kurel Packages

Design: Kurel Package Spec

Status: Final | Issue: #36

VersionDateSummary
1.12026-05-14Complete §6 (parameter syntax — Option A); fix GVK references; remove backup from Phase 1 trait table; fix §5 diagram label
1.02026-04-19Initial 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

my-app/
├── kurel.yaml        # package identity and parameter schema
├── app.yaml          # launcher Application (launcher.gokure.dev/v1alpha1)
└── examples/
    ├── production.yaml   # example values for a production deployment
    └── staging.yaml      # example values for a staging deployment

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.

apiVersion: launcher.gokure.dev/v1alpha1
kind: Package
metadata:
  name: webservice        # package identifier
  version: "1.0.0"        # semver
  description: "A stateless web service with ingress, TLS, and optional autoscaling."
  # Future: home, keywords, maintainers (informational only)
spec:
  parameters:
  - name: image
    type: string
    required: true
    description: "Container image with tag, e.g. registry/app:v1.2.3"
  - name: domain
    type: string
    required: true
    description: "Primary hostname, e.g. app.example.com"
  - name: replicas
    type: integer
    required: false
    default: 1

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

apiVersion: launcher.gokure.dev/v1alpha1
kind: Application
metadata:
  name: my-app
spec:
  components:
  - name: web
    type: webservice        # must match a registered ComponentHandler
    properties:
      image: myregistry/myapp:latest
      port: 8080
      replicas: 1
    traits:
    - type: expose          # must match a registered TraitHandler
      properties:
        rules:
        - host: my-app.example.com
          paths:
          - path: /
            port: 8080

4.2 Supported component types (Phase 1)

Migrated from crane. Each type maps to a ComponentHandler implementation in pkg/oam/builtin/:

typedescription
webserviceLong-running HTTP service: Deployment + Service
workerLong-running background worker: Deployment (no Service)
cronjobScheduled task: CronJob
postgresqlPostgreSQL instance (CNPG)
helmchartFluxCD 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.
daemonsetDaemonSet 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.
statefulsetStatefulSet for ordered, persistent workloads
passthroughGeneric 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.
crdEmits 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.
manifestsEmits 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.
ociReconciles 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/:

typerequires capabilitydescription
exposeyes — controllerTypeDispatches to ingress or httproute based on platform
ingressnoKubernetes Ingress
httproutenoGateway API HTTPRoute
certificateyes — issuerRefcert-manager Certificate
external-secretyes — secretStoreRefExternalSecrets ExternalSecret
configmapnoConfigMap with optional volume mount
scalernoHPA + 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.

spec:
  # ...
  policies:
  - name: resource-limits
    type: env-policy
    properties:
      # crane-style EnvironmentPolicy fields
      enforced:
        maxReplicas: 5

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

┌─────────────────────────────────────────────────────────────────┐
│  Application team provides:                                     │
│  - app.yaml (launcher Application — what to run, what capabilities)  │
│  - values  (image, replicas, domains — per deployment)          │
└─────────────────────┬───────────────────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────────────────┐
│  kurel build                                                    │
│  1. Resolve application values into OAM Application             │
│  2. Load ClusterProfile (platform profile)                         │
│  3. For each trait: merge capability rendering into properties     │
│  4. Dispatch to component and trait handlers                    │
│  5. Output: static Kubernetes manifests                         │
└─────────────────────┬───────────────────────────────────────────┘
                      │
┌─────────────────────▼───────────────────────────────────────────┐
│  Platform operator provides:                                    │
│  - cluster.yaml (ClusterProfile — how traits are implemented)   │
└─────────────────────────────────────────────────────────────────┘

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.

spec:
  parameters:
  - name: image
    type: string
    required: true
    description: "Container image with tag, e.g. registry/app:v1.2.3"
  - name: replicas
    type: integer
    required: false
    default: 1
  - name: domain
    type: string
    required: true
  - name: tlsSecret
    type: string
    required: false
    default: "${name}-tls"   # may reference other parameters

Supported types: string, integer, boolean.

6.2 Placeholder syntax in app.yaml

spec:
  components:
  - name: web
    type: webservice
    properties:
      image: "${image}"          # scalar substitution — resolves to string
      replicas: ${replicas}      # scalar substitution — resolves to integer
    traits:
    - type: expose
      properties:
        rules:
        - host: "${domain}"
    - type: certificate
      properties:
        secretName: "${tlsSecret}"
        dnsNames:
        - "${domain}"

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

kurel build . --profile cluster.yaml --values values.yaml

# --set flags — scalars only
kurel build . --profile cluster.yaml \
    --set image=myregistry/app:v1.2.3 \
    --set replicas=3 \
    --set domain=app.example.com
# values.yaml
image: myregistry/app:v1.2.3
replicas: 3
domain: app.example.com

6.5 Validation

  • Missing required parameter → build error naming the parameter before any resolution
  • Optional parameter with no value → default value used
  • default may itself contain ${name} references to other parameters; these are resolved in declaration order
  • default values are type-checked at ParsePackage time; a string default that is not parseable as the declared type (e.g. default: foo for type: integer) is rejected immediately

7. Build Invocation

kurel build <package-dir> \
    --profile cluster.yaml \
    [--values values.yaml | --set key=value]

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.