@cloudbase/manager-node 5.8.2 → 5.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/lib/database/index.js +11 -6
- package/lib/deploy/DeployOrchestrator.js +170 -278
- package/lib/deploy/FunctionDeployer.js +310 -52
- package/lib/deploy/StaticDeployer.js +7 -1
- package/lib/deploy/function/artifact.js +55 -41
- package/lib/deploy/function/builders/cloud.js +58 -359
- package/lib/deploy/function/cam-preflight.js +2 -1
- package/lib/deploy/function/cloud-build-service.js +2 -16
- package/lib/deploy/function/config-guard.js +210 -55
- package/lib/deploy/function/enterprise-tcr.js +195 -0
- package/lib/deploy/function/image-tag.js +31 -0
- package/lib/deploy/function/local-builder.js +178 -32
- package/lib/deploy/function/personal-tcr.js +153 -0
- package/lib/deploy/function/preflight.js +3 -2
- package/lib/deploy/types.js +11 -0
- package/lib/hosting/index.js +27 -0
- package/lib/projectValidator/index.js +22 -23
- package/package.json +2 -2
- package/types/database/index.d.ts +1 -0
- package/types/deploy/DeployOrchestrator.d.ts +19 -25
- package/types/deploy/FunctionDeployer.d.ts +35 -3
- package/types/deploy/StaticDeployer.d.ts +11 -0
- package/types/deploy/function/artifact.d.ts +0 -5
- package/types/deploy/function/builders/cloud.d.ts +6 -39
- package/types/deploy/function/config-guard.d.ts +18 -0
- 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 +110 -28
- package/types/hosting/index.d.ts +13 -0
- package/types/projectValidator/index.d.ts +3 -2
- package/types/projectValidator/types.d.ts +0 -1
- package/lib/deploy/StateStore.js +0 -328
- package/types/deploy/StateStore.d.ts +0 -174
|
@@ -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 检查 */
|
|
@@ -44,19 +61,19 @@ const defaultDeps = {
|
|
|
44
61
|
};
|
|
45
62
|
/** 解析构建上下文目录(绝对路径) */
|
|
46
63
|
function resolveContext(config) {
|
|
47
|
-
const cwd = config.build.cwd || config.functionPath || process.cwd();
|
|
64
|
+
const cwd = config.imageConfig.build.cwd || config.functionPath || process.cwd();
|
|
48
65
|
return path_1.default.resolve(cwd);
|
|
49
66
|
}
|
|
50
67
|
/** 解析目标镜像完整引用 imageUri(repository 需为含域名与命名空间的完整仓库地址,个人版/企业版通用) */
|
|
51
|
-
function resolveImageUri(config) {
|
|
52
|
-
const
|
|
68
|
+
async function resolveImageUri(config, context) {
|
|
69
|
+
const build = config.imageConfig.build;
|
|
70
|
+
const repository = build.repository;
|
|
53
71
|
if (typeof repository !== 'string' || !repository.trim()) {
|
|
54
|
-
throw new error_1.CloudBaseError('local 策略缺少目标镜像仓库地址 build.repository', {
|
|
72
|
+
throw new error_1.CloudBaseError('local 策略缺少目标镜像仓库地址 imageConfig.build.repository', {
|
|
55
73
|
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_REPOSITORY_MISSING
|
|
56
74
|
});
|
|
57
75
|
}
|
|
58
|
-
|
|
59
|
-
const tag = config.build.tag || `v${Date.now()}`;
|
|
76
|
+
const tag = await (0, image_tag_1.resolveImageTag)({ explicitTag: build.tag, cwd: context });
|
|
60
77
|
return `${repository.trim()}:${tag}`;
|
|
61
78
|
}
|
|
62
79
|
/** 从 buildx imagetools inspect 输出解析 digest */
|
|
@@ -80,50 +97,179 @@ function formatBuildCommandForLog(args) {
|
|
|
80
97
|
}
|
|
81
98
|
return `docker ${displayArgs.join(' ')}`;
|
|
82
99
|
}
|
|
83
|
-
/**
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
* @param onLog 可选日志回调,用于透出构建输出(不含凭证)
|
|
88
|
-
* @param deps 可注入依赖,默认真实命令与 fs;测试可传 mock
|
|
89
|
-
* @returns 最终镜像引用与 digest
|
|
90
|
-
*/
|
|
91
|
-
async function buildAndPushImage(config, onLog, deps = defaultDeps) {
|
|
100
|
+
/** 本地构建镜像,不执行推送。 */
|
|
101
|
+
async function buildLocalImage(config, onLog, options = {}) {
|
|
102
|
+
const deps = options.deps || defaultDeps;
|
|
103
|
+
const commandOptions = options.commandOptions || {};
|
|
92
104
|
const context = resolveContext(config);
|
|
93
105
|
if (!deps.existsSync(context) || !deps.isDirectory(context)) {
|
|
94
106
|
throw new error_1.CloudBaseError(`构建上下文目录不存在:${context}`, {
|
|
95
107
|
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_CONTEXT_MISSING
|
|
96
108
|
});
|
|
97
109
|
}
|
|
98
|
-
const dockerfile = config.build.dockerfile || types_1.DEFAULT_DOCKERFILE;
|
|
110
|
+
const dockerfile = config.imageConfig.build.dockerfile || types_1.DEFAULT_DOCKERFILE;
|
|
99
111
|
const dockerfilePath = path_1.default.resolve(context, dockerfile);
|
|
100
112
|
if (!deps.existsSync(dockerfilePath) || !deps.isFile(dockerfilePath)) {
|
|
101
113
|
throw new error_1.CloudBaseError(`Dockerfile 不存在:${dockerfilePath}`, {
|
|
102
114
|
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_DOCKERFILE_MISSING
|
|
103
115
|
});
|
|
104
116
|
}
|
|
105
|
-
const imageUri = resolveImageUri(config);
|
|
106
|
-
const platform = config.build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
117
|
+
const imageUri = await resolveImageUri(config, context);
|
|
118
|
+
const platform = config.imageConfig.build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
|
|
119
|
+
const args = [
|
|
120
|
+
'buildx',
|
|
121
|
+
'build',
|
|
122
|
+
'--platform',
|
|
123
|
+
platform,
|
|
124
|
+
'-f',
|
|
125
|
+
dockerfilePath,
|
|
126
|
+
'-t',
|
|
127
|
+
imageUri,
|
|
128
|
+
'--load'
|
|
129
|
+
];
|
|
130
|
+
if (config.imageConfig.build.forceBuild) {
|
|
110
131
|
args.push('--no-cache');
|
|
111
132
|
}
|
|
112
|
-
|
|
113
|
-
const buildArgs = config.build.buildArgs || {};
|
|
133
|
+
const buildArgs = config.imageConfig.build.buildArgs || {};
|
|
114
134
|
for (const key of Object.keys(buildArgs)) {
|
|
115
135
|
args.push('--build-arg', `${key}=${buildArgs[key]}`);
|
|
116
136
|
}
|
|
117
|
-
args.push(
|
|
137
|
+
args.push(context);
|
|
118
138
|
onLog === null || onLog === void 0 ? void 0 : onLog(`执行 ${formatBuildCommandForLog(args)}`);
|
|
119
|
-
const
|
|
139
|
+
const buildStartedAt = Date.now();
|
|
140
|
+
const build = await deps.runCommand('docker', args, commandOptions);
|
|
141
|
+
const buildDurationMs = Date.now() - buildStartedAt;
|
|
120
142
|
if (!build.ok) {
|
|
121
|
-
throw new error_1.CloudBaseError('
|
|
122
|
-
code: types_1.FUNCTION_DEPLOY_ERROR.
|
|
143
|
+
throw new error_1.CloudBaseError('镜像构建失败,请检查 Dockerfile、构建上下文和本地 Docker 输出。', {
|
|
144
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_FAILED
|
|
123
145
|
});
|
|
124
146
|
}
|
|
125
|
-
|
|
126
|
-
|
|
147
|
+
return { imageUri, buildDurationMs };
|
|
148
|
+
}
|
|
149
|
+
/** 推送已构建镜像,并尽力查询远端 digest。 */
|
|
150
|
+
async function pushLocalImage(imageUri, onLog, options = {}) {
|
|
151
|
+
const deps = options.deps || defaultDeps;
|
|
152
|
+
const commandOptions = options.commandOptions || {};
|
|
153
|
+
onLog === null || onLog === void 0 ? void 0 : onLog(`执行 docker push ${imageUri}`);
|
|
154
|
+
const pushStartedAt = Date.now();
|
|
155
|
+
const push = await deps.runCommand('docker', ['push', imageUri], commandOptions);
|
|
156
|
+
const pushDurationMs = Date.now() - pushStartedAt;
|
|
157
|
+
if (!push.ok) {
|
|
158
|
+
const authFailed = /unauthorized|authentication required|denied|no basic auth credentials/i.test(`${push.stdout}\n${push.stderr}`);
|
|
159
|
+
throw new error_1.CloudBaseError(authFailed
|
|
160
|
+
? '镜像推送鉴权失败,请确认镜像仓库登录凭证有效,并且当前身份具备目标仓库写权限。'
|
|
161
|
+
: '镜像推送失败,请检查网络、镜像仓库地址和写入权限。', { code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_PUSH_FAILED });
|
|
162
|
+
}
|
|
163
|
+
const inspect = await deps.runCommand('docker', ['buildx', 'imagetools', 'inspect', imageUri], commandOptions);
|
|
127
164
|
const imageDigest = inspect.ok ? parseDigest(inspect.stdout) : undefined;
|
|
128
|
-
|
|
165
|
+
if (!inspect.ok) {
|
|
166
|
+
onLog === null || onLog === void 0 ? void 0 : onLog('镜像已推送成功,但未能查询远端 digest,将继续使用镜像 tag 部署');
|
|
167
|
+
}
|
|
168
|
+
return { imageDigest, pushDurationMs };
|
|
169
|
+
}
|
|
170
|
+
function resolveRegistry(imageUri) {
|
|
171
|
+
const repositoryWithRegistry = imageUri.split('@')[0].replace(/:[^/:]+$/, '');
|
|
172
|
+
const slashIndex = repositoryWithRegistry.indexOf('/');
|
|
173
|
+
if (slashIndex <= 0) {
|
|
174
|
+
throw new error_1.CloudBaseError('local 策略的目标镜像地址缺少仓库域名', {
|
|
175
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_REPOSITORY_MISSING
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return repositoryWithRegistry.slice(0, slashIndex);
|
|
179
|
+
}
|
|
180
|
+
async function loginDockerRegistry(registry, credential, options = {}) {
|
|
181
|
+
const deps = options.deps || defaultDeps;
|
|
182
|
+
const commandOptions = options.commandOptions || {};
|
|
183
|
+
const result = await deps.runCommand('docker', ['login', '--username', credential.username, '--password-stdin', registry], Object.assign(Object.assign({}, commandOptions), { input: credential.secret }));
|
|
184
|
+
if (!result.ok) {
|
|
185
|
+
throw new error_1.CloudBaseError('镜像仓库 Docker 登录失败,请检查登录凭证、仓库地址和访问权限。', {
|
|
186
|
+
code: types_1.FUNCTION_DEPLOY_ERROR.TCR_DOCKER_LOGIN_FAILED
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function linkDockerState(sourceDir, targetDir, name) {
|
|
191
|
+
const source = path_1.default.join(sourceDir, name);
|
|
192
|
+
const target = path_1.default.join(targetDir, name);
|
|
193
|
+
if (!fs_1.default.existsSync(source)) {
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
fs_1.default.symlinkSync(source, target, 'junction');
|
|
198
|
+
}
|
|
199
|
+
catch (_a) {
|
|
200
|
+
// Docker 状态继承失败时继续使用已复制的基础配置,后续命令会返回稳定错误码
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
function copySanitizedDockerConfig(sourceDir, targetDir) {
|
|
204
|
+
const source = path_1.default.join(sourceDir, 'config.json');
|
|
205
|
+
if (!fs_1.default.existsSync(source)) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
const config = JSON.parse(fs_1.default.readFileSync(source, 'utf8'));
|
|
210
|
+
delete config.auths;
|
|
211
|
+
delete config.credsStore;
|
|
212
|
+
delete config.credHelpers;
|
|
213
|
+
fs_1.default.writeFileSync(path_1.default.join(targetDir, 'config.json'), JSON.stringify(config, null, 2), {
|
|
214
|
+
encoding: 'utf8',
|
|
215
|
+
mode: 0o600
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (_a) {
|
|
219
|
+
// 配置不可读或格式异常时使用空配置,避免复制潜在认证信息
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function createTemporaryDockerConfig() {
|
|
223
|
+
const sourceDir = process.env.DOCKER_CONFIG || path_1.default.join(os_1.default.homedir(), '.docker');
|
|
224
|
+
const configDir = fs_1.default.mkdtempSync(path_1.default.join(os_1.default.tmpdir(), 'tcb-docker-config-'));
|
|
225
|
+
copySanitizedDockerConfig(sourceDir, configDir);
|
|
226
|
+
['cli-plugins', 'contexts', 'buildx'].forEach(name => linkDockerState(sourceDir, configDir, name));
|
|
227
|
+
return {
|
|
228
|
+
env: { DOCKER_CONFIG: configDir },
|
|
229
|
+
cleanup: () => {
|
|
230
|
+
try {
|
|
231
|
+
;
|
|
232
|
+
fs_1.default.rmSync(configDir, { recursive: true, force: true });
|
|
233
|
+
}
|
|
234
|
+
catch (_a) {
|
|
235
|
+
// 临时登录态清理失败不覆盖部署主错误
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* 本地构建镜像并推送到目标仓库。
|
|
242
|
+
*
|
|
243
|
+
* 保留组合入口以兼容已有调用;需要实时阶段进度的编排器应分别调用
|
|
244
|
+
* buildLocalImage 与 pushLocalImage。
|
|
245
|
+
*/
|
|
246
|
+
async function buildAndPushImage(config, onLog, deps = defaultDeps) {
|
|
247
|
+
const startedAt = Date.now();
|
|
248
|
+
const credential = config.imageConfig.build.registryCredential;
|
|
249
|
+
const dockerConfig = credential ? createTemporaryDockerConfig() : undefined;
|
|
250
|
+
const commandOptions = dockerConfig ? { env: dockerConfig.env } : undefined;
|
|
251
|
+
try {
|
|
252
|
+
const { imageUri, buildDurationMs } = await buildLocalImage(config, onLog, {
|
|
253
|
+
deps,
|
|
254
|
+
commandOptions
|
|
255
|
+
});
|
|
256
|
+
if (credential) {
|
|
257
|
+
const registry = resolveRegistry(imageUri);
|
|
258
|
+
await loginDockerRegistry(registry, { username: credential.username, secret: credential.password }, { deps, commandOptions });
|
|
259
|
+
}
|
|
260
|
+
const { imageDigest, pushDurationMs } = await pushLocalImage(imageUri, onLog, {
|
|
261
|
+
deps,
|
|
262
|
+
commandOptions
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
imageUri,
|
|
266
|
+
imageDigest,
|
|
267
|
+
buildDurationMs,
|
|
268
|
+
pushDurationMs,
|
|
269
|
+
durationMs: Date.now() - startedAt
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
finally {
|
|
273
|
+
dockerConfig === null || dockerConfig === void 0 ? void 0 : dockerConfig.cleanup();
|
|
274
|
+
}
|
|
129
275
|
}
|
|
@@ -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
|
+
}
|
|
@@ -71,7 +71,8 @@ function safeStat(target) {
|
|
|
71
71
|
}
|
|
72
72
|
/** 检查 cloud 构建上下文与 Dockerfile,确保任何云端操作前即可失败 */
|
|
73
73
|
function checkCloudBuildSource(config, checks) {
|
|
74
|
-
const
|
|
74
|
+
const build = config.imageConfig.build;
|
|
75
|
+
const cwd = path_1.default.resolve(build.cwd || config.functionPath || process.cwd());
|
|
75
76
|
const contextStat = safeStat(cwd);
|
|
76
77
|
if (contextStat === null) {
|
|
77
78
|
checks.push(fail(types_1.FUNCTION_DEPLOY_ERROR.BUILD_CONTEXT_INVALID, '云端镜像构建上下文目录不存在', {
|
|
@@ -87,7 +88,7 @@ function checkCloudBuildSource(config, checks) {
|
|
|
87
88
|
}));
|
|
88
89
|
return;
|
|
89
90
|
}
|
|
90
|
-
const dockerfile =
|
|
91
|
+
const dockerfile = build.dockerfile || types_1.DEFAULT_DOCKERFILE;
|
|
91
92
|
const dockerfilePath = path_1.default.resolve(cwd, dockerfile);
|
|
92
93
|
const dockerfileStat = safeStat(dockerfilePath);
|
|
93
94
|
if (dockerfileStat === null || !dockerfileStat.isFile()) {
|
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([
|
|
@@ -23,7 +23,6 @@ const path_1 = __importDefault(require("path"));
|
|
|
23
23
|
const toolbox_1 = require("@cloudbase/toolbox");
|
|
24
24
|
class ProjectValidator {
|
|
25
25
|
async validate(options = {}) {
|
|
26
|
-
var _a;
|
|
27
26
|
const cwd = path_1.default.resolve(options.cwd || process.cwd());
|
|
28
27
|
const errors = [];
|
|
29
28
|
const warnings = [];
|
|
@@ -32,7 +31,6 @@ class ProjectValidator {
|
|
|
32
31
|
mode: options.mode,
|
|
33
32
|
errors,
|
|
34
33
|
});
|
|
35
|
-
const version = String(((_a = loaded.rawConfig) === null || _a === void 0 ? void 0 : _a.version) || '2.1');
|
|
36
34
|
if (loaded.rawConfig) {
|
|
37
35
|
this.validateSchema(loaded.rawConfig, errors);
|
|
38
36
|
}
|
|
@@ -47,7 +45,6 @@ class ProjectValidator {
|
|
|
47
45
|
const summary = this.getSummary(loaded.config);
|
|
48
46
|
return {
|
|
49
47
|
valid: errors.length === 0,
|
|
50
|
-
version,
|
|
51
48
|
errors,
|
|
52
49
|
warnings,
|
|
53
50
|
summary,
|
|
@@ -92,12 +89,25 @@ class ProjectValidator {
|
|
|
92
89
|
}
|
|
93
90
|
validateSchema(config, errors) {
|
|
94
91
|
const result = (0, toolbox_1.validateCloudBaseConfigBySchema)(config);
|
|
95
|
-
result.errors
|
|
92
|
+
result.errors
|
|
93
|
+
.filter((error) => error.keyword !== 'if')
|
|
94
|
+
.forEach((error) => {
|
|
95
|
+
const errorParams = error.params;
|
|
96
|
+
const missingProperty = error.keyword === 'required' && typeof errorParams.missingProperty === 'string'
|
|
97
|
+
? errorParams.missingProperty
|
|
98
|
+
: undefined;
|
|
99
|
+
const baseField = this.normalizeSchemaPath(error.dataPath);
|
|
100
|
+
const field = [baseField, missingProperty].filter(Boolean).join('.');
|
|
101
|
+
const isImageTypeMissing = missingProperty === 'imageType';
|
|
96
102
|
errors.push({
|
|
97
103
|
level: 'error',
|
|
98
|
-
field
|
|
99
|
-
message:
|
|
100
|
-
|
|
104
|
+
field,
|
|
105
|
+
message: isImageTypeMissing
|
|
106
|
+
? 'local/cloud 镜像构建必须显式声明 imageConfig.imageType'
|
|
107
|
+
: `配置不符合 v2.1 Schema:${error.message}`,
|
|
108
|
+
fix: isImageTypeMissing
|
|
109
|
+
? '请根据镜像仓库类型填写 imageConfig.imageType: "enterprise" 或 "personal"'
|
|
110
|
+
: `请检查 ${field || 'cloudbaserc'} 字段`,
|
|
101
111
|
});
|
|
102
112
|
});
|
|
103
113
|
}
|
|
@@ -133,8 +143,9 @@ class ProjectValidator {
|
|
|
133
143
|
});
|
|
134
144
|
}
|
|
135
145
|
/**
|
|
136
|
-
* 个人版 TCR 的 username/password
|
|
137
|
-
*
|
|
146
|
+
* 个人版 TCR 的 username/password 必须在 cloudbaserc 中显式声明。
|
|
147
|
+
* namespace/repository 可省略,分别由部署器按 envId/函数名推导。
|
|
148
|
+
* ConfigParser 已先解析 {{env.*}},因此这里同时承担缺失凭证 env 值的前置校验。
|
|
138
149
|
*/
|
|
139
150
|
validatePersonalRegistryConfig(config, errors) {
|
|
140
151
|
;
|
|
@@ -144,16 +155,6 @@ class ProjectValidator {
|
|
|
144
155
|
return;
|
|
145
156
|
}
|
|
146
157
|
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
158
|
const credential = build === null || build === void 0 ? void 0 : build.registryCredential;
|
|
158
159
|
if (!credential || typeof credential !== 'object') {
|
|
159
160
|
errors.push({
|
|
@@ -190,12 +191,10 @@ class ProjectValidator {
|
|
|
190
191
|
return (config.functions || []).some((func) => this.isPersonalCloudFunction(func));
|
|
191
192
|
}
|
|
192
193
|
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');
|
|
194
|
+
var _a;
|
|
196
195
|
return (func.type === 'HTTP' &&
|
|
197
196
|
func.buildStrategy === 'cloud' &&
|
|
198
|
-
imageType === 'personal');
|
|
197
|
+
((_a = func.imageConfig) === null || _a === void 0 ? void 0 : _a.imageType) === 'personal');
|
|
199
198
|
}
|
|
200
199
|
validateResources(cwd, config) {
|
|
201
200
|
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.4",
|
|
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",
|