rune cast

Install or upgrade a release from YAML files, directories, runeset bundles, or remote URLs — plan, confirm, apply.

Aliases: apply.

cast is the universal "apply this spec" command. It accepts:

  • A single YAML file
  • A directory of YAML files
  • A glob (services/*.yaml)
  • A runeset directory or .runeset.tgz archive
  • A remote URL or git ref
rune cast my-service.yaml
rune cast my-service.yaml --namespace=production
rune cast my-service.yaml --tag=stable
rune cast my-directory/ --recursive
rune cast services/*.yaml
rune cast github.com/org/repo/path@ref --create-namespace
rune cast https://example.com/runeset.tgz --release=my-release
rune cast ./runeset.tgz --release=my-release
rune cast ./runeset --render --set=key=value
rune cast ./runeset --render --values=values.yaml

Every cast is a tracked release. There is no fire-and-forget path. A loose service.yaml is treated as an inline runeset and gets a release named after the file (with a warning); pin it with --release. Operate releases afterward with rune release.

The cast pipeline

A cast is declarative — closer to terraform apply than kubectl apply. It runs:

render ─► lint ─► PLAN (3-way diff) ─► [confirm] ─► APPLY (create → update → verify) ─► PRUNE

The CLI renders and lints locally, then the server computes the plan, applies it in order, and prunes last. You see a plan block before anything changes:

Release "runesight" → revision 3   (namespace: observability)
  + create    2  (service/query-api, volume/ch-data)
  ~ update    1  (service/ingester)
  - prune     1  (configmap/old-settings)        ⚠ destructive
  ↪ adopt     0
  = reference 0

A plan that prunes or adopts asks for confirmation before applying — type yes, or pass --yes (CI and --detach skip the prompt). Pure create/update proceeds automatically.

Flags

FlagDefaultNotes
-n, --namespacefrom contextTarget / home namespace for the release.
--create-namespacefalseCreate the namespace if it doesn't exist (referenced, never owned).
--release <name>derivedRelease identity. Defaults to the runeset/file basename.
--dry-runfalseCompute and print the plan; apply nothing. See below.
--detachfalseReturn once accepted; don't wait for rollout. Create/update-only.
--yesfalseSkip the confirm prompt for plans that prune or adopt.
--adoptfalseTake over resources that are unmanaged or owned by another release.
--timeout <dur>15mRollout wait timeout for the whole release.
-r, --recursivefalseRecurse into directories.
--atomicfalseRoll back this cast's changes if it fails. A verify timeout also triggers rollback. See Updating running services.
--forcefalseAccepted but has no effect today. See Updating running services.
--tag <name>—Tag this deployment for later reference.
--renderfalsePrint rendered YAML and exit (works for inline casts too).
--set k=v—Override a value (repeatable).
-f, --values <file>—Extra values file (repeatable; last wins).
-o, --output—json — emit the structured plan + result.

--release, --render, --set, and --values are universal — they apply to loose castfiles (inline runesets), not just packaged runesets.

Plan and dry-run

--dry-run renders, lints, and prints the 3-way plan — but applies nothing. It now contacts the server to compare against the release's current state, so the plan includes prunes and adoption conflicts:

rune cast ./my-app --release=my-app-prod --values=prod.yaml --dry-run

This is the same plan rune release diff shows for an already-installed release; use --dry-run when you're casting from changed source or values. For purely static, offline validation (schema, templates, cycles) use rune lint.

Permissions

A cast needs the same verb on each resource that the direct API call would, checked in that resource's namespace (which can differ from the release's): create for a new resource, update for a changed or adopted one, delete for a pruned one. A cast that fails the check is denied before anything is written, with no pending revision. Every cast, not only --dry-run, also needs get on each resource it creates, updates, adopts or prunes, because the CLI plans before it applies (resources the release only references are skipped).

The built-in cast role (the recommended CI scope) can create and update services, configmaps, secrets and volumes, but cannot delete anything. A cast with that token that would prune a resource, including one dropped from the castfile, is denied before anything is applied:

permission denied: <subject> is not allowed to "delete" <kind> in namespace "<ns>"

<kind> is services, secrets, configmaps or volumes; together with the namespace it tells you which pruned resource tripped it. Either grant the token delete on that kind, or keep it as it is and remove the resource once with an admin token (rune delete service <name> -n prod), then re-cast. Adding --adopt doesn't get around it: adopting counts as an update, and pruning the adopted resource still needs delete. A hand-written narrow policy needs the per-kind verbs, volumes and get included.

Since v0.0.1-dev.148

Since v0.0.1-dev.148. Before that, a cast token only needs releases:create and can adopt then prune resources it could not delete directly.

Adoption

A cast that would overwrite a resource not owned by this release fails with an ownership conflict — the guardrail that keeps two releases from fighting over the same resource. Pass --adopt to take ownership (e.g. pulling pre-existing, pre-release resources under management on first cast):

rune cast ./my-app --release=my-app-prod --adopt

Updating running services

Since v0.0.1-dev.148

Since v0.0.1-dev.148.

Casting a changed service updates it in place: it keeps its identity, its generation counters and any virtual IP it has been given. Only a change to something an instance is built from (the image, env, ports, mounts, probes, security context and so on) replaces instances, and they are replaced under the service's updateStrategy, not all at once. Edits to scale, networking, ingress and similar settings apply without replacing anything. The full split is in what replaces instances.

  • Notes after the apply. Some edits move the service's generation but change nothing that is already running: labels, imagePull and imagePullAnonymous (copied onto an instance when it is created), and a claimTemplate change (existing volumes are kept as they are, so a new size or class reaches only new replicas). rune cast prints a note for each after the release summary, and -o json includes them per change. They appear after the apply only, not on --dry-run or at the confirm prompt, and a failed or denied cast prints none.
  • A service being deleted is refused. Casting onto a service that is still tearing down fails with service/<namespace>/<name> is being deleted; cast again once teardown finishes, rather than writing a spec that would be deleted with it.
  • --atomic rolls back more than it changed. The rollback puts the previous spec back and replaces instances that are older than it. Instances the failed cast never touched count as older, so a failure after replacing one of six instances rolls all six. The rollback is budgeted like any update, so availability holds, but expect more churn than the failure caused (rune#263).
  • --force does nothing yet. The flag is parsed but never sent (rune#260). To roll a service whose image tag moved but whose spec did not, run rune restart on it (imagePull defaults to always, so the replacements pull the new image).

Detach

--detach returns as soon as the cast is accepted instead of waiting for rollout. It is create/update-only: a plan that prunes refuses --detach so the destructive half is never orphaned. Drop --detach to apply a destructive plan, or inspect it first with rune release diff.

Behavior

  • Idempotent. Re-casting the same spec into the same release is a no-op plan.
  • Aggregate timeout. --timeout bounds the whole release rollout, not each service.
  • Deterministic apply. Resources apply in a stable order, then prune runs last.
  • Multi-document YAML is supported — separate documents with ---.
  • Mixed resource files (services + secrets + configmaps in one file) are fine.
  • Failures surface inline. Without --detach, cast waits for the release to become ready. If the reconciler reports Failed it stops waiting immediately, prints a short reason + the one-sentence cause + a rune get service <name> hint, and exits non-zero. The release is left failed with the prior revision live and nothing pruned.
    ✗ ImageUnreachable
      pull access denied for ghcr.io/acme/api, repository does not
      exist or may require 'docker login'
 
      image:   ghcr.io/acme/api:1.4.2
      service: prod/api
 
      rune get service api -n prod

Common patterns

# Validate first (offline, static)
rune lint manifests/ --recursive
 
# Preview the full plan against live state
rune cast manifests/ --release=app --dry-run
 
# Install / upgrade a release
rune cast ./my-app --release=my-app-prod --values=prod.yaml
 
# Non-interactive deploy in CI (structured output, skip the prompt)
rune cast ./my-app --release=my-app-prod -f prod.yaml --yes --output=json
 
# Diff a runeset's rendered YAML before installing
rune cast ./my-app --render --values=prod.yaml | diff - last-rendered.yaml

See also