@cloudbase/manager-node 5.8.1 → 5.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/context.js +2 -1
- package/lib/database/index.js +11 -6
- package/lib/deploy/DeployOrchestrator.js +30 -16
- package/lib/deploy/FunctionDeployer.js +269 -39
- package/lib/deploy/StaticDeployer.js +7 -1
- package/lib/deploy/function/builders/cloud.js +45 -348
- package/lib/deploy/function/cloud-build-service.js +2 -16
- package/lib/deploy/function/config-guard.js +42 -32
- package/lib/deploy/function/enterprise-tcr.js +194 -0
- package/lib/deploy/function/image-tag.js +31 -0
- package/lib/deploy/function/local-builder.js +170 -25
- package/lib/deploy/function/personal-tcr.js +153 -0
- package/lib/deploy/types.js +11 -0
- package/lib/hosting/index.js +27 -0
- package/lib/index.js +4 -3
- package/lib/projectValidator/index.js +22 -20
- package/lib/utils/cloud-api-request.js +26 -2
- package/package.json +2 -2
- package/types/context.d.ts +17 -3
- package/types/database/index.d.ts +1 -0
- package/types/deploy/DeployOrchestrator.d.ts +10 -0
- package/types/deploy/FunctionDeployer.d.ts +20 -2
- package/types/deploy/StaticDeployer.d.ts +11 -0
- package/types/deploy/function/builders/cloud.d.ts +6 -39
- package/types/deploy/function/enterprise-tcr.d.ts +32 -0
- package/types/deploy/function/image-tag.d.ts +10 -0
- package/types/deploy/function/local-builder.d.ts +35 -6
- package/types/deploy/function/personal-tcr.d.ts +35 -0
- package/types/deploy/types.d.ts +34 -8
- package/types/hosting/index.d.ts +13 -0
- package/types/index.d.ts +5 -0
- package/types/projectValidator/index.d.ts +3 -2
- package/types/utils/cloud-api-request.d.ts +3 -2
|
@@ -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,257 +108,6 @@ 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
112
|
const build = config.build;
|
|
369
113
|
const repository = (build.repository || config.name).trim();
|
|
@@ -384,10 +128,6 @@ function resolveTarget(config) {
|
|
|
384
128
|
let hasExplicitRegistry = false;
|
|
385
129
|
let namespace = (build.namespace || '').trim();
|
|
386
130
|
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
131
|
if (repository.includes('/')) {
|
|
392
132
|
assertSafeRegistryToken(repository, 'build.repository');
|
|
393
133
|
const parts = repository.split('/').filter(Boolean);
|
|
@@ -465,14 +205,23 @@ function resolveCloudBuildOptions(config) {
|
|
|
465
205
|
*
|
|
466
206
|
* Dockerfile、platform 和 build arg value 通过 CloudApp Env 注入;只有通过严格
|
|
467
207
|
* ENV key 校验的 build arg key 会进入固定 command。forceBuild 显式映射为 --no-cache。
|
|
208
|
+
*
|
|
209
|
+
* push-image 直接在 CloudApp 构建容器内执行 docker login 与 docker push:
|
|
210
|
+
* 登录凭证一律经 --password-stdin 传入,不落 ps/日志,也不再向构建上下文注入脚本文件。
|
|
468
211
|
*/
|
|
469
|
-
function buildCustomSteps(options) {
|
|
212
|
+
function buildCustomSteps(options, isEnterpriseRegistry) {
|
|
470
213
|
const buildArgs = Object.keys(options.buildArgs)
|
|
471
214
|
.sort()
|
|
472
215
|
.map(key => `--build-arg ${key}="$BUILD_ARG_${key}"`)
|
|
473
216
|
.join(' ');
|
|
474
217
|
const noCache = options.forceBuild ? ' --no-cache' : '';
|
|
475
218
|
const extraArgs = buildArgs ? ` ${buildArgs}` : '';
|
|
219
|
+
const image = '"$TCR_REGISTRY/$TCR_NAMESPACE/$CLOUDBASE_SERVICE_NAME:$CLOUDBASE_VERSION_NAME"';
|
|
220
|
+
// 企业版临时登录凭证由 manager-node 在 CreateCloudApp 前换取并经 CloudApp Secrets 注入;
|
|
221
|
+
// 个人版沿用账号 ID 与 Base64 封装的固定密码 Secret,容器内解码后直接送入 stdin。
|
|
222
|
+
const loginCommand = isEnterpriseRegistry
|
|
223
|
+
? 'printf \'%s\' "$SECRET_TCR_TOKEN_PASSWORD" | docker login -u "$SECRET_TCR_TOKEN_USERNAME" --password-stdin "$TCR_REGISTRY"'
|
|
224
|
+
: 'printf \'%s\' "$SECRET_TCR_PASSWORD_B64" | base64 --decode | docker login -u "$TCR_USERNAME" --password-stdin "$TCR_REGISTRY"';
|
|
476
225
|
return [
|
|
477
226
|
{
|
|
478
227
|
name: 'build-image',
|
|
@@ -481,24 +230,17 @@ function buildCustomSteps(options) {
|
|
|
481
230
|
' -f "$CLOUDBASE_DOCKERFILE"' +
|
|
482
231
|
noCache +
|
|
483
232
|
extraArgs +
|
|
484
|
-
|
|
233
|
+
` -t ${image} .`
|
|
485
234
|
},
|
|
486
235
|
{
|
|
487
236
|
name: 'push-image',
|
|
488
|
-
command:
|
|
237
|
+
command: `${loginCommand} && docker push ${image}`
|
|
489
238
|
}
|
|
490
239
|
];
|
|
491
240
|
}
|
|
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 = [
|
|
241
|
+
/** 组装 custom 构建的环境变量(用户可控值已校验,此处只做键值透传) */
|
|
242
|
+
function buildEnv(target, buildOptions) {
|
|
243
|
+
return [
|
|
502
244
|
{ key: 'TCR_REGISTRY', value: target.registry },
|
|
503
245
|
{ key: 'TCR_NAMESPACE', value: target.namespace },
|
|
504
246
|
{ key: 'CLOUDBASE_DOCKERFILE', value: buildOptions.dockerfile },
|
|
@@ -508,23 +250,25 @@ function buildEnv(target, buildOptions, region) {
|
|
|
508
250
|
value
|
|
509
251
|
}))
|
|
510
252
|
];
|
|
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
253
|
}
|
|
520
254
|
/**
|
|
521
|
-
*
|
|
522
|
-
*
|
|
255
|
+
* 组装 TCR 登录凭证。
|
|
256
|
+
* 企业版使用 manager-node 预先换取的临时 Username/Token;个人版 username 可作为
|
|
257
|
+
* 普通构建变量,password 必须经 Base64 封装后进入 CloudApp Secrets。
|
|
523
258
|
*/
|
|
524
|
-
function buildRegistryCredentials(config, target) {
|
|
259
|
+
function buildRegistryCredentials(config, target, enterpriseCredential) {
|
|
525
260
|
var _a;
|
|
526
261
|
if (target.registryId) {
|
|
527
|
-
|
|
262
|
+
if (!(enterpriseCredential === null || enterpriseCredential === void 0 ? void 0 : enterpriseCredential.username) || !(enterpriseCredential === null || enterpriseCredential === void 0 ? void 0 : enterpriseCredential.token)) {
|
|
263
|
+
throw new error_1.CloudBaseError('企业版 TCR 云端构建缺少临时登录凭证,请确认当前身份具备 CreateInstanceToken 权限', { code: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_REGISTRY_CREDENTIAL_MISSING });
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
env: [],
|
|
267
|
+
secrets: [
|
|
268
|
+
{ name: 'TCR_TOKEN_USERNAME', value: enterpriseCredential.username },
|
|
269
|
+
{ name: 'TCR_TOKEN_PASSWORD', value: enterpriseCredential.token }
|
|
270
|
+
]
|
|
271
|
+
};
|
|
528
272
|
}
|
|
529
273
|
const credential = config.build.registryCredential;
|
|
530
274
|
if (!credential) {
|
|
@@ -581,35 +325,9 @@ function resolveImageUri(target, versionName, artifacts) {
|
|
|
581
325
|
const imageUri = `${target.registry}/${target.namespace}/${target.serviceName}:${versionName}`;
|
|
582
326
|
return { imageUri, imageDigest: imageArtifact === null || imageArtifact === void 0 ? void 0 : imageArtifact.Digest };
|
|
583
327
|
}
|
|
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
|
-
});
|
|
328
|
+
/** 判断失败是否发生在 push-image 步骤,用于给出针对性的排查引导 */
|
|
329
|
+
function isPushStepFailure(steps) {
|
|
330
|
+
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
331
|
}
|
|
614
332
|
/** 汇总失败 Step 的定位信息(脱敏:只取步骤名/状态/退出码,不含日志明文) */
|
|
615
333
|
function summarizeFailedSteps(steps) {
|
|
@@ -629,25 +347,19 @@ function summarizeFailedSteps(steps) {
|
|
|
629
347
|
*
|
|
630
348
|
* @param config 规范化的 cloud 部署配置
|
|
631
349
|
* @param service 注入的外部能力(打包/tcb API/COS 上传)
|
|
632
|
-
* @param options 可选项:日志回调 onLog
|
|
350
|
+
* @param options 可选项:日志回调 onLog、企业版临时登录凭证
|
|
633
351
|
*/
|
|
634
352
|
async function buildImageOnCloud(config, service, options = {}) {
|
|
635
|
-
const { onLog,
|
|
353
|
+
const { onLog, enterpriseCredential } = options;
|
|
636
354
|
const target = resolveTarget(config);
|
|
637
355
|
const buildOptions = resolveCloudBuildOptions(config);
|
|
638
356
|
// 在打包和上传前完成真实消费字段校验,避免无效配置触发远端资源操作。
|
|
639
|
-
const registryCredentials = buildRegistryCredentials(config, target);
|
|
357
|
+
const registryCredentials = buildRegistryCredentials(config, target, enterpriseCredential);
|
|
640
358
|
const serviceName = target.serviceName;
|
|
641
359
|
const cwd = resolveCloudBuildContext(config, buildOptions.dockerfile);
|
|
642
|
-
// 1.
|
|
360
|
+
// 1. 只打包用户构建上下文;登录推送逻辑已收敛到 CloudApp CustomStep 命令
|
|
643
361
|
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
|
-
]);
|
|
362
|
+
const zipPath = await service.packContext(cwd);
|
|
651
363
|
try {
|
|
652
364
|
// 2. 获取 COS 上传凭证
|
|
653
365
|
onLog === null || onLog === void 0 ? void 0 : onLog('获取 COS 上传凭证');
|
|
@@ -665,12 +377,9 @@ async function buildImageOnCloud(config, service, options = {}) {
|
|
|
665
377
|
const created = await service.createBuild({
|
|
666
378
|
serviceName,
|
|
667
379
|
cosTimestamp: cosInfo.unixTimestamp,
|
|
668
|
-
env: [
|
|
669
|
-
...buildEnv(target, buildOptions, region),
|
|
670
|
-
...registryCredentials.env
|
|
671
|
-
],
|
|
380
|
+
env: [...buildEnv(target, buildOptions), ...registryCredentials.env],
|
|
672
381
|
secrets: registryCredentials.secrets,
|
|
673
|
-
customSteps: buildCustomSteps(buildOptions)
|
|
382
|
+
customSteps: buildCustomSteps(buildOptions, Boolean(target.registryId))
|
|
674
383
|
});
|
|
675
384
|
if (!(created === null || created === void 0 ? void 0 : created.versionName)) {
|
|
676
385
|
throw new error_1.CloudBaseError('触发云端构建失败:返回缺少 VersionName', {
|
|
@@ -693,27 +402,15 @@ async function buildImageOnCloud(config, service, options = {}) {
|
|
|
693
402
|
const reqId = finalStatus.requestId
|
|
694
403
|
? `,requestId=${finalStatus.requestId}`
|
|
695
404
|
: '';
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
const personalRepositoryQuotaFailure = isPersonalRepositoryQuotaFailure(target, finalStatus.steps);
|
|
405
|
+
// 登录凭证与目标仓库均在触发构建前由 manager-node 校验并准备,
|
|
406
|
+
// 因此这里只区分取消与失败,具体原因以构建日志中的 docker 输出为准。
|
|
699
407
|
const code = finalStatus.status === 'CANCELED'
|
|
700
408
|
? 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'
|
|
409
|
+
: types_1.FUNCTION_DEPLOY_ERROR.CLOUD_BUILD_FAILED;
|
|
410
|
+
const guidance = finalStatus.status === 'CANCELED' || !isPushStepFailure(finalStatus.steps)
|
|
709
411
|
? ''
|
|
710
|
-
:
|
|
711
|
-
|
|
712
|
-
: registryIdFailure
|
|
713
|
-
? ';企业版 TCR 实例不存在或实例 ID 无效,请检查 build.registryId 对应的环境变量和实例状态'
|
|
714
|
-
: personalRepositoryQuotaFailure
|
|
715
|
-
? ';个人版 TCR 当前地域的镜像仓库数量已达到配额,目标仓库无法自动创建。请在容器镜像服务个人版中删除不再使用的仓库(删除前请备份其中镜像),或将 build.repository 改为已有仓库;如需更多仓库,请切换企业版 TCR。实际配额请以控制台为准'
|
|
716
|
-
: '';
|
|
412
|
+
: ';推送镜像失败,请在云开发控制台查看构建日志中的 docker login/push 输出,' +
|
|
413
|
+
'并确认镜像仓库凭证、命名空间与仓库配额';
|
|
717
414
|
throw new error_1.CloudBaseError(`云端构建未成功(${finalStatus.status})${detail}${reqId}${guidance}`, { code });
|
|
718
415
|
}
|
|
719
416
|
// 6. 得到不可变镜像地址与digest
|
|
@@ -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) {
|