@aipt/idp-deploy 0.1.2 → 0.1.4
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 +12 -17
- package/docs/config-dir-security.md +3 -0
- package/docs/local-deployment.md +3 -0
- package/docs/vibe-delivery.md +3 -0
- package/documentation-manifest.json +10 -0
- package/package.json +5 -1
- package/src/application-config-location.mjs +50 -0
- package/src/cli.mjs +24 -11
- package/src/local-source.mjs +50 -49
- package/src/workbench-projects.mjs +7 -4
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# IDP Deploy
|
|
2
2
|
|
|
3
|
+
业务项目不复制本仓部署长文档。Dyyto Bootstrap 锁定本包版本和 `local-deployment` 文档 ID;`dyyto docs show --id local-deployment` 提供面向 Agent 的精简入口。
|
|
4
|
+
|
|
3
5
|
`idp-deploy` 是企业内部开发者平台的轻量部署与运维控制面。它把 PostgreSQL、私有 npm Registry、Tech、Flow、Portal、SmartGo 和 Caddy 组合为可独立启停的服务片段;Dyyto、Bench、Forge 仍是按需运行的 CLI/MCP 工具,不被强制常驻。SmartGo 是显式选择的业务 Profile,不进入默认 IDP Core。
|
|
4
6
|
|
|
5
7
|
Kubernetes/GitOps 已进入当前 CLI:应用通过 ComponentContract、Bench EnvironmentBinding 和 ReleaseCandidate 生成不可变 DeploymentPlan,再由同一 Kernel 完成确定性 Render、Repository Apply、Git commit、Preimage/CAS 保护和 Repository Verify。它保留历史 `kubernetes-config` 的中央期望状态与 Argo 收敛能力,但不复制跨仓 `sed`、可变 tag、明文 Secret 或宽权限。完整合同与所有权见 [`docs/features/kubernetes-gitops-deployment-backend/DESIGN.md`](docs/features/kubernetes-gitops-deployment-backend/DESIGN.md)。
|
|
@@ -72,31 +74,29 @@ Bench 原有部署资产已按能力而非目录整体接收:受控运维语
|
|
|
72
74
|
export IDP_DEPLOY_ROOT=/Users/dyyto/Desktop/works/sources/spaces/idp-deploy
|
|
73
75
|
export APPLICATION_ROOT=/absolute/path/to/new-project
|
|
74
76
|
export IDP_CONFIG_DIR=/Users/Shared/company/idp/config
|
|
77
|
+
export APP_CONFIG_DIR=/Users/Shared/company/app/config
|
|
75
78
|
|
|
76
79
|
# 1. 根据Dyyto ApplicationConfigContract创建应用.env、secrets/和certs/;重入不覆盖已有值。
|
|
77
80
|
node "$IDP_DEPLOY_ROOT/bin/idpctl.mjs" app config-init \
|
|
78
81
|
--project "$APPLICATION_ROOT" \
|
|
79
|
-
--
|
|
82
|
+
--environment local
|
|
80
83
|
|
|
81
84
|
# 2. 填写命令不能代替外部系统签发的required Secret;不要把值写入项目。
|
|
82
|
-
chmod 600 "$
|
|
85
|
+
chmod 600 "$APP_CONFIG_DIR/<application-id>/local/secrets/"*
|
|
83
86
|
|
|
84
87
|
# 3. 生成不可变Local DeploymentPlan;本步只执行docker compose config和安全检查。
|
|
85
88
|
node "$IDP_DEPLOY_ROOT/bin/idpctl.mjs" local app plan \
|
|
86
89
|
--project "$APPLICATION_ROOT" \
|
|
87
90
|
--compose-file "$APPLICATION_ROOT/deploy/compose.local.yaml" \
|
|
88
|
-
--output "$
|
|
89
|
-
--config-dir "$IDP_CONFIG_DIR"
|
|
91
|
+
--output "$APP_CONFIG_DIR/<application-id>/local/plans/local-applications/<application-id>-local.json"
|
|
90
92
|
|
|
91
93
|
# 4. 审查Plan中的项目、输入摘要、Compose摘要和promotable:false后执行。
|
|
92
94
|
node "$IDP_DEPLOY_ROOT/bin/idpctl.mjs" local app apply \
|
|
93
|
-
--plan "$
|
|
94
|
-
--config-dir "$IDP_CONFIG_DIR"
|
|
95
|
+
--plan "$APP_CONFIG_DIR/<application-id>/local/plans/local-applications/<application-id>-local.json"
|
|
95
96
|
|
|
96
97
|
# 5. 随时重新核验精确Plan与容器健康状态。
|
|
97
98
|
node "$IDP_DEPLOY_ROOT/bin/idpctl.mjs" local app verify \
|
|
98
|
-
--plan "$
|
|
99
|
-
--config-dir "$IDP_CONFIG_DIR"
|
|
99
|
+
--plan "$APP_CONFIG_DIR/<application-id>/local/plans/local-applications/<application-id>-local.json"
|
|
100
100
|
```
|
|
101
101
|
|
|
102
102
|
代码、Dockerfile、Compose、`.env` 或任一 `*_FILE` 发生变化后,旧 Plan 会 fail-closed;删除旧 Plan 文件并用新的输出文件名重新 `plan`、审查和 `apply`。停止保留容器,移除不删除 Volume:
|
|
@@ -363,6 +363,7 @@ node bin/idpctl.mjs sources import bench \
|
|
|
363
363
|
|
|
364
364
|
```bash
|
|
365
365
|
export IDP_CONFIG_DIR=/Users/Shared/company/idp/config
|
|
366
|
+
export APP_CONFIG_DIR=/Users/Shared/company/app/config
|
|
366
367
|
node bin/idpctl.mjs workbench projects init
|
|
367
368
|
```
|
|
368
369
|
|
|
@@ -376,21 +377,15 @@ node bin/idpctl.mjs workbench projects init
|
|
|
376
377
|
"id": "apex",
|
|
377
378
|
"hostPath": "/Users/dyyto/Desktop/works/sources/spaces/apex",
|
|
378
379
|
"configBindings": [
|
|
379
|
-
{
|
|
380
|
-
|
|
381
|
-
"applicationConfigDir": "/Users/Shared/company/apps/apex-client/local"
|
|
382
|
-
},
|
|
383
|
-
{
|
|
384
|
-
"relativePath": "apps/server",
|
|
385
|
-
"applicationConfigDir": "/Users/Shared/company/apps/apex-server/local"
|
|
386
|
-
}
|
|
380
|
+
{ "relativePath": "apps/client" },
|
|
381
|
+
{ "relativePath": "apps/server" }
|
|
387
382
|
]
|
|
388
383
|
}
|
|
389
384
|
]
|
|
390
385
|
}
|
|
391
386
|
```
|
|
392
387
|
|
|
393
|
-
|
|
388
|
+
`APP_CONFIG_DIR` 是业务配置总根;上例自动派生 `apex/apps/client/local` 和 `apex/apps/server/local`。各 Config Dir 应先由 `app config-init --workspace-root <apex-root>` 创建并由操作者填写 `.env`、`secrets/` 和 `certs/`。随后生成不可变 Plan、应用并验证:
|
|
394
389
|
|
|
395
390
|
```bash
|
|
396
391
|
node bin/idpctl.mjs workbench projects plan \
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"package": "@aipt/idp-deploy",
|
|
4
|
+
"version": "0.1.4",
|
|
5
|
+
"documents": [
|
|
6
|
+
{ "id": "config-dir-security", "path": "docs/config-dir-security.md", "digest": "sha256:057b3f07104255812d2f26f59842c42f995305c64c5494889fee59772041e324" },
|
|
7
|
+
{ "id": "local-deployment", "path": "docs/local-deployment.md", "digest": "sha256:4d86d7e7ed8619fa750430989afefe71e033673adedb924bb60b4272d7d2e3f6" },
|
|
8
|
+
{ "id": "vibe-delivery", "path": "docs/vibe-delivery.md", "digest": "sha256:3e6dee64c85417c1402ad3d9433e24c223acef584bc2f71cec0711085e5438e6" }
|
|
9
|
+
]
|
|
10
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aipt/idp-deploy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "IDP 私有化部署、外部配置、备份恢复与交付编排 CLI。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,11 @@
|
|
|
13
13
|
"contracts",
|
|
14
14
|
"release",
|
|
15
15
|
"governance",
|
|
16
|
+
"docs/config-dir-security.md",
|
|
17
|
+
"docs/local-deployment.md",
|
|
18
|
+
"docs/vibe-delivery.md",
|
|
16
19
|
"docs/restore-runbook.md",
|
|
20
|
+
"documentation-manifest.json",
|
|
17
21
|
"README.md",
|
|
18
22
|
"deploy.sh"
|
|
19
23
|
],
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { invariant } from './errors.mjs';
|
|
4
|
+
import { assertOutsideRepositories, secureDirectoryRoot } from './security.mjs';
|
|
5
|
+
|
|
6
|
+
const ID = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
|
|
7
|
+
const UNIT = /^(?:apps|packages)\/[a-z0-9][a-z0-9._-]*$/u;
|
|
8
|
+
|
|
9
|
+
function manifest(root) {
|
|
10
|
+
try { return JSON.parse(fs.readFileSync(path.join(root, 'project-capabilities.json'), 'utf8')); }
|
|
11
|
+
catch { return null; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function discoverWorkspace(projectRoot, explicitWorkspaceRoot) {
|
|
15
|
+
const project = fs.realpathSync.native(projectRoot);
|
|
16
|
+
if (explicitWorkspaceRoot) {
|
|
17
|
+
invariant(path.isAbsolute(explicitWorkspaceRoot), 'IDP_WORKSPACE_ROOT_NOT_ABSOLUTE', '--workspace-root必须是绝对路径');
|
|
18
|
+
const root = fs.realpathSync.native(explicitWorkspaceRoot), relative = path.relative(root, project);
|
|
19
|
+
invariant((relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) && manifest(root)?.profile?.id === 'workspace-root', 'IDP_WORKSPACE_ROOT_INVALID', '--workspace-root必须是包含项目的Dyyto Workspace Root');
|
|
20
|
+
return relative === '' ? null : { root, id: path.basename(root), unitPath: relative.split(path.sep).join('/') };
|
|
21
|
+
}
|
|
22
|
+
let cursor = path.dirname(project);
|
|
23
|
+
while (cursor !== path.dirname(cursor)) {
|
|
24
|
+
if (manifest(cursor)?.profile?.id === 'workspace-root') {
|
|
25
|
+
const unitPath = path.relative(cursor, project).split(path.sep).join('/');
|
|
26
|
+
return { root: cursor, id: path.basename(cursor), unitPath };
|
|
27
|
+
}
|
|
28
|
+
cursor = path.dirname(cursor);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveApplicationConfigDirectory({ projectRoot, applicationId, appConfigRoot, applicationConfigRoot, workspaceRoot, environment = 'local', create = false }) {
|
|
34
|
+
invariant(ID.test(applicationId ?? ''), 'IDP_APPLICATION_CONFIG_IDENTITY_INVALID', 'Application identity无效');
|
|
35
|
+
invariant(ID.test(environment), 'IDP_APPLICATION_ENVIRONMENT_INVALID', '应用环境名称无效');
|
|
36
|
+
if (applicationConfigRoot) {
|
|
37
|
+
const app = secureDirectoryRoot(applicationConfigRoot, '应用最终Config Dir', { create }); assertOutsideRepositories(app, '应用最终Config Dir');
|
|
38
|
+
return { root: app, app, environment, workspace: null, relativeDirectory: '.' };
|
|
39
|
+
}
|
|
40
|
+
invariant(path.isAbsolute(appConfigRoot ?? ''), 'APP_CONFIG_DIR_REQUIRED', '请设置绝对APP_CONFIG_DIR或传入--application-config-dir');
|
|
41
|
+
const root = secureDirectoryRoot(appConfigRoot, 'APP_CONFIG_DIR', { create }); assertOutsideRepositories(root, 'APP_CONFIG_DIR');
|
|
42
|
+
const workspace = discoverWorkspace(projectRoot, workspaceRoot);
|
|
43
|
+
if (workspace) invariant(UNIT.test(workspace.unitPath), 'IDP_WORKSPACE_UNIT_INVALID', 'Workspace应用必须位于apps/<name>或packages/<name>');
|
|
44
|
+
const relativeDirectory = workspace ? `${workspace.id}/${workspace.unitPath}/${environment}` : `${applicationId}/${environment}`;
|
|
45
|
+
const app = path.resolve(root, ...relativeDirectory.split('/'));
|
|
46
|
+
invariant(app.startsWith(`${root}${path.sep}`), 'APP_CONFIG_DIRECTORY_ESCAPE', '应用Config Dir逃逸APP_CONFIG_DIR');
|
|
47
|
+
if (create) secureDirectoryRoot(app, '应用Config Dir', { create: true });
|
|
48
|
+
else secureDirectoryRoot(app, '应用Config Dir');
|
|
49
|
+
return { root: app, app, environment, workspace, relativeDirectory };
|
|
50
|
+
}
|
package/src/cli.mjs
CHANGED
|
@@ -30,6 +30,18 @@ export function configRootFrom(args, { environment = process.env } = {}) {
|
|
|
30
30
|
return path.resolve(value);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export function appConfigRootFrom(args, { environment = process.env, required = false } = {}) {
|
|
34
|
+
const index = args.indexOf('--app-config-dir');
|
|
35
|
+
if (index >= 0 && (!args[index + 1] || args[index + 1].startsWith('--'))) throw new IdpError('APP_CONFIG_ARGUMENT_MISSING', '--app-config-dir后必须提供绝对路径');
|
|
36
|
+
const value = index >= 0 ? args[index + 1] : environment.APP_CONFIG_DIR;
|
|
37
|
+
if (!value) {
|
|
38
|
+
if (required) throw new IdpError('APP_CONFIG_DIR_REQUIRED', '请设置APP_CONFIG_DIR或传入--app-config-dir');
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (!path.isAbsolute(value)) throw new IdpError('APP_CONFIG_DIR_NOT_ABSOLUTE', 'APP_CONFIG_DIR必须是绝对路径');
|
|
42
|
+
return path.resolve(value);
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
export function readSourceImportFile(configRoot, rawCandidate) {
|
|
34
46
|
if (!path.isAbsolute(rawCandidate ?? '')) throw new IdpError('IDP_SOURCE_FILE_NOT_ABSOLUTE', '--file必须是原始绝对路径');
|
|
35
47
|
const { root } = doctorConfig(configRoot, { requireConfigured: false, profile: 'registry' });
|
|
@@ -72,7 +84,7 @@ function print(value) {
|
|
|
72
84
|
export async function main(args) {
|
|
73
85
|
const [command, subcommand] = args;
|
|
74
86
|
if (!command || ['help', '--help', '-h'].includes(command)) {
|
|
75
|
-
process.stdout.write('本地项目命令:\n app config-init --project
|
|
87
|
+
process.stdout.write('本地项目命令:\n app config-init --project <绝对项目目录> [--workspace-root <绝对目录>] [--environment local]\n local app plan --project <绝对目录> [--workspace-root <绝对目录>] --compose-file <绝对文件> --output <应用Config Dir/plans/local-applications/...>\n local app apply|verify|stop --plan <绝对文件>\n local app remove --plan <绝对文件> --confirm <application-id>\n\nAPP_CONFIG_DIR是全部业务工作区/应用配置总根;显式--application-config-dir仅兼容覆盖最终应用目录。\n\n');
|
|
76
88
|
process.stdout.write('Docker工作台项目命令:\n workbench projects init\n workbench projects plan --dyyto-root <绝对目录> --file <candidate> --output <Plan>\n workbench projects apply --plan <Plan>\n workbench projects verify\n\n');
|
|
77
89
|
print(`idpctl:私有IDP部署与运维工具\n\n命令:\n config init\n config generate-secrets\n config snapshot\n release deploy [profile] [--version <semver>] [--oci-repository-root <host/namespace/repository>]\n gitops app plan-create|plan-update|plan-remove --component-contract <绝对路径> --environment-binding <绝对路径> --release-candidate <绝对路径> --gitops-root <绝对路径> --output <IDP_CONFIG_DIR/plans/...>\n gitops app apply --plan <绝对路径>\n gitops app verify --application <名称> --environment <名称> --gitops-root <绝对路径>\n foundation doctor|verify [--context <kube-context>]\n foundation directory sync [--argo-url <URL>] [--rollouts-url <URL>] [--registry-url <URL>]\n sources import dyyto|bench --stdin\n sources import dyyto|bench --file <IDP_CONFIG_DIR/imports/...>\n images lock\n doctor [profile] [--require-configured]\n compose <profile>\n pull <profile>\n up <profile>\n switch <profile>\n down <profile>\n verify <profile>\n bindings sync <active-profile>\n accept mac-m4 <profile> --confirm-restart <IDP_INSTANCE_ID>\n backup create [profile]\n backup verify <generation-id>\n restore test <generation-id>\n restore apply <generation-id>\n restore verify <candidate-id>\n restore promote <candidate-id> --confirm <candidate-id>\n contracts verify config-release|release-candidate|deployment-evidence --file <绝对JSON路径>\n\nProfile:registry、tech-only、foundation、flow、portal、core、full、business-smartgo、core-smartgo;release deploy默认foundation。SmartGo不会进入默认core/full,需显式选择business-smartgo或core-smartgo。\n所有命令只通过IDP_CONFIG_DIR读取仓库外配置;Secret不会打印到终端。GitOps写入必须来自不可变Plan,Apply复验输入、HEAD与Preimage并在外部配置根保存恢复证据。release deploy在同一实例Lease内完成自研镜像的amd64/arm64构建与digest绑定、已有部署备份和隔离恢复演练、镜像锁、Compose、Verifier与Binding事务。bindings sync只从已启用且verify通过的当前运行事实生成Bench ServiceBinding,并保护用户修改与历史Preimage。跨Profile必须使用switch,它会显式清理旧Profile Orphan。restore apply先生成隔离候选;promote保留物理或逻辑Preimage并在失败时自动回滚。`);
|
|
78
90
|
process.stdout.write('无集群开发者命令:\n app inspect --component-contract <绝对路径> [--environment-binding <绝对路径>]\n workflow generate|verify --file <绝对路径>\n build plan --component-contract <绝对路径> --repository <Git URL> --commit <40位SHA> --image-repository <OCI仓库> --output <绝对路径>\n build trigger --plan <绝对路径> --provider manual --output <绝对路径>\n build status --trigger <绝对路径>\n附加只读命令:\n lifecycle check\n contracts verify ...\n');
|
|
@@ -131,19 +143,20 @@ export async function main(args) {
|
|
|
131
143
|
return;
|
|
132
144
|
}
|
|
133
145
|
const configRoot = configRootFrom(args);
|
|
146
|
+
const appConfigRoot = appConfigRootFrom(args);
|
|
134
147
|
if (command === 'app' && subcommand === 'config-init') {
|
|
135
|
-
const values = namedArguments(args.slice(2), ['--project', '--config-dir', '--application-config-dir']);
|
|
148
|
+
const values = namedArguments(args.slice(2), ['--project', '--config-dir', '--app-config-dir', '--application-config-dir', '--workspace-root', '--environment']);
|
|
136
149
|
invariantNamed(values, '--project', 'app config-init');
|
|
137
|
-
print(initializeApplicationConfig({ configRoot, projectRoot: values['--project'], applicationConfigRoot: values['--application-config-dir'] }));
|
|
150
|
+
print(initializeApplicationConfig({ configRoot, appConfigRoot, projectRoot: values['--project'], applicationConfigRoot: values['--application-config-dir'], workspaceRoot: values['--workspace-root'], environment: values['--environment'] ?? 'local' }));
|
|
138
151
|
return;
|
|
139
152
|
}
|
|
140
153
|
if (command === 'workbench' && subcommand === 'projects') {
|
|
141
154
|
const action = args[2];
|
|
142
|
-
const values = namedArguments(args.slice(3), ['--dyyto-root', '--file', '--output', '--plan', '--config-dir']);
|
|
155
|
+
const values = namedArguments(args.slice(3), ['--dyyto-root', '--file', '--output', '--plan', '--config-dir', '--app-config-dir']);
|
|
143
156
|
if (action === 'init') print(initializeWorkbenchProjects({ configRoot }));
|
|
144
157
|
else if (action === 'plan') {
|
|
145
158
|
for (const key of ['--dyyto-root', '--file', '--output']) invariantNamed(values, key, 'workbench projects plan');
|
|
146
|
-
print(createWorkbenchProjectsPlan({ configRoot, dyytoRoot: values['--dyyto-root'], candidateFile: values['--file'], output: values['--output'] }));
|
|
159
|
+
print(createWorkbenchProjectsPlan({ configRoot, appConfigRoot, dyytoRoot: values['--dyyto-root'], candidateFile: values['--file'], output: values['--output'] }));
|
|
147
160
|
} else if (action === 'apply') { invariantNamed(values, '--plan', 'workbench projects apply'); print(applyWorkbenchProjectsPlan({ configRoot, planFile: values['--plan'] })); }
|
|
148
161
|
else if (action === 'verify') print(verifyWorkbenchProjects({ configRoot }));
|
|
149
162
|
else throw new IdpError('IDP_WORKBENCH_ACTION_UNKNOWN', `未知工作台项目动作:${action ?? ''}`);
|
|
@@ -151,17 +164,17 @@ export async function main(args) {
|
|
|
151
164
|
}
|
|
152
165
|
if (command === 'local' && subcommand === 'app') {
|
|
153
166
|
const action = args[2];
|
|
154
|
-
const values = namedArguments(args.slice(3), ['--project', '--compose-file', '--output', '--plan', '--confirm', '--config-dir', '--application-config-dir']);
|
|
167
|
+
const values = namedArguments(args.slice(3), ['--project', '--workspace-root', '--environment', '--compose-file', '--output', '--plan', '--confirm', '--config-dir', '--app-config-dir', '--application-config-dir']);
|
|
155
168
|
if (action === 'plan') {
|
|
156
169
|
for (const key of ['--project', '--compose-file', '--output']) invariantNamed(values, key, 'local app plan');
|
|
157
|
-
print(createLocalSourcePlan({ configRoot, applicationConfigRoot: values['--application-config-dir'], projectRoot: values['--project'], composeFile: values['--compose-file'], output: values['--output'] }));
|
|
170
|
+
print(createLocalSourcePlan({ configRoot, appConfigRoot, applicationConfigRoot: values['--application-config-dir'], projectRoot: values['--project'], workspaceRoot: values['--workspace-root'], environment: values['--environment'] ?? 'local', composeFile: values['--compose-file'], output: values['--output'] }));
|
|
158
171
|
return;
|
|
159
172
|
}
|
|
160
173
|
invariantNamed(values, '--plan', `local app ${action ?? ''}`);
|
|
161
|
-
if (action === 'apply') print(applyLocalSourceDeployment({ configRoot, applicationConfigRoot: values['--application-config-dir'], planFile: values['--plan'] }));
|
|
162
|
-
else if (action === 'verify') print(verifyLocalSourceDeployment({ configRoot, applicationConfigRoot: values['--application-config-dir'], planFile: values['--plan'] }));
|
|
163
|
-
else if (action === 'stop') print(stopLocalSourceDeployment({ configRoot, applicationConfigRoot: values['--application-config-dir'], planFile: values['--plan'] }));
|
|
164
|
-
else if (action === 'remove') print(removeLocalSourceDeployment({ configRoot, applicationConfigRoot: values['--application-config-dir'], planFile: values['--plan'], confirmation: values['--confirm'] }));
|
|
174
|
+
if (action === 'apply') print(applyLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot: values['--application-config-dir'], planFile: values['--plan'] }));
|
|
175
|
+
else if (action === 'verify') print(verifyLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot: values['--application-config-dir'], planFile: values['--plan'] }));
|
|
176
|
+
else if (action === 'stop') print(stopLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot: values['--application-config-dir'], planFile: values['--plan'] }));
|
|
177
|
+
else if (action === 'remove') print(removeLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot: values['--application-config-dir'], planFile: values['--plan'], confirmation: values['--confirm'] }));
|
|
165
178
|
else throw new IdpError('IDP_LOCAL_ACTION_UNKNOWN', `未知本地应用动作:${action ?? ''}`);
|
|
166
179
|
return;
|
|
167
180
|
}
|
package/src/local-source.mjs
CHANGED
|
@@ -4,6 +4,7 @@ import { IdpError, invariant } from './errors.mjs';
|
|
|
4
4
|
import { hashFile, sha256, stableJson } from './hash.mjs';
|
|
5
5
|
import { run } from './process.mjs';
|
|
6
6
|
import { assertOutsideRepositories, assertSafeRegularFile, atomicWrite, canonicalPlannedDirectory, parseEnv, resolveContained, secureDirectoryRoot } from './security.mjs';
|
|
7
|
+
import { resolveApplicationConfigDirectory } from './application-config-location.mjs';
|
|
7
8
|
|
|
8
9
|
const APPLICATION_ID = /^[a-z0-9][a-z0-9._-]{0,127}$/u;
|
|
9
10
|
const ENV_NAME = /^[A-Z][A-Z0-9_]{0,127}$/u;
|
|
@@ -72,12 +73,8 @@ function validateApplicationConfigContract(contract, manifest) {
|
|
|
72
73
|
return contract;
|
|
73
74
|
}
|
|
74
75
|
|
|
75
|
-
function applicationDirectory(configRoot, id, { create = false, applicationConfigRoot } = {}) {
|
|
76
|
-
if (applicationConfigRoot) {
|
|
77
|
-
const app = secureDirectoryRoot(applicationConfigRoot, 'APP_CONFIG_DIR', { create });
|
|
78
|
-
assertOutsideRepositories(app, 'APP_CONFIG_DIR');
|
|
79
|
-
return { root: app, app };
|
|
80
|
-
}
|
|
76
|
+
function applicationDirectory(configRoot, id, { create = false, applicationConfigRoot, appConfigRoot, projectRoot, workspaceRoot, environment = 'local' } = {}) {
|
|
77
|
+
if (applicationConfigRoot || appConfigRoot) return resolveApplicationConfigDirectory({ projectRoot, applicationId: id, appConfigRoot, applicationConfigRoot, workspaceRoot, environment, create });
|
|
81
78
|
const root = secureDirectoryRoot(configRoot, 'IDP_CONFIG_DIR', { create });
|
|
82
79
|
assertOutsideRepositories(root, 'IDP_CONFIG_DIR');
|
|
83
80
|
const applications = resolveContained(root, 'applications', '应用配置目录');
|
|
@@ -96,9 +93,9 @@ function savePreimage(root, id, name, bytes) {
|
|
|
96
93
|
return { path: candidate, digest };
|
|
97
94
|
}
|
|
98
95
|
|
|
99
|
-
export function initializeApplicationConfig({ configRoot, projectRoot, applicationConfigRoot }) {
|
|
96
|
+
export function initializeApplicationConfig({ configRoot, appConfigRoot, projectRoot, applicationConfigRoot, workspaceRoot, environment = 'local' }) {
|
|
100
97
|
const project = readProject(projectRoot);
|
|
101
|
-
const { root, app } = applicationDirectory(configRoot, project.contract.application.id, { create: true, applicationConfigRoot });
|
|
98
|
+
const { root, app, relativeDirectory, workspace } = applicationDirectory(configRoot, project.contract.application.id, { create: true, applicationConfigRoot, appConfigRoot, projectRoot: project.root, workspaceRoot, environment });
|
|
102
99
|
for (const name of ['secrets', 'certs']) secureDirectoryRoot(path.join(app, name), `应用${name}目录`, { create: true });
|
|
103
100
|
const envFile = path.join(app, '.env');
|
|
104
101
|
const before = fs.existsSync(envFile) ? fs.readFileSync(envFile) : Buffer.from('');
|
|
@@ -121,11 +118,11 @@ export function initializeApplicationConfig({ configRoot, projectRoot, applicati
|
|
|
121
118
|
preimage = savePreimage(root, project.contract.application.id, 'env', before);
|
|
122
119
|
atomicWrite(envFile, `${before.toString('utf8').trimEnd()}\n# 由app config-init补齐;已有值未覆盖。\n${additions.join('\n')}\n`, 0o600);
|
|
123
120
|
}
|
|
124
|
-
return { schemaVersion: 'idp.application-config-materialization/v1', status: 'ready', application: project.contract.application.id, directory: app, environmentFile: envFile, createdFiles, additions: additions.map((line) => line.slice(0, line.indexOf('='))), ...(preimage ? { preimage } : {}) };
|
|
121
|
+
return { schemaVersion: 'idp.application-config-materialization/v1', status: 'ready', application: project.contract.application.id, environment, directory: app, relativeDirectory, workspace: workspace ? { id: workspace.id, root: workspace.root, unitPath: workspace.unitPath } : null, environmentFile: envFile, createdFiles, additions: additions.map((line) => line.slice(0, line.indexOf('='))), ...(preimage ? { preimage } : {}) };
|
|
125
122
|
}
|
|
126
123
|
|
|
127
|
-
function inspectApplicationConfig(configRoot, contract, applicationConfigRoot) {
|
|
128
|
-
const { app } = applicationDirectory(configRoot, contract.application.id, { applicationConfigRoot });
|
|
124
|
+
function inspectApplicationConfig(configRoot, contract, { applicationConfigRoot, appConfigRoot, projectRoot, workspaceRoot, environment = 'local' }) {
|
|
125
|
+
const { app } = applicationDirectory(configRoot, contract.application.id, { applicationConfigRoot, appConfigRoot, projectRoot, workspaceRoot, environment });
|
|
129
126
|
const envFile = path.join(app, '.env');
|
|
130
127
|
assertSafeRegularFile(envFile, 0o600, { allowEmpty: contract.bindings.length === 0, role: '应用.env' });
|
|
131
128
|
const env = parseEnv(fs.readFileSync(envFile, 'utf8'), envFile);
|
|
@@ -152,16 +149,16 @@ function executeCompose(runner, args, cwd, capture = true) {
|
|
|
152
149
|
return runner('docker', args, { cwd, capture, maxBuffer: 16 * 1024 * 1024 });
|
|
153
150
|
}
|
|
154
151
|
|
|
155
|
-
function normalizedCompose({ projectRoot, composeFile, envFile, composeProject, runner }) {
|
|
156
|
-
const result = executeCompose(runner, ['compose', '--project-name', composeProject, '--env-file', envFile, '--file', composeFile, 'config', '--format', 'json'],
|
|
152
|
+
function normalizedCompose({ projectRoot, sourceRoot, composeFile, envFile, composeProject, runner }) {
|
|
153
|
+
const result = executeCompose(runner, ['compose', '--project-name', composeProject, '--env-file', envFile, '--file', composeFile, 'config', '--format', 'json'], sourceRoot, true);
|
|
157
154
|
let document;
|
|
158
155
|
try { document = JSON.parse(result.stdout); }
|
|
159
156
|
catch (error) { throw new IdpError('IDP_LOCAL_COMPOSE_JSON_INVALID', 'docker compose未返回有效JSON', { cause: error.message }); }
|
|
160
|
-
validateCompose(document, projectRoot);
|
|
157
|
+
validateCompose(document, projectRoot, sourceRoot);
|
|
161
158
|
return { document, bytes: `${stableJson(document)}\n` };
|
|
162
159
|
}
|
|
163
160
|
|
|
164
|
-
function validateCompose(document, projectRoot) {
|
|
161
|
+
function validateCompose(document, projectRoot, sourceRoot) {
|
|
165
162
|
const services = document?.services;
|
|
166
163
|
invariant(services && typeof services === 'object' && Object.keys(services).length > 0, 'IDP_LOCAL_COMPOSE_INVALID', 'Compose必须至少声明一个服务');
|
|
167
164
|
let healthchecks = 0;
|
|
@@ -173,9 +170,9 @@ function validateCompose(document, projectRoot) {
|
|
|
173
170
|
if (service.build) {
|
|
174
171
|
const context = typeof service.build === 'string' ? service.build : service.build.context;
|
|
175
172
|
invariant(typeof context === 'string', 'IDP_LOCAL_COMPOSE_BUILD_INVALID', `服务${name}构建上下文无效`);
|
|
176
|
-
const absoluteContext = path.resolve(projectRoot, context);
|
|
177
|
-
const contextRelative = path.relative(
|
|
178
|
-
invariant(contextRelative === '' || (!contextRelative.startsWith('..') && !path.isAbsolute(contextRelative)), '
|
|
173
|
+
const absoluteContext = path.isAbsolute(context) ? context : path.resolve(projectRoot, context);
|
|
174
|
+
const contextRelative = path.relative(sourceRoot, absoluteContext);
|
|
175
|
+
invariant(contextRelative === '' || (!contextRelative.startsWith('..') && !path.isAbsolute(contextRelative)), 'IDP_LOCAL_COMPOSE_BUILD_OUTSIDE_WORKSPACE', `服务${name}构建上下文必须位于Workspace源码边界内`);
|
|
179
176
|
}
|
|
180
177
|
if (service.healthcheck && !service.healthcheck.disable) healthchecks += 1;
|
|
181
178
|
for (const [key, value] of Object.entries(service.environment ?? {})) {
|
|
@@ -199,24 +196,27 @@ function validateCompose(document, projectRoot) {
|
|
|
199
196
|
|
|
200
197
|
function planIdentity(core) { return sha256(stableJson(core)); }
|
|
201
198
|
|
|
202
|
-
export function createLocalSourcePlan({ configRoot, applicationConfigRoot, projectRoot, composeFile, output, runner = run, now = new Date().toISOString() }) {
|
|
199
|
+
export function createLocalSourcePlan({ configRoot, appConfigRoot, applicationConfigRoot, projectRoot, workspaceRoot, environment = 'local', composeFile, output, runner = run, now = new Date().toISOString() }) {
|
|
203
200
|
const project = readProject(projectRoot);
|
|
201
|
+
const sourceRoot = workspaceRoot ? fs.realpathSync.native(workspaceRoot) : project.root;
|
|
202
|
+
const sourceRelative = path.relative(sourceRoot, project.root);
|
|
203
|
+
invariant(sourceRelative === '' || (!sourceRelative.startsWith('..') && !path.isAbsolute(sourceRelative)), 'IDP_LOCAL_PROJECT_OUTSIDE_WORKSPACE', '项目必须位于--workspace-root内');
|
|
204
204
|
invariant(path.isAbsolute(composeFile ?? ''), 'IDP_LOCAL_COMPOSE_NOT_ABSOLUTE', '--compose-file必须是绝对路径');
|
|
205
205
|
const composePath = fs.realpathSync.native(composeFile);
|
|
206
206
|
const relativeCompose = path.relative(project.root, composePath);
|
|
207
207
|
invariant(relativeCompose && !relativeCompose.startsWith('..') && !path.isAbsolute(relativeCompose), 'IDP_LOCAL_COMPOSE_OUTSIDE_PROJECT', 'Compose文件必须位于项目内');
|
|
208
208
|
assertSafeRegularFile(composePath, 0o644, { allowEmpty: false, role: '本地Compose文件' });
|
|
209
|
-
const config = inspectApplicationConfig(configRoot, project.contract, applicationConfigRoot);
|
|
210
|
-
const composeProject = `idp-${project.contract.application.id}
|
|
211
|
-
const normalized = normalizedCompose({ projectRoot: project.root, composeFile: composePath, envFile: config.envFile, composeProject, runner });
|
|
209
|
+
const config = inspectApplicationConfig(configRoot, project.contract, { applicationConfigRoot, appConfigRoot, projectRoot: project.root, workspaceRoot, environment });
|
|
210
|
+
const composeProject = `idp-${project.contract.application.id}-${environment}`.replace(/[^a-z0-9_-]/gu, '-').slice(0, 63);
|
|
211
|
+
const normalized = normalizedCompose({ projectRoot: project.root, sourceRoot, composeFile: composePath, envFile: config.envFile, composeProject, runner });
|
|
212
212
|
const core = {
|
|
213
213
|
schemaVersion: PLAN_SCHEMA, mode: 'local-source', promotable: false, application: project.contract.application.id,
|
|
214
|
-
project: { root: project.root, sourceDigest: projectSourceDigest(
|
|
214
|
+
project: { root: project.root, workspaceRoot: sourceRoot, unitPath: sourceRelative.split(path.sep).join('/') || '.', sourceDigest: projectSourceDigest(sourceRoot), manifest: { path: project.manifestFile, digest: hashFile(project.manifestFile) }, applicationConfigContract: { path: project.contractFile, digest: hashFile(project.contractFile) } },
|
|
215
215
|
config: { directory: config.app, environmentFile: config.envFile, environmentDigest: config.envDigest, fileBindings: config.fileBindings },
|
|
216
216
|
compose: { path: composePath, sourceDigest: hashFile(composePath), normalizedDigest: sha256(normalized.bytes) }, composeProject, createdAt: now,
|
|
217
217
|
};
|
|
218
218
|
const plan = { ...core, planId: planIdentity(core) };
|
|
219
|
-
const { root } = applicationDirectory(configRoot, project.contract.application.id, { applicationConfigRoot });
|
|
219
|
+
const { root } = applicationDirectory(configRoot, project.contract.application.id, { applicationConfigRoot, appConfigRoot, projectRoot: project.root, workspaceRoot, environment });
|
|
220
220
|
invariant(path.isAbsolute(output ?? ''), 'IDP_LOCAL_PLAN_OUTPUT_NOT_ABSOLUTE', '--output必须是绝对路径');
|
|
221
221
|
const plans = resolveContained(root, 'plans/local-applications', '本地应用Plan目录');
|
|
222
222
|
secureDirectoryRoot(plans, '本地应用Plan目录', { create: true });
|
|
@@ -228,27 +228,28 @@ export function createLocalSourcePlan({ configRoot, applicationConfigRoot, proje
|
|
|
228
228
|
return plan;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
-
function readPlan(configRoot, planFile, applicationConfigRoot) {
|
|
231
|
+
function readPlan(configRoot, planFile, applicationConfigRoot, appConfigRoot) {
|
|
232
232
|
invariant(path.isAbsolute(planFile ?? ''), 'IDP_LOCAL_PLAN_NOT_ABSOLUTE', '--plan必须是绝对路径');
|
|
233
|
-
const { root } = applicationDirectory(configRoot, 'placeholder', { applicationConfigRoot });
|
|
234
|
-
const plans = resolveContained(root, 'plans/local-applications', '本地应用Plan目录');
|
|
235
233
|
const candidate = fs.realpathSync.native(planFile);
|
|
236
|
-
const relative = path.relative(plans, candidate);
|
|
237
|
-
invariant(relative && !relative.startsWith('..') && !path.isAbsolute(relative), 'IDP_LOCAL_PLAN_OUTSIDE_CONFIG', 'Plan必须位于IDP_CONFIG_DIR/plans/local-applications');
|
|
238
234
|
assertSafeRegularFile(candidate, 0o600, { allowEmpty: false, role: 'Local DeploymentPlan' });
|
|
239
235
|
const plan = readJson(candidate, 'Local DeploymentPlan');
|
|
240
236
|
invariant(plan.schemaVersion === PLAN_SCHEMA && plan.mode === 'local-source' && plan.promotable === false, 'IDP_LOCAL_PLAN_INVALID', 'Local DeploymentPlan版本无效');
|
|
241
237
|
const { planId, ...core } = plan;
|
|
242
238
|
invariant(planIdentity(core) === planId, 'IDP_LOCAL_PLAN_DIGEST_INVALID', 'Local DeploymentPlan摘要无效');
|
|
243
|
-
|
|
239
|
+
const root = applicationConfigRoot ? secureDirectoryRoot(applicationConfigRoot, '应用最终Config Dir') : appConfigRoot ? secureDirectoryRoot(appConfigRoot, 'APP_CONFIG_DIR') : secureDirectoryRoot(configRoot, 'IDP_CONFIG_DIR');
|
|
240
|
+
const plans = appConfigRoot && !applicationConfigRoot ? path.join(plan.config.directory, 'plans/local-applications') : resolveContained(root, 'plans/local-applications', '本地应用Plan目录');
|
|
241
|
+
const relative = path.relative(plans, candidate);
|
|
242
|
+
invariant(relative && !relative.startsWith('..') && !path.isAbsolute(relative), 'IDP_LOCAL_PLAN_OUTSIDE_CONFIG', 'Plan必须位于应用Config Dir的plans/local-applications');
|
|
243
|
+
const runtimeRoot = appConfigRoot && !applicationConfigRoot ? plan.config.directory : root;
|
|
244
|
+
return { plan, candidate, root: runtimeRoot };
|
|
244
245
|
}
|
|
245
246
|
|
|
246
247
|
function revalidate(plan, runner) {
|
|
247
|
-
invariant(projectSourceDigest(plan.project.root) === plan.project.sourceDigest, 'IDP_LOCAL_SOURCE_CHANGED', '
|
|
248
|
+
invariant(projectSourceDigest(plan.project.workspaceRoot ?? plan.project.root) === plan.project.sourceDigest, 'IDP_LOCAL_SOURCE_CHANGED', 'Workspace构建源已变化,请重新生成Plan');
|
|
248
249
|
invariant(hashFile(plan.project.manifest.path) === plan.project.manifest.digest && hashFile(plan.project.applicationConfigContract.path) === plan.project.applicationConfigContract.digest && hashFile(plan.compose.path) === plan.compose.sourceDigest, 'IDP_LOCAL_INPUT_CHANGED', '项目Manifest、配置合同或Compose已变化,请重新生成Plan');
|
|
249
250
|
invariant(hashFile(plan.config.environmentFile) === plan.config.environmentDigest, 'IDP_LOCAL_CONFIG_CHANGED', '应用.env已变化,请重新生成Plan');
|
|
250
251
|
for (const binding of plan.config.fileBindings) invariant(hashFile(path.join(plan.config.directory, binding.path)) === binding.digest, 'IDP_LOCAL_CONFIG_CHANGED', `${binding.name}已变化,请重新生成Plan`);
|
|
251
|
-
const normalized = normalizedCompose({ projectRoot: plan.project.root, composeFile: plan.compose.path, envFile: plan.config.environmentFile, composeProject: plan.composeProject, runner });
|
|
252
|
+
const normalized = normalizedCompose({ projectRoot: plan.project.root, sourceRoot: plan.project.workspaceRoot ?? plan.project.root, composeFile: plan.compose.path, envFile: plan.config.environmentFile, composeProject: plan.composeProject, runner });
|
|
252
253
|
invariant(sha256(normalized.bytes) === plan.compose.normalizedDigest, 'IDP_LOCAL_COMPOSE_RENDER_CHANGED', 'Compose规范化结果已变化,请重新生成Plan');
|
|
253
254
|
return normalized;
|
|
254
255
|
}
|
|
@@ -289,11 +290,11 @@ function activeState(root, plan, { required = true } = {}) {
|
|
|
289
290
|
return { candidate, bytes, state };
|
|
290
291
|
}
|
|
291
292
|
|
|
292
|
-
export function verifyLocalSourceDeployment({ configRoot, applicationConfigRoot, planFile, runner = run, requireActiveState = true }) {
|
|
293
|
-
const { plan, root } = readPlan(configRoot, planFile, applicationConfigRoot);
|
|
293
|
+
export function verifyLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot, planFile, runner = run, requireActiveState = true }) {
|
|
294
|
+
const { plan, root } = readPlan(configRoot, planFile, applicationConfigRoot, appConfigRoot);
|
|
294
295
|
revalidate(plan, runner);
|
|
295
296
|
activeState(root, plan, { required: requireActiveState });
|
|
296
|
-
const ps = parsePs(executeCompose(runner, composeArgs(plan, ['ps', '--format', 'json']), plan.project.root, true).stdout);
|
|
297
|
+
const ps = parsePs(executeCompose(runner, composeArgs(plan, ['ps', '--format', 'json']), plan.project.workspaceRoot ?? plan.project.root, true).stdout);
|
|
297
298
|
invariant(ps.length > 0, 'IDP_LOCAL_DEPLOYMENT_NOT_RUNNING', '本地应用没有运行中的Compose服务');
|
|
298
299
|
for (const service of ps) {
|
|
299
300
|
const state = String(service.State ?? service.state ?? '').toLowerCase();
|
|
@@ -304,8 +305,8 @@ export function verifyLocalSourceDeployment({ configRoot, applicationConfigRoot,
|
|
|
304
305
|
return evidence(root, plan, 'verify', 'verified', { services: ps.map((item) => item.Service ?? item.Name).filter(Boolean) });
|
|
305
306
|
}
|
|
306
307
|
|
|
307
|
-
export function applyLocalSourceDeployment({ configRoot, applicationConfigRoot, planFile, runner = run }) {
|
|
308
|
-
const { plan, root, candidate: canonicalPlanFile } = readPlan(configRoot, planFile, applicationConfigRoot);
|
|
308
|
+
export function applyLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot, planFile, runner = run }) {
|
|
309
|
+
const { plan, root, candidate: canonicalPlanFile } = readPlan(configRoot, planFile, applicationConfigRoot, appConfigRoot);
|
|
309
310
|
revalidate(plan, runner);
|
|
310
311
|
const current = activeState(root, plan, { required: false });
|
|
311
312
|
const stateFile = current.candidate;
|
|
@@ -313,41 +314,41 @@ export function applyLocalSourceDeployment({ configRoot, applicationConfigRoot,
|
|
|
313
314
|
const preimageFile = path.join(path.dirname(stateFile), `.${plan.application}.${plan.planId.slice(7, 19)}.preimage.json`);
|
|
314
315
|
if (before) atomicWrite(preimageFile, before, 0o600);
|
|
315
316
|
try {
|
|
316
|
-
executeCompose(runner, composeArgs(plan, ['build']), plan.project.root, false);
|
|
317
|
-
executeCompose(runner, composeArgs(plan, ['up', '-d', '--wait', '--remove-orphans']), plan.project.root, false);
|
|
317
|
+
executeCompose(runner, composeArgs(plan, ['build']), plan.project.workspaceRoot ?? plan.project.root, false);
|
|
318
|
+
executeCompose(runner, composeArgs(plan, ['up', '-d', '--wait', '--remove-orphans']), plan.project.workspaceRoot ?? plan.project.root, false);
|
|
318
319
|
const afterCommands = fs.existsSync(stateFile) ? fs.readFileSync(stateFile) : null;
|
|
319
320
|
invariant((before === null && afterCommands === null) || (before !== null && afterCommands !== null && before.equals(afterCommands)), 'IDP_LOCAL_RUNTIME_STATE_CHANGED', '执行期间运行态被并发修改,拒绝提交Receipt');
|
|
320
|
-
const receipt = verifyLocalSourceDeployment({ configRoot, applicationConfigRoot, planFile, runner, requireActiveState: false });
|
|
321
|
+
const receipt = verifyLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot, planFile, runner, requireActiveState: false });
|
|
321
322
|
atomicWrite(stateFile, `${JSON.stringify({ schemaVersion: 'idp.local-source-runtime/v1', application: plan.application, planId: plan.planId, composeProject: plan.composeProject, composeFile: plan.compose.path, planFile: canonicalPlanFile, receiptId: receipt.receiptId }, null, 2)}\n`, 0o600);
|
|
322
323
|
return evidence(root, plan, 'apply', 'verified', { verifyReceiptId: receipt.receiptId });
|
|
323
324
|
} catch (error) {
|
|
324
325
|
try {
|
|
325
326
|
if (before && current.state?.planFile) {
|
|
326
|
-
const previous = readPlan(configRoot, current.state.planFile, applicationConfigRoot).plan;
|
|
327
|
+
const previous = readPlan(configRoot, current.state.planFile, applicationConfigRoot, appConfigRoot).plan;
|
|
327
328
|
revalidate(previous, runner);
|
|
328
|
-
executeCompose(runner, composeArgs(previous, ['up', '-d', '--wait', '--remove-orphans']), previous.project.root, false);
|
|
329
|
+
executeCompose(runner, composeArgs(previous, ['up', '-d', '--wait', '--remove-orphans']), previous.project.workspaceRoot ?? previous.project.root, false);
|
|
329
330
|
}
|
|
330
|
-
else executeCompose(runner, composeArgs(plan, ['down', '--remove-orphans']), plan.project.root, false);
|
|
331
|
+
else executeCompose(runner, composeArgs(plan, ['down', '--remove-orphans']), plan.project.workspaceRoot ?? plan.project.root, false);
|
|
331
332
|
} catch {}
|
|
332
333
|
evidence(root, plan, 'apply', 'failed', { code: error.code ?? 'IDP_PROCESS_FAILED' });
|
|
333
334
|
throw error;
|
|
334
335
|
}
|
|
335
336
|
}
|
|
336
337
|
|
|
337
|
-
export function stopLocalSourceDeployment({ configRoot, applicationConfigRoot, planFile, runner = run }) {
|
|
338
|
-
const { plan, root } = readPlan(configRoot, planFile, applicationConfigRoot);
|
|
338
|
+
export function stopLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot, planFile, runner = run }) {
|
|
339
|
+
const { plan, root } = readPlan(configRoot, planFile, applicationConfigRoot, appConfigRoot);
|
|
339
340
|
revalidate(plan, runner);
|
|
340
341
|
activeState(root, plan);
|
|
341
|
-
executeCompose(runner, composeArgs(plan, ['stop']), plan.project.root, false);
|
|
342
|
+
executeCompose(runner, composeArgs(plan, ['stop']), plan.project.workspaceRoot ?? plan.project.root, false);
|
|
342
343
|
return evidence(root, plan, 'stop', 'stopped');
|
|
343
344
|
}
|
|
344
345
|
|
|
345
|
-
export function removeLocalSourceDeployment({ configRoot, applicationConfigRoot, planFile, confirmation, runner = run }) {
|
|
346
|
-
const { plan, root } = readPlan(configRoot, planFile, applicationConfigRoot);
|
|
346
|
+
export function removeLocalSourceDeployment({ configRoot, appConfigRoot, applicationConfigRoot, planFile, confirmation, runner = run }) {
|
|
347
|
+
const { plan, root } = readPlan(configRoot, planFile, applicationConfigRoot, appConfigRoot);
|
|
347
348
|
invariant(confirmation === plan.application, 'IDP_LOCAL_REMOVE_CONFIRMATION_REQUIRED', `remove必须通过--confirm ${plan.application}精确确认`);
|
|
348
349
|
revalidate(plan, runner);
|
|
349
350
|
activeState(root, plan);
|
|
350
|
-
executeCompose(runner, composeArgs(plan, ['down', '--remove-orphans']), plan.project.root, false);
|
|
351
|
+
executeCompose(runner, composeArgs(plan, ['down', '--remove-orphans']), plan.project.workspaceRoot ?? plan.project.root, false);
|
|
351
352
|
const candidate = statePath(root, plan.application);
|
|
352
353
|
if (fs.existsSync(candidate)) fs.unlinkSync(candidate);
|
|
353
354
|
return evidence(root, plan, 'remove', 'removed', { volumesRemoved: false });
|
|
@@ -25,7 +25,7 @@ export function initializeWorkbenchProjects({ configRoot: input }) {
|
|
|
25
25
|
return { schemaVersion: 'idp.workbench-project-candidate/v1', status: 'ready', candidate };
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
function readCandidate(root, file) {
|
|
28
|
+
function readCandidate(root, file, appConfigRoot) {
|
|
29
29
|
invariant(path.isAbsolute(file ?? ''), 'IDP_WORKBENCH_CANDIDATE_NOT_ABSOLUTE', '--file必须是绝对路径');
|
|
30
30
|
const workbench = resolveContained(root, 'workbench', '工作台配置目录'); const candidate = fs.realpathSync.native(file);
|
|
31
31
|
const relative = path.relative(workbench, candidate); invariant(relative && !relative.startsWith('..') && !path.isAbsolute(relative), 'IDP_WORKBENCH_CANDIDATE_OUTSIDE_CONFIG', 'candidate必须位于IDP_CONFIG_DIR/workbench');
|
|
@@ -46,7 +46,8 @@ function readCandidate(root, file) {
|
|
|
46
46
|
const applicationRoot = fs.realpathSync.native(path.resolve(hostPath, relativePath));
|
|
47
47
|
invariant(applicationRoot === hostPath || applicationRoot.startsWith(`${hostPath}${path.sep}`), 'IDP_WORKBENCH_PROJECT_RELATIVE_PATH_ESCAPE', `项目${item.id}的应用路径逃逸项目目录`);
|
|
48
48
|
invariant(fs.existsSync(path.join(applicationRoot, 'project-capabilities.json')), 'IDP_WORKBENCH_APPLICATION_INVALID', `项目${item.id}的${relativePath}缺少project-capabilities.json`);
|
|
49
|
-
const
|
|
49
|
+
const derived = appConfigRoot ? path.join(appConfigRoot, item.id, ...(relativePath === '.' ? [] : relativePath.split('/')), 'local') : undefined;
|
|
50
|
+
const applicationConfigDir = safeDirectory(binding.applicationConfigDir ?? derived, `项目${item.id}/${relativePath} Config Dir`); assertOutsideRepositories(applicationConfigDir, `项目${item.id}/${relativePath} Config Dir`);
|
|
50
51
|
return { relativePath, applicationConfigDir, containerPath: relativePath === '.' ? `/workspaces/${item.id}` : `/workspaces/${item.id}/${relativePath}`, configDirectory: `/config/applications/${item.id}-${index}/local` };
|
|
51
52
|
});
|
|
52
53
|
return { id: item.id, hostPath, containerPath: `/workspaces/${item.id}`, configBindings };
|
|
@@ -54,10 +55,12 @@ function readCandidate(root, file) {
|
|
|
54
55
|
return { candidate, projects };
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
export function createWorkbenchProjectsPlan({ configRoot: input, dyytoRoot, candidateFile, output, now = new Date().toISOString() }) {
|
|
58
|
+
export function createWorkbenchProjectsPlan({ configRoot: input, appConfigRoot, dyytoRoot, candidateFile, output, now = new Date().toISOString() }) {
|
|
58
59
|
const root = configRoot(input), dyyto = safeDirectory(dyytoRoot, 'Dyyto仓库');
|
|
59
60
|
invariant(fs.existsSync(path.join(dyyto, 'compose.yaml')), 'IDP_WORKBENCH_DYYTO_INVALID', 'Dyyto仓库缺少compose.yaml');
|
|
60
|
-
const
|
|
61
|
+
const applications = appConfigRoot ? safeDirectory(appConfigRoot, 'APP_CONFIG_DIR') : undefined;
|
|
62
|
+
if (applications) assertOutsideRepositories(applications, 'APP_CONFIG_DIR');
|
|
63
|
+
const candidate = readCandidate(root, candidateFile, applications);
|
|
61
64
|
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
65
|
const plan = { ...core, planId: sha256(stableJson(core)) };
|
|
63
66
|
const plans = resolveContained(root, 'plans/workbench-projects', '工作台Plan目录'); secureDirectoryRoot(plans, '工作台Plan目录', { create: true });
|