M7-15: OpenTofu module generic-vm¶
Status: ready | Depends on: M7-29 | Milestone: M7
Goal¶
After this ticket the module infra/tofu/modules/generic-vm sets up a Linux host for Bothy from its inputs (ADR 0012, infra/tofu/README.md): it renders cloud-init user data that installs Docker Engine and the Compose plugin from the Docker package repositories, creates the user bothy and the stack directory, downloads the product bundle of bothy_version with its signature and checksum, verifies them (18 6), runs the setup wizard install.sh --bundle without a terminal with options derived from the inputs (the same path as every installation, 16 3.1), installs a system service that starts the stack at boot, and returns the outputs of the module contract. Secrets are never inputs, outputs or part of the rendered user data; they are generated on the host by the wizard (infra/tofu/README.md rule 1). tofu validate is green, and a test boots a local VM without any cloud cost (QEMU with a pinned cloud image and a cloud-init seed), applies the user data, and passes the smoke test against the stack started from the local trial release. Why: the cloud modules (M7-16) only create network, host and storage and then call this module; the host setup must be one tested path, and portability means the same images on any host (16 1 goal 2).
Read first¶
infra/tofu/README.md(structure, outputs, rules 1 to 5, "Checked"),infra/tofu/modules/generic-vm/README.mdandvariables.tf(the shared inputs andssh_host,ssh_user; "Open": the tool choice).docs/spec/16-operations.md: 1, 3 (targets, rule 7), 3.1 (options ofinstall.sh), 4.1 to 4.3 (edges), 5.3, 11, 14;docs/spec/18-release.md2 (bundle), 2.1, 6 (verification of files).docs/adr/0012-oci-images-compose-opentofu.md,docs/operations/install-compose.md,docs/operations/cloud-migration.md,docs/OPEN-QUESTIONS.mdOQ-09.tools/bin/tools.lock.json(tofuis pinned),docs/quality/toolchain.md,Makefile(trivyscans Terraform files).docs/DECISIONS.md: D-W-100 and its extension to M7 (D-W-125), the trial release (D-W-127); section 10 (format ofD-M7-15-<n>rows).- Briefs (merged names win, D-W-100):
M7-29-setup-wizard.md(install.shoptions,--bundle, the product trial),M7-10-server-release-chain.md(trial registry, verification commands),M1-73-compose-smoke.md(infra/compose/smoke.sh).
This ticket touches no table, no action and no event.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them. Use a hyphen or rephrase. - White label: examples use
chat.example.com, the owneralex; no company, host or personal names, no real IP addresses outside the documentation ranges. - Provider versions, the cloud image and every tool are pinned (checksum or digest); Renovate keeps them current.
- Specification first: a change to the module contract updates
infra/tofu/README.mdand the module README in the same change. The core contract is not edited by this ticket. Everything this ticket adds is English. - No cost (D-W-125): no cloud account; the VM test runs locally on the build host (D-W-127).
- Never tag, never create a release, never publish, never apply against a provider account (D-W-125).
Product requirements implemented here:
- No secret in inputs, outputs, state or rendered user data; a test scans the rendered user data and the plan output for every secret name of the catalogue and for key material.
- The bundle and its signature are verified on the host before anything is unpacked; a wrong checksum or signature stops the setup.
- The host setup calls
install.sh --bundlewith options derived from the inputs, so the module never writes.envorsecrets/itself;edge_modemaps to the options of 16 3.1 (directbehind the host's reverse proxy,tailscale, or no public port forprivate). - Only the edge port is public (
infra/tofu/README.mdrule 3); the rendered firewall rules or Compose publishing say so. - A second apply on the same host changes nothing (the wizard's rule 1 and idempotent cloud-init parts).
Scope¶
infra/tofu/modules/generic-vm/:main.tf,outputs.tf,versions.tf(pinned providers, for examplecloudinitandnullorterraform_data), templates of the user data and the system service, the README (inputs, outputs, the tool choice of "Open").infra/tofu/test/generic-vm/: a root module for the test and the VM runner (Design note 2).Makefile: targettofu-check(tofu fmt -check,tofu init -backend=false,tofu validateover a root that calls all four modules; inmake check, offline with a provider mirror or a cached plugin directory as the toolchain allows) and targettofu-vm-test(the local VM test, outsidecheck)..github/workflows/ci.yml: no job for the VM test.make tofu-vm-testruns only on the build host, because the product trial it starts refuses to run underGITHUB_ACTIONS(D-W-127); CI covers the modules throughtofu-checkinmake check.- Docs:
infra/tofu/README.md(status column, "Checked"),docs/operations/cloud-migration.mdwhere it names the module,docs/quality/gates.md,CHANGELOG.mdunder[Unreleased],docs/DECISIONS.mdsection 10 rowsD-M7-15-<n>.
Out of scope¶
- The cloud modules
hetzner,aws,azure(M7-16) and any live test in a cloud (OQ-09). - A managed database or object storage; Kubernetes (V3).
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M7-15-tofu-generic-vm; one writer per worktree. - Conventional Commits with the component as scope (for example
feat(tofu): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag, never publish an image or a release, never create remote repositories or cloud resources; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules). The VM uses user-mode networking; forwarded ports bind only
127.0.0.1on random ports. - No network calls except downloads of pinned tools, the pinned cloud image (verified by checksum) and the package repositories the user data names inside the VM.
- Stop only processes you started (the VM by PID). Temporary directories only via
mktemp -d, removed at the end with the VM disk. - Never read or print secrets.
- QEMU must be available; run
make tofu-vm-testonce on the build host (install QEMU with a package manager if it is missing), or stop withVerdict: BLOCKED qemu missing. - Never lower a threshold, disable a gate or delete a test.
- If the core contract lacks something this ticket needs, stop with
Verdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M7 by D-W-125). Where the merged code of an earlier ticket names a package, type, function, file or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Tool choice ("Open" of the module README; Ruling for docs/DECISIONS.md). New hosts get cloud-init user data (the cloud modules pass it to their VM resource); existing hosts get the same script over SSH from a
terraform_dataprovisioner withssh_hostandssh_user. One script, two delivery paths. -
Local VM test (OQ-09). Run the trial release of the product (M7-29) on the build host, serve bundle, signature and
install.shon a local static server, render the user data with the test root (download address pointed at the host side of QEMU user networking), boot a pinned Ubuntu or Debian cloud image with a NoCloud seed in QEMU (KVM or HVF where available, TCG otherwise; record the time), wait for cloud-init, then run the smoke test of M1-73 against the forwarded port. The VM pulls images from the trial registry on the host through user networking; configure that registry as insecure only inside the test VM and record it. A second apply run inside the VM changes nothing. -
Rulings for
docs/DECISIONS.md(D-M7-15-<n>): tool choice (note 1), VM runner and image pin (note 2), the offlinetofu-checkapproach.
Tests (write them first)¶
tofu validateover the test root that calls all four modules (make tofu-check).- A script test (Python or shell) that renders the user data for each
edge_modeand asserts: no secret names or key material, the verification step before unpacking, the options passed toinstall.sh, only the edge port public. make tofu-vm-test: the VM comes up, the stack is ready, the smoke test passes, a second run changes nothing; a bundle with a wrong checksum stops the setup.
Commands that must pass, from the repository root:
make tofu-checkmake tofu-vm-teston the build hostmake trivy,make lint,make runners-check,make docs,make linkcheckmake check
Acceptance criteria¶
-
tofu validateis green;make tofu-checkruns inmake check. - A test on a local VM without cloud costs starts the stack and passes the smoke test.
- No secret in inputs, outputs, state or user data; bundle and signature are verified before unpacking.
- READMEs,
gates.md, CHANGELOG and the rulings are updated. -
make checkis green.
Definition of done¶
- Code and tests as specified; comments explain why; every variable and output has a description.
- Module README and
infra/tofu/README.mdupdated in English. - Rulings of Design note 3 recorded in
docs/DECISIONS.mdsection 10. CHANGELOG.mdentry under 'Unreleased'.- An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make checkand ofmake tofu-vm-testwith the acceleration used and the duration. - Review and red team verdicts and the findings fixed.
- The rendered user data of one
edge_mode(without values that would identify a host). - What is not proven (real cloud hosts).
- Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.