@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.
@@ -182,7 +182,7 @@ function inferHttpBuildStrategy(config) {
182
182
  * 以便 checkDeployConfig 汇总全部问题
183
183
  */
184
184
  function normalizeDeployConfig(config) {
185
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k;
185
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
186
186
  if (!config || typeof config !== 'object') {
187
187
  throw new error_1.CloudBaseError('部署配置必须是对象', {
188
188
  code: types_1.FUNCTION_DEPLOY_ERROR.CONFIG_INVALID
@@ -211,19 +211,19 @@ function normalizeDeployConfig(config) {
211
211
  ignore: ignoreList.filter((item) => !/node_modules/.test(item)) });
212
212
  }
213
213
  const normalizedImage = Object.assign(Object.assign({}, cloned), { type: 'HTTP', buildStrategy, runtime: types_1.CUSTOM_IMAGE_RUNTIME, imagePort: (_a = cloned.imagePort) !== null && _a !== void 0 ? _a : types_1.IMAGE_FUNCTION_PORT });
214
- if (buildStrategy === 'cloud' || buildStrategy === 'local') {
215
- normalizedImage.imageType =
216
- (_b = cloned.imageType) !== null && _b !== void 0 ? _b : (((_c = cloned.build) === null || _c === void 0 ? void 0 : _c.registryId) ? 'enterprise' : 'personal');
217
- }
214
+ normalizedImage.imageType =
215
+ (_d = (_b = cloned.imageType) !== null && _b !== void 0 ? _b : (_c = cloned.imageConfig) === null || _c === void 0 ? void 0 : _c.imageType) !== null && _d !== void 0 ? _d : (((_g = (_e = cloned.registryId) !== null && _e !== void 0 ? _e : (_f = cloned.imageConfig) === null || _f === void 0 ? void 0 : _f.registryId) !== null && _g !== void 0 ? _g : (_h = cloned.build) === null || _h === void 0 ? void 0 : _h.registryId)
216
+ ? 'enterprise'
217
+ : 'personal');
218
218
  // 镜像函数由容器启动命令决定入口,不使用 handler 与云端装依赖
219
219
  delete normalizedImage.handler;
220
220
  delete normalizedImage.installDependency;
221
221
  if (buildStrategy === 'cloud' || buildStrategy === 'local') {
222
- normalizedImage.build = Object.assign(Object.assign({}, (cloned.build || {})), { dockerfile: (_e = (_d = cloned.build) === null || _d === void 0 ? void 0 : _d.dockerfile) !== null && _e !== void 0 ? _e : types_1.DEFAULT_DOCKERFILE, platform: (_g = (_f = cloned.build) === null || _f === void 0 ? void 0 : _f.platform) !== null && _g !== void 0 ? _g : types_1.DEFAULT_IMAGE_PLATFORM, repository: (_j = (_h = cloned.build) === null || _h === void 0 ? void 0 : _h.repository) !== null && _j !== void 0 ? _j : cloned.name });
222
+ normalizedImage.build = Object.assign(Object.assign({}, (cloned.build || {})), { dockerfile: (_k = (_j = cloned.build) === null || _j === void 0 ? void 0 : _j.dockerfile) !== null && _k !== void 0 ? _k : types_1.DEFAULT_DOCKERFILE, platform: (_m = (_l = cloned.build) === null || _l === void 0 ? void 0 : _l.platform) !== null && _m !== void 0 ? _m : types_1.DEFAULT_IMAGE_PLATFORM, repository: (_p = (_o = cloned.build) === null || _o === void 0 ? void 0 : _o.repository) !== null && _p !== void 0 ? _p : cloned.name });
223
223
  }
224
224
  if (buildStrategy === 'local') {
225
225
  // 默认不静默改变构建环境
226
- normalizedImage.localFallback = (_k = cloned.localFallback) !== null && _k !== void 0 ? _k : 'error';
226
+ normalizedImage.localFallback = (_q = cloned.localFallback) !== null && _q !== void 0 ? _q : 'error';
227
227
  }
228
228
  return normalizedImage;
229
229
  }
@@ -377,13 +377,9 @@ function checkImageIdentity(raw, checks) {
377
377
  const imageConfig = raw === null || raw === void 0 ? void 0 : raw.imageConfig;
378
378
  const buildRegistryId = (_a = raw === null || raw === void 0 ? void 0 : raw.build) === null || _a === void 0 ? void 0 : _a.registryId;
379
379
  const buildStrategy = raw === null || raw === void 0 ? void 0 : raw.buildStrategy;
380
- const defaultImageType = buildStrategy === 'cloud' || buildStrategy === 'local'
381
- ? buildRegistryId
382
- ? 'enterprise'
383
- : 'personal'
384
- : 'enterprise';
385
- const imageType = (_c = (_b = raw === null || raw === void 0 ? void 0 : raw.imageType) !== null && _b !== void 0 ? _b : imageConfig === null || imageConfig === void 0 ? void 0 : imageConfig.imageType) !== null && _c !== void 0 ? _c : defaultImageType;
386
- const registryId = (_e = (_d = raw === null || raw === void 0 ? void 0 : raw.registryId) !== null && _d !== void 0 ? _d : imageConfig === null || imageConfig === void 0 ? void 0 : imageConfig.registryId) !== null && _e !== void 0 ? _e : buildRegistryId;
380
+ const registryId = (_c = (_b = raw === null || raw === void 0 ? void 0 : raw.registryId) !== null && _b !== void 0 ? _b : imageConfig === null || imageConfig === void 0 ? void 0 : imageConfig.registryId) !== null && _c !== void 0 ? _c : buildRegistryId;
381
+ const defaultImageType = registryId ? 'enterprise' : 'personal';
382
+ const imageType = (_e = (_d = raw === null || raw === void 0 ? void 0 : raw.imageType) !== null && _d !== void 0 ? _d : imageConfig === null || imageConfig === void 0 ? void 0 : imageConfig.imageType) !== null && _e !== void 0 ? _e : defaultImageType;
387
383
  if (imageType !== 'enterprise' && imageType !== 'personal') {
388
384
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_TYPE_INVALID, '镜像类型不合法', 'imageType 只能是 enterprise 或 personal'));
389
385
  return;
@@ -403,17 +399,34 @@ function checkImageIdentity(raw, checks) {
403
399
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_TYPE_INVALID, '个人版镜像不能配置企业版仓库实例 ID', '请移除 build.registryId,或将 imageType 设置为 enterprise'));
404
400
  }
