@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.
- package/CHANGELOG.md +6 -0
- package/lib/database/index.js +11 -6
- package/lib/deploy/DeployOrchestrator.js +30 -16
- package/lib/deploy/FunctionDeployer.js +269 -39
- package/lib/deploy/StaticDeployer.js +7 -1
- package/lib/deploy/function/builders/cloud.js +45 -348
- package/lib/deploy/function/cloud-build-service.js +2 -16
- package/lib/deploy/function/config-guard.js +42 -32
- package/lib/deploy/function/enterprise-tcr.js +194 -0
- package/lib/deploy/function/image-tag.js +31 -0
- package/lib/deploy/function/local-builder.js +170 -25
- package/lib/deploy/function/personal-tcr.js +153 -0
- package/lib/deploy/types.js +11 -0
- package/lib/hosting/index.js +27 -0
- package/lib/projectValidator/index.js +22 -20
- package/package.json +2 -2
- package/types/database/index.d.ts +1 -0
- package/types/deploy/DeployOrchestrator.d.ts +10 -0
- package/types/deploy/FunctionDeployer.d.ts +20 -2
- package/types/deploy/StaticDeployer.d.ts +11 -0
- package/types/deploy/function/builders/cloud.d.ts +6 -39
- package/types/deploy/function/enterprise-tcr.d.ts +32 -0
- package/types/deploy/function/image-tag.d.ts +10 -0
- package/types/deploy/function/local-builder.d.ts +35 -6
- package/types/deploy/function/personal-tcr.d.ts +35 -0
- package/types/deploy/types.d.ts +34 -8
- package/types/hosting/index.d.ts +13 -0
- package/types/projectValidator/index.d.ts +3 -2
|
@@ -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]
|
|
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
|
|
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
|
|
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 = (
|
|
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
|
|
382
|
-
const imageType = (
|
|
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, '
|
|
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
|
-
|
|
389
|
-
|
|
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
|
-
|
|
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 : (
|
|
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
|
-
|
|
445
|
-
|
|
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
|
|
474
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
543
|
-
checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_INVALID, 'registryCredential 仅支持
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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(
|
|
136
|
+
args.push(context);
|
|
118
137
|
onLog === null || onLog === void 0 ? void 0 : onLog(`执行 ${formatBuildCommandForLog(args)}`);
|
|
119
|
-
const
|
|
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('
|
|
122
|
-
code: types_1.FUNCTION_DEPLOY_ERROR.
|
|
142
|
+
throw new error_1.CloudBaseError('镜像构建失败,请检查 Dockerfile、构建上下文和本地 Docker 输出。', {
|
|
143
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_FAILED
|
|
123
144
|
});
|
|
124
145
|
}
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
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
|
}
|