@cloudbase/manager-node 5.8.2 → 5.8.3

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.
@@ -29,7 +29,7 @@ const IMAGE_TAG_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/;
29
29
  /** 镜像 digest 规则 */
30
30
  const IMAGE_DIGEST_PATTERN = /^[a-z0-9]+(?:[.+_-][a-z0-9]+)*:[a-fA-F0-9]{32,}$/;
31
31
  /** 镜像仓库路径规则 */
32
- const IMAGE_REPOSITORY_PATTERN = /^[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*(?:\/[a-z0-9]+(?:(?:[._]|__|-+)[a-z0-9]+)*)*$/;
32
+ const IMAGE_REPOSITORY_PATTERN = /^[a-z0-9][a-z0-9._-]*(?:\/[a-z0-9][a-z0-9._-]*)*$/;
33
33
  /** 网关路径字符白名单,避免注入与歧义字符 */
34
34
  const GATEWAY_PATH_PATTERN = /^[A-Za-z0-9\-._~%/:@]+$/;
35
35
  /** 环境变量与构建参数名规则 */
@@ -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, _l, _m, _o, _p, _q;
185
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
186
186
  if (!config || typeof config !== 'object') {
187
187
  throw new error_1.CloudBaseError('部署配置必须是对象', {
188
188
  code: types_1.FUNCTION_DEPLOY_ERROR.CONFIG_INVALID
@@ -219,11 +219,11 @@ function normalizeDeployConfig(config) {
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: (_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 });
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 });
223
223
  }
224
224
  if (buildStrategy === 'local') {
225
225
  // 默认不静默改变构建环境
226
- normalizedImage.localFallback = (_q = cloned.localFallback) !== null && _q !== void 0 ? _q : 'error';
226
+ normalizedImage.localFallback = (_o = cloned.localFallback) !== null && _o !== void 0 ? _o : 'error';
227
227
  }
228
228
  return normalizedImage;
229
229
  }
@@ -378,21 +378,23 @@ function checkImageIdentity(raw, checks) {
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
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;
381
+ const explicitImageType = (_d = raw === null || raw === void 0 ? void 0 : raw.imageType) !== null && _d !== void 0 ? _d : imageConfig === null || imageConfig === void 0 ? void 0 : imageConfig.imageType;
382
+ const imageType = resolveBuildImageType(raw, (_e = raw === null || raw === void 0 ? void 0 : raw.build) !== null && _e !== void 0 ? _e : {});
383
383
  if (imageType !== 'enterprise' && imageType !== 'personal') {
384
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_TYPE_INVALID, '镜像类型不合法', 'imageType 只能是 enterprise 或 personal'));
384
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_TYPE_INVALID, buildStrategy === 'cloud' || buildStrategy === 'local'
385
+ ? `${buildStrategy} 策略必须显式声明镜像类型`
386
+ : '镜像类型不合法', '请配置 imageConfig.imageType 为 enterprise 或 personal'));
385
387
  return;
386
388
  }
387
389
  if (imageType === 'enterprise') {
388
- const registryField = buildStrategy === 'cloud' || buildStrategy === 'local'
389
- ? 'build.registryId'
390
- : 'registryId';
391
- if (registryId === undefined || registryId === null) {
392
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_MISSING, '企业版镜像未配置仓库实例 ID', `请配置 ${registryField};如果当前没有企业版 TCR 实例,请将 imageType 设置为 personal 并改用个人版仓库配置`));
390
+ if (buildStrategy === 'image' && (registryId === undefined || registryId === null)) {
391
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_MISSING, '企业版镜像未配置仓库实例 ID', '请配置 imageConfig.registryId 或顶层 registryId'));
393
392
  }
394
- else if (typeof registryId !== 'string' || !registryId.trim()) {
395
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_INVALID, '企业版镜像仓库实例 ID 无效', `${registryField} 已声明但解析结果为空;若使用 {{env.*}},请检查对应环境变量已赋值为实际企业版 TCR 实例 ID。没有企业版实例时请改用 imageType=personal`));
393
+ else if (registryId !== undefined && (typeof registryId !== 'string' || !registryId.trim())) {
394
+ const registryField = buildStrategy === 'cloud' || buildStrategy === 'local'
395
+ ? 'build.registryId'
396
+ : 'registryId';
397
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_INVALID, '企业版镜像仓库实例 ID 无效', `${registryField} 已声明但解析结果为空;若使用 {{env.*}},请检查对应环境变量已赋值为实际企业版 TCR 实例 ID`));
396
398
  }
397
399
  }