405
401
  }
406
- /** 校验镜像构建目标配置 */
407
- function checkBuildTarget(raw, checks) {
408
- var _a, _b, _c;
409
- const build = raw.build;
410
- if (!lodash_1.default.isPlainObject(build)) {
411
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_CONFIG_MISSING, '缺少镜像构建配置', '请配置 build,至少包含构建上下文与 Dockerfile 信息'));
412
- return;
402
+ /** 解析构建策略使用的镜像类型 */
403
+ function resolveBuildImageType(raw, build) {
404
+ var _a, _b, _c, _d, _e, _f;
405
+ return (_c = (_a = raw.imageType) !== null && _a !== void 0 ? _a : (_b = raw.imageConfig) === null || _b === void 0 ? void 0 : _b.imageType) !== null && _c !== void 0 ? _c : (((_f = (_d = raw.registryId) !== null && _d !== void 0 ? _d : (_e = raw.imageConfig) === null || _e === void 0 ? void 0 : _e.registryId) !== null && _f !== void 0 ? _f : build.registryId) ? 'enterprise' : 'personal');
406
+ }
407
+ /** 校验构建 tag */
408
+ function checkBuildTag(raw, build) {
409
+ if (build.tag === undefined) {
410
+ return [];
413
411
  }
414
- if (raw.buildStrategy === 'cloud' && build.tag !== undefined) {
415
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_TAG_INVALID, 'cloud 策略不支持自定义镜像 tag', '请移除 build.tag,云端构建会统一使用平台生成的 CLOUDBASE_VERSION_NAME'));
412
+ if (raw.buildStrategy === 'cloud') {
413
+ return [
414
+ fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_TAG_INVALID, 'cloud 策略不支持自定义镜像 tag', '请移除 build.tag,云端构建会统一使用平台生成的 CLOUDBASE_VERSION_NAME')
415
+ ];
416
416
  }
417
+ if (typeof build.tag !== 'string' || !IMAGE_TAG_PATTERN.test(build.tag)) {
418
+ return [fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_TAG_INVALID, '镜像 tag 不合法')];
419
+ }
420
+ if (build.tag === 'latest') {
421
+ return [
422
+ fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_TAG_LATEST_FORBIDDEN, '镜像 tag 禁止使用 latest', '请使用不可变 tag,或不指定 tag 由构建器生成时间戳 tag')
423
+ ];
424
+ }
425
+ return [];
426
+ }
427
+ /** 校验构建上下文、Dockerfile 与目标平台 */
428
+ function checkBuildSource(build, checks) {
429
+ var _a, _b;
417
430
  if (build.cwd !== undefined && (typeof build.cwd !== 'string' || !build.cwd.trim())) {
418
431
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_CONTEXT_INVALID, '构建上下文目录不合法', 'build.cwd 需为非空字符串'));
419
432
  }
