@cloudbase/manager-node 5.8.0-beta.0 → 5.8.0-beta.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.
@@ -158,7 +158,7 @@ class DeployOrchestrator {
158
158
  // (失败资源保留旧快照,下次 diff 仍会检测到差异);快照写入失败不影响部署结果
159
159
  if (!dryRun) {
160
160
  try {
161
- await this.persistState({ config, plan, results, cwd });
161
+ await this.persistState({ config, plan, results, cwd, envId: options.envId });
162
162
  }
163
163
  catch (e) {
164
164
  (_j = log === null || log === void 0 ? void 0 : log.warn) === null || _j === void 0 ? void 0 : _j.call(log, `状态快照写入失败:${(e === null || e === void 0 ? void 0 : e.message) || String(e)}`);
@@ -203,7 +203,7 @@ class DeployOrchestrator {
203
203
  */
204
204
  async persistState(options) {
205
205
  var _a, _b, _c, _d, _e, _f;
206
- const { config, plan, results, cwd } = options;
206
+ const { config, plan, results, cwd, envId } = options;
207
207
  const stateStore = new StateStore_1.StateStore({ cwd });
208
208
  const prev = stateStore.read();
209
209
  // hosting
@@ -263,7 +263,7 @@ class DeployOrchestrator {
263
263
  resources.gateway = prev.resources.gateway;
264
264
  }
265
265
  if (Object.keys(resources).length) {
266
- stateStore.write({ resources });
266
+ stateStore.write({ resources, envId });
267
267
  }
268
268
  }
269
269
  /**
@@ -291,7 +291,11 @@ class DeployOrchestrator {
291
291
  };
292
292
  // 本地 state 快照(diff 基准):--refresh 时忽略本地 skip 判定
293
293
  const stateStore = new StateStore_1.StateStore({ cwd });
294
- const state = refresh ? null : stateStore.read();
294
+ const rawState = refresh ? null : stateStore.read();
295
+ // 跨环境保护:快照记录的 envId 与目标环境不一致(或旧格式快照无 envId)时,
296
+ // 快照是其他环境部署后的指纹,不可作为 skip 依据 → 视为无快照,强制走全量部署,
297
+ // 避免切换环境后误判「产物未变更」导致新环境 COS 404
298
+ const state = rawState && rawState.envId === envId ? rawState : null;
295
299
  const planCtx = { config, envId, stateStore, state, refresh, cwd };
296
300
  for (const type of DEPLOY_ORDER) {
297
301
  if (!isEnabled(type))
@@ -199,9 +199,7 @@ class FunctionDeployer {
199
199
  const ctx = { steps: [], options };
200
200
  const warnings = [];
201
201
  const report = (0, config_guard_1.checkDeployConfig)(config);
202
- report.checks
203
- .filter(item => item.status === 'warn')
204
- .forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
202
+ this.appendWarnings(warnings, report);
205
203
  const normalized = await this.runStep(ctx, 'validate', async () => {
206
204
  return (0, config_guard_1.assertDeployConfig)(config);
207
205
  });
@@ -221,26 +219,20 @@ class FunctionDeployer {
221
219
  }
222
220
  // 不可原地更新的变更必须在任何可能产生副作用的 Preflight 之前中止。
223
221
  // 尤其避免镜像拉取授权自动补齐等云端写操作先发生,再提示用户手工删除函数。
224
- this.assertPlanExecutable(normalized, plan);
222
+ this.assertPlanExecutable(plan);
225
223
  await this.runStep(ctx, 'preflight', async () => {
226
224
  const report = (0, preflight_1.runLocalPreflight)(normalized);
227
- report.checks
228
- .filter(item => item.status === 'warn')
229
- .forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
225
+ this.appendWarnings(warnings, report);
230
226
  this.assertPreflightReady(report);
231
227
  });
232
228
  await this.runStep(ctx, 'preflight', async () => {
233
229
  const report = await (0, cam_preflight_1.runImagePullAuthPreflight)(this.createCamPreflightAdapter(), normalized, options.autoGrant !== false);
234
- report.checks
235
- .filter(item => item.status === 'warn')
236
- .forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
230
+ this.appendWarnings(warnings, report);
237
231
  this.assertPreflightReady(report);
238
232
  });
239
233
  await this.runStep(ctx, 'preflight', async () => {
240
234
  const report = await (0, image_preflight_1.runImageRemotePreflight)(normalized, this.resolveDeployRegion(), this.imagePreflightDeps);
241
- report.checks
242
- .filter(item => item.status === 'warn')
243
- .forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
235
+ this.appendWarnings(warnings, report);
244
236
  this.assertPreflightReady(report);
245
237
  });
246
238
  // noop:函数本身无变更,但仍可能需要收敛访问配置
@@ -367,6 +359,15 @@ class FunctionDeployer {
367
359
  const code = (error === null || error === void 0 ? void 0 : error.code) || ((_b = (_a = error === null || error === void 0 ? void 0 : error.original) === null || _a === void 0 ? void 0 : _a.Error) === null || _b === void 0 ? void 0 : _b.Code) || '';
368
360
  return typeof code === 'string' && code.startsWith('ResourceNotFound');
369
361
  }
362
+ /** 将 warning 连同可执行修复建议汇入部署结果,避免授权链接在编排层丢失 */
363
+ appendWarnings(warnings, report) {
364
+ report.checks
365
+ .filter(item => item.status === 'warn')
366
+ .forEach(item => {
367
+ const remediation = item.remediation ? `,${item.remediation}` : '';
368
+ warnings.push(`[${item.id}] ${item.summary}${remediation}`);
369
+ });
370
+ }
370
371
  /**
371
372
  * 校验 Preflight 报告,存在 fail 项时聚合抛错
372
373
  */
@@ -390,16 +391,12 @@ class FunctionDeployer {
390
391
  *
391
392
  * 未实现的能力必须显式报错,避免用户误认为访问规则或镜像构建已经生效
392
393
  */
393
- assertPlanExecutable(config, plan) {
394
+ assertPlanExecutable(plan) {
394
395
  if (plan.action === 'replace') {
395
396
  throw new error_1.CloudBaseError(`函数 ${plan.functionName} 存在无法原地更新的变更:\n` +
396
397
  plan.replaceReasons.map(reason => `- ${reason}`).join('\n') +
397
398
  '\n部署器不会自动删除已有云函数。请先在云开发控制台确认触发器、访问配置和服务中断影响,手工删除该函数后重新部署。', { code: types_1.FUNCTION_DEPLOY_ERROR.FUNCTION_RECREATE_REQUIRED });
398
399
  }
399
- // local 个人版先行已支持;企业版 TCR 增强后置,暂未实现
400
- if (config.buildStrategy === 'local' && config.imageType === 'enterprise') {
401
- throw new error_1.CloudBaseError('local 策略的企业版 TCR 构建推送尚未实现,当前支持个人版镜像仓库(不指定 imageType 或 personal)', { code: types_1.FUNCTION_DEPLOY_ERROR.NOT_IMPLEMENTED });
402
- }
403
400
  }
404
401
  /**
405
402
  * 准备部署物
@@ -427,9 +424,7 @@ class FunctionDeployer {
427
424
  if (config.buildStrategy === 'local') {
428
425
  await this.runStep(ctx, 'preflight', async () => {
429
426
  const report = await (0, docker_preflight_1.runDockerPreflight)();
430
- report.checks
431
- .filter(item => item.status === 'warn')
432
- .forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
427
+ this.appendWarnings(warnings, report);
433
428
  this.assertPreflightReady(report);
434
429
  });
435
430
  const localConfig = config;
@@ -78,6 +78,7 @@ class StateStore {
78
78
  version: 1,
79
79
  updatedAt: new Date().toISOString(),
80
80
  gitCommit: (_a = state.gitCommit) !== null && _a !== void 0 ? _a : this.resolveGitCommit(),
81
+ envId: state.envId,
81
82
  resources: state.resources
82
83
  };
83
84
  fs_1.default.writeFileSync(filePath, JSON.stringify(full, null, 2), 'utf8');
@@ -33,10 +33,9 @@ function createImageArtifactFromConfig(config) {
33
33
  // imageType/registryId 的契约位置在配置顶层(IHttpImageRuntimeCommon),
34
34
  // 与 local/cloud 策略一致;imageConfig 内的同名字段仅作向后兼容兜底。
35
35
  // 个人版镜像(personal,如个人版 CCR)不应携带 registryId。
36
- const imageType = (_b = (_a = config.imageType) !== null && _a !== void 0 ? _a : imageConfig.imageType) !== null && _b !== void 0 ? _b : 'enterprise';
37
- const registryId = imageType === 'enterprise'
38
- ? (_c = config.registryId) !== null && _c !== void 0 ? _c : imageConfig.registryId
39
- : undefined;
36
+ const configuredRegistryId = (_a = config.registryId) !== null && _a !== void 0 ? _a : imageConfig.registryId;
37
+ const imageType = (_c = (_b = config.imageType) !== null && _b !== void 0 ? _b : imageConfig.imageType) !== null && _c !== void 0 ? _c : (configuredRegistryId ? 'enterprise' : 'personal');
38
+ const registryId = imageType === 'enterprise' ? configuredRegistryId : undefined;
40
39
  return {
41
40
  kind: 'image',
42
41
  deployMode: 'image',
@@ -60,14 +59,18 @@ function createImageArtifactFromConfig(config) {
60
59
  * 此处仅承载最终 imageUri/digest 与容器启动配置,之后复用镜像部署链。
61
60
  */
62
61
  function createImageArtifactFromBuild(config, build) {
63
- var _a;
62
+ var _a, _b, _c;
63
+ const imageType = (_a = config.imageType) !== null && _a !== void 0 ? _a : (((_b = config.registryId) !== null && _b !== void 0 ? _b : config.build.registryId) ? 'enterprise' : 'personal');
64
+ const registryId = imageType === 'enterprise'
65
+ ? (_c = config.registryId) !== null && _c !== void 0 ? _c : config.build.registryId
66
+ : undefined;
64
67
  return {
65
68
  kind: 'image',
66
69
  deployMode: 'image',
67
70
  imageUri: build.imageUri,
68
71
  imageDigest: build.imageDigest,
69
- // 个人版先行:local 构建统一按 personal 处理,企业版增强后置
70
- imageType: (_a = config.imageType) !== null && _a !== void 0 ? _a : 'personal',
72
+ imageType,
73
+ registryId,
71
74
  imagePort: types_1.IMAGE_FUNCTION_PORT,
72
75
  effectiveStrategy: 'local',
73
76
  entryPoint: config.entryPoint,
@@ -112,11 +115,18 @@ function createImageArtifactFromCloudBuild(config, build) {
112
115
  containerImageAccelerate: config.containerImageAccelerate
113
116
  };
114
117
  }
118
+ /** 将构建产出的 tag 地址与 digest 合成为 SCF 支持的不可变镜像引用 */
119
+ function withImageDigest(imageUri, imageDigest) {
120
+ if (!imageDigest || imageUri.includes('@')) {
121
+ return imageUri;
122
+ }
123
+ return `${imageUri}@${imageDigest}`;
124
+ }
115
125
  /** 将镜像部署物转换为 SCF 镜像配置 */
116
126
  function toFunctionImageConfig(artifact) {
117
127
  const imageConfig = {
118
128
  imageType: artifact.imageType,
119
- imageUri: artifact.imageUri,
129
+ imageUri: withImageDigest(artifact.imageUri, artifact.imageDigest),
120
130
  imagePort: artifact.imagePort
121
131
  };
122
132
  if (artifact.registryId !== undefined) {
@@ -165,7 +175,22 @@ function toCloudFunction(config, artifact) {
165
175
  'role',
166
176
  'codeSecret',
167
177
  'l5',
168
- 'ignore'
178
+ 'ignore',
179
+ // —— 高级配置:与 fn deploy 传统通道(configToParams 白名单)对齐,
180
+ // 字段名需与其大小写不敏感匹配规则一致,否则会被下游静默丢弃 ——
181
+ 'tags',
182
+ 'clsLogsetId',
183
+ 'clsTopicId',
184
+ 'deadLetterConfig',
185
+ 'publicNetConfig',
186
+ 'cfsConfig',
187
+ 'intranetConfig',
188
+ 'initTimeout',
189
+ 'asyncRunEnable',
190
+ 'traceEnable',
191
+ 'autoDeployClsTopicIndex',
192
+ 'autoCreateClsTopic',
193
+ 'dnsCache'
169
194
  ];
170
195
  passthroughFields.forEach(field => {
171
196
  if (source[field] !== undefined) {
@@ -161,7 +161,8 @@ if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
161
161
  TIMESTAMP=$(date +%s)
162
162
  DATE=$(date -u -d "@\${TIMESTAMP}" +"%Y-%m-%d" 2>/dev/null || date -u -r "\${TIMESTAMP}" +"%Y-%m-%d")
163
163
 
164
- PAYLOAD=$(printf '{"RegistryId":"%s","TokenType":"LongTermToken"}' "\${TCR_INSTANCE_ID}")
164
+ # 构建阶段只需要一次 docker login/push,使用 1 小时临时凭证,避免创建长期凭证。
165
+ PAYLOAD=$(printf '{"RegistryId":"%s","TokenType":"temp"}' "\${TCR_INSTANCE_ID}")
165
166
  HASHED_PAYLOAD=$(printf '%s' "\${PAYLOAD}" | openssl dgst -sha256 -hex | awk '{print $NF}')
166
167
  CANONICAL_HEADERS="content-type:application/json; charset=utf-8\\nhost:\${HOST}\\nx-tc-action:$(echo "\${ACTION}" | tr '[:upper:]' '[:lower:]')\\n"
167
168
  SIGNED_HEADERS="content-type;host;x-tc-action"
@@ -189,7 +190,123 @@ if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
189
190
  -H "X-TC-Token: \${API_TOKEN}" \\
190
191
  --data "\${PAYLOAD}")
191
192
 
192
- ERROR_CODE=$(printf '%s' "\${RESP}" | jq -r '.Response.Error.Code // empty')
193
+ # CloudApp custom Runner 已提供 awk(上面的 TC3 签名也依赖它),但不保证安装 jq。
194
+ # 这里先限定顶层 Response 对象,再解析其中的非空 JSON 字符串字段;凭证字段
195
+ # 不允许 JSON 转义,避免用 sed/grep 对认证响应做宽松匹配。
196
+ extract_json_string() {
197
+ local field="$1"
198
+ printf '%s' "\${RESP}" | JSON_FIELD="\${field}" awk '
199
+ BEGIN {
200
+ field = ENVIRON["JSON_FIELD"]
201
+ quote = sprintf("%c", 34)
202
+ backslash = sprintf("%c", 92)
203
+ json = ""
204
+ }
205
+ { json = json $0 }
206
+ END {
207
+ response_needle = quote "Response" quote "[[:space:]]*:[[:space:]]*\\{"
208
+ if (!match(json, response_needle)) exit 1
209
+ response_start = RSTART + RLENGTH - 1
210
+ response = ""
211
+ depth = 0
212
+ in_string = 0
213
+ escaped = 0
214
+ for (i = response_start; i <= length(json); i++) {
215
+ ch = substr(json, i, 1)
216
+ response = response ch
217
+ if (in_string) {
218
+ if (escaped) escaped = 0
219
+ else if (ch == backslash) escaped = 1
220
+ else if (ch == quote) in_string = 0
221
+ continue
222
+ }
223
+ if (ch == quote) in_string = 1
224
+ else if (ch == "{") depth++
225
+ else if (ch == "}") {
226
+ depth--
227
+ if (depth == 0) break
228
+ }
229
+ }
230
+ if (depth != 0) exit 1
231
+
232
+ depth = 1
233
+ for (i = 2; i < length(response); i++) {
234
+ ch = substr(response, i, 1)
235
+ if (ch == "{") depth++
236
+ else if (ch == "}") depth--
237
+ else if (ch == quote) {
238
+ key = ""
239
+ escaped = 0
240
+ for (j = i + 1; j <= length(response); j++) {
241
+ current = substr(response, j, 1)
242
+ if (escaped) {
243
+ escaped = 0
244
+ key = key current
245
+ } else if (current == backslash) {
246
+ escaped = 1
247
+ key = key current
248
+ } else if (current == quote) break
249
+ else key = key current
250
+ }
251
+ if (j > length(response)) exit 1
252
+ k = j + 1
253
+ while (substr(response, k, 1) ~ /[[:space:]]/) k++
254
+ if (depth == 1 && substr(response, k, 1) == ":" && key == field) {
255
+ k++
256
+ while (substr(response, k, 1) ~ /[[:space:]]/) k++
257
+ if (substr(response, k, 1) != quote) exit 1
258
+ value = ""
259
+ for (k = k + 1; k <= length(response); k++) {
260
+ current = substr(response, k, 1)
261
+ if (current == quote) {
262
+ if (length(value) == 0) exit 1
263
+ printf "%s", value
264
+ exit 0
265
+ }
266
+ if (current == backslash) exit 2
267
+ value = value current
268
+ }
269
+ exit 1
270
+ }
271
+ i = j
272
+ }
273
+ }
274
+ exit 1
275
+ }
276
+ '
277
+ }
278
+
279
+ extract_tcr_error_code() {
280
+ printf '%s' "\${RESP}" | awk '
281
+ BEGIN {
282
+ quote = sprintf("%c", 34)
283
+ json = ""
284
+ }
285
+ { json = json $0 }
286
+ END {
287
+ error_needle = quote "Error" quote "[[:space:]]*:[[:space:]]*\\{"
288
+ if (!match(json, error_needle)) exit 1
289
+ error_json = substr(json, RSTART + RLENGTH)
290
+ code_needle = quote "Code" quote "[[:space:]]*:[[:space:]]*" quote
291
+ if (!match(error_json, code_needle)) exit 1
292
+ rest = substr(error_json, RSTART + RLENGTH)
293
+ value = ""
294
+ for (i = 1; i <= length(rest); i++) {
295
+ ch = substr(rest, i, 1)
296
+ if (ch == quote) {
297
+ if (length(value) == 0) exit 1
298
+ printf "%s", value
299
+ exit 0
300
+ }
301
+ if (ch == backslash) exit 1
302
+ value = value ch
303
+ }
304
+ exit 1
305
+ }
306
+ '
307
+ }
308
+
309
+ ERROR_CODE=$(extract_tcr_error_code || true)
193
310
  if [ -n "\${ERROR_CODE}" ]; then
194
311
  case "\${ERROR_CODE}" in
195
312
  AuthFailure*|UnauthorizedOperation*|FailedOperation.NoPermission*)
@@ -207,8 +324,16 @@ if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
207
324
  esac
208
325
  fi
209
326
 
210
- TOKEN_USER=$(printf '%s' "\${RESP}" | jq -er '.Response.Username')
211
- TOKEN_PASS=$(printf '%s' "\${RESP}" | jq -er '.Response.Token')
327
+ if ! TOKEN_USER=$(extract_json_string "Username"); then
328
+ ERROR_CODE=$(extract_tcr_error_code || true)
329
+ echo "CreateInstanceToken 未返回有效 Username\${ERROR_CODE:+,错误码:\${ERROR_CODE}}" >&2
330
+ exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
331
+ fi
332
+ if ! TOKEN_PASS=$(extract_json_string "Token"); then
333
+ ERROR_CODE=$(extract_tcr_error_code || true)
334
+ echo "CreateInstanceToken 未返回有效 Token\${ERROR_CODE:+,错误码:\${ERROR_CODE}}" >&2
335
+ exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
336
+ fi
212
337
 
213
338
  printf '%s' "\${TOKEN_PASS}" | docker login -u "\${TOKEN_USER}" --password-stdin "\${TCR_REGISTRY}"
214
339
  else
@@ -1,31 +1,36 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.IMAGE_PULL_GRANT_URL = exports.REQUIRED_IMAGE_PULL_POLICY = exports.SCF_PULL_IMAGE_ROLE = void 0;
3
+ exports.TCR_PUSH_GRANT_URL = exports.IMAGE_PULL_GRANT_URL = exports.REQUIRED_TCR_PUSH_POLICY = exports.TCB_PUSH_ENTERPRISE_IMAGE_ROLE = exports.REQUIRED_IMAGE_PULL_POLICY = exports.SCF_PULL_IMAGE_ROLE = void 0;
4
4
  exports.isCamPermissionError = isCamPermissionError;
5
5
  exports.runImagePullAuthPreflight = runImagePullAuthPreflight;
6
6
  const types_1 = require("../types");
7
7
  /**
8
- * 镜像拉取授权 Preflight(涉及 CAM,scope: cloud)
8
+ * 镜像仓库授权 Preflight(涉及 CAM,scope: cloud)
9
9
  *
10
- * SCF 拉取镜像由服务角色 SCF_QcsRole 代理,个人版镜像要求该角色绑定
11
- * QcloudAccessForSCFRoleInPullImage 策略。本模块在 image 策略部署前:
12
- * 1. 探测该角色与策略是否就绪;
13
- * 2. 缺失且允许时,自动绑定「唯一白名单策略」补齐(绝不放大到其它策略);
14
- * 3. 当前身份无 CAM 权限时不硬失败,降级为结构化指引 + 一键授权链接兜底。
10
+ * 镜像部署涉及两个职责不同的服务角色:
11
+ * - SCF_QcsRole:SCF 从个人版或企业版 TCR 拉取镜像;
12
+ * - TCB_QcsRole:CloudApp 云端构建向企业版 TCR 推送镜像。
15
13
  *
16
- * 安全边界(对应 security_rules 的 AuthZ / 最小权限):
17
- * - 自动授权只允许绑定 REQUIRED_IMAGE_PULL_POLICY 这一条部署必需的预置策略;
18
- * - 不代用户创建角色、不绑定任何白名单外策略;
19
- * - 探测/授权失败一律走降级,不阻断到无法给出可操作指引。
14
+ * 本模块在部署前探测所需角色与策略;缺失且允许时,只自动绑定各场景明确列入
15
+ * 白名单的预置策略;当前身份无 CAM 权限时,则返回结构化指引和一键授权链接。
16
+ * 模块不会创建角色,也不会绑定白名单之外的策略。
20
17
  */
21
18
  /** SCF 拉取镜像使用的服务角色 */
22
19
  exports.SCF_PULL_IMAGE_ROLE = 'SCF_QcsRole';
23
- /** 拉取个人版镜像所需的唯一预置策略(自动授权白名单,仅此一条) */
20
+ /** SCF 拉取个人版或企业版 TCR 镜像所需的预置策略 */
24
21
  exports.REQUIRED_IMAGE_PULL_POLICY = 'QcloudAccessForSCFRoleInPullImage';
25
- /** 官方一键授权链接,作为无 CAM 权限时的兜底指引 */
22
+ /** CloudApp 云端构建向企业版 TCR 推送镜像使用的服务角色 */
23
+ exports.TCB_PUSH_ENTERPRISE_IMAGE_ROLE = 'TCB_QcsRole';
24
+ /** CloudApp 云端构建向企业版 TCR 推送镜像所需的预置策略 */
25
+ exports.REQUIRED_TCR_PUSH_POLICY = 'QcloudAccessForTCBRoleInTCR';
26
+ /** SCF 拉取镜像的一键授权链接 */
26
27
  exports.IMAGE_PULL_GRANT_URL = 'https://console.cloud.tencent.com/cam/role/grant' +
27
28
  `?roleName=${exports.SCF_PULL_IMAGE_ROLE}&policyName=${exports.REQUIRED_IMAGE_PULL_POLICY}` +
28
29
  '&principal=eyJzZXJ2aWNlIjoic2NmLnFjbG91ZC5jb20ifQ%3D%3D';
30
+ /** 企业版 TCR 云端构建推送的一键授权链接 */
31
+ exports.TCR_PUSH_GRANT_URL = 'https://console.cloud.tencent.com/cam/role/grant' +
32
+ `?roleName=${exports.TCB_PUSH_ENTERPRISE_IMAGE_ROLE}&policyName=${exports.REQUIRED_TCR_PUSH_POLICY}` +
33
+ '&principal=eyJzZXJ2aWNlIjoidGNiLmNsb3VkLnRlbmNlbnQuY29tIn0%3D';
29
34
  /** 判断错误是否为 CAM 权限不足 */
30
35
  function isCamPermissionError(error) {
31
36
  const code = typeof (error === null || error === void 0 ? void 0 : error.code) === 'string' ? error.code : '';
@@ -60,98 +65,109 @@ function buildReport(checks) {
60
65
  checks
61
66
  };
62
67
  }
63
- /** 是否需要执行镜像拉取授权检查 */
68
+ /** image/local/cloud 最终都需要 SCF 从镜像仓库拉取镜像 */
64
69
  function needsImagePullAuth(config) {
65
- var _a;
66
- if (config.type !== 'HTTP') {
67
- return false;
68
- }
69
- // image:已有个人版镜像;local:本地构建推送的也是个人版镜像,SCF 拉取同样需要该授权
70
- if (config.buildStrategy === 'image') {
71
- // 联合类型已按 buildStrategy 收窄为 INormalizedHttpImageConfig,可直接访问顶层与 imageConfig 字段
72
- const imageType = ((_a = config.imageConfig) === null || _a === void 0 ? void 0 : _a.imageType) || config.imageType;
73
- return imageType === 'personal';
70
+ return (config.type === 'HTTP' &&
71
+ (config.buildStrategy === 'image' ||
72
+ config.buildStrategy === 'local' ||
73
+ config.buildStrategy === 'cloud'));
74
+ }
75
+ /** 只有企业版 cloud 策略需要 CloudApp 向企业版 TCR 推送镜像 */
76
+ function needsEnterpriseTcrPushAuth(config) {
77
+ return (config.type === 'HTTP' &&
78
+ config.buildStrategy === 'cloud' &&
79
+ config.imageType === 'enterprise');
80
+ }
81
+ function resolveRequirements(config) {
82
+ const requirements = [];
83
+ if (needsImagePullAuth(config)) {
84
+ requirements.push({
85
+ roleName: exports.SCF_PULL_IMAGE_ROLE,
86
+ policyName: exports.REQUIRED_IMAGE_PULL_POLICY,
87
+ purpose: 'SCF 拉取 TCR 镜像',
88
+ grantUrl: exports.IMAGE_PULL_GRANT_URL,
89
+ roleMissingCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_ROLE_MISSING,
90
+ policyMissingCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_POLICY_MISSING,
91
+ grantedCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_AUTH_GRANTED,
92
+ unverifiableCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_AUTH_UNVERIFIABLE
93
+ });
74
94
  }
75
- if (config.buildStrategy === 'local' || config.buildStrategy === 'cloud') {
76
- // local/cloud 个人版先行:构建推送的默认是个人版镜像,SCF 拉取同样需要该授权;
77
- // 未显式指定 imageType 时视为 personal;企业版另行处理
78
- const imageType = config.imageType;
79
- return imageType === undefined || imageType === 'personal';
95
+ if (needsEnterpriseTcrPushAuth(config)) {
96
+ requirements.push({
97
+ roleName: exports.TCB_PUSH_ENTERPRISE_IMAGE_ROLE,
98
+ policyName: exports.REQUIRED_TCR_PUSH_POLICY,
99
+ purpose: 'CloudApp 云端构建推送企业版 TCR 镜像',
100
+ grantUrl: exports.TCR_PUSH_GRANT_URL,
101
+ roleMissingCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_TCR_PUSH_ROLE_MISSING,
102
+ policyMissingCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_TCR_PUSH_POLICY_MISSING,
103
+ grantedCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_TCR_PUSH_AUTH_GRANTED,
104
+ unverifiableCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_TCR_PUSH_AUTH_UNVERIFIABLE
105
+ });
80
106
  }
81
- return false;
107
+ return requirements;
82
108
  }
83
- /** CAM 读权限时的降级检查项:无法自查,转手动指引 */
84
- function unverifiableCheck() {
85
- return warn(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_AUTH_UNVERIFIABLE, `当前身份无 CAM 权限,无法自动校验或补齐 ${exports.SCF_PULL_IMAGE_ROLE} 的拉镜像授权`, `若个人版镜像拉取失败,请用具备 CAM 权限的账号访问一键授权链接完成授权:${exports.IMAGE_PULL_GRANT_URL}`);
109
+ function unverifiableCheck(requirement) {
110
+ return warn(requirement.unverifiableCode, `当前身份无 CAM 权限,无法自动校验或补齐 ${requirement.roleName} 的${requirement.purpose}授权`, `若部署失败,请用具备 CAM 权限的账号访问一键授权链接完成授权:${requirement.grantUrl}`);
86
111
  }
87
- /**
88
- * 执行镜像拉取授权 Preflight
89
- *
90
- * @param service CAM 能力适配(无则视为无法校验,直接降级为提示)
91
- * @param config 规范化部署配置
92
- * @param autoGrant 是否允许自动补齐授权(默认 true)
93
- * @returns scope 为 cloud 的检查报告,聚合全部结论
94
- */
95
- async function runImagePullAuthPreflight(service, config, autoGrant = true) {
96
- if (!needsImagePullAuth(config)) {
97
- return buildReport([]);
98
- }
99
- // 无 CAM 适配能力:不阻断,仅提示手动授权入口
112
+ async function checkRequirement(service, requirement, autoGrant) {
100
113
  if (!service) {
101
- return buildReport([unverifiableCheck()]);
114
+ return unverifiableCheck(requirement);
102
115
  }
103
- const checks = [];
104
116
  let roleDetail;
105
117
  try {
106
- roleDetail = await service.getRoleWithPolicies(exports.SCF_PULL_IMAGE_ROLE);
118
+ roleDetail = await service.getRoleWithPolicies(requirement.roleName);
107
119
  }
108
120
  catch (error) {
109
121
  if (isCamPermissionError(error)) {
110
- // 无 CAM 读权限,无法自查,降级为提示(warn,不阻断部署尝试)
111
- return buildReport([unverifiableCheck()]);
122
+ return unverifiableCheck(requirement);
112
123
  }
113
124
  if (isRoleNotFoundError(error)) {
114
- // 角色不存在属于真实前置缺失,不自动创建角色,给出可操作指引
115
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_ROLE_MISSING, `缺少服务角色 ${exports.SCF_PULL_IMAGE_ROLE}`, {
116
- remediation: `请访问一键授权链接创建角色并授予拉镜像策略:${exports.IMAGE_PULL_GRANT_URL}`
117
- }));
118
- return buildReport(checks);
125
+ return fail(requirement.roleMissingCode, `缺少服务角色 ${requirement.roleName}`, {
126
+ remediation: `请访问一键授权链接创建角色并授予 ${requirement.policyName}:${requirement.grantUrl}`
127
+ });
119
128
  }
120
- // 其它未知错误同样降级为提示,避免因 CAM 波动阻断部署
121
- return buildReport([unverifiableCheck()]);
129
+ return unverifiableCheck(requirement);
122
130
  }
123
131
  const attached = roleDetail.attachedPolicyNames || [];
124
- if (attached.includes(exports.REQUIRED_IMAGE_PULL_POLICY)) {
125
- checks.push(pass(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_POLICY_MISSING, `${exports.SCF_PULL_IMAGE_ROLE} 已具备拉镜像策略 ${exports.REQUIRED_IMAGE_PULL_POLICY}`));
126
- return buildReport(checks);
132
+ if (attached.includes(requirement.policyName)) {
133
+ return pass(requirement.policyMissingCode, `${requirement.roleName} 已具备${requirement.purpose}策略 ${requirement.policyName}`);
127
134
  }
128
- // 未绑定拉镜像策略
129
135
  if (!autoGrant) {
130
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_POLICY_MISSING, `${exports.SCF_PULL_IMAGE_ROLE} 缺少拉镜像策略 ${exports.REQUIRED_IMAGE_PULL_POLICY}`, {
131
- remediation: `已关闭自动授权(autoGrant=false)。请访问一键授权链接手动授予:${exports.IMAGE_PULL_GRANT_URL}`
132
- }));
133
- return buildReport(checks);
136
+ return fail(requirement.policyMissingCode, `${requirement.roleName} 缺少${requirement.purpose}策略 ${requirement.policyName}`, {
137
+ remediation: `已关闭自动授权(autoGrant=false)。请访问一键授权链接手动授予:${requirement.grantUrl}`
138
+ });
134
139
  }
135
- // 自动授权:只绑定唯一白名单策略
136
140
  try {
137
- await service.attachPolicyByName(exports.SCF_PULL_IMAGE_ROLE, exports.REQUIRED_IMAGE_PULL_POLICY);
138
- checks.push(pass(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_AUTH_GRANTED, `已自动为 ${exports.SCF_PULL_IMAGE_ROLE} 绑定拉镜像策略 ${exports.REQUIRED_IMAGE_PULL_POLICY}`));
139
- return buildReport(checks);
141
+ await service.attachPolicyByName(requirement.roleName, requirement.policyName);
142
+ return pass(requirement.grantedCode, `已自动为 ${requirement.roleName} 绑定${requirement.purpose}策略 ${requirement.policyName}`);
140
143
  }
141
144
  catch (error) {
142
145
  if (isCamPermissionError(error)) {
143
- // CAM 写权限:不硬失败,降级为可操作指引 + 兜底链接
144
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_CAM_PERMISSION_DENIED, `当前身份无 CAM 授权权限,无法自动为 ${exports.SCF_PULL_IMAGE_ROLE} 补齐拉镜像策略`, {
145
- remediation: `请用具备 CAM 权限的账号(如主账号)访问一键授权链接完成授权:${exports.IMAGE_PULL_GRANT_URL}`,
146
+ return fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_CAM_PERMISSION_DENIED, `当前身份无 CAM 授权权限,无法自动为 ${requirement.roleName} 补齐${requirement.purpose}策略`, {
147
+ remediation: `请用具备 CAM 权限的账号(如主账号)访问一键授权链接完成授权:${requirement.grantUrl}`,
146
148
  detail: error === null || error === void 0 ? void 0 : error.message
147
- }));
148
- return buildReport(checks);
149
+ });
149
150
  }
150
- // 其它授权错误同样给出结构化失败与指引
151
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_POLICY_MISSING, `自动绑定拉镜像策略 ${exports.REQUIRED_IMAGE_PULL_POLICY} 失败`, {
152
- remediation: `请访问一键授权链接手动授予:${exports.IMAGE_PULL_GRANT_URL}`,
151
+ return fail(requirement.policyMissingCode, `自动绑定策略 ${requirement.policyName} 失败`, {
152
+ remediation: `请访问一键授权链接手动授予:${requirement.grantUrl}`,
153
153
  detail: error === null || error === void 0 ? void 0 : error.message
154
- }));
155
- return buildReport(checks);
154
+ });
155
+ }
156
+ }
157
+ /**
158
+ * 执行镜像仓库授权 Preflight。
159
+ *
160
+ * 企业版 cloud 策略会同时检查 SCF 拉取权限和 TCB 云端构建推送权限;
161
+ * 其他镜像策略只检查与自身相关的授权,不会额外要求 TCB 推送权限。
162
+ */
163
+ async function runImagePullAuthPreflight(service, config, autoGrant = true) {
164
+ const requirements = resolveRequirements(config);
165
+ if (requirements.length === 0) {
166
+ return buildReport([]);
167
+ }
168
+ const checks = [];
169
+ for (const requirement of requirements) {
170
+ checks.push(await checkRequirement(service, requirement, autoGrant));
156
171
  }
172
+ return buildReport(checks);
157
173
  }