398
400
  else if (registryId !== undefined) {
@@ -402,7 +404,11 @@ function checkImageIdentity(raw, checks) {
402
404
  /** 解析构建策略使用的镜像类型 */
403
405
  function resolveBuildImageType(raw, build) {
404
406
  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');
407
+ 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 : (raw.buildStrategy === 'local' || raw.buildStrategy === 'cloud'
408
+ ? 'enterprise'
409
+ : ((_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)
410
+ ? 'enterprise'
411
+ : 'personal');
406
412
  }
407
413
  /** 校验构建 tag */
408
414
  function checkBuildTag(raw, build) {
@@ -441,8 +447,9 @@ function checkBuildSource(build, checks) {
441
447
  }
442
448
  /** 判断当前构建策略是否要求完整仓库地址 */
443
449
  function requiresFullBuildRepository(raw, imageType) {
444
- return raw.buildStrategy === 'local' ||
445
- (raw.buildStrategy === 'cloud' && imageType === 'enterprise');
450
+ var _a;
451
+ return raw.buildStrategy === 'local' && imageType === 'personal' &&
452
+ ((_a = raw.build) === null || _a === void 0 ? void 0 : _a.repository) !== undefined;
446
453
  }
447
454
  /** 添加仓库地址缺失或不完整的错误 */
448
455
  function addFullRepositoryError(raw, checks, missing) {
@@ -470,14 +477,21 @@ function checkBuildRepository(raw, build, checks) {
470
477
  return;
471
478
  }
472
479
  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) {
480
+ const isShortRepository = typeof repository === 'string' &&
481
+ IMAGE_REPOSITORY_PATTERN.test(repository) &&
482
+ !repository.split('/')[0].includes('.') &&
483
+ !repository.split('/')[0].includes(':');
484
+ const parsedRepository = typeof repository === 'string' && !isShortRepository
485
+ ? parseImageReference(repository)
486
+ : null;
487
+ if (!isShortRepository && (!parsedRepository || parsedRepository.tag || parsedRepository.digest)) {
475
488
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID, '镜像仓库地址不合法', 'repository 需为不含 tag 或 digest 的镜像仓库路径'));
476
489
  return;
477
490
  }
478
- const repositoryPath = parsedRepository.repository;
491
+ const repositoryPath = isShortRepository ? repository : parsedRepository.repository;
479
492
  if (requiresFullRepository &&
480
- (!parsedRepository.registry || !repositoryPath.includes('/'))) {
493
+ (!(parsedRepository === null || parsedRepository === void 0 ? void 0 : parsedRepository.registry) || !repositoryPath.includes('/')) &&
494
+ repository !== raw.name) {
481
495
  addFullRepositoryError(raw, checks, false);
482
496
  }
483
497
  }
@@ -511,10 +525,10 @@ function checkBuildArgs(raw, build, checks) {
511
525
  checkBuildArg(raw, buildArg, checks);
512
526
  });
513
527
  }
514
- /** 校验个人版 TCR 云端推送凭证 */
528
+ /** 校验个人版 TCR 推送凭证 */
515
529
  function checkPersonalRegistryCredential(registryCredential, checks) {
516
530
  if (!lodash_1.default.isPlainObject(registryCredential)) {
517
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING, '个人版 TCR 云端构建缺少推送凭证', '请在 build.registryCredential 中显式声明 username/password,' +
531
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING, '个人版 TCR 构建缺少推送凭证', '请在 build.registryCredential 中显式声明 username/password,' +
518
532
  '推荐使用 {{env.TCB_TCR_USERNAME}} 与 {{env.TCB_TCR_PASSWORD}}'));
519
533
  return;
520
534
  }
@@ -527,20 +541,16 @@ function checkPersonalRegistryCredential(registryCredential, checks) {
527
541
  checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, '个人版 TCR 固定密码未配置或长度不合法', 'password 必须显式声明且不能为空,推荐使用 {{env.TCB_TCR_PASSWORD}}'));
528
542
  }
529
543
  }
530
- /** 校验镜像仓库命名空间与推送凭证 */
544
+ /** 校验镜像仓库配置 */
531
545
  function checkBuildRegistry(raw, build, checks) {
532
546
  const imageType = resolveBuildImageType(raw, build);
533
547
  const registryCredential = build.registryCredential;
534
- if (raw.buildStrategy === 'cloud' && imageType === 'personal') {
535
- if (typeof build.namespace !== 'string' ||
536
- !build.namespace.trim() ||
537
- !IMAGE_REPOSITORY_PATTERN.test(build.namespace.trim())) {
538
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID, '个人版 TCR 命名空间未配置或不合法', '请在 build.namespace 中显式声明,推荐使用 {{env.TCB_TCR_NAMESPACE}}'));
539
- }
548
+ if (imageType === 'personal') {
540
549
  checkPersonalRegistryCredential(registryCredential, checks);
550
+ return;
541
551
  }
