fix(ql3): require read-only legacy target mount

This commit is contained in:
whyour
2026-08-31 00:09:41 +08:00
parent 32c0143f5d
commit 4b0241f992
2 changed files with 51 additions and 6 deletions
@@ -361,6 +361,7 @@ function mappedMount(
hostPath: string, hostPath: string,
targetPath: string, targetPath: string,
label: string, label: string,
expectedReadWrite = true,
): DockerMount { ): DockerMount {
const matches = mounts.filter((mount) => { const matches = mounts.filter((mount) => {
const relative = path.relative(mount.source, hostPath); const relative = path.relative(mount.source, hostPath);
@@ -370,8 +371,13 @@ function mappedMount(
path.join(mount.destination, relative) === targetPath path.join(mount.destination, relative) === targetPath
); );
}); });
if (matches.length !== 1 || matches[0]?.readWrite !== true) { if (
configurationError(`${label} must have one read-write bind mapping`); matches.length !== 1 ||
matches[0]?.readWrite !== expectedReadWrite
) {
configurationError(
`${label} must have one ${expectedReadWrite ? 'read-write' : 'read-only'} bind mapping`,
);
} }
return matches[0]!; return matches[0]!;
} }
@@ -597,6 +603,7 @@ export function parseTargetContainerEvidence(
command.request.legacySourcePath, command.request.legacySourcePath,
application.targetLegacySourcePath, application.targetLegacySourcePath,
'target legacy source', 'target legacy source',
false,
); );
const databaseMount = mappedMount( const databaseMount = mappedMount(
mounts, mounts,
@@ -88,7 +88,7 @@ function stoppedLegacyInspection(state, running = false) {
]); ]);
} }
function targetInspection(state) { function targetInspection(state, options = {}) {
return JSON.stringify([ return JSON.stringify([
{ {
Id: state.targetContainerId, Id: state.targetContainerId,
@@ -115,10 +115,27 @@ function targetInspection(state) {
Mounts: [ Mounts: [
{ {
Type: 'bind', Type: 'bind',
Source: state.managementRoot, Source: state.deploymentRoot,
Destination: '/host', Destination: targetPath(state, state.deploymentRoot),
RW: true, RW: true,
}, },
{
Type: 'bind',
Source: state.legacySourcePath,
Destination: targetPath(state, state.legacySourcePath),
RW: options.writableLegacySource === true,
},
...[
state.targetDatabasePath,
state.recoveryPath,
state.manifestPath,
state.activationPath,
].map((filePath) => ({
Type: 'bind',
Source: filePath,
Destination: targetPath(state, filePath),
RW: true,
})),
], ],
}, },
]); ]);
@@ -472,7 +489,7 @@ function harness(state, options = {}) {
); );
} }
if (args[0] === 'container' && args[1] === 'inspect') { if (args[0] === 'container' && args[1] === 'inspect') {
return targetInspection(state); return targetInspection(state, options);
} }
if (args[0] === 'container' && args[1] === 'start') { if (args[0] === 'container' && args[1] === 'start') {
if (args[2] === state.legacyContainerId) { if (args[2] === state.legacyContainerId) {
@@ -574,6 +591,27 @@ test('starts an exact target once and replays the active commitment without Dock
assert.equal(replay.recordDigest, active.recordDigest); assert.equal(replay.recordDigest, active.recordDigest);
}); });
test('requires the target container to keep the Legacy source read-only', async (t) => {
const state = fixture(t);
const controller = harness(state, { writableLegacySource: true });
const result = await runLocalDeploymentDockerTarget(
command(state),
controller,
);
assert.equal(result.state, 'manual_required');
assert.equal(
controller.calls.filter((args) => args[1] === 'start').length,
0,
);
const request = JSON.parse(
fs.readFileSync(
path.join(state.journal, '0003-target-start-decision.json'),
'utf8',
),
);
assert.equal(request.evidence.reason, 'target_preflight_unproved');
});
test('starts an offline Trial Kit image only when its local reference and content ID both match', async (t) => { test('starts an offline Trial Kit image only when its local reference and content ID both match', async (t) => {
const state = fixture(t); const state = fixture(t);
state.targetImageAuthority = 'local-image-id'; state.targetImageAuthority = 'local-image-id';