@cloudbase/manager-node 5.8.2 → 5.8.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/database/index.js +11 -6
  3. package/lib/deploy/DeployOrchestrator.js +170 -278
  4. package/lib/deploy/FunctionDeployer.js +310 -52
  5. package/lib/deploy/StaticDeployer.js +7 -1
  6. package/lib/deploy/function/artifact.js +55 -41
  7. package/lib/deploy/function/builders/cloud.js +58 -359
  8. package/lib/deploy/function/cam-preflight.js +2 -1
  9. package/lib/deploy/function/cloud-build-service.js +2 -16
  10. package/lib/deploy/function/config-guard.js +210 -55
  11. package/lib/deploy/function/enterprise-tcr.js +195 -0
  12. package/lib/deploy/function/image-tag.js +31 -0
  13. package/lib/deploy/function/local-builder.js +178 -32
  14. package/lib/deploy/function/personal-tcr.js +153 -0
  15. package/lib/deploy/function/preflight.js +3 -2
  16. package/lib/deploy/types.js +11 -0
  17. package/lib/hosting/index.js +27 -0
  18. package/lib/projectValidator/index.js +22 -23
  19. package/package.json +2 -2
  20. package/types/database/index.d.ts +1 -0
  21. package/types/deploy/DeployOrchestrator.d.ts +19 -25
  22. package/types/deploy/FunctionDeployer.d.ts +35 -3
  23. package/types/deploy/StaticDeployer.d.ts +11 -0
  24. package/types/deploy/function/artifact.d.ts +0 -5
  25. package/types/deploy/function/builders/cloud.d.ts +6 -39
  26. package/types/deploy/function/config-guard.d.ts +18 -0
  27. package/types/deploy/function/enterprise-tcr.d.ts +32 -0
  28. package/types/deploy/function/image-tag.d.ts +10 -0
  29. package/types/deploy/function/local-builder.d.ts +35 -6
  30. package/types/deploy/function/personal-tcr.d.ts +35 -0
  31. package/types/deploy/types.d.ts +110 -28
  32. package/types/hosting/index.d.ts +13 -0
  33. package/types/projectValidator/index.d.ts +3 -2
  34. package/types/projectValidator/types.d.ts +0 -1
  35. package/lib/deploy/StateStore.js +0 -328
  36. package/types/deploy/StateStore.d.ts +0 -174
@@ -3,7 +3,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.TCR_LOGIN_PUSH_SCRIPT = exports.TCR_PUSH_SCRIPT_ZIP_PATH = void 0;
7
6
  exports.buildImageOnCloud = buildImageOnCloud;
8
7
  const fs_1 = __importDefault(require("fs"));
9
8
  const path_1 = __importDefault(require("path"));
@@ -42,10 +41,6 @@ const POLL_MAX_INTERVAL = 30000;
42
41
  const POLL_TOTAL_TIMEOUT = 15 * 60 * 1000;
43
42
  /** 轮询临时错误最大连续重试次数,超过则判失败 */
44
43
  const POLL_MAX_TRANSIENT_ERRORS = 5;
45
- const TCR_REGISTRY_AUTH_EXIT_CODE = 42;
46
- const TCR_REGISTRY_INVALID_EXIT_CODE = 43;
47
- /** 个人版 TCR 自动创建目标仓库时,仓库数量已达到地域配额 */
48
- const TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE = 44;
49
44
  /** 仓库地址/命名空间/服务名等安全字符白名单:域名、路径、字母数字与常见分隔符 */
50
45
  const SAFE_REGISTRY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._\-/]*$/;
51
46
  /** tag 白名单:字母数字、下划线、点、中划线 */
@@ -113,263 +108,13 @@ function assertSafeBuildArgs(buildArgs) {
113
108
  }
114
109
  /** 个人版 CCR 默认域名 */
115
110
  const DEFAULT_PERSONAL_REGISTRY = 'ccr.ccs.tencentyun.com';