@@ -425,68 +438,123 @@ function checkBuildTarget(raw, checks) {
425
438
  if (platform !== types_1.DEFAULT_IMAGE_PLATFORM) {
426
439
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_PLATFORM_INVALID, `当前仅支持构建 ${types_1.DEFAULT_IMAGE_PLATFORM} 镜像`, `请移除 build.platform 或设置为 ${types_1.DEFAULT_IMAGE_PLATFORM}`));
427
440
  }
428
- if (build.tag !== undefined && raw.buildStrategy !== 'cloud') {
429
- if (typeof build.tag !== 'string' || !IMAGE_TAG_PATTERN.test(build.tag)) {
430
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_TAG_INVALID, '镜像 tag 不合法'));
431
- }
432
- else if (build.tag === 'latest') {
433
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_TAG_LATEST_FORBIDDEN, '镜像 tag 禁止使用 latest', '请使用不可变 tag,或不指定 tag 由构建器按内容摘要生成'));
441
+ }
442
+ /** 判断当前构建策略是否要求完整仓库地址 */
443
+ function requiresFullBuildRepository(raw, imageType) {
444
+ return raw.buildStrategy === 'local' ||
445
+ (raw.buildStrategy === 'cloud' && imageType === 'enterprise');
446
+ }
447
+ /** 添加仓库地址缺失或不完整的错误 */
448
+ function addFullRepositoryError(raw, checks, missing) {
449
+ const isLocal = raw.buildStrategy === 'local';
450
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID, missing
451
+ ? isLocal
452
+ ? 'local 策略缺少目标镜像仓库地址'
453
+ : 'enterprise cloud 策略缺少目标镜像仓库地址'
454
+ : isLocal
455
+ ? 'local 策略需要完整镜像仓库地址'
456
+ : 'enterprise cloud 策略需要完整镜像仓库地址', missing
457
+ ? '请配置 build.repository 为 registry/namespace/repository'
458
+ : isLocal
459
+ ? '请配置 registry/namespace/repository;当前 local 构建使用本机 Docker 登录态直接 push'
460
+ : 'P1 自动发现 TCR 实例域名完成前,请配置 registry/namespace/repository'));
461
+ }
462
+ /** 校验目标镜像仓库 */
463
+ function checkBuildRepository(raw, build, checks) {
464
+ const imageType = resolveBuildImageType(raw, build);
465
+ const requiresFullRepository = requiresFullBuildRepository(raw, imageType);
466
+ if (build.repository === undefined) {
467
+ if (requiresFullRepository) {
468
+ addFullRepositoryError(raw, checks, true);
434
469
  }
470
+ return;
435
471
  }
436
- if (build.repository !== undefined) {
437
- if (typeof build.repository !== 'string' ||
438
- !IMAGE_REPOSITORY_PATTERN.test(build.repository)) {
439
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID, '镜像仓库名不合法', '只能包含小写字母、数字与 . _ - 分隔符'));
440
- }
472
+ const repository = typeof build.repository === 'string' ? build.repository.trim() : build.repository;
473
+ const parsedRepository = typeof repository === 'string' ? parseImageReference(repository) : null;
474
+ if (!parsedRepository || parsedRepository.tag || parsedRepository.digest) {
475
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID, '镜像仓库地址不合法', 'repository 需为不含 tag digest 的镜像仓库路径'));
476
+ return;
441
477
  }
