@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/cli.mjs
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { initializeConfig, doctorConfig, generateSecrets } from './config.mjs';
|
|
5
|
+
import { acceptMacM4 } from './acceptance.mjs';
|
|
6
|
+
import { syncManagedBindings } from './bindings.mjs';
|
|
7
|
+
import { applyRestore, composeOperation, createBackup, promoteRestore, verifyBackup, verifyDeployment, verifyRestoreCandidate, testRestore } from './operations.mjs';
|
|
8
|
+
import { IdpError } from './errors.mjs';
|
|
9
|
+
import { lockImages } from './images.mjs';
|
|
10
|
+
import { releaseDeploy } from './release.mjs';
|
|
11
|
+
import { importSourceSnapshot } from './sources.mjs';
|
|
12
|
+
import { verifyAssetLifecycle } from './lifecycle.mjs';
|
|
13
|
+
import { assertSafeDirectory, assertSafeRegularFile, resolveContained } from './security.mjs';
|
|
14
|
+
import { validateFoundationContract } from './foundation-contracts.mjs';
|
|
15
|
+
import { applyGitOpsPlan, createGitOpsPlan, verifyGitOpsRepository } from './gitops.mjs';
|
|
16
|
+
import { doctorFoundation, syncFoundationDirectory, verifyFoundation } from './foundation.mjs';
|
|
17
|
+
import { buildStatus, createBuildPlan, generateGithubWorkflow, inspectApplication, triggerBuild, verifyGithubWorkflow } from './delivery.mjs';
|
|
18
|
+
import { applyLocalSourceDeployment, createLocalSourcePlan, initializeApplicationConfig, removeLocalSourceDeployment, stopLocalSourceDeployment, verifyLocalSourceDeployment } from './local-source.mjs';
|
|
19
|
+
import { applyWorkbenchProjectsPlan, createWorkbenchProjectsPlan, initializeWorkbenchProjects, verifyWorkbenchProjects } from './workbench-projects.mjs';
|
|
20
|
+
|
|
21
|
+
const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
22
|
+
|
|
23
|
+
export function configRootFrom(args, { environment = process.env } = {}) {
|
|
24
|
+
const index = args.indexOf('--config-dir');
|
|
25
|
+
if (index >= 0 && (!args[index + 1] || args[index + 1].startsWith('--'))) throw new IdpError('IDP_CONFIG_ARGUMENT_MISSING', '--config-dir后必须提供绝对路径');
|
|
26
|
+
const explicit = index >= 0 ? args[index + 1] : undefined;
|
|
27
|
+
const value = explicit ?? environment.IDP_CONFIG_DIR;
|
|
28
|
+
if (!value) throw new IdpError('IDP_CONFIG_REQUIRED', '请设置IDP_CONFIG_DIR或传入--config-dir');
|
|
29
|
+
if (!path.isAbsolute(value)) throw new IdpError('IDP_CONFIG_NOT_ABSOLUTE', 'IDP_CONFIG_DIR必须使用原始绝对路径,不能依赖当前工作目录解析');
|
|
30
|
+
return path.resolve(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function readSourceImportFile(configRoot, rawCandidate) {
|
|
34
|
+
if (!path.isAbsolute(rawCandidate ?? '')) throw new IdpError('IDP_SOURCE_FILE_NOT_ABSOLUTE', '--file必须是原始绝对路径');
|
|
35
|
+
const { root } = doctorConfig(configRoot, { requireConfigured: false, profile: 'registry' });
|
|
36
|
+
const imports = resolveContained(root, 'imports', 'Snapshot imports目录');
|
|
37
|
+
assertSafeDirectory(imports, 0o700, { role: 'Snapshot imports目录' });
|
|
38
|
+
const candidate = path.resolve(rawCandidate);
|
|
39
|
+
const relative = path.relative(imports, candidate);
|
|
40
|
+
if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) throw new IdpError('IDP_SOURCE_FILE_OUTSIDE_IMPORTS', '--file必须位于IDP_CONFIG_DIR/imports内');
|
|
41
|
+
const safeCandidate = resolveContained(imports, relative, 'Snapshot导入文件');
|
|
42
|
+
const stat = assertSafeRegularFile(safeCandidate, 0o600, { allowEmpty: false, role: 'Snapshot导入文件' });
|
|
43
|
+
if (stat.size > 16 * 1024 * 1024) throw new IdpError('IDP_SOURCE_FILE_INVALID', '导入文件必须是权限不超过0600、1 byte到16 MiB的普通非链接文件');
|
|
44
|
+
return fs.readFileSync(safeCandidate);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function composeRequiresConfigured(command, args) {
|
|
48
|
+
const allowUnconfigured = args.includes('--allow-unconfigured');
|
|
49
|
+
if (allowUnconfigured && command !== 'compose') {
|
|
50
|
+
throw new IdpError('IDP_UNCONFIGURED_BYPASS_FORBIDDEN', '--allow-unconfigured只允许只读的compose配置预览,不能用于pull、up或down');
|
|
51
|
+
}
|
|
52
|
+
return command !== 'down' && !allowUnconfigured;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function readContractFile(rawCandidate) {
|
|
56
|
+
if (!path.isAbsolute(rawCandidate ?? '')) throw new IdpError('IDP_CONTRACT_FILE_NOT_ABSOLUTE', 'contracts verify的--file必须是原始绝对路径');
|
|
57
|
+
const candidate = path.resolve(rawCandidate);
|
|
58
|
+
const stat = fs.lstatSync(candidate, { throwIfNoEntry: false });
|
|
59
|
+
if (!stat || !stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size < 2 || stat.size > 4 * 1024 * 1024) {
|
|
60
|
+
throw new IdpError('IDP_CONTRACT_FILE_INVALID', '合同文件必须是1 byte到4 MiB的普通非链接文件');
|
|
61
|
+
}
|
|
62
|
+
let document;
|
|
63
|
+
try { document = JSON.parse(fs.readFileSync(candidate, 'utf8')); }
|
|
64
|
+
catch (error) { throw new IdpError('IDP_CONTRACT_JSON_INVALID', '合同文件不是有效JSON', { cause: error.message }); }
|
|
65
|
+
return { candidate, document };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function print(value) {
|
|
69
|
+
process.stdout.write(`${typeof value === 'string' ? value : JSON.stringify(value, null, 2)}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function main(args) {
|
|
73
|
+
const [command, subcommand] = args;
|
|
74
|
+
if (!command || ['help', '--help', '-h'].includes(command)) {
|
|
75
|
+
process.stdout.write('本地项目命令:\n app config-init --project <绝对项目目录>\n local app plan --project <绝对目录> --compose-file <绝对文件> --output <IDP_CONFIG_DIR/plans/local-applications/...>\n local app apply|verify|stop --plan <绝对文件>\n local app remove --plan <绝对文件> --confirm <application-id>\n\n');
|
|
76
|
+
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
|
+
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
|
+
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');
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (command === 'app' && subcommand === 'inspect') {
|
|
82
|
+
const values = namedArguments(args.slice(2), ['--component-contract', '--environment-binding']);
|
|
83
|
+
invariantDelivery(values, '--component-contract', 'app inspect');
|
|
84
|
+
print(inspectApplication({ componentFile: values['--component-contract'], environmentBindingFile: values['--environment-binding'] }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (command === 'workflow' && ['generate', 'verify'].includes(subcommand)) {
|
|
88
|
+
const values = namedArguments(args.slice(2), ['--file']);
|
|
89
|
+
invariantDelivery(values, '--file', `workflow ${subcommand}`);
|
|
90
|
+
print(subcommand === 'generate' ? generateGithubWorkflow({ output: values['--file'] }) : verifyGithubWorkflow({ workflowFile: values['--file'] }));
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (command === 'build' && subcommand === 'plan') {
|
|
94
|
+
const values = namedArguments(args.slice(2), ['--component-contract', '--repository', '--commit', '--image-repository', '--workflow', '--output']);
|
|
95
|
+
for (const key of ['--component-contract', '--repository', '--commit', '--image-repository', '--output']) invariantDelivery(values, key, 'build plan');
|
|
96
|
+
print(createBuildPlan({ componentFile: values['--component-contract'], repository: values['--repository'], commit: values['--commit'], imageRepository: values['--image-repository'], workflow: values['--workflow'], output: values['--output'] }));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (command === 'build' && subcommand === 'trigger') {
|
|
100
|
+
const values = namedArguments(args.slice(2), ['--plan', '--provider', '--output']);
|
|
101
|
+
for (const key of ['--plan', '--output']) invariantDelivery(values, key, 'build trigger');
|
|
102
|
+
print(triggerBuild({ planFile: values['--plan'], provider: values['--provider'] ?? 'manual', output: values['--output'] }));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (command === 'build' && subcommand === 'status') {
|
|
106
|
+
const values = namedArguments(args.slice(2), ['--trigger']);
|
|
107
|
+
invariantDelivery(values, '--trigger', 'build status');
|
|
108
|
+
print(buildStatus({ triggerFile: values['--trigger'] }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (command === 'lifecycle' && subcommand === 'check') {
|
|
112
|
+
print(verifyAssetLifecycle(repositoryRoot));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (command === 'contracts' && subcommand === 'verify') {
|
|
116
|
+
const kind = args[2];
|
|
117
|
+
const fileIndex = args.indexOf('--file');
|
|
118
|
+
if (fileIndex < 0 || !args[fileIndex + 1] || args[fileIndex + 1].startsWith('--')) throw new IdpError('IDP_CONTRACT_FILE_REQUIRED', 'contracts verify需要--file <绝对JSON路径>');
|
|
119
|
+
const { candidate, document } = readContractFile(args[fileIndex + 1]);
|
|
120
|
+
const validated = validateFoundationContract(kind, document);
|
|
121
|
+
print({ schemaVersion: 'idp.contract-verification/v1', status: 'valid', kind, file: candidate, digest: validated.digest });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (command === 'foundation' && ['doctor', 'verify'].includes(subcommand)) {
|
|
125
|
+
const values = namedArguments(args.slice(2), ['--context']);
|
|
126
|
+
print(subcommand === 'doctor' ? doctorFoundation({ context: values['--context'] }) : verifyFoundation({ context: values['--context'] }));
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (command === 'release' && subcommand === 'deploy' && args.some((value) => ['--help', '-h'].includes(value))) {
|
|
130
|
+
print(`用法:\n ./deploy.sh [foundation|core|business-smartgo|core-smartgo] [--version <semver>] [--oci-repository-root <host/namespace/repository>] [--config-dir <绝对路径>]\n\n默认部署foundation,并自动完成配置初始化、Secret生成、自研镜像双架构构建与推送、镜像digest锁定、pull、switch、verify和Binding同步。SmartGo不会进入默认core;已有Core并行业务开发请显式选择core-smartgo。标准路径使用release/defaults.v1.json中的企业OCI仓库根;--oci-repository-root只用于显式覆盖。`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
const configRoot = configRootFrom(args);
|
|
134
|
+
if (command === 'app' && subcommand === 'config-init') {
|
|
135
|
+
const values = namedArguments(args.slice(2), ['--project', '--config-dir', '--application-config-dir']);
|
|
136
|
+
invariantNamed(values, '--project', 'app config-init');
|
|
137
|
+
print(initializeApplicationConfig({ configRoot, projectRoot: values['--project'], applicationConfigRoot: values['--application-config-dir'] }));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (command === 'workbench' && subcommand === 'projects') {
|
|
141
|
+
const action = args[2];
|
|
142
|
+
const values = namedArguments(args.slice(3), ['--dyyto-root', '--file', '--output', '--plan', '--config-dir']);
|
|
143
|
+
if (action === 'init') print(initializeWorkbenchProjects({ configRoot }));
|
|
144
|
+
else if (action === 'plan') {
|
|
145
|
+
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'] }));
|
|
147
|
+
} else if (action === 'apply') { invariantNamed(values, '--plan', 'workbench projects apply'); print(applyWorkbenchProjectsPlan({ configRoot, planFile: values['--plan'] })); }
|
|
148
|
+
else if (action === 'verify') print(verifyWorkbenchProjects({ configRoot }));
|
|
149
|
+
else throw new IdpError('IDP_WORKBENCH_ACTION_UNKNOWN', `未知工作台项目动作:${action ?? ''}`);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (command === 'local' && subcommand === 'app') {
|
|
153
|
+
const action = args[2];
|
|
154
|
+
const values = namedArguments(args.slice(3), ['--project', '--compose-file', '--output', '--plan', '--confirm', '--config-dir', '--application-config-dir']);
|
|
155
|
+
if (action === 'plan') {
|
|
156
|
+
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'] }));
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
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'] }));
|
|
165
|
+
else throw new IdpError('IDP_LOCAL_ACTION_UNKNOWN', `未知本地应用动作:${action ?? ''}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (command === 'foundation' && subcommand === 'directory' && args[2] === 'sync') {
|
|
169
|
+
const values = namedArguments(args.slice(3), ['--argo-url', '--rollouts-url', '--registry-url', '--config-dir']);
|
|
170
|
+
print(syncFoundationDirectory({ configRoot, argoUrl: values['--argo-url'], rolloutsUrl: values['--rollouts-url'], registryUrl: values['--registry-url'] }));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
if (command === 'gitops' && subcommand === 'app') {
|
|
174
|
+
const action = args[2];
|
|
175
|
+
const values = namedArguments(args.slice(3), ['--component-contract', '--environment-binding', '--release-candidate', '--gitops-root', '--output', '--plan', '--application', '--environment', '--config-dir']);
|
|
176
|
+
if (['plan-create', 'plan-update', 'plan-remove'].includes(action)) {
|
|
177
|
+
for (const key of ['--component-contract', '--environment-binding', '--release-candidate', '--gitops-root', '--output']) invariantNamed(values, key, action);
|
|
178
|
+
print(createGitOpsPlan({ operation: action.slice(5), componentFile: values['--component-contract'], bindingFile: values['--environment-binding'], candidateFile: values['--release-candidate'], gitopsRoot: values['--gitops-root'], output: values['--output'], configRoot }));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (action === 'apply') {
|
|
182
|
+
invariantNamed(values, '--plan', action);
|
|
183
|
+
print(applyGitOpsPlan({ planFile: values['--plan'], configRoot }));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (action === 'verify') {
|
|
187
|
+
for (const key of ['--application', '--environment', '--gitops-root']) invariantNamed(values, key, action);
|
|
188
|
+
print(verifyGitOpsRepository({ gitopsRoot: values['--gitops-root'], application: values['--application'], environment: values['--environment'] }));
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
throw new IdpError('IDP_GITOPS_ACTION_UNKNOWN', `未知GitOps应用动作:${action ?? ''}`);
|
|
192
|
+
}
|
|
193
|
+
if (command === 'release' && subcommand === 'deploy') {
|
|
194
|
+
const { profile, version, ociRepositoryRoot } = releaseDeployArguments(args);
|
|
195
|
+
print(releaseDeploy({ repositoryRoot, configRoot, profile, version, ociRepositoryRoot }));
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (command === 'sources' && subcommand === 'import') {
|
|
199
|
+
const source = args[2];
|
|
200
|
+
const fileIndex = args.indexOf('--file');
|
|
201
|
+
const useStdin = args.includes('--stdin');
|
|
202
|
+
invariantSourceArguments(source, useStdin, fileIndex, args);
|
|
203
|
+
let bytes;
|
|
204
|
+
if (useStdin) bytes = await readBoundedStdin();
|
|
205
|
+
else bytes = readSourceImportFile(configRoot, args[fileIndex + 1]);
|
|
206
|
+
print(importSourceSnapshot({ configRoot, source, bytes }));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
if (command === 'config' && subcommand === 'init') {
|
|
210
|
+
print(initializeConfig(configRoot).report);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
if (command === 'config' && subcommand === 'generate-secrets') {
|
|
214
|
+
print(generateSecrets(configRoot));
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (command === 'images' && subcommand === 'lock') {
|
|
218
|
+
print(lockImages(configRoot));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (command === 'doctor') {
|
|
222
|
+
const profile = subcommand && !subcommand.startsWith('-') ? subcommand : 'full';
|
|
223
|
+
print(doctorConfig(configRoot, { requireConfigured: args.includes('--require-configured'), profile }).report);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (command === 'verify') {
|
|
227
|
+
const profile = subcommand ?? 'core';
|
|
228
|
+
print(verifyDeployment({ repositoryRoot, configRoot, profile }).receipt);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (command === 'bindings' && subcommand === 'sync') {
|
|
232
|
+
const profile = args[2];
|
|
233
|
+
if (!profile || profile.startsWith('-')) throw new IdpError('IDP_BINDING_PROFILE_REQUIRED', 'bindings sync后必须提供当前活动Profile');
|
|
234
|
+
print(syncManagedBindings({ configRoot, profile }));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (command === 'accept' && subcommand === 'mac-m4') {
|
|
238
|
+
const profile = args[2] && !args[2].startsWith('-') ? args[2] : 'core';
|
|
239
|
+
const confirmIndex = args.indexOf('--confirm-restart');
|
|
240
|
+
const confirmation = confirmIndex >= 0 ? args[confirmIndex + 1] : undefined;
|
|
241
|
+
print(acceptMacM4({ repositoryRoot, configRoot, profile, confirmation }).receipt);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
if (['compose', 'pull', 'up', 'switch', 'down'].includes(command)) {
|
|
245
|
+
const profile = subcommand ?? 'core';
|
|
246
|
+
const composeCommand = command === 'compose' ? ['config'] : command === 'pull' ? ['pull'] : ['up', 'switch'].includes(command) ? ['up', '-d', '--wait'] : ['down'];
|
|
247
|
+
const result = composeOperation({ repositoryRoot, configRoot, profile, command: composeCommand, requireConfigured: composeRequiresConfigured(command === 'switch' ? 'up' : command, args), allowProfileSwitch: command === 'switch' });
|
|
248
|
+
print(result.receipt);
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (command === 'backup' && (!subcommand || subcommand === 'create')) {
|
|
252
|
+
const profile = args[2] && !args[2].startsWith('-') ? args[2] : 'core';
|
|
253
|
+
print(createBackup({ repositoryRoot, configRoot, profile }).receipt);
|
|
254
|
+
return;
|
|
255
|
+
}
|
|
256
|
+
if (command === 'backup' && subcommand === 'verify') {
|
|
257
|
+
const generationId = args[2];
|
|
258
|
+
if (!generationId || generationId.startsWith('-')) throw new IdpError('IDP_BACKUP_ID_REQUIRED', 'backup verify需要代次ID');
|
|
259
|
+
print(verifyBackup({ configRoot, generationId }).receipt);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (command === 'restore' && subcommand === 'test') {
|
|
263
|
+
const generationId = args[2];
|
|
264
|
+
if (!generationId || generationId.startsWith('-')) throw new IdpError('IDP_BACKUP_ID_REQUIRED', 'restore test需要代次ID');
|
|
265
|
+
print(testRestore({ repositoryRoot, configRoot, generationId }).receipt);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (command === 'restore' && subcommand === 'apply') {
|
|
269
|
+
const generationId = args[2];
|
|
270
|
+
if (!generationId || generationId.startsWith('-')) throw new IdpError('IDP_BACKUP_ID_REQUIRED', 'restore apply需要代次ID');
|
|
271
|
+
print(applyRestore({ repositoryRoot, configRoot, generationId }).receipt);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (command === 'restore' && subcommand === 'verify') {
|
|
275
|
+
const candidateId = args[2];
|
|
276
|
+
if (!candidateId || candidateId.startsWith('-')) throw new IdpError('IDP_RESTORE_CANDIDATE_ID_REQUIRED', 'restore verify需要候选ID');
|
|
277
|
+
print(verifyRestoreCandidate({ configRoot, candidateId }).receipt);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
if (command === 'restore' && subcommand === 'promote') {
|
|
281
|
+
const candidateId = args[2];
|
|
282
|
+
const confirmIndex = args.indexOf('--confirm');
|
|
283
|
+
const confirmation = confirmIndex >= 0 ? args[confirmIndex + 1] : undefined;
|
|
284
|
+
if (!candidateId || candidateId.startsWith('-')) throw new IdpError('IDP_RESTORE_CANDIDATE_ID_REQUIRED', 'restore promote需要候选ID');
|
|
285
|
+
print(promoteRestore({ repositoryRoot, configRoot, candidateId, confirmation }).receipt);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (command === 'config' && subcommand === 'snapshot') {
|
|
289
|
+
const { report } = doctorConfig(configRoot, { requireConfigured: false });
|
|
290
|
+
const target = path.join(configRoot, 'snapshots', `${Date.now()}-${report.digest.slice(7, 19)}.json`);
|
|
291
|
+
fs.writeFileSync(target, `${JSON.stringify(report, null, 2)}\n`, { mode: 0o600, flag: 'wx' });
|
|
292
|
+
print({ target, digest: report.digest });
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
throw new IdpError('IDP_COMMAND_UNKNOWN', `未知命令:${args.join(' ')}`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function releaseDeployArguments(args) {
|
|
299
|
+
const values = {};
|
|
300
|
+
const positionals = [];
|
|
301
|
+
const optionCodes = {
|
|
302
|
+
'--version': ['version', 'IDP_RELEASE_VERSION_REQUIRED', '--version后必须提供SemVer版本号'],
|
|
303
|
+
'--config-dir': ['configDir', 'IDP_CONFIG_ARGUMENT_MISSING', '--config-dir后必须提供绝对路径'],
|
|
304
|
+
'--oci-repository-root': ['ociRepositoryRoot', 'IDP_OCI_REPOSITORY_ROOT_REQUIRED', '--oci-repository-root后必须提供OCI仓库根'],
|
|
305
|
+
};
|
|
306
|
+
for (let index = 2; index < args.length; index += 1) {
|
|
307
|
+
const value = args[index];
|
|
308
|
+
const contract = optionCodes[value];
|
|
309
|
+
if (contract) {
|
|
310
|
+
const [key, code, message] = contract;
|
|
311
|
+
const optionValue = args[index + 1];
|
|
312
|
+
if (!optionValue || optionValue.startsWith('--')) throw new IdpError(code, message);
|
|
313
|
+
if (values[key] !== undefined) throw new IdpError('IDP_RELEASE_ARGUMENT_DUPLICATE', `release deploy参数重复:${value}`);
|
|
314
|
+
values[key] = optionValue;
|
|
315
|
+
index += 1;
|
|
316
|
+
} else if (value.startsWith('-')) {
|
|
317
|
+
throw new IdpError('IDP_RELEASE_ARGUMENT_UNKNOWN', `release deploy包含未知参数:${value}`);
|
|
318
|
+
} else positionals.push(value);
|
|
319
|
+
}
|
|
320
|
+
if (positionals.length > 1) throw new IdpError('IDP_RELEASE_ARGUMENT_UNKNOWN', `release deploy包含多余位置参数:${positionals.slice(1).join(' ')}`);
|
|
321
|
+
return { profile: positionals[0] ?? 'foundation', version: values.version, ociRepositoryRoot: values.ociRepositoryRoot };
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function invariantSourceArguments(source, useStdin, fileIndex, args) {
|
|
325
|
+
if (!['dyyto', 'bench'].includes(source)) throw new IdpError('IDP_SOURCE_UNKNOWN', 'sources import需要dyyto或bench');
|
|
326
|
+
if (useStdin === (fileIndex >= 0)) throw new IdpError('IDP_SOURCE_INPUT_INVALID', 'sources import必须且只能选择--stdin或--file');
|
|
327
|
+
if (fileIndex >= 0 && (!args[fileIndex + 1] || args[fileIndex + 1].startsWith('--'))) throw new IdpError('IDP_SOURCE_FILE_REQUIRED', '--file后必须提供绝对路径');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function readBoundedStdin() {
|
|
331
|
+
const chunks = []; let size = 0;
|
|
332
|
+
for await (const chunk of process.stdin) {
|
|
333
|
+
size += chunk.length;
|
|
334
|
+
if (size > 16 * 1024 * 1024) throw new IdpError('IDP_SOURCE_SIZE_INVALID', 'stdin Snapshot不能超过16 MiB');
|
|
335
|
+
chunks.push(chunk);
|
|
336
|
+
}
|
|
337
|
+
if (size === 0) throw new IdpError('IDP_SOURCE_SIZE_INVALID', 'stdin Snapshot不能为空');
|
|
338
|
+
return Buffer.concat(chunks);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function namedArguments(args, allowed) {
|
|
342
|
+
const result = {};
|
|
343
|
+
for (let index = 0; index < args.length; index += 2) {
|
|
344
|
+
const key = args[index];
|
|
345
|
+
const value = args[index + 1];
|
|
346
|
+
if (!allowed.includes(key)) throw new IdpError('IDP_ARGUMENT_UNKNOWN', `未知参数:${key ?? ''}`);
|
|
347
|
+
if (!value || value.startsWith('--')) throw new IdpError('IDP_ARGUMENT_VALUE_REQUIRED', `${key}后必须提供值`);
|
|
348
|
+
if (result[key] !== undefined) throw new IdpError('IDP_ARGUMENT_DUPLICATE', `参数重复:${key}`);
|
|
349
|
+
result[key] = value;
|
|
350
|
+
}
|
|
351
|
+
return result;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function invariantNamed(values, key, action) {
|
|
355
|
+
if (!values[key]) throw new IdpError('IDP_GITOPS_ARGUMENT_REQUIRED', `${action}需要${key}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function invariantDelivery(values, key, action) {
|
|
359
|
+
if (!values[key]) throw new IdpError('IDP_DELIVERY_ARGUMENT_REQUIRED', `${action}需要${key}`);
|
|
360
|
+
}
|
package/src/compose.mjs
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { resolveProfile } from './profiles.mjs';
|
|
3
|
+
|
|
4
|
+
const FRAGMENTS = {
|
|
5
|
+
postgresql: 'postgresql.yaml',
|
|
6
|
+
registry: 'registry.yaml',
|
|
7
|
+
tech: 'tech.yaml',
|
|
8
|
+
flow: 'flow.yaml',
|
|
9
|
+
portal: 'portal.yaml',
|
|
10
|
+
smartgo: 'smartgo.yaml',
|
|
11
|
+
edge: 'edge.yaml',
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export function composeArgs(repositoryRoot, configRoot, profile, command, { projectName = 'company-idp' } = {}) {
|
|
15
|
+
const args = ['compose', '--project-name', projectName, '--env-file', path.join(configRoot, '.env')];
|
|
16
|
+
for (const component of resolveProfile(profile)) args.push('-f', path.join(repositoryRoot, 'compose', FRAGMENTS[component]));
|
|
17
|
+
args.push(...command);
|
|
18
|
+
return args;
|
|
19
|
+
}
|