116
- /** 注入到构建上下文 zip 中的推送脚本相对路径(与 CustomSteps 的 push命令一致) */
117
- exports.TCR_PUSH_SCRIPT_ZIP_PATH = 'scripts/tcr-login-and-push.sh';
118
- /**
119
- * 注入到构建 zip 的 TCR/CCR 登录推送脚本(完整可运行)
120
- *
121
- * 对应构建方案(https://iwiki.woa.com/p/4020519533)「阶段 A」的 push 步骤:
122
- * push-image 步骤执行 `bash./scripts/tcr-login-and-push.sh`,该脚本必须随源码
123
- * 一同打进zip 根目录,否则云端会 `No such file or directory` 秒挂。
124
- *
125
- * 关键设计:
126
- * - 地域参数化:REGION 读环境变量 $TCR_REGION(由 buildEnv 按部署地域注入),
127
- * 避免硬编码 ap-shanghai跨地域时 AuthFailure.SignatureFailure。
128
- * - 双仓库兼容:设置了 $TCR_INSTANCE_ID 走企业版 TCR CreateInstanceToken;
129
- * 否则走个人版 CCR,用户名来自普通 Env,固定密码来自 CloudApp Secrets。
130
- * - 安全红线:凭证一律经 --password-stdin 传入,绝不出现在 ps/日志;
131
- * 不打印任何 $API_SECRET_* / token / $SECRET_TCR_PASSWORD_B64 或解码后的密码。
132
- *
133
- * 脚本内容为SDK 固定模板,不拼接任何用户输入(镜像地址等均由已校验的
134
- * 环境变量在容器内组装),无命令注入面。
135
- */
136
- exports.TCR_LOGIN_PUSH_SCRIPT = `#!/usr/bin/env bash
137
- # scripts/tcr-login-and-push.sh
138
- # 由 CloudBase SDK 自动注入:企业版使用 STS 换取临时 token,个人版使用 CloudApp Secret
139
- set -euo pipefail
140
-
141
- : "\${TCR_REGISTRY:?TCR_REGISTRY 未设置}"
142
- : "\${TCR_NAMESPACE:?TCR_NAMESPACE 未设置}"
143
- : "\${CLOUDBASE_SERVICE_NAME:?平台变量缺失}"
144
- : "\${CLOUDBASE_VERSION_NAME:?平台变量缺失}"
145
-
146
- REGION="\${TCR_REGION:-ap-shanghai}"
147
- IMAGE="\${TCR_REGISTRY}/\${TCR_NAMESPACE}/\${CLOUDBASE_SERVICE_NAME}:\${CLOUDBASE_VERSION_NAME}"
148
- IS_PERSONAL_REGISTRY=0
149
-
150
- if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
151
- : "\${API_SECRET_ID:?STS 凭证缺失}"
152
- : "\${API_SECRET_KEY:?STS 凭证缺失}"
153
- : "\${API_TOKEN:?STS 凭证缺失}"
154
-
155
- # ---- 企业版 TCR:CreateInstanceToken 换登录态 ----
156
- HOST="tcr.tencentcloudapi.com"
157
- SERVICE="tcr"
158
- VERSION="2019-09-24"
159
- ACTION="CreateInstanceToken"
160
- ALGORITHM="TC3-HMAC-SHA256"
161
- TIMESTAMP=$(date +%s)
162
- DATE=$(date -u -d "@\${TIMESTAMP}" +"%Y-%m-%d" 2>/dev/null || date -u -r "\${TIMESTAMP}" +"%Y-%m-%d")
163
-
164
- # 构建阶段只需要一次 docker login/push,使用 1 小时临时凭证,避免创建长期凭证。
165
- PAYLOAD=$(printf '{"RegistryId":"%s","TokenType":"temp"}' "\${TCR_INSTANCE_ID}")
166
- HASHED_PAYLOAD=$(printf '%s' "\${PAYLOAD}" | openssl dgst -sha256 -hex | awk '{print $NF}')
167
- CANONICAL_HEADERS="content-type:application/json; charset=utf-8\\nhost:\${HOST}\\nx-tc-action:$(echo "\${ACTION}" | tr '[:upper:]' '[:lower:]')\\n"
168
- SIGNED_HEADERS="content-type;host;x-tc-action"
169
- CANONICAL_REQUEST="POST\\n/\\n\\n\${CANONICAL_HEADERS}\\n\${SIGNED_HEADERS}\\n\${HASHED_PAYLOAD}"
170
-
171
- CREDENTIAL_SCOPE="\${DATE}/\${SERVICE}/tc3_request"
172
- HASHED_CR=$(printf "\${CANONICAL_REQUEST}" | openssl dgst -sha256 -hex | awk '{print $NF}')
173
- STRING_TO_SIGN="\${ALGORITHM}\\n\${TIMESTAMP}\\n\${CREDENTIAL_SCOPE}\\n\${HASHED_CR}"
174
-
175
- secret_date=$(printf '%s' "\${DATE}" | openssl dgst -sha256 -hmac "TC3\${API_SECRET_KEY}" -hex | awk '{print $NF}')
176
- secret_svc=$(printf '%s' "\${SERVICE}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_date}" -hex | awk '{print $NF}')
177
- secret_sign=$(printf '%s' "tc3_request" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_svc}" -hex | awk '{print $NF}')
178
- SIGNATURE=$(printf "\${STRING_TO_SIGN}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_sign}" -hex | awk '{print $NF}')
179
-
180
- AUTHZ="\${ALGORITHM} Credential=\${API_SECRET_ID}/\${CREDENTIAL_SCOPE}, SignedHeaders=\${SIGNED_HEADERS}, Signature=\${SIGNATURE}"
181
-
182
- RESP=$(curl -sS -X POST "https://\${HOST}" \\
183
- -H "Authorization: \${AUTHZ}" \\
184
- -H "Content-Type: application/json; charset=utf-8" \\
185
- -H "Host: \${HOST}" \\
186
- -H "X-TC-Action: \${ACTION}" \\
187
- -H "X-TC-Timestamp: \${TIMESTAMP}" \\
188
- -H "X-TC-Version: \${VERSION}" \\
189
- -H "X-TC-Region: \${REGION}" \\
190
- -H "X-TC-Token: \${API_TOKEN}" \\
191
- --data "\${PAYLOAD}")
192
-
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)
310
- if [ -n "\${ERROR_CODE}" ]; then
311
- case "\${ERROR_CODE}" in
312
- AuthFailure*|UnauthorizedOperation*|FailedOperation.NoPermission*)
313
- echo "企业版 TCR 鉴权失败,请确认当前身份有权访问该实例并调用 CreateInstanceToken" >&2
314
- exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
315
- ;;
316
- ResourceNotFound*|InvalidParameterValue.RegistryId*)
317
- echo "企业版 TCR 实例不存在或实例 ID 无效" >&2
318
- exit ${TCR_REGISTRY_INVALID_EXIT_CODE}
319
- ;;
320
- *)
321
- echo "企业版 TCR 获取登录凭证失败:\${ERROR_CODE}" >&2
322
- exit 1
323
- ;;
324
- esac
325
- fi
326
-
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
337
-
338
- printf '%s' "\${TOKEN_PASS}" | docker login -u "\${TOKEN_USER}" --password-stdin "\${TCR_REGISTRY}"
339
- else
340
- # ---- 个人版 CCR:账号 ID + 固定密码(Base64 封装后由 CloudApp Secrets 注入) ----
341
- IS_PERSONAL_REGISTRY=1
342
- : "\${TCR_USERNAME:?TCR_USERNAME 未设置}"
343
- : "\${SECRET_TCR_PASSWORD_B64:?TCR_PASSWORD 未设置}"
344
- printf '%s' "\${SECRET_TCR_PASSWORD_B64}" | base64 --decode | docker login -u "\${TCR_USERNAME}" --password-stdin "\${TCR_REGISTRY}"
345
- fi
346
-
347
- PUSH_LOG=$(mktemp)
348
- trap 'rm -f "\${PUSH_LOG}"' EXIT
349
- set +e
350
- docker push "\${IMAGE}" 2>&1 | tee "\${PUSH_LOG}"
351
- PUSH_STATUS=\${PIPESTATUS[0]}
352
- set -e
353
-
354
- if [ "\${PUSH_STATUS}" -ne 0 ]; then
355
- if [ "\${IS_PERSONAL_REGISTRY}" -eq 1 ] && {
356
- grep -Fq 'LimitExceeded.ErrRepoMaxLimit' "\${PUSH_LOG}" ||
357
- grep -Fq '用户仓库已经达到最大配额' "\${PUSH_LOG}"
358
- }; then
359
- echo "个人版 TCR 镜像仓库数量已达到当前地域配额,无法自动创建目标仓库" >&2
360
- exit ${TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE}
361
- fi
362
- exit 1
363
- fi
364
-
365
- echo "::output::tag=\${CLOUDBASE_VERSION_NAME}"
366
- `;
367
111
  function resolveTarget(config) {
368
- const build = config.build;
112
+ const build = config.imageConfig.build;
113
+ const imageType = config.imageConfig.imageType;
369
114
  const repository = (build.repository || config.name).trim();
370
115
  const rawRegistryId = build.registryId;
371
116
  const registryId = typeof rawRegistryId === 'string' ? rawRegistryId.trim() : rawRegistryId;
372
- const isEnterprise = config.imageType === 'enterprise' || Boolean(registryId);
117
+ const isEnterprise = imageType === 'enterprise' || Boolean(registryId);
373
118
  // CloudApp custom 构建与推送必须使用同一个平台版本 tag。
374
119
  // 若允许自定义 tag,build-image 会构建自定义 tag,而固定推送脚本会推
375
120
  // $CLOUDBASE_VERSION_NAME,最终必然找不到本地镜像。
@@ -384,10 +129,6 @@ function resolveTarget(config) {
384
129
  let hasExplicitRegistry = false;
385
130
  let namespace = (build.namespace || '').trim();
386
131
  let imageName = repository;
387
- if (!isEnterprise && !namespace) {
388
- throw new error_1.CloudBaseError('个人版 TCR 云端构建缺少 build.namespace;请在 cloudbaserc 中显式声明,' +
389
- '推荐使用 "{{env.TCB_TCR_NAMESPACE}}"', { code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID });
390
- }
391
132
  if (repository.includes('/')) {
392
133
  assertSafeRegistryToken(repository, 'build.repository');
393
134
  const parts = repository.split('/').filter(Boolean);
@@ -415,10 +156,10 @@ function resolveTarget(config) {
415
156
  throw new error_1.CloudBaseError('cloud 策略缺少 TCR 命名空间,请配置 build.namespace 或在完整 repository 中声明', { code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID });
416
157
  }
417
158
  assertSafeRegistryToken(namespace, 'TCR namespace');
418
- if (config.imageType === 'personal' && registryId) {
159
+ if (imageType === 'personal' && registryId) {
419
160
  throw new error_1.CloudBaseError('cloud 策略的 personal 镜像不能配置 build.registryId;请移除该字段或改用 enterprise', { code: types_1.FUNCTION_DEPLOY_ERROR.IMAGE_TYPE_INVALID });
420
161
  }
421
- if (config.imageType === 'enterprise') {
162
+ if (imageType === 'enterprise') {
422
163
  if (rawRegistryId === undefined || rawRegistryId === null) {
423
164
  throw new error_1.CloudBaseError('cloud 策略的企业版 TCR 未配置 build.registryId;如果当前没有企业版实例,请改用 imageType=personal', { code: types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_MISSING });
424
165
  }
@@ -443,9 +184,10 @@ function resolveTarget(config) {
443
184
  * CustomSteps 只包含 SDK 固定命令和经过白名单校验的 build arg key。
444
185
  */
445
186
  function resolveCloudBuildOptions(config) {
446
- const dockerfile = config.build.dockerfile || types_1.DEFAULT_DOCKERFILE;
447
- const platform = config.build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
448
- const buildArgs = config.build.buildArgs || {};
187
+ const build = config.imageConfig.build;
188
+ const dockerfile = build.dockerfile || types_1.DEFAULT_DOCKERFILE;
189
+ const platform = build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
190
+ const buildArgs = build.buildArgs || {};
449
191
  assertSafeDockerfile(dockerfile);
450
192
  if (platform !== types_1.DEFAULT_IMAGE_PLATFORM) {
451
193
  throw new error_1.CloudBaseError(`当前仅支持构建 ${types_1.DEFAULT_IMAGE_PLATFORM} 镜像`, {
@@ -457,7 +199,7 @@ function resolveCloudBuildOptions(config) {
457
199
  dockerfile,
458
200
  platform,
459
201
  buildArgs,
460
- forceBuild: config.build.forceBuild === true
202
+ forceBuild: build.forceBuild === true
461
203
  };
462
204
  }
463
205
  /**
@@ -465,14 +207,23 @@ function resolveCloudBuildOptions(config) {
465
207
  *
466
208
  * Dockerfile、platform 和 build arg value 通过 CloudApp Env 注入;只有通过严格
467
209
  * ENV key 校验的 build arg key 会进入固定 command。forceBuild 显式映射为 --no-cache。
210
+ *
211
+ * push-image 直接在 CloudApp 构建容器内执行 docker login 与 docker push:
212
+ * 登录凭证一律经 --password-stdin 传入,不落 ps/日志,也不再向构建上下文注入脚本文件。
468
213
  */
469
- function buildCustomSteps(options) {
214
+ function buildCustomSteps(options, isEnterpriseRegistry) {
470
215
  const buildArgs = Object.keys(options.buildArgs)
471
216
  .sort()
472
217
  .map(key => `--build-arg ${key}="$BUILD_ARG_${key}"`)
473
218
  .join(' ');
474
219
  const noCache = options.forceBuild ? ' --no-cache' : '';
475
220
  const extraArgs = buildArgs ? ` ${buildArgs}` : '';
221
+ const image = '"$TCR_REGISTRY/$TCR_NAMESPACE/$CLOUDBASE_SERVICE_NAME:$CLOUDBASE_VERSION_NAME"';
222
+ // 企业版临时登录凭证由 manager-node 在 CreateCloudApp 前换取并经 CloudApp Secrets 注入;
223
+ // 个人版沿用账号 ID 与 Base64 封装的固定密码 Secret,容器内解码后直接送入 stdin。
224
+ const loginCommand = isEnterpriseRegistry
225
+ ? 'printf \'%s\' "$SECRET_TCR_TOKEN_PASSWORD" | docker login -u "$SECRET_TCR_TOKEN_USERNAME" --password-stdin "$TCR_REGISTRY"'
226
+ : 'printf \'%s\' "$SECRET_TCR_PASSWORD_B64" | base64 --decode | docker login -u "$TCR_USERNAME" --password-stdin "$TCR_REGISTRY"';
476
227
  return [
477
228
  {
478
229
  name: 'build-image',
@@ -481,24 +232,17 @@ function buildCustomSteps(options) {
481
232
  ' -f "$CLOUDBASE_DOCKERFILE"' +
482
233
  noCache +
483
234
  extraArgs +
484
- ' -t $TCR_REGISTRY/$TCR_NAMESPACE/$CLOUDBASE_SERVICE_NAME:$CLOUDBASE_VERSION_NAME .'
235
+ ` -t ${image} .`
485
236
  },
486
237
  {
487
238
  name: 'push-image',
488
- command: 'bash ./scripts/tcr-login-and-push.sh'
239
+ command: `${loginCommand} && docker push ${image}`
489
240
  }
490
241
  ];
491
242
  }
492
- /**
493
- * 组装 custom 构建的环境变量(用户可控值已校验,此处只做键值透传)
494
- *
495
- * - TCR_REGION:部署地域,供推送脚本换取镜像仓库登录态时签名/请求用,
496
- * 缺省交由脚本回退默认地域;参数化后跨地域不会 SignatureFailure。
497
- * - TCR_INSTANCE_ID:仅企业版 TCR 注入,脚本据此走 CreateInstanceToken 分支;
498
- * 个人版 CCR 不注入,脚本走 STS 直接 docker login 分支。
499
- */
500
- function buildEnv(target, buildOptions, region) {
501
- const env = [
243
+ /** 组装 custom 构建的环境变量(用户可控值已校验,此处只做键值透传) */
244
+ function buildEnv(target, buildOptions) {
245
+ return [
502
246
  { key: 'TCR_REGISTRY', value: target.registry },
503
247
  { key: 'TCR_NAMESPACE', value: target.namespace },
504
248
  { key: 'CLOUDBASE_DOCKERFILE', value: buildOptions.dockerfile },
@@ -508,27 +252,29 @@ function buildEnv(target, buildOptions, region) {
508
252
  value
509
253
  }))
510
254
  ];
511
- if (region) {
512
- env.push({ key: 'TCR_REGION', value: region });
513
- }
514
- if (target.registryId) {
515
- assertSafeRegistryToken(target.registryId, 'registryId');
516
- env.push({ key: 'TCR_INSTANCE_ID', value: target.registryId });
517
- }
518
- return env;
519
255
  }
520
256
  /**
521
- * 组装个人版 TCR 凭证。
522
- * username 可作为普通构建变量;password 必须进入 CloudApp Secrets。
257
+ * 组装 TCR 登录凭证。
258
+ * 企业版使用 manager-node 预先换取的临时 Username/Token;个人版 username 可作为
259
+ * 普通构建变量,password 必须经 Base64 封装后进入 CloudApp Secrets。
523
260
  */
524
- function buildRegistryCredentials(config, target) {
261
+ function buildRegistryCredentials(config, target, enterpriseCredential) {
525
262
  var _a;
526
263
  if (target.registryId) {
527
- return { env: [] };
264
+ if (!(enterpriseCredential === null || enterpriseCredential === void 0 ? void 0 : enterpriseCredential.username) || !(enterpriseCredential === null || enterpriseCredential === void 0 ? void 0 : enterpriseCredential.token)) {
265
+ throw new error_1.CloudBaseError('企业版 TCR 云端构建缺少临时登录凭证,请确认当前身份具备 CreateInstanceToken 权限', { code: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING });
266
+ }
267
+ return {
268
+ env: [],
269
+ secrets: [
270
+ { name: 'TCR_TOKEN_USERNAME', value: enterpriseCredential.username },
271
+ { name: 'TCR_TOKEN_PASSWORD', value: enterpriseCredential.token }
272
+ ]
273
+ };
528
274
  }
529
- const credential = config.build.registryCredential;
275
+ const credential = config.imageConfig.build.registryCredential;
530
276
  if (!credential) {
531
- throw new error_1.CloudBaseError('个人版 TCR 云端构建缺少 build.registryCredential;请在 cloudbaserc 中显式声明 ' +
277
+ throw new error_1.CloudBaseError('个人版 TCR 云端构建缺少 imageConfig.build.registryCredential;请在 cloudbaserc 中显式声明 ' +
532
278
  'username/password,推荐分别使用 "{{env.TCB_TCR_USERNAME}}" 和 ' +
533
279
  '"{{env.TCB_TCR_PASSWORD}}"', { code: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING });
534
280
  }
@@ -552,7 +298,7 @@ function buildRegistryCredentials(config, target) {
552
298
  };
553
299
  }
554
300
  function resolveCloudBuildContext(config, dockerfile) {
555
- const cwd = path_1.default.resolve(config.build.cwd || config.functionPath || process.cwd());
301
+ const cwd = path_1.default.resolve(config.imageConfig.build.cwd || config.functionPath || process.cwd());
556
302
  const dockerfilePath = path_1.default.resolve(cwd, dockerfile);
557
303
  let stat = null;
558
304
  try {
@@ -581,35 +327,9 @@ function resolveImageUri(target, versionName, artifacts) {
581
327
  const imageUri = `${target.registry}/${target.namespace}/${target.serviceName}:${versionName}`;
582
328
  return { imageUri, imageDigest: imageArtifact === null || imageArtifact === void 0 ? void 0 : imageArtifact.Digest };
583
329
  }
584
- /** 判断企业版 TCR 推送步骤是否因实例鉴权失败 */
585
- function isRegistryAuthFailure(steps) {
586
- return (steps || []).some(step => {
587
- var _a;
588
- return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
589
- ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
590
- step.ExitCode === TCR_REGISTRY_AUTH_EXIT_CODE;
591
- });
592
- }
593
- /** 判断企业版 TCR 推送步骤是否因实例 ID 无效失败 */
594
- function isRegistryIdFailure(steps) {
595
- return (steps || []).some(step => {
596
- var _a;
597
- return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
598
- ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
599
- step.ExitCode === TCR_REGISTRY_INVALID_EXIT_CODE;
600
- });
601
- }
602
- /** 判断个人版 TCR 是否因自动创建目标仓库时达到仓库数量配额而失败 */
603
- function isPersonalRepositoryQuotaFailure(target, steps) {
604
- if (target.registryId) {
605
- return false;
606
- }
607
- return (steps || []).some(step => {
608
- var _a;
609
- return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
610
- ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
611
- step.ExitCode === TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE;
612
- });
330
+ /** 判断失败是否发生在 push-image 步骤,用于给出针对性的排查引导 */
331
+ function isPushStepFailure(steps) {
332
+ return (steps || []).some(step => { var _a; return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' && ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed'; });
613
333
  }
614
334
  /** 汇总失败 Step 的定位信息(脱敏:只取步骤名/状态/退出码,不含日志明文) */
615
335
  function summarizeFailedSteps(steps) {
@@ -629,25 +349,19 @@ function summarizeFailedSteps(steps) {
629
349
  *
630
350
  * @param config 规范化的 cloud 部署配置
631
351
  * @param service 注入的外部能力(打包/tcb API/COS 上传)
632
- * @param options 可选项:日志回调 onLog、部署地域 region
352
+ * @param options 可选项:日志回调 onLog、企业版临时登录凭证
633
353
  */
634
354
  async function buildImageOnCloud(config, service, options = {}) {
635
- const { onLog, region } = options;
355
+ const { onLog, enterpriseCredential } = options;
636
356
  const target = resolveTarget(config);
637
357
  const buildOptions = resolveCloudBuildOptions(config);
638
358
  // 在打包和上传前完成真实消费字段校验,避免无效配置触发远端资源操作。
639
- const registryCredentials = buildRegistryCredentials(config, target);
359
+ const registryCredentials = buildRegistryCredentials(config, target, enterpriseCredential);
640
360
  const serviceName = target.serviceName;
641
361
  const cwd = resolveCloudBuildContext(config, buildOptions.dockerfile);
642
- // 1. 打包构建上下文(注入 push 脚本,不污染用户源码目录)
362
+ // 1. 只打包用户构建上下文;登录推送逻辑已收敛到 CloudApp CustomStep 命令
643
363
  onLog === null || onLog === void 0 ? void 0 : onLog('打包构建上下文');
644
- const zipPath = await service.packContext(cwd, [
645
- {
646
- zipPath: exports.TCR_PUSH_SCRIPT_ZIP_PATH,
647
- content: exports.TCR_LOGIN_PUSH_SCRIPT,
648
- mode: 0o755
649
- }
650
- ]);
364
+ const zipPath = await service.packContext(cwd);
651
365
  try {
652
366
  // 2. 获取 COS 上传凭证
653
367
  onLog === null || onLog === void 0 ? void 0 : onLog('获取 COS 上传凭证');
@@ -665,12 +379,9 @@ async function buildImageOnCloud(config, service, options = {}) {
665
379
  const created = await service.createBuild({
666
380
  serviceName,
667
381
  cosTimestamp: cosInfo.unixTimestamp,
668
- env: [
669
- ...buildEnv(target, buildOptions, region),
670
- ...registryCredentials.env
671
- ],
382
+ env: [...buildEnv(target, buildOptions), ...registryCredentials.env],
672
383
  secrets: registryCredentials.secrets,
673
- customSteps: buildCustomSteps(buildOptions)
384
+ customSteps: buildCustomSteps(buildOptions, Boolean(target.registryId))
674
385
  });
675
386
  if (!(created === null || created === void 0 ? void 0 : created.versionName)) {
676
387
  throw new error_1.CloudBaseError('触发云端构建失败:返回缺少 VersionName', {
@@ -693,27 +404,15 @@ async function buildImageOnCloud(config, service, options = {}) {
693
404
  const reqId = finalStatus.requestId
694
405
  ? `,requestId=${finalStatus.requestId}`
695
406
  : '';
696
- const registryAuthFailure = isRegistryAuthFailure(finalStatus.steps);
697
- const registryIdFailure = isRegistryIdFailure(finalStatus.steps);
698
- const personalRepositoryQuotaFailure = isPersonalRepositoryQuotaFailure(target, finalStatus.steps);
407
+ // 登录凭证与目标仓库均在触发构建前由 manager-node 校验并准备,
408
+ // 因此这里只区分取消与失败,具体原因以构建日志中的 docker 输出为准。
699
409
  const code = finalStatus.status === 'CANCELED'
700
410
  ? types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_CANCELED
701
- : registryAuthFailure
702
- ? types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_AUTH_FAILED
703
- : registryIdFailure
704
- ? types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_INVALID
705
- : personalRepositoryQuotaFailure
706
- ? types_1.FUNCTION_DEPLOY_ERROR.CLOUD_PERSONAL_REGISTRY_REPOSITORY_QUOTA_EXCEEDED
707
- : types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_FAILED;
708
- const guidance = finalStatus.status === 'CANCELED'
411
+ : types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_FAILED;
412
+ const guidance = finalStatus.status === 'CANCELED' || !isPushStepFailure(finalStatus.steps)
709
413
  ? ''
710
- : registryAuthFailure
711
- ? ';企业版 TCR 鉴权失败,请确认实例属于当前账号/地域,且当前身份拥有 CreateInstanceToken 和仓库推送权限'
712
- : registryIdFailure
713
- ? ';企业版 TCR 实例不存在或实例 ID 无效,请检查 build.registryId 对应的环境变量和实例状态'
714
- : personalRepositoryQuotaFailure
715
- ? ';个人版 TCR 当前地域的镜像仓库数量已达到配额,目标仓库无法自动创建。请在容器镜像服务个人版中删除不再使用的仓库(删除前请备份其中镜像),或将 build.repository 改为已有仓库;如需更多仓库,请切换企业版 TCR。实际配额请以控制台为准'
716
- : '';
414
+ : ';推送镜像失败,请在云开发控制台查看构建日志中的 docker login/push 输出,' +
415
+ '并确认镜像仓库凭证、命名空间与仓库配额';
717
416
  throw new error_1.CloudBaseError(`云端构建未成功(${finalStatus.status})${detail}${reqId}${guidance}`, { code });
718
417
  }
719
418
  // 6. 得到不可变镜像地址与digest
@@ -74,9 +74,10 @@ function needsImagePullAuth(config) {
74
74
  }
75
75
  /** 只有企业版 cloud 策略需要 CloudApp 向企业版 TCR 推送镜像 */
76
76
  function needsEnterpriseTcrPushAuth(config) {
77
+ var _a;
77
78
  return (config.type === 'HTTP' &&
78
79
  config.buildStrategy === 'cloud' &&
79
- config.imageType === 'enterprise');
80
+ ((_a = config.imageConfig) === null || _a === void 0 ? void 0 : _a.imageType) === 'enterprise');
80
81
  }
81
82
  function resolveRequirements(config) {
82
83
  const requirements = [];
@@ -60,32 +60,18 @@ const DEFAULT_IGNORE = [
60
60
  function createCloudBuildService(cloudApp, options = {}) {
61
61
  const ignore = [...DEFAULT_IGNORE, ...(options.ignore || [])];
62
62
  return {
63
- async packContext(cwd, extraEntries) {
63
+ async packContext(cwd) {
64
64
  if (!fs_1.default.existsSync(cwd) || !fs_1.default.statSync(cwd).isDirectory()) {
65
65
  throw new error_1.CloudBaseError(`构建上下文目录不存在或不是目录:${cwd}`, {
66
66
  code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_CONTEXT_INVALID
67
67
  });
68
68
  }
69
69
  const zipPath = path_1.default.join(os_1.default.tmpdir(), `fn-cloud-build-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`);
70
- const injectedEntries = (extraEntries || []).map(entry => ({
71
- zipPath: entry.zipPath.replace(/\\/g, '/').replace(/^\.\//, ''),
72
- content: entry.content,
73
- mode: entry.mode
74
- }));
75
- // 源码中若存在与 SDK 注入文件同名的路径,必须从 glob 中排除,
76
- // 避免 ZIP 出现重复 entry 后由解压顺序决定最终执行内容。
77
- const contextIgnore = [
78
- ...ignore,
79
- ...injectedEntries.map(entry => entry.zipPath)
80
- ];
81
- // 把 SDK 注入的辅助文件(如 push 脚本)随源码一起打进zip,
82
- // 通过临时文件注入,绝不写入用户源码目录。
83
70
  try {
84
71
  await (0, utils_1.compressToZip)({
85
72
  dirPath: cwd,
86
73
  outputPath: zipPath,
87
- ignore: contextIgnore,
88
- extraEntries: injectedEntries
74
+ ignore
89
75
  });
90
76
  }
91
77
  catch (error) {