fix(upgrade): refuse a pre-#385 library Console unit before units stop (#385)

This commit is contained in:
jason.woltje committed 2026-11-02 12:19:07 -06:00
1 parent d0ddfa8de2
commit 484aef7e2a
6 files changed
+153 -15

No files matched your search

+1
View File
@@ -5820,3 +5820,4 @@ Before: agents can read the library; supporting-path and push-credential depende
2026-10-29T03:27:31Z | Codex implementation sub-agent | #385 OIDC-only writer preparation on exact base 09a847edaa25b609bac7728ff83f224708a29399 | Added fail-closed CLI, programmatic and service-render refusals for `--send` without `--sso`; retained read-only header preview/proof; migrated inventoried writer tests to real in-process OIDC login/callback sessions and updated docs. Focused regression 5/5 and selected service cases 4/4 pass; syntax and diff checks pass. The broader writer suite is blocked before execution because the clean checkout has no `node_modules` and exact pinned `[email protected]` is unavailable; no install, commit, push, PR, queue move or green/completion claim. 2026-10-29T03:27:31Z | Codex implementation sub-agent | #385 OIDC-only writer preparation on exact base 09a847edaa25b609bac7728ff83f224708a29399 | Added fail-closed CLI, programmatic and service-render refusals for `--send` without `--sso`; retained read-only header preview/proof; migrated inventoried writer tests to real in-process OIDC login/callback sessions and updated docs. Focused regression 5/5 and selected service cases 4/4 pass; syntax and diff checks pass. The broader writer suite is blocked before execution because the clean checkout has no `node_modules` and exact pinned `[email protected]` is unavailable; no install, commit, push, PR, queue move or green/completion claim.
2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | CORRECTION and Mos2255 #385 review repair R2 | The 03:27:31Z dependency/no-commit statements are historical: exact pinned `[email protected]` is now materialized through the ignored `node_modules` fixture with no install, and original preparation is committed at `9936a1be1a37fdb30ba53cf2f14b7897225c3e4a`. On that unchanged HEAD, uncommitted R2 source/tests/docs make Case 2 library preview read-only until #412, drop inherited non-SSO `--send` during release planning, and refuse duplicate service `--send`. Focused RED was 0/2, 0/1, 0/1; focused GREEN and selected T53/T55/T57/T58 fixture blocks pass. No root/native/live/full-suite/CI action, commit, push, PR/API, merge or frozen/live-pin change. 2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | CORRECTION and Mos2255 #385 review repair R2 | The 03:27:31Z dependency/no-commit statements are historical: exact pinned `[email protected]` is now materialized through the ignored `node_modules` fixture with no install, and original preparation is committed at `9936a1be1a37fdb30ba53cf2f14b7897225c3e4a`. On that unchanged HEAD, uncommitted R2 source/tests/docs make Case 2 library preview read-only until #412, drop inherited non-SSO `--send` during release planning, and refuse duplicate service `--send`. Focused RED was 0/2, 0/1, 0/1; focused GREEN and selected T53/T55/T57/T58 fixture blocks pass. No root/native/live/full-suite/CI action, commit, push, PR/API, merge or frozen/live-pin change.
2026-11-02T18:03:53Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R3 on ab2bc23c7 | R2-F1: `scripts/stack-upgrade.sh` `read_args` now scans the serve argv after `--` and refuses `args-unreadable` before anything stops when the profile is library (no `--profile`, or the last one says library, as `case2-unit.sh` reads it) and `--send` is present; the message names the remediation, and the args file is never rewritten. `do_upgrade` and `do_rollback` both call `read_args` first, so rollback is covered; factory and tester paths are unchanged. RED on ab2bc23c7: new stack-upgrade case 0/1 (exit 2 rollback-failed, A4 and render-old failed, both units stopped), controls 1/1; GREEN 2/2. R2-I1: one component case runs the real `sso_for_target` then `send_for_target` on a legacy preview `--send` pointer, carried after `--sso` and before `--tester-socket`. R2-I2: TOOLS.md says a same-copy plain activate on a legacy pointer refuses already-active and needs `--no-send`. R2-I3: no code change; `uninstall`/`status` with `--send --send` exit 1, not 4, fail-closed either way. host-run-sheet section 7 gains the migration step. Whole files, one at a time: stack-upgrade 23/23 (69 s), stack-release 225/225 (608 s; a first run was cut by my 600 s background limit at 221 passing, its orphan fixture holder killed), case2-unit 48/48 (73 s). No root, systemd, container, network, push, PR or live action. 2026-11-02T18:03:53Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R3 on ab2bc23c7 | R2-F1: `scripts/stack-upgrade.sh` `read_args` now scans the serve argv after `--` and refuses `args-unreadable` before anything stops when the profile is library (no `--profile`, or the last one says library, as `case2-unit.sh` reads it) and `--send` is present; the message names the remediation, and the args file is never rewritten. `do_upgrade` and `do_rollback` both call `read_args` first, so rollback is covered; factory and tester paths are unchanged. RED on ab2bc23c7: new stack-upgrade case 0/1 (exit 2 rollback-failed, A4 and render-old failed, both units stopped), controls 1/1; GREEN 2/2. R2-I1: one component case runs the real `sso_for_target` then `send_for_target` on a legacy preview `--send` pointer, carried after `--sso` and before `--tester-socket`. R2-I2: TOOLS.md says a same-copy plain activate on a legacy pointer refuses already-active and needs `--no-send`. R2-I3: no code change; `uninstall`/`status` with `--send --send` exit 1, not 4, fail-closed either way. host-run-sheet section 7 gains the migration step. Whole files, one at a time: stack-upgrade 23/23 (69 s), stack-release 225/225 (608 s; a first run was cut by my 600 s background limit at 221 passing, its orphan fixture holder killed), case2-unit 48/48 (73 s). No root, systemd, container, network, push, PR or live action.
2026-11-02T18:19:04Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R4 on d0ddfa8de | R3-F1 fixed fail-closed, no migration: `scripts/stack-upgrade.sh` adds `legacy_console`, called right after `read_args` in `do_upgrade` and `do_rollback`. For the library profile (same default and last-wins rule as R3), an installed Console unit under the driver's UNIT_DIR whose ExecStart has a `--send` token after `serve` refuses new code `console-unit-legacy` (exit 3, nothing stopped). A missing unit is left to `read_old` (`units-unreadable`). The R3 args refusal stays. `case2-unit.sh`, factory, tester, stack-release.sh and OIDC/#412 untouched. Tests in stack-upgrade.test.mjs run the real base renderer (`git show 09a847eda` of case2-unit.sh and both templates) in the module fixture with fake getent/systemctl to make the legacy unit, prove the current case2-unit.sh refuses it, then drive upgrade and rollback with the real current renderer as A4's RENDER. Controls: read-only library unit, factory host, missing Console unit. RED on d0ddfa8de code 24/25 (the main case exits 2 rollback-failed, both units stopped), GREEN 25/25 (70.1 s). host-run-sheet section 7 now gives a read-only grep of the unit and says stop and report when it carries `--send`. TOOLS.md lists the code. No hash pins on changed files. stack-release.test.mjs and case2-unit.test.mjs read no changed file and were not rerun. One local commit, no push.
+1
View File
@@ -1882,3 +1882,4 @@ are never rewritten or removed; corrections are new entries.
2026-10-29T03:27:31Z | Codex Daybreak Blue xhigh implementation sub-agent | #385 OIDC-only Console writes, branch work/vikunja-19-385-q284-complete at exact base 09a847edaa25b609bac7728ff83f224708a29399 | Source/test/docs preparation complete for review; focused refusal 5/5 and selected service 4/4 pass, broader real-OIDC writer tests blocked by absent pinned oauth4webapi@3.8.8; no install/commit/push/PR/queue move or overall-green claim. 2026-10-29T03:27:31Z | Codex Daybreak Blue xhigh implementation sub-agent | #385 OIDC-only Console writes, branch work/vikunja-19-385-q284-complete at exact base 09a847edaa25b609bac7728ff83f224708a29399 | Source/test/docs preparation complete for review; focused refusal 5/5 and selected service 4/4 pass, broader real-OIDC writer tests blocked by absent pinned oauth4webapi@3.8.8; no install/commit/push/PR/queue move or overall-green claim.
2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | Mos2255 #385 review repair R2 on work/vikunja-19-385-q284-complete at unchanged HEAD 9936a1be1a37fdb30ba53cf2f14b7897225c3e4a/base 09a847edaa25b609bac7728ff83f224708a29399 | Corrects the 03:27:31Z historical blocker: pinned oauth4webapi3.8.8 fixture is now present without install and original prep is committed. F1/F2/F3 repaired in uncommitted source/tests/docs; F4/F7 disclosed only, F6 append conflicts preserved. Three focused test files only: intentional RED captured, GREEN controls plus T53 12/12, T55 8/8, T57 13/13 and T58 11/11 pass. No root/native/live/full-suite/CI, commit/push/PR/API/merge, frozen packet or live-pin action. 2026-10-29T05:49:53Z | Codex Daybreak Blue xhigh implementation sub-agent | Mos2255 #385 review repair R2 on work/vikunja-19-385-q284-complete at unchanged HEAD 9936a1be1a37fdb30ba53cf2f14b7897225c3e4a/base 09a847edaa25b609bac7728ff83f224708a29399 | Corrects the 03:27:31Z historical blocker: pinned oauth4webapi3.8.8 fixture is now present without install and original prep is committed. F1/F2/F3 repaired in uncommitted source/tests/docs; F4/F7 disclosed only, F6 append conflicts preserved. Three focused test files only: intentional RED captured, GREEN controls plus T53 12/12, T55 8/8, T57 13/13 and T58 11/11 pass. No root/native/live/full-suite/CI, commit/push/PR/API/merge, frozen packet or live-pin action.
2026-11-02T18:03:53Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R3 on work/vikunja-19-385-q284-complete at ab2bc23c7 | R2-F1 fixed: stack-upgrade refuses a library args file holding `--send` after `--` before any unit stops, for upgrade and rollback; R2-I1 component case and R2-I2 TOOLS.md precision added, R2-I3 noted. RED 0/1 then GREEN 2/2; stack-upgrade 23/23, stack-release 225/225, case2-unit 48/48. One local commit; no push, PR, Vikunja, Gitea or live action. 2026-11-02T18:03:53Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R3 on work/vikunja-19-385-q284-complete at ab2bc23c7 | R2-F1 fixed: stack-upgrade refuses a library args file holding `--send` after `--` before any unit stops, for upgrade and rollback; R2-I1 component case and R2-I2 TOOLS.md precision added, R2-I3 noted. RED 0/1 then GREEN 2/2; stack-upgrade 23/23, stack-release 225/225, case2-unit 48/48. One local commit; no push, PR, Vikunja, Gitea or live action.
2026-11-02T18:19:04Z | Claude Opus 5.5 implementation sub-agent (lead T3 c5c1e8ba) | #385 PR 1836 repair R4 on work/vikunja-19-385-q284-complete at d0ddfa8de | R3-F1 fixed: stack-upgrade refuses `console-unit-legacy` before any unit stops when a library host's installed Console unit runs serve with `--send`, for upgrade and rollback; host-run-sheet section 7 and TOOLS.md updated. RED 24/25 then GREEN 25/25 in stack-upgrade.test.mjs; other suites not rerun (no changed file read). One local commit; no push, PR, Vikunja, Gitea or live action.
+8 -1
View File
@@ -857,7 +857,14 @@ writes: one `case2-unit.sh` argument per line, the serve flags after `--`,
owned by root with no group or other write, exactly one `--copy`, and that owned by root with no group or other write, exactly one `--copy`, and that
copy the one both installed units run. Otherwise the run refuses copy the one both installed units run. Otherwise the run refuses
`args-unreadable` or `args-differ`. The driver writes the new copy id into `args-unreadable` or `args-differ`. The driver writes the new copy id into
that file only after `confirm` passes. that file only after `confirm` passes. For the `library` profile, an installed
Console unit whose `ExecStart` has a `--send` token after `serve` was rendered
before #385, and the current `case2-unit.sh` refuses it. `upgrade` and
`rollback` refuse `console-unit-legacy` right after reading the args file,
before any step runs, because no migration exists yet
(`docs/guides/host-run-sheet.md` section 7). A missing Console unit is not
this refusal; the later unit read refuses it `units-unreadable`, also before
anything stops.
`upgrade`, in order. Every stack user step runs as `upgrade`, in order. Every stack user step runs as
`sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from the `sudo -u USER -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin` from the
+27 -11
View File
@@ -385,21 +385,37 @@ but omits `--send`. That Console is read-only until Case 2 gains real OIDC in #4
explicitly refuses `--send`. Proxy preview and proof-protected reads remain available. The `factory` explicitly refuses `--send`. Proxy preview and proof-protected reads remain available. The `factory`
profile and its private tester socket are unchanged. profile and its private tester socket are unchanged.
A library host installed before #385 has a `--send` line after `--`, because the earlier A library host installed before #385 has `--send` in two places, because the earlier `case2-unit.sh`
`case2-unit.sh` required it. `stack-upgrade.sh upgrade` and `rollback` read the args file before required it: a `--send` line after `--` in the args file, and `--send` in the installed Console unit's
anything stops, and when the profile is `library` (no `--profile` line, or the last one says `ExecStart`. `stack-upgrade.sh upgrade` and `rollback` check both before anything stops, when the
`library`) and `--send` appears after `--`, they refuse `args-unreadable` with both units still profile is `library` (no `--profile` line, or the last one says `library`):
running. The script never rewrites the file. To migrate, as root:
1. Open `/etc/mosaic-stack/dev.args` and delete the one line that reads `--send`. Change nothing else. - `--send` after `--` in the args file refuses `args-unreadable`.
2. Check it: `stat -c '%U %a' /etc/mosaic-stack/dev.args` still gives `root` and a mode with no group or - A `--send` token after `serve` in `/etc/systemd/system/mosaic-webui.service`'s `ExecStart` refuses
other write, and `grep -cx -- --send /etc/mosaic-stack/dev.args` prints `0`. `console-unit-legacy`. The current `case2-unit.sh` refuses that installed unit, so an upgrade or
3. Rerun the same `stack-upgrade.sh` command. rollback would stop both units and then fail both renders.
Both refusals leave the units running, and the script rewrites neither file. Check the installed unit
read-only, as root:
```sh
grep -n '^ExecStart=' /etc/systemd/system/mosaic-webui.service | grep -c -- ' --send'
```
- `0`, and no installed unit carries `--send`: removing the args line is enough. Open
`/etc/mosaic-stack/dev.args` and delete the one line that reads `--send`, changing nothing else. Check
that `stat -c '%U %a' /etc/mosaic-stack/dev.args` still gives `root` and a mode with no group or other
write, and that `grep -cx -- --send /etc/mosaic-stack/dev.args` prints `0`. Then rerun the same
`stack-upgrade.sh` command.
- `1` or more: the host needs a reviewed migration of the unit, the args file and the release pointer
together, and none exists yet. Stop. Leave the units running, change neither file and report to the
lead with the command's output. Removing only the args line does not help: the run then refuses
`console-unit-legacy`.
The Console stays read-only after the upgrade, until #412 (#385). The Console stays read-only after the upgrade, until #412 (#385).
Precheck line: none. `stack-upgrade.sh` refuses `args-unreadable` on any of these rules, before it stops Precheck line: none. `stack-upgrade.sh` refuses `args-unreadable` or `console-unit-legacy` on any of these
a unit. rules, before it stops a unit.
## 8. Enrollment, once per host ## 8. Enrollment, once per host
+87 -2
View File
@@ -5,14 +5,15 @@
// stack-release-fixture.mjs's. Root's sudo, case2-unit.sh render and row 240 hostcheck are stand-ins // stack-release-fixture.mjs's. Root's sudo, case2-unit.sh render and row 240 hostcheck are stand-ins
// here; the stack's user's steps are the real stack-release.sh from the user's clone, except the // here; the stack's user's steps are the real stack-release.sh from the user's clone, except the
// rehearsal, which the fixture's stub copies can't pass (its own tests are in stack-release.test.mjs). // rehearsal, which the fixture's stub copies can't pass (its own tests are in stack-release.test.mjs).
// The #385 R3-F1 cases run the real base and current case2-unit.sh instead of the render stand-in.
import test from 'node:test'; import test from 'node:test';
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import {execFileSync,spawn} from 'node:child_process'; import {execFileSync,spawn,spawnSync} from 'node:child_process';
import {mkdirSync,writeFileSync,readFileSync,existsSync,readdirSync,rmSync,chmodSync,copyFileSync,symlinkSync,statSync} from 'node:fs'; import {mkdirSync,writeFileSync,readFileSync,existsSync,readdirSync,rmSync,chmodSync,copyFileSync,symlinkSync,statSync} from 'node:fs';
import {DatabaseSync} from 'node:sqlite'; import {DatabaseSync} from 'node:sqlite';
import {join,dirname} from 'node:path'; import {join,dirname} from 'node:path';
import {once} from 'node:events'; import {once} from 'node:events';
import {B,C,C1,U,COPIES,NODE,SBIN,base,fault,git,lines,logLines,mk,pointer,repo,sha,storeVersion,sysctl,units} from './stack-release-fixture.mjs'; import {B,C,C1,U,COPIES,NODE,ROOT,SBIN,base,fault,git,lines,logLines,mk,pointer,repo,sha,storeVersion,sysctl,units} from './stack-release-fixture.mjs';
// --- Root's stand-ins. Each is serialized into its own file with K, its baked paths. --- // --- Root's stand-ins. Each is serialized into its own file with K, its baked paths. ---
@@ -479,6 +480,90 @@ test('#385 R2-F1 controls: library args without --send and a factory args file p
assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]); assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]);
}); });
// R3-F1 (#385, PR 1836): a library host rendered by base case2-unit.sh has --send in its installed Console
// unit's ExecStart, not only in its args file. With the args line removed, A2 would stop both units and A4
// and render-old would both refuse the installed unit (check_unit's ExecStart and the full-text match), so
// the driver refuses console-unit-legacy right after read_args. These cases run real renderers: base
// 09a847eda's case2-unit.sh, from git show into the module fixture, renders the legacy pair, and the
// checkout's own case2-unit.sh is A4's RENDER, both into an owned unit directory with fake getent and
// systemctl.
const BASE_REV='09a847eda';
let baseRenderer;
const baseCase2=()=>{
if(baseRenderer)return baseRenderer;
const d=join(base,'case2-base');
for(const p of ['scripts/case2-unit.sh','packages/cli/systemd/mosaic-bus.service.in','packages/webui/systemd/mosaic-webui.service.in']){
mkdirSync(dirname(join(d,p)),{recursive:true});writeFileSync(join(d,p),execFileSync('git',['-C',ROOT,'show',`${BASE_REV}:${p}`]),{mode:p.endsWith('.sh')?0o755:0o644});
}
return baseRenderer=join(d,'scripts','case2-unit.sh');
};
// The stack's home gets a proof file; dir is a Case 2 unit directory that A4's real renderer and the driver
// read. args(copy, extra) is a library args file's lines without --send; legacy() renders base's pair into dir.
function case2Host(e,dir){
const home=e.env.HOME,secrets=join(home,'.config','mosaic-dev','secrets');mkdirSync(secrets,{recursive:true,mode:0o700});
const proof=join(secrets,'proxy-proof');writeFileSync(proof,'made-up-proof\n');chmodSync(proof,0o600);
const c2=join(e.root,'case2'),bin=join(c2,'bin'),log=join(c2,'systemctl.log');mkdirSync(bin,{recursive:true});mkdirSync(dir,{recursive:true});
const uid=process.getuid(),gid=process.getgid();
writeFileSync(join(bin,'getent'),`#!/bin/sh\ncase "$1 $2" in\n "passwd fixture") echo "fixture:x:${uid}:${gid}::${home}:/bin/bash" ;;\n "group ${gid}") echo "fixture:x:${gid}:" ;;\n *) exit 2 ;;\nesac\n`,{mode:0o755});
writeFileSync(join(bin,'systemctl'),`#!/bin/sh\necho "$*" >> '${log}'\n[ "$1" != is-active ] || exit 3\n`,{mode:0o755});
const env={CASE2_UNIT_DIR:dir,CASE2_GETENT:join(bin,'getent'),CASE2_SYSTEMCTL:join(bin,'systemctl')};
const serve=['--port','443','--business','acme','--public-host','books.example','--auth-header','X-Auth-User','--allow-user','melanie','--proof-header','X-Proxy-Proof','--proof-file',proof];
const args=(copy=B,extra=[])=>['--user','fixture','--home',home,'--business','acme','--copy',copy,'--node',NODE,...extra,'--',...serve];
const render=(script,a)=>{const r=spawnSync('bash',[script,...a],{env:{PATH:e.env.PATH,...env},encoding:'utf8'});return {code:r.status,out:r.stdout,err:r.stderr}};
const legacy=()=>{const a=args();a.splice(a.indexOf('melanie')+1,0,'--send');const r=render(baseCase2(),a);assert.equal(r.code,0,r.err);return r};
const bytes=()=>Object.fromEntries(readdirSync(dir).sort().map((n)=>[n,readFileSync(join(dir,n),'utf8')]));
const syslog=()=>lines(log);
// The driver reads dir, and A4's RENDER is the checkout's real case2-unit.sh with the same seams.
const use=(text)=>{writeFileSync(e.args,text);chmodSync(e.args,0o640);e.argsText=text;e.denv.STACK_UPGRADE_UNIT_DIR=dir;delete e.denv.STACK_UPGRADE_RENDER;Object.assign(e.denv,env)};
return {home,dir,args,render,legacy,bytes,syslog,use,con:join(dir,'mosaic-webui.service')};
}
const legacyUnit=(f)=>`${f} runs serve with --send, so it was rendered before #385, and stack-upgrade.sh cannot migrate it to the read-only library Console: the units are left running; stop and report (docs/guides/host-run-sheet.md section 7) (nothing was stopped)`;
test('#385 R3-F1: an installed pre-#385 library Console unit with --send refuses console-unit-legacy before anything stops, for upgrade and rollback',async(t)=>{
const e=await useed(t),h=case2Host(e,join(e.root,'case2','units'));
h.legacy();
const c=readFileSync(h.con,'utf8');
assert.ok(c.includes(`\nExecStart=${NODE} ${e.hc}/${B}/packages/webui/src/cli.mjs serve --port 443 --business acme --public-host books.example --auth-header X-Auth-User --allow-user melanie --send --proof-header X-Proxy-Proof --proof-file `),c);
// The operator removed the args file's --send line, as R3's section 7 said, so read_args passes.
h.use(h.args().join('\n')+'\n');
assert.ok(!e.argsText.includes('--send'));
const before=h.bytes(),sys=h.syslog().length;
// The premise: the current renderer, as A4 runs it, refuses the installed legacy unit and writes nothing.
const p=h.render(join(checkout(C1),'scripts','case2-unit.sh'),h.args(C1));
assert.equal(p.code,1,p.out+p.err);
assert.ok(p.err.startsWith(`case2-unit: installed ${h.con}: ExecStart= must be exactly once in [Service], as ExecStart=${NODE} ${e.hc}/${B}/packages/webui/src/cli.mjs serve --port 443 `),p.err);
assert.deepEqual(h.bytes(),before);assert.equal(h.syslog().length,sys);
for(const [why,run] of [['upgrade',()=>upgrade(e,C1)],['rollback',()=>drive(e,B,'rollback','--instance','dev','--to',B,...TO)]]){
const n=existsSync(e.slog)?slog(e).length:0;
const r=await run();assert.equal(r.code,3,`${why}: ${r.all}`);
assert.equal(r.err.split('\n').at(-2),`stack-upgrade: console-unit-legacy: ${legacyUnit(h.con)}`,why);
assert.deepEqual(slog(e).slice(n).map((x)=>x.split(' ')[0]),['start','result'],why);
assert.equal(slog(e).at(-1),`result exit=3 console-unit-legacy ${legacyUnit(h.con)}`,why);
argsKept(e);assert.deepEqual(h.bytes(),before,why);
}
assert.ok(!existsSync(join(e.sd,'sudo.log')));assert.ok(!existsSync(join(e.sd,'render.log')));
assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.equal(h.syslog().length,sys);
assert.deepEqual(live(e),['active','active',B,B]);
});
test('#385 R3-F1 controls: a read-only library Console unit, a factory host\'s unit and a missing Console unit pass the check',async(t)=>{
const e=await useed(t);
const ro=case2Host(e,join(e.root,'case2','ro')),old=case2Host(e,join(e.root,'case2','legacy')),none=case2Host(e,join(e.root,'case2','none'));
const r0=ro.render(join(checkout(C1),'scripts','case2-unit.sh'),ro.args());assert.equal(r0.code,0,r0.err);
assert.ok(!readFileSync(ro.con,'utf8').includes('--send'));
old.legacy();
none.legacy();rmSync(none.con);
// A bundle that does not exist stops the run at its first step after the check, with nothing stopped.
const gone=join(e.root,'no.bundle');
for(const [why,h,extra] of [['a read-only library unit',ro,[]],['a factory args file over a unit holding --send',old,['--profile','factory']],['no installed Console unit',none,[]]]){
h.use(h.args(B,extra).join('\n')+'\n');
const n=existsSync(e.slog)?slog(e).length:0;
const r=await drive(e,C1,'upgrade','--instance','dev','--bundle',gone,'--sha',C1,...TO);assert.equal(r.code,3,`${why}: ${r.all}`);
assert.deepEqual(slog(e).slice(n).map((x)=>x.split(' ')[0]),['start','fetch','result'],why);
assert.match(slog(e).at(-1),/^result exit=3 fetch-failed /,why);
}
assert.ok(!existsSync(join(e.sd,'root-systemctl.log')));assert.deepEqual(live(e),['active','active',B,B]);
});
test('row 233: hostcheck (row 240): a nonzero --check or a missing file refuses profile-drift before the plan',async(t)=>{ test('row 233: hostcheck (row 240): a nonzero --check or a missing file refuses profile-drift before the plan',async(t)=>{
const e=await useed(t); const e=await useed(t);
fault(e,'hostcheck-rc','1'); fault(e,'hostcheck-rc','1');
+29 -1
View File
@@ -120,7 +120,8 @@ logl "start rc=0 argv:$(printf ' %q' "$0" "${ARGV[@]}")"
# --- The args file and the stack's user --- # --- The args file and the stack's user ---
# I2's file: one case2-unit.sh argument per line, owned by root with no group or other write. Sets # I2's file: one case2-unit.sh argument per line, owned by root with no group or other write. Sets
# ARGS (the lines), S_USER, S_HOME, BUSINESS, NODE and ARGS_COPY, and the paths derived from them. # ARGS (the lines), S_USER, S_HOME, BUSINESS, NODE, ARGS_COPY and ARGS_PROFILE, and the paths derived
# from them.
# #385 R2-F1: base case2-unit.sh required --send for the library profile (its default, and the last # #385 R2-F1: base case2-unit.sh required --send for the library profile (its default, and the last
# --profile wins, as there), and the current one refuses it. A2 would stop both units, and A4 and # --profile wins, as there), and the current one refuses it. A2 would stop both units, and A4 and
# render-old would both fail on the same args, so a library file holding --send after -- refuses # render-old would both fail on the same args, so a library file holding --send after -- refuses
@@ -148,6 +149,7 @@ read_args() {
esac esac
done done
[ "$copies" = 1 ] || refuse args-unreadable "$f needs exactly one --copy before --, not $copies" [ "$copies" = 1 ] || refuse args-unreadable "$f needs exactly one --copy before --, not $copies"
ARGS_PROFILE="$profile"
if [ "$profile" = library ]; then if [ "$profile" = library ]; then
for ((i = i + 1; i < ${#ARGS[@]}; i++)); do for ((i = i + 1; i < ${#ARGS[@]}; i++)); do
[ "${ARGS[i]}" != --send ] || [ "${ARGS[i]}" != --send ] ||
@@ -165,6 +167,30 @@ read_args() {
AS_USER=("$SUDO" -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin") AS_USER=("$SUDO" -u "$S_USER" -H env "PATH=$(dirname "$NODE"):/usr/local/bin:/usr/bin:/bin")
} }
# #385 R3-F1: base case2-unit.sh also put --send in the library Console unit's ExecStart. The current
# one checks an installed unit's ExecStart against its own read-only command, so A4 and render-old
# would both refuse that unit after A2 stopped both. No reviewed migration exists, so a library host
# whose installed Console unit has a --send token after serve refuses here, before anything runs. A
# missing unit is read_old's to refuse.
legacy_console() {
local f="$UNIT_DIR/$CON_UNIT" line t past
local -a lines words
[ "$ARGS_PROFILE" = library ] && [ -f "$f" ] && [ ! -L "$f" ] || return 0
mapfile -t lines < "$f" || refuse units-unreadable "cannot read $f"
for line in "${lines[@]}"; do
[[ "$line" =~ ^[[:space:]]*ExecStart[[:space:]]*=(.*)$ ]] || continue
read -r -a words <<< "${BASH_REMATCH[1]}"
past=""
for t in "${words[@]}"; do
if [ -n "$past" ]; then
[ "$t" != --send ] ||
refuse console-unit-legacy "$f runs serve with --send, so it was rendered before #385, and stack-upgrade.sh cannot migrate it to the read-only library Console: the units are left running; stop and report (docs/guides/host-run-sheet.md section 7)"
elif [ "$t" = serve ]; then past=1
fi
done
done
}
# RENDER_ARGS: the args file's lines with copy $1 in place of --copy's value. # RENDER_ARGS: the args file's lines with copy $1 in place of --copy's value.
args_with() { args_with() {
local i past="" local i past=""
@@ -296,6 +322,7 @@ run_steps() {
do_upgrade() { do_upgrade() {
read_args read_args
legacy_console
# Before A1: the bundle into the stack's user's clone, then the copy from it (I1). # Before A1: the bundle into the stack's user's clone, then the copy from it (I1).
step fetch "${AS_USER[@]}" git -C "$CLONE" fetch -q "$BUNDLE" refs/heads/refactor:refs/remotes/origin/refactor || step fetch "${AS_USER[@]}" git -C "$CLONE" fetch -q "$BUNDLE" refs/heads/refactor:refs/remotes/origin/refactor ||
refuse fetch-failed "the stack's user could not fetch $BUNDLE into $CLONE" refuse fetch-failed "the stack's user could not fetch $BUNDLE into $CLONE"
@@ -342,6 +369,7 @@ rehearse() {
# check: the target is an older copy than this checkout, and no rehearsal. # check: the target is an older copy than this checkout, and no rehearsal.
do_rollback() { do_rollback() {
read_args read_args
legacy_console
[ -x "$COPIES_DIR/$TO/scripts/mosaic" ] || refuse copy-missing "no $COPIES_DIR/$TO/scripts/mosaic" [ -x "$COPIES_DIR/$TO/scripts/mosaic" ] || refuse copy-missing "no $COPIES_DIR/$TO/scripts/mosaic"
hostcheck hostcheck
read_old read_old