@cloudbase/manager-node 5.8.0-beta.0 → 5.8.0-beta.2
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/lib/deploy/DeployOrchestrator.js +8 -4
- package/lib/deploy/FunctionDeployer.js +16 -21
- package/lib/deploy/StateStore.js +1 -0
- package/lib/deploy/function/artifact.js +34 -9
- package/lib/deploy/function/builders/cloud.js +129 -4
- package/lib/deploy/function/cam-preflight.js +95 -79
- package/lib/deploy/function/config-guard.js +138 -70
- package/lib/deploy/function/local-builder.js +1 -1
- package/lib/deploy/types.js +5 -1
- package/lib/function/index.js +34 -4
- package/lib/projectValidator/index.js +5 -3
- package/package.json +2 -2
- package/types/deploy/FunctionDeployer.d.ts +2 -0
- package/types/deploy/StateStore.d.ts +4 -0
- package/types/deploy/function/builders/cloud.d.ts +1 -1
- package/types/deploy/function/cam-preflight.d.ts +19 -23
- package/types/deploy/types.d.ts +38 -5
- package/types/function/index.d.ts +3 -0
|
@@ -158,7 +158,7 @@ class DeployOrchestrator {
|
|
|
158
158
|
// (失败资源保留旧快照,下次 diff 仍会检测到差异);快照写入失败不影响部署结果
|
|
159
159
|
if (!dryRun) {
|
|
160
160
|
try {
|
|
161
|
-
await this.persistState({ config, plan, results, cwd });
|
|
161
|
+
await this.persistState({ config, plan, results, cwd, envId: options.envId });
|
|
162
162
|
}
|
|
163
163
|
catch (e) {
|
|
164
164
|
(_j = log === null || log === void 0 ? void 0 : log.warn) === null || _j === void 0 ? void 0 : _j.call(log, `状态快照写入失败:${(e === null || e === void 0 ? void 0 : e.message) || String(e)}`);
|
|
@@ -203,7 +203,7 @@ class DeployOrchestrator {
|
|
|
203
203
|
*/
|
|
204
204
|
async persistState(options) {
|
|
205
205
|
var _a, _b, _c, _d, _e, _f;
|
|
206
|
-
const { config, plan, results, cwd } = options;
|
|
206
|
+
const { config, plan, results, cwd, envId } = options;
|
|
207
207
|
const stateStore = new StateStore_1.StateStore({ cwd });
|
|
208
208
|
const prev = stateStore.read();
|
|
209
209
|
// hosting
|
|
@@ -263,7 +263,7 @@ class DeployOrchestrator {
|
|
|
263
263
|
resources.gateway = prev.resources.gateway;
|
|
264
264
|
}
|
|
265
265
|
if (Object.keys(resources).length) {
|
|
266
|
-
stateStore.write({ resources });
|
|
266
|
+
stateStore.write({ resources, envId });
|
|
267
267
|
}
|
|
268
268
|
}
|
|
269
269
|
/**
|
|
@@ -291,7 +291,11 @@ class DeployOrchestrator {
|
|
|
291
291
|
};
|
|
292
292
|
// 本地 state 快照(diff 基准):--refresh 时忽略本地 skip 判定
|
|
293
293
|
const stateStore = new StateStore_1.StateStore({ cwd });
|
|
294
|
-
const
|
|
294
|
+
const rawState = refresh ? null : stateStore.read();
|
|
295
|
+
// 跨环境保护:快照记录的 envId 与目标环境不一致(或旧格式快照无 envId)时,
|
|
296
|
+
// 快照是其他环境部署后的指纹,不可作为 skip 依据 → 视为无快照,强制走全量部署,
|
|
297
|
+
// 避免切换环境后误判「产物未变更」导致新环境 COS 404
|
|
298
|
+
const state = rawState && rawState.envId === envId ? rawState : null;
|
|
295
299
|
const planCtx = { config, envId, stateStore, state, refresh, cwd };
|
|
296
300
|
for (const type of DEPLOY_ORDER) {
|
|
297
301
|
if (!isEnabled(type))
|
|
@@ -199,9 +199,7 @@ class FunctionDeployer {
|
|
|
199
199
|
const ctx = { steps: [], options };
|
|
200
200
|
const warnings = [];
|
|
201
201
|
const report = (0, config_guard_1.checkDeployConfig)(config);
|
|
202
|
-
report
|
|
203
|
-
.filter(item => item.status === 'warn')
|
|
204
|
-
.forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
|
|
202
|
+
this.appendWarnings(warnings, report);
|
|
205
203
|
const normalized = await this.runStep(ctx, 'validate', async () => {
|
|
206
204
|
return (0, config_guard_1.assertDeployConfig)(config);
|
|
207
205
|
});
|
|
@@ -221,26 +219,20 @@ class FunctionDeployer {
|
|
|
221
219
|
}
|
|
222
220
|
// 不可原地更新的变更必须在任何可能产生副作用的 Preflight 之前中止。
|
|
223
221
|
// 尤其避免镜像拉取授权自动补齐等云端写操作先发生,再提示用户手工删除函数。
|
|
224
|
-
this.assertPlanExecutable(
|
|
222
|
+
this.assertPlanExecutable(plan);
|
|
225
223
|
await this.runStep(ctx, 'preflight', async () => {
|
|
226
224
|
const report = (0, preflight_1.runLocalPreflight)(normalized);
|
|
227
|
-
report
|
|
228
|
-
.filter(item => item.status === 'warn')
|
|
229
|
-
.forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
|
|
225
|
+
this.appendWarnings(warnings, report);
|
|
230
226
|
this.assertPreflightReady(report);
|
|
231
227
|
});
|
|
232
228
|
await this.runStep(ctx, 'preflight', async () => {
|
|
233
229
|
const report = await (0, cam_preflight_1.runImagePullAuthPreflight)(this.createCamPreflightAdapter(), normalized, options.autoGrant !== false);
|
|
234
|
-
report
|
|
235
|
-
.filter(item => item.status === 'warn')
|
|
236
|
-
.forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
|
|
230
|
+
this.appendWarnings(warnings, report);
|
|
237
231
|
this.assertPreflightReady(report);
|
|
238
232
|
});
|
|
239
233
|
await this.runStep(ctx, 'preflight', async () => {
|
|
240
234
|
const report = await (0, image_preflight_1.runImageRemotePreflight)(normalized, this.resolveDeployRegion(), this.imagePreflightDeps);
|
|
241
|
-
report
|
|
242
|
-
.filter(item => item.status === 'warn')
|
|
243
|
-
.forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
|
|
235
|
+
this.appendWarnings(warnings, report);
|
|
244
236
|
this.assertPreflightReady(report);
|
|
245
237
|
});
|
|
246
238
|
// noop:函数本身无变更,但仍可能需要收敛访问配置
|
|
@@ -367,6 +359,15 @@ class FunctionDeployer {
|
|
|
367
359
|
const code = (error === null || error === void 0 ? void 0 : error.code) || ((_b = (_a = error === null || error === void 0 ? void 0 : error.original) === null || _a === void 0 ? void 0 : _a.Error) === null || _b === void 0 ? void 0 : _b.Code) || '';
|
|
368
360
|
return typeof code === 'string' && code.startsWith('ResourceNotFound');
|
|
369
361
|
}
|
|
362
|
+
/** 将 warning 连同可执行修复建议汇入部署结果,避免授权链接在编排层丢失 */
|
|
363
|
+
appendWarnings(warnings, report) {
|
|
364
|
+
report.checks
|
|
365
|
+
.filter(item => item.status === 'warn')
|
|
366
|
+
.forEach(item => {
|
|
367
|
+
const remediation = item.remediation ? `,${item.remediation}` : '';
|
|
368
|
+
warnings.push(`[${item.id}] ${item.summary}${remediation}`);
|
|
369
|
+
});
|
|
370
|
+
}
|
|
370
371
|
/**
|
|
371
372
|
* 校验 Preflight 报告,存在 fail 项时聚合抛错
|
|
372
373
|
*/
|
|
@@ -390,16 +391,12 @@ class FunctionDeployer {
|
|
|
390
391
|
*
|
|
391
392
|
* 未实现的能力必须显式报错,避免用户误认为访问规则或镜像构建已经生效
|
|
392
393
|
*/
|
|
393
|
-
assertPlanExecutable(
|
|
394
|
+
assertPlanExecutable(plan) {
|
|
394
395
|
if (plan.action === 'replace') {
|
|
395
396
|
throw new error_1.CloudBaseError(`函数 ${plan.functionName} 存在无法原地更新的变更:\n` +
|
|
396
397
|
plan.replaceReasons.map(reason => `- ${reason}`).join('\n') +
|
|
397
398
|
'\n部署器不会自动删除已有云函数。请先在云开发控制台确认触发器、访问配置和服务中断影响,手工删除该函数后重新部署。', { code: types_1.FUNCTION_DEPLOY_ERROR.FUNCTION_RECREATE_REQUIRED });
|
|
398
399
|
}
|
|
399
|
-
// local 个人版先行已支持;企业版 TCR 增强后置,暂未实现
|
|
400
|
-
if (config.buildStrategy === 'local' && config.imageType === 'enterprise') {
|
|
401
|
-
throw new error_1.CloudBaseError('local 策略的企业版 TCR 构建推送尚未实现,当前支持个人版镜像仓库(不指定 imageType 或 personal)', { code: types_1.FUNCTION_DEPLOY_ERROR.NOT_IMPLEMENTED });
|
|
402
|
-
}
|
|
403
400
|
}
|
|
404
401
|
/**
|
|
405
402
|
* 准备部署物
|
|
@@ -427,9 +424,7 @@ class FunctionDeployer {
|
|
|
427
424
|
if (config.buildStrategy === 'local') {
|
|
428
425
|
await this.runStep(ctx, 'preflight', async () => {
|
|
429
426
|
const report = await (0, docker_preflight_1.runDockerPreflight)();
|
|
430
|
-
report
|
|
431
|
-
.filter(item => item.status === 'warn')
|
|
432
|
-
.forEach(item => warnings.push(`[${item.id}] ${item.summary}`));
|
|
427
|
+
this.appendWarnings(warnings, report);
|
|
433
428
|
this.assertPreflightReady(report);
|
|
434
429
|
});
|
|
435
430
|
const localConfig = config;
|
package/lib/deploy/StateStore.js
CHANGED
|
@@ -78,6 +78,7 @@ class StateStore {
|
|
|
78
78
|
version: 1,
|
|
79
79
|
updatedAt: new Date().toISOString(),
|
|
80
80
|
gitCommit: (_a = state.gitCommit) !== null && _a !== void 0 ? _a : this.resolveGitCommit(),
|
|
81
|
+
envId: state.envId,
|
|
81
82
|
resources: state.resources
|
|
82
83
|
};
|
|
83
84
|
fs_1.default.writeFileSync(filePath, JSON.stringify(full, null, 2), 'utf8');
|
|
@@ -33,10 +33,9 @@ function createImageArtifactFromConfig(config) {
|
|
|
33
33
|
// imageType/registryId 的契约位置在配置顶层(IHttpImageRuntimeCommon),
|
|
34
34
|
// 与 local/cloud 策略一致;imageConfig 内的同名字段仅作向后兼容兜底。
|
|
35
35
|
// 个人版镜像(personal,如个人版 CCR)不应携带 registryId。
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
: undefined;
|
|
36
|
+
const configuredRegistryId = (_a = config.registryId) !== null && _a !== void 0 ? _a : imageConfig.registryId;
|
|
37
|
+
const imageType = (_c = (_b = config.imageType) !== null && _b !== void 0 ? _b : imageConfig.imageType) !== null && _c !== void 0 ? _c : (configuredRegistryId ? 'enterprise' : 'personal');
|
|
38
|
+
const registryId = imageType === 'enterprise' ? configuredRegistryId : undefined;
|
|
40
39
|
return {
|
|
41
40
|
kind: 'image',
|
|
42
41
|
deployMode: 'image',
|
|
@@ -60,14 +59,18 @@ function createImageArtifactFromConfig(config) {
|
|
|
60
59
|
* 此处仅承载最终 imageUri/digest 与容器启动配置,之后复用镜像部署链。
|
|
61
60
|
*/
|
|
62
61
|
function createImageArtifactFromBuild(config, build) {
|
|
63
|
-
var _a;
|
|
62
|
+
var _a, _b, _c;
|
|
63
|
+
const imageType = (_a = config.imageType) !== null && _a !== void 0 ? _a : (((_b = config.registryId) !== null && _b !== void 0 ? _b : config.build.registryId) ? 'enterprise' : 'personal');
|
|
64
|
+
const registryId = imageType === 'enterprise'
|
|
65
|
+
? (_c = config.registryId) !== null && _c !== void 0 ? _c : config.build.registryId
|
|
66
|
+
: undefined;
|
|
64
67
|
return {
|
|
65
68
|
kind: 'image',
|
|
66
69
|
deployMode: 'image',
|
|
67
70
|
imageUri: build.imageUri,
|
|
68
71
|
imageDigest: build.imageDigest,
|
|
69
|
-
|
|
70
|
-
|
|
72
|
+
imageType,
|
|
73
|
+
registryId,
|
|
71
74
|
imagePort: types_1.IMAGE_FUNCTION_PORT,
|
|
72
75
|
effectiveStrategy: 'local',
|
|
73
76
|
entryPoint: config.entryPoint,
|
|
@@ -112,11 +115,18 @@ function createImageArtifactFromCloudBuild(config, build) {
|
|
|
112
115
|
containerImageAccelerate: config.containerImageAccelerate
|
|
113
116
|
};
|
|
114
117
|
}
|
|
118
|
+
/** 将构建产出的 tag 地址与 digest 合成为 SCF 支持的不可变镜像引用 */
|
|
119
|
+
function withImageDigest(imageUri, imageDigest) {
|
|
120
|
+
if (!imageDigest || imageUri.includes('@')) {
|
|
121
|
+
return imageUri;
|
|
122
|
+
}
|
|
123
|
+
return `${imageUri}@${imageDigest}`;
|
|
124
|
+
}
|
|
115
125
|
/** 将镜像部署物转换为 SCF 镜像配置 */
|
|
116
126
|
function toFunctionImageConfig(artifact) {
|
|
117
127
|
const imageConfig = {
|
|
118
128
|
imageType: artifact.imageType,
|
|
119
|
-
imageUri: artifact.imageUri,
|
|
129
|
+
imageUri: withImageDigest(artifact.imageUri, artifact.imageDigest),
|
|
120
130
|
imagePort: artifact.imagePort
|
|
121
131
|
};
|
|
122
132
|
if (artifact.registryId !== undefined) {
|
|
@@ -165,7 +175,22 @@ function toCloudFunction(config, artifact) {
|
|
|
165
175
|
'role',
|
|
166
176
|
'codeSecret',
|
|
167
177
|
'l5',
|
|
168
|
-
'ignore'
|
|
178
|
+
'ignore',
|
|
179
|
+
// —— 高级配置:与 fn deploy 传统通道(configToParams 白名单)对齐,
|
|
180
|
+
// 字段名需与其大小写不敏感匹配规则一致,否则会被下游静默丢弃 ——
|
|
181
|
+
'tags',
|
|
182
|
+
'clsLogsetId',
|
|
183
|
+
'clsTopicId',
|
|
184
|
+
'deadLetterConfig',
|
|
185
|
+
'publicNetConfig',
|
|
186
|
+
'cfsConfig',
|
|
187
|
+
'intranetConfig',
|
|
188
|
+
'initTimeout',
|
|
189
|
+
'asyncRunEnable',
|
|
190
|
+
'traceEnable',
|
|
191
|
+
'autoDeployClsTopicIndex',
|
|
192
|
+
'autoCreateClsTopic',
|
|
193
|
+
'dnsCache'
|
|
169
194
|
];
|
|
170
195
|
passthroughFields.forEach(field => {
|
|
171
196
|
if (source[field] !== undefined) {
|
|
@@ -161,7 +161,8 @@ if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
|
|
|
161
161
|
TIMESTAMP=$(date +%s)
|
|
162
162
|
DATE=$(date -u -d "@\${TIMESTAMP}" +"%Y-%m-%d" 2>/dev/null || date -u -r "\${TIMESTAMP}" +"%Y-%m-%d")
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
# 构建阶段只需要一次 docker login/push,使用 1 小时临时凭证,避免创建长期凭证。
|
|
165
|
+
PAYLOAD=$(printf '{"RegistryId":"%s","TokenType":"temp"}' "\${TCR_INSTANCE_ID}")
|
|
165
166
|
HASHED_PAYLOAD=$(printf '%s' "\${PAYLOAD}" | openssl dgst -sha256 -hex | awk '{print $NF}')
|
|
166
167
|
CANONICAL_HEADERS="content-type:application/json; charset=utf-8\\nhost:\${HOST}\\nx-tc-action:$(echo "\${ACTION}" | tr '[:upper:]' '[:lower:]')\\n"
|
|
167
168
|
SIGNED_HEADERS="content-type;host;x-tc-action"
|
|
@@ -189,7 +190,123 @@ if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
|
|
|
189
190
|
-H "X-TC-Token: \${API_TOKEN}" \\
|
|
190
191
|
--data "\${PAYLOAD}")
|
|
191
192
|
|
|
192
|
-
|
|
193
|
+
# CloudApp custom Runner 已提供 awk(上面的 TC3 签名也依赖它),但不保证安装 jq。
|
|
194
|
+
# 这里先限定顶层 Response 对象,再解析其中的非空 JSON 字符串字段;凭证字段
|
|
195
|
+
# 不允许 JSON 转义,避免用 sed/grep 对认证响应做宽松匹配。
|
|
196
|
+
extract_json_string() {
|
|
197
|
+
local field="$1"
|
|
198
|
+
printf '%s' "\${RESP}" | JSON_FIELD="\${field}" awk '
|
|
199
|
+
BEGIN {
|
|
200
|
+
field = ENVIRON["JSON_FIELD"]
|
|
201
|
+
quote = sprintf("%c", 34)
|
|
202
|
+
backslash = sprintf("%c", 92)
|
|
203
|
+
json = ""
|
|
204
|
+
}
|
|
205
|
+
{ json = json $0 }
|
|
206
|
+
END {
|
|
207
|
+
response_needle = quote "Response" quote "[[:space:]]*:[[:space:]]*\\{"
|
|
208
|
+
if (!match(json, response_needle)) exit 1
|
|
209
|
+
response_start = RSTART + RLENGTH - 1
|
|
210
|
+
response = ""
|
|
211
|
+
depth = 0
|
|
212
|
+
in_string = 0
|
|
213
|
+
escaped = 0
|
|
214
|
+
for (i = response_start; i <= length(json); i++) {
|
|
215
|
+
ch = substr(json, i, 1)
|
|
216
|
+
response = response ch
|
|
217
|
+
if (in_string) {
|
|
218
|
+
if (escaped) escaped = 0
|
|
219
|
+
else if (ch == backslash) escaped = 1
|
|
220
|
+
else if (ch == quote) in_string = 0
|
|
221
|
+
continue
|
|
222
|
+
}
|
|
223
|
+
if (ch == quote) in_string = 1
|
|
224
|
+
else if (ch == "{") depth++
|
|
225
|
+
else if (ch == "}") {
|
|
226
|
+
depth--
|
|
227
|
+
if (depth == 0) break
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (depth != 0) exit 1
|
|
231
|
+
|
|
232
|
+
depth = 1
|
|
233
|
+
for (i = 2; i < length(response); i++) {
|
|
234
|
+
ch = substr(response, i, 1)
|
|
235
|
+
if (ch == "{") depth++
|
|
236
|
+
else if (ch == "}") depth--
|
|
237
|
+
else if (ch == quote) {
|
|
238
|
+
key = ""
|
|
239
|
+
escaped = 0
|
|
240
|
+
for (j = i + 1; j <= length(response); j++) {
|
|
241
|
+
current = substr(response, j, 1)
|
|
242
|
+
if (escaped) {
|
|
243
|
+
escaped = 0
|
|
244
|
+
key = key current
|
|
245
|
+
} else if (current == backslash) {
|
|
246
|
+
escaped = 1
|
|
247
|
+
key = key current
|
|
248
|
+
} else if (current == quote) break
|
|
249
|
+
else key = key current
|
|
250
|
+
}
|
|
251
|
+
if (j > length(response)) exit 1
|
|
252
|
+
k = j + 1
|
|
253
|
+
while (substr(response, k, 1) ~ /[[:space:]]/) k++
|
|
254
|
+
if (depth == 1 && substr(response, k, 1) == ":" && key == field) {
|
|
255
|
+
k++
|
|
256
|
+
while (substr(response, k, 1) ~ /[[:space:]]/) k++
|
|
257
|
+
if (substr(response, k, 1) != quote) exit 1
|
|
258
|
+
value = ""
|
|
259
|
+
for (k = k + 1; k <= length(response); k++) {
|
|
260
|
+
current = substr(response, k, 1)
|
|
261
|
+
if (current == quote) {
|
|
262
|
+
if (length(value) == 0) exit 1
|
|
263
|
+
printf "%s", value
|
|
264
|
+
exit 0
|
|
265
|
+
}
|
|
266
|
+
if (current == backslash) exit 2
|
|
267
|
+
value = value current
|
|
268
|
+
}
|
|
269
|
+
exit 1
|
|
270
|
+
}
|
|
271
|
+
i = j
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
exit 1
|
|
275
|
+
}
|
|
276
|
+
'
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
extract_tcr_error_code() {
|
|
280
|
+
printf '%s' "\${RESP}" | awk '
|
|
281
|
+
BEGIN {
|
|
282
|
+
quote = sprintf("%c", 34)
|
|
283
|
+
json = ""
|
|
284
|
+
}
|
|
285
|
+
{ json = json $0 }
|
|
286
|
+
END {
|
|
287
|
+
error_needle = quote "Error" quote "[[:space:]]*:[[:space:]]*\\{"
|
|
288
|
+
if (!match(json, error_needle)) exit 1
|
|
289
|
+
error_json = substr(json, RSTART + RLENGTH)
|
|
290
|
+
code_needle = quote "Code" quote "[[:space:]]*:[[:space:]]*" quote
|
|
291
|
+
if (!match(error_json, code_needle)) exit 1
|
|
292
|
+
rest = substr(error_json, RSTART + RLENGTH)
|
|
293
|
+
value = ""
|
|
294
|
+
for (i = 1; i <= length(rest); i++) {
|
|
295
|
+
ch = substr(rest, i, 1)
|
|
296
|
+
if (ch == quote) {
|
|
297
|
+
if (length(value) == 0) exit 1
|
|
298
|
+
printf "%s", value
|
|
299
|
+
exit 0
|
|
300
|
+
}
|
|
301
|
+
if (ch == backslash) exit 1
|
|
302
|
+
value = value ch
|
|
303
|
+
}
|
|
304
|
+
exit 1
|
|
305
|
+
}
|
|
306
|
+
'
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
ERROR_CODE=$(extract_tcr_error_code || true)
|
|
193
310
|
if [ -n "\${ERROR_CODE}" ]; then
|
|
194
311
|
case "\${ERROR_CODE}" in
|
|
195
312
|
AuthFailure*|UnauthorizedOperation*|FailedOperation.NoPermission*)
|
|
@@ -207,8 +324,16 @@ if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
|
|
|
207
324
|
esac
|
|
208
325
|
fi
|
|
209
326
|
|
|
210
|
-
TOKEN_USER=$(
|
|
211
|
-
|
|
327
|
+
if ! TOKEN_USER=$(extract_json_string "Username"); then
|
|
328
|
+
ERROR_CODE=$(extract_tcr_error_code || true)
|
|
329
|
+
echo "CreateInstanceToken 未返回有效 Username\${ERROR_CODE:+,错误码:\${ERROR_CODE}}" >&2
|
|
330
|
+
exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
|
|
331
|
+
fi
|
|
332
|
+
if ! TOKEN_PASS=$(extract_json_string "Token"); then
|
|
333
|
+
ERROR_CODE=$(extract_tcr_error_code || true)
|
|
334
|
+
echo "CreateInstanceToken 未返回有效 Token\${ERROR_CODE:+,错误码:\${ERROR_CODE}}" >&2
|
|
335
|
+
exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
|
|
336
|
+
fi
|
|
212
337
|
|
|
213
338
|
printf '%s' "\${TOKEN_PASS}" | docker login -u "\${TOKEN_USER}" --password-stdin "\${TCR_REGISTRY}"
|
|
214
339
|
else
|
|
@@ -1,31 +1,36 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.IMAGE_PULL_GRANT_URL = exports.REQUIRED_IMAGE_PULL_POLICY = exports.SCF_PULL_IMAGE_ROLE = void 0;
|
|
3
|
+
exports.TCR_PUSH_GRANT_URL = exports.IMAGE_PULL_GRANT_URL = exports.REQUIRED_TCR_PUSH_POLICY = exports.TCB_PUSH_ENTERPRISE_IMAGE_ROLE = exports.REQUIRED_IMAGE_PULL_POLICY = exports.SCF_PULL_IMAGE_ROLE = void 0;
|
|
4
4
|
exports.isCamPermissionError = isCamPermissionError;
|
|
5
5
|
exports.runImagePullAuthPreflight = runImagePullAuthPreflight;
|
|
6
6
|
const types_1 = require("../types");
|
|
7
7
|
/**
|
|
8
|
-
*
|
|
8
|
+
* 镜像仓库授权 Preflight(涉及 CAM,scope: cloud)
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 2. 缺失且允许时,自动绑定「唯一白名单策略」补齐(绝不放大到其它策略);
|
|
14
|
-
* 3. 当前身份无 CAM 权限时不硬失败,降级为结构化指引 + 一键授权链接兜底。
|
|
10
|
+
* 镜像部署涉及两个职责不同的服务角色:
|
|
11
|
+
* - SCF_QcsRole:SCF 从个人版或企业版 TCR 拉取镜像;
|
|
12
|
+
* - TCB_QcsRole:CloudApp 云端构建向企业版 TCR 推送镜像。
|
|
15
13
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - 探测/授权失败一律走降级,不阻断到无法给出可操作指引。
|
|
14
|
+
* 本模块在部署前探测所需角色与策略;缺失且允许时,只自动绑定各场景明确列入
|
|
15
|
+
* 白名单的预置策略;当前身份无 CAM 权限时,则返回结构化指引和一键授权链接。
|
|
16
|
+
* 模块不会创建角色,也不会绑定白名单之外的策略。
|
|
20
17
|
*/
|
|
21
18
|
/** SCF 拉取镜像使用的服务角色 */
|
|
22
19
|
exports.SCF_PULL_IMAGE_ROLE = 'SCF_QcsRole';
|
|
23
|
-
/**
|
|
20
|
+
/** SCF 拉取个人版或企业版 TCR 镜像所需的预置策略 */
|
|
24
21
|
exports.REQUIRED_IMAGE_PULL_POLICY = 'QcloudAccessForSCFRoleInPullImage';
|
|
25
|
-
/**
|
|
22
|
+
/** CloudApp 云端构建向企业版 TCR 推送镜像使用的服务角色 */
|
|
23
|
+
exports.TCB_PUSH_ENTERPRISE_IMAGE_ROLE = 'TCB_QcsRole';
|
|
24
|
+
/** CloudApp 云端构建向企业版 TCR 推送镜像所需的预置策略 */
|
|
25
|
+
exports.REQUIRED_TCR_PUSH_POLICY = 'QcloudAccessForTCBRoleInTCR';
|
|
26
|
+
/** SCF 拉取镜像的一键授权链接 */
|
|
26
27
|
exports.IMAGE_PULL_GRANT_URL = 'https://console.cloud.tencent.com/cam/role/grant' +
|
|
27
28
|
`?roleName=${exports.SCF_PULL_IMAGE_ROLE}&policyName=${exports.REQUIRED_IMAGE_PULL_POLICY}` +
|
|
28
29
|
'&principal=eyJzZXJ2aWNlIjoic2NmLnFjbG91ZC5jb20ifQ%3D%3D';
|
|
30
|
+
/** 企业版 TCR 云端构建推送的一键授权链接 */
|
|
31
|
+
exports.TCR_PUSH_GRANT_URL = 'https://console.cloud.tencent.com/cam/role/grant' +
|
|
32
|
+
`?roleName=${exports.TCB_PUSH_ENTERPRISE_IMAGE_ROLE}&policyName=${exports.REQUIRED_TCR_PUSH_POLICY}` +
|
|
33
|
+
'&principal=eyJzZXJ2aWNlIjoidGNiLmNsb3VkLnRlbmNlbnQuY29tIn0%3D';
|
|
29
34
|
/** 判断错误是否为 CAM 权限不足 */
|
|
30
35
|
function isCamPermissionError(error) {
|
|
31
36
|
const code = typeof (error === null || error === void 0 ? void 0 : error.code) === 'string' ? error.code : '';
|
|
@@ -60,98 +65,109 @@ function buildReport(checks) {
|
|
|
60
65
|
checks
|
|
61
66
|
};
|
|
62
67
|
}
|
|
63
|
-
/**
|
|
68
|
+
/** image/local/cloud 最终都需要 SCF 从镜像仓库拉取镜像 */
|
|
64
69
|
function needsImagePullAuth(config) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
70
|
+
return (config.type === 'HTTP' &&
|
|
71
|
+
(config.buildStrategy === 'image' ||
|
|
72
|
+
config.buildStrategy === 'local' ||
|
|
73
|
+
config.buildStrategy === 'cloud'));
|
|
74
|
+
}
|
|
75
|
+
/** 只有企业版 cloud 策略需要 CloudApp 向企业版 TCR 推送镜像 */
|
|
76
|
+
function needsEnterpriseTcrPushAuth(config) {
|
|
77
|
+
return (config.type === 'HTTP' &&
|
|
78
|
+
config.buildStrategy === 'cloud' &&
|
|
79
|
+
config.imageType === 'enterprise');
|
|
80
|
+
}
|
|
81
|
+
function resolveRequirements(config) {
|
|
82
|
+
const requirements = [];
|
|
83
|
+
if (needsImagePullAuth(config)) {
|
|
84
|
+
requirements.push({
|
|
85
|
+
roleName: exports.SCF_PULL_IMAGE_ROLE,
|
|
86
|
+
policyName: exports.REQUIRED_IMAGE_PULL_POLICY,
|
|
87
|
+
purpose: 'SCF 拉取 TCR 镜像',
|
|
88
|
+
grantUrl: exports.IMAGE_PULL_GRANT_URL,
|
|
89
|
+
roleMissingCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_ROLE_MISSING,
|
|
90
|
+
policyMissingCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_POLICY_MISSING,
|
|
91
|
+
grantedCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_AUTH_GRANTED,
|
|
92
|
+
unverifiableCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_AUTH_UNVERIFIABLE
|
|
93
|
+
});
|
|
74
94
|
}
|
|
75
|
-
if (config
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
95
|
+
if (needsEnterpriseTcrPushAuth(config)) {
|
|
96
|
+
requirements.push({
|
|
97
|
+
roleName: exports.TCB_PUSH_ENTERPRISE_IMAGE_ROLE,
|
|
98
|
+
policyName: exports.REQUIRED_TCR_PUSH_POLICY,
|
|
99
|
+
purpose: 'CloudApp 云端构建推送企业版 TCR 镜像',
|
|
100
|
+
grantUrl: exports.TCR_PUSH_GRANT_URL,
|
|
101
|
+
roleMissingCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_TCR_PUSH_ROLE_MISSING,
|
|
102
|
+
policyMissingCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_TCR_PUSH_POLICY_MISSING,
|
|
103
|
+
grantedCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_TCR_PUSH_AUTH_GRANTED,
|
|
104
|
+
unverifiableCode: types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_TCR_PUSH_AUTH_UNVERIFIABLE
|
|
105
|
+
});
|
|
80
106
|
}
|
|
81
|
-
return
|
|
107
|
+
return requirements;
|
|
82
108
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return warn(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_IMAGE_PULL_AUTH_UNVERIFIABLE, `当前身份无 CAM 权限,无法自动校验或补齐 ${exports.SCF_PULL_IMAGE_ROLE} 的拉镜像授权`, `若个人版镜像拉取失败,请用具备 CAM 权限的账号访问一键授权链接完成授权:${exports.IMAGE_PULL_GRANT_URL}`);
|
|
109
|
+
function unverifiableCheck(requirement) {
|
|
110
|
+
return warn(requirement.unverifiableCode, `当前身份无 CAM 权限,无法自动校验或补齐 ${requirement.roleName} 的${requirement.purpose}授权`, `若部署失败,请用具备 CAM 权限的账号访问一键授权链接完成授权:${requirement.grantUrl}`);
|
|
86
111
|
}
|
|
87
|
-
|
|
88
|
-
* 执行镜像拉取授权 Preflight
|
|
89
|
-
*
|
|
90
|
-
* @param service CAM 能力适配(无则视为无法校验,直接降级为提示)
|
|
91
|
-
* @param config 规范化部署配置
|
|
92
|
-
* @param autoGrant 是否允许自动补齐授权(默认 true)
|
|
93
|
-
* @returns scope 为 cloud 的检查报告,聚合全部结论
|
|
94
|
-
*/
|
|
95
|
-
async function runImagePullAuthPreflight(service, config, autoGrant = true) {
|
|
96
|
-
if (!needsImagePullAuth(config)) {
|
|
97
|
-
return buildReport([]);
|
|
98
|
-
}
|
|
99
|
-
// 无 CAM 适配能力:不阻断,仅提示手动授权入口
|
|
112
|
+
async function checkRequirement(service, requirement, autoGrant) {
|
|
100
113
|
if (!service) {
|
|
101
|
-
return
|
|
114
|
+
return unverifiableCheck(requirement);
|
|
102
115
|
}
|
|
103
|
-
const checks = [];
|
|
104
116
|
let roleDetail;
|
|
105
117
|
try {
|
|
106
|
-
roleDetail = await service.getRoleWithPolicies(
|
|
118
|
+
roleDetail = await service.getRoleWithPolicies(requirement.roleName);
|
|
107
119
|
}
|
|
108
120
|
catch (error) {
|
|
109
121
|
if (isCamPermissionError(error)) {
|
|
110
|
-
|
|
111
|
-
return buildReport([unverifiableCheck()]);
|
|
122
|
+
return unverifiableCheck(requirement);
|
|
112
123
|
}
|
|
113
124
|
if (isRoleNotFoundError(error)) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}));
|
|
118
|
-
return buildReport(checks);
|
|
125
|
+
return fail(requirement.roleMissingCode, `缺少服务角色 ${requirement.roleName}`, {
|
|
126
|
+
remediation: `请访问一键授权链接创建角色并授予 ${requirement.policyName}:${requirement.grantUrl}`
|
|
127
|
+
});
|
|
119
128
|
}
|
|
120
|
-
|
|
121
|
-
return buildReport([unverifiableCheck()]);
|
|
129
|
+
return unverifiableCheck(requirement);
|
|
122
130
|
}
|
|
123
131
|
const attached = roleDetail.attachedPolicyNames || [];
|
|
124
|
-
if (attached.includes(
|
|
125
|
-
|
|
126
|
-
return buildReport(checks);
|
|
132
|
+
if (attached.includes(requirement.policyName)) {
|
|
133
|
+
return pass(requirement.policyMissingCode, `${requirement.roleName} 已具备${requirement.purpose}策略 ${requirement.policyName}`);
|
|
127
134
|
}
|
|
128
|
-
// 未绑定拉镜像策略
|
|
129
135
|
if (!autoGrant) {
|
|
130
|
-
|
|
131
|
-
remediation: `已关闭自动授权(autoGrant=false)。请访问一键授权链接手动授予:${
|
|
132
|
-
})
|
|
133
|
-
return buildReport(checks);
|
|
136
|
+
return fail(requirement.policyMissingCode, `${requirement.roleName} 缺少${requirement.purpose}策略 ${requirement.policyName}`, {
|
|
137
|
+
remediation: `已关闭自动授权(autoGrant=false)。请访问一键授权链接手动授予:${requirement.grantUrl}`
|
|
138
|
+
});
|
|
134
139
|
}
|
|
135
|
-
// 自动授权:只绑定唯一白名单策略
|
|
136
140
|
try {
|
|
137
|
-
await service.attachPolicyByName(
|
|
138
|
-
|
|
139
|
-
return buildReport(checks);
|
|
141
|
+
await service.attachPolicyByName(requirement.roleName, requirement.policyName);
|
|
142
|
+
return pass(requirement.grantedCode, `已自动为 ${requirement.roleName} 绑定${requirement.purpose}策略 ${requirement.policyName}`);
|
|
140
143
|
}
|
|
141
144
|
catch (error) {
|
|
142
145
|
if (isCamPermissionError(error)) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
remediation: `请用具备 CAM 权限的账号(如主账号)访问一键授权链接完成授权:${exports.IMAGE_PULL_GRANT_URL}`,
|
|
146
|
+
return fail(types_1.FUNCTION_DEPLOY_ERROR.PREFLIGHT_CAM_PERMISSION_DENIED, `当前身份无 CAM 授权权限,无法自动为 ${requirement.roleName} 补齐${requirement.purpose}策略`, {
|
|
147
|
+
remediation: `请用具备 CAM 权限的账号(如主账号)访问一键授权链接完成授权:${requirement.grantUrl}`,
|
|
146
148
|
detail: error === null || error === void 0 ? void 0 : error.message
|
|
147
|
-
})
|
|
148
|
-
return buildReport(checks);
|
|
149
|
+
});
|
|
149
150
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
remediation: `请访问一键授权链接手动授予:${exports.IMAGE_PULL_GRANT_URL}`,
|
|
151
|
+
return fail(requirement.policyMissingCode, `自动绑定策略 ${requirement.policyName} 失败`, {
|
|
152
|
+
remediation: `请访问一键授权链接手动授予:${requirement.grantUrl}`,
|
|
153
153
|
detail: error === null || error === void 0 ? void 0 : error.message
|
|
154
|
-
})
|
|
155
|
-
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 执行镜像仓库授权 Preflight。
|
|
159
|
+
*
|
|
160
|
+
* 企业版 cloud 策略会同时检查 SCF 拉取权限和 TCB 云端构建推送权限;
|
|
161
|
+
* 其他镜像策略只检查与自身相关的授权,不会额外要求 TCB 推送权限。
|
|
162
|
+
*/
|
|
163
|
+
async function runImagePullAuthPreflight(service, config, autoGrant = true) {
|
|
164
|
+
const requirements = resolveRequirements(config);
|
|
165
|
+
if (requirements.length === 0) {
|
|
166
|
+
return buildReport([]);
|
|
167
|
+
}
|
|
168
|
+
const checks = [];
|
|
169
|
+
for (const requirement of requirements) {
|
|
170
|
+
checks.push(await checkRequirement(service, requirement, autoGrant));
|
|
156
171
|
}
|
|
172
|
+
return buildReport(checks);
|
|
157
173
|
}
|