@cloudbase/manager-node 5.7.1-beta.1 → 5.8.0-beta.1

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.
Files changed (63) hide show
  1. package/lib/cloudApp/index.js +12 -3
  2. package/lib/deploy/DatabaseDeployer.js +238 -0
  3. package/lib/deploy/DeployOrchestrator.js +645 -0
  4. package/lib/deploy/FunctionDeployer.js +606 -0
  5. package/lib/deploy/GatewayDeployer.js +612 -0
  6. package/lib/deploy/StateStore.js +327 -0
  7. package/lib/deploy/StaticDeployer.js +236 -0
  8. package/lib/deploy/domain.js +41 -0
  9. package/lib/deploy/framework.js +230 -0
  10. package/lib/deploy/function/artifact.js +214 -0
  11. package/lib/deploy/function/builders/cloud.js +779 -0
  12. package/lib/deploy/function/cam-preflight.js +173 -0
  13. package/lib/deploy/function/cloud-build-service.js +191 -0
  14. package/lib/deploy/function/config-guard.js +595 -0
  15. package/lib/deploy/function/docker-preflight.js +87 -0
  16. package/lib/deploy/function/image-preflight.js +164 -0
  17. package/lib/deploy/function/local-builder.js +129 -0
  18. package/lib/deploy/function/planner.js +278 -0
  19. package/lib/deploy/function/preflight.js +329 -0
  20. package/lib/deploy/types.js +107 -0
  21. package/lib/env/index.js +0 -27
  22. package/lib/environment.js +11 -0
  23. package/lib/function/index.js +30 -4
  24. package/lib/hosting/index.js +4 -1
  25. package/lib/index.js +31 -0
  26. package/lib/projectValidator/index.js +452 -0
  27. package/lib/projectValidator/types.js +2 -0
  28. package/lib/storage/index.js +6 -7
  29. package/lib/utils/index.js +62 -5
  30. package/package.json +2 -1
  31. package/types/cloudApp/index.d.ts +4 -0
  32. package/types/cloudApp/types.d.ts +87 -6
  33. package/types/deploy/DatabaseDeployer.d.ts +70 -0
  34. package/types/deploy/DeployOrchestrator.d.ts +170 -0
  35. package/types/deploy/FunctionDeployer.d.ts +161 -0
  36. package/types/deploy/GatewayDeployer.d.ts +194 -0
  37. package/types/deploy/StateStore.d.ts +170 -0
  38. package/types/deploy/StaticDeployer.d.ts +97 -0
  39. package/types/deploy/domain.d.ts +17 -0
  40. package/types/deploy/framework.d.ts +63 -0
  41. package/types/deploy/function/artifact.d.ts +40 -0
  42. package/types/deploy/function/builders/cloud.d.ts +110 -0
  43. package/types/deploy/function/cam-preflight.d.ts +47 -0
  44. package/types/deploy/function/cloud-build-service.d.ts +10 -0
  45. package/types/deploy/function/config-guard.d.ts +41 -0
  46. package/types/deploy/function/docker-preflight.d.ts +17 -0
  47. package/types/deploy/function/image-preflight.d.ts +21 -0
  48. package/types/deploy/function/local-builder.d.ts +26 -0
  49. package/types/deploy/function/planner.d.ts +15 -0
  50. package/types/deploy/function/preflight.d.ts +9 -0
  51. package/types/deploy/types.d.ts +433 -0
  52. package/types/env/index.d.ts +1 -17
  53. package/types/env/type.d.ts +6 -260
  54. package/types/environment.d.ts +7 -0
  55. package/types/function/index.d.ts +2 -0
  56. package/types/function/types.d.ts +15 -0
  57. package/types/hosting/index.d.ts +6 -0
  58. package/types/index.d.ts +18 -0
  59. package/types/interfaces/function.interface.d.ts +1 -1
  60. package/types/projectValidator/index.d.ts +38 -0
  61. package/types/projectValidator/types.d.ts +26 -0
  62. package/types/storage/index.d.ts +6 -0
  63. package/types/utils/index.d.ts +13 -0
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runDockerPreflight = runDockerPreflight;
4
+ const child_process_1 = require("child_process");
5
+ const types_1 = require("../types");
6
+ /**
7
+ * Docker 本地能力 Preflight(local 策略,scope: local)
8
+ *
9
+ * 在本地构建镜像前,检测 docker CLI、daemon 与 buildx 是否可用。
10
+ * 全部为只读命令,不产生任何副作用,不涉及网络与凭证。
11
+ *
12
+ * 安全:所有命令用 execFile + 参数数组执行,shell 关闭,命令与参数均为
13
+ * 固定常量、无外部输入拼接,杜绝命令注入(对应 security_rules 的 RCE 防护)。
14
+ * 本地能力缺失统一标注 canFallbackToCloud,供上层决定是否回退云端构建。
15
+ *
16
+ * 命令执行器以参数注入,默认使用真实 execFile,便于在测试中替换(不依赖 jest.mock)。
17
+ */
18
+ /** 单条命令执行超时(毫秒),避免 daemon 卡死拖垮检查 */
19
+ const COMMAND_TIMEOUT = 15000;
20
+ /** 默认执行器:安全执行只读命令(固定参数数组、无 shell、带超时) */
21
+ function defaultRunner(command, args) {
22
+ return new Promise(resolve => {
23
+ (0, child_process_1.execFile)(command, args, { timeout: COMMAND_TIMEOUT, shell: false, windowsHide: true }, (error, stdout, stderr) => {
24
+ resolve({
25
+ ok: !error,
26
+ stdout: (stdout || '').toString().trim(),
27
+ stderr: (stderr || '').toString().trim()
28
+ });
29
+ });
30
+ });
31
+ }
32
+ function fail(id, summary, remediation) {
33
+ return { id, status: 'fail', summary, remediation, canFallbackToCloud: true };
34
+ }
35
+ function pass(id, summary) {
36
+ return { id, status: 'pass', summary };
37
+ }
38
+ function buildReport(checks) {
39
+ const ready = !checks.some(item => item.status === 'fail');
40
+ const report = {
41
+ scope: 'local',
42
+ strategy: 'local',
43
+ ready,
44
+ checks
45
+ };
46
+ // 存在可回退云端的失败项时,建议改用 cloud 策略
47
+ if (!ready && checks.some(item => item.status === 'fail' && item.canFallbackToCloud)) {
48
+ report.recommendedStrategy = 'cloud';
49
+ }
50
+ return report;
51
+ }
52
+ /**
53
+ * 执行 Docker 本地能力检查
54
+ *
55
+ * 检测顺序:CLI 存在性 → daemon 可用性 → buildx 可用性。
56
+ * CLI 缺失时短路(daemon/buildx 无从谈起),其余项独立收集后聚合返回。
57
+ *
58
+ * @param runner 命令执行器,默认真实 execFile;测试可注入 mock
59
+ */
60
+ async function runDockerPreflight(runner = defaultRunner) {
61
+ const checks = [];
62
+ // 1. Docker CLI 是否存在(不需要 daemon)
63
+ const cli = await runner('docker', ['--version']);
64
+ if (!cli.ok) {
65
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_DOCKER_CLI_MISSING, '未检测到 docker 命令', '请安装 Docker 并确保 docker 在 PATH 中,或改用 cloud 策略在云端构建镜像'));
66
+ // CLI 不存在,后续检查无意义,直接返回
67
+ return buildReport(checks);
68
+ }
69
+ checks.push(pass(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_DOCKER_CLI_MISSING, `Docker CLI 可用:${cli.stdout}`));
70
+ // 2. Docker daemon 是否可用
71
+ const daemon = await runner('docker', ['info', '--format', '{{.ServerVersion}}']);
72
+ if (!daemon.ok || !daemon.stdout) {
73
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_DOCKER_DAEMON_UNAVAILABLE, 'Docker daemon 不可用或未运行', '请启动 Docker daemon(如 Docker Desktop)后重试,或改用 cloud 策略'));
74
+ }
75
+ else {
76
+ checks.push(pass(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_DOCKER_DAEMON_UNAVAILABLE, `Docker daemon 可用:${daemon.stdout}`));
77
+ }
78
+ // 3. buildx 是否可用(跨平台构建 amd64 依赖它)
79
+ const buildx = await runner('docker', ['buildx', 'version']);
80
+ if (!buildx.ok) {
81
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_DOCKER_BUILDX_MISSING, '未检测到 docker buildx', '请安装/启用 docker buildx 插件以构建 linux/amd64 镜像,或改用 cloud 策略'));
82
+ }
83
+ else {
84
+ checks.push(pass(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_DOCKER_BUILDX_MISSING, `docker buildx 可用:${buildx.stdout.split('\n')[0]}`));
85
+ }
86
+ return buildReport(checks);
87
+ }
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runImageRemotePreflight = runImageRemotePreflight;
4
+ const child_process_1 = require("child_process");
5
+ const config_guard_1 = require("./config-guard");
6
+ const types_1 = require("../types");
7
+ /**
8
+ * 镜像远端校验 Preflight(有 IO,scope: cloud)
9
+ *
10
+ * 仅用于 image 策略(已有镜像)。config-guard 已做过纯字符串层面的校验
11
+ * (URI 格式、缺 tag/digest、禁 latest、imageType/registryId);本模块进一步
12
+ * 校验镜像在远端仓库的“真实状态”:
13
+ * 1. 远端 manifest 是否存在(镜像确实已推送),并解析出不可变 digest;
14
+ * 2. 企业版 TCR:镜像 registry 域名地域与函数部署地域是否一致(跨地域拉取失败/慢)。
15
+ *
16
+ * 设计边界(与 cam-preflight 同构):
17
+ * - 命令一律 execFile + 参数数组 + shell 关闭,imageUri 作为独立参数传入,杜绝命令注入;
18
+ * - 本模块不接触任何凭证,假设调用方已 docker login 目标私有仓库;
19
+ * - 远端查不到镜像属于真实前置缺失 → fail 阻断;
20
+ * - inspect 因命令缺失/网络波动等无法执行时不硬失败 → 降级 warn,避免误伤;
21
+ * - 个人版 CCR 无地域概念,无法从域名解析地域或部署地域未知时跳过地域校验,不误报。
22
+ */
23
+ /** imagetools inspect 超时(毫秒):仅查manifest,较快 */
24
+ const INSPECT_TIMEOUT = 60 * 1000;
25
+ /** 默认命令执行器:固定参数数组、无 shell、带超时 */
26
+ function defaultRunCommand(command, args) {
27
+ return new Promise(resolve => {
28
+ (0, child_process_1.execFile)(command, args, { timeout: INSPECT_TIMEOUT, shell: false, windowsHide: true, maxBuffer: 16 * 1024 * 1024 }, (error, stdout, stderr) => {
29
+ // ENOENT 表示命令不存在(docker 未安装),需与“执行了但失败”区分
30
+ const spawnFailed = !!error && (error.code === 'ENOENT' || error.errno === 'ENOENT');
31
+ resolve({
32
+ ok: !error,
33
+ stdout: (stdout || '').toString(),
34
+ stderr: (stderr || '').toString(),
35
+ spawnFailed
36
+ });
37
+ });
38
+ });
39
+ }
40
+ const defaultDeps = {
41
+ runCommand: defaultRunCommand
42
+ };
43
+ function pass(id, summary) {
44
+ return { id, status: 'pass', summary };
45
+ }
46
+ function warn(id, summary, remediation) {
47
+ return { id, status: 'warn', summary, remediation };
48
+ }
49
+ function fail(id, summary, options = {}) {
50
+ return { id, status: 'fail', summary, remediation: options.remediation, detail: options.detail };
51
+ }
52
+ function buildReport(checks) {
53
+ return {
54
+ scope: 'cloud',
55
+ strategy: 'image',
56
+ ready: !checks.some(item => item.status === 'fail'),
57
+ checks
58
+ };
59
+ }
60
+ /** 从 imagetools inspect 输出解析远端 digest */
61
+ function parseRemoteDigest(output) {
62
+ const match = output.match(/Digest:\s*(sha256:[0-9a-f]{64})/i);
63
+ return match ? match[1] : undefined;
64
+ }
65
+ /**
66
+ * 从企业版 TCR registry 域名解析地域
67
+ *
68
+ * 企业版 TCR 域名形如 `<instance>.tencentcloudcr.com`,地域信息不在域名中,
69
+ * 需由 registryId/其它渠道确定,本阶段无法仅凭域名解析 → 返回 undefined 跳过。
70
+ * 个人版 CCR `ccr.ccs.tencentyun.com` 为全局服务,无地域概念 → 同样返回 undefined。
71
+ *
72
+ * 仅当 registry 域名中显式内嵌可识别地域(如包含 ap-guangzhou 等地域片段)时才解析出,
73
+ * 用于对“显式跨地域”这一确定性错误给出拦截,其余情况一律跳过、不误报。
74
+ */
75
+ function resolveRegistryRegion(registry) {
76
+ if (!registry) {
77
+ return undefined;
78
+ }
79
+ const lower = registry.toLowerCase();
80
+ // 个人版 CCR 与企业版通用域名无地域信息,跳过
81
+ if (lower.includes('ccr.ccs.tencentyun.com') || /^[^.]+\.tencentcloudcr\.com$/.test(lower)) {
82
+ return undefined;
83
+ }
84
+ // 匹配域名中显式内嵌的腾讯云地域片段(ap-/eu-/na-/sa- 前缀)
85
+ const match = lower.match(/\b((?:ap|eu|na|sa)-[a-z]+(?:-[a-z]+)?)\b/);
86
+ return match ? match[1] : undefined;
87
+ }
88
+ /** 是否需要执行远端校验:仅 HTTP image 策略 */
89
+ function needsRemoteCheck(config) {
90
+ return config.type === 'HTTP' && config.buildStrategy === 'image';
91
+ }
92
+ /** 校验 registry 地域与函数部署地域一致性 */
93
+ function checkRegion(registry, deployRegion, checks) {
94
+ const imageRegion = resolveRegistryRegion(registry);
95
+ // 任一地域无法确定则跳过,避免对个人版/无地域信息的场景误报
96
+ if (!imageRegion || !deployRegion) {
97
+ return;
98
+ }
99
+ const target = deployRegion.toLowerCase();
100
+ if (imageRegion !== target) {
101
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_REGION_MISMATCH, `镜像仓库地域(${imageRegion})与函数部署地域(${target})不一致`, {
102
+ remediation: '跨地域拉取镜像会失败或明显变慢,请使用与函数同地域的镜像仓库',
103
+ detail: `registry:${registry}`
104
+ }));
105
+ }
106
+ }
107
+ /**
108
+ * 执行镜像远端校验 Preflight
109
+ *
110
+ * @param config 规范化部署配置
111
+ * @param deployRegion 函数部署地域(可选,来源于 environment;未知时跳过地域校验)
112
+ * @param deps 可注入依赖,默认真实命令;测试可传 mock
113
+ * @returns scope 为 cloud 的检查报告
114
+ */
115
+ async function runImageRemotePreflight(config, deployRegion, deps = defaultDeps) {
116
+ var _a;
117
+ if (!needsRemoteCheck(config)) {
118
+ return buildReport([]);
119
+ }
120
+ // needsRemoteCheck 为类型守卫,early-return 后 config 已收窄为 INormalizedHttpImageConfig
121
+ const imageUri = (_a = config.imageConfig) === null || _a === void 0 ? void 0 : _a.imageUri;
122
+ if (typeof imageUri !== 'string' || !imageUri.trim()) {
123
+ // 缺 URI 属于配置契约问题,已由 config-guard 覆盖,此处不重复
124
+ return buildReport([]);
125
+ }
126
+ const parsed = (0, config_guard_1.parseImageReference)(imageUri);
127
+ const checks = [];
128
+ // 地域校验(不依赖远端查询,先做)
129
+ checkRegion(parsed === null || parsed === void 0 ? void 0 : parsed.registry, deployRegion, checks);
130
+ // 远端 manifest 查询
131
+ const inspect = await deps.runCommand('docker', ['buildx', 'imagetools', 'inspect', imageUri]);
132
+ if (inspect.spawnFailed) {
133
+ // docker 不可用,无法远端自查→ 降级提示,不阻断(zip/已存在镜像等场景可能无docker)
134
+ checks.push(warn(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_REMOTE_UNVERIFIABLE, '本地无可用的 docker,无法校验镜像远端是否存在', '若镜像确实已推送到目标仓库可忽略;否则请确认镜像已 push 且账号有拉取权限'));
135
+ return buildReport(checks);
136
+ }
137
+ if (!inspect.ok) {
138
+ const combined = `${inspect.stderr} ${inspect.stdout}`.toLowerCase();
139
+ // 明确的“不存在”类错误 → fail 阻断
140
+ const notFound = combined.includes('not found') ||
141
+ combined.includes('manifest unknown') ||
142
+ combined.includes('no such') ||
143
+ combined.includes('does not exist');
144
+ if (notFound) {
145
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_REMOTE_NOT_FOUND, '目标镜像在远端仓库不存在', {
146
+ remediation: '请确认 imageConfig.imageUri 正确、镜像已 push 到该仓库,且当前账号已 docker login 具备拉取权限',
147
+ detail: `imageUri:${imageUri}`
148
+ }));
149
+ return buildReport(checks);
150
+ }
151
+ // 其它错误(如未登录私有仓库、网络波动)无法断定不存在 → 降级 warn,不误伤
152
+ checks.push(warn(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_REMOTE_UNVERIFIABLE, '无法确认镜像远端状态(可能未登录私有仓库或网络异常)', `请确认已 docker login 目标仓库后重试;错误:${(inspect.stderr || inspect.stdout || '').trim().slice(0, 200)}`));
153
+ return buildReport(checks);
154
+ }
155
+ // 远端存在,解析 digest
156
+ const digest = parseRemoteDigest(inspect.stdout);
157
+ if (digest) {
158
+ checks.push(pass(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_REMOTE_NOT_FOUND, `镜像远端存在,digest 为 ${digest}`));
159
+ }
160
+ else {
161
+ checks.push(warn(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_DIGEST_UNRESOLVED, '镜像远端存在,但未能解析出 digest', '部署可继续,但无法记录不可变 digest 用于回溯'));
162
+ }
163
+ return buildReport(checks);
164
+ }
@@ -0,0 +1,129 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.buildAndPushImage = buildAndPushImage;
7
+ const child_process_1 = require("child_process");
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const error_1 = require("../../error");
11
+ const types_1 = require("../types");
12
+ /**
13
+ * 本地镜像构建与推送(local 策略,个人版先行版)
14
+ *
15
+ * 流程:解析构建上下文与目标镜像 → buildx 构建 amd64 并推送 → 查询远端 digest。
16
+ *
17
+ * 安全(对应 security_rules 的 RCE 防护):
18
+ * - 所有 docker 命令用 execFile + 参数数组执行,shell 关闭;tag、路径、buildArgs
19
+ * 都作为独立参数传入,绝不拼接进 shell 字符串,杜绝命令注入。
20
+ * - 不处理登录:假设调用方已 docker login 目标仓库;本模块不接触任何凭证/密码。
21
+ *
22
+ * 命令与文件系统操作以依赖注入方式提供,默认使用真实实现,便于测试替换(不依赖 jest.mock)。
23
+ */
24
+ /** 构建/推送命令超时(毫秒),镜像构建较慢,给足时间 */
25
+ const BUILD_TIMEOUT = 10 * 60 * 1000;
26
+ /** 默认命令执行器:固定参数数组、无 shell、带超时与最大缓冲 */
27
+ function defaultRunCommand(command, args) {
28
+ return new Promise(resolve => {
29
+ (0, child_process_1.execFile)(command, args, { timeout: BUILD_TIMEOUT, shell: false, windowsHide: true, maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => {
30
+ resolve({
31
+ ok: !error,
32
+ stdout: (stdout || '').toString(),
33
+ stderr: (stderr || '').toString()
34
+ });
35
+ });
36
+ });
37
+ }
38
+ /** 默认依赖:真实命令执行与 fs 检查 */
39
+ const defaultDeps = {
40
+ runCommand: defaultRunCommand,
41
+ existsSync: target => fs_1.default.existsSync(target),
42
+ isDirectory: target => fs_1.default.statSync(target).isDirectory(),
43
+ isFile: target => fs_1.default.statSync(target).isFile()
44
+ };
45
+ /** 解析构建上下文目录(绝对路径) */
46
+ function resolveContext(config) {
47
+ const cwd = config.build.cwd || config.functionPath || process.cwd();
48
+ return path_1.default.resolve(cwd);
49
+ }
50
+ /** 解析目标镜像完整引用 imageUri(repository 需为含域名与命名空间的完整仓库地址,个人版/企业版通用) */
51
+ function resolveImageUri(config) {
52
+ const repository = config.build.repository;
53
+ if (typeof repository !== 'string' || !repository.trim()) {
54
+ throw new error_1.CloudBaseError('local 策略缺少目标镜像仓库地址 build.repository', {
55
+ code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_REPOSITORY_MISSING
56
+ });
57
+ }
58
+ // tag 缺省用时间戳,避免 latest;不可变 tag 便于回溯
59
+ const tag = config.build.tag || `v${Date.now()}`;
60
+ return `${repository.trim()}:${tag}`;
61
+ }
62
+ /** 从 buildx imagetools inspect 输出解析 digest */
63
+ function parseDigest(output) {
64
+ const match = output.match(/Digest:\s*(sha256:[0-9a-f]{64})/i);
65
+ return match ? match[1] : undefined;
66
+ }
67
+ /** 生成脱敏后的命令摘要,避免 buildArgs 值进入部署日志 */
68
+ function formatBuildCommandForLog(args) {
69
+ const displayArgs = [];
70
+ for (let index = 0; index < args.length; index += 1) {
71
+ const arg = args[index];
72
+ displayArgs.push(arg);
73
+ if (arg === '--build-arg' && index + 1 < args.length) {
74
+ const buildArg = args[index + 1];
75
+ const separatorIndex = buildArg.indexOf('=');
76
+ const key = separatorIndex >= 0 ? buildArg.slice(0, separatorIndex) : buildArg;
77
+ displayArgs.push(`${key}=<redacted>`);
78
+ index += 1;
79
+ }
80
+ }
81
+ return `docker ${displayArgs.join(' ')}`;
82
+ }
83
+ /**
84
+ * 本地构建镜像并推送到目标仓库
85
+ *
86
+ * @param config 规范化的 local 部署配置
87
+ * @param onLog 可选日志回调,用于透出构建输出(不含凭证)
88
+ * @param deps 可注入依赖,默认真实命令与 fs;测试可传 mock
89
+ * @returns 最终镜像引用与 digest
90
+ */
91
+ async function buildAndPushImage(config, onLog, deps = defaultDeps) {
92
+ const context = resolveContext(config);
93
+ if (!deps.existsSync(context) || !deps.isDirectory(context)) {
94
+ throw new error_1.CloudBaseError(`构建上下文目录不存在:${context}`, {
95
+ code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_CONTEXT_MISSING
96
+ });
97
+ }
98
+ const dockerfile = config.build.dockerfile || types_1.DEFAULT_DOCKERFILE;
99
+ const dockerfilePath = path_1.default.resolve(context, dockerfile);
100
+ if (!deps.existsSync(dockerfilePath) || !deps.isFile(dockerfilePath)) {
101
+ throw new error_1.CloudBaseError(`Dockerfile 不存在:${dockerfilePath}`, {
102
+ code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_DOCKERFILE_MISSING
103
+ });
104
+ }
105
+ const imageUri = resolveImageUri(config);
106
+ const platform = config.build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
107
+ // 组装 buildx 参数:构建目标平台镜像并直接推送
108
+ const args = ['buildx', 'build', '--platform', platform, '-f', dockerfilePath, '-t', imageUri];
109
+ if (config.build.forceBuild) {
110
+ args.push('--no-cache');
111
+ }
112
+ // 追加构建参数(键值均作为独立参数,无 shell 拼接)
113
+ const buildArgs = config.build.buildArgs || {};
114
+ for (const key of Object.keys(buildArgs)) {
115
+ args.push('--build-arg', `${key}=${buildArgs[key]}`);
116
+ }
117
+ args.push('--push', context);
118
+ onLog === null || onLog === void 0 ? void 0 : onLog(`执行 ${formatBuildCommandForLog(args)}`);
119
+ const build = await deps.runCommand('docker', args);
120
+ if (!build.ok) {
121
+ throw new error_1.CloudBaseError('镜像构建或推送失败,请查看 Docker 构建输出定位具体原因', {
122
+ code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_PUSH_FAILED
123
+ });
124
+ }
125
+ // 查询远端 digest,产出不可变引用
126
+ const inspect = await deps.runCommand('docker', ['buildx', 'imagetools', 'inspect', imageUri]);
127
+ const imageDigest = inspect.ok ? parseDigest(inspect.stdout) : undefined;
128
+ return { imageUri, imageDigest };
129
+ }
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toCurrentState = toCurrentState;
4
+ exports.createDeployPlan = createDeployPlan;
5
+ const constant_1 = require("../../constant");
6
+ const config_guard_1 = require("./config-guard");
7
+ /**
8
+ * 部署计划生成
9
+ *
10
+ * 纯函数实现,线上函数状态由调用方查询后传入,便于单独测试
11
+ */
12
+ /** 将 SCF 返回的函数详情收敛为计划所需的状态摘要 */
13
+ function toCurrentState(info) {
14
+ var _a;
15
+ const rawType = typeof (info === null || info === void 0 ? void 0 : info.Type) === 'string' ? info.Type : '';
16
+ const type = rawType.toLowerCase() === 'http' ? 'HTTP' : 'Event';
17
+ const imageUri = typeof ((_a = info === null || info === void 0 ? void 0 : info.ImageConfig) === null || _a === void 0 ? void 0 : _a.ImageUri) === 'string'
18
+ ? info.ImageConfig.ImageUri
19
+ : undefined;
20
+ return {
21
+ functionName: info === null || info === void 0 ? void 0 : info.FunctionName,
22
+ type,
23
+ runtime: info === null || info === void 0 ? void 0 : info.Runtime,
24
+ status: info === null || info === void 0 ? void 0 : info.Status,
25
+ protocolType: info === null || info === void 0 ? void 0 : info.ProtocolType,
26
+ triggerNames: Array.isArray(info === null || info === void 0 ? void 0 : info.Triggers)
27
+ ? info.Triggers.map(item => (item === null || item === void 0 ? void 0 : item.TriggerName) || (item === null || item === void 0 ? void 0 : item.Name)).filter(Boolean)
28
+ : [],
29
+ isCustomImage: (0, config_guard_1.isCustomImageRuntime)(info === null || info === void 0 ? void 0 : info.Runtime),
30
+ imageUri,
31
+ timeout: typeof (info === null || info === void 0 ? void 0 : info.Timeout) === 'number' ? info.Timeout : undefined,
32
+ memorySize: typeof (info === null || info === void 0 ? void 0 : info.MemorySize) === 'number' ? info.MemorySize : undefined,
33
+ description: typeof (info === null || info === void 0 ? void 0 : info.Description) === 'string' ? info.Description : undefined,
34
+ envVariables: extractEnvVariables(info)
35
+ };
36
+ }
37
+ /** 从线上函数详情提取环境变量为键值对 */
38
+ function extractEnvVariables(info) {
39
+ var _a;
40
+ const list = (_a = info === null || info === void 0 ? void 0 : info.Environment) === null || _a === void 0 ? void 0 : _a.Variables;
41
+ if (!Array.isArray(list)) {
42
+ return undefined;
43
+ }
44
+ const result = {};
45
+ for (const item of list) {
46
+ const key = item === null || item === void 0 ? void 0 : item.Key;
47
+ if (typeof key === 'string') {
48
+ result[key] = typeof (item === null || item === void 0 ? void 0 : item.Value) === 'string' ? item.Value : '';
49
+ }
50
+ }
51
+ return result;
52
+ }
53
+ /** 归一化运行时字符串,便于大小写不敏感比较 */
54
+ function normalizeRuntime(runtime) {
55
+ return typeof runtime === 'string' ? runtime.trim().toLowerCase() : '';
56
+ }
57
+ /** 归一化协议类型:SCF 缺省/空值与显式 HTTP 都表示普通 HTTP */
58
+ function normalizeProtocolType(protocolType) {
59
+ const normalized = typeof protocolType === 'string' ? protocolType.trim().toUpperCase() : '';
60
+ return normalized || 'HTTP';
61
+ }
62
+ /**
63
+ * 计算运行时变化导致的重建原因
64
+ *
65
+ * 需要区分三类不可原地修改的变化:
66
+ * - 托管运行时与自定义镜像互切
67
+ * - 两个不同托管运行时之间互切(SCF Runtime 是创建期字段)
68
+ * 两侧都是自定义镜像时,runtime 统一为 CustomImage,镜像内容变化走更新而非重建
69
+ */
70
+ function collectRuntimeReplaceReasons(config, current) {
71
+ const reasons = [];
72
+ const targetIsCustomImage = (0, config_guard_1.isCustomImageRuntime)(config.runtime);
73
+ if (current.isCustomImage !== targetIsCustomImage) {
74
+ reasons.push(targetIsCustomImage
75
+ ? `运行时需要从 ${current.runtime || '托管运行时'} 变更为自定义镜像`
76
+ : '运行时需要从自定义镜像变更为托管运行时');
77
+ return reasons;
78
+ }
79
+ // 两侧都是镜像时,runtime 恒为 CustomImage,不因镜像地址差异触发重建
80
+ if (targetIsCustomImage) {
81
+ return reasons;
82
+ }
83
+ const targetRuntime = config.runtime;
84
+ // 目标未显式指定 runtime 时不推断变化,交由更新流程按现状处理
85
+ if (targetRuntime === undefined) {
86
+ return reasons;
87
+ }
88
+ if (normalizeRuntime(targetRuntime) !== normalizeRuntime(current.runtime)) {
89
+ reasons.push(`运行时需要从 ${current.runtime || '未知'} 变更为 ${targetRuntime}`);
90
+ }
91
+ return reasons;
92
+ }
93
+ /**
94
+ * 计算协议类型变化导致的重建原因
95
+ *
96
+ * ProtocolType 是创建期字段,新增、移除或切换都无法通过更新接口生效。
97
+ * 仅 HTTP 函数存在协议类型,Event 函数不参与判断。
98
+ */
99
+ function collectProtocolReplaceReasons(config, current) {
100
+ if (config.type !== 'HTTP' || current.type !== 'HTTP') {
101
+ return [];
102
+ }
103
+ const target = normalizeProtocolType(config.protocolType);
104
+ const online = normalizeProtocolType(current.protocolType);
105
+ if (target === online) {
106
+ return [];
107
+ }
108
+ const describe = (value) => (value === '' ? '普通 HTTP' : value);
109
+ return [`协议类型需要从 ${describe(online)} 变更为 ${describe(target)}`];
110
+ }
111
+ /**
112
+ * 计算必须删除重建的原因
113
+ *
114
+ * SCF 的 Type、Runtime、ProtocolType 只能在创建时指定,
115
+ * 这些字段发生变化时无法通过更新接口生效
116
+ */
117
+ function collectReplaceReasons(config, current) {
118
+ const reasons = [];
119
+ if (current.type !== config.type) {
120
+ reasons.push(`函数类型需要从 ${current.type} 变更为 ${config.type}`);
121
+ }
122
+ reasons.push(...collectRuntimeReplaceReasons(config, current));
123
+ reasons.push(...collectProtocolReplaceReasons(config, current));
124
+ return reasons;
125
+ }
126
+ /** 计算部署后需要收敛的访问相关操作 */
127
+ function collectAccessOperations(config) {
128
+ const operations = [];
129
+ if (config.type !== 'HTTP') {
130
+ return operations;
131
+ }
132
+ if (config.public !== undefined) {
133
+ operations.push('configure-access');
134
+ }
135
+ if (config.gatewayPath !== undefined) {
136
+ operations.push('configure-gateway');
137
+ }
138
+ return operations;
139
+ }
140
+ /** 归一化环境变量为可比较的稳定字符串(键排序后 JSON) */
141
+ function serializeEnv(env) {
142
+ if (!env || typeof env !== 'object') {
143
+ return '';
144
+ }
145
+ const keys = Object.keys(env).sort();
146
+ return keys.map(key => `${key}=${String(env[key])}`).join('\n');
147
+ }
148
+ /**
149
+ * 计算相对线上状态的变更原因
150
+ *
151
+ * 返回空数组表示所有可比字段一致,可判定为 noop。
152
+ * 保守原则:任何无法可靠比较的维度都视为“有变更”,宁可多更新也不漏更新。
153
+ * - code 类(zip/cos)无法从线上取回源码比较,始终视为有变更;
154
+ * - 仅比较配置方显式提供的字段,未提供的字段不参与(避免默认值误判)。
155
+ */
156
+ function collectChangeReasons(config, current) {
157
+ var _a;
158
+ const reasons = [];
159
+ // code 类部署无法可靠比较本地代码与线上代码,始终按更新处理
160
+ if (config.buildStrategy !== 'image') {
161
+ reasons.push('代码包部署无法比对本地与线上代码内容,按更新处理');
162
+ return reasons;
163
+ }
164
+ // image 策略:比较镜像地址
165
+ const targetImageUri = (_a = config.imageConfig) === null || _a === void 0 ? void 0 : _a.imageUri;
166
+ if (typeof targetImageUri === 'string' && targetImageUri.trim()) {
167
+ // 线上ImageUri 可能带 @sha256 digest 后缀,用前缀匹配容忍 digest 附加
168
+ const online = current.imageUri || '';
169
+ const matched = online === targetImageUri || online.startsWith(`${targetImageUri}@`);
170
+ if (!matched) {
171
+ reasons.push(`镜像地址需要从 ${online || '未知'} 变更为 ${targetImageUri}`);
172
+ }
173
+ }
174
+ else {
175
+ // 期望镜像地址缺失,无法比较,保守更新
176
+ reasons.push('无法确定目标镜像地址,按更新处理');
177
+ }
178
+ reasons.push(...collectConfigChangeReasons(config, current));
179
+ return reasons;
180
+ }
181
+ /** 比较可原地更新的配置字段(仅比较配置方显式提供的) */
182
+ function collectConfigChangeReasons(config, current) {
183
+ var _a, _b;
184
+ const reasons = [];
185
+ const cfg = config;
186
+ if (typeof cfg.timeout === 'number' && cfg.timeout !== current.timeout) {
187
+ reasons.push(`超时时间需要从 ${(_a = current.timeout) !== null && _a !== void 0 ? _a : '未知'} 变更为 ${cfg.timeout}`);
188
+ }
189
+ if (typeof cfg.memorySize === 'number' && cfg.memorySize !== current.memorySize) {
190
+ reasons.push(`内存规格需要从 ${(_b = current.memorySize) !== null && _b !== void 0 ? _b : '未知'} 变更为 ${cfg.memorySize}`);
191
+ }
192
+ if (typeof cfg.description === 'string' && cfg.description !== (current.description || '')) {
193
+ reasons.push('函数描述发生变更');
194
+ }
195
+ if (cfg.envVariables !== undefined) {
196
+ const target = serializeEnv(cfg.envVariables);
197
+ const online = serializeEnv(current.envVariables);
198
+ if (target !== online) {
199
+ reasons.push('环境变量发生变更');
200
+ }
201
+ }
202
+ return reasons;
203
+ }
204
+ /**
205
+ * 生成部署计划
206
+ * @param config 已规范化的部署配置
207
+ * @param current 线上函数详情,不存在时传 null
208
+ */
209
+ function createDeployPlan(config, current) {
210
+ const currentState = current ? toCurrentState(current) : undefined;
211
+ const accessOperations = collectAccessOperations(config);
212
+ const reasons = [];
213
+ // 函数不存在,或存在但处于 CREATE_FAILED 状态(上次部署失败残留,视为需重新创建)。
214
+ // 走 create 通道后,FunctionService.createFunction 内部会检测到同名残留并自动删除重建,
215
+ // 对齐 tcb fn deploy 的 CREATE_FAILED 清理行为。
216
+ if (!currentState || currentState.status === constant_1.SCF_STATUS.CREATE_FAILED) {
217
+ const operations = ['create-function', ...accessOperations];
218
+ reasons.push((currentState === null || currentState === void 0 ? void 0 : currentState.status) === constant_1.SCF_STATUS.CREATE_FAILED
219
+ ? `检测到同名函数处于 CreateFailed 状态(上次部署失败残留),将清理后重新创建 ${config.type} 函数`
220
+ : `线上不存在同名函数,将创建 ${config.type} 函数`);
221
+ return {
222
+ functionName: config.name,
223
+ functionType: config.type,
224
+ buildStrategy: config.buildStrategy,
225
+ exists: false,
226
+ action: 'create',
227
+ operations,
228
+ reasons,
229
+ replaceReasons: []
230
+ };
231
+ }
232
+ const replaceReasons = collectReplaceReasons(config, currentState);
233
+ if (replaceReasons.length > 0) {
234
+ return {
235
+ functionName: config.name,
236
+ functionType: config.type,
237
+ buildStrategy: config.buildStrategy,
238
+ exists: true,
239
+ action: 'replace',
240
+ operations: ['replace-function', ...accessOperations],
241
+ reasons: [
242
+ '存在无法通过更新接口生效的变更,需要删除后重新创建函数',
243
+ ...replaceReasons
244
+ ],
245
+ replaceReasons,
246
+ current: currentState
247
+ };
248
+ }
249
+ const changeReasons = collectChangeReasons(config, currentState);
250
+ // 无任何可检出的变更时判定为 noop,跳过代码与配置更新
251
+ // 但访问收敛(public/gatewayPath)若配置方显式要求,仍需保留为待执行操作
252
+ if (changeReasons.length === 0) {
253
+ return {
254
+ functionName: config.name,
255
+ functionType: config.type,
256
+ buildStrategy: config.buildStrategy,
257
+ exists: true,
258
+ action: 'noop',
259
+ operations: [...accessOperations],
260
+ reasons: ['线上函数与目标配置一致,无需更新代码或配置'],
261
+ replaceReasons: [],
262
+ current: currentState
263
+ };
264
+ }
265
+ reasons.push('线上已存在同名函数,将更新代码与配置');
266
+ reasons.push(...changeReasons);
267
+ return {
268
+ functionName: config.name,
269
+ functionType: config.type,
270
+ buildStrategy: config.buildStrategy,
271
+ exists: true,
272
+ action: 'update',
273
+ operations: ['update-code', 'update-config', ...accessOperations],
274
+ reasons,
275
+ replaceReasons: [],
276
+ current: currentState
277
+ };
278
+ }