diff --git a/docs/prs/2026-09-03-PR#18-remote-host-layout.md b/docs/prs/2026-09-03-PR#18-remote-host-layout.md new file mode 100644 index 0000000..612aeb2 --- /dev/null +++ b/docs/prs/2026-09-03-PR#18-remote-host-layout.md @@ -0,0 +1,98 @@ +> **WARNING:** This document describes only the change applied in this PR. +> It may already be outdated once the next PR is merged. +> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation. + +## The Problem + +`tools/remote` grew with the Hostsharing Managed Webspace rollout (steps 21 and 23) and encoded that rollout's layout as if it were the only one. +The watched repository had to be `$WERKATOR_PATH/werkator`, the runtime `$WERKATOR_PATH/.werkator/werkator`, and the systemd unit was spelled out as `werkator-werkator.service`; a werkdock binary and a bwrap rootfs archive were built and uploaded unconditionally. +`vm4006`, the Docker host that has been running Werkator since long before the script existed, matches none of that: its watched repository is `~/hs.hsadmin.ng` (so its unit is `werkator-hs.hsadmin.ng.service`), its runtime lives in `~/opt/werkator`, and it has neither werkdock nor a rootfs because its builds run in Docker. +So that host could only be deployed by hand, and had drifted nine releases behind — the question that started this PR was whether the Werkdock and multi-repo work had broken it, which it had not: what was broken was the deployment tooling. + +The second problem surfaced while deploying: `instance-update` stopped the systemd unit and *then* started the upload. +The 66 MB transfer to `vm4006` died with `scp: Connection closed`, leaving the host with no running Werkator and nothing new to start. + +## Non-Goals + +- `instance-start` for a Docker host: it places a Hostsharing `.htaccess` into a `doms//` docroot, which only exists on a Managed Webspace. `vm4006` uses the managed nginx container instead, and keeps its existing units. +- `repo-init` for `vm4006` — the repository has been cloned and configured there for months; only `instance-update` was needed. +- Making the machine configuration of `vm4006` current: it still carries the pre-rename `gitTally:` meta key, which is read by nothing today. Harmless while `ConfigVersions.FORMAT_BROKE_IN` is empty, a trap on the day it is not. + +## The Scenarios + +### Feature: one deployment command for every host layout + +#### Background + +- The env file carries transport values only; the three new keys describe *where* things are on the host, not what Werkator does. +- The defaults are exactly the layout `instance-install` creates, so an env file naming none of them resolves as before. + +#### Scenario#18.01: A host that predates the script can be deployed with it + +So that an installation is not condemned to hand-typed `scp` sequences because it was set up before the tooling existed. + +- **Given** an env file with `WERKATOR_REPO_DIR=hs.hsadmin.ng`, `WERKATOR_INSTALL_DIR=/home/tallyman/opt` and `WERKATOR_SANDBOX=docker` +- **When** `tools/remote --env-file .env.vm4006 werkator instance-update` runs +- **Then** it addresses `werkator-hs.hsadmin.ng.service`, unpacks into `~/opt`, and uploads neither a werkdock binary nor a rootfs archive + +##### Verified by + +- Live on `vm4006` (2026-09-03): `check-prerequisites` reported the Docker daemon instead of running `werkdock doctor`; the update swapped `~/opt/werkator` from v1.0.1 to **v1.1.2**, the unit came up `active`, `/api/watcher` polls without errors, and `/api/system` still reports real disk figures (`diskSource.kind: volume`) rather than the quota shape from PR#16. + +#### Scenario#18.02: The webspace hosts are unaffected + +So that making the script layout-aware does not break the deployment path that is actually in production. + +- **Given** `.env.mih09` and `.env.mih34`, neither naming any of the new keys +- **When** the layout is resolved +- **Then** repository directory, install directory, unit name and sandbox are identical to the hardcoded values they replace + +##### Verified by + +- Resolution measured for both env files (2026-09-03): `REPO_DIR=$WERKATOR_PATH/werkator`, `INSTALL_DIR=$WERKATOR_PATH/.werkator`, `UNIT=werkator-werkator.service`, `SANDBOX=bwrap`. +- Live on `mih09` (2026-09-03): a full `instance-update` ran through the bwrap path — werkdock uploaded, `werkdock 0.1.0-dev` reported after the swap, service `active`, `https://werkator.javagil.de/` answering 200. + +#### Scenario#18.03: A failed transfer does not take the service down + +So that a dropped connection costs the upload and nothing else. + +- **Given** an instance whose service is running +- **When** the runtime bundle cannot be transferred +- **Then** the service is still running, because the upload happens before the stop, and a partially transferred file is never moved into place + +##### Verified by + +- The failure itself on `vm4006` (2026-09-03), which is what this scenario is written from: with the old order, `scp: Connection closed` left the unit stopped and the host without Werkator. +- Live on `mih09` (2026-09-03): the bundle already on the host was recognised by its sha256 and skipped, so the upload step cost nothing and the stop followed only after it. + +## The Solution + +Three optional env keys replace three hardcoded assumptions. +`WERKATOR_REPO_DIR` and `WERKATOR_INSTALL_DIR` are resolved absolute-or-relative-to-`WERKATOR_PATH`, and the unit name is now *derived* from the repository directory the way `SystemdServiceFiles.unitName` derives it (basename, every character outside `[A-Za-z0-9_.-]` replaced by a dash) instead of being spelled out — one rule, in two places, with the script naming the Kotlin function it mirrors. +`WERKATOR_SANDBOX=docker` skips everything bwrap-shaped: no werkdock build, no werkdock upload, no rootfs archive, and `check-prerequisites` asks the Docker daemon instead of running `werkdock doctor`. +`repo-add` clones beside the watched repository rather than into `WERKATOR_PATH`, which is the same directory whenever the default layout is used. + +`deploy_instance` is split into `upload_instance_artifacts` and `swap_instance_runtime`, and `instance_update` calls the first *before* stopping the unit. +Every artifact is uploaded to `.part`, compared by sha256 with the local file, and only then moved into place; three attempts, and an unchanged artifact is skipped entirely. +The checksum is not belt-and-braces: a truncated tarball would unpack into a runtime that starts and misbehaves, which is far worse than the failed transfer it came from. + +## Open Questions + +- **Should `instance-start` learn the Docker-host shape too?** Not answered here. `vm4006` keeps its existing units and its managed nginx; the day it needs regenerating, `init --systemd` on the host is the documented path. + +## Additional Changes + +- None beyond the feature itself. + +## Follow-up work discovered while deploying this + +- `https://vm4006.hostsharing.net:8443/` is not reachable from outside the host, while `https://127.0.0.1:8443/` answers 200 and the `werkator-nginx-hs.hsadmin.ng` container publishes both ports. Pre-existing and unrelated to this PR — the host's own firewall, not Werkator. +- The machine configuration on `vm4006` still declares `gitTally: version: since: "0.9.20"`; the current code reads only `werkator:`, so the file's version claim is silently ignored (`werkator.version.since` prints empty). It costs nothing while `ConfigVersions.FORMAT_BROKE_IN` is `""`, and stops protecting that host the moment plan step 18 sets it. + +## Prerequisite PRs + +- None; it changes only `tools/remote` and the documentation. + +## Follow-up PRs + +- None planned.