@cloudbase/manager-node 5.8.0-beta.0 → 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.
@@ -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;
@@ -112,11 +112,18 @@ function createImageArtifactFromCloudBuild(config, build) {
112
112
  containerImageAccelerate: config.containerImageAccelerate
113
113
  };
114
114
  }
115
+ /** 将构建产出的 tag 地址与 digest 合成为 SCF 支持的不可变镜像引用 */
116
+ function withImageDigest(imageUri, imageDigest) {
117
+ if (!imageDigest || imageUri.includes('@')) {
118
+ return imageUri;
119
+ }
120
+ return `${imageUri}@${imageDigest}`;
121
+ }
115
122
  /** 将镜像部署物转换为 SCF 镜像配置 */
116
123
  function toFunctionImageConfig(artifact) {
117
124
  const imageConfig = {
118
125
  imageType: artifact.imageType,
119
- imageUri: artifact.imageUri,
126
+ imageUri: withImageDigest(artifact.imageUri, artifact.imageDigest),
120
127
  imagePort: artifact.imagePort
121
128
  };
122
129
  if (artifact.registryId !== 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
  }
@@ -47,7 +47,7 @@ function resolveContext(config) {
47
47
  const cwd = config.build.cwd || config.functionPath || process.cwd();
48
48
  return path_1.default.resolve(cwd);
49
49
  }
50
- /** 解析目标镜像完整引用 imageUri(个人版:repository 需为含域名与命名空间的完整仓库地址) */
50
+ /** 解析目标镜像完整引用 imageUrirepository 需为含域名与命名空间的完整仓库地址,个人版/企业版通用) */
51
51
  function resolveImageUri(config) {
52
52
  const repository = config.build.repository;
53
53
  if (typeof repository !== 'string' || !repository.trim()) {
@@ -92,11 +92,15 @@ exports.FUNCTION_DEPLOY_ERROR = {
92
92
  PREFLIGHT_IMAGE_REMOTE_UNVERIFIABLE: 'PREFLIGHT_IMAGE_REMOTE_UNVERIFIABLE',
93
93
  PREFLIGHT_IMAGE_DIGEST_UNRESOLVED: 'PREFLIGHT_IMAGE_DIGEST_UNRESOLVED',
94
94
  PREFLIGHT_IMAGE_REGION_MISMATCH: 'PREFLIGHT_IMAGE_REGION_MISMATCH',
95
- // 镜像拉取授权 Preflight(涉及 CAM,scope: cloud)
95
+ // 镜像仓库授权 Preflight(涉及 CAM,scope: cloud)
96
96
  PREFLIGHT_IMAGE_PULL_ROLE_MISSING: 'PREFLIGHT_IMAGE_PULL_ROLE_MISSING',
97
97
  PREFLIGHT_IMAGE_PULL_POLICY_MISSING: 'PREFLIGHT_IMAGE_PULL_POLICY_MISSING',
98
98
  PREFLIGHT_IMAGE_PULL_AUTH_GRANTED: 'PREFLIGHT_IMAGE_PULL_AUTH_GRANTED',
99
99
  PREFLIGHT_IMAGE_PULL_AUTH_UNVERIFIABLE: 'PREFLIGHT_IMAGE_PULL_AUTH_UNVERIFIABLE',
100
+ PREFLIGHT_TCR_PUSH_ROLE_MISSING: 'PREFLIGHT_TCR_PUSH_ROLE_MISSING',
101
+ PREFLIGHT_TCR_PUSH_POLICY_MISSING: 'PREFLIGHT_TCR_PUSH_POLICY_MISSING',
102
+ PREFLIGHT_TCR_PUSH_AUTH_GRANTED: 'PREFLIGHT_TCR_PUSH_AUTH_GRANTED',
103
+ PREFLIGHT_TCR_PUSH_AUTH_UNVERIFIABLE: 'PREFLIGHT_TCR_PUSH_AUTH_UNVERIFIABLE',
100
104
  PREFLIGHT_CAM_PERMISSION_DENIED: 'PREFLIGHT_CAM_PERMISSION_DENIED',
101
105
  FUNCTION_RECREATE_REQUIRED: 'FUNCTION_RECREATE_REQUIRED',
102
106
  NOT_IMPLEMENTED: 'NOT_IMPLEMENTED'
@@ -9,7 +9,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
9
9
  return (mod && mod.__esModule) ? mod : { "default": mod };
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.FunctionService = void 0;
12
+ exports.FunctionService = exports.ENTERPRISE_IMAGE_REGION_GUIDANCE = void 0;
13
13
  const fs_1 = __importDefault(require("fs"));
14
14
  const path_1 = __importDefault(require("path"));
15
15
  const lodash_1 = __importDefault(require("lodash"));
@@ -113,6 +113,24 @@ function getFieldIgnoreCase(obj, fieldName) {
113
113
  }
114
114
  return undefined;
115
115
  }
116
+ /** 企业版 TCR 镜像对 SCF 不可见时的排查指引 */
117
+ exports.ENTERPRISE_IMAGE_REGION_GUIDANCE = '请确认 TCR 企业版实例已在当前函数地域提供可访问的实例或复制实例,并检查 RegistryId、镜像地址及拉取权限。';
118
+ /**
119
+ * 为企业版镜像的典型不可见错误补充排查指引,同时保留原始错误和 requestId。
120
+ * 仅匹配 SCF 已进入 enterprise 镜像查询阶段的错误,避免给普通网络或鉴权错误误导提示。
121
+ */
122
+ function formatImageDeploymentErrorMessage(error, imageConfig) {
123
+ const message = typeof (error === null || error === void 0 ? void 0 : error.message) === 'string' ? error.message : String(error);
124
+ const code = typeof (error === null || error === void 0 ? void 0 : error.code) === 'string' ? error.code : '';
125
+ const haystack = `${code} ${message}`.toLowerCase();
126
+ const isEnterprise = (imageConfig === null || imageConfig === void 0 ? void 0 : imageConfig.imageType) === 'enterprise';
127
+ const isEnterpriseImageNotFound = haystack.includes('resourcenotfound.image') ||
128
+ haystack.includes('describe images enterprise resource not found');
129
+ if (!isEnterprise || !isEnterpriseImageNotFound || message.includes(exports.ENTERPRISE_IMAGE_REGION_GUIDANCE)) {
130
+ return message;
131
+ }
132
+ return `${message}\n提示:${exports.ENTERPRISE_IMAGE_REGION_GUIDANCE}`;
133
+ }
116
134
  // 解析函数配置,换成请求参数(用于 CreateFunction)
117
135
  function configToParams(options) {
118
136
  var _a, _b, _c;
@@ -442,7 +460,15 @@ class FunctionService {
442
460
  }
443
461
  // 不强制覆盖,抛出错误
444
462
  if (e.message && !force) {
445
- throw new error_1.CloudBaseError(`[${funcName}] 部署失败:\n${e.message}`, {
463
+ throw new error_1.CloudBaseError(`[${funcName}] 部署失败:\n${formatImageDeploymentErrorMessage(e, isImageDeploy ? func.imageConfig : undefined)}`, {
464
+ code: e.code,
465
+ requestId: e.requestId
466
+ });
467
+ }
468
+ // force=true 时既有逻辑会直接透传原始错误;企业版镜像地域问题仍需补充可操作指引
469
+ const enhancedMessage = formatImageDeploymentErrorMessage(e, isImageDeploy ? func.imageConfig : undefined);
470
+ if (enhancedMessage !== e.message) {
471
+ throw new error_1.CloudBaseError(`[${funcName}] 部署失败:\n${enhancedMessage}`, {
446
472
  code: e.code,
447
473
  requestId: e.requestId
448
474
  });
@@ -879,7 +905,7 @@ class FunctionService {
879
905
  return await this.functionCloudService.request('UpdateFunctionCode', params);
880
906
  }
881
907
  catch (e) {
882
- throw new error_1.CloudBaseError(`[${funcName}] 函数代码更新失败:${e.message}`, {
908
+ throw new error_1.CloudBaseError(`[${funcName}] 函数代码更新失败:${formatImageDeploymentErrorMessage(e, func.imageConfig)}`, {
883
909
  code: e.code,
884
910
  requestId: e.requestId
885
911
  });
@@ -91,7 +91,7 @@ class ProjectValidator {
91
91
  }
92
92
  }
93
93
  validateSchema(config, errors) {
94
- const result = (0, toolbox_1.validateCloudBaseConfigBySchema)(config, { version: '2.1' });
94
+ const result = (0, toolbox_1.validateCloudBaseConfigBySchema)(config);
95
95
  result.errors.forEach((error) => {
96
96
  errors.push({
97
97
  level: 'error',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/manager-node",
3
- "version": "5.8.0-beta.0",
3
+ "version": "5.8.0-beta.1",
4
4
  "description": "The node manage service api for cloudbase.",
5
5
  "main": "lib/index.js",
6
6
  "scripts": {
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@cloudbase/signature-nodejs": "^1.0.0",
45
- "@cloudbase/toolbox": "^0.8.3-beta.0",
45
+ "@cloudbase/toolbox": "^0.8.3-beta.1",
46
46
  "@cloudbase/database": "^0.6.2",
47
47
  "archiver": "^3.1.1",
48
48
  "cos-nodejs-sdk-v5": "^2.14.0",
@@ -113,6 +113,8 @@ export declare class FunctionDeployer {
113
113
  private getCurrentFunction;
114
114
  /** 判断是否为函数不存在错误,其他错误必须继续抛出 */
115
115
  private isFunctionNotFoundError;
116
+ /** 将 warning 连同可执行修复建议汇入部署结果,避免授权链接在编排层丢失 */
117
+ private appendWarnings;
116
118
  /**
117
119
  * 校验 Preflight 报告,存在 fail 项时聚合抛错
118
120
  */
@@ -92,7 +92,7 @@ export declare const TCR_PUSH_SCRIPT_ZIP_PATH = "scripts/tcr-login-and-push.sh";
92
92
  * 脚本内容为SDK 固定模板,不拼接任何用户输入(镜像地址等均由已校验的
93
93
  * 环境变量在容器内组装),无命令注入面。
94
94
  */
95
- export declare const TCR_LOGIN_PUSH_SCRIPT = "#!/usr/bin/env bash\n# scripts/tcr-login-and-push.sh\n# \u7531 CloudBase SDK \u81EA\u52A8\u6CE8\u5165\uFF1A\u4F01\u4E1A\u7248\u4F7F\u7528 STS \u6362\u53D6\u4E34\u65F6 token\uFF0C\u4E2A\u4EBA\u7248\u4F7F\u7528 CloudApp Secret\nset -euo pipefail\n\n: \"${TCR_REGISTRY:?TCR_REGISTRY \u672A\u8BBE\u7F6E}\"\n: \"${TCR_NAMESPACE:?TCR_NAMESPACE \u672A\u8BBE\u7F6E}\"\n: \"${CLOUDBASE_SERVICE_NAME:?\u5E73\u53F0\u53D8\u91CF\u7F3A\u5931}\"\n: \"${CLOUDBASE_VERSION_NAME:?\u5E73\u53F0\u53D8\u91CF\u7F3A\u5931}\"\n\nREGION=\"${TCR_REGION:-ap-shanghai}\"\nIMAGE=\"${TCR_REGISTRY}/${TCR_NAMESPACE}/${CLOUDBASE_SERVICE_NAME}:${CLOUDBASE_VERSION_NAME}\"\nIS_PERSONAL_REGISTRY=0\n\nif [ -n \"${TCR_INSTANCE_ID:-}\" ]; then\n : \"${API_SECRET_ID:?STS \u51ED\u8BC1\u7F3A\u5931}\"\n : \"${API_SECRET_KEY:?STS \u51ED\u8BC1\u7F3A\u5931}\"\n : \"${API_TOKEN:?STS \u51ED\u8BC1\u7F3A\u5931}\"\n\n # ---- \u4F01\u4E1A\u7248 TCR\uFF1ACreateInstanceToken \u6362\u767B\u5F55\u6001 ----\n HOST=\"tcr.tencentcloudapi.com\"\n SERVICE=\"tcr\"\n VERSION=\"2019-09-24\"\n ACTION=\"CreateInstanceToken\"\n ALGORITHM=\"TC3-HMAC-SHA256\"\n TIMESTAMP=$(date +%s)\n DATE=$(date -u -d \"@${TIMESTAMP}\" +\"%Y-%m-%d\" 2>/dev/null || date -u -r \"${TIMESTAMP}\" +\"%Y-%m-%d\")\n\n PAYLOAD=$(printf '{\"RegistryId\":\"%s\",\"TokenType\":\"LongTermToken\"}' \"${TCR_INSTANCE_ID}\")\n HASHED_PAYLOAD=$(printf '%s' \"${PAYLOAD}\" | openssl dgst -sha256 -hex | awk '{print $NF}')\n CANONICAL_HEADERS=\"content-type:application/json; charset=utf-8\\nhost:${HOST}\\nx-tc-action:$(echo \"${ACTION}\" | tr '[:upper:]' '[:lower:]')\\n\"\n SIGNED_HEADERS=\"content-type;host;x-tc-action\"\n CANONICAL_REQUEST=\"POST\\n/\\n\\n${CANONICAL_HEADERS}\\n${SIGNED_HEADERS}\\n${HASHED_PAYLOAD}\"\n\n CREDENTIAL_SCOPE=\"${DATE}/${SERVICE}/tc3_request\"\n HASHED_CR=$(printf \"${CANONICAL_REQUEST}\" | openssl dgst -sha256 -hex | awk '{print $NF}')\n STRING_TO_SIGN=\"${ALGORITHM}\\n${TIMESTAMP}\\n${CREDENTIAL_SCOPE}\\n${HASHED_CR}\"\n\n secret_date=$(printf '%s' \"${DATE}\" | openssl dgst -sha256 -hmac \"TC3${API_SECRET_KEY}\" -hex | awk '{print $NF}')\n secret_svc=$(printf '%s' \"${SERVICE}\" | openssl dgst -sha256 -mac HMAC -macopt hexkey:\"${secret_date}\" -hex | awk '{print $NF}')\n secret_sign=$(printf '%s' \"tc3_request\" | openssl dgst -sha256 -mac HMAC -macopt hexkey:\"${secret_svc}\" -hex | awk '{print $NF}')\n SIGNATURE=$(printf \"${STRING_TO_SIGN}\" | openssl dgst -sha256 -mac HMAC -macopt hexkey:\"${secret_sign}\" -hex | awk '{print $NF}')\n\n AUTHZ=\"${ALGORITHM} Credential=${API_SECRET_ID}/${CREDENTIAL_SCOPE}, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}\"\n\n RESP=$(curl -sS -X POST \"https://${HOST}\" \\\n -H \"Authorization: ${AUTHZ}\" \\\n -H \"Content-Type: application/json; charset=utf-8\" \\\n -H \"Host: ${HOST}\" \\\n -H \"X-TC-Action: ${ACTION}\" \\\n -H \"X-TC-Timestamp: ${TIMESTAMP}\" \\\n -H \"X-TC-Version: ${VERSION}\" \\\n -H \"X-TC-Region: ${REGION}\" \\\n -H \"X-TC-Token: ${API_TOKEN}\" \\\n --data \"${PAYLOAD}\")\n\n ERROR_CODE=$(printf '%s' \"${RESP}\" | jq -r '.Response.Error.Code // empty')\n if [ -n \"${ERROR_CODE}\" ]; then\n case \"${ERROR_CODE}\" in\n AuthFailure*|UnauthorizedOperation*|FailedOperation.NoPermission*)\n echo \"\u4F01\u4E1A\u7248 TCR \u9274\u6743\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u5F53\u524D\u8EAB\u4EFD\u6709\u6743\u8BBF\u95EE\u8BE5\u5B9E\u4F8B\u5E76\u8C03\u7528 CreateInstanceToken\" >&2\n exit 42\n ;;\n ResourceNotFound*|InvalidParameterValue.RegistryId*)\n echo \"\u4F01\u4E1A\u7248 TCR \u5B9E\u4F8B\u4E0D\u5B58\u5728\u6216\u5B9E\u4F8B ID \u65E0\u6548\" >&2\n exit 43\n ;;\n *)\n echo \"\u4F01\u4E1A\u7248 TCR \u83B7\u53D6\u767B\u5F55\u51ED\u8BC1\u5931\u8D25\uFF1A${ERROR_CODE}\" >&2\n exit 1\n ;;\n esac\n fi\n\n TOKEN_USER=$(printf '%s' \"${RESP}\" | jq -er '.Response.Username')\n TOKEN_PASS=$(printf '%s' \"${RESP}\" | jq -er '.Response.Token')\n\n printf '%s' \"${TOKEN_PASS}\" | docker login -u \"${TOKEN_USER}\" --password-stdin \"${TCR_REGISTRY}\"\nelse\n # ---- \u4E2A\u4EBA\u7248 CCR\uFF1A\u8D26\u53F7 ID + \u56FA\u5B9A\u5BC6\u7801\uFF08Base64 \u5C01\u88C5\u540E\u7531 CloudApp Secrets \u6CE8\u5165\uFF09 ----\n IS_PERSONAL_REGISTRY=1\n : \"${TCR_USERNAME:?TCR_USERNAME \u672A\u8BBE\u7F6E}\"\n : \"${SECRET_TCR_PASSWORD_B64:?TCR_PASSWORD \u672A\u8BBE\u7F6E}\"\n printf '%s' \"${SECRET_TCR_PASSWORD_B64}\" | base64 --decode | docker login -u \"${TCR_USERNAME}\" --password-stdin \"${TCR_REGISTRY}\"\nfi\n\nPUSH_LOG=$(mktemp)\ntrap 'rm -f \"${PUSH_LOG}\"' EXIT\nset +e\ndocker push \"${IMAGE}\" 2>&1 | tee \"${PUSH_LOG}\"\nPUSH_STATUS=${PIPESTATUS[0]}\nset -e\n\nif [ \"${PUSH_STATUS}\" -ne 0 ]; then\n if [ \"${IS_PERSONAL_REGISTRY}\" -eq 1 ] && {\n grep -Fq 'LimitExceeded.ErrRepoMaxLimit' \"${PUSH_LOG}\" ||\n grep -Fq '\u7528\u6237\u4ED3\u5E93\u5DF2\u7ECF\u8FBE\u5230\u6700\u5927\u914D\u989D' \"${PUSH_LOG}\"\n }; then\n echo \"\u4E2A\u4EBA\u7248 TCR \u955C\u50CF\u4ED3\u5E93\u6570\u91CF\u5DF2\u8FBE\u5230\u5F53\u524D\u5730\u57DF\u914D\u989D\uFF0C\u65E0\u6CD5\u81EA\u52A8\u521B\u5EFA\u76EE\u6807\u4ED3\u5E93\" >&2\n exit 44\n fi\n exit 1\nfi\n\necho \"::output::tag=${CLOUDBASE_VERSION_NAME}\"\n";
95
+ export declare const TCR_LOGIN_PUSH_SCRIPT = "#!/usr/bin/env bash\n# scripts/tcr-login-and-push.sh\n# \u7531 CloudBase SDK \u81EA\u52A8\u6CE8\u5165\uFF1A\u4F01\u4E1A\u7248\u4F7F\u7528 STS \u6362\u53D6\u4E34\u65F6 token\uFF0C\u4E2A\u4EBA\u7248\u4F7F\u7528 CloudApp Secret\nset -euo pipefail\n\n: \"${TCR_REGISTRY:?TCR_REGISTRY \u672A\u8BBE\u7F6E}\"\n: \"${TCR_NAMESPACE:?TCR_NAMESPACE \u672A\u8BBE\u7F6E}\"\n: \"${CLOUDBASE_SERVICE_NAME:?\u5E73\u53F0\u53D8\u91CF\u7F3A\u5931}\"\n: \"${CLOUDBASE_VERSION_NAME:?\u5E73\u53F0\u53D8\u91CF\u7F3A\u5931}\"\n\nREGION=\"${TCR_REGION:-ap-shanghai}\"\nIMAGE=\"${TCR_REGISTRY}/${TCR_NAMESPACE}/${CLOUDBASE_SERVICE_NAME}:${CLOUDBASE_VERSION_NAME}\"\nIS_PERSONAL_REGISTRY=0\n\nif [ -n \"${TCR_INSTANCE_ID:-}\" ]; then\n : \"${API_SECRET_ID:?STS \u51ED\u8BC1\u7F3A\u5931}\"\n : \"${API_SECRET_KEY:?STS \u51ED\u8BC1\u7F3A\u5931}\"\n : \"${API_TOKEN:?STS \u51ED\u8BC1\u7F3A\u5931}\"\n\n # ---- \u4F01\u4E1A\u7248 TCR\uFF1ACreateInstanceToken \u6362\u767B\u5F55\u6001 ----\n HOST=\"tcr.tencentcloudapi.com\"\n SERVICE=\"tcr\"\n VERSION=\"2019-09-24\"\n ACTION=\"CreateInstanceToken\"\n ALGORITHM=\"TC3-HMAC-SHA256\"\n TIMESTAMP=$(date +%s)\n DATE=$(date -u -d \"@${TIMESTAMP}\" +\"%Y-%m-%d\" 2>/dev/null || date -u -r \"${TIMESTAMP}\" +\"%Y-%m-%d\")\n\n # \u6784\u5EFA\u9636\u6BB5\u53EA\u9700\u8981\u4E00\u6B21 docker login/push\uFF0C\u4F7F\u7528 1 \u5C0F\u65F6\u4E34\u65F6\u51ED\u8BC1\uFF0C\u907F\u514D\u521B\u5EFA\u957F\u671F\u51ED\u8BC1\u3002\n PAYLOAD=$(printf '{\"RegistryId\":\"%s\",\"TokenType\":\"temp\"}' \"${TCR_INSTANCE_ID}\")\n HASHED_PAYLOAD=$(printf '%s' \"${PAYLOAD}\" | openssl dgst -sha256 -hex | awk '{print $NF}')\n CANONICAL_HEADERS=\"content-type:application/json; charset=utf-8\\nhost:${HOST}\\nx-tc-action:$(echo \"${ACTION}\" | tr '[:upper:]' '[:lower:]')\\n\"\n SIGNED_HEADERS=\"content-type;host;x-tc-action\"\n CANONICAL_REQUEST=\"POST\\n/\\n\\n${CANONICAL_HEADERS}\\n${SIGNED_HEADERS}\\n${HASHED_PAYLOAD}\"\n\n CREDENTIAL_SCOPE=\"${DATE}/${SERVICE}/tc3_request\"\n HASHED_CR=$(printf \"${CANONICAL_REQUEST}\" | openssl dgst -sha256 -hex | awk '{print $NF}')\n STRING_TO_SIGN=\"${ALGORITHM}\\n${TIMESTAMP}\\n${CREDENTIAL_SCOPE}\\n${HASHED_CR}\"\n\n secret_date=$(printf '%s' \"${DATE}\" | openssl dgst -sha256 -hmac \"TC3${API_SECRET_KEY}\" -hex | awk '{print $NF}')\n secret_svc=$(printf '%s' \"${SERVICE}\" | openssl dgst -sha256 -mac HMAC -macopt hexkey:\"${secret_date}\" -hex | awk '{print $NF}')\n secret_sign=$(printf '%s' \"tc3_request\" | openssl dgst -sha256 -mac HMAC -macopt hexkey:\"${secret_svc}\" -hex | awk '{print $NF}')\n SIGNATURE=$(printf \"${STRING_TO_SIGN}\" | openssl dgst -sha256 -mac HMAC -macopt hexkey:\"${secret_sign}\" -hex | awk '{print $NF}')\n\n AUTHZ=\"${ALGORITHM} Credential=${API_SECRET_ID}/${CREDENTIAL_SCOPE}, SignedHeaders=${SIGNED_HEADERS}, Signature=${SIGNATURE}\"\n\n RESP=$(curl -sS -X POST \"https://${HOST}\" \\\n -H \"Authorization: ${AUTHZ}\" \\\n -H \"Content-Type: application/json; charset=utf-8\" \\\n -H \"Host: ${HOST}\" \\\n -H \"X-TC-Action: ${ACTION}\" \\\n -H \"X-TC-Timestamp: ${TIMESTAMP}\" \\\n -H \"X-TC-Version: ${VERSION}\" \\\n -H \"X-TC-Region: ${REGION}\" \\\n -H \"X-TC-Token: ${API_TOKEN}\" \\\n --data \"${PAYLOAD}\")\n\n # CloudApp custom Runner \u5DF2\u63D0\u4F9B awk\uFF08\u4E0A\u9762\u7684 TC3 \u7B7E\u540D\u4E5F\u4F9D\u8D56\u5B83\uFF09\uFF0C\u4F46\u4E0D\u4FDD\u8BC1\u5B89\u88C5 jq\u3002\n # \u8FD9\u91CC\u5148\u9650\u5B9A\u9876\u5C42 Response \u5BF9\u8C61\uFF0C\u518D\u89E3\u6790\u5176\u4E2D\u7684\u975E\u7A7A JSON \u5B57\u7B26\u4E32\u5B57\u6BB5\uFF1B\u51ED\u8BC1\u5B57\u6BB5\n # \u4E0D\u5141\u8BB8 JSON \u8F6C\u4E49\uFF0C\u907F\u514D\u7528 sed/grep \u5BF9\u8BA4\u8BC1\u54CD\u5E94\u505A\u5BBD\u677E\u5339\u914D\u3002\n extract_json_string() {\n local field=\"$1\"\n printf '%s' \"${RESP}\" | JSON_FIELD=\"${field}\" awk '\n BEGIN {\n field = ENVIRON[\"JSON_FIELD\"]\n quote = sprintf(\"%c\", 34)\n backslash = sprintf(\"%c\", 92)\n json = \"\"\n }\n { json = json $0 }\n END {\n response_needle = quote \"Response\" quote \"[[:space:]]*:[[:space:]]*\\{\"\n if (!match(json, response_needle)) exit 1\n response_start = RSTART + RLENGTH - 1\n response = \"\"\n depth = 0\n in_string = 0\n escaped = 0\n for (i = response_start; i <= length(json); i++) {\n ch = substr(json, i, 1)\n response = response ch\n if (in_string) {\n if (escaped) escaped = 0\n else if (ch == backslash) escaped = 1\n else if (ch == quote) in_string = 0\n continue\n }\n if (ch == quote) in_string = 1\n else if (ch == \"{\") depth++\n else if (ch == \"}\") {\n depth--\n if (depth == 0) break\n }\n }\n if (depth != 0) exit 1\n\n depth = 1\n for (i = 2; i < length(response); i++) {\n ch = substr(response, i, 1)\n if (ch == \"{\") depth++\n else if (ch == \"}\") depth--\n else if (ch == quote) {\n key = \"\"\n escaped = 0\n for (j = i + 1; j <= length(response); j++) {\n current = substr(response, j, 1)\n if (escaped) {\n escaped = 0\n key = key current\n } else if (current == backslash) {\n escaped = 1\n key = key current\n } else if (current == quote) break\n else key = key current\n }\n if (j > length(response)) exit 1\n k = j + 1\n while (substr(response, k, 1) ~ /[[:space:]]/) k++\n if (depth == 1 && substr(response, k, 1) == \":\" && key == field) {\n k++\n while (substr(response, k, 1) ~ /[[:space:]]/) k++\n if (substr(response, k, 1) != quote) exit 1\n value = \"\"\n for (k = k + 1; k <= length(response); k++) {\n current = substr(response, k, 1)\n if (current == quote) {\n if (length(value) == 0) exit 1\n printf \"%s\", value\n exit 0\n }\n if (current == backslash) exit 2\n value = value current\n }\n exit 1\n }\n i = j\n }\n }\n exit 1\n }\n '\n }\n\n extract_tcr_error_code() {\n printf '%s' \"${RESP}\" | awk '\n BEGIN {\n quote = sprintf(\"%c\", 34)\n json = \"\"\n }\n { json = json $0 }\n END {\n error_needle = quote \"Error\" quote \"[[:space:]]*:[[:space:]]*\\{\"\n if (!match(json, error_needle)) exit 1\n error_json = substr(json, RSTART + RLENGTH)\n code_needle = quote \"Code\" quote \"[[:space:]]*:[[:space:]]*\" quote\n if (!match(error_json, code_needle)) exit 1\n rest = substr(error_json, RSTART + RLENGTH)\n value = \"\"\n for (i = 1; i <= length(rest); i++) {\n ch = substr(rest, i, 1)\n if (ch == quote) {\n if (length(value) == 0) exit 1\n printf \"%s\", value\n exit 0\n }\n if (ch == backslash) exit 1\n value = value ch\n }\n exit 1\n }\n '\n }\n\n ERROR_CODE=$(extract_tcr_error_code || true)\n if [ -n \"${ERROR_CODE}\" ]; then\n case \"${ERROR_CODE}\" in\n AuthFailure*|UnauthorizedOperation*|FailedOperation.NoPermission*)\n echo \"\u4F01\u4E1A\u7248 TCR \u9274\u6743\u5931\u8D25\uFF0C\u8BF7\u786E\u8BA4\u5F53\u524D\u8EAB\u4EFD\u6709\u6743\u8BBF\u95EE\u8BE5\u5B9E\u4F8B\u5E76\u8C03\u7528 CreateInstanceToken\" >&2\n exit 42\n ;;\n ResourceNotFound*|InvalidParameterValue.RegistryId*)\n echo \"\u4F01\u4E1A\u7248 TCR \u5B9E\u4F8B\u4E0D\u5B58\u5728\u6216\u5B9E\u4F8B ID \u65E0\u6548\" >&2\n exit 43\n ;;\n *)\n echo \"\u4F01\u4E1A\u7248 TCR \u83B7\u53D6\u767B\u5F55\u51ED\u8BC1\u5931\u8D25\uFF1A${ERROR_CODE}\" >&2\n exit 1\n ;;\n esac\n fi\n\n if ! TOKEN_USER=$(extract_json_string \"Username\"); then\n ERROR_CODE=$(extract_tcr_error_code || true)\n echo \"CreateInstanceToken \u672A\u8FD4\u56DE\u6709\u6548 Username${ERROR_CODE:+\uFF0C\u9519\u8BEF\u7801\uFF1A${ERROR_CODE}}\" >&2\n exit 42\n fi\n if ! TOKEN_PASS=$(extract_json_string \"Token\"); then\n ERROR_CODE=$(extract_tcr_error_code || true)\n echo \"CreateInstanceToken \u672A\u8FD4\u56DE\u6709\u6548 Token${ERROR_CODE:+\uFF0C\u9519\u8BEF\u7801\uFF1A${ERROR_CODE}}\" >&2\n exit 42\n fi\n\n printf '%s' \"${TOKEN_PASS}\" | docker login -u \"${TOKEN_USER}\" --password-stdin \"${TCR_REGISTRY}\"\nelse\n # ---- \u4E2A\u4EBA\u7248 CCR\uFF1A\u8D26\u53F7 ID + \u56FA\u5B9A\u5BC6\u7801\uFF08Base64 \u5C01\u88C5\u540E\u7531 CloudApp Secrets \u6CE8\u5165\uFF09 ----\n IS_PERSONAL_REGISTRY=1\n : \"${TCR_USERNAME:?TCR_USERNAME \u672A\u8BBE\u7F6E}\"\n : \"${SECRET_TCR_PASSWORD_B64:?TCR_PASSWORD \u672A\u8BBE\u7F6E}\"\n printf '%s' \"${SECRET_TCR_PASSWORD_B64}\" | base64 --decode | docker login -u \"${TCR_USERNAME}\" --password-stdin \"${TCR_REGISTRY}\"\nfi\n\nPUSH_LOG=$(mktemp)\ntrap 'rm -f \"${PUSH_LOG}\"' EXIT\nset +e\ndocker push \"${IMAGE}\" 2>&1 | tee \"${PUSH_LOG}\"\nPUSH_STATUS=${PIPESTATUS[0]}\nset -e\n\nif [ \"${PUSH_STATUS}\" -ne 0 ]; then\n if [ \"${IS_PERSONAL_REGISTRY}\" -eq 1 ] && {\n grep -Fq 'LimitExceeded.ErrRepoMaxLimit' \"${PUSH_LOG}\" ||\n grep -Fq '\u7528\u6237\u4ED3\u5E93\u5DF2\u7ECF\u8FBE\u5230\u6700\u5927\u914D\u989D' \"${PUSH_LOG}\"\n }; then\n echo \"\u4E2A\u4EBA\u7248 TCR \u955C\u50CF\u4ED3\u5E93\u6570\u91CF\u5DF2\u8FBE\u5230\u5F53\u524D\u5730\u57DF\u914D\u989D\uFF0C\u65E0\u6CD5\u81EA\u52A8\u521B\u5EFA\u76EE\u6807\u4ED3\u5E93\" >&2\n exit 44\n fi\n exit 1\nfi\n\necho \"::output::tag=${CLOUDBASE_VERSION_NAME}\"\n";
96
96
  /** buildImageOnCloud 可选项 */
97
97
  export interface IBuildImageOnCloudOptions {
98
98
  /** 日志回调,用于透出构建进度(不含凭证) */
@@ -1,36 +1,34 @@
1
1
  import { ICheckReport, INormalizedDeployConfig } from '../types';
2
2
  /**
3
- * 镜像拉取授权 Preflight(涉及 CAM,scope: cloud)
3
+ * 镜像仓库授权 Preflight(涉及 CAM,scope: cloud)
4
4
  *
5
- * SCF 拉取镜像由服务角色 SCF_QcsRole 代理,个人版镜像要求该角色绑定
6
- * QcloudAccessForSCFRoleInPullImage 策略。本模块在 image 策略部署前:
7
- * 1. 探测该角色与策略是否就绪;
8
- * 2. 缺失且允许时,自动绑定「唯一白名单策略」补齐(绝不放大到其它策略);
9
- * 3. 当前身份无 CAM 权限时不硬失败,降级为结构化指引 + 一键授权链接兜底。
5
+ * 镜像部署涉及两个职责不同的服务角色:
6
+ * - SCF_QcsRole:SCF 从个人版或企业版 TCR 拉取镜像;
7
+ * - TCB_QcsRole:CloudApp 云端构建向企业版 TCR 推送镜像。
10
8
  *
11
- * 安全边界(对应 security_rules 的 AuthZ / 最小权限):
12
- * - 自动授权只允许绑定 REQUIRED_IMAGE_PULL_POLICY 这一条部署必需的预置策略;
13
- * - 不代用户创建角色、不绑定任何白名单外策略;
14
- * - 探测/授权失败一律走降级,不阻断到无法给出可操作指引。
9
+ * 本模块在部署前探测所需角色与策略;缺失且允许时,只自动绑定各场景明确列入
10
+ * 白名单的预置策略;当前身份无 CAM 权限时,则返回结构化指引和一键授权链接。
11
+ * 模块不会创建角色,也不会绑定白名单之外的策略。
15
12
  */
16
13
  /** SCF 拉取镜像使用的服务角色 */
17
14
  export declare const SCF_PULL_IMAGE_ROLE = "SCF_QcsRole";
18
- /** 拉取个人版镜像所需的唯一预置策略(自动授权白名单,仅此一条) */
15
+ /** SCF 拉取个人版或企业版 TCR 镜像所需的预置策略 */
19
16
  export declare const REQUIRED_IMAGE_PULL_POLICY = "QcloudAccessForSCFRoleInPullImage";
20
- /** 官方一键授权链接,作为无 CAM 权限时的兜底指引 */
17
+ /** CloudApp 云端构建向企业版 TCR 推送镜像使用的服务角色 */
18
+ export declare const TCB_PUSH_ENTERPRISE_IMAGE_ROLE = "TCB_QcsRole";
19
+ /** CloudApp 云端构建向企业版 TCR 推送镜像所需的预置策略 */
20
+ export declare const REQUIRED_TCR_PUSH_POLICY = "QcloudAccessForTCBRoleInTCR";
21
+ /** SCF 拉取镜像的一键授权链接 */
21
22
  export declare const IMAGE_PULL_GRANT_URL: string;
23
+ /** 企业版 TCR 云端构建推送的一键授权链接 */
24
+ export declare const TCR_PUSH_GRANT_URL: string;
22
25
  /** 角色详情,仅取判断所需字段 */
23
26
  interface IRoleDetail {
24
27
  RoleName?: string;
25
28
  /** 已绑定的策略名集合,具体来源由适配层填充 */
26
29
  attachedPolicyNames?: string[];
27
30
  }
28
- /**
29
- * CAM 能力的最小依赖接口
30
- *
31
- * 只声明本模块需要的方法,便于解耦具体实现与单元测试。
32
- * 适配层负责把 CamService 的返回适配为 attachedPolicyNames。
33
- */
31
+ /** CAM 能力的最小依赖接口 */
34
32
  export interface ICamPreflightService {
35
33
  /** 获取角色详情及其已绑定策略名;角色不存在应抛出可识别错误 */
36
34
  getRoleWithPolicies: (roleName: string) => Promise<IRoleDetail>;
@@ -40,12 +38,10 @@ export interface ICamPreflightService {
40
38
  /** 判断错误是否为 CAM 权限不足 */
41
39
  export declare function isCamPermissionError(error: any): boolean;
42
40
  /**
43
- * 执行镜像拉取授权 Preflight
41
+ * 执行镜像仓库授权 Preflight
44
42
  *
45
- * @param service CAM 能力适配(无则视为无法校验,直接降级为提示)
46
- * @param config 规范化部署配置
47
- * @param autoGrant 是否允许自动补齐授权(默认 true)
48
- * @returns scope 为 cloud 的检查报告,聚合全部结论
43
+ * 企业版 cloud 策略会同时检查 SCF 拉取权限和 TCB 云端构建推送权限;
44
+ * 其他镜像策略只检查与自身相关的授权,不会额外要求 TCB 推送权限。
49
45
  */
50
46
  export declare function runImagePullAuthPreflight(service: ICamPreflightService | null, config: INormalizedDeployConfig, autoGrant?: boolean): Promise<ICheckReport>;
51
47
  export {};
@@ -130,6 +130,10 @@ export declare const FUNCTION_DEPLOY_ERROR: {
130
130
  readonly PREFLIGHT_IMAGE_PULL_POLICY_MISSING: "PREFLIGHT_IMAGE_PULL_POLICY_MISSING";
131
131
  readonly PREFLIGHT_IMAGE_PULL_AUTH_GRANTED: "PREFLIGHT_IMAGE_PULL_AUTH_GRANTED";
132
132
  readonly PREFLIGHT_IMAGE_PULL_AUTH_UNVERIFIABLE: "PREFLIGHT_IMAGE_PULL_AUTH_UNVERIFIABLE";
133
+ readonly PREFLIGHT_TCR_PUSH_ROLE_MISSING: "PREFLIGHT_TCR_PUSH_ROLE_MISSING";
134
+ readonly PREFLIGHT_TCR_PUSH_POLICY_MISSING: "PREFLIGHT_TCR_PUSH_POLICY_MISSING";
135
+ readonly PREFLIGHT_TCR_PUSH_AUTH_GRANTED: "PREFLIGHT_TCR_PUSH_AUTH_GRANTED";
136
+ readonly PREFLIGHT_TCR_PUSH_AUTH_UNVERIFIABLE: "PREFLIGHT_TCR_PUSH_AUTH_UNVERIFIABLE";
133
137
  readonly PREFLIGHT_CAM_PERMISSION_DENIED: "PREFLIGHT_CAM_PERMISSION_DENIED";
134
138
  readonly FUNCTION_RECREATE_REQUIRED: "FUNCTION_RECREATE_REQUIRED";
135
139
  readonly NOT_IMPLEMENTED: "NOT_IMPLEMENTED";
@@ -225,6 +225,8 @@ export interface ILayer {
225
225
  LayerName: string;
226
226
  LayerVersion: number;
227
227
  }
228
+ /** 企业版 TCR 镜像对 SCF 不可见时的排查指引 */
229
+ export declare const ENTERPRISE_IMAGE_REGION_GUIDANCE = "\u8BF7\u786E\u8BA4 TCR \u4F01\u4E1A\u7248\u5B9E\u4F8B\u5DF2\u5728\u5F53\u524D\u51FD\u6570\u5730\u57DF\u63D0\u4F9B\u53EF\u8BBF\u95EE\u7684\u5B9E\u4F8B\u6216\u590D\u5236\u5B9E\u4F8B\uFF0C\u5E76\u68C0\u67E5 RegistryId\u3001\u955C\u50CF\u5730\u5740\u53CA\u62C9\u53D6\u6743\u9650\u3002";
228
230
  export declare class FunctionService {
229
231
  private environment;
230
232
  private vpcService;