Decisions
ADR 0015 — The workload binding, where the learner touches it
Architecture decision records — why it was built this way.
docs/adr/0015-learner-facing-workload-binding.mdStatus: Accepted Date: 2026-09-11
Context
ADR-0014 made scenarios run against any app, and the engine binds everything it reads or runs itself. Running a scenario against echo-server showed where the binding still stopped: at the learner's own terminal and browser.
- Scripts a learner runs by hand read
WORKLOAD_NAME, which only the engine exports. From a terminal they fell back togo-api, sobuild-image.shbuilt go-api andenable-tracing.shpatched go-api while the scenario was graded against echo-server. labctl traffic startalways loaded go-api.- Dashboards baked the app into every query, so a dashboard could only ever show the app it was installed for, and a scenario could never be active for two apps without its dashboards colliding.
/etc/hostscame from a hostname list in the binary. It knewgo-api-dev.k3d.localbut notecho-server-dev.k3d.local, and nevervault.k3d.local.
One later requirement shapes all of these: a scenario may one day be active for two apps at once. Any answer that looks up "the app this scenario is active for" becomes ambiguous then.
Decision
- Hand-run scripts take the binding as flags. Scenario scripts source
scenarios/_lib/workload.sh, which reads--appand--namespace, falls back to the engine's environment, and otherwise stops with a usage error. Commands in content are written with--app {{.WorkloadName}} --namespace {{.WorkloadNamespace}}, so the text a learner copies is already filled in.labctl validatefails a command that omits--app.labctl traffic startfollows the same rule with--app. - Dashboards select the app. A scenario dashboard declares
appandnamespacevariables that default to the activation's values, and its queries use$appand$namespace. There is one shared Application Request Metrics dashboard with a multi-select App variable, not a copy per app. - The Ingress is the source of truth for hostnames.
labctl hosts addwrites the platform hostnames, one per app underapps/, and every Ingress host in the cluster under the domain suffix. - An exercise is a field, not a sentence.
exercise: trueon a snippet replaces "APPLY THIS ONE YOURSELF" in its label, and the UI offers a ready-to-run apply command.
Alternatives considered
| Alternative | Why rejected |
|---|---|
labctl scenario env <name>, which scripts call to look up the active app |
Ambiguous once one scenario is active for two apps; adds a command whose only job is a lookup; hides from the learner which app a command acts on |
Environment variables in front of each command (WORKLOAD_NAME=… bash …) |
Long, exposes internal names, and dropping one variable silently reintroduces the go-api fallback |
labctl scenario exec <name> -- script.sh |
Hides the script the learner is meant to read, and adds interactive streaming to the run engine for no grading benefit |
| One provisioned dashboard per app | Cannot compare apps side by side, and needs its own create and delete lifecycle whenever an app is added or removed |
| Wildcard DNS for the domain suffix | /etc/hosts has no wildcards; a resolver needs a local DNS daemon or a different domain, which is a larger change for every existing lab |
Consequences
- A command copied from a README without flags fails with a usage error naming the fix, instead of acting on the wrong app.
- The App selector lists apps that expose the contract's request metric, plus
the activation's own app whatever it exposes. The shared dashboard queries the
semconv name directly, so an app declaring a different
APP_REQUEST_METRICdoes not appear there. hosts addstill needs a re-run when a scenario adds a hostname;scenario upandapp deploynow say when that is.- Parallel activation remains unbuilt. These choices do not have to be revisited for it.