@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
|
@@ -3,7 +3,6 @@ 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.TCR_LOGIN_PUSH_SCRIPT = exports.TCR_PUSH_SCRIPT_ZIP_PATH = void 0;
|
|
7
6
|
exports.buildImageOnCloud = buildImageOnCloud;
|
|
8
7
|
const fs_1 = __importDefault(require("fs"));
|
|
9
8
|
const path_1 = __importDefault(require("path"));
|
|
@@ -42,10 +41,6 @@ const POLL_MAX_INTERVAL = 30000;
|
|
|
42
41
|
const POLL_TOTAL_TIMEOUT = 15 * 60 * 1000;
|
|
43
42
|
/** 轮询临时错误最大连续重试次数,超过则判失败 */
|
|
44
43
|
const POLL_MAX_TRANSIENT_ERRORS = 5;
|
|
45
|
-
const TCR_REGISTRY_AUTH_EXIT_CODE = 42;
|
|
46
|
-
const TCR_REGISTRY_INVALID_EXIT_CODE = 43;
|
|
47
|
-
/** 个人版 TCR 自动创建目标仓库时,仓库数量已达到地域配额 */
|
|
48
|
-
const TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE = 44;
|
|
49
44
|
/** 仓库地址/命名空间/服务名等安全字符白名单:域名、路径、字母数字与常见分隔符 */
|
|
50
45
|
const SAFE_REGISTRY_PATTERN = /^[a-zA-Z0-9][a-zA-Z0-9._\-/]*$/;
|
|
51
46
|
/** tag 白名单:字母数字、下划线、点、中划线 */
|
|
@@ -113,263 +108,13 @@ function assertSafeBuildArgs(buildArgs) {
|
|
|
113
108
|
}
|
|
114
109
|
/** 个人版 CCR 默认域名 */
|
|
115
110
|
const DEFAULT_PERSONAL_REGISTRY = 'ccr.ccs.tencentyun.com';
|
|
116
|
-
/** 注入到构建上下文 zip 中的推送脚本相对路径(与 CustomSteps 的 push命令一致) */
|
|
117
|
-
exports.TCR_PUSH_SCRIPT_ZIP_PATH = 'scripts/tcr-login-and-push.sh';
|
|
118
|
-
/**
|
|
119
|
-
* 注入到构建 zip 的 TCR/CCR 登录推送脚本(完整可运行)
|
|
120
|
-
*
|
|
121
|
-
* 对应构建方案(https://iwiki.woa.com/p/4020519533)「阶段 A」的 push 步骤:
|
|
122
|
-
* push-image 步骤执行 `bash./scripts/tcr-login-and-push.sh`,该脚本必须随源码
|
|
123
|
-
* 一同打进zip 根目录,否则云端会 `No such file or directory` 秒挂。
|
|
124
|
-
*
|
|
125
|
-
* 关键设计:
|
|
126
|
-
* - 地域参数化:REGION 读环境变量 $TCR_REGION(由 buildEnv 按部署地域注入),
|
|
127
|
-
* 避免硬编码 ap-shanghai跨地域时 AuthFailure.SignatureFailure。
|
|
128
|
-
* - 双仓库兼容:设置了 $TCR_INSTANCE_ID 走企业版 TCR CreateInstanceToken;
|
|
129
|
-
* 否则走个人版 CCR,用户名来自普通 Env,固定密码来自 CloudApp Secrets。
|
|
130
|
-
* - 安全红线:凭证一律经 --password-stdin 传入,绝不出现在 ps/日志;
|
|
131
|
-
* 不打印任何 $API_SECRET_* / token / $SECRET_TCR_PASSWORD_B64 或解码后的密码。
|
|
132
|
-
*
|
|
133
|
-
* 脚本内容为SDK 固定模板,不拼接任何用户输入(镜像地址等均由已校验的
|
|
134
|
-
* 环境变量在容器内组装),无命令注入面。
|
|
135
|
-
*/
|
|
136
|
-
exports.TCR_LOGIN_PUSH_SCRIPT = `#!/usr/bin/env bash
|
|
137
|
-
# scripts/tcr-login-and-push.sh
|
|
138
|
-
# 由 CloudBase SDK 自动注入:企业版使用 STS 换取临时 token,个人版使用 CloudApp Secret
|
|
139
|
-
set -euo pipefail
|
|
140
|
-
|
|
141
|
-
: "\${TCR_REGISTRY:?TCR_REGISTRY 未设置}"
|
|
142
|
-
: "\${TCR_NAMESPACE:?TCR_NAMESPACE 未设置}"
|
|
143
|
-
: "\${CLOUDBASE_SERVICE_NAME:?平台变量缺失}"
|
|
144
|
-
: "\${CLOUDBASE_VERSION_NAME:?平台变量缺失}"
|
|
145
|
-
|
|
146
|
-
REGION="\${TCR_REGION:-ap-shanghai}"
|
|
147
|
-
IMAGE="\${TCR_REGISTRY}/\${TCR_NAMESPACE}/\${CLOUDBASE_SERVICE_NAME}:\${CLOUDBASE_VERSION_NAME}"
|
|
148
|
-
IS_PERSONAL_REGISTRY=0
|
|
149
|
-
|
|
150
|
-
if [ -n "\${TCR_INSTANCE_ID:-}" ]; then
|
|
151
|
-
: "\${API_SECRET_ID:?STS 凭证缺失}"
|
|
152
|
-
: "\${API_SECRET_KEY:?STS 凭证缺失}"
|
|
153
|
-
: "\${API_TOKEN:?STS 凭证缺失}"
|
|
154
|
-
|
|
155
|
-
# ---- 企业版 TCR:CreateInstanceToken 换登录态 ----
|
|
156
|
-
HOST="tcr.tencentcloudapi.com"
|
|
157
|
-
SERVICE="tcr"
|
|
158
|
-
VERSION="2019-09-24"
|
|
159
|
-
ACTION="CreateInstanceToken"
|
|
160
|
-
ALGORITHM="TC3-HMAC-SHA256"
|
|
161
|
-
TIMESTAMP=$(date +%s)
|
|
162
|
-
DATE=$(date -u -d "@\${TIMESTAMP}" +"%Y-%m-%d" 2>/dev/null || date -u -r "\${TIMESTAMP}" +"%Y-%m-%d")
|
|
163
|
-
|
|
164
|
-
# 构建阶段只需要一次 docker login/push,使用 1 小时临时凭证,避免创建长期凭证。
|
|
165
|
-
PAYLOAD=$(printf '{"RegistryId":"%s","TokenType":"temp"}' "\${TCR_INSTANCE_ID}")
|
|
166
|
-
HASHED_PAYLOAD=$(printf '%s' "\${PAYLOAD}" | openssl dgst -sha256 -hex | awk '{print $NF}')
|
|
167
|
-
CANONICAL_HEADERS="content-type:application/json; charset=utf-8\\nhost:\${HOST}\\nx-tc-action:$(echo "\${ACTION}" | tr '[:upper:]' '[:lower:]')\\n"
|
|
168
|
-
SIGNED_HEADERS="content-type;host;x-tc-action"
|
|
169
|
-
CANONICAL_REQUEST="POST\\n/\\n\\n\${CANONICAL_HEADERS}\\n\${SIGNED_HEADERS}\\n\${HASHED_PAYLOAD}"
|
|
170
|
-
|
|
171
|
-
CREDENTIAL_SCOPE="\${DATE}/\${SERVICE}/tc3_request"
|
|
172
|
-
HASHED_CR=$(printf "\${CANONICAL_REQUEST}" | openssl dgst -sha256 -hex | awk '{print $NF}')
|
|
173
|
-
STRING_TO_SIGN="\${ALGORITHM}\\n\${TIMESTAMP}\\n\${CREDENTIAL_SCOPE}\\n\${HASHED_CR}"
|
|
174
|
-
|
|
175
|
-
secret_date=$(printf '%s' "\${DATE}" | openssl dgst -sha256 -hmac "TC3\${API_SECRET_KEY}" -hex | awk '{print $NF}')
|
|
176
|
-
secret_svc=$(printf '%s' "\${SERVICE}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_date}" -hex | awk '{print $NF}')
|
|
177
|
-
secret_sign=$(printf '%s' "tc3_request" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_svc}" -hex | awk '{print $NF}')
|
|
178
|
-
SIGNATURE=$(printf "\${STRING_TO_SIGN}" | openssl dgst -sha256 -mac HMAC -macopt hexkey:"\${secret_sign}" -hex | awk '{print $NF}')
|
|
179
|
-
|
|
180
|
-
AUTHZ="\${ALGORITHM} Credential=\${API_SECRET_ID}/\${CREDENTIAL_SCOPE}, SignedHeaders=\${SIGNED_HEADERS}, Signature=\${SIGNATURE}"
|
|
181
|
-
|
|
182
|
-
RESP=$(curl -sS -X POST "https://\${HOST}" \\
|
|
183
|
-
-H "Authorization: \${AUTHZ}" \\
|
|
184
|
-
-H "Content-Type: application/json; charset=utf-8" \\
|
|
185
|
-
-H "Host: \${HOST}" \\
|
|
186
|
-
-H "X-TC-Action: \${ACTION}" \\
|
|
187
|
-
-H "X-TC-Timestamp: \${TIMESTAMP}" \\
|
|
188
|
-
-H "X-TC-Version: \${VERSION}" \\
|
|
189
|
-
-H "X-TC-Region: \${REGION}" \\
|
|
190
|
-
-H "X-TC-Token: \${API_TOKEN}" \\
|
|
191
|
-
--data "\${PAYLOAD}")
|
|
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)
|
|
310
|
-
if [ -n "\${ERROR_CODE}" ]; then
|
|
311
|
-
case "\${ERROR_CODE}" in
|
|
312
|
-
AuthFailure*|UnauthorizedOperation*|FailedOperation.NoPermission*)
|
|
313
|
-
echo "企业版 TCR 鉴权失败,请确认当前身份有权访问该实例并调用 CreateInstanceToken" >&2
|
|
314
|
-
exit ${TCR_REGISTRY_AUTH_EXIT_CODE}
|
|
315
|
-
;;
|
|
316
|
-
ResourceNotFound*|InvalidParameterValue.RegistryId*)
|
|
317
|
-
echo "企业版 TCR 实例不存在或实例 ID 无效" >&2
|
|
318
|
-
exit ${TCR_REGISTRY_INVALID_EXIT_CODE}
|
|
319
|
-
;;
|
|
320
|
-
*)
|
|
321
|
-
echo "企业版 TCR 获取登录凭证失败:\${ERROR_CODE}" >&2
|
|
322
|
-
exit 1
|
|
323
|
-
;;
|
|
324
|
-
esac
|
|
325
|
-
fi
|
|
326
|
-
|
|
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
|
|
337
|
-
|
|
338
|
-
printf '%s' "\${TOKEN_PASS}" | docker login -u "\${TOKEN_USER}" --password-stdin "\${TCR_REGISTRY}"
|
|
339
|
-
else
|
|
340
|
-
# ---- 个人版 CCR:账号 ID + 固定密码(Base64 封装后由 CloudApp Secrets 注入) ----
|
|
341
|
-
IS_PERSONAL_REGISTRY=1
|
|
342
|
-
: "\${TCR_USERNAME:?TCR_USERNAME 未设置}"
|
|
343
|
-
: "\${SECRET_TCR_PASSWORD_B64:?TCR_PASSWORD 未设置}"
|
|
344
|
-
printf '%s' "\${SECRET_TCR_PASSWORD_B64}" | base64 --decode | docker login -u "\${TCR_USERNAME}" --password-stdin "\${TCR_REGISTRY}"
|
|
345
|
-
fi
|
|
346
|
-
|
|
347
|
-
PUSH_LOG=$(mktemp)
|
|
348
|
-
trap 'rm -f "\${PUSH_LOG}"' EXIT
|
|
349
|
-
set +e
|
|
350
|
-
docker push "\${IMAGE}" 2>&1 | tee "\${PUSH_LOG}"
|
|
351
|
-
PUSH_STATUS=\${PIPESTATUS[0]}
|
|
352
|
-
set -e
|
|
353
|
-
|
|
354
|
-
if [ "\${PUSH_STATUS}" -ne 0 ]; then
|
|
355
|
-
if [ "\${IS_PERSONAL_REGISTRY}" -eq 1 ] && {
|
|
356
|
-
grep -Fq 'LimitExceeded.ErrRepoMaxLimit' "\${PUSH_LOG}" ||
|
|
357
|
-
grep -Fq '用户仓库已经达到最大配额' "\${PUSH_LOG}"
|
|
358
|
-
}; then
|
|
359
|
-
echo "个人版 TCR 镜像仓库数量已达到当前地域配额,无法自动创建目标仓库" >&2
|
|
360
|
-
exit ${TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE}
|
|
361
|
-
fi
|
|
362
|
-
exit 1
|
|
363
|
-
fi
|
|
364
|
-
|
|
365
|
-
echo "::output::tag=\${CLOUDBASE_VERSION_NAME}"
|
|
366
|
-
`;
|
|
367
111
|
function resolveTarget(config) {
|
|
368
|
-
const build = config.build;
|
|
112
|
+
const build = config.imageConfig.build;
|
|
113
|
+
const imageType = config.imageConfig.imageType;
|
|
369
114
|
const repository = (build.repository || config.name).trim();
|
|
370
115
|
const rawRegistryId = build.registryId;
|
|
371
116
|
const registryId = typeof rawRegistryId === 'string' ? rawRegistryId.trim() : rawRegistryId;
|
|
372
|
-
const isEnterprise =
|
|
117
|
+
const isEnterprise = imageType === 'enterprise' || Boolean(registryId);
|
|
373
118
|
// CloudApp custom 构建与推送必须使用同一个平台版本 tag。
|
|
374
119
|
// 若允许自定义 tag,build-image 会构建自定义 tag,而固定推送脚本会推
|
|
375
120
|
// $CLOUDBASE_VERSION_NAME,最终必然找不到本地镜像。
|
|
@@ -384,10 +129,6 @@ function resolveTarget(config) {
|
|
|
384
129
|
let hasExplicitRegistry = false;
|
|
385
130
|
let namespace = (build.namespace || '').trim();
|
|
386
131
|
let imageName = repository;
|
|
387
|
-
if (!isEnterprise && !namespace) {
|
|
388
|
-
throw new error_1.CloudBaseError('个人版 TCR 云端构建缺少 build.namespace;请在 cloudbaserc 中显式声明,' +
|
|
389
|
-
'推荐使用 "{{env.TCB_TCR_NAMESPACE}}"', { code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID });
|
|
390
|
-
}
|
|
391
132
|
if (repository.includes('/')) {
|
|
392
133
|
assertSafeRegistryToken(repository, 'build.repository');
|
|
393
134
|
const parts = repository.split('/').filter(Boolean);
|
|
@@ -415,10 +156,10 @@ function resolveTarget(config) {
|
|
|
415
156
|
throw new error_1.CloudBaseError('cloud 策略缺少 TCR 命名空间,请配置 build.namespace 或在完整 repository 中声明', { code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_REPOSITORY_INVALID });
|
|
416
157
|
}
|
|
417
158
|
assertSafeRegistryToken(namespace, 'TCR namespace');
|
|
418
|
-
if (
|
|
159
|
+
if (imageType === 'personal' && registryId) {
|
|
419
160
|
throw new error_1.CloudBaseError('cloud 策略的 personal 镜像不能配置 build.registryId;请移除该字段或改用 enterprise', { code: types_1.FUNCTION_DEPLOY_ERROR.IMAGE_TYPE_INVALID });
|
|
420
161
|
}
|
|
421
|
-
if (
|
|
162
|
+
if (imageType === 'enterprise') {
|
|
422
163
|
if (rawRegistryId === undefined || rawRegistryId === null) {
|
|
423
164
|
throw new error_1.CloudBaseError('cloud 策略的企业版 TCR 未配置 build.registryId;如果当前没有企业版实例,请改用 imageType=personal', { code: types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_MISSING });
|
|
424
165
|
}
|
|
@@ -443,9 +184,10 @@ function resolveTarget(config) {
|
|
|
443
184
|
* CustomSteps 只包含 SDK 固定命令和经过白名单校验的 build arg key。
|
|
444
185
|
*/
|
|
445
186
|
function resolveCloudBuildOptions(config) {
|
|
446
|
-
const
|
|
447
|
-
const
|
|
448
|
-
const
|
|
187
|
+
const build = config.imageConfig.build;
|
|
188
|
+
const dockerfile = build.dockerfile || types_1.DEFAULT_DOCKERFILE;
|
|
189
|
+
const platform = build.platform || types_1.DEFAULT_IMAGE_PLATFORM;
|
|
190
|
+
const buildArgs = build.buildArgs || {};
|
|
449
191
|
assertSafeDockerfile(dockerfile);
|
|
450
192
|
if (platform !== types_1.DEFAULT_IMAGE_PLATFORM) {
|
|
451
193
|
throw new error_1.CloudBaseError(`当前仅支持构建 ${types_1.DEFAULT_IMAGE_PLATFORM} 镜像`, {
|
|
@@ -457,7 +199,7 @@ function resolveCloudBuildOptions(config) {
|
|
|
457
199
|
dockerfile,
|
|
458
200
|
platform,
|
|
459
201
|
buildArgs,
|
|
460
|
-
forceBuild:
|
|
202
|
+
forceBuild: build.forceBuild === true
|
|
461
203
|
};
|
|
462
204
|
}
|
|
463
205
|
/**
|
|
@@ -465,14 +207,23 @@ function resolveCloudBuildOptions(config) {
|
|
|
465
207
|
*
|
|
466
208
|
* Dockerfile、platform 和 build arg value 通过 CloudApp Env 注入;只有通过严格
|
|
467
209
|
* ENV key 校验的 build arg key 会进入固定 command。forceBuild 显式映射为 --no-cache。
|
|
210
|
+
*
|
|
211
|
+
* push-image 直接在 CloudApp 构建容器内执行 docker login 与 docker push:
|
|
212
|
+
* 登录凭证一律经 --password-stdin 传入,不落 ps/日志,也不再向构建上下文注入脚本文件。
|
|
468
213
|
*/
|
|
469
|
-
function buildCustomSteps(options) {
|
|
214
|
+
function buildCustomSteps(options, isEnterpriseRegistry) {
|
|
470
215
|
const buildArgs = Object.keys(options.buildArgs)
|
|
471
216
|
.sort()
|
|
472
217
|
.map(key => `--build-arg ${key}="$BUILD_ARG_${key}"`)
|
|
473
218
|
.join(' ');
|
|
474
219
|
const noCache = options.forceBuild ? ' --no-cache' : '';
|
|
475
220
|
const extraArgs = buildArgs ? ` ${buildArgs}` : '';
|
|
221
|
+
const image = '"$TCR_REGISTRY/$TCR_NAMESPACE/$CLOUDBASE_SERVICE_NAME:$CLOUDBASE_VERSION_NAME"';
|
|
222
|
+
// 企业版临时登录凭证由 manager-node 在 CreateCloudApp 前换取并经 CloudApp Secrets 注入;
|
|
223
|
+
// 个人版沿用账号 ID 与 Base64 封装的固定密码 Secret,容器内解码后直接送入 stdin。
|
|
224
|
+
const loginCommand = isEnterpriseRegistry
|
|
225
|
+
? 'printf \'%s\' "$SECRET_TCR_TOKEN_PASSWORD" | docker login -u "$SECRET_TCR_TOKEN_USERNAME" --password-stdin "$TCR_REGISTRY"'
|
|
226
|
+
: 'printf \'%s\' "$SECRET_TCR_PASSWORD_B64" | base64 --decode | docker login -u "$TCR_USERNAME" --password-stdin "$TCR_REGISTRY"';
|
|
476
227
|
return [
|
|
477
228
|
{
|
|
478
229
|
name: 'build-image',
|
|
@@ -481,24 +232,17 @@ function buildCustomSteps(options) {
|
|
|
481
232
|
' -f "$CLOUDBASE_DOCKERFILE"' +
|
|
482
233
|
noCache +
|
|
483
234
|
extraArgs +
|
|
484
|
-
|
|
235
|
+
` -t ${image} .`
|
|
485
236
|
},
|
|
486
237
|
{
|
|
487
238
|
name: 'push-image',
|
|
488
|
-
command:
|
|
239
|
+
command: `${loginCommand} && docker push ${image}`
|
|
489
240
|
}
|
|
490
241
|
];
|
|
491
242
|
}
|
|
492
|
-
/**
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
* - TCR_REGION:部署地域,供推送脚本换取镜像仓库登录态时签名/请求用,
|
|
496
|
-
* 缺省交由脚本回退默认地域;参数化后跨地域不会 SignatureFailure。
|
|
497
|
-
* - TCR_INSTANCE_ID:仅企业版 TCR 注入,脚本据此走 CreateInstanceToken 分支;
|
|
498
|
-
* 个人版 CCR 不注入,脚本走 STS 直接 docker login 分支。
|
|
499
|
-
*/
|
|
500
|
-
function buildEnv(target, buildOptions, region) {
|
|
501
|
-
const env = [
|
|
243
|
+
/** 组装 custom 构建的环境变量(用户可控值已校验,此处只做键值透传) */
|
|
244
|
+
function buildEnv(target, buildOptions) {
|
|
245
|
+
return [
|
|
502
246
|
{ key: 'TCR_REGISTRY', value: target.registry },
|
|
503
247
|
{ key: 'TCR_NAMESPACE', value: target.namespace },
|
|
504
248
|
{ key: 'CLOUDBASE_DOCKERFILE', value: buildOptions.dockerfile },
|
|
@@ -508,27 +252,29 @@ function buildEnv(target, buildOptions, region) {
|
|
|
508
252
|
value
|
|
509
253
|
}))
|
|
510
254
|
];
|
|
511
|
-
if (region) {
|
|
512
|
-
env.push({ key: 'TCR_REGION', value: region });
|
|
513
|
-
}
|
|
514
|
-
if (target.registryId) {
|
|
515
|
-
assertSafeRegistryToken(target.registryId, 'registryId');
|
|
516
|
-
env.push({ key: 'TCR_INSTANCE_ID', value: target.registryId });
|
|
517
|
-
}
|
|
518
|
-
return env;
|
|
519
255
|
}
|
|
520
256
|
/**
|
|
521
|
-
*
|
|
522
|
-
*
|
|
257
|
+
* 组装 TCR 登录凭证。
|
|
258
|
+
* 企业版使用 manager-node 预先换取的临时 Username/Token;个人版 username 可作为
|
|
259
|
+
* 普通构建变量,password 必须经 Base64 封装后进入 CloudApp Secrets。
|
|
523
260
|
*/
|
|
524
|
-
function buildRegistryCredentials(config, target) {
|
|
261
|
+
function buildRegistryCredentials(config, target, enterpriseCredential) {
|
|
525
262
|
var _a;
|
|
526
263
|
if (target.registryId) {
|
|
527
|
-
|
|
264
|
+
if (!(enterpriseCredential === null || enterpriseCredential === void 0 ? void 0 : enterpriseCredential.username) || !(enterpriseCredential === null || enterpriseCredential === void 0 ? void 0 : enterpriseCredential.token)) {
|
|
265
|
+
throw new error_1.CloudBaseError('企业版 TCR 云端构建缺少临时登录凭证,请确认当前身份具备 CreateInstanceToken 权限', { code: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING });
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
env: [],
|
|
269
|
+
secrets: [
|
|
270
|
+
{ name: 'TCR_TOKEN_USERNAME', value: enterpriseCredential.username },
|
|
271
|
+
{ name: 'TCR_TOKEN_PASSWORD', value: enterpriseCredential.token }
|
|
272
|
+
]
|
|
273
|
+
};
|
|
528
274
|
}
|
|
529
|
-
const credential = config.build.registryCredential;
|
|
275
|
+
const credential = config.imageConfig.build.registryCredential;
|
|
530
276
|
if (!credential) {
|
|
531
|
-
throw new error_1.CloudBaseError('个人版 TCR 云端构建缺少 build.registryCredential;请在 cloudbaserc 中显式声明 ' +
|
|
277
|
+
throw new error_1.CloudBaseError('个人版 TCR 云端构建缺少 imageConfig.build.registryCredential;请在 cloudbaserc 中显式声明 ' +
|
|
532
278
|
'username/password,推荐分别使用 "{{env.TCB_TCR_USERNAME}}" 和 ' +
|
|
533
279
|
'"{{env.TCB_TCR_PASSWORD}}"', { code: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING });
|
|
534
280
|
}
|
|
@@ -552,7 +298,7 @@ function buildRegistryCredentials(config, target) {
|
|
|
552
298
|
};
|
|
553
299
|
}
|
|
554
300
|
function resolveCloudBuildContext(config, dockerfile) {
|
|
555
|
-
const cwd = path_1.default.resolve(config.build.cwd || config.functionPath || process.cwd());
|
|
301
|
+
const cwd = path_1.default.resolve(config.imageConfig.build.cwd || config.functionPath || process.cwd());
|
|
556
302
|
const dockerfilePath = path_1.default.resolve(cwd, dockerfile);
|
|
557
303
|
let stat = null;
|
|
558
304
|
try {
|
|
@@ -581,35 +327,9 @@ function resolveImageUri(target, versionName, artifacts) {
|
|
|
581
327
|
const imageUri = `${target.registry}/${target.namespace}/${target.serviceName}:${versionName}`;
|
|
582
328
|
return { imageUri, imageDigest: imageArtifact === null || imageArtifact === void 0 ? void 0 : imageArtifact.Digest };
|
|
583
329
|
}
|
|
584
|
-
/**
|
|
585
|
-
function
|
|
586
|
-
return (steps || []).some(step => {
|
|
587
|
-
var _a;
|
|
588
|
-
return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
|
|
589
|
-
((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
|
|
590
|
-
step.ExitCode === TCR_REGISTRY_AUTH_EXIT_CODE;
|
|
591
|
-
});
|
|
592
|
-
}
|
|
593
|
-
/** 判断企业版 TCR 推送步骤是否因实例 ID 无效失败 */
|
|
594
|
-
function isRegistryIdFailure(steps) {
|
|
595
|
-
return (steps || []).some(step => {
|
|
596
|
-
var _a;
|
|
597
|
-
return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
|
|
598
|
-
((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
|
|
599
|
-
step.ExitCode === TCR_REGISTRY_INVALID_EXIT_CODE;
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
/** 判断个人版 TCR 是否因自动创建目标仓库时达到仓库数量配额而失败 */
|
|
603
|
-
function isPersonalRepositoryQuotaFailure(target, steps) {
|
|
604
|
-
if (target.registryId) {
|
|
605
|
-
return false;
|
|
606
|
-
}
|
|
607
|
-
return (steps || []).some(step => {
|
|
608
|
-
var _a;
|
|
609
|
-
return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' &&
|
|
610
|
-
((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed' &&
|
|
611
|
-
step.ExitCode === TCR_PERSONAL_REPOSITORY_QUOTA_EXIT_CODE;
|
|
612
|
-
});
|
|
330
|
+
/** 判断失败是否发生在 push-image 步骤,用于给出针对性的排查引导 */
|
|
331
|
+
function isPushStepFailure(steps) {
|
|
332
|
+
return (steps || []).some(step => { var _a; return (step === null || step === void 0 ? void 0 : step.Name) === 'push-image' && ((_a = step === null || step === void 0 ? void 0 : step.Status) === null || _a === void 0 ? void 0 : _a.toLowerCase()) === 'failed'; });
|
|
613
333
|
}
|
|
614
334
|
/** 汇总失败 Step 的定位信息(脱敏:只取步骤名/状态/退出码,不含日志明文) */
|
|
615
335
|
function summarizeFailedSteps(steps) {
|
|
@@ -629,25 +349,19 @@ function summarizeFailedSteps(steps) {
|
|
|
629
349
|
*
|
|
630
350
|
* @param config 规范化的 cloud 部署配置
|
|
631
351
|
* @param service 注入的外部能力(打包/tcb API/COS 上传)
|
|
632
|
-
* @param options 可选项:日志回调 onLog
|
|
352
|
+
* @param options 可选项:日志回调 onLog、企业版临时登录凭证
|
|
633
353
|
*/
|
|
634
354
|
async function buildImageOnCloud(config, service, options = {}) {
|
|
635
|
-
const { onLog,
|
|
355
|
+
const { onLog, enterpriseCredential } = options;
|
|
636
356
|
const target = resolveTarget(config);
|
|
637
357
|
const buildOptions = resolveCloudBuildOptions(config);
|
|
638
358
|
// 在打包和上传前完成真实消费字段校验,避免无效配置触发远端资源操作。
|
|
639
|
-
const registryCredentials = buildRegistryCredentials(config, target);
|
|
359
|
+
const registryCredentials = buildRegistryCredentials(config, target, enterpriseCredential);
|
|
640
360
|
const serviceName = target.serviceName;
|
|
641
361
|
const cwd = resolveCloudBuildContext(config, buildOptions.dockerfile);
|
|
642
|
-
// 1.
|
|
362
|
+
// 1. 只打包用户构建上下文;登录推送逻辑已收敛到 CloudApp CustomStep 命令
|
|
643
363
|
onLog === null || onLog === void 0 ? void 0 : onLog('打包构建上下文');
|
|
644
|
-
const zipPath = await service.packContext(cwd
|
|
645
|
-
{
|
|
646
|
-
zipPath: exports.TCR_PUSH_SCRIPT_ZIP_PATH,
|
|
647
|
-
content: exports.TCR_LOGIN_PUSH_SCRIPT,
|
|
648
|
-
mode: 0o755
|
|
649
|
-
}
|
|
650
|
-
]);
|
|
364
|
+
const zipPath = await service.packContext(cwd);
|
|
651
365
|
try {
|
|
652
366
|
// 2. 获取 COS 上传凭证
|
|
653
367
|
onLog === null || onLog === void 0 ? void 0 : onLog('获取 COS 上传凭证');
|
|
@@ -665,12 +379,9 @@ async function buildImageOnCloud(config, service, options = {}) {
|
|
|
665
379
|
const created = await service.createBuild({
|
|
666
380
|
serviceName,
|
|
667
381
|
cosTimestamp: cosInfo.unixTimestamp,
|
|
668
|
-
env: [
|
|
669
|
-
...buildEnv(target, buildOptions, region),
|
|
670
|
-
...registryCredentials.env
|
|
671
|
-
],
|
|
382
|
+
env: [...buildEnv(target, buildOptions), ...registryCredentials.env],
|
|
672
383
|
secrets: registryCredentials.secrets,
|
|
673
|
-
customSteps: buildCustomSteps(buildOptions)
|
|
384
|
+
customSteps: buildCustomSteps(buildOptions, Boolean(target.registryId))
|
|
674
385
|
});
|
|
675
386
|
if (!(created === null || created === void 0 ? void 0 : created.versionName)) {
|
|
676
387
|
throw new error_1.CloudBaseError('触发云端构建失败:返回缺少 VersionName', {
|
|
@@ -693,27 +404,15 @@ async function buildImageOnCloud(config, service, options = {}) {
|
|
|
693
404
|
const reqId = finalStatus.requestId
|
|
694
405
|
? `,requestId=${finalStatus.requestId}`
|
|
695
406
|
: '';
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const personalRepositoryQuotaFailure = isPersonalRepositoryQuotaFailure(target, finalStatus.steps);
|
|
407
|
+
// 登录凭证与目标仓库均在触发构建前由 manager-node 校验并准备,
|
|
408
|
+
// 因此这里只区分取消与失败,具体原因以构建日志中的 docker 输出为准。
|
|
699
409
|
const code = finalStatus.status === 'CANCELED'
|
|
700
410
|
? types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_CANCELED
|
|
701
|
-
:
|
|
702
|
-
|
|
703
|
-
: registryIdFailure
|
|
704
|
-
? types_1.FUNCTION_DEPLOY_ERROR.IMAGE_REGISTRY_ID_INVALID
|
|
705
|
-
: personalRepositoryQuotaFailure
|
|
706
|
-
? types_1.FUNCTION_DEPLOY_ERROR.CLOUD_PERSONAL_REGISTRY_REPOSITORY_QUOTA_EXCEEDED
|
|
707
|
-
: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_FAILED;
|
|
708
|
-
const guidance = finalStatus.status === 'CANCELED'
|
|
411
|
+
: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_FAILED;
|
|
412
|
+
const guidance = finalStatus.status === 'CANCELED' || !isPushStepFailure(finalStatus.steps)
|
|
709
413
|
? ''
|
|
710
|
-
:
|
|
711
|
-
|
|
712
|
-
: registryIdFailure
|
|
713
|
-
? ';企业版 TCR 实例不存在或实例 ID 无效,请检查 build.registryId 对应的环境变量和实例状态'
|
|
714
|
-
: personalRepositoryQuotaFailure
|
|
715
|
-
? ';个人版 TCR 当前地域的镜像仓库数量已达到配额,目标仓库无法自动创建。请在容器镜像服务个人版中删除不再使用的仓库(删除前请备份其中镜像),或将 build.repository 改为已有仓库;如需更多仓库,请切换企业版 TCR。实际配额请以控制台为准'
|
|
716
|
-
: '';
|
|
414
|
+
: ';推送镜像失败,请在云开发控制台查看构建日志中的 docker login/push 输出,' +
|
|
415
|
+
'并确认镜像仓库凭证、命名空间与仓库配额';
|
|
717
416
|
throw new error_1.CloudBaseError(`云端构建未成功(${finalStatus.status})${detail}${reqId}${guidance}`, { code });
|
|
718
417
|
}
|
|
719
418
|
// 6. 得到不可变镜像地址与digest
|
|
@@ -74,9 +74,10 @@ function needsImagePullAuth(config) {
|
|
|
74
74
|
}
|
|
75
75
|
/** 只有企业版 cloud 策略需要 CloudApp 向企业版 TCR 推送镜像 */
|
|
76
76
|
function needsEnterpriseTcrPushAuth(config) {
|
|
77
|
+
var _a;
|
|
77
78
|
return (config.type === 'HTTP' &&
|
|
78
79
|
config.buildStrategy === 'cloud' &&
|
|
79
|
-
config.imageType === 'enterprise');
|
|
80
|
+
((_a = config.imageConfig) === null || _a === void 0 ? void 0 : _a.imageType) === 'enterprise');
|
|
80
81
|
}
|
|
81
82
|
function resolveRequirements(config) {
|
|
82
83
|
const requirements = [];
|
|
@@ -60,32 +60,18 @@ const DEFAULT_IGNORE = [
|
|
|
60
60
|
function createCloudBuildService(cloudApp, options = {}) {
|
|
61
61
|
const ignore = [...DEFAULT_IGNORE, ...(options.ignore || [])];
|
|
62
62
|
return {
|
|
63
|
-
async packContext(cwd
|
|
63
|
+
async packContext(cwd) {
|
|
64
64
|
if (!fs_1.default.existsSync(cwd) || !fs_1.default.statSync(cwd).isDirectory()) {
|
|
65
65
|
throw new error_1.CloudBaseError(`构建上下文目录不存在或不是目录:${cwd}`, {
|
|
66
66
|
code: types_1.FUNCTION_DEPLOY_ERROR.BUILD_CONTEXT_INVALID
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
69
|
const zipPath = path_1.default.join(os_1.default.tmpdir(), `fn-cloud-build-${Date.now()}-${Math.random().toString(36).slice(2)}.zip`);
|
|
70
|
-
const injectedEntries = (extraEntries || []).map(entry => ({
|
|
71
|
-
zipPath: entry.zipPath.replace(/\\/g, '/').replace(/^\.\//, ''),
|
|
72
|
-
content: entry.content,
|
|
73
|
-
mode: entry.mode
|
|
74
|
-
}));
|
|
75
|
-
// 源码中若存在与 SDK 注入文件同名的路径,必须从 glob 中排除,
|
|
76
|
-
// 避免 ZIP 出现重复 entry 后由解压顺序决定最终执行内容。
|
|
77
|
-
const contextIgnore = [
|
|
78
|
-
...ignore,
|
|
79
|
-
...injectedEntries.map(entry => entry.zipPath)
|
|
80
|
-
];
|
|
81
|
-
// 把 SDK 注入的辅助文件(如 push 脚本)随源码一起打进zip,
|
|
82
|
-
// 通过临时文件注入,绝不写入用户源码目录。
|
|
83
70
|
try {
|
|
84
71
|
await (0, utils_1.compressToZip)({
|
|
85
72
|
dirPath: cwd,
|
|
86
73
|
outputPath: zipPath,
|
|
87
|
-
ignore
|
|
88
|
-
extraEntries: injectedEntries
|
|
74
|
+
ignore
|
|
89
75
|
});
|
|
90
76
|
}
|
|
91
77
|
catch (error) {
|