@h-ai/deploy 1.0.0-beta.22 → 1.0.0-beta.23
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/README.en.md +6 -10
- package/README.ja.md +5 -9
- package/README.md +5 -9
- package/dist/deploy-runtime-env.mjs +3 -11
- package/dist/index.d.ts +6 -38
- package/dist/index.js +92 -617
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
- package/docs/credential-import.en.md +0 -40
- package/docs/credential-import.ja.md +0 -40
- package/docs/credential-import.md +0 -70
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { createHash, randomUUID, randomBytes, X509Certificate, createPublicKey, createPrivateKey } from 'crypto';
|
|
2
|
-
import { createReadStream, readFileSync, existsSync, lstatSync, mkdirSync, readdirSync,
|
|
2
|
+
import { createReadStream, readFileSync, existsSync, lstatSync, mkdirSync, readdirSync, openSync, constants, fstatSync, readSync, closeSync, fchmodSync, writeFileSync, fsyncSync, renameSync, unlinkSync, chmodSync, mkdtempSync, copyFileSync, rmSync, realpathSync, createWriteStream, statSync } from 'fs';
|
|
3
3
|
import { resolve, extname, join, dirname, basename, relative, sep } from 'path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { core, err, ok, CoreConfigSchema, RequestAccessConfigSchema, TrustedProxiesSchema } from '@h-ai/core';
|
|
6
|
-
import {
|
|
6
|
+
import { execFileSync, spawn } from 'child_process';
|
|
7
7
|
import process from 'process';
|
|
8
8
|
import { LineCounter, parseDocument, parse, stringify, parseAllDocuments } from 'yaml';
|
|
9
9
|
import { Buffer, constants as constants$1 } from 'buffer';
|
|
@@ -22,17 +22,14 @@ import { isIP } from 'net';
|
|
|
22
22
|
// src/deploy-artifact.ts
|
|
23
23
|
var DeploymentArtifactSchema = z.object({
|
|
24
24
|
/** 清单格式版本,与应用版本和部署环境无关。 */
|
|
25
|
-
version: z.
|
|
26
|
-
z.literal(4),
|
|
27
|
-
z.literal(5)
|
|
28
|
-
]),
|
|
25
|
+
version: z.literal(5),
|
|
29
26
|
/** 主运行服务:独立后端为 serv,独立浏览器站点(静态或 SSR)为 web。 */
|
|
30
27
|
primaryService: z.enum(["serv", "web"]),
|
|
31
28
|
/** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
|
|
32
29
|
appVersion: z.string().max(80).regex(/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9a-z-]+(?:\.[0-9a-z-]+)*)?(?:\+[0-9a-z-]+(?:\.[0-9a-z-]+)*)?$/i),
|
|
33
30
|
/** 本次构建标识;同一应用版本允许重复构建。 */
|
|
34
31
|
buildId: z.string().max(128).regex(/^[a-z0-9][\w.+-]*$/i),
|
|
35
|
-
/** 构建启动时观察到的 Git
|
|
32
|
+
/** 构建启动时观察到的 Git 来源。 */
|
|
36
33
|
source: z.object({
|
|
37
34
|
/** 工程 package.json 所在仓库的提交 SHA;无法确定时为 unknown。 */
|
|
38
35
|
commit: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|unknown)$/),
|
|
@@ -43,8 +40,8 @@ var DeploymentArtifactSchema = z.object({
|
|
|
43
40
|
partial: z.boolean().default(false),
|
|
44
41
|
/** 是否包含浏览器端;仅构建 serv 时也保留完整工程的 Web 拓扑。 */
|
|
45
42
|
hasWeb: z.boolean().default(false),
|
|
46
|
-
/** Web
|
|
47
|
-
webBasePath: z.string().refine((value) => value === "" || /^\/[\w.-]+(?:\/[\w.-]+)*$/.test(value) && !value.split("/").some((segment) => segment === "." || segment === ".."))
|
|
43
|
+
/** Web 构建的公开挂载目录,无末尾斜杠;空字符串表示域名根目录。增量制品须保留完整拓扑的同一值。 */
|
|
44
|
+
webBasePath: z.string().refine((value) => value === "" || /^\/[\w.-]+(?:\/[\w.-]+)*$/.test(value) && !value.split("/").some((segment) => segment === "." || segment === "..")),
|
|
48
45
|
/** 工程显式配置的后端端口,部分构建未包含 serv 时仍保留。 */
|
|
49
46
|
servContainerPort: z.number().int().min(1).max(65535).optional(),
|
|
50
47
|
/** 工程显式配置的 Web 端口,部分构建未包含 web 镜像时仍保留;缺省使用部署默认值。 */
|
|
@@ -59,12 +56,12 @@ var DeploymentArtifactSchema = z.object({
|
|
|
59
56
|
imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
|
|
60
57
|
/** tar 内容的 SHA-256,用于部署前校验。 */
|
|
61
58
|
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
62
|
-
/** 引擎实际构建的镜像 OS
|
|
59
|
+
/** 引擎实际构建的镜像 OS/架构。 */
|
|
63
60
|
platform: z.string().regex(/^[a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*$/).optional(),
|
|
64
61
|
/** 容器内部监听端口;服务器公开端口由部署选择。 */
|
|
65
62
|
containerPort: z.number().int().min(1).max(65535).optional()
|
|
66
63
|
})).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
|
|
67
|
-
}).refine((artifact) =>
|
|
64
|
+
}).refine((artifact) => !artifact.webBasePath || artifact.hasWeb).refine((artifact) => artifact.images.every((image) => image.file === `${image.name}-${artifact.appVersion}.tar`)).refine((artifact) => artifact.primaryService !== "web" || artifact.hasWeb).refine((artifact) => artifact.images.every((image) => image.name === artifact.primaryService || image.name === "web" && artifact.hasWeb)).refine((artifact) => artifact.partial || artifact.images.some((image) => image.name === artifact.primaryService)).refine((artifact) => artifact.partial || !artifact.hasWeb || artifact.images.some((image) => image.name === "web"));
|
|
68
65
|
async function artifactChecksum(file) {
|
|
69
66
|
const hash = createHash("sha256");
|
|
70
67
|
for await (const chunk of createReadStream(file))
|
|
@@ -189,9 +186,7 @@ var en_US_default = {
|
|
|
189
186
|
deploy_uploadPasswordCancelled: "Password input cancelled; no server connection was made.",
|
|
190
187
|
deploy_uploadPasswordInvalid: "SSH passwords must contain 1\u20131023 UTF-8 bytes without line breaks.",
|
|
191
188
|
deploy_managedSecretsInvalid: "Invalid generated middleware credential candidates. Rebuild the deployment bundle; server credentials will not be rotated automatically.",
|
|
192
|
-
deploy_secretImportFailed: "Legacy credential import was rejected during {phase}; check private input permissions, project/volume identity, live credentials and the deployment lock. No services were updated; inspect the protected state file if commit is unconfirmed",
|
|
193
189
|
deploy_projectNameRequired: "The root package.json.name is missing or cannot be normalized into an image and deployment resource identity.",
|
|
194
|
-
deploy_projectNameOverrideRemoved: "Deployment identity comes from the root package.json.name; remove the obsolete projectName override.",
|
|
195
190
|
deploy_requestAccessInvalid: "Invalid request access policy ({field}). Check fields, IP addresses, timezones and time windows. For a file reference, use readable YAML no larger than 1 MiB. Fix the configuration and create a new bundle.",
|
|
196
191
|
deploy_httpsPortsInvalid: "HTTP and HTTPS ports must differ; Let\u2019s Encrypt HTTP-01 requires host port 80.",
|
|
197
192
|
deploy_httpsCertificateInvalid: "Invalid HTTPS certificate. Check readable PEM files, matching unencrypted private key, validity dates and domain, then repackage.",
|
|
@@ -334,9 +329,7 @@ var ja_JP_default = {
|
|
|
334
329
|
deploy_uploadPasswordCancelled: "\u30D1\u30B9\u30EF\u30FC\u30C9\u5165\u529B\u3092\u4E2D\u6B62\u3057\u307E\u3057\u305F\u3002\u30B5\u30FC\u30D0\u30FC\u306B\u306F\u672A\u63A5\u7D9A\u3067\u3059\u3002",
|
|
335
330
|
deploy_uploadPasswordInvalid: "SSH \u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u6539\u884C\u3092\u542B\u307E\u306A\u3044 1\u20131023 UTF-8 \u30D0\u30A4\u30C8\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
336
331
|
deploy_managedSecretsInvalid: "\u81EA\u5EFA\u30DF\u30C9\u30EB\u30A6\u30A7\u30A2\u306E\u5019\u88DC\u8A8D\u8A3C\u60C5\u5831\u304C\u7121\u52B9\u3067\u3059\u3002\u914D\u7F6E\u30D1\u30C3\u30B1\u30FC\u30B8\u3092\u518D\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30B5\u30FC\u30D0\u30FC\u306E\u8A8D\u8A3C\u60C5\u5831\u306F\u81EA\u52D5\u5909\u66F4\u3055\u308C\u307E\u305B\u3093\u3002",
|
|
337
|
-
deploy_secretImportFailed: "\u65E7\u8A8D\u8A3C\u60C5\u5831\u306E\u30A4\u30F3\u30DD\u30FC\u30C8\u306F {phase} \u6BB5\u968E\u3067\u62D2\u5426\u3055\u308C\u307E\u3057\u305F\u3002\u5165\u529B\u6A29\u9650\u3001\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3068\u30DC\u30EA\u30E5\u30FC\u30E0\u3001\u5B9F\u969B\u306E\u8A8D\u8A3C\u60C5\u5831\u3001\u30C7\u30D7\u30ED\u30A4\u30ED\u30C3\u30AF\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30B5\u30FC\u30D3\u30B9\u306F\u66F4\u65B0\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u78BA\u5B9A\u304C\u4E0D\u660E\u306A\u5834\u5408\u306F\u4FDD\u8B77\u3055\u308C\u305F\u72B6\u614B\u30D5\u30A1\u30A4\u30EB\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
338
332
|
deploy_projectNameRequired: "\u5DE5\u7A0B\u30EB\u30FC\u30C8\u306E package.json.name \u304C\u306A\u3044\u304B\u6B63\u898F\u5316\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u30A4\u30E1\u30FC\u30B8\u3068\u30C7\u30D7\u30ED\u30A4\u30EA\u30BD\u30FC\u30B9\u306E\u8B58\u5225\u540D\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3002",
|
|
339
|
-
deploy_projectNameOverrideRemoved: "\u30C7\u30D7\u30ED\u30A4\u8B58\u5225\u540D\u306F\u5DE5\u7A0B\u30EB\u30FC\u30C8\u306E package.json.name \u304B\u3089\u53D6\u5F97\u3057\u307E\u3059\u3002\u65E7 projectName \u4E0A\u66F8\u304D\u5F15\u6570\u3092\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
340
333
|
deploy_requestAccessInvalid: "\u30EA\u30AF\u30A8\u30B9\u30C8\u30A2\u30AF\u30BB\u30B9\u8A2D\u5B9A\uFF08{field}\uFF09\u304C\u7121\u52B9\u3067\u3059\u3002\u30D5\u30A3\u30FC\u30EB\u30C9\u3001IP\u3001\u30BF\u30A4\u30E0\u30BE\u30FC\u30F3\u3001\u6642\u9593\u5E2F\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30D5\u30A1\u30A4\u30EB\u306F\u8AAD\u307F\u53D6\u308A\u53EF\u80FD\u306A 1 MiB \u4EE5\u4E0B\u306E YAML \u3092\u6307\u5B9A\u3057\u3001\u4FEE\u6B63\u5F8C\u306B\u518D\u30D1\u30C3\u30B1\u30FC\u30B8\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
341
334
|
deploy_httpsPortsInvalid: "HTTP \u3068 HTTPS \u306E\u30DD\u30FC\u30C8\u306F\u7570\u306A\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002Let\u2019s Encrypt HTTP-01 \u306B\u306F\u30DB\u30B9\u30C8\u306E\u30DD\u30FC\u30C8 80 \u304C\u5FC5\u8981\u3067\u3059\u3002",
|
|
342
335
|
deploy_httpsCertificateInvalid: "HTTPS \u8A3C\u660E\u66F8\u304C\u7121\u52B9\u3067\u3059\u3002PEM \u30D5\u30A1\u30A4\u30EB\u3001\u6697\u53F7\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u79D8\u5BC6\u9375\u3068\u306E\u4E00\u81F4\u3001\u6709\u52B9\u671F\u9593\u3001\u30C9\u30E1\u30A4\u30F3\u3092\u78BA\u8A8D\u3057\u3001\u518D\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
@@ -479,9 +472,7 @@ var zh_CN_default = {
|
|
|
479
472
|
deploy_uploadPasswordCancelled: "\u5DF2\u53D6\u6D88\u5BC6\u7801\u8F93\u5165\uFF0C\u5C1A\u672A\u8FDE\u63A5\u670D\u52A1\u5668\u3002",
|
|
480
473
|
deploy_uploadPasswordInvalid: "SSH \u5BC6\u7801\u987B\u4E3A 1\u20131023 \u4E2A UTF-8 \u5B57\u8282\uFF0C\u4E14\u4E0D\u80FD\u5305\u542B\u6362\u884C\u3002",
|
|
481
474
|
deploy_managedSecretsInvalid: "\u81EA\u5EFA\u4E2D\u95F4\u4EF6\u5019\u9009\u51ED\u636E\u65E0\u6548\uFF0C\u8BF7\u91CD\u65B0\u751F\u6210\u90E8\u7F72\u5305\uFF1B\u670D\u52A1\u5668\u51ED\u636E\u4E0D\u4F1A\u81EA\u52A8\u66F4\u6362\u3002",
|
|
482
|
-
deploy_secretImportFailed: "\u65E7\u51ED\u636E\u5BFC\u5165\u5728 {phase} \u9636\u6BB5\u88AB\u62D2\u7EDD\uFF1B\u8BF7\u6838\u5BF9\u79C1\u6709\u8F93\u5165\u6743\u9650\u3001\u9879\u76EE/\u5377\u8EAB\u4EFD\u3001\u5B9E\u9645\u670D\u52A1\u51ED\u636E\u53CA\u90E8\u7F72\u9501\uFF0C\u672A\u6267\u884C\u670D\u52A1\u66F4\u65B0\uFF1B\u63D0\u4EA4\u7ED3\u679C\u4E0D\u660E\u65F6\u8BF7\u6838\u5BF9\u53D7\u4FDD\u62A4\u7684\u72B6\u6001\u6587\u4EF6",
|
|
483
475
|
deploy_projectNameRequired: "\u5DE5\u7A0B\u6839 package.json.name \u7F3A\u5931\u6216\u65E0\u6CD5\u5F52\u4E00\u5316\uFF0C\u4E0D\u80FD\u751F\u6210\u955C\u50CF\u4E0E\u90E8\u7F72\u8D44\u6E90\u8EAB\u4EFD\u3002",
|
|
484
|
-
deploy_projectNameOverrideRemoved: "\u90E8\u7F72\u9879\u76EE\u540D\u7EDF\u4E00\u6765\u81EA\u5DE5\u7A0B\u6839 package.json.name\uFF1B\u8BF7\u79FB\u9664\u65E7 projectName \u8986\u76D6\u53C2\u6570\u3002",
|
|
485
476
|
deploy_requestAccessInvalid: "\u8BF7\u6C42\u8BBF\u95EE\u7B56\u7565\u914D\u7F6E\u65E0\u6548\uFF08{field}\uFF09\u3002\u8BF7\u68C0\u67E5\u5B57\u6BB5\u3001IP\u3001\u65F6\u533A\u53CA\u65F6\u95F4\u7A97\u53E3\uFF1B\u5982\u5F15\u7528\u6587\u4EF6\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u53EF\u8BFB\u3001\u683C\u5F0F\u4E3A YAML \u4E14\u4E0D\u8D85\u8FC7 1 MiB\u3002\u4FEE\u6B63\u540E\u91CD\u65B0\u6253\u5305\u3002",
|
|
486
477
|
deploy_httpsPortsInvalid: "HTTP \u4E0E HTTPS \u7AEF\u53E3\u5FC5\u987B\u4E0D\u540C\uFF1BLet\u2019s Encrypt \u7684 HTTP-01 \u5165\u53E3\u5FC5\u987B\u4F7F\u7528\u5BBF\u4E3B\u673A 80 \u7AEF\u53E3\u3002",
|
|
487
478
|
deploy_httpsCertificateInvalid: "HTTPS \u8BC1\u4E66\u65E0\u6548\uFF1A\u8BF7\u6838\u5BF9 PEM \u6587\u4EF6\u53EF\u8BFB\u3001\u8BC1\u4E66\u4E0E\u672A\u52A0\u5BC6\u79C1\u94A5\u5339\u914D\u3001\u8BC1\u4E66\u6709\u6548\u671F\u53CA\u57DF\u540D\uFF0C\u7136\u540E\u91CD\u65B0\u6253\u5305\u3002",
|
|
@@ -6261,9 +6252,9 @@ function existingFile(path) {
|
|
|
6261
6252
|
throw error;
|
|
6262
6253
|
}
|
|
6263
6254
|
}
|
|
6264
|
-
function writeLocalFile(path, content,
|
|
6255
|
+
function writeLocalFile(path, content, privateFile = false) {
|
|
6265
6256
|
const previous = existingFile(path);
|
|
6266
|
-
const mode =
|
|
6257
|
+
const mode = privateFile ? 384 : previous ? Number(previous.mode & 0o777n) : 420;
|
|
6267
6258
|
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
6268
6259
|
let fd;
|
|
6269
6260
|
let owned;
|
|
@@ -6470,7 +6461,7 @@ function isSecret(name, value) {
|
|
|
6470
6461
|
}
|
|
6471
6462
|
function generatedVariables(values) {
|
|
6472
6463
|
const generated = {};
|
|
6473
|
-
const
|
|
6464
|
+
const groups2 = /* @__PURE__ */ new Map();
|
|
6474
6465
|
const structures = /* @__PURE__ */ new Set();
|
|
6475
6466
|
for (const [name, value] of Object.entries(values)) {
|
|
6476
6467
|
if (value === void 0)
|
|
@@ -6482,9 +6473,9 @@ function generatedVariables(values) {
|
|
|
6482
6473
|
const module = /^HAI_([A-Z0-9]+)(?:_|$)/.exec(name)?.[1]?.toLowerCase();
|
|
6483
6474
|
if (!module || !shapes[module])
|
|
6484
6475
|
continue;
|
|
6485
|
-
const group =
|
|
6476
|
+
const group = groups2.get(module) ?? {};
|
|
6486
6477
|
group[name] = text;
|
|
6487
|
-
|
|
6478
|
+
groups2.set(module, group);
|
|
6488
6479
|
try {
|
|
6489
6480
|
const parsed = JSON.parse(text);
|
|
6490
6481
|
if (parsed !== null && typeof parsed === "object")
|
|
@@ -6492,7 +6483,7 @@ function generatedVariables(values) {
|
|
|
6492
6483
|
} catch {
|
|
6493
6484
|
}
|
|
6494
6485
|
}
|
|
6495
|
-
for (const [module, group] of
|
|
6486
|
+
for (const [module, group] of groups2) {
|
|
6496
6487
|
const structuredNames = Object.keys(group).filter((name) => structures.has(name));
|
|
6497
6488
|
if (!structuredNames.length)
|
|
6498
6489
|
continue;
|
|
@@ -6539,7 +6530,7 @@ function generatedVariables(values) {
|
|
|
6539
6530
|
}
|
|
6540
6531
|
return generated;
|
|
6541
6532
|
}
|
|
6542
|
-
function updateFile(path, values,
|
|
6533
|
+
function updateFile(path, values, privateFile) {
|
|
6543
6534
|
let content = existsSync(path) ? readLocalFile(path).toString("utf8") : "# Local private deployment configuration; do not commit.\n";
|
|
6544
6535
|
for (const [name, value] of Object.entries(values)) {
|
|
6545
6536
|
if (!/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
@@ -6563,15 +6554,15 @@ ${line}
|
|
|
6563
6554
|
content = withoutParent.slice(0, child.index) + parent[0].replace(/\r?\n?$/, "\n") + withoutParent.slice(child.index);
|
|
6564
6555
|
}
|
|
6565
6556
|
}
|
|
6566
|
-
writeLocalFile(path, content,
|
|
6557
|
+
writeLocalFile(path, content, privateFile);
|
|
6567
6558
|
}
|
|
6568
|
-
function removeOverrides(path, names,
|
|
6559
|
+
function removeOverrides(path, names, privateFile = true) {
|
|
6569
6560
|
if (!existsSync(path) || names.length === 0)
|
|
6570
6561
|
return;
|
|
6571
6562
|
let content = readLocalFile(path).toString("utf8");
|
|
6572
6563
|
for (const name of names)
|
|
6573
6564
|
content = content.replace(new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
|
|
6574
|
-
writeLocalFile(path, content,
|
|
6565
|
+
writeLocalFile(path, content, privateFile);
|
|
6575
6566
|
}
|
|
6576
6567
|
function saveDeploymentEnvironment(environment, values, options = {}) {
|
|
6577
6568
|
try {
|
|
@@ -7134,47 +7125,6 @@ async function scanApp(inputDir, options) {
|
|
|
7134
7125
|
}
|
|
7135
7126
|
}
|
|
7136
7127
|
|
|
7137
|
-
// src/container/deploy-compose-identity.ts
|
|
7138
|
-
function createComposeContainerLookupScript() {
|
|
7139
|
-
const labelFormat = [
|
|
7140
|
-
"com.docker.compose.project",
|
|
7141
|
-
"com.docker.compose.service",
|
|
7142
|
-
"io.podman.compose.project",
|
|
7143
|
-
"io.podman.compose.service"
|
|
7144
|
-
].map((key) => `{{index .Config.Labels \`${key}\`}}`).join("|");
|
|
7145
|
-
return String.raw`compose_container_ids() (
|
|
7146
|
-
if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
|
|
7147
|
-
lookup_runtime=$1
|
|
7148
|
-
lookup_project=$2
|
|
7149
|
-
lookup_service=$3
|
|
7150
|
-
lookup_selector=$4
|
|
7151
|
-
case "$lookup_runtime" in docker|podman) ;; *) printf 'Invalid Compose lookup runtime.\n' >&2; exit 1;; esac
|
|
7152
|
-
case "$lookup_project" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose project identity.\n' >&2; exit 1;; esac
|
|
7153
|
-
case "$lookup_service" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose service identity.\n' >&2; exit 1;; esac
|
|
7154
|
-
case "$lookup_selector" in -q|-aq) ;; *) printf 'Invalid Compose container selector.\n' >&2; exit 1;; esac
|
|
7155
|
-
lookup_ids=''
|
|
7156
|
-
for lookup_namespace in com.docker.compose io.podman.compose; do
|
|
7157
|
-
lookup_found=$("$lookup_runtime" ps "$lookup_selector" --filter "label=$lookup_namespace.project=$lookup_project" --filter "label=$lookup_namespace.service=$lookup_service") || { printf 'Cannot query Compose containers.\n' >&2; exit 1; }
|
|
7158
|
-
printf '%s\n' "$lookup_found" | LC_ALL=C awk 'NF {if(NF!=1 || $0!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/) exit 1}' || { printf 'Invalid Compose container identifiers.\n' >&2; exit 1; }
|
|
7159
|
-
lookup_ids=$(printf '%s\n%s\n' "$lookup_ids" "$lookup_found" | LC_ALL=C awk 'NF && !seen[$0]++ {print}') || exit 1
|
|
7160
|
-
done
|
|
7161
|
-
for lookup_id in $lookup_ids; do
|
|
7162
|
-
lookup_labels=$("$lookup_runtime" inspect --format '${labelFormat}' "$lookup_id") || { printf 'Cannot inspect Compose container labels.\n' >&2; exit 1; }
|
|
7163
|
-
printf '%s\n' "$lookup_labels" | LC_ALL=C awk -F'|' -v project="$lookup_project" -v service="$lookup_service" '
|
|
7164
|
-
NR!=1 || NF!=4 {exit 1}
|
|
7165
|
-
{
|
|
7166
|
-
docker=($1!="" && $1!="<no value>") || ($2!="" && $2!="<no value>")
|
|
7167
|
-
podman=($3!="" && $3!="<no value>") || ($4!="" && $4!="<no value>")
|
|
7168
|
-
if((!docker && !podman) || (docker && ($1!=project || $2!=service)) || (podman && ($3!=project || $4!=service))) exit 1
|
|
7169
|
-
}
|
|
7170
|
-
END {if(NR!=1) exit 1}
|
|
7171
|
-
' || { printf 'Container labels do not confirm the requested Compose project and service.\n' >&2; exit 1; }
|
|
7172
|
-
done
|
|
7173
|
-
if [ -n "$lookup_ids" ]; then printf '%s\n' "$lookup_ids"; fi
|
|
7174
|
-
)
|
|
7175
|
-
`;
|
|
7176
|
-
}
|
|
7177
|
-
|
|
7178
7128
|
// src/container/deploy-json.ts
|
|
7179
7129
|
function createDeploymentJsonTool() {
|
|
7180
7130
|
return String.raw`function fail(message) {
|
|
@@ -7499,7 +7449,7 @@ deployment_metadata_init() {
|
|
|
7499
7449
|
deployment_metadata_file() {
|
|
7500
7450
|
metadata_directory=$1
|
|
7501
7451
|
metadata_name=$2
|
|
7502
|
-
case "$metadata_name" in layout|health-paths|image-list|dependencies|managed-
|
|
7452
|
+
case "$metadata_name" in layout|health-paths|image-list|dependencies|managed-bindings|managed-volumes|managed-app-keys|unmanaged-app-keys|state-backends|storage-routing|web-base-path|node-runtime|proxy-service|https) ;; *) printf 'Unknown deployment metadata field.\n' >&2; return 1;; esac
|
|
7503
7453
|
test -f "$metadata_directory/manifest.json" && test ! -L "$metadata_directory/manifest.json" || return 1
|
|
7504
7454
|
metadata_identity=$(printf '%s' "$metadata_directory" | sha256sum | awk '{print $1}')
|
|
7505
7455
|
metadata_cached="$metadata_work/$metadata_identity-$metadata_name"
|
|
@@ -7596,8 +7546,6 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
|
|
|
7596
7546
|
}
|
|
7597
7547
|
}
|
|
7598
7548
|
return {
|
|
7599
|
-
"managed-project": `hai-${projectName}
|
|
7600
|
-
`,
|
|
7601
7549
|
"managed-candidates.env": candidates.length ? `${candidates.join("\n")}
|
|
7602
7550
|
` : "",
|
|
7603
7551
|
"managed-bindings": bindings.size ? `${[...bindings].join("\n")}
|
|
@@ -7607,29 +7555,6 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
|
|
|
7607
7555
|
"resolve-managed-secrets.sh": createManagedSecretScript()
|
|
7608
7556
|
};
|
|
7609
7557
|
}
|
|
7610
|
-
function createManagedSecretValidationScript() {
|
|
7611
|
-
return String.raw`valid_values() {
|
|
7612
|
-
version=$(awk -F= '$1=="FORMAT_VERSION" {print $2}' "$1")
|
|
7613
|
-
if [ "$version" = 2 ]; then
|
|
7614
|
-
LC_ALL=C awk -F= '
|
|
7615
|
-
NF {
|
|
7616
|
-
if(NF!=2 || seen[$1]++) exit 1
|
|
7617
|
-
if($1=="FORMAT_VERSION") {if($2!="2") exit 1; next}
|
|
7618
|
-
if($1=="PROJECT") {if($2!~/^[A-Za-z0-9][A-Za-z0-9_.-]*$/) exit 1; project=1; next}
|
|
7619
|
-
if($1~/^VOLUME_[0-9]+$/) {if($2!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD):[A-Za-z0-9][A-Za-z0-9_.-]*$/) exit 1; next}
|
|
7620
|
-
if($1!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD)$/ || $2!~/^[0-9a-f]+$/ || length($2)%2 || length($2)>8192) exit 1
|
|
7621
|
-
for(i=1;i<=length($2);i+=2) {b=substr($2,i,2); if(b~/^[01]/ || b=="7f") exit 1}
|
|
7622
|
-
}
|
|
7623
|
-
END {if(!project) exit 1}
|
|
7624
|
-
' "$1"
|
|
7625
|
-
return
|
|
7626
|
-
fi
|
|
7627
|
-
LC_ALL=C awk -F= '
|
|
7628
|
-
NF {if(NF!=2 || $1!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD)$/ || $2!~/^[A-Za-z0-9_-]+$/ || length($2)!=32 || seen[$1]++) exit 1}
|
|
7629
|
-
' "$1"
|
|
7630
|
-
}
|
|
7631
|
-
`;
|
|
7632
|
-
}
|
|
7633
7558
|
function createManagedSecretScript() {
|
|
7634
7559
|
return String.raw`#!/bin/sh
|
|
7635
7560
|
# 由 apply.sh 在项目部署锁内调用;长期凭据只保存在服务器,不依赖打包机。
|
|
@@ -7644,12 +7569,15 @@ test -d "$root" && test ! -L "$root" && test -d "$release/.hai" && test ! -L "$r
|
|
|
7644
7569
|
@@MANIFEST_FUNCTIONS@@
|
|
7645
7570
|
deployment_metadata_init
|
|
7646
7571
|
trap deployment_metadata_cleanup EXIT
|
|
7647
|
-
managed_project=$(deployment_metadata_file "$release" managed-project)
|
|
7648
7572
|
managed_bindings=$(deployment_metadata_file "$release" managed-bindings)
|
|
7649
7573
|
managed_volumes=$(deployment_metadata_file "$release" managed-volumes)
|
|
7650
7574
|
test -s "$candidate_file" || exit 0
|
|
7651
7575
|
fail() { printf '%s\n' "$*" >&2; exit 1; }
|
|
7652
|
-
|
|
7576
|
+
valid_values() {
|
|
7577
|
+
LC_ALL=C awk -F= '
|
|
7578
|
+
NF {if(NF!=2 || $1!~/^(DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD)$/ || $2!~/^[A-Za-z0-9_-]+$/ || length($2)!=32 || seen[$1]++) exit 1}
|
|
7579
|
+
' "$1"
|
|
7580
|
+
}
|
|
7653
7581
|
value_of() { awk -F= -v key="$2" '$1==key {print $2}' "$1"; }
|
|
7654
7582
|
test ! -L "$state" || fail "Invalid managed middleware credential state: $state"
|
|
7655
7583
|
valid_values "$candidate_file" || fail 'Invalid managed middleware credential candidates; rebuild the deployment bundle.'
|
|
@@ -7660,22 +7588,6 @@ if [ -f "$state" ]; then
|
|
|
7660
7588
|
else
|
|
7661
7589
|
: > "$next"
|
|
7662
7590
|
fi
|
|
7663
|
-
format=$(value_of "$next" FORMAT_VERSION)
|
|
7664
|
-
if [ "$format" = 2 ]; then
|
|
7665
|
-
test "$(value_of "$next" PROJECT)" = "$(cat "$managed_project")" || fail 'Managed middleware credential project mismatch.'
|
|
7666
|
-
known=$("$runtime" volume ls -q) || fail 'Cannot check persistent volumes; managed middleware credentials were not changed.'
|
|
7667
|
-
while IFS=' ' read -r volume_group volume; do
|
|
7668
|
-
test -n "$volume" || continue
|
|
7669
|
-
binding="$volume_group:$volume"
|
|
7670
|
-
if ! awk -F= -v binding="$binding" '$1~/^VOLUME_[0-9]+$/ && $2==binding {found=1} END {exit !found}' "$next"; then
|
|
7671
|
-
if printf '%s\n' "$known" | grep -Fxq "$volume"; then
|
|
7672
|
-
fail 'Existing persistent volume is not bound to the imported credential state. Import its verified identity first.'
|
|
7673
|
-
fi
|
|
7674
|
-
index=$(awk -F= '$1~/^VOLUME_[0-9]+$/ {n=substr($1,8)+0; if(n>max)max=n} END {print max+1}' "$next")
|
|
7675
|
-
printf 'VOLUME_%s=%s\n' "$index" "$binding" >> "$next"
|
|
7676
|
-
fi
|
|
7677
|
-
done < "$managed_volumes"
|
|
7678
|
-
fi
|
|
7679
7591
|
volumes_checked=false
|
|
7680
7592
|
known_volumes=''
|
|
7681
7593
|
while IFS='=' read -r name candidate; do
|
|
@@ -7692,64 +7604,24 @@ while IFS='=' read -r name candidate; do
|
|
|
7692
7604
|
fail "Missing $name in $state for existing persistent volume $volume. Restore the server credential state before retrying."
|
|
7693
7605
|
fi
|
|
7694
7606
|
done < "$managed_volumes"
|
|
7695
|
-
|
|
7696
|
-
if [ "$format" = 2 ]; then value=$(printf '%s' "$candidate" | od -An -tx1 | tr -d ' \n'); fi
|
|
7697
|
-
printf '%s=%s\n' "$name" "$value" >> "$next"
|
|
7607
|
+
printf '%s=%s\n' "$name" "$candidate" >> "$next"
|
|
7698
7608
|
fi
|
|
7699
7609
|
done < "$candidate_file"
|
|
7700
7610
|
while IFS='=' read -r name candidate; do
|
|
7701
7611
|
test -n "$name" || continue
|
|
7702
7612
|
value=$(value_of "$next" "$name")
|
|
7703
7613
|
for file in config/.env.runtime .hai/middleware-runtime.env; do
|
|
7704
|
-
|
|
7705
|
-
if [ "$file" = .hai/middleware-runtime.env ]; then compose_environment=true; fi
|
|
7706
|
-
HAI_DEPLOY_SECRET_CANDIDATE="$candidate" HAI_DEPLOY_SECRET_VALUE="$value" LC_ALL=C awk -v group="$name" -v format="$format" -v compose_environment="$compose_environment" '
|
|
7614
|
+
HAI_DEPLOY_SECRET_CANDIDATE="$candidate" HAI_DEPLOY_SECRET_VALUE="$value" LC_ALL=C awk -v group="$name" '
|
|
7707
7615
|
FILENAME==ARGV[1] {if($1==group) {keys[$2]=$3}; next}
|
|
7708
7616
|
function replace_literal(line, needle, value, result, at) {
|
|
7709
7617
|
result=""
|
|
7710
7618
|
while((at=index(line,needle))>0) {result=result substr(line,1,at-1) value; line=substr(line,at+length(needle))}
|
|
7711
7619
|
return result line
|
|
7712
7620
|
}
|
|
7713
|
-
function decode_hex(hex, result, i, high, low) {
|
|
7714
|
-
for(i=1;i<=length(hex);i+=2) {high=index("0123456789abcdef",substr(hex,i,1))-1; low=index("0123456789abcdef",substr(hex,i+1,1))-1; result=result sprintf("%c",high*16+low)}
|
|
7715
|
-
return result
|
|
7716
|
-
}
|
|
7717
|
-
function json_escape(value, apostrophe, result, i, ch) {
|
|
7718
|
-
for(i=1;i<=length(value);i++) {
|
|
7719
|
-
ch=substr(value,i,1)
|
|
7720
|
-
if(ch=="\\") result=result "\\\\"
|
|
7721
|
-
else if(ch=="\"") result=result "\\\""
|
|
7722
|
-
else if(ch==sprintf("%c",39) && apostrophe) result=result "\\u0027"
|
|
7723
|
-
else result=result ch
|
|
7724
|
-
}
|
|
7725
|
-
return result
|
|
7726
|
-
}
|
|
7727
|
-
function uri_escape(value, result, i, ch, j, ordinal) {
|
|
7728
|
-
for(i=1;i<=length(value);i++) {
|
|
7729
|
-
ch=substr(value,i,1)
|
|
7730
|
-
if(ch~/[A-Za-z0-9_.~-]/) result=result ch
|
|
7731
|
-
else {ordinal=0; for(j=1;j<=255;j++) if(sprintf("%c",j)==ch) {ordinal=j; break}; result=result sprintf("%%%02X",ordinal)}
|
|
7732
|
-
}
|
|
7733
|
-
return result
|
|
7734
|
-
}
|
|
7735
7621
|
{
|
|
7736
7622
|
equal=index($0,"="); key=substr($0,1,equal-1); scoped=0
|
|
7737
7623
|
for(prefix in keys) if(key==prefix || (keys[prefix]=="prefix" && index(key,prefix "_")==1)) scoped=1
|
|
7738
|
-
if(scoped
|
|
7739
|
-
candidate=ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"]; value=decode_hex(ENVIRON["HAI_DEPLOY_SECRET_VALUE"])
|
|
7740
|
-
literal=substr($0,equal+1); quote=sprintf("%c",39)
|
|
7741
|
-
json=substr(literal,1,1)==quote && (substr(literal,2,1)=="{" || substr(literal,2,1)=="[")
|
|
7742
|
-
if(json) literal=substr(literal,2,length(literal)-2)
|
|
7743
|
-
literal=replace_literal(literal,":" candidate "@",":" uri_escape(value) "@")
|
|
7744
|
-
if(json) {literal=replace_literal(literal,candidate,json_escape(value,1)); print key "=" quote literal quote}
|
|
7745
|
-
else {
|
|
7746
|
-
if(substr(literal,1,1)==quote || substr(literal,1,1)=="\"") literal=substr(literal,2,length(literal)-2)
|
|
7747
|
-
literal=replace_literal(literal,candidate,value)
|
|
7748
|
-
literal=json_escape(literal,0); if(compose_environment=="true") literal=replace_literal(literal,"$","$")
|
|
7749
|
-
print key "=\"" literal "\""
|
|
7750
|
-
}
|
|
7751
|
-
}
|
|
7752
|
-
else if(scoped) print replace_literal($0,ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"],ENVIRON["HAI_DEPLOY_SECRET_VALUE"])
|
|
7624
|
+
if(scoped) print replace_literal($0,ENVIRON["HAI_DEPLOY_SECRET_CANDIDATE"],ENVIRON["HAI_DEPLOY_SECRET_VALUE"])
|
|
7753
7625
|
else print
|
|
7754
7626
|
}
|
|
7755
7627
|
' "$managed_bindings" "$release/$file" > "$release/$file.next"
|
|
@@ -7759,7 +7631,7 @@ done < "$candidate_file"
|
|
|
7759
7631
|
chmod 600 "$next"
|
|
7760
7632
|
mv "$next" "$state"
|
|
7761
7633
|
printf '[INFO] Managed middleware credential source: %s (server state; values hidden).\n' "$state"
|
|
7762
|
-
`.replace("@@
|
|
7634
|
+
`.replace("@@MANIFEST_FUNCTIONS@@", () => createDeploymentManifestFunctions());
|
|
7763
7635
|
}
|
|
7764
7636
|
|
|
7765
7637
|
// src/container/deploy-backend-state.ts
|
|
@@ -7822,11 +7694,11 @@ backend_manifest=$(deployment_metadata_file "$release" state-backends)
|
|
|
7822
7694
|
regular() { test -f "$1" && test ! -L "$1" || fail "Backend state must be a regular file: $1"; }
|
|
7823
7695
|
absent_or_regular() { if [ -e "$1" ] || [ -L "$1" ]; then regular "$1"; fi; }
|
|
7824
7696
|
valid_tuples() {
|
|
7825
|
-
LC_ALL=C awk
|
|
7697
|
+
LC_ALL=C awk '
|
|
7826
7698
|
NF {
|
|
7827
|
-
if(
|
|
7699
|
+
if(NF!=3 || roles[$1]++) exit 1
|
|
7828
7700
|
if(!(($1=="db" && $2~/^(postgresql|mysql)$/) || ($1=="storage" && $2~/^(rustfs|minio)$/) || ($1=="vecdb" && $2~/^(qdrant|pgvector|chroma)$/))) exit 1
|
|
7829
|
-
if(
|
|
7701
|
+
if($3!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ || volumes[$3]++) exit 1
|
|
7830
7702
|
}
|
|
7831
7703
|
' "$1"
|
|
7832
7704
|
}
|
|
@@ -7877,45 +7749,6 @@ known=$("$runtime" volume ls -q) || fail 'Cannot list persistent volumes; no bac
|
|
|
7877
7749
|
printf '%s\n' "$known" | LC_ALL=C awk 'NF {if(NF!=1 || $1!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ || seen[$1]++) exit 1}' || fail 'Invalid persistent volume listing.'
|
|
7878
7750
|
exists_volume() { printf '%s\n' "$known" | grep -Fxq "$1"; }
|
|
7879
7751
|
ledger_tuple() { printf '%s\n' "$ledger" | awk -v volume="$1" '$3==volume {print}'; }
|
|
7880
|
-
@@MANAGED_VALUES_VALIDATOR@@
|
|
7881
|
-
@@COMPOSE_CONTAINER_LOOKUP@@
|
|
7882
|
-
# 状态补证:卷挂载和镜像均取自同项目、同角色的唯一实际容器。
|
|
7883
|
-
container_binding() {
|
|
7884
|
-
binding_service=$1
|
|
7885
|
-
binding_family=$2
|
|
7886
|
-
binding_expected=$3
|
|
7887
|
-
binding_running=$4
|
|
7888
|
-
binding_flags=-aq
|
|
7889
|
-
if [ "$binding_running" = true ]; then binding_flags=-q; fi
|
|
7890
|
-
binding_containers=$(compose_container_ids "$runtime" "$project" "$binding_service" "$binding_flags") || fail 'Cannot inspect successful deployment containers.'
|
|
7891
|
-
printf '%s\n' "$binding_containers" | LC_ALL=C awk 'NR!=1 || NF!=1 || $1!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ {exit 1} END {if(NR!=1) exit 1}' || fail "Cannot prove the previous volume binding for $binding_service. Restore verified backend state."
|
|
7892
|
-
if [ "$binding_running" = true ]; then
|
|
7893
|
-
binding_status=$("$runtime" inspect --format '{{.State.Running}}' "$binding_containers") || fail 'Cannot verify the imported service is running.'
|
|
7894
|
-
test "$binding_status" = true || fail "Imported backend requires its verified original container to be running: $binding_service."
|
|
7895
|
-
fi
|
|
7896
|
-
binding_mounted=$("$runtime" inspect --format '{{range .Mounts}}{{if eq .Type "volume"}}{{println .Name}}{{end}}{{end}}' "$binding_containers") || fail 'Cannot inspect previous named volume mounts.'
|
|
7897
|
-
printf '%s\n' "$binding_mounted" | LC_ALL=C awk 'NR!=1 || NF!=1 || $1!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ {exit 1} END {if(NR!=1) exit 1}' || fail "Ambiguous previous volume binding for $binding_service."
|
|
7898
|
-
binding_image=$("$runtime" inspect --format '{{.Config.Image}}' "$binding_containers") || fail 'Cannot identify the previous container image.'
|
|
7899
|
-
binding_actual_family=$(printf '%s\n' "$binding_image" | LC_ALL=C awk 'NR!=1 || NF!=1 {exit 1} {image=$0; sub(/^.*\//,"",image); sub(/[:@].*$/,"",image); if(image=="postgres") image="postgresql"; print image}') || fail 'Invalid previous container image identity.'
|
|
7900
|
-
test "$binding_actual_family" = "$binding_family" || fail "Actual previous container image does not confirm its backend family: $binding_service."
|
|
7901
|
-
if [ -n "$binding_expected" ] && [ "$binding_expected" != "$binding_mounted" ]; then fail "Actual previous volume differs from its backend manifest: $binding_service."; fi
|
|
7902
|
-
printf '%s\n' "$binding_mounted"
|
|
7903
|
-
}
|
|
7904
|
-
confirmed_import() {
|
|
7905
|
-
import_service=$1
|
|
7906
|
-
import_family=$2
|
|
7907
|
-
import_volume=$3
|
|
7908
|
-
import_state="$root/managed-secrets.env"
|
|
7909
|
-
test -f "$import_state" && test ! -L "$import_state" || return 1
|
|
7910
|
-
valid_values "$import_state" || return 1
|
|
7911
|
-
test "$(awk -F= '$1=="FORMAT_VERSION" {print $2}' "$import_state")" = 2 || return 1
|
|
7912
|
-
test "$(awk -F= '$1=="PROJECT" {print $2}' "$import_state")" = "$project" || return 1
|
|
7913
|
-
case "$import_service" in db|vecdb) import_group=DB_PASSWORD;; storage) import_group=STORAGE_PASSWORD;; *) return 1;; esac
|
|
7914
|
-
awk -F= -v group="$import_group" -v binding="$import_group:$import_volume" '$1==group && $2!="" {credential=1} $1~/^VOLUME_[0-9]+$/ && $2==binding {volume=1} END {exit !(credential && volume)}' "$import_state" || return 1
|
|
7915
|
-
# 密码不证明目录格式;受控导入还须与唯一运行原容器的实际卷及镜像家族共同吻合。
|
|
7916
|
-
import_actual=$(container_binding "$import_service" "$import_family" "$import_volume" true) || return 1
|
|
7917
|
-
test "$import_actual" = "$import_volume"
|
|
7918
|
-
}
|
|
7919
7752
|
snapshot_previous=$previous
|
|
7920
7753
|
if [ -z "$snapshot_previous" ]; then snapshot_previous=-; fi
|
|
7921
7754
|
if [ "$mode" = recover ]; then
|
|
@@ -7933,7 +7766,7 @@ RECOVERY_TUPLES
|
|
|
7933
7766
|
exit 0
|
|
7934
7767
|
fi
|
|
7935
7768
|
regular "$backend_manifest"
|
|
7936
|
-
valid_tuples "$backend_manifest"
|
|
7769
|
+
valid_tuples "$backend_manifest" || fail 'Invalid backend manifest; rebuild the deployment bundle.'
|
|
7937
7770
|
active=''
|
|
7938
7771
|
if [ -n "$previous" ]; then
|
|
7939
7772
|
baseline="$root/releases/$previous"
|
|
@@ -7942,7 +7775,7 @@ if [ -n "$previous" ]; then
|
|
|
7942
7775
|
baseline_services=$(deployment_services_file "$baseline")
|
|
7943
7776
|
previous_manifest=$(deployment_metadata_file "$baseline" state-backends)
|
|
7944
7777
|
regular "$previous_manifest"
|
|
7945
|
-
valid_tuples "$previous_manifest"
|
|
7778
|
+
valid_tuples "$previous_manifest" || fail 'Invalid successful backend manifest.'
|
|
7946
7779
|
LC_ALL=C awk 'NF {if(NF!=1 || $1!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/ || seen[$1]++) exit 1}' "$baseline_services" || fail 'Invalid successful deployment services.'
|
|
7947
7780
|
for active_service in db storage vecdb; do
|
|
7948
7781
|
if ! grep -Fxq "$active_service" "$baseline_services"; then continue; fi
|
|
@@ -7955,28 +7788,18 @@ if [ -n "$previous" ]; then
|
|
|
7955
7788
|
printf '%s %s %s\n' "$active_service" "$active_family" "$active_volume" | LC_ALL=C awk 'NF!=3 || !(($1=="db" && $2~/^(postgresql|mysql)$/) || ($1=="storage" && $2~/^(rustfs|minio)$/) || ($1=="vecdb" && $2~/^(qdrant|pgvector|chroma)$/)) {exit 1}' || fail "Unknown installed backend for $active_service."
|
|
7956
7789
|
exists_volume "$active_volume" || fail "Active persistent volume is missing: $active_volume. Restore its backup instead of creating an empty replacement."
|
|
7957
7790
|
confirmed=$(ledger_tuple "$active_volume")
|
|
7958
|
-
test
|
|
7959
|
-
if [ -z "$confirmed" ]; then
|
|
7960
|
-
proven_volume=$(container_binding "$active_service" "$active_family" "$active_volume" false)
|
|
7961
|
-
test "$proven_volume" = "$active_volume" || fail "Cannot confirm the active persistent volume: $active_service."
|
|
7962
|
-
fi
|
|
7791
|
+
test "$confirmed" = "$active_service $active_family $active_volume" || fail "Active persistent backend is missing from or conflicts with the protected ledger: $active_service. Restore verified backend state."
|
|
7963
7792
|
active=$(printf '%s\n%s %s %s\n' "$active" "$active_service" "$active_family" "$active_volume" | sed '/^$/d')
|
|
7964
7793
|
done
|
|
7965
7794
|
fi
|
|
7966
|
-
#
|
|
7967
|
-
printf '%s\n' "$active" | valid_tuples /dev/stdin
|
|
7795
|
+
# 同一实际卷不能绑定给多个有状态服务。
|
|
7796
|
+
printf '%s\n' "$active" | valid_tuples /dev/stdin || fail 'Conflicting successful persistent volume bindings.'
|
|
7968
7797
|
# 合并时只替换同一卷的无数据预认领,不能按本次拓扑裁剪历史绑定。
|
|
7969
7798
|
merged=$ledger
|
|
7970
7799
|
merge_tuple() {
|
|
7971
7800
|
merged=$(printf '%s\n' "$merged" | awk -v volume="$3" 'NF && $3!=volume {print}')
|
|
7972
7801
|
merged=$(printf '%s\n%s %s %s\n' "$merged" "$1" "$2" "$3" | sed '/^$/d')
|
|
7973
7802
|
}
|
|
7974
|
-
while IFS=' ' read -r active_service active_family active_volume; do
|
|
7975
|
-
test -n "$active_service" || continue
|
|
7976
|
-
merge_tuple "$active_service" "$active_family" "$active_volume"
|
|
7977
|
-
done <<ACTIVE_TUPLES
|
|
7978
|
-
$active
|
|
7979
|
-
ACTIVE_TUPLES
|
|
7980
7803
|
while IFS=' ' read -r candidate_service candidate_family candidate_volume; do
|
|
7981
7804
|
test -n "$candidate_service" || continue
|
|
7982
7805
|
prior_active=$(printf '%s\n' "$active" | awk -v service="$candidate_service" '$1==service {print}')
|
|
@@ -7984,8 +7807,6 @@ while IFS=' ' read -r candidate_service candidate_family candidate_volume; do
|
|
|
7984
7807
|
test -z "$prior_active" || test "$prior_active" = "$tuple" || fail "Persistent backend $candidate_service changed or its active volume binding changed. Use a separate deployment identity and migrate or restore data explicitly."
|
|
7985
7808
|
if exists_volume "$candidate_volume"; then
|
|
7986
7809
|
confirmed=$(ledger_tuple "$candidate_volume")
|
|
7987
|
-
if [ -z "$confirmed" ]; then confirmed=$(printf '%s\n' "$active" | awk -v volume="$candidate_volume" '$3==volume {print}'); fi
|
|
7988
|
-
if [ -z "$confirmed" ] && confirmed_import "$candidate_service" "$candidate_family" "$candidate_volume"; then confirmed=$tuple; fi
|
|
7989
7810
|
test "$confirmed" = "$tuple" || fail "Existing persistent volume has an unknown or incompatible backend: $candidate_service. Restore verified backend state; in-place reuse is refused."
|
|
7990
7811
|
else
|
|
7991
7812
|
users=$("$runtime" ps -aq --filter "volume=$candidate_volume") || fail 'Cannot check persistent volume users.'
|
|
@@ -8005,7 +7826,7 @@ mv -f -- "$snapshot.next" "$snapshot"
|
|
|
8005
7826
|
(set -C; printf '%s\n' "$state_content" > "$state.next") || fail 'Cannot stage the persistent backend ledger.'
|
|
8006
7827
|
chmod 600 "$state.next"
|
|
8007
7828
|
mv -f -- "$state.next" "$state"
|
|
8008
|
-
`.replace("@@PROJECT@@", () => projectName).replace("@@
|
|
7829
|
+
`.replace("@@PROJECT@@", () => projectName).replace("@@MANIFEST_FUNCTIONS@@", () => createDeploymentManifestFunctions());
|
|
8009
7830
|
}
|
|
8010
7831
|
var nativeJob = String.raw`
|
|
8011
7832
|
using System;
|
|
@@ -8204,7 +8025,7 @@ async function groupExited(pid) {
|
|
|
8204
8025
|
async function helper(command, args, timeoutMs) {
|
|
8205
8026
|
if (timeoutMs <= 0)
|
|
8206
8027
|
return false;
|
|
8207
|
-
return new Promise((
|
|
8028
|
+
return new Promise((resolve8) => {
|
|
8208
8029
|
const child = spawn(command, args, { windowsHide: true, shell: false, stdio: "ignore" });
|
|
8209
8030
|
let settled = false;
|
|
8210
8031
|
let timer;
|
|
@@ -8214,7 +8035,7 @@ async function helper(command, args, timeoutMs) {
|
|
|
8214
8035
|
settled = true;
|
|
8215
8036
|
if (timer)
|
|
8216
8037
|
clearTimeout(timer);
|
|
8217
|
-
|
|
8038
|
+
resolve8(success);
|
|
8218
8039
|
};
|
|
8219
8040
|
timer = globalThis.setTimeout(() => {
|
|
8220
8041
|
child.kill("SIGKILL");
|
|
@@ -8367,7 +8188,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8367
8188
|
ext: { commandExecution: state }
|
|
8368
8189
|
});
|
|
8369
8190
|
try {
|
|
8370
|
-
return await new Promise((
|
|
8191
|
+
return await new Promise((resolve8) => {
|
|
8371
8192
|
if (typeof command !== "string" || !command || command.includes("\0") || args.some((argument) => typeof argument !== "string" || argument.includes("\0")))
|
|
8372
8193
|
throw new TypeError("Invalid command arguments");
|
|
8373
8194
|
if (options.input !== void 0 && typeof options.input !== "string")
|
|
@@ -8410,7 +8231,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8410
8231
|
settled = true;
|
|
8411
8232
|
if (timer)
|
|
8412
8233
|
clearTimeout(timer);
|
|
8413
|
-
|
|
8234
|
+
resolve8(result);
|
|
8414
8235
|
};
|
|
8415
8236
|
const interrupt = (reason, message) => {
|
|
8416
8237
|
interrupted = true;
|
|
@@ -8597,6 +8418,47 @@ watch_certificate &
|
|
|
8597
8418
|
`;
|
|
8598
8419
|
}
|
|
8599
8420
|
|
|
8421
|
+
// src/container/deploy-compose-identity.ts
|
|
8422
|
+
function createComposeContainerLookupScript() {
|
|
8423
|
+
const labelFormat = [
|
|
8424
|
+
"com.docker.compose.project",
|
|
8425
|
+
"com.docker.compose.service",
|
|
8426
|
+
"io.podman.compose.project",
|
|
8427
|
+
"io.podman.compose.service"
|
|
8428
|
+
].map((key) => `{{index .Config.Labels \`${key}\`}}`).join("|");
|
|
8429
|
+
return String.raw`compose_container_ids() (
|
|
8430
|
+
if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
|
|
8431
|
+
lookup_runtime=$1
|
|
8432
|
+
lookup_project=$2
|
|
8433
|
+
lookup_service=$3
|
|
8434
|
+
lookup_selector=$4
|
|
8435
|
+
case "$lookup_runtime" in docker|podman) ;; *) printf 'Invalid Compose lookup runtime.\n' >&2; exit 1;; esac
|
|
8436
|
+
case "$lookup_project" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose project identity.\n' >&2; exit 1;; esac
|
|
8437
|
+
case "$lookup_service" in ''|*[!A-Za-z0-9_.-]*) printf 'Invalid Compose service identity.\n' >&2; exit 1;; esac
|
|
8438
|
+
case "$lookup_selector" in -q|-aq) ;; *) printf 'Invalid Compose container selector.\n' >&2; exit 1;; esac
|
|
8439
|
+
lookup_ids=''
|
|
8440
|
+
for lookup_namespace in com.docker.compose io.podman.compose; do
|
|
8441
|
+
lookup_found=$("$lookup_runtime" ps "$lookup_selector" --filter "label=$lookup_namespace.project=$lookup_project" --filter "label=$lookup_namespace.service=$lookup_service") || { printf 'Cannot query Compose containers.\n' >&2; exit 1; }
|
|
8442
|
+
printf '%s\n' "$lookup_found" | LC_ALL=C awk 'NF {if(NF!=1 || $0!~/^[A-Za-z0-9_][A-Za-z0-9_.-]*$/) exit 1}' || { printf 'Invalid Compose container identifiers.\n' >&2; exit 1; }
|
|
8443
|
+
lookup_ids=$(printf '%s\n%s\n' "$lookup_ids" "$lookup_found" | LC_ALL=C awk 'NF && !seen[$0]++ {print}') || exit 1
|
|
8444
|
+
done
|
|
8445
|
+
for lookup_id in $lookup_ids; do
|
|
8446
|
+
lookup_labels=$("$lookup_runtime" inspect --format '${labelFormat}' "$lookup_id") || { printf 'Cannot inspect Compose container labels.\n' >&2; exit 1; }
|
|
8447
|
+
printf '%s\n' "$lookup_labels" | LC_ALL=C awk -F'|' -v project="$lookup_project" -v service="$lookup_service" '
|
|
8448
|
+
NR!=1 || NF!=4 {exit 1}
|
|
8449
|
+
{
|
|
8450
|
+
docker=($1!="" && $1!="<no value>") || ($2!="" && $2!="<no value>")
|
|
8451
|
+
podman=($3!="" && $3!="<no value>") || ($4!="" && $4!="<no value>")
|
|
8452
|
+
if((!docker && !podman) || (docker && ($1!=project || $2!=service)) || (podman && ($3!=project || $4!=service))) exit 1
|
|
8453
|
+
}
|
|
8454
|
+
END {if(NR!=1) exit 1}
|
|
8455
|
+
' || { printf 'Container labels do not confirm the requested Compose project and service.\n' >&2; exit 1; }
|
|
8456
|
+
done
|
|
8457
|
+
if [ -n "$lookup_ids" ]; then printf '%s\n' "$lookup_ids"; fi
|
|
8458
|
+
)
|
|
8459
|
+
`;
|
|
8460
|
+
}
|
|
8461
|
+
|
|
8600
8462
|
// src/container/deploy-runtime-script.ts
|
|
8601
8463
|
function createRuntimeConfigurationScript(primaryService) {
|
|
8602
8464
|
return String.raw`configuration_mount() {
|
|
@@ -9256,7 +9118,7 @@ if [ "$partial" = true ]; then
|
|
|
9256
9118
|
fi
|
|
9257
9119
|
;;
|
|
9258
9120
|
esac
|
|
9259
|
-
#
|
|
9121
|
+
# Web 挂载目录由完整拓扑确定,服务增量必须匹配已部署的目录。
|
|
9260
9122
|
current_web_base=$(deployment_json read metadata.web-base-path string "$source_dir/manifest.json")
|
|
9261
9123
|
previous_web_base=$(deployment_json read metadata.web-base-path string "$root/releases/$previous/manifest.json")
|
|
9262
9124
|
test "$current_web_base" = "$previous_web_base" || { printf 'Web base path changed. Match the installed prefix for partial updates, or create a full deployment.\n' >&2; exit 1; }
|
|
@@ -10152,7 +10014,7 @@ async function createDeploymentBundle(options) {
|
|
|
10152
10014
|
const metadata = {
|
|
10153
10015
|
"layout": `${config.expose.hostPort}:${options.containerPort}:${options.hasWebConfig ? options.proxyService === "gateway" ? "gateway" : "true" : "false"}:${artifact.primaryService}:${options.hasWebConfig ? await artifactChecksum(join(directory, "web.conf")) : ""}${httpsFingerprint ? `:${httpsFingerprint}` : ""}
|
|
10154
10016
|
`,
|
|
10155
|
-
"web-base-path": `${artifact.webBasePath
|
|
10017
|
+
"web-base-path": `${artifact.webBasePath}
|
|
10156
10018
|
`,
|
|
10157
10019
|
"health-paths": `${options.healthPaths.join("\n")}
|
|
10158
10020
|
`,
|
|
@@ -10165,7 +10027,6 @@ async function createDeploymentBundle(options) {
|
|
|
10165
10027
|
"node-runtime": String(nodeRuntime),
|
|
10166
10028
|
"proxy-service": options.hasWebConfig ? options.proxyService ?? "web" : "",
|
|
10167
10029
|
"https": options.httpsAssets?.https ?? "",
|
|
10168
|
-
"managed-project": managed["managed-project"],
|
|
10169
10030
|
// 未选应用时只带容器原生绑定,保持当前发布的应用连接归属。
|
|
10170
10031
|
"managed-bindings": updatesApp ? managed["managed-bindings"] : managed["managed-bindings"].replace(/^.* prefix\n/gm, ""),
|
|
10171
10032
|
"managed-volumes": managed["managed-volumes"],
|
|
@@ -11470,7 +11331,7 @@ function makeDeploymentId() {
|
|
|
11470
11331
|
return `${core.time.formatDate(/* @__PURE__ */ new Date(), "YYYYMMDD-HHmmss")}-${randomBytes(3).toString("hex")}`;
|
|
11471
11332
|
}
|
|
11472
11333
|
function sleep(ms) {
|
|
11473
|
-
return new Promise((
|
|
11334
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
11474
11335
|
}
|
|
11475
11336
|
function createDockerSshProvider(config, container) {
|
|
11476
11337
|
const ssh = config.ssh ? createSshExecutor(config.ssh) : void 0;
|
|
@@ -11502,16 +11363,16 @@ function createDockerSshProvider(config, container) {
|
|
|
11502
11363
|
for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
|
|
11503
11364
|
try {
|
|
11504
11365
|
if (certificate) {
|
|
11505
|
-
const healthy = await new Promise((
|
|
11366
|
+
const healthy = await new Promise((resolve8) => {
|
|
11506
11367
|
const request$1 = request(url, {
|
|
11507
11368
|
ca: [...rootCertificates, certificate],
|
|
11508
11369
|
timeout: 5e3
|
|
11509
11370
|
}, (response) => {
|
|
11510
11371
|
response.resume();
|
|
11511
|
-
|
|
11372
|
+
resolve8(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
|
|
11512
11373
|
});
|
|
11513
|
-
request$1.on("error", () =>
|
|
11514
|
-
request$1.on("close", () =>
|
|
11374
|
+
request$1.on("error", () => resolve8(false));
|
|
11375
|
+
request$1.on("close", () => resolve8(false));
|
|
11515
11376
|
request$1.on("timeout", () => request$1.destroy());
|
|
11516
11377
|
request$1.end();
|
|
11517
11378
|
});
|
|
@@ -11562,7 +11423,7 @@ function createDockerSshProvider(config, container) {
|
|
|
11562
11423
|
const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
|
|
11563
11424
|
const host = config.ssh?.host;
|
|
11564
11425
|
const url = https ? `https://${https.domain}${https.port === 443 ? "" : `:${https.port}`}` : host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
|
|
11565
|
-
const webBasePath = artifact.webBasePath
|
|
11426
|
+
const webBasePath = artifact.webBasePath;
|
|
11566
11427
|
const applicationUrl = `${url}${webBasePath ? `${webBasePath}/` : ""}`;
|
|
11567
11428
|
const standaloneWebConfig = primaryService === "web" && !scan.isSvelteKit && (!!webBasePath || !!https);
|
|
11568
11429
|
const healthPaths = hasWebProxy ? ["/_hai/web-health", `${webBasePath}/`] : scan.isSvelteKit ? [`${webBasePath}/`] : primaryService === "serv" ? [] : standaloneWebConfig ? ["/health", `${webBasePath}/`] : ["/health"];
|
|
@@ -12340,7 +12201,7 @@ async function pollDeploymentStatus(token, deploymentId) {
|
|
|
12340
12201
|
if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
|
|
12341
12202
|
return "error";
|
|
12342
12203
|
}
|
|
12343
|
-
await new Promise((
|
|
12204
|
+
await new Promise((resolve8) => setTimeout(resolve8, POLL_INTERVAL));
|
|
12344
12205
|
}
|
|
12345
12206
|
return "building";
|
|
12346
12207
|
}
|
|
@@ -13161,8 +13022,6 @@ var deploy = {
|
|
|
13161
13022
|
if (!currentProvider && !currentContainerProvider) {
|
|
13162
13023
|
return notInitialized.result();
|
|
13163
13024
|
}
|
|
13164
|
-
if (options && "projectName" in options && options.projectName !== void 0)
|
|
13165
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
|
|
13166
13025
|
logger12.info("Starting deployment", { appDir, environment: options?.environment });
|
|
13167
13026
|
const scanOptions = { ...options, environment: options?.environment ?? currentConfig?.environment };
|
|
13168
13027
|
const scanResult = await scanApp(appDir, scanOptions);
|
|
@@ -13896,8 +13755,6 @@ function describeMiddlewareBackend(backend) {
|
|
|
13896
13755
|
return deployM(backend.purpose);
|
|
13897
13756
|
}
|
|
13898
13757
|
async function createDeploymentPlan(appDir, input, options = {}) {
|
|
13899
|
-
if ("projectName" in options && options.projectName !== void 0)
|
|
13900
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
|
|
13901
13758
|
const parsed = DeployConfigSchema.safeParse(input);
|
|
13902
13759
|
if (!parsed.success) {
|
|
13903
13760
|
return err({
|
|
@@ -14098,389 +13955,7 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
14098
13955
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
14099
13956
|
}
|
|
14100
13957
|
}
|
|
14101
|
-
var nameSchema = z.string().regex(/^[a-z0-9][\w.-]{0,127}$/i);
|
|
14102
|
-
var passwordSchema = z.string().min(1).max(1024).refine((value) => Buffer.from(value, "utf8").toString("utf8") === value && [...value].every((character) => character.codePointAt(0) >= 32 && character.codePointAt(0) !== 127));
|
|
14103
|
-
var importSchema = z.strictObject({
|
|
14104
|
-
version: z.literal(1),
|
|
14105
|
-
projectName: nameSchema,
|
|
14106
|
-
credentials: z.strictObject({
|
|
14107
|
-
DB_PASSWORD: passwordSchema.optional(),
|
|
14108
|
-
STORAGE_PASSWORD: passwordSchema.optional(),
|
|
14109
|
-
LDAP_PASSWORD: passwordSchema.optional()
|
|
14110
|
-
}),
|
|
14111
|
-
services: z.array(z.strictObject({
|
|
14112
|
-
service: z.enum(["db", "vecdb", "storage", "ldap"]),
|
|
14113
|
-
container: nameSchema,
|
|
14114
|
-
kind: z.enum(["postgresql", "mysql", "s3", "ldap"]),
|
|
14115
|
-
volumes: z.array(nameSchema).min(1).max(8),
|
|
14116
|
-
username: z.string().min(1).max(128).default("hai"),
|
|
14117
|
-
database: z.string().min(1).max(128).default("hai"),
|
|
14118
|
-
bucket: z.string().regex(/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/).default("hai"),
|
|
14119
|
-
accessKeyId: z.string().min(1).max(128).default("hai-storage"),
|
|
14120
|
-
region: z.string().regex(/^[a-z0-9-]+$/).default("us-east-1"),
|
|
14121
|
-
bindDn: z.string().min(1).max(512).default("cn=admin,dc=example,dc=org")
|
|
14122
|
-
})).min(1).max(4)
|
|
14123
|
-
});
|
|
14124
|
-
var groups2 = {
|
|
14125
|
-
db: "DB_PASSWORD",
|
|
14126
|
-
vecdb: "DB_PASSWORD",
|
|
14127
|
-
storage: "STORAGE_PASSWORD",
|
|
14128
|
-
ldap: "LDAP_PASSWORD"
|
|
14129
|
-
};
|
|
14130
|
-
var importCommitScript = String.raw`(async()=>{const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto');const root=process.argv[1];let temporary;
|
|
14131
|
-
try{process.stdout.write('LOCKED\n');let input='';for await(const chunk of process.stdin){input+=chunk;if(input.length>65536)throw Error()}
|
|
14132
|
-
if(!input)return;const {content}=JSON.parse(input);if(typeof content!=='string'||!content.startsWith('FORMAT_VERSION=2\n'))throw Error();
|
|
14133
|
-
const state=path.join(root,'managed-secrets.env');try{fs.lstatSync(state);throw Error('exists')}catch(error){if(error.code!=='ENOENT')throw error}
|
|
14134
|
-
temporary=path.join(root,'.managed-secrets-'+crypto.randomUUID()+'.next');const fd=fs.openSync(temporary,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_EXCL|fs.constants.O_NOFOLLOW,0o600);
|
|
14135
|
-
try{fs.writeFileSync(fd,content);fs.fsyncSync(fd)}finally{fs.closeSync(fd)}fs.renameSync(temporary,state);temporary=undefined;
|
|
14136
|
-
const directory=fs.openSync(root,fs.constants.O_RDONLY);try{fs.fsyncSync(directory)}finally{fs.closeSync(directory)}process.stdout.write('COMMITTED\n');
|
|
14137
|
-
}catch{process.exitCode=1}finally{if(temporary)try{fs.unlinkSync(temporary)}catch{}}})()`;
|
|
14138
|
-
var s3VerificationScript = String.raw`(async()=>{try{
|
|
14139
|
-
const {createHash,createHmac}=require('node:crypto');let input='';for await(const chunk of process.stdin)input+=chunk;
|
|
14140
|
-
const {endpoint,bucket,accessKeyId,secret,region}=JSON.parse(input);const url=new URL('/'+encodeURIComponent(bucket),endpoint);
|
|
14141
|
-
const hash=value=>createHash('sha256').update(value).digest('hex');const hmac=(key,value)=>createHmac('sha256',key).update(value).digest();
|
|
14142
|
-
const date=new Date().toISOString().replace(/[:-]|\.\d{3}/g,'');const day=date.slice(0,8);const payload=hash('');
|
|
14143
|
-
const headers='host:'+url.host+'\nx-amz-content-sha256:'+payload+'\nx-amz-date:'+date+'\n';const signed='host;x-amz-content-sha256;x-amz-date';
|
|
14144
|
-
const scope=day+'/'+region+'/s3/aws4_request';const canonical=['HEAD',url.pathname,'',headers,signed,payload].join('\n');
|
|
14145
|
-
const key=hmac(hmac(hmac(hmac('AWS4'+secret,day),region),'s3'),'aws4_request');const signature=hmac(key,['AWS4-HMAC-SHA256',date,scope,hash(canonical)].join('\n')).toString('hex');
|
|
14146
|
-
const response=await fetch(url,{method:'HEAD',redirect:'manual',signal:AbortSignal.timeout(10000),headers:{'x-amz-date':date,'x-amz-content-sha256':payload,Authorization:'AWS4-HMAC-SHA256 Credential='+accessKeyId+'/'+scope+', SignedHeaders='+signed+', Signature='+signature}});
|
|
14147
|
-
await response.body?.cancel();if(response.ok)process.stdout.write('VERIFIED');else process.exitCode=1;
|
|
14148
|
-
}catch{process.exitCode=1}})()`;
|
|
14149
|
-
async function importDeploymentSecrets(options) {
|
|
14150
|
-
let lock;
|
|
14151
|
-
let commitAttempted = false;
|
|
14152
|
-
let phase = "input";
|
|
14153
|
-
try {
|
|
14154
|
-
if (!["docker", "podman"].includes(options.runtime))
|
|
14155
|
-
throw new Error("input");
|
|
14156
|
-
const root = resolve(options.directory);
|
|
14157
|
-
if (realpathSync(root) !== root || !lstatSync(root).isDirectory() || process.platform !== "win32" && lstatSync(root).mode & 18)
|
|
14158
|
-
throw new Error("input");
|
|
14159
|
-
const input = privateFile(resolve(options.inputFile));
|
|
14160
|
-
const parsed = importSchema.safeParse(JSON.parse(input));
|
|
14161
|
-
if (!parsed.success) {
|
|
14162
|
-
return err({
|
|
14163
|
-
...HaiDeployError.CONFIG_ERROR,
|
|
14164
|
-
message: deployM("deploy_secretImportFailed", {
|
|
14165
|
-
params: { phase }
|
|
14166
|
-
}),
|
|
14167
|
-
ext: {
|
|
14168
|
-
phase,
|
|
14169
|
-
state: "unchanged",
|
|
14170
|
-
fields: parsed.error.issues.map((issue) => issue.path.join("."))
|
|
14171
|
-
}
|
|
14172
|
-
});
|
|
14173
|
-
}
|
|
14174
|
-
const config = parsed.data;
|
|
14175
|
-
const project = `hai-${config.projectName}`;
|
|
14176
|
-
const selectedGroups = [
|
|
14177
|
-
...new Set(config.services.map((service) => groups2[service.service]))
|
|
14178
|
-
].sort();
|
|
14179
|
-
if (new Set(config.services.map((service) => service.service)).size !== config.services.length || Object.keys(config.credentials).some((group) => !selectedGroups.includes(group)) || selectedGroups.some((group) => !config.credentials[group])) {
|
|
14180
|
-
throw new Error("input");
|
|
14181
|
-
}
|
|
14182
|
-
const secrets = Object.values(config.credentials).filter((value) => typeof value === "string");
|
|
14183
|
-
const stateFile = join(root, "managed-secrets.env");
|
|
14184
|
-
const lockFile = join(root, ".deploy.lock");
|
|
14185
|
-
phase = "lock";
|
|
14186
|
-
if (options.dryRun === false) {
|
|
14187
|
-
if (process.platform !== "linux")
|
|
14188
|
-
throw new Error("lock");
|
|
14189
|
-
if (existsSync(lockFile) && (lstatSync(lockFile).isSymbolicLink() || !lstatSync(lockFile).isFile() || lstatSync(lockFile).nlink !== 1))
|
|
14190
|
-
throw new Error("lock");
|
|
14191
|
-
lock = await acquireImportLock(lockFile, root);
|
|
14192
|
-
}
|
|
14193
|
-
phase = "identity";
|
|
14194
|
-
const volumes = [];
|
|
14195
|
-
for (const service of config.services) {
|
|
14196
|
-
const kindValid = service.service === "storage" ? service.kind === "s3" : service.service === "ldap" ? service.kind === "ldap" : ["postgresql", "mysql"].includes(service.kind);
|
|
14197
|
-
if (!kindValid)
|
|
14198
|
-
throw new Error("identity");
|
|
14199
|
-
if (["postgresql", "mysql"].includes(service.kind) && (service.username !== "hai" || service.database !== "hai") || service.kind === "s3" && (service.accessKeyId !== "hai-storage" || service.bucket !== "hai" || service.region !== "us-east-1") || service.kind === "ldap" && service.bindDn !== "cn=admin,dc=example,dc=org") {
|
|
14200
|
-
throw new Error("identity");
|
|
14201
|
-
}
|
|
14202
|
-
const identity = await inspectContainer(options.runtime, service.container, secrets);
|
|
14203
|
-
if (identity.project !== project || identity.service !== service.service || !identity.running)
|
|
14204
|
-
throw new Error("identity");
|
|
14205
|
-
for (const volume of service.volumes) {
|
|
14206
|
-
if (!identity.volumes.includes(volume))
|
|
14207
|
-
throw new Error("identity");
|
|
14208
|
-
volumes.push({ group: groups2[service.service], name: volume });
|
|
14209
|
-
}
|
|
14210
|
-
phase = "verification";
|
|
14211
|
-
const password = config.credentials[groups2[service.service]];
|
|
14212
|
-
let verified = false;
|
|
14213
|
-
if (service.kind === "s3") {
|
|
14214
|
-
const helper2 = `hai-${config.projectName}-app`;
|
|
14215
|
-
const application = await inspectContainer(options.runtime, helper2, secrets);
|
|
14216
|
-
if (application.project !== project || application.service !== "app" || !application.running || !application.networks.some((network) => identity.networks.includes(network)))
|
|
14217
|
-
throw new Error("identity");
|
|
14218
|
-
const result = await runCommand(options.runtime, [
|
|
14219
|
-
"exec",
|
|
14220
|
-
"-i",
|
|
14221
|
-
helper2,
|
|
14222
|
-
"node",
|
|
14223
|
-
"-e",
|
|
14224
|
-
s3VerificationScript
|
|
14225
|
-
], {
|
|
14226
|
-
input: JSON.stringify({
|
|
14227
|
-
endpoint: `http://${service.container}:9000`,
|
|
14228
|
-
bucket: service.bucket,
|
|
14229
|
-
accessKeyId: service.accessKeyId,
|
|
14230
|
-
secret: password,
|
|
14231
|
-
region: service.region
|
|
14232
|
-
}),
|
|
14233
|
-
secrets,
|
|
14234
|
-
timeoutMs: 15e3,
|
|
14235
|
-
maxCaptureChars: 1024
|
|
14236
|
-
});
|
|
14237
|
-
verified = result.success && result.data.stdout.trim() === "VERIFIED";
|
|
14238
|
-
} else {
|
|
14239
|
-
const args = service.kind === "ldap" ? [
|
|
14240
|
-
"exec",
|
|
14241
|
-
"-i",
|
|
14242
|
-
service.container,
|
|
14243
|
-
"ldapwhoami",
|
|
14244
|
-
"-x",
|
|
14245
|
-
"-H",
|
|
14246
|
-
"ldap://127.0.0.1:389",
|
|
14247
|
-
"-D",
|
|
14248
|
-
service.bindDn,
|
|
14249
|
-
"-y",
|
|
14250
|
-
"/dev/stdin"
|
|
14251
|
-
] : [
|
|
14252
|
-
"exec",
|
|
14253
|
-
"-i",
|
|
14254
|
-
service.container,
|
|
14255
|
-
"sh",
|
|
14256
|
-
"-c",
|
|
14257
|
-
service.kind === "postgresql" ? 'IFS= read -r PGPASSWORD; export PGPASSWORD; exec psql --no-psqlrc -h "$3" -U "$1" -d "dbname=$2 require_auth=scram-sha-256,md5,password" -Atc "SELECT 1"' : 'IFS= read -r MYSQL_PWD; export MYSQL_PWD; exec mysql -h 127.0.0.1 -u "$1" "$2" -Nse "SELECT 1"',
|
|
14258
|
-
"hai-secret-check",
|
|
14259
|
-
service.username,
|
|
14260
|
-
service.database,
|
|
14261
|
-
service.container
|
|
14262
|
-
];
|
|
14263
|
-
const result = await runCommand(options.runtime, args, {
|
|
14264
|
-
input: service.kind === "ldap" ? password : `${password}
|
|
14265
|
-
`,
|
|
14266
|
-
secrets,
|
|
14267
|
-
timeoutMs: 15e3,
|
|
14268
|
-
maxCaptureChars: 1024
|
|
14269
|
-
});
|
|
14270
|
-
verified = result.success && !result.data.stdoutTruncated && (service.kind === "ldap" ? result.data.stdout.trim().startsWith("dn:") : result.data.stdout.trim() === "1");
|
|
14271
|
-
if (verified && service.kind === "mysql") {
|
|
14272
|
-
const rootCheck = await runCommand(options.runtime, [
|
|
14273
|
-
...args.slice(0, -3),
|
|
14274
|
-
"root",
|
|
14275
|
-
service.database,
|
|
14276
|
-
service.container
|
|
14277
|
-
], {
|
|
14278
|
-
input: `${password}
|
|
14279
|
-
`,
|
|
14280
|
-
secrets,
|
|
14281
|
-
timeoutMs: 15e3,
|
|
14282
|
-
maxCaptureChars: 1024
|
|
14283
|
-
});
|
|
14284
|
-
verified = rootCheck.success && !rootCheck.data.stdoutTruncated && rootCheck.data.stdout.trim() === "1";
|
|
14285
|
-
const invalidPassword = `hai-import-invalid-${randomUUID()}`;
|
|
14286
|
-
for (const username of [service.username, "root"]) {
|
|
14287
|
-
if (!verified)
|
|
14288
|
-
break;
|
|
14289
|
-
const negative = await runCommand(options.runtime, [
|
|
14290
|
-
...args.slice(0, -3),
|
|
14291
|
-
username,
|
|
14292
|
-
service.database,
|
|
14293
|
-
service.container
|
|
14294
|
-
], {
|
|
14295
|
-
input: `${invalidPassword}
|
|
14296
|
-
`,
|
|
14297
|
-
secrets: [...secrets, invalidPassword],
|
|
14298
|
-
timeoutMs: 15e3,
|
|
14299
|
-
maxCaptureChars: 1024
|
|
14300
|
-
});
|
|
14301
|
-
verified = !negative.success && /ERROR 1045 \(28000\)/.test(negative.error.message);
|
|
14302
|
-
}
|
|
14303
|
-
}
|
|
14304
|
-
}
|
|
14305
|
-
if (!verified)
|
|
14306
|
-
throw new Error("verification");
|
|
14307
|
-
phase = "identity";
|
|
14308
|
-
}
|
|
14309
|
-
phase = "state";
|
|
14310
|
-
const uniqueVolumes = [
|
|
14311
|
-
...new Map(volumes.map((volume) => [`${volume.group}:${volume.name}`, volume])).values()
|
|
14312
|
-
].sort((a, b) => `${a.group}:${a.name}`.localeCompare(`${b.group}:${b.name}`));
|
|
14313
|
-
const lines = [
|
|
14314
|
-
"FORMAT_VERSION=2",
|
|
14315
|
-
`PROJECT=${project}`,
|
|
14316
|
-
...selectedGroups.map((group) => `${group}=${Buffer.from(config.credentials[group], "utf8").toString("hex")}`),
|
|
14317
|
-
...uniqueVolumes.map((volume, index) => `VOLUME_${index + 1}=${volume.group}:${volume.name}`)
|
|
14318
|
-
];
|
|
14319
|
-
let changed = false;
|
|
14320
|
-
if (existsSync(stateFile) || isSymbolicLink(stateFile)) {
|
|
14321
|
-
const existing = readImportedState(privateFile(stateFile));
|
|
14322
|
-
if (existing.FORMAT_VERSION !== "2" || existing.PROJECT !== project || selectedGroups.some((group) => existing[group] !== Buffer.from(config.credentials[group], "utf8").toString("hex")) || uniqueVolumes.some((volume) => !Object.entries(existing).some(([key, value]) => /^VOLUME_\d+$/.test(key) && value === `${volume.group}:${volume.name}`)))
|
|
14323
|
-
throw new Error("state");
|
|
14324
|
-
} else if (options.dryRun === false) {
|
|
14325
|
-
commitAttempted = true;
|
|
14326
|
-
if (!lock || !await lock.commit(`${lines.join("\n")}
|
|
14327
|
-
`))
|
|
14328
|
-
throw new Error("lock");
|
|
14329
|
-
changed = true;
|
|
14330
|
-
}
|
|
14331
|
-
return ok({
|
|
14332
|
-
projectName: config.projectName,
|
|
14333
|
-
stateVersion: 2,
|
|
14334
|
-
groups: selectedGroups,
|
|
14335
|
-
volumes: uniqueVolumes.map((volume) => volume.name),
|
|
14336
|
-
changed
|
|
14337
|
-
});
|
|
14338
|
-
} catch {
|
|
14339
|
-
return err({
|
|
14340
|
-
...HaiDeployError.CONFIG_ERROR,
|
|
14341
|
-
message: deployM("deploy_secretImportFailed", {
|
|
14342
|
-
params: { phase }
|
|
14343
|
-
}),
|
|
14344
|
-
ext: {
|
|
14345
|
-
phase,
|
|
14346
|
-
state: commitAttempted ? "unknown" : "unchanged"
|
|
14347
|
-
}
|
|
14348
|
-
});
|
|
14349
|
-
} finally {
|
|
14350
|
-
await lock?.release();
|
|
14351
|
-
}
|
|
14352
|
-
}
|
|
14353
|
-
function readImportedState(text) {
|
|
14354
|
-
const result = {};
|
|
14355
|
-
for (const line of text.trimEnd().split("\n")) {
|
|
14356
|
-
const match = /^(FORMAT_VERSION|PROJECT|DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD|VOLUME_[1-9]\d*)=([^=]+)$/.exec(line);
|
|
14357
|
-
if (!match || Object.hasOwn(result, match[1]))
|
|
14358
|
-
throw new Error("state");
|
|
14359
|
-
const key = match[1];
|
|
14360
|
-
const value = match[2];
|
|
14361
|
-
if (key.endsWith("_PASSWORD")) {
|
|
14362
|
-
if (!/^(?:[a-f0-9]{2})+$/.test(value))
|
|
14363
|
-
throw new Error("state");
|
|
14364
|
-
const decoded = Buffer.from(value, "hex").toString("utf8");
|
|
14365
|
-
if (!passwordSchema.safeParse(decoded).success || Buffer.from(decoded, "utf8").toString("hex") !== value)
|
|
14366
|
-
throw new Error("state");
|
|
14367
|
-
} else if (key.startsWith("VOLUME_") && !/^(?:DB_PASSWORD|STORAGE_PASSWORD|LDAP_PASSWORD):[a-zA-Z0-9][\w.-]*$/.test(value)) {
|
|
14368
|
-
throw new Error("state");
|
|
14369
|
-
}
|
|
14370
|
-
result[key] = value;
|
|
14371
|
-
}
|
|
14372
|
-
if (result.FORMAT_VERSION !== "2" || !nameSchema.safeParse(result.PROJECT).success)
|
|
14373
|
-
throw new Error("state");
|
|
14374
|
-
return result;
|
|
14375
|
-
}
|
|
14376
|
-
function isSymbolicLink(path) {
|
|
14377
|
-
try {
|
|
14378
|
-
return lstatSync(path).isSymbolicLink();
|
|
14379
|
-
} catch {
|
|
14380
|
-
return false;
|
|
14381
|
-
}
|
|
14382
|
-
}
|
|
14383
|
-
function privateFile(path) {
|
|
14384
|
-
return readLocalFile(path, { maxBytes: 65536, private: true }).toString("utf8");
|
|
14385
|
-
}
|
|
14386
|
-
async function inspectContainer(runtime, container, secrets) {
|
|
14387
|
-
const result = await runCommand(runtime, [
|
|
14388
|
-
"inspect",
|
|
14389
|
-
"--format",
|
|
14390
|
-
"{{json .Config.Labels}}\n{{json .Mounts}}\n{{json .State.Running}}\n{{json .NetworkSettings.Networks}}",
|
|
14391
|
-
container
|
|
14392
|
-
], { secrets, timeoutMs: 1e4, maxCaptureChars: 32768 });
|
|
14393
|
-
if (!result.success || result.data.stdoutTruncated)
|
|
14394
|
-
throw new Error("identity");
|
|
14395
|
-
const [labelsText, mountsText, runningText, networksText] = result.data.stdout.trim().split("\n");
|
|
14396
|
-
const labels = z.record(z.string(), z.string()).parse(JSON.parse(labelsText));
|
|
14397
|
-
const mounts = z.array(z.object({
|
|
14398
|
-
Type: z.string(),
|
|
14399
|
-
Name: z.string().optional()
|
|
14400
|
-
})).parse(JSON.parse(mountsText));
|
|
14401
|
-
const networks = z.record(z.string(), z.object({
|
|
14402
|
-
NetworkID: z.string()
|
|
14403
|
-
})).parse(JSON.parse(networksText) ?? {});
|
|
14404
|
-
const dockerProject = labels["com.docker.compose.project"] || void 0;
|
|
14405
|
-
const podmanProject = labels["io.podman.compose.project"] || void 0;
|
|
14406
|
-
const dockerService = labels["com.docker.compose.service"] || void 0;
|
|
14407
|
-
const podmanService = labels["io.podman.compose.service"] || void 0;
|
|
14408
|
-
if (Boolean(dockerProject) !== Boolean(dockerService) || Boolean(podmanProject) !== Boolean(podmanService) || dockerProject && podmanProject && dockerProject !== podmanProject || dockerService && podmanService && dockerService !== podmanService) {
|
|
14409
|
-
throw new Error("identity");
|
|
14410
|
-
}
|
|
14411
|
-
return {
|
|
14412
|
-
project: dockerProject ?? podmanProject,
|
|
14413
|
-
service: dockerService ?? podmanService,
|
|
14414
|
-
running: runningText === "true",
|
|
14415
|
-
volumes: mounts.flatMap((mount) => mount.Type === "volume" && mount.Name ? [mount.Name] : []),
|
|
14416
|
-
networks: Object.values(networks).map((network) => network.NetworkID)
|
|
14417
|
-
};
|
|
14418
|
-
}
|
|
14419
|
-
async function acquireImportLock(path, root) {
|
|
14420
|
-
return new Promise((resolveLock, reject) => {
|
|
14421
|
-
const child = spawn("flock", [
|
|
14422
|
-
"-n",
|
|
14423
|
-
path,
|
|
14424
|
-
process.execPath,
|
|
14425
|
-
"-e",
|
|
14426
|
-
importCommitScript,
|
|
14427
|
-
root
|
|
14428
|
-
], {
|
|
14429
|
-
stdio: ["pipe", "pipe", "ignore"],
|
|
14430
|
-
windowsHide: true
|
|
14431
|
-
});
|
|
14432
|
-
let acquired = false;
|
|
14433
|
-
let exited = false;
|
|
14434
|
-
let output = "";
|
|
14435
|
-
const finished = new Promise((done) => child.once("close", () => {
|
|
14436
|
-
exited = true;
|
|
14437
|
-
done();
|
|
14438
|
-
}));
|
|
14439
|
-
child.stdin.on("error", () => {
|
|
14440
|
-
});
|
|
14441
|
-
const timer = setTimeout(() => {
|
|
14442
|
-
child.stdin.end();
|
|
14443
|
-
child.kill();
|
|
14444
|
-
reject(new Error("lock"));
|
|
14445
|
-
}, 5e3);
|
|
14446
|
-
child.once("error", () => {
|
|
14447
|
-
clearTimeout(timer);
|
|
14448
|
-
reject(new Error("lock"));
|
|
14449
|
-
});
|
|
14450
|
-
child.once("exit", () => {
|
|
14451
|
-
if (!acquired) {
|
|
14452
|
-
clearTimeout(timer);
|
|
14453
|
-
reject(new Error("lock"));
|
|
14454
|
-
}
|
|
14455
|
-
});
|
|
14456
|
-
child.stdout.on("data", (data) => {
|
|
14457
|
-
output += data.toString();
|
|
14458
|
-
if (acquired || !output.includes("\n"))
|
|
14459
|
-
return;
|
|
14460
|
-
if (!output.startsWith("LOCKED\n") || exited) {
|
|
14461
|
-
child.stdin.end();
|
|
14462
|
-
reject(new Error("lock"));
|
|
14463
|
-
return;
|
|
14464
|
-
}
|
|
14465
|
-
acquired = true;
|
|
14466
|
-
clearTimeout(timer);
|
|
14467
|
-
resolveLock({
|
|
14468
|
-
commit: async (content) => {
|
|
14469
|
-
if (exited)
|
|
14470
|
-
return false;
|
|
14471
|
-
child.stdin.end(JSON.stringify({ content }));
|
|
14472
|
-
await finished;
|
|
14473
|
-
return child.exitCode === 0 && output === "LOCKED\nCOMMITTED\n";
|
|
14474
|
-
},
|
|
14475
|
-
release: async () => {
|
|
14476
|
-
child.stdin.end();
|
|
14477
|
-
await finished;
|
|
14478
|
-
}
|
|
14479
|
-
});
|
|
14480
|
-
});
|
|
14481
|
-
});
|
|
14482
|
-
}
|
|
14483
13958
|
|
|
14484
|
-
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, canKeepMiddleware, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery,
|
|
13959
|
+
export { DEPLOY_MIDDLEWARE, DeployConfigSchema, DeploymentArtifactSchema, HaiDeployError, ProviderConfigSchema, artifactChecksum, canKeepMiddleware, createDeploymentPlan, deploy, describeMiddlewareBackend, getDeployCommandState, getDeployRecovery, loadDeploymentConfig, loadDeploymentEnvironment, mergeEnvironmentLayers, normalizeProjectName, parseMiddlewareField, readDeploymentArtifact, resolveDownloadProxy, resolveImageReference, saveDeploymentEnvironment, saveMiddlewareEnvironment, validateMiddlewareField };
|
|
14485
13960
|
//# sourceMappingURL=index.js.map
|
|
14486
13961
|
//# sourceMappingURL=index.js.map
|