fix(release): validate non-console config keys with target loader
This commit is contained in:
1 parent
01983cd3f1
commit
0585b32130
3 files changed
+81
-14
No files matched your search
+6
-3
@@ -632,9 +632,12 @@ onto the pointer's copy from the target, past any same-copy re-pin, checked
|
||||
against the set's `SHA256SUMS`. `--send` and `--tester-port` follow the pointer.
|
||||
When that set is missing, fails its sums, holds no readable argv or already ran
|
||||
`--sso`, the run refuses `sso-rollback-flags`. Any activate or rollback also
|
||||
refuses `config-console-key` before anything stops when the config holds a
|
||||
`console` key the target's `scripts/mosaic-config.mjs validate` refuses (a copy
|
||||
before row 192 refuses the key). The message says to remove the key first. So a
|
||||
refuses `config-console-key` before anything stops when the target's
|
||||
`scripts/mosaic-config.mjs validate` refuses the config, including a non-console
|
||||
key. Every existing parseable config is checked; a missing file passes. A target
|
||||
without a loader accepts only the oldest top-level keys: `configVersion`,
|
||||
`environment`, `dataRoot`, and `execution`; any later key refuses. The refusal
|
||||
names the rejected key and says to restore the config.json backup first. So a
|
||||
window re-pins to row 192 or later before it writes the key, and a rollback past
|
||||
row 192 removes the key first.
|
||||
|
||||
|
||||
@@ -2585,3 +2585,60 @@ test('row196 rehearsal selects socket only for marked copies and uses scratch da
|
||||
assert.equal(untouched(e),before);
|
||||
});
|
||||
});
|
||||
|
||||
// #1797: validate every config with the target loader, even without console.
|
||||
function anyConfigLoader(file,text){
|
||||
const old=existsSync(file)?readFileSync(file):null;
|
||||
if(old!==null)chmodSync(file,0o644);
|
||||
mkdirSync(dirname(file),{recursive:true});writeFileSync(file,text);
|
||||
return ()=>{if(old===null)rmSync(file);else{writeFileSync(file,old);chmodSync(file,0o444);}};
|
||||
}
|
||||
const ANY_CONFIG_MSG=(e,to,op,why)=>`${e.cfg} is refused by ${to}'s loader (${why}); remove the refused key first (restore the config.json backup from the window's step 1), then run the ${op} again`;
|
||||
test('config-any-key: a non-console target refusal stops activate and rollback before any stop',async t=>{
|
||||
const e=await mk(t);await seed(e);
|
||||
let r=await run(e,'activate',C1,'acme');assert.equal(r.code,0,r.out+r.err);
|
||||
const why='mosaic-config: unsupported configuration key: "execution"';
|
||||
const undo=anyConfigLoader(join(COPIES,B,'scripts/mosaic-config.mjs'),`console.error(${JSON.stringify(why)});process.exitCode=2;\n`);
|
||||
try{
|
||||
assert.equal(Object.hasOwn(JSON.parse(readFileSync(e.cfg,'utf8')),'console'),false);
|
||||
for(const op of ['activate','rollback']){
|
||||
const args=op==='activate'?['activate',B,'acme']:['rollback','acme'];
|
||||
await refused(e,args,'config-console-key',ANY_CONFIG_MSG(e,B,op,why));
|
||||
}
|
||||
const out=await planOnly(e,3,'rollback','acme');
|
||||
assert.deepEqual(out.filter(l=>l.startsWith('refuses ')),[`refuses config-console-key: ${ANY_CONFIG_MSG(e,B,'rollback',why)}`]);
|
||||
}finally{undo();}
|
||||
});
|
||||
test('config-any-key: accepting loaders run validate with the selected config on activate and rollback',async t=>{
|
||||
const e=await mk(t);await seed(e);const witness=join(e.root,'loader-calls.jsonl'),canary='resolved-config-must-not-leak';
|
||||
const undos=[B,C1].map(id=>anyConfigLoader(join(COPIES,id,'scripts/mosaic-config.mjs'),`import fs from 'node:fs';fs.appendFileSync(${JSON.stringify(witness)},JSON.stringify({copy:${JSON.stringify(id)},args:process.argv.slice(2),config:process.env.MOSAIC_CONFIG})+'\\n');console.log(${JSON.stringify(canary)});\n`));
|
||||
try{
|
||||
for(const args of [['activate',C1,'acme'],['rollback','acme']]){
|
||||
const r=await run(e,...args);assert.equal(r.code,0,r.out+r.err);assert.equal((r.out+r.err).includes(canary),false);
|
||||
}
|
||||
assert.deepEqual(lines(witness).map(JSON.parse),[
|
||||
{copy:C1,args:['validate'],config:e.cfg},{copy:B,args:['validate'],config:e.cfg},
|
||||
]);
|
||||
}finally{for(const undo of undos)undo();}
|
||||
});
|
||||
function isolatedConfigChecks(e){
|
||||
const source=readFileSync(SR,'utf8'),body=source.split('config_checks() {')[1]?.split('\n}\n')[0];assert.ok(body,'config_checks exists');
|
||||
return spawnSync('bash',['-c','config_checks() {'+body+'\n}\ncheck_refuse(){ printf "%s: %s\\n" "$1" "$2" >&2; exit 3; }; config_checks'],{
|
||||
encoding:'utf8',env:{...e.env,COPIES:join(e.root,'target-copies'),TO:'old',OP:'activate'},
|
||||
});
|
||||
}
|
||||
test('config-any-key: missing-loader copies accept only the four oldest keys',async t=>{
|
||||
const e=await mk(t);let r=isolatedConfigChecks(e);assert.equal(r.status,0,r.stderr);
|
||||
const original=JSON.parse(readFileSync(e.cfg,'utf8'));
|
||||
for(const [key,value] of [['compactionReadBytes',1048576],['compactionStreams',['messages']]]){
|
||||
writeFileSync(e.cfg,JSON.stringify({...original,[key]:value}));r=isolatedConfigChecks(e);
|
||||
assert.equal(r.status,3,r.stderr);assert.match(r.stderr,new RegExp(`configuration key: "${key}"`));
|
||||
assert.ok(r.stderr.includes('restore the config.json backup from the window\'s step 1'));
|
||||
}
|
||||
});
|
||||
test('config-any-key: missing config passes but malformed JSON refuses without calling the loader',async t=>{
|
||||
const e=await mk(t),file=join(e.root,'target-copies/old/scripts/mosaic-config.mjs'),witness=join(e.root,'validate-called');
|
||||
mkdirSync(dirname(file),{recursive:true});writeFileSync(file,`import fs from 'node:fs';fs.writeFileSync(${JSON.stringify(witness)},'called');\n`);
|
||||
rmSync(e.cfg);let r=isolatedConfigChecks(e);assert.equal(r.status,0,r.stderr);assert.equal(existsSync(witness),false);
|
||||
writeFileSync(e.cfg,'{');r=isolatedConfigChecks(e);assert.equal(r.status,3,r.stderr);assert.match(r.stderr,/cannot be read as JSON/);assert.equal(existsSync(witness),false);
|
||||
});
|
||||
+18
-11
@@ -1073,27 +1073,34 @@ slot_problem() {
|
||||
[ "$size" != 0 ] || echo "is empty"
|
||||
}
|
||||
|
||||
# Row 200 item 3 and the design's phase 2 rollback order: a config.json with a `console` key must be
|
||||
# one the target's loader reads, or its bus, Console and seats fail on their next read. Runs the
|
||||
# target's own scripts/mosaic-config.mjs validate; its output, the resolved config, is not kept.
|
||||
# Row 200 item 3 and #1797: every existing parseable config must be one the
|
||||
# target loader reads. Older copies without a loader accept only the four keys
|
||||
# in c3d29e796's first loader. Resolved config stdout is never kept.
|
||||
config_checks() {
|
||||
local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first
|
||||
# 0: a console key; 1: no key, or no file; 2: the file doesn't parse, so whether it holds the key
|
||||
# is unknown, and the run refuses rather than pass it (webby-0028 N2). load_config already exits on
|
||||
# such a file before anything is read or stopped, so this is reached only if the file changes after it.
|
||||
local cfg="${MOSAIC_CONFIG:-$HOME/.config/mosaic-dev/config.json}" out loader="$COPIES/$TO/scripts/mosaic-config.mjs" first extra
|
||||
# 1: missing file; 2: unreadable JSON. This also fences a change after load_config.
|
||||
local key=0
|
||||
node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)}
|
||||
process.exit(d!==null&&typeof d==="object"&&Object.hasOwn(d,"console")?0:1)' -- "$cfg" 2>/dev/null || key=$?
|
||||
extra="$(node -e 'const fs=require("fs"),f=process.argv[1];if(!fs.existsSync(f))process.exit(1);let d;try{d=JSON.parse(fs.readFileSync(f,"utf8"))}catch{process.exit(2)}
|
||||
const oldest=["configVersion","environment","dataRoot","execution"];
|
||||
if(d===null||typeof d!=="object"||Array.isArray(d)){process.stdout.write(JSON.stringify("[configuration shape]"));process.exit(0)}
|
||||
const k=Object.hasOwn(d,"console")?"console":Object.keys(d).find(k=>!oldest.includes(k));if(k!==undefined)process.stdout.write(JSON.stringify(k))' -- "$cfg" 2>/dev/null)" || key=$?
|
||||
[ "$key" != 1 ] || return 0
|
||||
if [ "$key" != 0 ]; then
|
||||
check_refuse config-console-key "$cfg cannot be read as JSON, so whether $TO's loader reads it is unknown; fix the config first, then run the $OP again"
|
||||
return 0
|
||||
fi
|
||||
if [ ! -f "$loader" ]; then out="it has no scripts/mosaic-config.mjs"
|
||||
if [ ! -f "$loader" ]; then
|
||||
[ -n "$extra" ] || return 0
|
||||
if [ "$extra" = '"console"' ]; then out="it has no scripts/mosaic-config.mjs"
|
||||
else out="it has no scripts/mosaic-config.mjs to validate configuration key: $extra"; fi
|
||||
elif out="$(MOSAIC_CONFIG="$cfg" node "$loader" validate 2>&1 >/dev/null)"; then return 0
|
||||
fi
|
||||
first="${out%%$'\n'*}"
|
||||
check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
|
||||
if [ "$extra" = '"console"' ]; then
|
||||
check_refuse config-console-key "$cfg holds a console key, and $TO's loader refuses it ($first); remove the key first (restore the config.json backup from the window's step 1), then run the $OP again"
|
||||
else
|
||||
check_refuse config-console-key "$cfg is refused by $TO's loader ($first); remove the refused key first (restore the config.json backup from the window's step 1), then run the $OP again"
|
||||
fi
|
||||
}
|
||||
|
||||
# Row 189 (mos-0972): --no-send drops --send from RUN_CONSOLE, so the unit and the pointer both lose
|
||||
|
||||
Reference in new issue
Block a user