442
- if (build.buildArgs !== undefined) {
443
- if (!lodash_1.default.isPlainObject(build.buildArgs)) {
444
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, '构建参数必须是对象'));
445
- }
446
- else {
447
- Object.entries(build.buildArgs).forEach(([key, value]) => {
448
- if (!ENV_KEY_PATTERN.test(key)) {
449
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, `构建参数名不合法:${key}`));
450
- }
451
- if (typeof value !== 'string') {
452
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, `构建参数 ${key} 的值必须是字符串`));
453
- }
454
- if (SENSITIVE_KEY_PATTERN.test(key)) {
455
- const message = `构建参数 ${key} 可能包含敏感信息`;
456
- const remediation = '构建参数会写入镜像构建记录,请改用运行时环境变量或密钥管理';
457
- checks.push(raw.buildStrategy === 'cloud'
458
- ? fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, message, remediation)
459
- : warn(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, message, remediation));
460
- }
461
- });
462
- }
478
+ const repositoryPath = parsedRepository.repository;
479
+ if (requiresFullRepository &&
480
+ (!parsedRepository.registry || !repositoryPath.includes('/'))) {
481
+ addFullRepositoryError(raw, checks, false);
482
+ }
483
+ }
484
+ /** 校验单个 Docker build arg */
485
+ function checkBuildArg(raw, buildArg, checks) {
486
+ const [key, value] = buildArg;
487
+ if (!ENV_KEY_PATTERN.test(key)) {
488
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, `构建参数名不合法:${key}`));
489
+ }
490
+ if (typeof value !== 'string') {
491
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, `构建参数 ${key} 的值必须是字符串`));
492
+ }
493
+ if (SENSITIVE_KEY_PATTERN.test(key)) {
494
+ const message = `构建参数 ${key} 可能包含敏感信息`;
495
+ const remediation = '构建参数会写入镜像构建记录,请改用运行时环境变量或密钥管理';
496
+ checks.push(raw.buildStrategy === 'cloud'
497
+ ? fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, message, remediation)
498
+ : warn(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, message, remediation));
499
+ }
500
+ }
501
+ /** 校验 Docker build args */
502
+ function checkBuildArgs(raw, build, checks) {
503
+ if (build.buildArgs === undefined) {
504
+ return;
505
+ }
506
+ if (!lodash_1.default.isPlainObject(build.buildArgs)) {
507
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_ARG_INVALID, '构建参数必须是对象'));
508
+ return;
509
+ }
510
+ Object.entries(build.buildArgs).forEach(buildArg => {
511
+ checkBuildArg(raw, buildArg, checks);
512
+ });
513
+ }
514
+ /** 校验个人版 TCR 云端推送凭证 */
515
+ function checkPersonalRegistryCredential(registryCredential, checks) {
516
+ if (!lodash_1.default.isPlainObject(registryCredential)) {
517
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING, '个人版 TCR 云端构建缺少推送凭证', '请在 build.registryCredential 中显式声明 username/password,' +
518
+ '推荐使用 {{env.TCB_TCR_USERNAME}} 与 {{env.TCB_TCR_PASSWORD}}'));
519
+ return;
520
+ }
521
+ const username = registryCredential.username;
522
+ const password = registryCredential.password;
523
+ if (typeof username !== 'string' || !/^\d{5,20}$/.test(username.trim())) {
524
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, '个人版 TCR 登录用户名不合法', 'username 需为腾讯云账号 ID(UIN),推荐使用 {{env.TCB_TCR_USERNAME}}'));
525
+ }
526
+ if (typeof password !== 'string' || !password || password.length > 16 * 1024) {
527
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, '个人版 TCR 固定密码未配置或长度不合法', 'password 必须显式声明且不能为空,推荐使用 {{env.TCB_TCR_PASSWORD}}'));
463
528
  }
529
+ }
530
+ /** 校验镜像仓库命名空间与推送凭证 */
531
+ function checkBuildRegistry(raw, build, checks) {
532
+ const imageType = resolveBuildImageType(raw, build);
464
533
  const registryCredential = build.registryCredential;
465
- const imageType = (_c = raw.imageType) !== null && _c !== void 0 ? _c : (build.registryId ? 'enterprise' : 'personal');
466
534
  if (raw.buildStrategy === 'cloud' && imageType === 'personal') {
467
535
  if (typeof build.namespace !== 'string' ||
468
536
  !build.namespace.trim() ||
469
537
  !IMAGE_REPOSITORY_PATTERN.test(build.namespace.trim())) {
470
538
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID, '个人版 TCR 命名空间未配置或不合法', '请在 build.namespace 中显式声明,推荐使用 {{env.TCB_TCR_NAMESPACE}}'));
471
539
  }
472
- if (!lodash_1.default.isPlainObject(registryCredential)) {
473
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING, '个人版 TCR 云端构建缺少推送凭证', '请在 build.registryCredential 中显式声明 username/password,' +
474
- '推荐使用 {{env.TCB_TCR_USERNAME}} 与 {{env.TCB_TCR_PASSWORD}}'));
475
- }
476
- else {
477
- const username = registryCredential.username;
478
- const password = registryCredential.password;
479
- if (typeof username !== 'string' || !/^\d{5,20}$/.test(username.trim())) {
480
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, '个人版 TCR 登录用户名不合法', 'username 需为腾讯云账号 ID(UIN),推荐使用 {{env.TCB_TCR_USERNAME}}'));
481
- }
482
- if (typeof password !== 'string' || !password || password.length > 16 * 1024) {
483
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, '个人版 TCR 固定密码未配置或长度不合法', 'password 必须显式声明且不能为空,推荐使用 {{env.TCB_TCR_PASSWORD}}'));
484
- }
485
- }
540
+ checkPersonalRegistryCredential(registryCredential, checks);
486
541
  }
