test: add todo-note runtime artifact proof
This commit is contained in:
@@ -159,6 +159,7 @@ interface ArtifactConsumerTarget {
|
||||
projectHint?: string;
|
||||
requiredRuntimeSecrets?: RuntimeSecretRequirement[];
|
||||
authHealthGate?: AuthHealthGate;
|
||||
syntheticHealthDeployProof?: SyntheticHealthDeployProof;
|
||||
};
|
||||
k3s?: {
|
||||
namespace: string;
|
||||
@@ -184,6 +185,11 @@ interface AuthHealthGate {
|
||||
recoveryHint: string;
|
||||
}
|
||||
|
||||
interface SyntheticHealthDeployProof {
|
||||
kind: "compose-container-runtime-metadata";
|
||||
description: string;
|
||||
}
|
||||
|
||||
interface ComposeArtifactRuntime {
|
||||
workDir: string;
|
||||
composeFile: string;
|
||||
@@ -230,8 +236,13 @@ export interface RuntimeSecretContract {
|
||||
dryRunDisposition: "not-required" | "ready-for-live-apply" | "secret-source-blocked";
|
||||
}
|
||||
|
||||
const todoNoteSyntheticHealthDeployProof: SyntheticHealthDeployProof = {
|
||||
kind: "compose-container-runtime-metadata",
|
||||
description: "todo-note /api/health does not natively expose deploy metadata, so the artifact consumer synthesizes health.deploy from the recreated container's UNIDESK_DEPLOY_* env and verifies it against image/container labels.",
|
||||
};
|
||||
|
||||
function todoNoteHealthProbeCommand(): string {
|
||||
return "bun -e \"fetch('http://127.0.0.1:4211/api/health').then(async r=>{const text=await r.text(); let body; try{body=JSON.parse(text)}catch{body={ok:r.ok,raw:text}}; const deploy=body.deploy&&typeof body.deploy==='object'&&!Array.isArray(body.deploy)?body.deploy:{}; body.deploy={...deploy,serviceId:deploy.serviceId||process.env.UNIDESK_DEPLOY_SERVICE_ID||'todo-note',ref:deploy.ref||process.env.UNIDESK_DEPLOY_REF||'',repo:deploy.repo||process.env.UNIDESK_DEPLOY_REPO||'',commit:deploy.commit||process.env.UNIDESK_DEPLOY_COMMIT||'',requestedCommit:deploy.requestedCommit||process.env.UNIDESK_DEPLOY_REQUESTED_COMMIT||''}; console.log(JSON.stringify(body)); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"";
|
||||
return "bun -e \"fetch('http://127.0.0.1:4211/api/health').then(async r=>{const text=await r.text(); let body; try{body=JSON.parse(text)}catch{body={ok:r.ok,raw:text}}; console.log(JSON.stringify(body)); process.exit(r.ok?0:1)}).catch(e=>{console.error(e); process.exit(1)})\"";
|
||||
}
|
||||
|
||||
export const baiduNetdiskRuntimeSecretRequirements: RuntimeSecretRequirement[] = [
|
||||
@@ -777,6 +788,7 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
|
||||
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
|
||||
healthProbeCommand: todoNoteHealthProbeCommand(),
|
||||
requireHealthCommit: true,
|
||||
syntheticHealthDeployProof: todoNoteSyntheticHealthDeployProof,
|
||||
},
|
||||
},
|
||||
prod: {
|
||||
@@ -789,6 +801,7 @@ const artifactConsumerSpecs: Record<string, ArtifactConsumerSpec> = {
|
||||
deployEnvPrefix: "UNIDESK_TODO_NOTE_DEPLOY",
|
||||
healthProbeCommand: todoNoteHealthProbeCommand(),
|
||||
requireHealthCommit: true,
|
||||
syntheticHealthDeployProof: todoNoteSyntheticHealthDeployProof,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2166,6 +2179,70 @@ function composeArtifactEnvValues(spec: ArtifactConsumerSpec, target: ArtifactCo
|
||||
};
|
||||
}
|
||||
|
||||
function syntheticComposeHealthDeployScript(compose: NonNullable<ArtifactConsumerTarget["compose"]>, commit: string, sourceRepo: string): string[] {
|
||||
if (compose.syntheticHealthDeployProof === undefined) return [];
|
||||
const prefix = compose.deployEnvPrefix;
|
||||
const envKeys = [
|
||||
"UNIDESK_DEPLOY_SERVICE_ID",
|
||||
"UNIDESK_DEPLOY_REF",
|
||||
"UNIDESK_DEPLOY_REPO",
|
||||
"UNIDESK_DEPLOY_COMMIT",
|
||||
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
|
||||
`${prefix}_SERVICE_ID`,
|
||||
`${prefix}_REF`,
|
||||
`${prefix}_REPO`,
|
||||
`${prefix}_COMMIT`,
|
||||
`${prefix}_REQUESTED_COMMIT`,
|
||||
];
|
||||
const envKeysJson = JSON.stringify(envKeys);
|
||||
return [
|
||||
"runtime_metadata_json=$(docker inspect \"$cid\" --format '{{json .Config.Env}}')",
|
||||
"container_label_commit=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-commit\" }}' \"$cid\")",
|
||||
"container_label_requested_commit=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-requested-commit\" }}' \"$cid\")",
|
||||
"container_label_repo=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-repo\" }}' \"$cid\")",
|
||||
"container_label_ref=$(docker inspect -f '{{ index .Config.Labels \"unidesk.ai/deploy-ref\" }}' \"$cid\")",
|
||||
`python3 - "$health_json" "$runtime_metadata_json" ${shellQuote(envKeysJson)} ${shellQuote(prefix)} ${shellQuote(commit)} ${shellQuote(sourceRepo)} "$actual_commit" "$actual_service" "$actual_repo" "$container_label_commit" "$container_label_requested_commit" "$container_label_repo" "$container_label_ref" <<'PY'`,
|
||||
"import json, sys",
|
||||
"path, env_json, keys_json, prefix, expected_commit, expected_repo, image_commit, image_service, image_repo, label_commit, label_requested_commit, label_repo, label_ref = sys.argv[1:]",
|
||||
"body = json.load(open(path, encoding='utf-8'))",
|
||||
"env_items = json.loads(env_json)",
|
||||
"allow = set(json.loads(keys_json))",
|
||||
"env = {}",
|
||||
"for item in env_items:",
|
||||
" if not isinstance(item, str) or '=' not in item:",
|
||||
" continue",
|
||||
" key, value = item.split('=', 1)",
|
||||
" if key in allow:",
|
||||
" env[key] = value",
|
||||
"deploy = body.get('deploy') if isinstance(body, dict) and isinstance(body.get('deploy'), dict) else {}",
|
||||
"synthetic = {",
|
||||
" 'serviceId': deploy.get('serviceId') or env.get('UNIDESK_DEPLOY_SERVICE_ID') or env.get(prefix + '_SERVICE_ID') or image_service,",
|
||||
" 'ref': deploy.get('ref') or env.get('UNIDESK_DEPLOY_REF') or env.get(prefix + '_REF') or label_ref,",
|
||||
" 'repo': deploy.get('repo') or env.get('UNIDESK_DEPLOY_REPO') or env.get(prefix + '_REPO') or label_repo or image_repo,",
|
||||
" 'commit': deploy.get('commit') or env.get('UNIDESK_DEPLOY_COMMIT') or env.get(prefix + '_COMMIT') or label_commit or image_commit,",
|
||||
" 'requestedCommit': deploy.get('requestedCommit') or env.get('UNIDESK_DEPLOY_REQUESTED_COMMIT') or env.get(prefix + '_REQUESTED_COMMIT') or label_requested_commit,",
|
||||
"}",
|
||||
"body['deploy'] = synthetic",
|
||||
"body['deployProof'] = {",
|
||||
" 'kind': 'compose-container-runtime-metadata',",
|
||||
" 'sources': ['service-health', 'container-env', 'container-labels', 'image-labels'],",
|
||||
" 'sourceDirectoryUsed': False,",
|
||||
" 'envKeysUsed': sorted([key for key in env if key.startswith('UNIDESK_DEPLOY_') or key.startswith(prefix + '_')]),",
|
||||
"}",
|
||||
"if synthetic.get('commit') != expected_commit or synthetic.get('requestedCommit') != expected_commit:",
|
||||
" raise SystemExit('synthetic deploy commit mismatch: ' + json.dumps(synthetic, sort_keys=True))",
|
||||
"if synthetic.get('repo') != expected_repo:",
|
||||
" raise SystemExit('synthetic deploy repo mismatch: ' + json.dumps(synthetic, sort_keys=True))",
|
||||
"if synthetic.get('serviceId') != image_service:",
|
||||
" raise SystemExit('synthetic deploy service mismatch: ' + json.dumps(synthetic, sort_keys=True))",
|
||||
"with open(path, 'w', encoding='utf-8') as handle:",
|
||||
" json.dump(body, handle, separators=(',', ':'))",
|
||||
" handle.write('\\n')",
|
||||
"print('synthetic_health_deploy_proof=compose-container-runtime-metadata sourceDirectoryUsed=false')",
|
||||
"PY",
|
||||
];
|
||||
}
|
||||
|
||||
async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec: ArtifactConsumerSpec, target: ArtifactConsumerTarget): Promise<Record<string, unknown>> {
|
||||
const commit = options.commit;
|
||||
if (commit === null) throw new Error("artifact-registry deploy-service requires --commit <full-sha>");
|
||||
@@ -2202,6 +2279,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
return { ok: false, serviceId: spec.serviceId, error: "D601 artifact registry is not healthy", health };
|
||||
}
|
||||
const sourceImage = artifactImageRef(options, spec, commit);
|
||||
const sourceRepo = sourceRepoFor(options, spec);
|
||||
const composeImage = options.targetImage ?? target.targetImage;
|
||||
const commitImage = `${composeImage}:${commit}`;
|
||||
const registryProbe = runRemoteScript(options, registryArtifactProbeScript(options, spec, commit), Math.max(options.timeoutMs, 120_000));
|
||||
@@ -2227,7 +2305,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
};
|
||||
}
|
||||
const localLoadedImage = sourceImage;
|
||||
const labelFailure = verifyLocalArtifactLabels(localLoadedImage, spec, commit, sourceRepoFor(options, spec));
|
||||
const labelFailure = verifyLocalArtifactLabels(localLoadedImage, spec, commit, sourceRepo);
|
||||
if (labelFailure !== null) return { ...labelFailure, registryProbe: commandTail(registryProbe) };
|
||||
const tag = runCommand(["docker", "tag", localLoadedImage, composeImage], repoRoot);
|
||||
if (tag.exitCode !== 0 || tag.timedOut) {
|
||||
@@ -2265,9 +2343,14 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
`cid=$(docker ps -q --filter label=com.docker.compose.project=${shellQuote(composeProject)} --filter label=com.docker.compose.service=${shellQuote(serviceName)} --filter label=com.docker.compose.oneoff=False | head -1)`,
|
||||
"image_id=$(docker inspect -f '{{.Image}}' \"$cid\")",
|
||||
"actual_commit=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-commit\" }}' \"$image_id\")",
|
||||
"actual_service=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/service-id\" }}' \"$image_id\")",
|
||||
"actual_repo=$(docker image inspect -f '{{ index .Config.Labels \"unidesk.ai/source-repo\" }}' \"$image_id\")",
|
||||
`test "$actual_commit" = ${shellQuote(commit)}`,
|
||||
`test "$actual_service" = ${shellQuote(spec.serviceId)}`,
|
||||
`test "$actual_repo" = ${shellQuote(sourceRepo)}`,
|
||||
`health_json=/tmp/unidesk-${safeName(spec.serviceId)}-health.json`,
|
||||
`docker exec "$cid" sh -lc ${shellQuote(target.compose.healthProbeCommand)} > "$health_json"`,
|
||||
...syntheticComposeHealthDeployScript(target.compose, commit, sourceRepo),
|
||||
"cat \"$health_json\"",
|
||||
...authHealthGateScript(target.compose.authHealthGate, "\"$health_json\""),
|
||||
...(target.compose.requireHealthCommit ? [
|
||||
@@ -2312,6 +2395,7 @@ async function deployComposeArtifactNow(options: ArtifactRegistryOptions, spec:
|
||||
imageLabelCommit: commit,
|
||||
serviceHealthCommit: target.compose.requireHealthCommit ? commit : "not-required",
|
||||
serviceHealthRequestedCommit: target.compose.requireHealthCommit ? commit : "not-required",
|
||||
runtimeProof: target.compose.syntheticHealthDeployProof === undefined ? "native-health" : target.compose.syntheticHealthDeployProof,
|
||||
requiredRuntimeSecrets: secretPreflight.requirements,
|
||||
runtimeSecrets,
|
||||
authHealthGate: target.compose.authHealthGate === undefined ? "not-required" : {
|
||||
@@ -2652,6 +2736,8 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
...(spec.kind === "d601-compose" ? ["running Compose container image label matches the requested commit"] : []),
|
||||
verificationBlocked
|
||||
? `blocked: ${spec.runtimeVerificationBlockReason}`
|
||||
: target.compose.requireHealthCommit && target.compose.syntheticHealthDeployProof !== undefined
|
||||
? `${spec.serviceId} runtime health proof synthesizes deploy.commit/deploy.requestedCommit from Compose container runtime metadata and verifies it against image/container labels, not source directory guesses`
|
||||
: target.compose.requireHealthCommit
|
||||
? `${spec.serviceId} runtime health probe succeeds and reports deploy.commit/deploy.requestedCommit matching the artifact commit`
|
||||
: `${spec.serviceId} /health succeeds for the recreated container`,
|
||||
@@ -2668,6 +2754,26 @@ function dryRunArtifactConsumerPlan(options: ArtifactRegistryOptions, spec: Arti
|
||||
requiredAuthFields: target.compose.authHealthGate.requiredFields,
|
||||
dryRunDisposition: "pending-live-check",
|
||||
},
|
||||
runtimeProof: target.compose.syntheticHealthDeployProof === undefined ? {
|
||||
kind: "native-health-deploy-metadata",
|
||||
sourceDirectoryUsed: false,
|
||||
} : {
|
||||
...target.compose.syntheticHealthDeployProof,
|
||||
sourceDirectoryUsed: false,
|
||||
sources: ["service-health", "container-env", "container-labels", "image-labels"],
|
||||
requiredEnvKeys: [
|
||||
"UNIDESK_DEPLOY_SERVICE_ID",
|
||||
"UNIDESK_DEPLOY_REF",
|
||||
"UNIDESK_DEPLOY_REPO",
|
||||
"UNIDESK_DEPLOY_COMMIT",
|
||||
"UNIDESK_DEPLOY_REQUESTED_COMMIT",
|
||||
`${target.compose.deployEnvPrefix}_SERVICE_ID`,
|
||||
`${target.compose.deployEnvPrefix}_REF`,
|
||||
`${target.compose.deployEnvPrefix}_REPO`,
|
||||
`${target.compose.deployEnvPrefix}_COMMIT`,
|
||||
`${target.compose.deployEnvPrefix}_REQUESTED_COMMIT`,
|
||||
],
|
||||
},
|
||||
rollback: rollbackInfo(spec, target, environment, commit),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1076,6 +1076,9 @@ function artifactConsumerPlanValidation(service: UniDeskMicroserviceConfig, envi
|
||||
...base,
|
||||
"recreates only the selected Compose service with --no-build --no-deps --force-recreate",
|
||||
"verifies running image labels and private service health",
|
||||
...(service.id === "todo-note" ? [
|
||||
"todo-note runtime proof synthesizes health.deploy.commit/requestedCommit from Compose container env, container labels, and image labels; it does not infer commit from /root/todo_note or any source directory",
|
||||
] : []),
|
||||
];
|
||||
}
|
||||
return ["unsupported service remains blocked before source materialization or runtime mutation"];
|
||||
|
||||
Reference in New Issue
Block a user