@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/database/index.js +11 -6
  3. package/lib/deploy/DeployOrchestrator.js +170 -278
  4. package/lib/deploy/FunctionDeployer.js +310 -52
  5. package/lib/deploy/StaticDeployer.js +7 -1
  6. package/lib/deploy/function/artifact.js +55 -41
  7. package/lib/deploy/function/builders/cloud.js +58 -359
  8. package/lib/deploy/function/cam-preflight.js +2 -1
  9. package/lib/deploy/function/cloud-build-service.js +2 -16
  10. package/lib/deploy/function/config-guard.js +210 -55
  11. package/lib/deploy/function/enterprise-tcr.js +195 -0
  12. package/lib/deploy/function/image-tag.js +31 -0
  13. package/lib/deploy/function/local-builder.js +178 -32
  14. package/lib/deploy/function/personal-tcr.js +153 -0
  15. package/lib/deploy/function/preflight.js +3 -2
  16. package/lib/deploy/types.js +11 -0
  17. package/lib/hosting/index.js +27 -0
  18. package/lib/projectValidator/index.js +22 -23
  19. package/package.json +2 -2
  20. package/types/database/index.d.ts +1 -0
  21. package/types/deploy/DeployOrchestrator.d.ts +19 -25
  22. package/types/deploy/FunctionDeployer.d.ts +35 -3
  23. package/types/deploy/StaticDeployer.d.ts +11 -0
  24. package/types/deploy/function/artifact.d.ts +0 -5
  25. package/types/deploy/function/builders/cloud.d.ts +6 -39
  26. package/types/deploy/function/config-guard.d.ts +18 -0
  27. package/types/deploy/function/enterprise-tcr.d.ts +32 -0
  28. package/types/deploy/function/image-tag.d.ts +10 -0
  29. package/types/deploy/function/local-builder.d.ts +35 -6
  30. package/types/deploy/function/personal-tcr.d.ts +35 -0
  31. package/types/deploy/types.d.ts +110 -28
  32. package/types/hosting/index.d.ts +13 -0
  33. package/types/projectValidator/index.d.ts +3 -2
  34. package/types/projectValidator/types.d.ts +0 -1
  35. package/lib/deploy/StateStore.js +0 -328
  36. 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
- (0, child_process_1.execFile)(command, args, { timeout: BUILD_TIMEOUT, shell: false, windowsHide: true, maxBuffer: 32 * 1024 * 1024 }, (error, stdout, stderr) => {
35
+ var _a, _b;
36
+ const child = (0, child_process_1.execFile)(command, args, {
37
+ timeout: BUILD_TIMEOUT,
38
+ shell: false,
39
+ windowsHide: true,
40
+ maxBuffer: 32 * 1024 * 1024,
41
+ env: options.env ? Object.assign(Object.assign({}, process.env), options.env) : process.env
42
+ }, (error, stdout, stderr) => {
30
43
  resolve({
31
44
  ok: !error,
32
45
  stdout: (stdout || '').toString(),
33
46
  stderr: (stderr || '').toString()
34
47
  });
35
48
  });
49
+ if (options.input !== undefined) {
50
+ (_a = child.stdin) === null || _a === void 0 ? void 0 : _a.write(options.input);
51
+ (_b = child.stdin) === null || _b === void 0 ? void 0 : _b.end();
52
+ }
36
53
  });
37
54
  }
38
55
  /** 默认依赖:真实命令执行与 fs 检查 */
@@ -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 repository = config.build.repository;
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
- // tag 缺省用时间戳,避免 latest;不可变 tag 便于回溯
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
- * @param config 规范化的 local 部署配置
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
- // 组装 buildx 参数:构建目标平台镜像并直接推送
108
- const args = ['buildx', 'build', '--platform', platform, '-f', dockerfilePath, '-t', imageUri];
109
- if (config.build.forceBuild) {
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
- // 追加构建参数(键值均作为独立参数,无 shell 拼接)
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('--push', context);
137
+ args.push(context);
118
138
  onLog === null || onLog === void 0 ? void 0 : onLog(`执行 ${formatBuildCommandForLog(args)}`);
119
- const build = await deps.runCommand('docker', args);
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('镜像构建或推送失败,请查看 Docker 构建输出定位具体原因', {
122
- code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_PUSH_FAILED
143
+ throw new error_1.CloudBaseError('镜像构建失败,请检查 Dockerfile、构建上下文和本地 Docker 输出。', {
144
+ code: types_1.FUNCTION_DEPLOY_ERROR.LOCAL_BUILD_FAILED
123
145
  });
124
146
  }
125
- // 查询远端 digest,产出不可变引用
126
- const inspect = await deps.runCommand('docker', ['buildx', 'imagetools', 'inspect', imageUri]);
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
- return { imageUri, imageDigest };
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 cwd = path_1.default.resolve(config.build.cwd || config.functionPath || process.cwd());
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 = config.build.dockerfile || types_1.DEFAULT_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()) {
@@ -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 的本地检查)
@@ -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.forEach((error) => {
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: this.normalizeSchemaPath(error.dataPath),
99
- message: `配置不符合 v2.1 Schema:${error.message}`,
100
- fix: `请检查 ${this.normalizeSchemaPath(error.dataPath) || 'cloudbaserc'} 字段`,
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/namespace 必须在 cloudbaserc 中显式声明。
137
- * ConfigParser 已先解析 {{env.*}},因此这里同时承担缺失 env 值的前置校验。
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, _b, _c, _d, _e, _f;
194
- const registryId = (_b = (_a = func.imageConfig) === null || _a === void 0 ? void 0 : _a.registryId) !== null && _b !== void 0 ? _b : (_d = (_c = func.imageConfig) === null || _c === void 0 ? void 0 : _c.build) === null || _d === void 0 ? void 0 : _d.registryId;
195
- const imageType = (_f = (_e = func.imageConfig) === null || _e === void 0 ? void 0 : _e.imageType) !== null && _f !== void 0 ? _f : (registryId ? 'enterprise' : 'personal');
194
+ 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.2",
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.3",
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",