542
- else if (registryCredential !== undefined) {
543
- checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, 'registryCredential 仅支持 cloud + personal 构建', '企业版使用 STS 临时凭证换取 token;local 构建使用本机 Docker 登录态'));
552
+ if (registryCredential !== undefined) {
553
+ checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, 'registryCredential 仅支持 personal 镜像构建', '企业版使用 STS 临时凭证换取 token;personal local/cloud 必须显式提供 username/password'));
544
554
  }
545
555
  }
546
556
  /** 校验镜像构建目标配置 */
@@ -0,0 +1,194 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.prepareEnterpriseTcrTarget = prepareEnterpriseTcrTarget;
4
+ exports.createEnterpriseTcrCredential = createEnterpriseTcrCredential;
5
+ exports.createTcrApiService = createTcrApiService;
6
+ const error_1 = require("../../error");
7
+ const types_1 = require("../types");
8
+ const config_guard_1 = require("./config-guard");
9
+ const TCR_API_VERSION = '2019-09-24';
10
+ const NAMESPACE_PATTERN = /^[a-z0-9](?:[a-z0-9]|[._-](?![._-])){0,28}[a-z0-9]$/;
11
+ function responseOf(result) {
12
+ return (result === null || result === void 0 ? void 0 : result.Response) || result || {};
13
+ }
14
+ function requestIdOf(result) {
15
+ var _a;
16
+ const requestId = (_a = responseOf(result)) === null || _a === void 0 ? void 0 : _a.RequestId;
17
+ return typeof requestId === 'string' && requestId ? requestId : undefined;
18
+ }
19
+ function errorRequestId(error) {
20
+ var _a, _b, _c;
21
+ return (error === null || error === void 0 ? void 0 : error.requestId) || ((_a = error === null || error === void 0 ? void 0 : error.original) === null || _a === void 0 ? void 0 : _a.requestId) || ((_c = (_b = error === null || error === void 0 ? void 0 : error.original) === null || _b === void 0 ? void 0 : _b.Response) === null || _c === void 0 ? void 0 : _c.RequestId);
22
+ }
23
+ function throwTcrError(message, code, requestId) {
24
+ throw new error_1.CloudBaseError(message, Object.assign({ code }, (requestId ? { requestId } : {})));
25
+ }
26
+ function assertNamespace(namespace) {
27
+ if (!NAMESPACE_PATTERN.test(namespace)) {
28
+ throwTcrError(`企业版 TCR 命名空间不合法:${namespace}。名称需为 2-30 位小写字母、数字、点、下划线或中划线,且不能以分隔符开头、结尾或连续出现。`, types_1.FUNCTION_DEPLOY_ERROR.TCR_NAMESPACE_INVALID);
29
+ }
30
+ }
31
+ function parseRepositoryTarget(repository) {
32
+ const parsed = (0, config_guard_1.parseImageReference)(repository);
33
+ if (!parsed || parsed.tag || parsed.digest) {
34
+ throwTcrError('企业版 TCR repository 不合法,需为 repository、namespace/repository 或 registry/namespace/repository,且不能包含 tag 或 digest。', types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID);
35
+ }
36
+ const parts = parsed.repository.split('/');
37
+ if (parsed.registry) {
38
+ if (parts.length < 2) {
39
+ throwTcrError('完整企业版 TCR repository 必须包含 namespace/repository。', types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID);
40
+ }
41
+ return {
42
+ registry: parsed.registry,
43
+ namespace: parts.slice(0, -1).join('/'),
44
+ repository: parts[parts.length - 1]
45
+ };
46
+ }
47
+ if (parts.length >= 2) {
48
+ return {
49
+ namespace: parts.slice(0, -1).join('/'),
50
+ repository: parts[parts.length - 1]
51
+ };
52
+ }
53
+ return { repository: parts[0] };
54
+ }
55
+ function isRunningRegistry(registry) {
56
+ return !registry.Status || registry.Status.toLowerCase() === 'running';
57
+ }
58
+ async function resolveRegistry(service, region, explicitRegistryId) {
59
+ let result;
60
+ try {
61
+ result = await service.call({
62
+ Action: 'DescribeInstances',
63
+ Param: Object.assign({ AllRegion: true, Limit: 100 }, (explicitRegistryId ? { Registryids: [explicitRegistryId] } : {}))
64
+ });
65
+ }
66
+ catch (error) {
67
+ throw new error_1.CloudBaseError('查询企业版 TCR 实例失败,请确认当前身份具备 DescribeInstances 权限。', {
68
+ code: types_1.FUNCTION_DEPLOY_ERROR.TCR_INSTANCE_DISCOVERY_FAILED,
69
+ requestId: errorRequestId(error) || ''
70
+ });
71
+ }
72
+ const response = responseOf(result);
73
+ const registries = Array.isArray(response.Registries)
74
+ ? response.Registries
75
+ : Array.isArray(response.InstanceSet)
76
+ ? response.InstanceSet
77
+ : [];
78
+ const matching = registries.filter(item => {
79
+ const registryId = item.RegistryId || item.InstanceId;
80
+ const registryRegion = item.RegionName || item.Region;
81
+ return ((!explicitRegistryId || registryId === explicitRegistryId) &&
82
+ registryRegion === region &&
83
+ isRunningRegistry(item));
84
+ });
85
+ const requestId = requestIdOf(result);
86
+ if (matching.length === 0) {
87
+ const message = explicitRegistryId
88
+ ? `未找到地域 ${region} 内可用的企业版 TCR 实例 ${explicitRegistryId},请确认实例 ID、状态及其与 SCF 的地域一致。`
89
+ : `地域 ${region} 内没有可用的企业版 TCR 实例,请先创建实例或显式指定同地域 registryId。`;
90
+ throwTcrError(message, types_1.FUNCTION_DEPLOY_ERROR.TCR_INSTANCE_NOT_FOUND, requestId);
91
+ }
92
+ if (matching.length > 1) {
93
+ throwTcrError(`地域 ${region} 内发现多个可用的企业版 TCR 实例,请通过 imageConfig.registryId 显式指定目标实例。`, types_1.FUNCTION_DEPLOY_ERROR.TCR_INSTANCE_AMBIGUOUS, requestId);
94
+ }
95
+ const selected = matching[0];
96
+ const registryId = selected.RegistryId || selected.InstanceId;
97
+ const registry = selected.PublicDomain || selected.RegistryDomain || selected.RegistryDomainName;
98
+ if (!registryId || !registry) {
99
+ throwTcrError('企业版 TCR 实例信息缺少 RegistryId 或 PublicDomain,无法生成镜像地址。', types_1.FUNCTION_DEPLOY_ERROR.TCR_INSTANCE_INVALID, requestId);
100
+ }
101
+ return { registryId, registry };
102
+ }
103
+ function isNamespaceConflict(error) {
104
+ var _a, _b;
105
+ const code = String((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) || '');
106
+ return /ResourceConflict|AlreadyExists|ResourceInUse/i.test(code);
107
+ }
108
+ async function ensureNamespace(service, registryId, namespace) {
109
+ var _a;
110
+ let describeResult;
111
+ try {
112
+ describeResult = await service.call({
113
+ Action: 'DescribeNamespaces',
114
+ Param: { RegistryId: registryId, NamespaceName: namespace, Limit: 1, Offset: 0 }
115
+ });
116
+ }
117
+ catch (error) {
118
+ throw new error_1.CloudBaseError(`查询企业版 TCR 命名空间 ${namespace} 失败。`, {
119
+ code: types_1.FUNCTION_DEPLOY_ERROR.TCR_NAMESPACE_QUERY_FAILED,
120
+ requestId: errorRequestId(error) || ''
121
+ });
122
+ }
123
+ const namespaceList = (_a = responseOf(describeResult)) === null || _a === void 0 ? void 0 : _a.NamespaceList;
124
+ if (Array.isArray(namespaceList) && namespaceList.some(item => (item === null || item === void 0 ? void 0 : item.Name) === namespace)) {
125
+ return;
126
+ }
127
+ try {
128
+ await service.call({
129
+ Action: 'CreateNamespace',
130
+ Param: { RegistryId: registryId, NamespaceName: namespace, IsPublic: false }
131
+ });
132
+ }
133
+ catch (error) {
134
+ if (isNamespaceConflict(error)) {
135
+ return;
136
+ }
137
+ throw new error_1.CloudBaseError(`创建企业版 TCR 命名空间 ${namespace} 失败。`, {
138
+ code: types_1.FUNCTION_DEPLOY_ERROR.TCR_NAMESPACE_CREATE_FAILED,
139
+ requestId: errorRequestId(error) || ''
140
+ });
141
+ }
142
+ }
143
+ async function prepareEnterpriseTcrTarget(config, options) {
144
+ var _a;
145
+ const configuredRegistryId = (_a = config.registryId) !== null && _a !== void 0 ? _a : config.build.registryId;
146
+ const explicitRegistryId = typeof configuredRegistryId === 'string' && configuredRegistryId.trim()
147
+ ? configuredRegistryId.trim()
148
+ : undefined;
149
+ const resolvedRegistry = await resolveRegistry(options.service, options.region, explicitRegistryId);
150
+ const repositoryTarget = parseRepositoryTarget(config.build.repository || config.name);
151
+ const namespace = (config.build.namespace || repositoryTarget.namespace || options.envId).trim();
152
+ assertNamespace(namespace);
153
+ if (config.build.namespace && repositoryTarget.namespace && config.build.namespace !== repositoryTarget.namespace) {
154
+ throwTcrError('build.namespace 与 build.repository 中的命名空间不一致。', types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID);
155
+ }
156
+ await ensureNamespace(options.service, resolvedRegistry.registryId, namespace);
157
+ const registry = repositoryTarget.registry || resolvedRegistry.registry;
158
+ const repository = repositoryTarget.repository || config.name;
159
+ const imageRepository = `${registry}/${namespace}/${repository}`;
160
+ const preparedConfig = Object.assign(Object.assign({}, config), { imageType: 'enterprise', registryId: resolvedRegistry.registryId, build: Object.assign(Object.assign({}, config.build), { registryId: resolvedRegistry.registryId, namespace, repository: imageRepository }) });
161
+ return {
162
+ config: preparedConfig,
163
+ target: {
164
+ registryId: resolvedRegistry.registryId,
165
+ registry,
166
+ namespace,
167
+ repository,
168
+ imageRepository
169
+ }
170
+ };
171
+ }
172
+ async function createEnterpriseTcrCredential(service, registryId) {
173
+ let result;
174
+ try {
175
+ result = await service.call({
176
+ Action: 'CreateInstanceToken',
177
+ Param: { RegistryId: registryId, TokenType: 'temp' }
178
+ });
179
+ }
180
+ catch (error) {
181
+ throw new error_1.CloudBaseError('获取企业版 TCR 临时登录凭证失败,请确认当前身份具备 CreateInstanceToken 权限。', {
182
+ code: types_1.FUNCTION_DEPLOY_ERROR.TCR_TOKEN_CREATE_FAILED,
183
+ requestId: errorRequestId(error) || ''
184
+ });
185
+ }
186
+ const response = responseOf(result);
187
+ if (typeof response.Username !== 'string' || !response.Username || typeof response.Token !== 'string' || !response.Token) {
188
+ throwTcrError('CreateInstanceToken 未返回有效的 Username 或 Token。', types_1.FUNCTION_DEPLOY_ERROR.TCR_TOKEN_INVALID, requestIdOf(result));
189
+ }
190
+ return { username: response.Username, token: response.Token };
191
+ }
192
+ function createTcrApiService(environment) {
193
+ return environment.getCommonService('tcr', TCR_API_VERSION);
194
+ }
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveImageTag = resolveImageTag;
4
+ const child_process_1 = require("child_process");
5
+ const util_1 = require("util");
6
+ const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
7
+ async function readGitShortSha(cwd) {
8
+ try {
9
+ const { stdout } = await execFileAsync('git', ['rev-parse', '--short=7', 'HEAD'], { cwd });
10
+ const sha = stdout.trim();
11
+ return /^[0-9a-f]{7}$/i.test(sha) ? sha.toLowerCase() : undefined;
12
+ }
13
+ catch (_) {
14
+ return undefined;
15
+ }
16
+ }
17
+ /**
18
+ * 生成镜像 tag:显式配置优先,其次 Git short SHA,最后回退到毫秒时间戳。
19
+ */
20
+ async function resolveImageTag(options) {
21
+ var _a;
22
+ const explicitTag = (_a = options.explicitTag) === null || _a === void 0 ? void 0 : _a.trim();
23
+ if (explicitTag) {
24
+ return explicitTag;
25
+ }
26
+ const gitShortSha = await (options.readGitShortSha || readGitShortSha)(options.cwd);
27
+ if (gitShortSha) {
28
+ return gitShortSha;
29
+ }
30
+ return String((options.now || Date.now)());
31
+ }
@@ -3,12 +3,18 @@ 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.buildLocalImage = buildLocalImage;
7
+ exports.pushLocalImage = pushLocalImage;
8
+ exports.loginDockerRegistry = loginDockerRegistry;
9
+ exports.createTemporaryDockerConfig = createTemporaryDockerConfig;
6
10
  exports.buildAndPushImage = buildAndPushImage;
