@aipt/idp-deploy 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +471 -0
- package/bin/idpctl.mjs +8 -0
- package/compose/edge.yaml +61 -0
- package/compose/flow.yaml +34 -0
- package/compose/portal.yaml +38 -0
- package/compose/postgresql.yaml +62 -0
- package/compose/registry.yaml +35 -0
- package/compose/smartgo.yaml +271 -0
- package/compose/tech.yaml +64 -0
- package/contracts/active-profile.schema.json +19 -0
- package/contracts/asset-lifecycle.schema.json +49 -0
- package/contracts/backup-generation.schema.json +60 -0
- package/contracts/component-runtime.schema.json +32 -0
- package/contracts/config-release.schema.json +33 -0
- package/contracts/deployment-evidence.schema.json +43 -0
- package/contracts/deployment-plan.schema.json +67 -0
- package/contracts/release-candidate.schema.json +33 -0
- package/contracts/release-defaults.schema.json +56 -0
- package/contracts/restore-candidate.schema.json +63 -0
- package/contracts/smartgo-component-config.schema.json +79 -0
- package/contracts/tech-backup-boundary.schema.json +61 -0
- package/contracts/tech-source-credentials.schema.json +17 -0
- package/deploy.sh +5 -0
- package/docs/restore-runbook.md +56 -0
- package/governance/asset-lifecycle.v1.json +79 -0
- package/package.json +42 -0
- package/release/defaults.v1.json +64 -0
- package/src/acceptance.mjs +127 -0
- package/src/bindings.mjs +518 -0
- package/src/cli.mjs +360 -0
- package/src/compose.mjs +19 -0
- package/src/config.mjs +903 -0
- package/src/delivery.mjs +123 -0
- package/src/errors.mjs +12 -0
- package/src/foundation-contracts.mjs +128 -0
- package/src/foundation.mjs +107 -0
- package/src/gitops.mjs +285 -0
- package/src/hash.mjs +47 -0
- package/src/image-lock.mjs +160 -0
- package/src/images.mjs +215 -0
- package/src/lifecycle.mjs +58 -0
- package/src/local-source.mjs +354 -0
- package/src/oci-mirror.mjs +10 -0
- package/src/operations.mjs +1484 -0
- package/src/process.mjs +46 -0
- package/src/profiles.mjs +41 -0
- package/src/release.mjs +1760 -0
- package/src/render.mjs +330 -0
- package/src/security.mjs +156 -0
- package/src/source-contracts.mjs +106 -0
- package/src/sources.mjs +78 -0
- package/src/workbench-projects.mjs +118 -0
package/src/sources.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { doctorConfig, referencedFiles } from './config.mjs';
|
|
5
|
+
import { invariant } from './errors.mjs';
|
|
6
|
+
import { sha256 } from './hash.mjs';
|
|
7
|
+
import { withInstanceLock, writeReceipt } from './operations.mjs';
|
|
8
|
+
import { assertSafeRegularFile, atomicWrite, resolveContained } from './security.mjs';
|
|
9
|
+
import { validateBenchSnapshot, validateDyytoSnapshot } from './source-contracts.mjs';
|
|
10
|
+
|
|
11
|
+
export { validateBenchSnapshot, validateDyytoSnapshot } from './source-contracts.mjs';
|
|
12
|
+
|
|
13
|
+
const MAX_SNAPSHOT_BYTES = 16 * 1024 * 1024;
|
|
14
|
+
|
|
15
|
+
function targetDefinitions(root, source) {
|
|
16
|
+
const file = source === 'dyyto' ? 'dyyto-package-release-catalog.json' : 'bench-environment-snapshot.json';
|
|
17
|
+
const key = source === 'dyyto' ? 'DYYTO_CATALOG_FILE' : 'BENCH_SNAPSHOT_FILE';
|
|
18
|
+
const componentTargets = referencedFiles(root).filter((ref) => ['flow', 'portal'].includes(ref.scope) && ref.key === `${ref.scope.toUpperCase()}_${key}`);
|
|
19
|
+
invariant(componentTargets.length === 2, 'IDP_SOURCE_TARGETS_INVALID', `${source} Snapshot必须同时声明Flow与Portal文件目标`);
|
|
20
|
+
return [
|
|
21
|
+
{ logical: `snapshots/${file}`, absolute: resolveContained(root, `snapshots/${file}`, '共享Snapshot') },
|
|
22
|
+
...componentTargets.map((ref) => ({ logical: path.relative(root, ref.absolute), absolute: ref.absolute })),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function capturePreimages(targets) {
|
|
27
|
+
return targets.map((target) => {
|
|
28
|
+
if (!fs.existsSync(target.absolute)) return { ...target, existed: false, bytes: null, digest: null };
|
|
29
|
+
assertSafeRegularFile(target.absolute, 0o600, { allowEmpty: true, role: target.logical });
|
|
30
|
+
const bytes = fs.readFileSync(target.absolute);
|
|
31
|
+
return { ...target, existed: true, bytes, digest: sha256(bytes) };
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function currentDigest(entry) {
|
|
36
|
+
if (!fs.existsSync(entry.absolute)) return null;
|
|
37
|
+
assertSafeRegularFile(entry.absolute, 0o600, { allowEmpty: true, role: entry.logical });
|
|
38
|
+
return sha256(fs.readFileSync(entry.absolute));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function importSourceSnapshot({ configRoot, source, bytes, receiptWriter = writeReceipt }) {
|
|
42
|
+
invariant(['dyyto', 'bench'].includes(source), 'IDP_SOURCE_UNKNOWN', 'source只能是dyyto或bench');
|
|
43
|
+
invariant(Buffer.byteLength(bytes) > 0 && Buffer.byteLength(bytes) <= MAX_SNAPSHOT_BYTES, 'IDP_SOURCE_SIZE_INVALID', 'Snapshot必须介于1 byte与16 MiB之间');
|
|
44
|
+
let document;
|
|
45
|
+
try { document = JSON.parse(Buffer.isBuffer(bytes) ? bytes.toString('utf8') : bytes); }
|
|
46
|
+
catch { invariant(false, 'IDP_SOURCE_JSON_INVALID', 'Snapshot不是有效JSON'); }
|
|
47
|
+
if (source === 'dyyto') validateDyytoSnapshot(document); else validateBenchSnapshot(document);
|
|
48
|
+
const normalized = Buffer.from(`${JSON.stringify(document, null, 2)}\n`);
|
|
49
|
+
invariant(normalized.byteLength <= MAX_SNAPSHOT_BYTES, 'IDP_SOURCE_SIZE_INVALID', 'Snapshot规范化后不能超过16 MiB');
|
|
50
|
+
const preliminary = doctorConfig(configRoot, { requireConfigured: false, profile: 'registry' });
|
|
51
|
+
return withInstanceLock(preliminary.root, preliminary.env, `sources:import:${source}`, () => {
|
|
52
|
+
const targets = targetDefinitions(preliminary.root, source);
|
|
53
|
+
for (const target of targets) fs.mkdirSync(path.dirname(target.absolute), { recursive: true, mode: 0o700 });
|
|
54
|
+
const preimages = capturePreimages(targets);
|
|
55
|
+
const generation = `${new Date().toISOString().replace(/[:.]/gu, '-')}-${sha256(normalized).slice(7, 19)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
56
|
+
const history = resolveContained(preliminary.root, `snapshots/history/${source}/${generation}`, 'Snapshot Preimage代次');
|
|
57
|
+
fs.mkdirSync(history, { recursive: true, mode: 0o700 });
|
|
58
|
+
for (const entry of preimages) {
|
|
59
|
+
if (entry.existed) fs.writeFileSync(path.join(history, `${entry.logical.replaceAll('/', '__')}.preimage`), entry.bytes, { mode: 0o600, flag: 'wx' });
|
|
60
|
+
}
|
|
61
|
+
fs.writeFileSync(path.join(history, 'preimages.json'), `${JSON.stringify({ schemaVersion: 1, source, targets: preimages.map(({ logical, existed, digest }) => ({ logical, existed, digest })) }, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
62
|
+
const completed = [];
|
|
63
|
+
try {
|
|
64
|
+
for (const entry of preimages) {
|
|
65
|
+
invariant(currentDigest(entry) === entry.digest, 'IDP_SOURCE_PREIMAGE_CONFLICT', `${entry.logical}在导入期间被并发修改`);
|
|
66
|
+
atomicWrite(entry.absolute, normalized, 0o600);
|
|
67
|
+
completed.push(entry);
|
|
68
|
+
}
|
|
69
|
+
return receiptWriter(preliminary.env, 'source-snapshot-import', { source, snapshotDigest: source === 'dyyto' ? document.sourceDigest : document.snapshotDigest, targets: targets.map(({ logical }) => logical), preimageGeneration: generation });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
for (const entry of completed.reverse()) {
|
|
72
|
+
invariant(currentDigest(entry) === sha256(normalized), 'IDP_SOURCE_RECOVERY_REQUIRED', `${entry.logical}在回滚前被并发修改,禁止静默覆盖`);
|
|
73
|
+
if (entry.existed) atomicWrite(entry.absolute, entry.bytes, 0o600); else fs.unlinkSync(entry.absolute);
|
|
74
|
+
}
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { IdpError, invariant } from './errors.mjs';
|
|
6
|
+
import { hashFile, sha256, stableJson } from './hash.mjs';
|
|
7
|
+
import { run } from './process.mjs';
|
|
8
|
+
import { assertOutsideRepositories, assertSafeRegularFile, atomicWrite, canonicalPlannedDirectory, resolveContained, secureDirectoryRoot } from './security.mjs';
|
|
9
|
+
|
|
10
|
+
const ID = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
|
|
11
|
+
|
|
12
|
+
function configRoot(root) { const canonical = secureDirectoryRoot(root, 'IDP_CONFIG_DIR'); assertOutsideRepositories(canonical, 'IDP_CONFIG_DIR'); return canonical; }
|
|
13
|
+
function safeDirectory(candidate, role) {
|
|
14
|
+
invariant(path.isAbsolute(candidate ?? ''), 'IDP_WORKBENCH_PATH_NOT_ABSOLUTE', `${role}必须是绝对路径`);
|
|
15
|
+
const canonical = fs.realpathSync.native(candidate); const stat = fs.lstatSync(canonical);
|
|
16
|
+
invariant(stat.isDirectory() && !stat.isSymbolicLink(), 'IDP_WORKBENCH_PATH_INVALID', `${role}必须是普通目录`);
|
|
17
|
+
invariant(canonical !== path.parse(canonical).root && canonical !== fs.realpathSync.native(os.homedir()), 'IDP_WORKBENCH_PATH_TOO_BROAD', `${role}不能是根目录或HOME`);
|
|
18
|
+
return canonical;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function initializeWorkbenchProjects({ configRoot: input }) {
|
|
22
|
+
const root = configRoot(input); const directory = resolveContained(root, 'workbench', '工作台配置目录'); secureDirectoryRoot(directory, '工作台配置目录', { create: true });
|
|
23
|
+
const candidate = path.join(directory, 'projects.candidate.json');
|
|
24
|
+
if (!fs.existsSync(candidate)) atomicWrite(candidate, `${JSON.stringify({ schemaVersion: 1, projects: [] }, null, 2)}\n`, 0o600);
|
|
25
|
+
return { schemaVersion: 'idp.workbench-project-candidate/v1', status: 'ready', candidate };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readCandidate(root, file) {
|
|
29
|
+
invariant(path.isAbsolute(file ?? ''), 'IDP_WORKBENCH_CANDIDATE_NOT_ABSOLUTE', '--file必须是绝对路径');
|
|
30
|
+
const workbench = resolveContained(root, 'workbench', '工作台配置目录'); const candidate = fs.realpathSync.native(file);
|
|
31
|
+
const relative = path.relative(workbench, candidate); invariant(relative && !relative.startsWith('..') && !path.isAbsolute(relative), 'IDP_WORKBENCH_CANDIDATE_OUTSIDE_CONFIG', 'candidate必须位于IDP_CONFIG_DIR/workbench');
|
|
32
|
+
assertSafeRegularFile(candidate, 0o600, { allowEmpty: false, role: '工作台项目candidate' });
|
|
33
|
+
const value = JSON.parse(fs.readFileSync(candidate, 'utf8'));
|
|
34
|
+
invariant(value.schemaVersion === 1 && Array.isArray(value.projects), 'IDP_WORKBENCH_CANDIDATE_INVALID', '工作台项目candidate版本无效');
|
|
35
|
+
const ids = new Set(); const roots = new Set();
|
|
36
|
+
const projects = value.projects.map((item) => {
|
|
37
|
+
invariant(ID.test(item?.id ?? '') && !ids.has(item.id), 'IDP_WORKBENCH_PROJECT_ID_INVALID', '项目id无效或重复'); ids.add(item.id);
|
|
38
|
+
const hostPath = safeDirectory(item.hostPath, `项目${item.id}`); invariant(!roots.has(hostPath), 'IDP_WORKBENCH_PROJECT_DUPLICATE', '项目宿主路径重复'); roots.add(hostPath);
|
|
39
|
+
const declaredBindings = Array.isArray(item.configBindings) ? item.configBindings : item.applicationConfigDir ? [{ relativePath: '.', applicationConfigDir: item.applicationConfigDir }] : [];
|
|
40
|
+
invariant(declaredBindings.length > 0, 'IDP_WORKBENCH_CONFIG_BINDING_REQUIRED', `项目${item.id}至少需要一个应用Config Binding`);
|
|
41
|
+
const relativePaths = new Set();
|
|
42
|
+
const configBindings = declaredBindings.map((binding, index) => {
|
|
43
|
+
const relativePath = binding?.relativePath === '.' ? '.' : path.posix.normalize(binding?.relativePath ?? '');
|
|
44
|
+
invariant(relativePath === '.' || (relativePath.length > 0 && !relativePath.startsWith('../') && !path.posix.isAbsolute(relativePath) && !relativePath.includes('\\')), 'IDP_WORKBENCH_PROJECT_RELATIVE_PATH_INVALID', `项目${item.id}的relativePath无效`);
|
|
45
|
+
invariant(!relativePaths.has(relativePath), 'IDP_WORKBENCH_PROJECT_RELATIVE_PATH_DUPLICATE', `项目${item.id}的relativePath重复`); relativePaths.add(relativePath);
|
|
46
|
+
const applicationRoot = fs.realpathSync.native(path.resolve(hostPath, relativePath));
|
|
47
|
+
invariant(applicationRoot === hostPath || applicationRoot.startsWith(`${hostPath}${path.sep}`), 'IDP_WORKBENCH_PROJECT_RELATIVE_PATH_ESCAPE', `项目${item.id}的应用路径逃逸项目目录`);
|
|
48
|
+
invariant(fs.existsSync(path.join(applicationRoot, 'project-capabilities.json')), 'IDP_WORKBENCH_APPLICATION_INVALID', `项目${item.id}的${relativePath}缺少project-capabilities.json`);
|
|
49
|
+
const applicationConfigDir = safeDirectory(binding.applicationConfigDir, `项目${item.id}/${relativePath} Config Dir`); assertOutsideRepositories(applicationConfigDir, `项目${item.id}/${relativePath} Config Dir`);
|
|
50
|
+
return { relativePath, applicationConfigDir, containerPath: relativePath === '.' ? `/workspaces/${item.id}` : `/workspaces/${item.id}/${relativePath}`, configDirectory: `/config/applications/${item.id}-${index}/local` };
|
|
51
|
+
});
|
|
52
|
+
return { id: item.id, hostPath, containerPath: `/workspaces/${item.id}`, configBindings };
|
|
53
|
+
});
|
|
54
|
+
return { candidate, projects };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function createWorkbenchProjectsPlan({ configRoot: input, dyytoRoot, candidateFile, output, now = new Date().toISOString() }) {
|
|
58
|
+
const root = configRoot(input), dyyto = safeDirectory(dyytoRoot, 'Dyyto仓库');
|
|
59
|
+
invariant(fs.existsSync(path.join(dyyto, 'compose.yaml')), 'IDP_WORKBENCH_DYYTO_INVALID', 'Dyyto仓库缺少compose.yaml');
|
|
60
|
+
const candidate = readCandidate(root, candidateFile);
|
|
61
|
+
const core = { schemaVersion: 'idp.workbench-project-plan/v1', dyytoRoot: dyyto, dyytoComposeDigest: hashFile(path.join(dyyto, 'compose.yaml')), candidate: { path: candidate.candidate, digest: hashFile(candidate.candidate) }, projects: candidate.projects, createdAt: now };
|
|
62
|
+
const plan = { ...core, planId: sha256(stableJson(core)) };
|
|
63
|
+
const plans = resolveContained(root, 'plans/workbench-projects', '工作台Plan目录'); secureDirectoryRoot(plans, '工作台Plan目录', { create: true });
|
|
64
|
+
const target = canonicalPlannedDirectory(output, '工作台Plan输出'); const relative = path.relative(plans, target);
|
|
65
|
+
invariant(relative && !relative.startsWith('..') && !path.isAbsolute(relative) && !fs.existsSync(target), 'IDP_WORKBENCH_PLAN_OUTPUT_INVALID', 'Plan必须以新文件写入IDP_CONFIG_DIR/plans/workbench-projects');
|
|
66
|
+
atomicWrite(target, `${JSON.stringify(plan, null, 2)}\n`, 0o600); return plan;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readPlan(root, file) {
|
|
70
|
+
invariant(path.isAbsolute(file ?? ''), 'IDP_WORKBENCH_PLAN_NOT_ABSOLUTE', '--plan必须是绝对路径'); const candidate = fs.realpathSync.native(file);
|
|
71
|
+
const plans = resolveContained(root, 'plans/workbench-projects', '工作台Plan目录'); const relative = path.relative(plans, candidate);
|
|
72
|
+
invariant(relative && !relative.startsWith('..') && !path.isAbsolute(relative), 'IDP_WORKBENCH_PLAN_OUTSIDE_CONFIG', 'Plan必须位于受管目录');
|
|
73
|
+
assertSafeRegularFile(candidate, 0o600, { allowEmpty: false, role: '工作台项目Plan' }); const plan = JSON.parse(fs.readFileSync(candidate, 'utf8')); const { planId, ...core } = plan;
|
|
74
|
+
invariant(plan.schemaVersion === 'idp.workbench-project-plan/v1' && sha256(stableJson(core)) === planId, 'IDP_WORKBENCH_PLAN_INVALID', '工作台项目Plan摘要无效'); return { plan, candidate };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function revalidate(plan) {
|
|
78
|
+
invariant(hashFile(path.join(plan.dyytoRoot, 'compose.yaml')) === plan.dyytoComposeDigest && hashFile(plan.candidate.path) === plan.candidate.digest, 'IDP_WORKBENCH_INPUT_CHANGED', 'Dyyto Compose或项目candidate已变化,请重新Plan');
|
|
79
|
+
for (const project of plan.projects) {
|
|
80
|
+
invariant(safeDirectory(project.hostPath, '项目目录') === project.hostPath, 'IDP_WORKBENCH_INPUT_CHANGED', '项目Binding路径已变化');
|
|
81
|
+
for (const binding of project.configBindings) invariant(safeDirectory(binding.applicationConfigDir, '应用Config Dir') === binding.applicationConfigDir, 'IDP_WORKBENCH_INPUT_CHANGED', '应用Config Binding路径已变化');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function rendered(plan, runtime) {
|
|
86
|
+
const bindingFile = path.join(runtime, 'project-bindings.json');
|
|
87
|
+
const compose = { services: { 'dyyto-workbench': { environment: { DYYTO_WORKBENCH_MODE: 'project-readonly', DYYTO_WORKBENCH_PROJECT_BINDINGS_FILE: '/var/lib/dyyto-workbench/project-bindings.json' }, volumes: [
|
|
88
|
+
{ type: 'bind', source: bindingFile, target: '/var/lib/dyyto-workbench/project-bindings.json', read_only: true },
|
|
89
|
+
...plan.projects.flatMap((project) => [{ type: 'bind', source: project.hostPath, target: project.containerPath, read_only: true }, ...project.configBindings.map((binding) => ({ type: 'bind', source: binding.applicationConfigDir, target: binding.configDirectory, read_only: true }))]),
|
|
90
|
+
] } } };
|
|
91
|
+
const bindings = { schemaVersion: 1, repositories: plan.projects.map((project) => ({ id: project.id, containerPath: project.containerPath })), projects: plan.projects.flatMap((project) => project.configBindings.map((binding) => ({ id: `${project.id}:${binding.relativePath}`, containerPath: binding.containerPath, configDirectory: binding.configDirectory }))) };
|
|
92
|
+
return { bindingFile, bindings, compose };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function applyWorkbenchProjectsPlan({ configRoot: input, planFile, runner = run }) {
|
|
96
|
+
const root = configRoot(input), { plan } = readPlan(root, planFile); revalidate(plan);
|
|
97
|
+
const runtime = resolveContained(root, 'runtime/workbench-projects', '工作台运行目录'); secureDirectoryRoot(runtime, '工作台运行目录', { create: true });
|
|
98
|
+
const output = rendered(plan, runtime), composeFile = path.join(runtime, 'compose.override.yaml'), stateFile = path.join(runtime, 'state.json');
|
|
99
|
+
const before = fs.existsSync(stateFile) ? fs.readFileSync(stateFile) : null;
|
|
100
|
+
if (before) atomicWrite(path.join(runtime, `preimage-${Date.now()}.json`), before, 0o600);
|
|
101
|
+
atomicWrite(output.bindingFile, `${JSON.stringify(output.bindings, null, 2)}\n`, 0o600); atomicWrite(composeFile, YAML.stringify(output.compose), 0o600);
|
|
102
|
+
runner('docker', ['compose', '--project-directory', plan.dyytoRoot, '--file', path.join(plan.dyytoRoot, 'compose.yaml'), '--file', composeFile, 'up', '-d', '--wait', '--remove-orphans'], { cwd: plan.dyytoRoot });
|
|
103
|
+
const mountTargets = [...plan.projects.map((project) => project.containerPath), ...plan.projects.flatMap((project) => project.configBindings.map((binding) => binding.configDirectory))];
|
|
104
|
+
const state = { schemaVersion: 'idp.workbench-project-runtime/v1', planId: plan.planId, dyytoRoot: plan.dyytoRoot, dyytoComposeFile: path.join(plan.dyytoRoot, 'compose.yaml'), composeFile, bindingFile: output.bindingFile, projects: output.bindings.projects, mountTargets };
|
|
105
|
+
atomicWrite(stateFile, `${JSON.stringify(state, null, 2)}\n`, 0o600); return { ...state, status: 'verified' };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function verifyWorkbenchProjects({ configRoot: input, runner = run }) {
|
|
109
|
+
const root = configRoot(input), stateFile = resolveContained(root, 'runtime/workbench-projects/state.json', '工作台运行态'); assertSafeRegularFile(stateFile, 0o600, { allowEmpty: false, role: '工作台运行态' });
|
|
110
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
111
|
+
const containerId = runner('docker', ['compose', '--project-directory', state.dyytoRoot, '--file', state.dyytoComposeFile, '--file', state.composeFile, 'ps', '-q', 'dyyto-workbench'], { capture: true }).stdout.trim();
|
|
112
|
+
invariant(containerId.length > 0 && !containerId.includes('\n'), 'IDP_WORKBENCH_CONTAINER_NOT_FOUND', '未找到唯一的Dyyto工作台容器');
|
|
113
|
+
const result = runner('docker', ['inspect', containerId, '--format', '{{json .Mounts}}'], { capture: true });
|
|
114
|
+
const mounts = JSON.parse(result.stdout); for (const target of state.mountTargets) {
|
|
115
|
+
const source = mounts.find((mount) => mount.Destination === target); invariant(source && source.RW === false, 'IDP_WORKBENCH_MOUNT_NOT_READONLY', `${target}未以只读方式挂载`);
|
|
116
|
+
}
|
|
117
|
+
return { schemaVersion: 'idp.workbench-project-verification/v1', status: 'verified', planId: state.planId, projects: state.projects.map((item) => item.id) };
|
|
118
|
+
}
|