@cloudbase/manager-node 5.6.8 → 5.7.0-beta.0
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/lib/cloudApp/index.js +12 -3
- package/lib/cloudrun/index.js +0 -18
- package/lib/deploy/DatabaseDeployer.js +238 -0
- package/lib/deploy/DeployOrchestrator.js +564 -0
- package/lib/deploy/FunctionDeployer.js +611 -0
- package/lib/deploy/GatewayDeployer.js +522 -0
- package/lib/deploy/StateStore.js +327 -0
- package/lib/deploy/StaticDeployer.js +289 -0
- package/lib/deploy/domain.js +39 -0
- package/lib/deploy/framework.js +105 -0
- package/lib/deploy/function/artifact.js +207 -0
- package/lib/deploy/function/builders/cloud.js +454 -0
- package/lib/deploy/function/cam-preflight.js +157 -0
- package/lib/deploy/function/cloud-build-service.js +170 -0
- package/lib/deploy/function/config-guard.js +588 -0
- package/lib/deploy/function/docker-preflight.js +87 -0
- package/lib/deploy/function/image-preflight.js +164 -0
- package/lib/deploy/function/local-builder.js +110 -0
- package/lib/deploy/function/planner.js +282 -0
- package/lib/deploy/function/preflight.js +264 -0
- package/lib/deploy/types.js +99 -0
- package/lib/environment.js +11 -0
- package/lib/hosting/index.js +4 -1
- package/lib/index.js +31 -0
- package/lib/projectValidator/index.js +375 -0
- package/lib/projectValidator/types.js +2 -0
- package/lib/storage/index.js +6 -7
- package/lib/utils/index.js +41 -1
- package/package.json +2 -1
- package/types/cloudApp/index.d.ts +4 -0
- package/types/cloudApp/types.d.ts +87 -6
- package/types/cloudrun/index.d.ts +1 -7
- package/types/cloudrun/type.d.ts +0 -22
- package/types/deploy/DatabaseDeployer.d.ts +70 -0
- package/types/deploy/DeployOrchestrator.d.ts +150 -0
- package/types/deploy/FunctionDeployer.d.ts +158 -0
- package/types/deploy/GatewayDeployer.d.ts +175 -0
- package/types/deploy/StateStore.d.ts +170 -0
- package/types/deploy/StaticDeployer.d.ts +104 -0
- package/types/deploy/domain.d.ts +17 -0
- package/types/deploy/framework.d.ts +23 -0
- package/types/deploy/function/artifact.d.ts +40 -0
- package/types/deploy/function/builders/cloud.d.ts +110 -0
- package/types/deploy/function/cam-preflight.d.ts +51 -0
- package/types/deploy/function/cloud-build-service.d.ts +10 -0
- package/types/deploy/function/config-guard.d.ts +41 -0
- package/types/deploy/function/docker-preflight.d.ts +17 -0
- package/types/deploy/function/image-preflight.d.ts +21 -0
- package/types/deploy/function/local-builder.d.ts +26 -0
- package/types/deploy/function/planner.d.ts +15 -0
- package/types/deploy/function/preflight.d.ts +9 -0
- package/types/deploy/types.d.ts +429 -0
- package/types/env/type.d.ts +4 -4
- package/types/environment.d.ts +7 -0
- package/types/function/types.d.ts +15 -0
- package/types/hosting/index.d.ts +6 -0
- package/types/index.d.ts +18 -0
- package/types/interfaces/function.interface.d.ts +1 -1
- package/types/projectValidator/index.d.ts +24 -0
- package/types/projectValidator/types.d.ts +26 -0
- package/types/storage/index.d.ts +6 -0
- package/types/utils/index.d.ts +13 -0
|
@@ -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,110 @@
|
|
|
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
|
+
/**
|
|
68
|
+
* 本地构建镜像并推送到目标仓库
|
|
69
|
+
*
|
|
70
|
+
* @param config 规范化的 local 部署配置
|
|
71
|
+
* @param onLog 可选日志回调,用于透出构建输出(不含凭证)
|
|
72
|
+
* @param deps 可注入依赖,默认真实命令与 fs;测试可传 mock
|
|
73
|
+
* @returns 最终镜像引用与 digest
|
|
74
|
+
*/
|
|
75
|
+
async function buildAndPushImage(config, onLog, deps = defaultDeps) {
|
|
76
|
+
const context = resolveContext(config);
|
|
77
|
+
if (!deps.existsSync(context) || !deps.isDirectory(context)) {
|
|
78
|
+
throw new error_1.CloudBaseError(`构建上下文目录不存在:${context}`, {
|
|
79
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_CONTEXT_MISSING
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const dockerfile = config.build.dockerfile || types_1.DEFAULT_DOCKERFILE;
|
|
83
|
+
const dockerfilePath = path_1.default.resolve(context, dockerfile);
|
|
84
|
+
if (!deps.existsSync(dockerfilePath) || !deps.isFile(dockerfilePath)) {
|
|
85
|
+
throw new error_1.CloudBaseError(`Dockerfile 不存在:${dockerfilePath}`, {
|
|
86
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_DOCKERFILE_MISSING
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const imageUri = resolveImageUri(config);
|
|
90
|
+
const platform = config.build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
|
|
91
|
+
// 组装 buildx 参数:构建目标平台镜像并直接推送
|
|
92
|
+
const args = ['buildx', 'build', '--platform', platform, '-f', dockerfilePath, '-t', imageUri];
|
|
93
|
+
// 追加构建参数(键值均作为独立参数,无 shell 拼接)
|
|
94
|
+
const buildArgs = config.build.buildArgs || {};
|
|
95
|
+
for (const key of Object.keys(buildArgs)) {
|
|
96
|
+
args.push('--build-arg', `${key}=${buildArgs[key]}`);
|
|
97
|
+
}
|
|
98
|
+
args.push('--push', context);
|
|
99
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`执行 docker ${args.join(' ')}`);
|
|
100
|
+
const build = await deps.runCommand('docker', args);
|
|
101
|
+
if (!build.ok) {
|
|
102
|
+
throw new error_1.CloudBaseError(`镜像构建或推送失败:${build.stderr || build.stdout}`, {
|
|
103
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_PUSH_FAILED
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
// 查询远端 digest,产出不可变引用
|
|
107
|
+
const inspect = await deps.runCommand('docker', ['buildx', 'imagetools', 'inspect', imageUri]);
|
|
108
|
+
const imageDigest = inspect.ok ? parseDigest(inspect.stdout) : undefined;
|
|
109
|
+
return { imageUri, imageDigest };
|
|
110
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
requiresReplaceApproval: false
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const replaceReasons = collectReplaceReasons(config, currentState);
|
|
234
|
+
if (replaceReasons.length > 0) {
|
|
235
|
+
return {
|
|
236
|
+
functionName: config.name,
|
|
237
|
+
functionType: config.type,
|
|
238
|
+
buildStrategy: config.buildStrategy,
|
|
239
|
+
exists: true,
|
|
240
|
+
action: 'replace',
|
|
241
|
+
operations: ['replace-function', ...accessOperations],
|
|
242
|
+
reasons: [
|
|
243
|
+
'存在无法通过更新接口生效的变更,需要删除后重新创建函数',
|
|
244
|
+
...replaceReasons
|
|
245
|
+
],
|
|
246
|
+
replaceReasons,
|
|
247
|
+
requiresReplaceApproval: true,
|
|
248
|
+
current: currentState
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
const changeReasons = collectChangeReasons(config, currentState);
|
|
252
|
+
// 无任何可检出的变更时判定为 noop,跳过代码与配置更新
|
|
253
|
+
// 但访问收敛(public/gatewayPath)若配置方显式要求,仍需保留为待执行操作
|
|
254
|
+
if (changeReasons.length === 0) {
|
|
255
|
+
return {
|
|
256
|
+
functionName: config.name,
|
|
257
|
+
functionType: config.type,
|
|
258
|
+
buildStrategy: config.buildStrategy,
|
|
259
|
+
exists: true,
|
|
260
|
+
action: 'noop',
|
|
261
|
+
operations: [...accessOperations],
|
|
262
|
+
reasons: ['线上函数与目标配置一致,无需更新代码或配置'],
|
|
263
|
+
replaceReasons: [],
|
|
264
|
+
requiresReplaceApproval: false,
|
|
265
|
+
current: currentState
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
reasons.push('线上已存在同名函数,将更新代码与配置');
|
|
269
|
+
reasons.push(...changeReasons);
|
|
270
|
+
return {
|
|
271
|
+
functionName: config.name,
|
|
272
|
+
functionType: config.type,
|
|
273
|
+
buildStrategy: config.buildStrategy,
|
|
274
|
+
exists: true,
|
|
275
|
+
action: 'update',
|
|
276
|
+
operations: ['update-code', 'update-config', ...accessOperations],
|
|
277
|
+
reasons,
|
|
278
|
+
replaceReasons: [],
|
|
279
|
+
requiresReplaceApproval: false,
|
|
280
|
+
current: currentState
|
|
281
|
+
};
|
|
282
|
+
}
|