7
11
  const child_process_1 = require("child_process");
8
12
  const fs_1 = __importDefault(require("fs"));
13
+ const os_1 = __importDefault(require("os"));
9
14
  const path_1 = __importDefault(require("path"));
10
15
  const error_1 = require("../../error");
11
16
  const types_1 = require("../types");
17
+ const image_tag_1 = require("./image-tag");
12
18
  /**
13
19
  * 本地镜像构建与推送(local 策略,个人版先行版)
14
20
  *
@@ -24,15 +30,26 @@ const types_1 = require("../types");
24
30
  /** 构建/推送命令超时(毫秒),镜像构建较慢,给足时间 */
25
31
  const BUILD_TIMEOUT = 10 * 60 * 1000;
26
32
  /** 默认命令执行器:固定参数数组、无 shell、带超时与最大缓冲 */
27
- function defaultRunCommand(command, args) {
33
+ function defaultRunCommand(command, args, options = {}) {
28
34
  return new Promise(resolve => {
29
- (0, child_process_1.execFile)(command, args, { timeout: BUILD_TIMEOUT, shell: false, windowsHide: true, maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => {
35
+ var _a, _b;
36
+ const child = (0, child_process_1.execFile)(command, args, {
37
+ timeout: BUILD_TIMEOUT,
38
+ shell: false,
39
+ windowsHide: true,
40
+ maxBuffer: 32 * 1024 * 1024,
41
+ env: options.env ? Object.assign(Object.assign({}, process.env), options.env) : process.env
42
+ }, (error, stdout, stderr) => {
30
43
  resolve({
31
44
  ok: !error,
32
45
  stdout: (stdout || '').toString(),
33
46
  stderr: (stderr || '').toString()
34
47
  });
35
48
  });
49
+ if (options.input !== undefined) {
50
+ (_a = child.stdin) === null || _a === void 0 ? void 0 : _a.write(options.input);
51
+ (_b = child.stdin) === null || _b === void 0 ? void 0 : _b.end();
52
+ }
36
53
  });
37
54
  }
38
55
  /** 默认依赖:真实命令执行与 fs 检查 */
@@ -48,15 +65,14 @@ function resolveContext(config) {
48
65
  return path_1.default.resolve(cwd);
49
66
  }
50
67
  /** 解析目标镜像完整引用 imageUri(repository 需为含域名与命名空间的完整仓库地址,个人版/企业版通用) */
51
- function resolveImageUri(config) {
68
+ async function resolveImageUri(config, context) {
52
69
  const repository = config.build.repository;
53
70
  if (typeof repository !== 'string' || !repository.trim()) {
54
71
  throw new error_1.CloudBaseError('local 策略缺少目标镜像仓库地址 build.repository', {
55
72
  code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_REPOSITORY_MISSING
56
73
  });
57
74
  }
58
- // tag 缺省用时间戳,避免 latest;不可变 tag 便于回溯
59
- const tag = config.build.tag || `v${Date.now()}`;
75
+ const tag = await (0, image_tag_1.resolveImageTag)({ explicitTag: config.build.tag, cwd: context });
60
76
  return `${repository.trim()}:${tag}`;
61
77
  }
62
78
  /** 从 buildx imagetools inspect 输出解析 digest */
@@ -80,15 +96,10 @@ function formatBuildCommandForLog(args) {
80
96
  }
81
97
  return `docker ${displayArgs.join(' ')}`;
82
98
  }
83
- /**
84
- * 本地构建镜像并推送到目标仓库
85
- *
86
- * @param config 规范化的 local 部署配置
87
- * @param onLog 可选日志回调,用于透出构建输出(不含凭证)
88
- * @param deps 可注入依赖,默认真实命令与 fs;测试可传 mock
89
- * @returns 最终镜像引用与 digest
90
- */
91
- async function buildAndPushImage(config, onLog, deps = defaultDeps) {
99
+ /** 本地构建镜像,不执行推送。 */
100
+ async function buildLocalImage(config, onLog, options = {}) {
101
+ const deps = options.deps || defaultDeps;
102
+ const commandOptions = options.commandOptions || {};
92
103
  const context = resolveContext(config);
93
104
  if (!deps.existsSync(context) || !deps.isDirectory(context)) {
94
105
  throw new error_1.CloudBaseError(`构建上下文目录不存在:${context}`, {
@@ -102,28 +113,162 @@ async function buildAndPushImage(config, onLog, deps = defaultDeps) {
102
113
  code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_DOCKERFILE_MISSING
103
114
  });
104
115
  }
105
- const imageUri = resolveImageUri(config);
116
+ const imageUri = await resolveImageUri(config, context);
106
117
  const platform = config.build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
107
- // 组装 buildx 参数:构建目标平台镜像并直接推送
108
- const args = ['buildx', 'build', '--platform', platform, '-f', dockerfilePath, '-t', imageUri];
118
+ const args = [
119
+ 'buildx',
120
+ 'build',
121
+ '--platform',
122
+ platform,
123
+ '-f',
124
+ dockerfilePath,
125
+ '-t',
126
+ imageUri,
127
+ '--load'
128
+ ];
109
129
  if (config.build.forceBuild) {
110
130
  args.push('--no-cache');
111
131
  }
112
- // 追加构建参数(键值均作为独立参数,无 shell 拼接)
113
132
  const buildArgs = config.build.buildArgs || {};
114
133
  for (const key of Object.keys(buildArgs)) {
115
134
  args.push('--build-arg', `${key}=${buildArgs[key]}`);
116
135
  }
117
- args.push('--push', context);
136
+ args.push(context);
118
137
  onLog === null || onLog === void 0 ? void 0 : onLog(`执行 ${formatBuildCommandForLog(args)}`);
119
- const build = await deps.runCommand('docker', args);
138
+ const buildStartedAt = Date.now();
139
+ const build = await deps.runCommand('docker', args, commandOptions);
140
+ const buildDurationMs = Date.now() - buildStartedAt;
120
141
  if (!build.ok) {
121
- throw new error_1.CloudBaseError('镜像构建或推送失败,请查看 Docker 构建输出定位具体原因', {
122
- code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_PUSH_FAILED
142
+ throw new error_1.CloudBaseError('镜像构建失败,请检查 Dockerfile、构建上下文和本地 Docker 输出。', {
143
+ code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_FAILED
123
144
  });
124
145
  }
125
- // 查询远端 digest,产出不可变引用
126
- const inspect = await deps.runCommand('docker', ['buildx', 'imagetools', 'inspect', imageUri]);
146
+ return { imageUri, buildDurationMs };
147
+ }
148
+ /** 推送已构建镜像,并尽力查询远端 digest。 */
149
+ async function pushLocalImage(imageUri, onLog, options = {}) {
150
+ const deps = options.deps || defaultDeps;
151
+ const commandOptions = options.commandOptions || {};
152
+ onLog === null || onLog === void 0 ? void 0 : onLog(`执行 docker push ${imageUri}`);
153
+ const pushStartedAt = Date.now();
154
+ const push = await deps.runCommand('docker', ['push', imageUri], commandOptions);
155
+ const pushDurationMs = Date.now() - pushStartedAt;
156
+ if (!push.ok) {
157
+ const authFailed = /unauthorized|authentication required|denied|no basic auth credentials/i.test(`${push.stdout}\n${push.stderr}`);
158
+ throw new error_1.CloudBaseError(authFailed
159
+ ? '镜像推送鉴权失败,请确认镜像仓库登录凭证有效,并且当前身份具备目标仓库写权限。'
160
+ : '镜像推送失败,请检查网络、镜像仓库地址和写入权限。', { code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_PUSH_FAILED });
161
+ }
162
+ const inspect = await deps.runCommand('docker', ['buildx', 'imagetools', 'inspect', imageUri], commandOptions);
127
163
  const imageDigest = inspect.ok ? parseDigest(inspect.stdout) : undefined;
128
- return { imageUri, imageDigest };
164
+ if (!inspect.ok) {
165
+ onLog === null || onLog === void 0 ? void 0 : onLog('镜像已推送成功,但未能查询远端 digest,将继续使用镜像 tag 部署');
166
+ }
167
+ return { imageDigest, pushDurationMs };
168
+ }
169
+ function resolveRegistry(imageUri) {
170
+ const repositoryWithRegistry = imageUri.split('@')[0].replace(/:[^/:]+$/, '');
171
+ const slashIndex = repositoryWithRegistry.indexOf('/');
172
+ if (slashIndex <= 0) {
173
+ throw new error_1.CloudBaseError('local 策略的目标镜像地址缺少仓库域名', {
174
+ code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_REPOSITORY_MISSING
175
+ });
176
+ }
177
+ return repositoryWithRegistry.slice(0, slashIndex);
178
+ }
179
+ async function loginDockerRegistry(registry, credential, options = {}) {
180
+ const deps = options.deps || defaultDeps;
181
+ const commandOptions = options.commandOptions || {};
182
+ const result = await deps.runCommand('docker', ['login', '--username', credential.username, '--password-stdin', registry], Object.assign(Object.assign({}, commandOptions), { input: credential.secret }));
183
+ if (!result.ok) {
184
+ throw new error_1.CloudBaseError('镜像仓库 Docker 登录失败,请检查登录凭证、仓库地址和访问权限。', {
185
+ code: types_1.FUNCTION_DEPLOY_ERROR.TCR_DOCKER_LOGIN_FAILED
186
+ });
187
+ }
188
+ }
189
+ function linkDockerState(sourceDir, targetDir, name) {
190
+ const source = path_1.default.join(sourceDir, name);
191
+ const target = path_1.default.join(targetDir, name);
192
+ if (!fs_1.default.existsSync(source)) {
193
+ return;
194
+ }
195
+ try {
196
+ fs_1.default.symlinkSync(source, target, 'junction');
197
+ }
198
+ catch (_a) {
199
+ // Docker 状态继承失败时继续使用已复制的基础配置,后续命令会返回稳定错误码
200
+ }
201
+ }
202
+ function copySanitizedDockerConfig(sourceDir, targetDir) {
203
+ const source = path_1.default.join(sourceDir, 'config.json');
204
+ if (!fs_1.default.existsSync(source)) {
205
+ return;
206
+ }
207
+ try {
208
+ const config = JSON.parse(fs_1.default.readFileSync(source, 'utf8'));
209
+ delete config.auths;
210
+ delete config.credsStore;
211
+ delete config.credHelpers;
212
+ fs_1.default.writeFileSync(path_1.default.join(targetDir, 'config.json'), JSON.stringify(config, null, 2), {
213
+ encoding: 'utf8',
214
+ mode: 0o600
215
+ });
216
+ }
217
+ catch (_a) {
218
+ // 配置不可读或格式异常时使用空配置,避免复制潜在认证信息
219
+ }
220
+ }
221
+ function createTemporaryDockerConfig() {
222
+ const sourceDir = process.env.DOCKER_CONFIG || path_1.default.join(os_1.default.homedir(), '.docker');
223
+ const configDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'tcb-docker-config-'));
224
+ copySanitizedDockerConfig(sourceDir, configDir);
225
+ ['cli-plugins', 'contexts', 'buildx'].forEach(name => linkDockerState(sourceDir, configDir, name));
226
+ return {
227
+ env: { DOCKER_CONFIG: configDir },
228
+ cleanup: () => {
229
+ try {
230
+ ;
231
+ fs_1.default.rmSync(configDir, { recursive: true, force: true });
232
+ }
233
+ catch (_a) {
234
+ // 临时登录态清理失败不覆盖部署主错误
235
+ }
236
+ }
237
+ };
238
+ }
239
+ /**
240
+ * 本地构建镜像并推送到目标仓库。
241
+ *
242
+ * 保留组合入口以兼容已有调用;需要实时阶段进度的编排器应分别调用
243
+ * buildLocalImage 与 pushLocalImage。
244
+ */
245
+ async function buildAndPushImage(config, onLog, deps = defaultDeps) {
246
+ const startedAt = Date.now();
247
+ const credential = config.build.registryCredential;
248
+ const dockerConfig = credential ? createTemporaryDockerConfig() : undefined;
249
+ const commandOptions = dockerConfig ? { env: dockerConfig.env } : undefined;
250
+ try {
251
+ const { imageUri, buildDurationMs } = await buildLocalImage(config, onLog, {
252
+ deps,
253
+ commandOptions
254
+ });
255
+ if (credential) {
256
+ const registry = resolveRegistry(imageUri);
257
+ await loginDockerRegistry(registry, { username: credential.username, secret: credential.password }, { deps, commandOptions });
258
+ }
259
+ const { imageDigest, pushDurationMs } = await pushLocalImage(imageUri, onLog, {
260
+ deps,
261
+ commandOptions
262
+ });
263
+ return {
264
+ imageUri,
265
+ imageDigest,
266
+ buildDurationMs,
267
+ pushDurationMs,
268
+ durationMs: Date.now() - startedAt
269
+ };
270
+ }
271
+ finally {
272
+ dockerConfig === null || dockerConfig === void 0 ? void 0 : dockerConfig.cleanup();
273
+ }
129
274
  }