487
542
  else if (registryCredential !== undefined) {
488
543
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, 'registryCredential 仅支持 cloud + personal 构建', '企业版使用 STS 临时凭证换取 token;local 构建使用本机 Docker 登录态'));
489
544
  }
545
+ }
546
+ /** 校验镜像构建目标配置 */
547
+ function checkBuildTarget(raw, checks) {
548
+ const build = raw.build;
549
+ if (!lodash_1.default.isPlainObject(build)) {
550
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_CONFIG_MISSING, '缺少镜像构建配置', '请配置 build,至少包含构建上下文与 Dockerfile 信息'));
551
+ return;
552
+ }
553
+ checks.push(...checkBuildTag(raw, build));
554
+ checkBuildSource(build, checks);
555
+ checkBuildRepository(raw, build, checks);
556
+ checkBuildArgs(raw, build, checks);
557
+ checkBuildRegistry(raw, build, checks);
490
558
  if (raw.imageConfig !== undefined) {
491
559
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_CONFIG_NOT_ALLOWED, `buildStrategy 为 ${raw.buildStrategy} 时不能指定 imageConfig`, '镜像地址将由构建结果决定,如需直接部署已有镜像请使用 buildStrategy: image'));
492
560
  }
@@ -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
  });
@@ -1141,7 +1167,7 @@ class FunctionService {
1141
1167
  * @returns {Promise<void>}
1142
1168
  */
1143
1169
  async downloadFunctionCode(options) {
1144
- const { destPath, envId, functionName, codeSecret } = options;
1170
+ const { destPath, envId, functionName, codeSecret, qualifier } = options;
1145
1171
  // 检验路径是否存在
1146
1172
  (0, utils_1.checkFullAccess)(destPath, true);
1147
1173
  // 获取下载链接:ENABLE_TCBF 开启走 tcb 的 DownloadFunction,否则走 scf 的 GetFunctionAddress
@@ -1150,6 +1176,10 @@ class FunctionService {
1150
1176
  FunctionName: functionName,
1151
1177
  [this.namespaceParamKey]: envId
1152
1178
  };
1179
+ // 指定版本时透传 Qualifier;默认 $LATEST 与现状一致
1180
+ if (qualifier) {
1181
+ params.Qualifier = qualifier;
1182
+ }
1153
1183
  if (codeSecret) {
1154
1184
  params.CodeSecret = codeSecret;
1155
1185
  }
@@ -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',
@@ -190,10 +190,12 @@ class ProjectValidator {
190
190
  return (config.functions || []).some((func) => this.isPersonalCloudFunction(func));
191
191
  }
192
192
  isPersonalCloudFunction(func) {
193
- var _a;
193
+ var _a, _b, _c, _d, _e, _f;
194
+ const registryId = (_b = (_a = func.imageConfig) === null || _a === void 0 ? void 0 : _a.registryId) !== null && _b !== void 0 ? _b : (_d = (_c = func.imageConfig) === null || _c === void 0 ? void 0 : _c.build) === null || _d === void 0 ? void 0 : _d.registryId;
195
+ const imageType = (_f = (_e = func.imageConfig) === null || _e === void 0 ? void 0 : _e.imageType) !== null && _f !== void 0 ? _f : (registryId ? 'enterprise' : 'personal');
194
196
  return (func.type === 'HTTP' &&
195
197
  func.buildStrategy === 'cloud' &&
196
- ((_a = func.imageConfig) === null || _a === void 0 ? void 0 : _a.imageType) === 'personal');
198
+ imageType === 'personal');
197
199
  }
198
200
  validateResources(cwd, config) {
199
201
  const errors = [];
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.2",
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.2",
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
  */
@@ -58,6 +58,8 @@ export interface IDeployState {
58
58
  version: number;
59
59
  updatedAt: string;
60
60
  gitCommit?: string;
61
+ /** 写入快照时的目标环境 ID:跨环境切换(如上海 → 新加坡)后旧快照不可信,skip 判定强制失效 */
62
+ envId?: string;
61
63
  resources: {
62
64
  hosting?: IHostingStateEntry[];
63
65
  gateway?: unknown;
@@ -102,6 +104,8 @@ export declare class StateStore {
102
104
  write(state: {
103
105
  resources: IDeployState['resources'];
104
106
  gitCommit?: string;
107
+ /** 本次部署的目标环境 ID(用于跨环境切换时使旧快照 skip 判定失效) */
108
+ envId?: string;
105
109
  }): void;
106
110
  /**
107
111
  * 计算产物目录逐文件 sha256 指纹(仅 hash,兼容旧调用)
@@ -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 {};