@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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PersonalTcrService = exports.DEFAULT_PERSONAL_TCR_REGISTRY = void 0;
|
|
4
|
+
exports.createPersonalTcrService = createPersonalTcrService;
|
|
5
|
+
const config_guard_1 = require("./config-guard");
|
|
6
|
+
const PERSONAL_TCR_VERSION = '2019-09-24';
|
|
7
|
+
exports.DEFAULT_PERSONAL_TCR_REGISTRY = 'ccr.ccs.tencentyun.com';
|
|
8
|
+
const DEFAULT_RETAINED_TAGS = 30;
|
|
9
|
+
const ALREADY_EXISTS_CODES = new Set([
|
|
10
|
+
'InvalidParameter.ErrNamespaceExist',
|
|
11
|
+
'InvalidParameter.ErrRepoExist',
|
|
12
|
+
'ResourceInUse.NamespaceExist',
|
|
13
|
+
'ResourceInUse.RepositoryExist',
|
|
14
|
+
'FailedOperation.AlreadyExists',
|
|
15
|
+
'ResourceConflict'
|
|
16
|
+
]);
|
|
17
|
+
function dataOf(response) {
|
|
18
|
+
var _a;
|
|
19
|
+
return (response === null || response === void 0 ? void 0 : response.Data) || ((_a = response === null || response === void 0 ? void 0 : response.Response) === null || _a === void 0 ? void 0 : _a.Data) || (response === null || response === void 0 ? void 0 : response.Response) || response || {};
|
|
20
|
+
}
|
|
21
|
+
function getErrorCode(error) {
|
|
22
|
+
var _a, _b;
|
|
23
|
+
return (error === null || error === void 0 ? void 0 : error.code) || (error === null || error === void 0 ? void 0 : error.Code) || ((_b = (_a = error === null || error === void 0 ? void 0 : error.response) === null || _a === void 0 ? void 0 : _a.Error) === null || _b === void 0 ? void 0 : _b.Code) || '';
|
|
24
|
+
}
|
|
25
|
+
function toTagTimestamp(item) {
|
|
26
|
+
const value = item.PushTime || item.CreationTime;
|
|
27
|
+
if (!value) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
const timestamp = Date.parse(value);
|
|
31
|
+
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
32
|
+
}
|
|
33
|
+
function resolvePersonalRepoName(namespace, repository) {
|
|
34
|
+
const parsed = (0, config_guard_1.parseImageReference)(repository);
|
|
35
|
+
if (parsed === null || parsed === void 0 ? void 0 : parsed.registry) {
|
|
36
|
+
return parsed.repository;
|
|
37
|
+
}
|
|
38
|
+
if (parsed === null || parsed === void 0 ? void 0 : parsed.repository.includes('/')) {
|
|
39
|
+
return parsed.repository;
|
|
40
|
+
}
|
|
41
|
+
return `${namespace}/${repository}`;
|
|
42
|
+
}
|
|
43
|
+
function isAlreadyExists(error) {
|
|
44
|
+
const code = getErrorCode(error);
|
|
45
|
+
const message = (error === null || error === void 0 ? void 0 : error.message) || (error === null || error === void 0 ? void 0 : error.Message) || '';
|
|
46
|
+
return (ALREADY_EXISTS_CODES.has(code) ||
|
|
47
|
+
/Err(?:Namespace|Repo)Exist|AlreadyExists|ResourceInUse|ResourceConflict/i.test(code) ||
|
|
48
|
+
/already exists?|has already (?:exist|register)|已存在/i.test(message));
|
|
49
|
+
}
|
|
50
|
+
class PersonalTcrService {
|
|
51
|
+
constructor(environment) {
|
|
52
|
+
const service = environment.getCommonService('tcr', PERSONAL_TCR_VERSION);
|
|
53
|
+
this.request = (action, data = {}) => service.call({ Action: action, Param: data });
|
|
54
|
+
}
|
|
55
|
+
async ensurePushTarget(options) {
|
|
56
|
+
const { namespace, repository, ensureNamespace, ensureRepository } = options;
|
|
57
|
+
if (ensureNamespace && !(await this.namespaceExists(namespace))) {
|
|
58
|
+
await this.createIgnoringExisting('CreateNamespacePersonal', {
|
|
59
|
+
Namespace: namespace
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
if (ensureRepository && !(await this.repositoryExists(namespace, repository))) {
|
|
63
|
+
await this.createIgnoringExisting('CreateRepositoryPersonal', {
|
|
64
|
+
RepoName: `${namespace}/${repository}`,
|
|
65
|
+
Public: 0,
|
|
66
|
+
Description: 'Managed by CloudBase CLI'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async cleanupTags(options) {
|
|
71
|
+
var _a;
|
|
72
|
+
const retain = (_a = options.retain) !== null && _a !== void 0 ? _a : DEFAULT_RETAINED_TAGS;
|
|
73
|
+
const repoName = resolvePersonalRepoName(options.namespace, options.repository);
|
|
74
|
+
const tags = await this.listAllTags(repoName);
|
|
75
|
+
const staleTags = tags
|
|
76
|
+
.filter(item => item.TagName)
|
|
77
|
+
.sort((left, right) => toTagTimestamp(right) - toTagTimestamp(left))
|
|
78
|
+
.slice(retain);
|
|
79
|
+
for (const item of staleTags) {
|
|
80
|
+
try {
|
|
81
|
+
await this.request('DeleteImagePersonal', {
|
|
82
|
+
RepoName: repoName,
|
|
83
|
+
Tag: item.TagName
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
const code = getErrorCode(error);
|
|
88
|
+
if (!/not.?found|not.?exist/i.test(code) && !/不存在/i.test((error === null || error === void 0 ? void 0 : error.message) || '')) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
async namespaceExists(namespace) {
|
|
95
|
+
const response = await this.request('DescribeNamespacePersonal', {
|
|
96
|
+
Namespace: namespace,
|
|
97
|
+
Limit: 100,
|
|
98
|
+
Offset: 0
|
|
99
|
+
});
|
|
100
|
+
const data = dataOf(response);
|
|
101
|
+
const namespaces = Array.isArray(data.NamespaceInfo)
|
|
102
|
+
? data.NamespaceInfo
|
|
103
|
+
: [];
|
|
104
|
+
return namespaces.some(item => item.Namespace === namespace);
|
|
105
|
+
}
|
|
106
|
+
async repositoryExists(namespace, repository) {
|
|
107
|
+
const repoName = `${namespace}/${repository}`;
|
|
108
|
+
const response = await this.request('DescribeRepositoryFilterPersonal', {
|
|
109
|
+
RepoName: repoName,
|
|
110
|
+
Limit: 100,
|
|
111
|
+
Offset: 0
|
|
112
|
+
});
|
|
113
|
+
const data = dataOf(response);
|
|
114
|
+
const repositories = Array.isArray(data.RepoInfo)
|
|
115
|
+
? data.RepoInfo
|
|
116
|
+
: [];
|
|
117
|
+
return repositories.some(item => item.RepoName === repoName);
|
|
118
|
+
}
|
|
119
|
+
async createIgnoringExisting(action, data) {
|
|
120
|
+
try {
|
|
121
|
+
await this.request(action, data);
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (!isAlreadyExists(error)) {
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
async listAllTags(repoName) {
|
|
130
|
+
var _a;
|
|
131
|
+
const result = [];
|
|
132
|
+
const limit = 100;
|
|
133
|
+
let offset = 0;
|
|
134
|
+
while (true) {
|
|
135
|
+
const response = await this.request('DescribeImagePersonal', {
|
|
136
|
+
RepoName: repoName,
|
|
137
|
+
Limit: limit,
|
|
138
|
+
Offset: offset
|
|
139
|
+
});
|
|
140
|
+
const data = (response === null || response === void 0 ? void 0 : response.Data) || ((_a = response === null || response === void 0 ? void 0 : response.Response) === null || _a === void 0 ? void 0 : _a.Data) || {};
|
|
141
|
+
const page = data.TagInfo || [];
|
|
142
|
+
result.push(...page);
|
|
143
|
+
offset += page.length;
|
|
144
|
+
if (!page.length || offset >= Number(data.TagCount || result.length)) {
|
|
145
|
+
return result;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
exports.PersonalTcrService = PersonalTcrService;
|
|
151
|
+
function createPersonalTcrService(environment) {
|
|
152
|
+
return new PersonalTcrService(environment);
|
|
153
|
+
}
|
package/lib/deploy/types.js
CHANGED
|
@@ -55,6 +55,17 @@ exports.FUNCTION_DEPLOY_ERROR = {
|
|
|
55
55
|
BUILD_ARG_INVALID: 'BUILD_ARG_INVALID',
|
|
56
56
|
CLOUD_REGISTRY_CREDENTIAL_MISSING: 'CLOUD_REGISTRY_CREDENTIAL_MISSING',
|
|
57
57
|
CLOUD_REGISTRY_CREDENTIAL_INVALID: 'CLOUD_REGISTRY_CREDENTIAL_INVALID',
|
|
58
|
+
TCR_REGION_MISSING: 'TCR_REGION_MISSING',
|
|
59
|
+
TCR_INSTANCE_DISCOVERY_FAILED: 'TCR_INSTANCE_DISCOVERY_FAILED',
|
|
60
|
+
TCR_INSTANCE_NOT_FOUND: 'TCR_INSTANCE_NOT_FOUND',
|
|
61
|
+
TCR_INSTANCE_AMBIGUOUS: 'TCR_INSTANCE_AMBIGUOUS',
|
|
62
|
+
TCR_INSTANCE_INVALID: 'TCR_INSTANCE_INVALID',
|
|
63
|
+
TCR_NAMESPACE_INVALID: 'TCR_NAMESPACE_INVALID',
|
|
64
|
+
TCR_NAMESPACE_QUERY_FAILED: 'TCR_NAMESPACE_QUERY_FAILED',
|
|
65
|
+
TCR_NAMESPACE_CREATE_FAILED: 'TCR_NAMESPACE_CREATE_FAILED',
|
|
66
|
+
TCR_TOKEN_CREATE_FAILED: 'TCR_TOKEN_CREATE_FAILED',
|
|
67
|
+
TCR_TOKEN_INVALID: 'TCR_TOKEN_INVALID',
|
|
68
|
+
TCR_DOCKER_LOGIN_FAILED: 'TCR_DOCKER_LOGIN_FAILED',
|
|
58
69
|
GATEWAY_PATH_INVALID: 'GATEWAY_PATH_INVALID',
|
|
59
70
|
LOCAL_FALLBACK_INVALID: 'LOCAL_FALLBACK_INVALID',
|
|
60
71
|
// Preflight(有 IO 的本地检查)
|
package/lib/hosting/index.js
CHANGED
|
@@ -348,19 +348,35 @@ class HostingService {
|
|
|
348
348
|
}
|
|
349
349
|
/**
|
|
350
350
|
* 查询域名状态信息
|
|
351
|
+
*
|
|
352
|
+
* 仅在 CDN 域名(domainType=cdn)或老环境(未返回 domainType)时调用 CDN 接口,
|
|
353
|
+
* 其他域名类型(gateway / eo 等)返回空结果
|
|
351
354
|
* @param options
|
|
352
355
|
*/
|
|
353
356
|
async tcbCheckResource(options) {
|
|
357
|
+
const domainType = await this.getStorageDomainType();
|
|
358
|
+
// 白名单:只有 cdn 或空值(老环境)才调用 CDN 接口
|
|
359
|
+
if (domainType !== 'cdn' && domainType) {
|
|
360
|
+
return { Domains: [], RecordCount: 0, RequestId: '' };
|
|
361
|
+
}
|
|
354
362
|
return this.cdnService.request('TcbCheckResource', {
|
|
355
363
|
Domains: options.domains
|
|
356
364
|
});
|
|
357
365
|
}
|
|
358
366
|
/**
|
|
359
367
|
* 域名配置变更
|
|
368
|
+
*
|
|
369
|
+
* 仅在 CDN 域名(domainType=cdn)或老环境(未返回 domainType)时调用 CDN 接口,
|
|
370
|
+
* 其他域名类型(gateway / eo 等)返回空结果
|
|
360
371
|
* @param options
|
|
361
372
|
*/
|
|
362
373
|
async tcbModifyAttribute(options) {
|
|
363
374
|
const { domain, domainId, domainConfig } = options;
|
|
375
|
+
const domainType = await this.getStorageDomainType();
|
|
376
|
+
// 白名单:只有 cdn 或空值(老环境)才调用 CDN 接口
|
|
377
|
+
if (domainType !== 'cdn' && domainType) {
|
|
378
|
+
return { RequestId: '' };
|
|
379
|
+
}
|
|
364
380
|
const res = await this.cdnService.request('TcbModifyAttribute', {
|
|
365
381
|
Domain: domain,
|
|
366
382
|
DomainId: domainId,
|
|
@@ -445,6 +461,17 @@ class HostingService {
|
|
|
445
461
|
}
|
|
446
462
|
return cloudPath[cloudPath.length - 1] === '/' ? cloudPath : `${cloudPath}/`;
|
|
447
463
|
}
|
|
464
|
+
/**
|
|
465
|
+
* 获取静态托管域名类型(cdn / gateway / eo)
|
|
466
|
+
*
|
|
467
|
+
* 后端通过 DescribeEnvInfo 返回 StaticStorages[0].DomainType,
|
|
468
|
+
* 仅 cdn 或空值(老环境)才允许调用 CDN 相关接口。
|
|
469
|
+
*/
|
|
470
|
+
async getStorageDomainType() {
|
|
471
|
+
var _a, _b;
|
|
472
|
+
const { EnvInfo } = await this.environment.getEnvService().getEnvInfo();
|
|
473
|
+
return (_b = (_a = EnvInfo === null || EnvInfo === void 0 ? void 0 : EnvInfo.StaticStorages) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.DomainType;
|
|
474
|
+
}
|
|
448
475
|
}
|
|
449
476
|
exports.HostingService = HostingService;
|
|
450
477
|
__decorate([
|
|
@@ -92,12 +92,25 @@ class ProjectValidator {
|
|
|
92
92
|
}
|
|
93
93
|
validateSchema(config, errors) {
|
|
94
94
|
const result = (0, toolbox_1.validateCloudBaseConfigBySchema)(config);
|
|
95
|
-
result.errors
|
|
95
|
+
result.errors
|
|
96
|
+
.filter((error) => error.keyword !== 'if')
|
|
97
|
+
.forEach((error) => {
|
|
98
|
+
const errorParams = error.params;
|
|
99
|
+
const missingProperty = error.keyword === 'required' && typeof errorParams.missingProperty === 'string'
|
|
100
|
+
? errorParams.missingProperty
|
|
101
|
+
: undefined;
|
|
102
|
+
const baseField = this.normalizeSchemaPath(error.dataPath);
|
|
103
|
+
const field = [baseField, missingProperty].filter(Boolean).join('.');
|
|
104
|
+
const isImageTypeMissing = missingProperty === 'imageType';
|
|
96
105
|
errors.push({
|
|
97
106
|
level: 'error',
|
|
98
|
-
field
|
|
99
|
-
message:
|
|
100
|
-
|
|
107
|
+
field,
|
|
108
|
+
message: isImageTypeMissing
|
|
109
|
+
? 'local/cloud 镜像构建必须显式声明 imageConfig.imageType'
|
|
110
|
+
: `配置不符合 v2.1 Schema:${error.message}`,
|
|
111
|
+
fix: isImageTypeMissing
|
|
112
|
+
? '请根据镜像仓库类型填写 imageConfig.imageType: "enterprise" 或 "personal"'
|
|
113
|
+
: `请检查 ${field || 'cloudbaserc'} 字段`,
|
|
101
114
|
});
|
|
102
115
|
});
|
|
103
116
|
}
|
|
@@ -133,8 +146,9 @@ class ProjectValidator {
|
|
|
133
146
|
});
|
|
134
147
|
}
|
|
135
148
|
/**
|
|
136
|
-
* 个人版 TCR 的 username/password
|
|
137
|
-
*
|
|
149
|
+
* 个人版 TCR 的 username/password 必须在 cloudbaserc 中显式声明。
|
|
150
|
+
* namespace/repository 可省略,分别由部署器按 envId/函数名推导。
|
|
151
|
+
* ConfigParser 已先解析 {{env.*}},因此这里同时承担缺失凭证 env 值的前置校验。
|
|
138
152
|
*/
|
|
139
153
|
validatePersonalRegistryConfig(config, errors) {
|
|
140
154
|
;
|
|
@@ -144,16 +158,6 @@ class ProjectValidator {
|
|
|
144
158
|
return;
|
|
145
159
|
}
|
|
146
160
|
const build = (_a = func.imageConfig) === null || _a === void 0 ? void 0 : _a.build;
|
|
147
|
-
const namespace = build === null || build === void 0 ? void 0 : build.namespace;
|
|
148
|
-
if (typeof namespace !== 'string' || !namespace.trim()) {
|
|
149
|
-
errors.push({
|
|
150
|
-
level: 'error',
|
|
151
|
-
resource: `functions[${index}]`,
|
|
152
|
-
field: 'imageConfig.build.namespace',
|
|
153
|
-
message: '个人版 TCR 缺少 namespace,或对应环境变量未赋值',
|
|
154
|
-
fix: '声明 namespace: "{{env.TCB_TCR_NAMESPACE}}",并在 .env* 中设置该变量',
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
161
|
const credential = build === null || build === void 0 ? void 0 : build.registryCredential;
|
|
158
162
|
if (!credential || typeof credential !== 'object') {
|
|
159
163
|
errors.push({
|
|
@@ -190,12 +194,10 @@ class ProjectValidator {
|
|
|
190
194
|
return (config.functions || []).some((func) => this.isPersonalCloudFunction(func));
|
|
191
195
|
}
|
|
192
196
|
isPersonalCloudFunction(func) {
|
|
193
|
-
var _a
|
|
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');
|
|
197
|
+
var _a;
|
|
196
198
|
return (func.type === 'HTTP' &&
|
|
197
199
|
func.buildStrategy === 'cloud' &&
|
|
198
|
-
imageType === 'personal');
|
|
200
|
+
((_a = func.imageConfig) === null || _a === void 0 ? void 0 : _a.imageType) === 'personal');
|
|
199
201
|
}
|
|
200
202
|
validateResources(cwd, config) {
|
|
201
203
|
const errors = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudbase/manager-node",
|
|
3
|
-
"version": "5.8.
|
|
3
|
+
"version": "5.8.3",
|
|
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.
|
|
45
|
+
"@cloudbase/toolbox": "~0.8.4",
|
|
46
46
|
"@cloudbase/database": "^0.6.2",
|
|
47
47
|
"archiver": "^3.1.1",
|
|
48
48
|
"cos-nodejs-sdk-v5": "^2.14.0",
|
|
@@ -269,5 +269,6 @@ export declare class DatabaseService {
|
|
|
269
269
|
* @returns 任务状态详情(TaskType、Status、Phase、Reason 等)
|
|
270
270
|
*/
|
|
271
271
|
describeTaskResult(options: IDescribeTaskResultOptions): Promise<IDescribeTaskResultResult>;
|
|
272
|
+
private getDefaultDatabaseInstanceId;
|
|
272
273
|
}
|
|
273
274
|
export {};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Environment } from '../environment';
|
|
2
|
+
import { IDeployProgressEvent } from './types';
|
|
2
3
|
/** 可编排的资源类型 */
|
|
3
4
|
export type ResourceType = 'database' | 'functions' | 'app' | 'hosting' | 'gateway';
|
|
4
5
|
/** 声明式部署选项 */
|
|
@@ -24,6 +25,11 @@ export interface IDeployOptions {
|
|
|
24
25
|
concurrency?: number;
|
|
25
26
|
/** 失败后继续部署其余资源(database 失败仍强制中断) */
|
|
26
27
|
continueOnError?: boolean;
|
|
28
|
+
/** 函数资源内部部署阶段进度(校验、构建、推送、部署、等待可用、访问配置) */
|
|
29
|
+
onProgress?: (event: IDeployProgressEvent & {
|
|
30
|
+
resourceType: ResourceType;
|
|
31
|
+
resourceName: string;
|
|
32
|
+
}) => void;
|
|
27
33
|
log?: {
|
|
28
34
|
info?: (msg: string) => void;
|
|
29
35
|
success?: (msg: string) => void;
|
|
@@ -66,7 +72,11 @@ export interface IDeployResult {
|
|
|
66
72
|
name: string;
|
|
67
73
|
ok: boolean;
|
|
68
74
|
url?: string;
|
|
75
|
+
/** 资源部署器返回的结构化详情(函数包含策略、镜像、访问地址、阶段状态和耗时) */
|
|
76
|
+
detail?: Record<string, any>;
|
|
69
77
|
error?: string;
|
|
78
|
+
errorCode?: string;
|
|
79
|
+
failedStage?: string;
|
|
70
80
|
/** 原始异常中的请求 ID(若可提取),用于定位平台侧接口报错 */
|
|
71
81
|
requestId?: string;
|
|
72
82
|
/** 未执行原因:skip=未变更自动跳过;no-confirm=无确认机制保守跳过;skipped-by-user=用户拒绝覆盖 */
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { Environment } from '../environment';
|
|
2
2
|
import { ICloudBuildService } from './function/builders/cloud';
|
|
3
3
|
import { IImagePreflightDeps } from './function/image-preflight';
|
|
4
|
+
import { ILocalBuilderDeps } from './function/local-builder';
|
|
5
|
+
import { ITcrApiService } from './function/enterprise-tcr';
|
|
6
|
+
import { IPersonalTcrService } from './function/personal-tcr';
|
|
4
7
|
import { ICheckReport, IFunctionDeployConfig, IFunctionDeployOptions, IFunctionDeployPlan, IFunctionDeployResult, INormalizedDeployConfig } from './types';
|
|
5
8
|
interface ILegacyFunctionDeployResult {
|
|
6
9
|
name: string;
|
|
@@ -16,7 +19,7 @@ interface ILegacyFunctionDeployResult {
|
|
|
16
19
|
*
|
|
17
20
|
* 已支持代码包、已有镜像、云端构建、本地构建、匿名访问与网关路径收敛;
|
|
18
21
|
* 函数类型、运行时或协议等创建期字段变化时不会自动删除函数,而是引导用户
|
|
19
|
-
* 在控制台确认并手工删除后重新部署。
|
|
22
|
+
* 在控制台确认并手工删除后重新部署。
|
|
20
23
|
*/
|
|
21
24
|
export declare class FunctionDeployer {
|
|
22
25
|
private readonly environment;
|
|
@@ -28,9 +31,15 @@ export declare class FunctionDeployer {
|
|
|
28
31
|
* 测试可注入以替换打包/网络/tcb API,避免真实 IO
|
|
29
32
|
*/
|
|
30
33
|
private readonly cloudBuildServiceFactory;
|
|
34
|
+
private readonly localBuilderDeps?;
|
|
35
|
+
private readonly tcrServiceFactory;
|
|
36
|
+
private readonly personalTcrServiceFactory;
|
|
31
37
|
constructor(environment: Environment, deps?: {
|
|
32
38
|
imagePreflightDeps?: IImagePreflightDeps;
|
|
33
39
|
cloudBuildServiceFactory?: () => ICloudBuildService;
|
|
40
|
+
localBuilderDeps?: ILocalBuilderDeps;
|
|
41
|
+
tcrServiceFactory?: () => ITcrApiService;
|
|
42
|
+
personalTcrServiceFactory?: () => IPersonalTcrService;
|
|
34
43
|
});
|
|
35
44
|
/**
|
|
36
45
|
* 兼容既有声明式部署入口
|
|
@@ -101,6 +110,8 @@ export declare class FunctionDeployer {
|
|
|
101
110
|
* 由image-preflight 自动跳过地域校验,不误报。
|
|
102
111
|
*/
|
|
103
112
|
private resolveDeployRegion;
|
|
113
|
+
private resolveEnvironmentId;
|
|
114
|
+
private requireDeployRegion;
|
|
104
115
|
/**
|
|
105
116
|
* 构造镜像拉取授权 Preflight 所需的 CAM 适配器
|
|
106
117
|
*
|
|
@@ -125,6 +136,8 @@ export declare class FunctionDeployer {
|
|
|
125
136
|
* 未实现的能力必须显式报错,避免用户误认为访问规则或镜像构建已经生效
|
|
126
137
|
*/
|
|
127
138
|
private assertPlanExecutable;
|
|
139
|
+
private prepareEnterpriseBuildConfig;
|
|
140
|
+
private preparePersonalBuildConfig;
|
|
128
141
|
/**
|
|
129
142
|
* 准备部署物
|
|
130
143
|
*
|
|
@@ -133,14 +146,19 @@ export declare class FunctionDeployer {
|
|
|
133
146
|
* 其余策略走同步的 pack。
|
|
134
147
|
*/
|
|
135
148
|
private prepareArtifact;
|
|
149
|
+
private resolveLocalRegistryCredential;
|
|
150
|
+
private resolveRegistryFromRepository;
|
|
136
151
|
/** 生成部署物 */
|
|
137
152
|
private createArtifact;
|
|
138
153
|
/** 执行部署计划 */
|
|
139
154
|
private applyPlan;
|
|
140
155
|
/** 收敛 HTTP 函数访问配置(匿名访问 + 网关路由) */
|
|
141
156
|
private applyAccessPlan;
|
|
142
|
-
/**
|
|
157
|
+
/** 执行普通步骤并记录耗时与进度 */
|
|
143
158
|
private runStep;
|
|
159
|
+
/** 执行带具体名称的步骤并记录耗时与进度 */
|
|
160
|
+
private runLabeledStep;
|
|
161
|
+
private executeStep;
|
|
144
162
|
/** 上报进度,回调异常不影响部署流程 */
|
|
145
163
|
private emitProgress;
|
|
146
164
|
/** 汇总部署结果 */
|
|
@@ -28,6 +28,17 @@ export interface IHostingDeployConfig {
|
|
|
28
28
|
ignore?: string[];
|
|
29
29
|
/** 构建时环境变量(注入本地构建进程,非敏感) */
|
|
30
30
|
envVariables?: Record<string, string | number | boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* 多页应用(MPA)入口文件列表,相对于产物目录,默认 ["index.html"]。
|
|
33
|
+
* 一致性上传时用于识别入口文件(内部映射为 uploadFiles 的 entryFiles)。
|
|
34
|
+
*/
|
|
35
|
+
entryFile?: string[];
|
|
36
|
+
/** 发布后校验远端文件与本地产物一致(开启后走一致性上传) */
|
|
37
|
+
verify?: boolean;
|
|
38
|
+
/** 发布前备份并在上传或校验失败时自动回滚(开启后走一致性上传) */
|
|
39
|
+
safe?: boolean;
|
|
40
|
+
/** 发布成功后清理远端冗余文件(危险操作,开启后走一致性上传) */
|
|
41
|
+
prune?: boolean;
|
|
31
42
|
}
|
|
32
43
|
export interface IStaticDeployResult {
|
|
33
44
|
name: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ICustomBuildEnv, ICustomBuildSecret, ICustomBuildStep, CloudAppArtifact, CloudAppBuildStep } from '../../../cloudApp/types';
|
|
2
2
|
import { INormalizedHttpCloudConfig } from '../../types';
|
|
3
|
+
import { IEnterpriseTcrCredential } from '../enterprise-tcr';
|
|
3
4
|
export interface ICloudBuildResult {
|
|
4
5
|
imageUri: string;
|
|
5
6
|
imageDigest?: string;
|
|
@@ -39,26 +40,13 @@ export interface ICloudVersionStatus {
|
|
|
39
40
|
/** DescribeCloudAppVersion 接口返回的 RequestId,便于把构建问题反馈给平台后端定位 */
|
|
40
41
|
requestId?: string;
|
|
41
42
|
}
|
|
42
|
-
/** 需要额外注入到构建 zip 的文件条目(zip 内路径 + 内容 + 可选 unix 权限位) */
|
|
43
|
-
export interface ICloudBuildExtraEntry {
|
|
44
|
-
/** zip 包内的相对路径,如 scripts/tcr-login-and-push.sh */
|
|
45
|
-
zipPath: string;
|
|
46
|
-
/** 文件文本内容 */
|
|
47
|
-
content: string;
|
|
48
|
-
/** unix 权限位(八进制),如 0o755;脚本需可执行 */
|
|
49
|
-
mode?: number;
|
|
50
|
-
}
|
|
51
43
|
/**
|
|
52
44
|
* 云端构建所需的外部能力,由编排器注入
|
|
53
45
|
*每个方法对应构建方案(https://iwiki.woa.com/p/4020519533)的一个 API/IO 步骤
|
|
54
46
|
*/
|
|
55
47
|
export interface ICloudBuildService {
|
|
56
|
-
/**
|
|
57
|
-
|
|
58
|
-
* @param cwd 构建上下文目录
|
|
59
|
-
* @param extraEntries 额外注入zip 的文件(如 push 脚本),不写入用户源码目录
|
|
60
|
-
*/
|
|
61
|
-
packContext: (cwd: string, extraEntries?: ICloudBuildExtraEntry[]) => Promise<string>;
|
|
48
|
+
/** 打包构建上下文为 ZIP,返回本地临时 zip 路径 */
|
|
49
|
+
packContext: (cwd: string) => Promise<string>;
|
|
62
50
|
/** 获取 COS 上传凭证(DescribeCloudAppCosInfo,DeployType:custom) */
|
|
63
51
|
getCosInfo: (serviceName: string) => Promise<ICloudCosInfo>;
|
|
64
52
|
/** PUT 上传 zip 到 COS,必须携带全部 uploadHeaders */
|
|
@@ -72,39 +60,18 @@ export interface ICloudBuildService {
|
|
|
72
60
|
/** 延时(便于测试注入立即返回) */
|
|
73
61
|
delay: (ms: number) => Promise<void>;
|
|
74
62
|
}
|
|
75
|
-
/** 注入到构建上下文 zip 中的推送脚本相对路径(与 CustomSteps 的 push命令一致) */
|
|
76
|
-
export declare const TCR_PUSH_SCRIPT_ZIP_PATH = "scripts/tcr-login-and-push.sh";
|
|
77
|
-
/**
|
|
78
|
-
* 注入到构建 zip 的 TCR/CCR 登录推送脚本(完整可运行)
|
|
79
|
-
*
|
|
80
|
-
* 对应构建方案(https://iwiki.woa.com/p/4020519533)「阶段 A」的 push 步骤:
|
|
81
|
-
* push-image 步骤执行 `bash./scripts/tcr-login-and-push.sh`,该脚本必须随源码
|
|
82
|
-
* 一同打进zip 根目录,否则云端会 `No such file or directory` 秒挂。
|
|
83
|
-
*
|
|
84
|
-
* 关键设计:
|
|
85
|
-
* - 地域参数化:REGION 读环境变量 $TCR_REGION(由 buildEnv 按部署地域注入),
|
|
86
|
-
* 避免硬编码 ap-shanghai跨地域时 AuthFailure.SignatureFailure。
|
|
87
|
-
* - 双仓库兼容:设置了 $TCR_INSTANCE_ID 走企业版 TCR CreateInstanceToken;
|
|
88
|
-
* 否则走个人版 CCR,用户名来自普通 Env,固定密码来自 CloudApp Secrets。
|
|
89
|
-
* - 安全红线:凭证一律经 --password-stdin 传入,绝不出现在 ps/日志;
|
|
90
|
-
* 不打印任何 $API_SECRET_* / token / $SECRET_TCR_PASSWORD_B64 或解码后的密码。
|
|
91
|
-
*
|
|
92
|
-
* 脚本内容为SDK 固定模板,不拼接任何用户输入(镜像地址等均由已校验的
|
|
93
|
-
* 环境变量在容器内组装),无命令注入面。
|
|
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 # \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
63
|
/** buildImageOnCloud 可选项 */
|
|
97
64
|
export interface IBuildImageOnCloudOptions {
|
|
98
65
|
/** 日志回调,用于透出构建进度(不含凭证) */
|
|
99
66
|
onLog?: (line: string) => void;
|
|
100
|
-
/**
|
|
101
|
-
|
|
67
|
+
/** 企业版 TCR 临时登录凭证,由 manager-node 在 CreateCloudApp 前换取 */
|
|
68
|
+
enterpriseCredential?: IEnterpriseTcrCredential;
|
|
102
69
|
}
|
|
103
70
|
/**
|
|
104
71
|
* 执行云端构建并推送镜像
|
|
105
72
|
*
|
|
106
73
|
* @param config 规范化的 cloud 部署配置
|
|
107
74
|
* @param service 注入的外部能力(打包/tcb API/COS 上传)
|
|
108
|
-
* @param options 可选项:日志回调 onLog
|
|
75
|
+
* @param options 可选项:日志回调 onLog、企业版临时登录凭证
|
|
109
76
|
*/
|
|
110
77
|
export declare function buildImageOnCloud(config: INormalizedHttpCloudConfig, service: ICloudBuildService, options?: IBuildImageOnCloudOptions): Promise<ICloudBuildResult>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { INormalizedHttpCloudConfig, INormalizedHttpLocalConfig } from '../types';
|
|
2
|
+
type EnterpriseBuildConfig = INormalizedHttpCloudConfig | INormalizedHttpLocalConfig;
|
|
3
|
+
export interface ITcrApiService {
|
|
4
|
+
call: (options: {
|
|
5
|
+
Action: string;
|
|
6
|
+
Param: Record<string, unknown>;
|
|
7
|
+
}) => Promise<any>;
|
|
8
|
+
}
|
|
9
|
+
export interface IEnterpriseTcrTarget {
|
|
10
|
+
registryId: string;
|
|
11
|
+
registry: string;
|
|
12
|
+
namespace: string;
|
|
13
|
+
repository: string;
|
|
14
|
+
imageRepository: string;
|
|
15
|
+
}
|
|
16
|
+
export interface IEnterpriseTcrCredential {
|
|
17
|
+
username: string;
|
|
18
|
+
token: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function prepareEnterpriseTcrTarget(config: EnterpriseBuildConfig, options: {
|
|
21
|
+
envId: string;
|
|
22
|
+
region: string;
|
|
23
|
+
service: ITcrApiService;
|
|
24
|
+
}): Promise<{
|
|
25
|
+
config: EnterpriseBuildConfig;
|
|
26
|
+
target: IEnterpriseTcrTarget;
|
|
27
|
+
}>;
|
|
28
|
+
export declare function createEnterpriseTcrCredential(service: ITcrApiService, registryId: string): Promise<IEnterpriseTcrCredential>;
|
|
29
|
+
export declare function createTcrApiService(environment: {
|
|
30
|
+
getCommonService: (serviceType: string, serviceVersion: string) => ITcrApiService;
|
|
31
|
+
}): ITcrApiService;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface IResolveImageTagOptions {
|
|
2
|
+
explicitTag?: string;
|
|
3
|
+
cwd: string;
|
|
4
|
+
now?: () => number;
|
|
5
|
+
readGitShortSha?: (cwd: string) => Promise<string | undefined>;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* 生成镜像 tag:显式配置优先,其次 Git short SHA,最后回退到毫秒时间戳。
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveImageTag(options: IResolveImageTagOptions): Promise<string>;
|
|
@@ -2,25 +2,54 @@ import { INormalizedHttpLocalConfig } from '../types';
|
|
|
2
2
|
export interface ILocalBuildResult {
|
|
3
3
|
imageUri: string;
|
|
4
4
|
imageDigest?: string;
|
|
5
|
+
buildDurationMs: number;
|
|
6
|
+
pushDurationMs: number;
|
|
7
|
+
durationMs: number;
|
|
5
8
|
}
|
|
6
9
|
export interface IExecResult {
|
|
7
10
|
ok: boolean;
|
|
8
11
|
stdout: string;
|
|
9
12
|
stderr: string;
|
|
10
13
|
}
|
|
14
|
+
export interface ILocalCommandOptions {
|
|
15
|
+
env?: NodeJS.ProcessEnv;
|
|
16
|
+
input?: string;
|
|
17
|
+
}
|
|
11
18
|
/** 可注入依赖:命令执行与文件系统检查 */
|
|
12
19
|
export interface ILocalBuilderDeps {
|
|
13
|
-
runCommand: (command: string, args: string[]) => Promise<IExecResult>;
|
|
20
|
+
runCommand: (command: string, args: string[], options?: ILocalCommandOptions) => Promise<IExecResult>;
|
|
14
21
|
existsSync: (target: string) => boolean;
|
|
15
22
|
isDirectory: (target: string) => boolean;
|
|
16
23
|
isFile: (target: string) => boolean;
|
|
17
24
|
}
|
|
25
|
+
/** 本地镜像构建阶段结果 */
|
|
26
|
+
export interface ILocalImageBuildResult {
|
|
27
|
+
imageUri: string;
|
|
28
|
+
buildDurationMs: number;
|
|
29
|
+
}
|
|
30
|
+
export interface ILocalBuilderOptions {
|
|
31
|
+
deps?: ILocalBuilderDeps;
|
|
32
|
+
commandOptions?: ILocalCommandOptions;
|
|
33
|
+
}
|
|
34
|
+
/** 本地构建镜像,不执行推送。 */
|
|
35
|
+
export declare function buildLocalImage(config: INormalizedHttpLocalConfig, onLog?: (line: string) => void, options?: ILocalBuilderOptions): Promise<ILocalImageBuildResult>;
|
|
36
|
+
/** 推送已构建镜像,并尽力查询远端 digest。 */
|
|
37
|
+
export declare function pushLocalImage(imageUri: string, onLog?: (line: string) => void, options?: ILocalBuilderOptions): Promise<{
|
|
38
|
+
imageDigest?: string;
|
|
39
|
+
pushDurationMs: number;
|
|
40
|
+
}>;
|
|
41
|
+
export declare function loginDockerRegistry(registry: string, credential: {
|
|
42
|
+
username: string;
|
|
43
|
+
secret: string;
|
|
44
|
+
}, options?: ILocalBuilderOptions): Promise<void>;
|
|
45
|
+
export declare function createTemporaryDockerConfig(): {
|
|
46
|
+
env: NodeJS.ProcessEnv;
|
|
47
|
+
cleanup: () => void;
|
|
48
|
+
};
|
|
18
49
|
/**
|
|
19
|
-
*
|
|
50
|
+
* 本地构建镜像并推送到目标仓库。
|
|
20
51
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* @param deps 可注入依赖,默认真实命令与 fs;测试可传 mock
|
|
24
|
-
* @returns 最终镜像引用与 digest
|
|
52
|
+
* 保留组合入口以兼容已有调用;需要实时阶段进度的编排器应分别调用
|
|
53
|
+
* buildLocalImage 与 pushLocalImage。
|
|
25
54
|
*/
|
|
26
55
|
export declare function buildAndPushImage(config: INormalizedHttpLocalConfig, onLog?: (line: string) => void, deps?: ILocalBuilderDeps): Promise<ILocalBuildResult>;
|