@h-ai/deploy 1.0.0-beta.17 → 1.0.0-beta.19
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 +19 -2
- package/README.ja.md +20 -3
- package/README.md +26 -10
- package/dist/deploy-runtime-env.mjs +37 -10
- package/dist/index.d.ts +11 -5
- package/dist/index.js +2050 -402
- package/dist/index.js.map +1 -1
- package/package.json +10 -10
package/dist/index.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { createHash, randomUUID, randomBytes, X509Certificate, createPublicKey, createPrivateKey } from 'crypto';
|
|
2
|
-
import { createReadStream, readFileSync, existsSync, lstatSync,
|
|
3
|
-
import { resolve, extname, join, dirname,
|
|
2
|
+
import { createReadStream, readFileSync, existsSync, lstatSync, mkdirSync, readdirSync, realpathSync, openSync, constants, fstatSync, readSync, closeSync, fchmodSync, writeFileSync, fsyncSync, renameSync, unlinkSync, chmodSync, mkdtempSync, copyFileSync, rmSync, createWriteStream, statSync } from 'fs';
|
|
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
6
|
import { spawn, execFileSync } from 'child_process';
|
|
7
7
|
import process from 'process';
|
|
8
8
|
import { LineCounter, parseDocument, parse, stringify, parseAllDocuments } from 'yaml';
|
|
9
|
+
import { Buffer, constants as constants$1 } from 'buffer';
|
|
9
10
|
import { homedir, tmpdir } from 'os';
|
|
10
11
|
import { request } from 'https';
|
|
11
12
|
import { rootCertificates } from 'tls';
|
|
@@ -16,19 +17,28 @@ import { Transform } from 'stream';
|
|
|
16
17
|
import { pipeline } from 'stream/promises';
|
|
17
18
|
import { readdir, readFile } from 'fs/promises';
|
|
18
19
|
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
19
|
-
import { Buffer } from 'buffer';
|
|
20
20
|
import { isIP } from 'net';
|
|
21
21
|
|
|
22
22
|
// src/deploy-artifact.ts
|
|
23
23
|
var DeploymentArtifactSchema = z.object({
|
|
24
24
|
/** 清单格式版本,与应用版本和部署环境无关。 */
|
|
25
|
-
version: z.union([
|
|
25
|
+
version: z.union([
|
|
26
|
+
z.literal(4),
|
|
27
|
+
z.literal(5)
|
|
28
|
+
]),
|
|
26
29
|
/** 主运行服务:独立后端为 serv,独立浏览器站点(静态或 SSR)为 web。 */
|
|
27
30
|
primaryService: z.enum(["serv", "web"]),
|
|
28
31
|
/** 应用根 package.json 的版本,用于归档、Compose 与桌面交付;最长 80 字符。 */
|
|
29
32
|
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),
|
|
30
33
|
/** 本次构建标识;同一应用版本允许重复构建。 */
|
|
31
34
|
buildId: z.string().max(128).regex(/^[a-z0-9][\w.+-]*$/i),
|
|
35
|
+
/** 构建启动时观察到的 Git 来源;历史清单可以省略。 */
|
|
36
|
+
source: z.object({
|
|
37
|
+
/** 工程 package.json 所在仓库的提交 SHA;无法确定时为 unknown。 */
|
|
38
|
+
commit: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|unknown)$/),
|
|
39
|
+
/** 工程及所选镜像上下文/构建定义有未提交改动时为 true;跨仓库或无法检查时为 null。 */
|
|
40
|
+
dirty: z.boolean().nullable()
|
|
41
|
+
}).optional(),
|
|
32
42
|
/** 只包含所选镜像的构建不能用于首次安装。 */
|
|
33
43
|
partial: z.boolean().default(false),
|
|
34
44
|
/** 是否包含浏览器端;仅构建 serv 时也保留完整工程的 Web 拓扑。 */
|
|
@@ -49,6 +59,8 @@ var DeploymentArtifactSchema = z.object({
|
|
|
49
59
|
imageRef: z.string().regex(/^[a-z0-9][a-z0-9./:_-]*:[a-z0-9_][a-z0-9_.-]*$/).refine((value) => !value.endsWith(":latest")),
|
|
50
60
|
/** tar 内容的 SHA-256,用于部署前校验。 */
|
|
51
61
|
sha256: z.string().regex(/^[a-f0-9]{64}$/),
|
|
62
|
+
/** 引擎实际构建的镜像 OS/架构;历史清单可以省略。 */
|
|
63
|
+
platform: z.string().regex(/^[a-z0-9][a-z0-9_.-]*\/[a-z0-9][a-z0-9_.-]*$/).optional(),
|
|
52
64
|
/** 容器内部监听端口;服务器公开端口由部署选择。 */
|
|
53
65
|
containerPort: z.number().int().min(1).max(65535).optional()
|
|
54
66
|
})).min(1).max(2).refine((images) => new Set(images.map((image) => image.name)).size === images.length)
|
|
@@ -552,8 +564,13 @@ var SshConfigSchema = z.object({
|
|
|
552
564
|
}).superRefine((ssh, context) => {
|
|
553
565
|
if (ssh.auth === "key" || ssh.auth === void 0 && !ssh.password)
|
|
554
566
|
return;
|
|
555
|
-
if (!ssh.password || /[\r\n\0]/.test(ssh.password) || new TextEncoder().encode(ssh.password).length > 1023)
|
|
556
|
-
context.addIssue({
|
|
567
|
+
if (!ssh.password || /[\r\n\0]/.test(ssh.password) || new TextEncoder().encode(ssh.password).length > 1023) {
|
|
568
|
+
context.addIssue({
|
|
569
|
+
code: "custom",
|
|
570
|
+
path: ["password"],
|
|
571
|
+
message: deployM("deploy_sshPasswordInvalid")
|
|
572
|
+
});
|
|
573
|
+
}
|
|
557
574
|
});
|
|
558
575
|
var HttpsDomainSchema = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i).refine((value) => !value.includes(".."));
|
|
559
576
|
var httpsOptions = {
|
|
@@ -590,8 +607,13 @@ var PortExposeSchema = z.object({
|
|
|
590
607
|
/** 可选 HTTPS:已有 PEM 文件或官方 Certbot 自动申请续期;省略保留 HTTP。 */
|
|
591
608
|
https: HttpsConfigSchema.optional().describe("\u53EF\u9009 HTTPS \u5165\u53E3\uFF1Bfiles \u4F7F\u7528\u5DF2\u6709\u8BC1\u4E66\uFF0Cletsencrypt \u4F7F\u7528\u5B98\u65B9 Certbot \u7533\u8BF7\u7EED\u671F\uFF0C\u7701\u7565\u4FDD\u6301 HTTP")
|
|
592
609
|
}).superRefine((expose, context) => {
|
|
593
|
-
if (expose.https && (expose.https.port === expose.hostPort || expose.https.type === "letsencrypt" && expose.hostPort !== 80))
|
|
594
|
-
context.addIssue({
|
|
610
|
+
if (expose.https && (expose.https.port === expose.hostPort || expose.https.type === "letsencrypt" && expose.hostPort !== 80)) {
|
|
611
|
+
context.addIssue({
|
|
612
|
+
code: "custom",
|
|
613
|
+
path: ["https"],
|
|
614
|
+
message: deployM("deploy_httpsPortsInvalid")
|
|
615
|
+
});
|
|
616
|
+
}
|
|
595
617
|
});
|
|
596
618
|
var ExposeSchema = PortExposeSchema;
|
|
597
619
|
var DockerSshProviderConfigSchema = z.object({
|
|
@@ -608,7 +630,12 @@ var DockerSshProviderConfigSchema = z.object({
|
|
|
608
630
|
/** 应用暴露方式 */
|
|
609
631
|
expose: ExposeSchema,
|
|
610
632
|
/** Nginx 前方可信负载均衡的 IP/CIDR;省略时不接受外部转发头。 */
|
|
611
|
-
trustedEdgeProxies: z.array(z.union([
|
|
633
|
+
trustedEdgeProxies: z.array(z.union([
|
|
634
|
+
z.ipv4(),
|
|
635
|
+
z.ipv6(),
|
|
636
|
+
z.cidrv4(),
|
|
637
|
+
z.cidrv6()
|
|
638
|
+
])).max(1e3).optional()
|
|
612
639
|
});
|
|
613
640
|
var ProviderConfigSchema = z.discriminatedUnion("type", [
|
|
614
641
|
VercelProviderConfigSchema,
|
|
@@ -649,23 +676,52 @@ var ServicesConfigSchema = z.object({
|
|
|
649
676
|
sms: z.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
|
|
650
677
|
}).optional();
|
|
651
678
|
function provisionerFields(provisioner) {
|
|
652
|
-
const schema = [
|
|
679
|
+
const schema = [
|
|
680
|
+
NeonServiceSchema,
|
|
681
|
+
UpstashServiceSchema,
|
|
682
|
+
R2ServiceSchema,
|
|
683
|
+
ResendServiceSchema,
|
|
684
|
+
AliyunSmsServiceSchema
|
|
685
|
+
].find((item) => item.shape.provisioner.value === provisioner);
|
|
653
686
|
return schema ? Object.keys(schema.shape).filter((name) => name !== "provisioner") : [];
|
|
654
687
|
}
|
|
655
|
-
var reservedComposeHosts = /* @__PURE__ */ new Set([
|
|
688
|
+
var reservedComposeHosts = /* @__PURE__ */ new Set([
|
|
689
|
+
"localhost",
|
|
690
|
+
"serv",
|
|
691
|
+
"web",
|
|
692
|
+
"gateway",
|
|
693
|
+
"certbot",
|
|
694
|
+
"db",
|
|
695
|
+
"cache",
|
|
696
|
+
"storage",
|
|
697
|
+
"vecdb",
|
|
698
|
+
"ldap",
|
|
699
|
+
"email",
|
|
700
|
+
"sms"
|
|
701
|
+
]);
|
|
656
702
|
var composeHostname = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)*[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i;
|
|
657
703
|
var ComposeServiceConfigSchema = z.strictObject({
|
|
658
704
|
/** 容器内的主机名到 IPv4 或 IPv6 地址映射,不能覆盖框架内部服务名。 */
|
|
659
|
-
extra_hosts: z.record(z.string(), z.union([
|
|
705
|
+
extra_hosts: z.record(z.string(), z.union([
|
|
706
|
+
z.ipv4(),
|
|
707
|
+
z.ipv6()
|
|
708
|
+
], {
|
|
660
709
|
error: () => deployM("deploy_composeHostAddressInvalid")
|
|
661
710
|
})).superRefine((hosts, ctx) => {
|
|
662
711
|
const seen = /* @__PURE__ */ new Set();
|
|
663
712
|
for (const host of Object.keys(hosts)) {
|
|
664
713
|
const key = host.toLowerCase();
|
|
665
714
|
if (host.length > 253 || !composeHostname.test(host)) {
|
|
666
|
-
ctx.addIssue({
|
|
715
|
+
ctx.addIssue({
|
|
716
|
+
code: "custom",
|
|
717
|
+
message: deployM("deploy_composeHostnameInvalid")
|
|
718
|
+
});
|
|
667
719
|
} else if (reservedComposeHosts.has(key) || seen.has(key)) {
|
|
668
|
-
ctx.addIssue({
|
|
720
|
+
ctx.addIssue({
|
|
721
|
+
code: "custom",
|
|
722
|
+
path: [host],
|
|
723
|
+
message: deployM("deploy_composeHostnameConflict")
|
|
724
|
+
});
|
|
669
725
|
}
|
|
670
726
|
seen.add(key);
|
|
671
727
|
}
|
|
@@ -703,8 +759,12 @@ function validateComposeServices(container, artifact) {
|
|
|
703
759
|
const field2 = `deploy.container.compose.services.${unavailable}`;
|
|
704
760
|
return err({
|
|
705
761
|
...HaiDeployError.CONFIG_ERROR,
|
|
706
|
-
message: deployM("deploy_composeServiceUnavailable", {
|
|
707
|
-
|
|
762
|
+
message: deployM("deploy_composeServiceUnavailable", {
|
|
763
|
+
params: { field: field2 }
|
|
764
|
+
}),
|
|
765
|
+
ext: {
|
|
766
|
+
fields: [field2]
|
|
767
|
+
}
|
|
708
768
|
});
|
|
709
769
|
}
|
|
710
770
|
function resolveImageReference(reference, source) {
|
|
@@ -721,8 +781,11 @@ function resolveImageReference(reference, source) {
|
|
|
721
781
|
parts.unshift("library");
|
|
722
782
|
const mappings = source.type === "cn" ? { "docker.io": "docker.1ms.run", "ghcr.io": "ghcr.1ms.run" } : source.registries;
|
|
723
783
|
const replacement = mappings?.[registry];
|
|
724
|
-
if (!replacement || typeof replacement !== "string")
|
|
725
|
-
throw new Error(deployM("deploy_imageSourceMissing", {
|
|
784
|
+
if (!replacement || typeof replacement !== "string") {
|
|
785
|
+
throw new Error(deployM("deploy_imageSourceMissing", {
|
|
786
|
+
params: { registry }
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
726
789
|
if (replacement.split("/").some((part) => part === "." || part === ".."))
|
|
727
790
|
throw new Error(deployM("deploy_downloadInvalid"));
|
|
728
791
|
let url;
|
|
@@ -739,20 +802,32 @@ function resolveImageReference(reference, source) {
|
|
|
739
802
|
function systemProxy() {
|
|
740
803
|
if (process.platform === "win32") {
|
|
741
804
|
const script = "$p=Get-ItemProperty -LiteralPath 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings'; @{enabled=($p.ProxyEnable -eq 1);server=$p.ProxyServer;bypass=$p.ProxyOverride;pac=$p.AutoConfigURL}|ConvertTo-Json -Compress";
|
|
742
|
-
const value = JSON.parse(execFileSync("powershell.exe", [
|
|
805
|
+
const value = JSON.parse(execFileSync("powershell.exe", [
|
|
806
|
+
"-NoProfile",
|
|
807
|
+
"-NonInteractive",
|
|
808
|
+
"-Command",
|
|
809
|
+
script
|
|
810
|
+
], { encoding: "utf8", windowsHide: true, timeout: 5e3 }));
|
|
743
811
|
if (value.pac)
|
|
744
812
|
throw new Error(deployM("deploy_proxySystemUnsupported"));
|
|
745
813
|
if (!value.enabled || !value.server)
|
|
746
814
|
return {};
|
|
747
815
|
const entries = Object.fromEntries(value.server.split(";").map((item) => item.includes("=") ? item.split("=", 2) : ["all", item]));
|
|
748
|
-
return {
|
|
816
|
+
return {
|
|
817
|
+
http: entries.http ?? entries.all,
|
|
818
|
+
https: entries.https ?? entries.http ?? entries.all,
|
|
819
|
+
bypass: value.bypass?.split(";").filter((item) => item !== "<local>").join(",")
|
|
820
|
+
};
|
|
749
821
|
}
|
|
750
822
|
if (process.platform === "darwin") {
|
|
751
823
|
const output = execFileSync("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8", timeout: 5e3 });
|
|
752
824
|
const get = (key) => output.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
753
825
|
if (get("ProxyAutoConfigEnable") === "1" || get("ProxyAutoDiscoveryEnable") === "1")
|
|
754
826
|
throw new Error(deployM("deploy_proxySystemUnsupported"));
|
|
755
|
-
return {
|
|
827
|
+
return {
|
|
828
|
+
http: get("HTTPEnable") === "1" ? `http://${get("HTTPProxy")}:${get("HTTPPort")}` : void 0,
|
|
829
|
+
https: get("HTTPSEnable") === "1" ? `http://${get("HTTPSProxy")}:${get("HTTPSPort")}` : void 0
|
|
830
|
+
};
|
|
756
831
|
}
|
|
757
832
|
throw new Error(deployM("deploy_proxySystemUnsupported"));
|
|
758
833
|
}
|
|
@@ -780,7 +855,18 @@ function resolveDownloadProxy(config) {
|
|
|
780
855
|
const bypass = mode === "none" ? "" : config?.bypass ?? values.bypass ?? "";
|
|
781
856
|
if (/[\r\n\0]/.test(bypass))
|
|
782
857
|
throw new Error(deployM("deploy_downloadInvalid"));
|
|
783
|
-
return {
|
|
858
|
+
return {
|
|
859
|
+
HTTP_PROXY: http,
|
|
860
|
+
http_proxy: http,
|
|
861
|
+
HTTPS_PROXY: https,
|
|
862
|
+
https_proxy: https,
|
|
863
|
+
NO_PROXY: bypass,
|
|
864
|
+
no_proxy: bypass,
|
|
865
|
+
ALL_PROXY: mode === "inherit" ? process.env.ALL_PROXY ?? process.env.all_proxy ?? "" : "",
|
|
866
|
+
all_proxy: mode === "inherit" ? process.env.ALL_PROXY ?? process.env.all_proxy ?? "" : "",
|
|
867
|
+
FTP_PROXY: mode === "inherit" ? process.env.FTP_PROXY ?? process.env.ftp_proxy ?? "" : "",
|
|
868
|
+
ftp_proxy: mode === "inherit" ? process.env.FTP_PROXY ?? process.env.ftp_proxy ?? "" : ""
|
|
869
|
+
};
|
|
784
870
|
}
|
|
785
871
|
|
|
786
872
|
// src/deploy-config-shapes.json
|
|
@@ -2483,6 +2569,18 @@ var deploy_config_shapes_default = {
|
|
|
2483
2569
|
type: "string",
|
|
2484
2570
|
pattern: "^[A-Z_]\\w*$"
|
|
2485
2571
|
}
|
|
2572
|
+
},
|
|
2573
|
+
gitEnvironmentRefs: {
|
|
2574
|
+
default: {},
|
|
2575
|
+
type: "object",
|
|
2576
|
+
propertyNames: {
|
|
2577
|
+
type: "string",
|
|
2578
|
+
pattern: "^[A-Z_]\\w*$"
|
|
2579
|
+
},
|
|
2580
|
+
additionalProperties: {
|
|
2581
|
+
type: "string",
|
|
2582
|
+
pattern: "^[A-Z_]\\w*$"
|
|
2583
|
+
}
|
|
2486
2584
|
}
|
|
2487
2585
|
},
|
|
2488
2586
|
required: [
|
|
@@ -6108,13 +6206,95 @@ var deploy_config_shapes_default = {
|
|
|
6108
6206
|
}
|
|
6109
6207
|
}
|
|
6110
6208
|
};
|
|
6209
|
+
function regular(metadata, options = {}) {
|
|
6210
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1n || metadata.size < 0n || metadata.size > BigInt(options.maxBytes ?? constants$1.MAX_LENGTH - 1) || options.private && process.platform !== "win32" && ((metadata.mode & 0o077n) !== 0n || process.getuid?.() !== 0 && metadata.uid !== BigInt(process.getuid?.() ?? -1))) {
|
|
6211
|
+
throw new Error("Unsafe local file metadata");
|
|
6212
|
+
}
|
|
6213
|
+
}
|
|
6214
|
+
function sameFile(left, right) {
|
|
6215
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
6216
|
+
}
|
|
6217
|
+
function readLocalFile(path, options = {}) {
|
|
6218
|
+
const before = lstatSync(path, { bigint: true });
|
|
6219
|
+
regular(before, options);
|
|
6220
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
6221
|
+
try {
|
|
6222
|
+
const opened = fstatSync(fd, { bigint: true });
|
|
6223
|
+
regular(opened, options);
|
|
6224
|
+
if (!sameFile(before, opened))
|
|
6225
|
+
throw new Error("Local file changed before opening");
|
|
6226
|
+
const size = Number(opened.size);
|
|
6227
|
+
const bytes = Buffer.alloc(size + 1);
|
|
6228
|
+
let length = 0;
|
|
6229
|
+
while (length < bytes.length) {
|
|
6230
|
+
const read = readSync(fd, bytes, length, bytes.length - length, null);
|
|
6231
|
+
if (!read)
|
|
6232
|
+
break;
|
|
6233
|
+
length += read;
|
|
6234
|
+
}
|
|
6235
|
+
const after = fstatSync(fd, { bigint: true });
|
|
6236
|
+
regular(after, options);
|
|
6237
|
+
if (!sameFile(opened, after) || length !== size || opened.size !== after.size || opened.mtimeNs !== after.mtimeNs || opened.ctimeNs !== after.ctimeNs) {
|
|
6238
|
+
throw new Error("Local file changed while reading");
|
|
6239
|
+
}
|
|
6240
|
+
return bytes.subarray(0, length);
|
|
6241
|
+
} finally {
|
|
6242
|
+
closeSync(fd);
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
function existingFile(path) {
|
|
6246
|
+
try {
|
|
6247
|
+
const metadata = lstatSync(path, { bigint: true });
|
|
6248
|
+
regular(metadata);
|
|
6249
|
+
return metadata;
|
|
6250
|
+
} catch (error) {
|
|
6251
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT")
|
|
6252
|
+
return void 0;
|
|
6253
|
+
throw error;
|
|
6254
|
+
}
|
|
6255
|
+
}
|
|
6256
|
+
function writeLocalFile(path, content, privateFile2 = false) {
|
|
6257
|
+
const previous = existingFile(path);
|
|
6258
|
+
const mode = privateFile2 ? 384 : previous ? Number(previous.mode & 0o777n) : 420;
|
|
6259
|
+
const temporary = join(dirname(path), `.${basename(path)}.${randomUUID()}.tmp`);
|
|
6260
|
+
let fd;
|
|
6261
|
+
let owned;
|
|
6262
|
+
let published = false;
|
|
6263
|
+
try {
|
|
6264
|
+
fd = openSync(temporary, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL, mode);
|
|
6265
|
+
owned = fstatSync(fd, { bigint: true });
|
|
6266
|
+
fchmodSync(fd, mode);
|
|
6267
|
+
writeFileSync(fd, content, "utf8");
|
|
6268
|
+
fsyncSync(fd);
|
|
6269
|
+
closeSync(fd);
|
|
6270
|
+
fd = void 0;
|
|
6271
|
+
existingFile(path);
|
|
6272
|
+
const candidate = lstatSync(temporary, { bigint: true });
|
|
6273
|
+
regular(candidate);
|
|
6274
|
+
if (!sameFile(owned, candidate))
|
|
6275
|
+
throw new Error("Local temporary file ownership changed");
|
|
6276
|
+
renameSync(temporary, path);
|
|
6277
|
+
published = true;
|
|
6278
|
+
} finally {
|
|
6279
|
+
if (fd !== void 0)
|
|
6280
|
+
closeSync(fd);
|
|
6281
|
+
if (!published && owned) {
|
|
6282
|
+
try {
|
|
6283
|
+
const candidate = lstatSync(temporary, { bigint: true });
|
|
6284
|
+
if (candidate.isFile() && sameFile(owned, candidate))
|
|
6285
|
+
unlinkSync(temporary);
|
|
6286
|
+
} catch {
|
|
6287
|
+
}
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
}
|
|
6111
6291
|
|
|
6112
6292
|
// src/deploy-environment.ts
|
|
6113
6293
|
var shapes = deploy_config_shapes_default;
|
|
6114
6294
|
function environmentFile(path) {
|
|
6115
6295
|
const values = {};
|
|
6116
6296
|
if (existsSync(path)) {
|
|
6117
|
-
for (const [name, value] of Object.entries(core.config.parseEnv(
|
|
6297
|
+
for (const [name, value] of Object.entries(core.config.parseEnv(readLocalFile(path).toString("utf8")))) {
|
|
6118
6298
|
if (value !== void 0)
|
|
6119
6299
|
values[name] = value;
|
|
6120
6300
|
}
|
|
@@ -6125,7 +6305,15 @@ function mergeEnvironmentLayers(layers, prefix) {
|
|
|
6125
6305
|
return core.config.mergeEnv(layers.map((layer) => Object.fromEntries(Object.entries(layer).filter(([name]) => !prefix || name === prefix || name.startsWith(`${prefix}_`)))));
|
|
6126
6306
|
}
|
|
6127
6307
|
function describeDeploymentVariableSources(directory, environment, values, names) {
|
|
6128
|
-
const files = [
|
|
6308
|
+
const files = [
|
|
6309
|
+
`.env.${environment}.local`,
|
|
6310
|
+
".env.local",
|
|
6311
|
+
`.env.${environment}`,
|
|
6312
|
+
".env"
|
|
6313
|
+
].map((name) => ({
|
|
6314
|
+
path: join(directory, name),
|
|
6315
|
+
values: environmentFile(join(directory, name))
|
|
6316
|
+
}));
|
|
6129
6317
|
return names.filter((name) => values[name]).map((name) => {
|
|
6130
6318
|
const source = process.env[name] !== void 0 ? "process.env" : files.find((file) => Object.hasOwn(file.values, name))?.path;
|
|
6131
6319
|
return `${name}: ${source ?? ".env.runtime"}`;
|
|
@@ -6140,32 +6328,65 @@ function loadDeploymentConfig(name, path, values) {
|
|
|
6140
6328
|
const project = JSON.parse(content);
|
|
6141
6329
|
input = project && typeof project === "object" && !Array.isArray(project) ? project[name] : void 0;
|
|
6142
6330
|
} catch {
|
|
6143
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
6331
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
6332
|
+
params: { path, line: "1" }
|
|
6333
|
+
}));
|
|
6334
|
+
}
|
|
6335
|
+
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
6336
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
6337
|
+
params: { path, line: "1" }
|
|
6338
|
+
}));
|
|
6144
6339
|
}
|
|
6145
|
-
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
6146
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
|
|
6147
6340
|
} else {
|
|
6148
6341
|
const lineCounter = new LineCounter();
|
|
6149
6342
|
const document = parseDocument(content, { lineCounter, prettyErrors: false });
|
|
6150
|
-
if (document.errors.length)
|
|
6151
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
6343
|
+
if (document.errors.length) {
|
|
6344
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
6345
|
+
params: {
|
|
6346
|
+
path,
|
|
6347
|
+
line: String(lineCounter.linePos(document.errors[0].pos[0]).line)
|
|
6348
|
+
}
|
|
6349
|
+
}));
|
|
6350
|
+
}
|
|
6152
6351
|
input = document.toJS();
|
|
6153
6352
|
}
|
|
6154
|
-
const resolved = core.config.resolve({
|
|
6353
|
+
const resolved = core.config.resolve({
|
|
6354
|
+
name,
|
|
6355
|
+
value: input,
|
|
6356
|
+
environment: values,
|
|
6357
|
+
shape: shapes[name],
|
|
6358
|
+
missing: "empty"
|
|
6359
|
+
});
|
|
6155
6360
|
if (!resolved.success) {
|
|
6156
6361
|
const cause = resolved.error.cause;
|
|
6157
6362
|
const fields = cause && typeof cause === "object" && "fields" in cause ? cause.fields : void 0;
|
|
6158
6363
|
const safeFields = Array.isArray(fields) ? fields.filter((field2) => typeof field2 === "string" && /^HAI_[A-Z0-9_]+$/.test(field2)).join(", ") : "";
|
|
6159
|
-
if (name === "reach" && /^HAI_REACH_PROVIDERS_\d+(?:_|$)/.test(safeFields) && /^\s*\[\s*\]\s*$/.test(values.HAI_REACH_PROVIDERS ?? ""))
|
|
6160
|
-
return err({
|
|
6161
|
-
|
|
6364
|
+
if (name === "reach" && /^HAI_REACH_PROVIDERS_\d+(?:_|$)/.test(safeFields) && /^\s*\[\s*\]\s*$/.test(values.HAI_REACH_PROVIDERS ?? "")) {
|
|
6365
|
+
return err({
|
|
6366
|
+
...resolved.error,
|
|
6367
|
+
message: deployM("deploy_reachEmptyProviderOverride", {
|
|
6368
|
+
params: { path, field: safeFields }
|
|
6369
|
+
})
|
|
6370
|
+
});
|
|
6371
|
+
}
|
|
6372
|
+
return err({
|
|
6373
|
+
...resolved.error,
|
|
6374
|
+
message: deployM("deploy_configResolutionFailed", {
|
|
6375
|
+
params: { path, fields: safeFields || name }
|
|
6376
|
+
})
|
|
6377
|
+
});
|
|
6162
6378
|
}
|
|
6163
6379
|
const config = resolved.data;
|
|
6164
|
-
if (config !== null && (typeof config !== "object" || Array.isArray(config)))
|
|
6165
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
6380
|
+
if (config !== null && (typeof config !== "object" || Array.isArray(config))) {
|
|
6381
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
6382
|
+
params: { path, line: "1" }
|
|
6383
|
+
}));
|
|
6384
|
+
}
|
|
6166
6385
|
return ok(config && typeof config === "object" && !Array.isArray(config) ? config : {});
|
|
6167
6386
|
} catch {
|
|
6168
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", {
|
|
6387
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", {
|
|
6388
|
+
params: { path }
|
|
6389
|
+
}));
|
|
6169
6390
|
}
|
|
6170
6391
|
}
|
|
6171
6392
|
function loadDeploymentEnvironment(options) {
|
|
@@ -6174,8 +6395,11 @@ function loadDeploymentEnvironment(options) {
|
|
|
6174
6395
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_invalidEnvironment"));
|
|
6175
6396
|
const envDir = resolve(options.envDir);
|
|
6176
6397
|
const target = join(envDir, `.env.${environment}`);
|
|
6177
|
-
if (options.requireFile !== false && !existsSync(target))
|
|
6178
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentMissing", {
|
|
6398
|
+
if (options.requireFile !== false && !existsSync(target)) {
|
|
6399
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentMissing", {
|
|
6400
|
+
params: { path: target }
|
|
6401
|
+
}));
|
|
6402
|
+
}
|
|
6179
6403
|
const loaded = core.config.loadEnvFiles({ mode: environment, envDir });
|
|
6180
6404
|
if (!loaded.success)
|
|
6181
6405
|
return loaded;
|
|
@@ -6189,25 +6413,40 @@ function loadDeploymentEnvironment(options) {
|
|
|
6189
6413
|
values.HAI_DEPLOY_ENVIRONMENT = environment;
|
|
6190
6414
|
if (values.HAI_CORE_ENV === void 0) {
|
|
6191
6415
|
const fallback = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
|
|
6192
|
-
const configured = core.config.resolve({
|
|
6416
|
+
const configured = core.config.resolve({
|
|
6417
|
+
name: "core",
|
|
6418
|
+
value: { env: fallback },
|
|
6419
|
+
environment: values,
|
|
6420
|
+
shape: shapes.core
|
|
6421
|
+
});
|
|
6193
6422
|
const settings = configured.success && configured.data && typeof configured.data === "object" ? configured.data : void 0;
|
|
6194
6423
|
values.HAI_CORE_ENV = typeof settings?.env === "string" ? settings.env : fallback;
|
|
6195
6424
|
}
|
|
6196
|
-
return ok({
|
|
6425
|
+
return ok({
|
|
6426
|
+
environment,
|
|
6427
|
+
envDir,
|
|
6428
|
+
values,
|
|
6429
|
+
targetValues: selected
|
|
6430
|
+
});
|
|
6197
6431
|
} catch {
|
|
6198
6432
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
|
|
6199
6433
|
}
|
|
6200
6434
|
}
|
|
6201
6435
|
var reachGatewayEnvironment = /^HAI_REACH_PROVIDERS_\d+_GATEWAY$/i;
|
|
6202
6436
|
function isSecret(name, value) {
|
|
6437
|
+
if (name === "headers" || /^HAI_REACH_PROVIDERS_\d+_HEADERS(?:_|$)/i.test(name))
|
|
6438
|
+
return true;
|
|
6203
6439
|
if (/^HAI_REACH_PROVIDERS_\d+_GATEWAY_(?:HEADERS|BODY)(?:_|$)/i.test(name))
|
|
6204
6440
|
return true;
|
|
6205
6441
|
if (/PASSWORD|SECRET|TOKEN|APIKEY|PRIVATEKEY|ACCESSKEY|AUTHORIZATION/i.test(name.replace(/[^a-z0-9]/gi, "")) || /(?:^|_)PASS$/i.test(name) || value.includes("PRIVATE KEY"))
|
|
6206
6442
|
return true;
|
|
6207
6443
|
try {
|
|
6208
6444
|
const url = new URL(value);
|
|
6209
|
-
if (url.username || url.password || [
|
|
6445
|
+
if (url.username || url.password || [
|
|
6446
|
+
...url.searchParams.keys()
|
|
6447
|
+
].some((key) => /password|secret|token|key|authorization/i.test(key))) {
|
|
6210
6448
|
return true;
|
|
6449
|
+
}
|
|
6211
6450
|
} catch {
|
|
6212
6451
|
}
|
|
6213
6452
|
try {
|
|
@@ -6249,7 +6488,13 @@ function generatedVariables(values) {
|
|
|
6249
6488
|
const structuredNames = Object.keys(group).filter((name) => structures.has(name));
|
|
6250
6489
|
if (!structuredNames.length)
|
|
6251
6490
|
continue;
|
|
6252
|
-
const resolved = core.config.resolve({
|
|
6491
|
+
const resolved = core.config.resolve({
|
|
6492
|
+
name: module,
|
|
6493
|
+
value: {},
|
|
6494
|
+
environment: group,
|
|
6495
|
+
shape: shapes[module],
|
|
6496
|
+
missing: "empty"
|
|
6497
|
+
});
|
|
6253
6498
|
if (resolved.success) {
|
|
6254
6499
|
const flattened = core.config.toEnv({ prefix: `HAI_${module.toUpperCase()}`, value: resolved.data, shape: shapes[module] });
|
|
6255
6500
|
const represented = structuredNames.every((name) => Object.entries(flattened).some(([key, value]) => key === name || key.startsWith(`${name}_`) || name.startsWith(`${key}_`) && value.startsWith("[")));
|
|
@@ -6287,12 +6532,12 @@ function generatedVariables(values) {
|
|
|
6287
6532
|
return generated;
|
|
6288
6533
|
}
|
|
6289
6534
|
function updateFile(path, values, privateFile2) {
|
|
6290
|
-
let content = existsSync(path) ?
|
|
6535
|
+
let content = existsSync(path) ? readLocalFile(path).toString("utf8") : "# Local private deployment configuration; do not commit.\n";
|
|
6291
6536
|
for (const [name, value] of Object.entries(values)) {
|
|
6292
6537
|
if (!/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
6293
6538
|
throw new Error("Invalid environment variable name");
|
|
6294
6539
|
const line = core.config.stringifyEnv({ [name]: value }).trimEnd();
|
|
6295
|
-
const pattern = new RegExp(`^(?:export\\
|
|
6540
|
+
const pattern = new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*`, "gm");
|
|
6296
6541
|
if (pattern.test(content))
|
|
6297
6542
|
content = content.replace(pattern, () => line);
|
|
6298
6543
|
else
|
|
@@ -6303,25 +6548,22 @@ ${line}
|
|
|
6303
6548
|
for (const [name, value] of Object.entries(core.config.parseEnv(content)).sort(([left], [right]) => left.length - right.length)) {
|
|
6304
6549
|
if (!name.startsWith("HAI_") || !/^\s*[[{]/.test(value))
|
|
6305
6550
|
continue;
|
|
6306
|
-
const parent = new RegExp(`^(?:export\\
|
|
6307
|
-
const child = new RegExp(`^(?:export\\
|
|
6551
|
+
const parent = new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "m").exec(content);
|
|
6552
|
+
const child = new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}_[A-Z0-9_]+[\\t ]*=`, "m").exec(content);
|
|
6308
6553
|
if (parent && child && parent.index > child.index) {
|
|
6309
6554
|
const withoutParent = content.slice(0, parent.index) + content.slice(parent.index + parent[0].length);
|
|
6310
6555
|
content = withoutParent.slice(0, child.index) + parent[0].replace(/\r?\n?$/, "\n") + withoutParent.slice(child.index);
|
|
6311
6556
|
}
|
|
6312
6557
|
}
|
|
6313
|
-
|
|
6314
|
-
if (privateFile2)
|
|
6315
|
-
chmodSync(path, 384);
|
|
6558
|
+
writeLocalFile(path, content, privateFile2);
|
|
6316
6559
|
}
|
|
6317
6560
|
function removeOverrides(path, names, privateFile2 = true) {
|
|
6318
6561
|
if (!existsSync(path) || names.length === 0)
|
|
6319
6562
|
return;
|
|
6320
|
-
let content =
|
|
6563
|
+
let content = readLocalFile(path).toString("utf8");
|
|
6321
6564
|
for (const name of names)
|
|
6322
|
-
content = content.replace(new RegExp(`^(?:export\\
|
|
6323
|
-
|
|
6324
|
-
chmodSync(path, privateFile2 ? 384 : 420);
|
|
6565
|
+
content = content.replace(new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*(?:\\r?\\n|$)`, "gm"), "");
|
|
6566
|
+
writeLocalFile(path, content, privateFile2);
|
|
6325
6567
|
}
|
|
6326
6568
|
function saveDeploymentEnvironment(environment, values, options = {}) {
|
|
6327
6569
|
try {
|
|
@@ -6331,8 +6573,12 @@ function saveDeploymentEnvironment(environment, values, options = {}) {
|
|
|
6331
6573
|
return value;
|
|
6332
6574
|
if (Array.isArray(value))
|
|
6333
6575
|
return value.map((item, index) => splitArrayValue2(`${name}_${index}`, item) ?? null);
|
|
6334
|
-
if (value && typeof value === "object" && !reachGatewayEnvironment.test(name) && !isSecret(name, "") && Object.keys(value).every((key) => /^[a-z][a-zA-Z0-9]*$/.test(key)))
|
|
6335
|
-
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
6576
|
+
if (value && typeof value === "object" && !reachGatewayEnvironment.test(name) && !isSecret(name, "") && Object.keys(value).every((key) => /^[a-z][a-zA-Z0-9]*$/.test(key))) {
|
|
6577
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [
|
|
6578
|
+
key,
|
|
6579
|
+
splitArrayValue2(`${name}_${key.toUpperCase()}`, item)
|
|
6580
|
+
]).filter(([, item]) => item !== void 0));
|
|
6581
|
+
}
|
|
6336
6582
|
privateValues[name] = literal;
|
|
6337
6583
|
return void 0;
|
|
6338
6584
|
};
|
|
@@ -6386,10 +6632,10 @@ function saveDeploymentEnvironment(environment, values, options = {}) {
|
|
|
6386
6632
|
removeOverrides(privatePath, Object.keys(shared).filter((name) => !(name in privateValues)));
|
|
6387
6633
|
if (Object.keys(privateValues).length) {
|
|
6388
6634
|
const ignorePath = join(environment.envDir, ".gitignore");
|
|
6389
|
-
const ignore = existsSync(ignorePath) ?
|
|
6635
|
+
const ignore = existsSync(ignorePath) ? readLocalFile(ignorePath).toString("utf8") : "";
|
|
6390
6636
|
const missing = [".env.local", ".env.*.local", ".hai/deploy/"].filter((line) => !ignore.split(/\r?\n/).includes(line));
|
|
6391
6637
|
if (missing.length)
|
|
6392
|
-
|
|
6638
|
+
writeLocalFile(ignorePath, `${ignore.trimEnd()}
|
|
6393
6639
|
${missing.join("\n")}
|
|
6394
6640
|
`);
|
|
6395
6641
|
}
|
|
@@ -6399,7 +6645,14 @@ ${missing.join("\n")}
|
|
|
6399
6645
|
}
|
|
6400
6646
|
}
|
|
6401
6647
|
function normalizeProjectName(name) {
|
|
6402
|
-
const
|
|
6648
|
+
const replaced = name.trim().replace(/^@[^/]+\//, "").toLowerCase().replace(/[^a-z0-9-]+/g, "-");
|
|
6649
|
+
let start = 0;
|
|
6650
|
+
let end = replaced.length;
|
|
6651
|
+
while (start < end && replaced[start] === "-")
|
|
6652
|
+
start++;
|
|
6653
|
+
while (end > start && replaced[end - 1] === "-")
|
|
6654
|
+
end--;
|
|
6655
|
+
const normalized = replaced.slice(start, end);
|
|
6403
6656
|
if (!normalized)
|
|
6404
6657
|
throw new Error(deployM("deploy_projectNameRequired"));
|
|
6405
6658
|
return normalized;
|
|
@@ -6452,7 +6705,9 @@ function normalizeCredentialEntries(entries) {
|
|
|
6452
6705
|
}
|
|
6453
6706
|
function assertAllowedCredentialKey(key) {
|
|
6454
6707
|
if (!isAllowedCredentialKey(key)) {
|
|
6455
|
-
throw new Error(deployM("deploy_credentialInvalidKey", {
|
|
6708
|
+
throw new Error(deployM("deploy_credentialInvalidKey", {
|
|
6709
|
+
params: { key }
|
|
6710
|
+
}));
|
|
6456
6711
|
}
|
|
6457
6712
|
}
|
|
6458
6713
|
function assertAllowedCredentialEntries(entries) {
|
|
@@ -6504,7 +6759,9 @@ function loadCredentials() {
|
|
|
6504
6759
|
return err(
|
|
6505
6760
|
HaiDeployError.CREDENTIAL_ERROR,
|
|
6506
6761
|
deployM("deploy_credentialError", {
|
|
6507
|
-
params: {
|
|
6762
|
+
params: {
|
|
6763
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6764
|
+
}
|
|
6508
6765
|
}),
|
|
6509
6766
|
error
|
|
6510
6767
|
);
|
|
@@ -6524,7 +6781,9 @@ function saveCredential(key, value) {
|
|
|
6524
6781
|
return err(
|
|
6525
6782
|
HaiDeployError.CREDENTIAL_ERROR,
|
|
6526
6783
|
deployM("deploy_credentialError", {
|
|
6527
|
-
params: {
|
|
6784
|
+
params: {
|
|
6785
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6786
|
+
}
|
|
6528
6787
|
}),
|
|
6529
6788
|
error
|
|
6530
6789
|
);
|
|
@@ -6546,7 +6805,9 @@ function saveCredentials(entries) {
|
|
|
6546
6805
|
return err(
|
|
6547
6806
|
HaiDeployError.CREDENTIAL_ERROR,
|
|
6548
6807
|
deployM("deploy_credentialError", {
|
|
6549
|
-
params: {
|
|
6808
|
+
params: {
|
|
6809
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6810
|
+
}
|
|
6550
6811
|
}),
|
|
6551
6812
|
error
|
|
6552
6813
|
);
|
|
@@ -6555,12 +6816,59 @@ function saveCredentials(entries) {
|
|
|
6555
6816
|
|
|
6556
6817
|
// src/deploy-middleware-config.ts
|
|
6557
6818
|
var connectionFields = {
|
|
6558
|
-
db: [
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6819
|
+
db: [
|
|
6820
|
+
"url",
|
|
6821
|
+
"host",
|
|
6822
|
+
"port",
|
|
6823
|
+
"database",
|
|
6824
|
+
"user",
|
|
6825
|
+
"password",
|
|
6826
|
+
"ssl"
|
|
6827
|
+
],
|
|
6828
|
+
cache: [
|
|
6829
|
+
"url",
|
|
6830
|
+
"host",
|
|
6831
|
+
"port",
|
|
6832
|
+
"password",
|
|
6833
|
+
"cluster",
|
|
6834
|
+
"sentinel",
|
|
6835
|
+
"tls"
|
|
6836
|
+
],
|
|
6837
|
+
storage: [
|
|
6838
|
+
"endpoint",
|
|
6839
|
+
"bucket",
|
|
6840
|
+
"region",
|
|
6841
|
+
"accessKeyId",
|
|
6842
|
+
"secretAccessKey",
|
|
6843
|
+
"forcePathStyle",
|
|
6844
|
+
"createBucket"
|
|
6845
|
+
],
|
|
6846
|
+
vecdb: [
|
|
6847
|
+
"url",
|
|
6848
|
+
"host",
|
|
6849
|
+
"port",
|
|
6850
|
+
"database",
|
|
6851
|
+
"user",
|
|
6852
|
+
"password",
|
|
6853
|
+
"apiKey",
|
|
6854
|
+
"path",
|
|
6855
|
+
"serverCommand",
|
|
6856
|
+
"startupTimeout"
|
|
6857
|
+
],
|
|
6858
|
+
ldap: [
|
|
6859
|
+
"url",
|
|
6860
|
+
"bindDn",
|
|
6861
|
+
"bindPassword",
|
|
6862
|
+
"searchBase",
|
|
6863
|
+
"useTls"
|
|
6864
|
+
],
|
|
6865
|
+
email: [
|
|
6866
|
+
"host",
|
|
6867
|
+
"port",
|
|
6868
|
+
"secure",
|
|
6869
|
+
"user",
|
|
6870
|
+
"pass"
|
|
6871
|
+
]
|
|
6564
6872
|
};
|
|
6565
6873
|
function mergeManagedMiddlewareConfig(requirement, supplied) {
|
|
6566
6874
|
if (!requirement || requirement.service !== "ldap" && requirement.backend !== supplied.type)
|
|
@@ -6568,7 +6876,10 @@ function mergeManagedMiddlewareConfig(requirement, supplied) {
|
|
|
6568
6876
|
const configured = { ...requirement.config };
|
|
6569
6877
|
for (const field2 of connectionFields[requirement.service] ?? [])
|
|
6570
6878
|
delete configured[field2];
|
|
6571
|
-
const merged = {
|
|
6879
|
+
const merged = {
|
|
6880
|
+
...configured,
|
|
6881
|
+
...supplied
|
|
6882
|
+
};
|
|
6572
6883
|
if (requirement.service === "cache" && typeof configured.db === "number" && typeof supplied.url === "string") {
|
|
6573
6884
|
const url = new URL(supplied.url);
|
|
6574
6885
|
if (configured.db !== 0 || url.pathname !== "" && url.pathname !== "/") {
|
|
@@ -6583,21 +6894,39 @@ var RecoverySchema = z.object({
|
|
|
6583
6894
|
stage: z.string(),
|
|
6584
6895
|
platformProjectId: z.string().optional(),
|
|
6585
6896
|
resources: z.array(z.object({
|
|
6586
|
-
serviceType: z.enum([
|
|
6897
|
+
serviceType: z.enum([
|
|
6898
|
+
"db",
|
|
6899
|
+
"cache",
|
|
6900
|
+
"storage",
|
|
6901
|
+
"email",
|
|
6902
|
+
"sms"
|
|
6903
|
+
]),
|
|
6587
6904
|
provisionerName: z.string(),
|
|
6588
6905
|
resourceInfo: z.string(),
|
|
6589
6906
|
resourceStatus: z.enum(["created", "reused", "unknown"])
|
|
6590
6907
|
}))
|
|
6591
6908
|
});
|
|
6592
6909
|
var CommandStateSchema = z.object({
|
|
6593
|
-
reason: z.enum([
|
|
6910
|
+
reason: z.enum([
|
|
6911
|
+
"timeout",
|
|
6912
|
+
"output",
|
|
6913
|
+
"input",
|
|
6914
|
+
"spawn",
|
|
6915
|
+
"exit",
|
|
6916
|
+
"configuration"
|
|
6917
|
+
]),
|
|
6594
6918
|
localTermination: z.enum(["confirmed", "unknown", "not-started"]),
|
|
6595
6919
|
remoteExecution: z.enum(["unknown", "finished"]).optional(),
|
|
6596
6920
|
deploymentId: z.string().optional(),
|
|
6597
6921
|
resultFile: z.string().optional(),
|
|
6598
6922
|
remoteExitCode: z.number().int().optional(),
|
|
6599
6923
|
remoteStage: z.string().optional(),
|
|
6600
|
-
remoteRecovery: z.enum([
|
|
6924
|
+
remoteRecovery: z.enum([
|
|
6925
|
+
"not-needed",
|
|
6926
|
+
"passed",
|
|
6927
|
+
"failed",
|
|
6928
|
+
"stopped"
|
|
6929
|
+
]).optional()
|
|
6601
6930
|
});
|
|
6602
6931
|
function getDeployCommandState(error) {
|
|
6603
6932
|
const parsed = CommandStateSchema.safeParse(error.ext?.commandExecution);
|
|
@@ -6613,7 +6942,9 @@ function recoveryFailure(error, recovery) {
|
|
|
6613
6942
|
httpStatus: error.httpStatus,
|
|
6614
6943
|
system: error.system,
|
|
6615
6944
|
module: error.module,
|
|
6616
|
-
message: deployM("deploy_recoveryFailure", {
|
|
6945
|
+
message: deployM("deploy_recoveryFailure", {
|
|
6946
|
+
params: { stage: recovery.stage }
|
|
6947
|
+
}),
|
|
6617
6948
|
suggestion: deployM("deploy_recoverySuggestion"),
|
|
6618
6949
|
ext: { recovery }
|
|
6619
6950
|
});
|
|
@@ -6648,12 +6979,15 @@ function runtimeDependencies(appDir) {
|
|
|
6648
6979
|
visited.add(id);
|
|
6649
6980
|
const metadata = importer === void 0 ? record(packages[id.split("(")[0]]) : {};
|
|
6650
6981
|
const peers = record(metadata.peerDependencies);
|
|
6651
|
-
for (const [name, reference] of Object.entries({
|
|
6982
|
+
for (const [name, reference] of Object.entries({
|
|
6983
|
+
...record(node.dependencies),
|
|
6984
|
+
...record(node.optionalDependencies)
|
|
6985
|
+
})) {
|
|
6652
6986
|
if (name in peers)
|
|
6653
6987
|
continue;
|
|
6654
6988
|
const version = typeof reference === "string" ? reference : record(reference).version;
|
|
6655
6989
|
if (typeof version !== "string")
|
|
6656
|
-
throw new
|
|
6990
|
+
throw new TypeError("Invalid pnpm dependency reference");
|
|
6657
6991
|
if (version.startsWith("link:")) {
|
|
6658
6992
|
if (importer === void 0)
|
|
6659
6993
|
throw new Error("Invalid workspace dependency");
|
|
@@ -6674,7 +7008,14 @@ function runtimeDependencies(appDir) {
|
|
|
6674
7008
|
}
|
|
6675
7009
|
};
|
|
6676
7010
|
visit(`workspace:${importerKey}`, record(importers[importerKey]), importerKey);
|
|
6677
|
-
return {
|
|
7011
|
+
return {
|
|
7012
|
+
runtime: names,
|
|
7013
|
+
build: /* @__PURE__ */ new Set([
|
|
7014
|
+
...names,
|
|
7015
|
+
...Object.keys(record(record(importers[importerKey]).devDependencies))
|
|
7016
|
+
]),
|
|
7017
|
+
root
|
|
7018
|
+
};
|
|
6678
7019
|
}
|
|
6679
7020
|
async function scanApp(inputDir, options) {
|
|
6680
7021
|
try {
|
|
@@ -6696,7 +7037,14 @@ async function scanApp(inputDir, options) {
|
|
|
6696
7037
|
const deps = graph.runtime;
|
|
6697
7038
|
const configs = {};
|
|
6698
7039
|
const enabledConfigs = /* @__PURE__ */ new Set();
|
|
6699
|
-
for (const [name, owner] of Object.entries({
|
|
7040
|
+
for (const [name, owner] of Object.entries({
|
|
7041
|
+
db: "reldb",
|
|
7042
|
+
cache: "cache",
|
|
7043
|
+
storage: "storage",
|
|
7044
|
+
vecdb: "vecdb",
|
|
7045
|
+
iam: "iam",
|
|
7046
|
+
reach: "reach"
|
|
7047
|
+
})) {
|
|
6700
7048
|
configs[name] = {};
|
|
6701
7049
|
if (!deps.has(`@h-ai/${owner}`))
|
|
6702
7050
|
continue;
|
|
@@ -6710,7 +7058,15 @@ async function scanApp(inputDir, options) {
|
|
|
6710
7058
|
}
|
|
6711
7059
|
const middlewares = [];
|
|
6712
7060
|
const add = (service, backend, configName = service, configPath = [], config = configs[configName] ?? {}, source = `_${configName}.yml`) => {
|
|
6713
|
-
middlewares.push({
|
|
7061
|
+
middlewares.push({
|
|
7062
|
+
service,
|
|
7063
|
+
backend,
|
|
7064
|
+
source,
|
|
7065
|
+
configName,
|
|
7066
|
+
configPath,
|
|
7067
|
+
envPrefix: ["HAI", configName, ...configPath].map((part) => part.toUpperCase()).join("_"),
|
|
7068
|
+
config
|
|
7069
|
+
});
|
|
6714
7070
|
};
|
|
6715
7071
|
const iam = enabledConfigs.has("iam");
|
|
6716
7072
|
if (enabledConfigs.has("db"))
|
|
@@ -6731,8 +7087,12 @@ async function scanApp(inputDir, options) {
|
|
|
6731
7087
|
const provider = record(item);
|
|
6732
7088
|
const type = String(provider.type ?? "");
|
|
6733
7089
|
const service = type === "aliyun-sms" || ["console", "api"].includes(type) && provider.name === "sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
|
|
6734
|
-
if (service)
|
|
6735
|
-
add(service, type, "reach", [
|
|
7090
|
+
if (service) {
|
|
7091
|
+
add(service, type, "reach", [
|
|
7092
|
+
"providers",
|
|
7093
|
+
String(index)
|
|
7094
|
+
], provider, "@h-ai/reach");
|
|
7095
|
+
}
|
|
6736
7096
|
}
|
|
6737
7097
|
}
|
|
6738
7098
|
if (enabledConfigs.has("reach") && providers === void 0)
|
|
@@ -6747,19 +7107,33 @@ async function scanApp(inputDir, options) {
|
|
|
6747
7107
|
isSvelteKit,
|
|
6748
7108
|
adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
|
|
6749
7109
|
nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
|
|
6750
|
-
requiredServices: [
|
|
7110
|
+
requiredServices: [
|
|
7111
|
+
...new Set(middlewares.map((item) => item.service))
|
|
7112
|
+
],
|
|
6751
7113
|
serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
|
|
6752
7114
|
};
|
|
6753
|
-
logger2.info("Application scanned", {
|
|
7115
|
+
logger2.info("Application scanned", {
|
|
7116
|
+
appName: scan.appName,
|
|
7117
|
+
haiModules: scan.haiModules,
|
|
7118
|
+
requiredServices: scan.requiredServices,
|
|
7119
|
+
serviceBackends: scan.serviceBackends
|
|
7120
|
+
});
|
|
6754
7121
|
return ok(scan);
|
|
6755
7122
|
} catch {
|
|
6756
|
-
return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", {
|
|
7123
|
+
return err(HaiDeployError.SCAN_FAILED, deployM("deploy_scanFailed", {
|
|
7124
|
+
params: { error: "Invalid or ambiguous application/configuration/lockfile; specify the application directory and run pnpm install" }
|
|
7125
|
+
}));
|
|
6757
7126
|
}
|
|
6758
7127
|
}
|
|
6759
7128
|
|
|
6760
7129
|
// src/container/deploy-compose-identity.ts
|
|
6761
7130
|
function createComposeContainerLookupScript() {
|
|
6762
|
-
const labelFormat = [
|
|
7131
|
+
const labelFormat = [
|
|
7132
|
+
"com.docker.compose.project",
|
|
7133
|
+
"com.docker.compose.service",
|
|
7134
|
+
"io.podman.compose.project",
|
|
7135
|
+
"io.podman.compose.service"
|
|
7136
|
+
].map((key) => `{{index .Config.Labels \`${key}\`}}`).join("|");
|
|
6763
7137
|
return String.raw`compose_container_ids() (
|
|
6764
7138
|
if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
|
|
6765
7139
|
lookup_runtime=$1
|
|
@@ -7147,11 +7521,28 @@ deployment_forget_metadata() {
|
|
|
7147
7521
|
|
|
7148
7522
|
// src/container/deploy-managed-secrets.ts
|
|
7149
7523
|
var groups = [
|
|
7150
|
-
{
|
|
7151
|
-
|
|
7152
|
-
|
|
7524
|
+
{
|
|
7525
|
+
name: "DB_PASSWORD",
|
|
7526
|
+
keys: ["POSTGRES_PASSWORD", "MYSQL_PASSWORD", "MYSQL_ROOT_PASSWORD"],
|
|
7527
|
+
services: ["db", "vecdb"]
|
|
7528
|
+
},
|
|
7529
|
+
{
|
|
7530
|
+
name: "STORAGE_PASSWORD",
|
|
7531
|
+
keys: ["RUSTFS_SECRET_KEY", "MINIO_ROOT_PASSWORD"],
|
|
7532
|
+
services: ["storage"]
|
|
7533
|
+
},
|
|
7534
|
+
{
|
|
7535
|
+
name: "LDAP_PASSWORD",
|
|
7536
|
+
keys: ["LDAP_ADMIN_PASSWORD"],
|
|
7537
|
+
services: ["ldap"]
|
|
7538
|
+
}
|
|
7153
7539
|
];
|
|
7154
|
-
var appPrefixes = {
|
|
7540
|
+
var appPrefixes = {
|
|
7541
|
+
db: "HAI_DB",
|
|
7542
|
+
vecdb: "HAI_VECDB",
|
|
7543
|
+
storage: "HAI_STORAGE",
|
|
7544
|
+
ldap: "HAI_IAM_LDAP"
|
|
7545
|
+
};
|
|
7155
7546
|
function resolveComposeNamedVolumes(projectName, mounts, declarations = {}) {
|
|
7156
7547
|
if (!Array.isArray(mounts))
|
|
7157
7548
|
return [];
|
|
@@ -7177,7 +7568,9 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
|
|
|
7177
7568
|
const bindings = /* @__PURE__ */ new Set();
|
|
7178
7569
|
const volumes = /* @__PURE__ */ new Set();
|
|
7179
7570
|
for (const group of groups) {
|
|
7180
|
-
const values = [
|
|
7571
|
+
const values = [
|
|
7572
|
+
...new Set(group.keys.flatMap((key) => native[key] === void 0 ? [] : [native[key]]))
|
|
7573
|
+
];
|
|
7181
7574
|
if (!values.length)
|
|
7182
7575
|
continue;
|
|
7183
7576
|
if (values.length !== 1 || !/^[\w-]{32}$/.test(values[0]))
|
|
@@ -7379,7 +7772,7 @@ function createBackendStateAssets(projectName, compose, selected) {
|
|
|
7379
7772
|
const service = compose.services[serviceName];
|
|
7380
7773
|
const image = service?.image;
|
|
7381
7774
|
if (typeof image !== "string")
|
|
7382
|
-
throw new
|
|
7775
|
+
throw new TypeError(deployM("deploy_artifactInvalid"));
|
|
7383
7776
|
const imageFamily = image.slice(image.lastIndexOf("/") + 1).split(":")[0];
|
|
7384
7777
|
const family = imageFamily === "postgres" ? "postgresql" : imageFamily;
|
|
7385
7778
|
const namedVolumes = resolveComposeNamedVolumes(projectName, service.volumes, compose.volumes);
|
|
@@ -7732,7 +8125,13 @@ public static class HaiCommandJob {
|
|
|
7732
8125
|
}
|
|
7733
8126
|
`;
|
|
7734
8127
|
function powershell(script) {
|
|
7735
|
-
return [
|
|
8128
|
+
return [
|
|
8129
|
+
"-NoLogo",
|
|
8130
|
+
"-NoProfile",
|
|
8131
|
+
"-NonInteractive",
|
|
8132
|
+
"-EncodedCommand",
|
|
8133
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
8134
|
+
];
|
|
7736
8135
|
}
|
|
7737
8136
|
function windowsCommand(command, args) {
|
|
7738
8137
|
const name = `Local\\hai-deploy-${randomUUID()}`;
|
|
@@ -7746,7 +8145,13 @@ ${nativeJob}
|
|
|
7746
8145
|
[Environment]::SetEnvironmentVariable('${key}', $null)
|
|
7747
8146
|
exit ([HaiCommandJob]::Run([string]$inputConfig.name, [string]$inputConfig.command, [string[]]$inputConfig.args))
|
|
7748
8147
|
} catch { [Console]::Error.WriteLine('Windows command job failed'); exit 125 }`;
|
|
7749
|
-
return {
|
|
8148
|
+
return {
|
|
8149
|
+
name,
|
|
8150
|
+
args: powershell(script),
|
|
8151
|
+
env: {
|
|
8152
|
+
[key]: JSON.stringify({ name, command, args })
|
|
8153
|
+
}
|
|
8154
|
+
};
|
|
7750
8155
|
}
|
|
7751
8156
|
function windowsJobProbe(name) {
|
|
7752
8157
|
return powershell(`$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
|
|
@@ -7816,7 +8221,10 @@ function spawnCommand(command, args, options) {
|
|
|
7816
8221
|
const job = process.platform === "win32" ? windowsCommand(command, args) : void 0;
|
|
7817
8222
|
const child = spawn(job ? "powershell.exe" : command, job?.args ?? args, {
|
|
7818
8223
|
...options,
|
|
7819
|
-
env: job ? {
|
|
8224
|
+
env: job ? {
|
|
8225
|
+
...options.env,
|
|
8226
|
+
...job.env
|
|
8227
|
+
} : options.env,
|
|
7820
8228
|
windowsHide: true,
|
|
7821
8229
|
shell: false,
|
|
7822
8230
|
detached: !job
|
|
@@ -7843,7 +8251,12 @@ function spawnCommand(command, args, options) {
|
|
|
7843
8251
|
if (child.pid) {
|
|
7844
8252
|
if (job) {
|
|
7845
8253
|
if (!exited) {
|
|
7846
|
-
windowsKillConfirmed = await helper("taskkill", [
|
|
8254
|
+
windowsKillConfirmed = await helper("taskkill", [
|
|
8255
|
+
"/pid",
|
|
8256
|
+
String(child.pid),
|
|
8257
|
+
"/t",
|
|
8258
|
+
"/f"
|
|
8259
|
+
], Math.max(1, deadline - performance.now()));
|
|
7847
8260
|
child.kill("SIGKILL");
|
|
7848
8261
|
}
|
|
7849
8262
|
} else {
|
|
@@ -7885,7 +8298,9 @@ function spawnCommand(command, args, options) {
|
|
|
7885
8298
|
|
|
7886
8299
|
// src/container/deploy-secret-masker.ts
|
|
7887
8300
|
function createSecretMasker(secrets = []) {
|
|
7888
|
-
const values = [
|
|
8301
|
+
const values = [
|
|
8302
|
+
...new Set(secrets.filter(Boolean))
|
|
8303
|
+
].sort((a, b) => b.length - a.length);
|
|
7889
8304
|
if (values.reduce((total, value) => total + value.length, 0) > 1048576)
|
|
7890
8305
|
throw new RangeError("Secret redaction input is too large");
|
|
7891
8306
|
const pattern = values.length ? new RegExp(values.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : void 0;
|
|
@@ -7957,11 +8372,19 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
7957
8372
|
throw new RangeError("Invalid command termination timeout");
|
|
7958
8373
|
if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
|
|
7959
8374
|
throw new RangeError("Invalid command timeout");
|
|
7960
|
-
const secrets = [
|
|
8375
|
+
const secrets = [
|
|
8376
|
+
...options.secrets ?? []
|
|
8377
|
+
];
|
|
7961
8378
|
const safeCommand = maskSecrets(command, secrets);
|
|
7962
8379
|
const outputMasker = createSecretMasker(secrets);
|
|
7963
8380
|
const errorMasker = createSecretMasker(secrets);
|
|
7964
|
-
const control = spawnCommand(command, args, {
|
|
8381
|
+
const control = spawnCommand(command, args, {
|
|
8382
|
+
cwd: options.cwd,
|
|
8383
|
+
env: options.env ? {
|
|
8384
|
+
...process.env,
|
|
8385
|
+
...options.env
|
|
8386
|
+
} : process.env
|
|
8387
|
+
});
|
|
7965
8388
|
const child = control.child;
|
|
7966
8389
|
child.stdout.setEncoding("utf8");
|
|
7967
8390
|
child.stderr.setEncoding("utf8");
|
|
@@ -8000,7 +8423,12 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8000
8423
|
try {
|
|
8001
8424
|
await options.onOutput?.(text);
|
|
8002
8425
|
} catch {
|
|
8003
|
-
await interrupt("output", deployM("deploy_commandFailed", {
|
|
8426
|
+
await interrupt("output", deployM("deploy_commandFailed", {
|
|
8427
|
+
params: {
|
|
8428
|
+
command: safeCommand,
|
|
8429
|
+
error: deployM("deploy_commandOutputFailed")
|
|
8430
|
+
}
|
|
8431
|
+
}));
|
|
8004
8432
|
}
|
|
8005
8433
|
};
|
|
8006
8434
|
const captureError = (text) => {
|
|
@@ -8010,7 +8438,9 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8010
8438
|
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
8011
8439
|
timer = setTimeout(() => {
|
|
8012
8440
|
logger3.error("Command timed out", { command: safeCommand, timeoutMs: options.timeoutMs });
|
|
8013
|
-
void interrupt("timeout", deployM("deploy_commandTimeout", {
|
|
8441
|
+
void interrupt("timeout", deployM("deploy_commandTimeout", {
|
|
8442
|
+
params: { command: safeCommand }
|
|
8443
|
+
}));
|
|
8014
8444
|
}, options.timeoutMs);
|
|
8015
8445
|
}
|
|
8016
8446
|
const outputTask = (async () => {
|
|
@@ -8025,7 +8455,12 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8025
8455
|
if (!settled && !interrupted)
|
|
8026
8456
|
await publish(outputMasker.end());
|
|
8027
8457
|
} catch {
|
|
8028
|
-
await interrupt("output", deployM("deploy_commandFailed", {
|
|
8458
|
+
await interrupt("output", deployM("deploy_commandFailed", {
|
|
8459
|
+
params: {
|
|
8460
|
+
command: safeCommand,
|
|
8461
|
+
error: deployM("deploy_commandOutputFailed")
|
|
8462
|
+
}
|
|
8463
|
+
}));
|
|
8029
8464
|
}
|
|
8030
8465
|
})();
|
|
8031
8466
|
const errorTask = (async () => {
|
|
@@ -8040,16 +8475,28 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8040
8475
|
if (!settled && !interrupted)
|
|
8041
8476
|
captureError(errorMasker.end());
|
|
8042
8477
|
} catch {
|
|
8043
|
-
await interrupt("output", deployM("deploy_commandFailed", {
|
|
8478
|
+
await interrupt("output", deployM("deploy_commandFailed", {
|
|
8479
|
+
params: {
|
|
8480
|
+
command: safeCommand,
|
|
8481
|
+
error: deployM("deploy_commandOutputFailed")
|
|
8482
|
+
}
|
|
8483
|
+
}));
|
|
8044
8484
|
}
|
|
8045
8485
|
})();
|
|
8046
8486
|
child.stdin.on("error", () => {
|
|
8047
|
-
void interrupt("input", deployM("deploy_commandFailed", {
|
|
8487
|
+
void interrupt("input", deployM("deploy_commandFailed", {
|
|
8488
|
+
params: {
|
|
8489
|
+
command: safeCommand,
|
|
8490
|
+
error: deployM("deploy_commandInputFailed")
|
|
8491
|
+
}
|
|
8492
|
+
}));
|
|
8048
8493
|
});
|
|
8049
8494
|
child.on("error", (error) => {
|
|
8050
8495
|
const safeError = maskSecrets(error.message, secrets);
|
|
8051
8496
|
logger3.error("Command failed to start", { command: safeCommand, error: safeError });
|
|
8052
|
-
void interrupt("spawn", deployM("deploy_commandFailed", {
|
|
8497
|
+
void interrupt("spawn", deployM("deploy_commandFailed", {
|
|
8498
|
+
params: { command: safeCommand, error: safeError }
|
|
8499
|
+
}));
|
|
8053
8500
|
});
|
|
8054
8501
|
child.on("close", async (code) => {
|
|
8055
8502
|
await Promise.all([outputTask, errorTask]);
|
|
@@ -8058,24 +8505,52 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8058
8505
|
if (timer)
|
|
8059
8506
|
clearTimeout(timer);
|
|
8060
8507
|
if (!await control.treeExited()) {
|
|
8061
|
-
await interrupt("exit", deployM("deploy_commandFailed", {
|
|
8508
|
+
await interrupt("exit", deployM("deploy_commandFailed", {
|
|
8509
|
+
params: {
|
|
8510
|
+
command: safeCommand,
|
|
8511
|
+
error: deployM("deploy_commandTreeActive")
|
|
8512
|
+
}
|
|
8513
|
+
}));
|
|
8062
8514
|
return;
|
|
8063
8515
|
}
|
|
8064
8516
|
const exitCode = code ?? -1;
|
|
8065
8517
|
if (exitCode === 0) {
|
|
8066
|
-
finish(ok({
|
|
8518
|
+
finish(ok({
|
|
8519
|
+
code: exitCode,
|
|
8520
|
+
stdout,
|
|
8521
|
+
stderr,
|
|
8522
|
+
stdoutTruncated,
|
|
8523
|
+
stderrTruncated
|
|
8524
|
+
}));
|
|
8067
8525
|
return;
|
|
8068
8526
|
}
|
|
8069
|
-
logger3.error("Command exited with non-zero code", {
|
|
8070
|
-
|
|
8071
|
-
|
|
8527
|
+
logger3.error("Command exited with non-zero code", {
|
|
8528
|
+
command: safeCommand,
|
|
8529
|
+
code: exitCode,
|
|
8530
|
+
stderr: maskSecrets(stderr, secrets).slice(-500),
|
|
8531
|
+
stdout: maskSecrets(stdout, secrets).slice(-3e3),
|
|
8532
|
+
stdoutTruncated,
|
|
8533
|
+
stderrTruncated
|
|
8534
|
+
});
|
|
8535
|
+
finish(failure(deployM("deploy_commandFailed", {
|
|
8536
|
+
params: {
|
|
8537
|
+
command: safeCommand,
|
|
8538
|
+
error: maskSecrets(`${stdout.slice(-2500)}
|
|
8539
|
+
${stderr || `exit ${exitCode}`}`, secrets).slice(-3e3)
|
|
8540
|
+
}
|
|
8541
|
+
}), { reason: "exit", localTermination: "confirmed" }));
|
|
8072
8542
|
});
|
|
8073
8543
|
if (options.input !== void 0)
|
|
8074
8544
|
child.stdin.write(options.input);
|
|
8075
8545
|
child.stdin.end();
|
|
8076
8546
|
});
|
|
8077
8547
|
} catch {
|
|
8078
|
-
return failure(deployM("deploy_commandFailed", {
|
|
8548
|
+
return failure(deployM("deploy_commandFailed", {
|
|
8549
|
+
params: {
|
|
8550
|
+
command: "<command>",
|
|
8551
|
+
error: deployM("deploy_commandExecutionFailed")
|
|
8552
|
+
}
|
|
8553
|
+
}), { reason: "configuration", localTermination: "not-started" });
|
|
8079
8554
|
}
|
|
8080
8555
|
}
|
|
8081
8556
|
|
|
@@ -8318,7 +8793,13 @@ printf '%s\n' "$HAI_DEPLOY_ASKPASS_PASSWORD"
|
|
|
8318
8793
|
`.replaceAll("\\${", "${");
|
|
8319
8794
|
var compileAskpassScript = "$ErrorActionPreference='Stop'; Add-Type -Language CSharp -TypeDefinition ([System.IO.File]::ReadAllText($env:HAI_DEPLOY_ASKPASS_SOURCE, [System.Text.Encoding]::UTF8)) -OutputType ConsoleApplication -OutputAssembly $env:HAI_DEPLOY_ASKPASS_OUTPUT";
|
|
8320
8795
|
function helperFailure(key, reason = "configuration") {
|
|
8321
|
-
return err({
|
|
8796
|
+
return err({
|
|
8797
|
+
...HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
8798
|
+
message: deployM(key),
|
|
8799
|
+
ext: {
|
|
8800
|
+
commandExecution: { reason, localTermination: "not-started" }
|
|
8801
|
+
}
|
|
8802
|
+
});
|
|
8322
8803
|
}
|
|
8323
8804
|
async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
|
|
8324
8805
|
let directory;
|
|
@@ -8349,7 +8830,12 @@ async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
|
|
|
8349
8830
|
const source = join(directory, "askpass.cs");
|
|
8350
8831
|
writeFileSync(source, windowsAskpassSource, { mode: 384 });
|
|
8351
8832
|
const compiler = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
8352
|
-
const compiled = await runCommand(compiler, [
|
|
8833
|
+
const compiled = await runCommand(compiler, [
|
|
8834
|
+
"-NoProfile",
|
|
8835
|
+
"-NonInteractive",
|
|
8836
|
+
"-Command",
|
|
8837
|
+
compileAskpassScript
|
|
8838
|
+
], {
|
|
8353
8839
|
env: { HAI_DEPLOY_ASKPASS_SOURCE: source, HAI_DEPLOY_ASKPASS_OUTPUT: helper2, HAI_DEPLOY_ASKPASS_PASSWORD: "" },
|
|
8354
8840
|
timeoutMs: Math.max(1, Math.min(3e4, Math.ceil(deadline - performance.now()))),
|
|
8355
8841
|
terminationTimeoutMs,
|
|
@@ -8370,7 +8856,16 @@ async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
|
|
|
8370
8856
|
const removed = cleanup();
|
|
8371
8857
|
return removed.success ? helperFailure("deploy_sshAskpassFailed", "timeout") : removed;
|
|
8372
8858
|
}
|
|
8373
|
-
return ok({
|
|
8859
|
+
return ok({
|
|
8860
|
+
env: {
|
|
8861
|
+
SSH_ASKPASS: helper2,
|
|
8862
|
+
SSH_ASKPASS_REQUIRE: "force",
|
|
8863
|
+
SSH_ASKPASS_PROMPT: "",
|
|
8864
|
+
DISPLAY: "hai-deploy:0",
|
|
8865
|
+
HAI_DEPLOY_ASKPASS_PASSWORD: password
|
|
8866
|
+
},
|
|
8867
|
+
cleanup
|
|
8868
|
+
});
|
|
8374
8869
|
} catch {
|
|
8375
8870
|
const removed = cleanup();
|
|
8376
8871
|
return removed.success ? helperFailure("deploy_sshAskpassFailed") : removed;
|
|
@@ -9331,7 +9826,10 @@ function usesSshPassword(config) {
|
|
|
9331
9826
|
}
|
|
9332
9827
|
function buildConnectOptions(config) {
|
|
9333
9828
|
const password = usesSshPassword(config);
|
|
9334
|
-
const args = [
|
|
9829
|
+
const args = [
|
|
9830
|
+
"-o",
|
|
9831
|
+
password ? "BatchMode=no" : "BatchMode=yes"
|
|
9832
|
+
];
|
|
9335
9833
|
if (password)
|
|
9336
9834
|
args.push("-o", "PreferredAuthentications=password", "-o", "PasswordAuthentication=yes", "-o", "PubkeyAuthentication=no", "-o", "NumberOfPasswordPrompts=1");
|
|
9337
9835
|
args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
|
|
@@ -9368,8 +9866,15 @@ function createSshExecutor(config) {
|
|
|
9368
9866
|
const runAuthenticated = async (command, args, options) => {
|
|
9369
9867
|
if (!usesSshPassword(config))
|
|
9370
9868
|
return runCommand(command, args, options);
|
|
9371
|
-
if (!config.password || options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
|
|
9372
|
-
return err({
|
|
9869
|
+
if (!config.password || options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647)) {
|
|
9870
|
+
return err({
|
|
9871
|
+
...HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
9872
|
+
message: deployM("deploy_sshPasswordInvalid"),
|
|
9873
|
+
ext: {
|
|
9874
|
+
commandExecution: { reason: "configuration", localTermination: "not-started" }
|
|
9875
|
+
}
|
|
9876
|
+
});
|
|
9877
|
+
}
|
|
9373
9878
|
const deadline = options.timeoutMs === void 0 ? Number.POSITIVE_INFINITY : performance.now() + options.timeoutMs;
|
|
9374
9879
|
const prepared = await prepareSshAskpass(config.password, deadline, options.terminationTimeoutMs);
|
|
9375
9880
|
if (!prepared.success)
|
|
@@ -9378,20 +9883,36 @@ function createSshExecutor(config) {
|
|
|
9378
9883
|
let removed = ok(void 0);
|
|
9379
9884
|
try {
|
|
9380
9885
|
const remaining = deadline - performance.now();
|
|
9381
|
-
result = remaining <= 0 ? err({
|
|
9886
|
+
result = remaining <= 0 ? err({
|
|
9887
|
+
...HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
9888
|
+
message: deployM("deploy_sshAskpassFailed"),
|
|
9889
|
+
ext: {
|
|
9890
|
+
commandExecution: { reason: "timeout", localTermination: "not-started" }
|
|
9891
|
+
}
|
|
9892
|
+
}) : await runCommand(command, args, {
|
|
9382
9893
|
...options,
|
|
9383
9894
|
env: prepared.data.env,
|
|
9384
9895
|
timeoutMs: Number.isFinite(deadline) ? Math.ceil(remaining) : void 0,
|
|
9385
|
-
secrets: [
|
|
9896
|
+
secrets: [
|
|
9897
|
+
...options.secrets ?? [],
|
|
9898
|
+
config.password
|
|
9899
|
+
]
|
|
9386
9900
|
});
|
|
9387
9901
|
} catch {
|
|
9388
|
-
result = err({
|
|
9902
|
+
result = err({
|
|
9903
|
+
...HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
9904
|
+
message: deployM("deploy_sshAskpassFailed"),
|
|
9905
|
+
ext: {
|
|
9906
|
+
commandExecution: { reason: "configuration", localTermination: "unknown" }
|
|
9907
|
+
}
|
|
9908
|
+
});
|
|
9389
9909
|
} finally {
|
|
9390
9910
|
removed = prepared.data.cleanup();
|
|
9391
9911
|
}
|
|
9392
|
-
if (!removed.success)
|
|
9912
|
+
if (!removed.success) {
|
|
9393
9913
|
return result.success ? removed : err({ ...result.error, message: `${result.error.message}
|
|
9394
9914
|
${removed.error.message}` });
|
|
9915
|
+
}
|
|
9395
9916
|
return result;
|
|
9396
9917
|
};
|
|
9397
9918
|
const exec = async (remoteCommand, options) => {
|
|
@@ -9405,13 +9926,31 @@ ${removed.error.message}` });
|
|
|
9405
9926
|
if (result.success)
|
|
9406
9927
|
return result;
|
|
9407
9928
|
const state = getDeployCommandState(result.error);
|
|
9408
|
-
return err({
|
|
9929
|
+
return err({
|
|
9930
|
+
...result.error,
|
|
9931
|
+
suggestion: deployM("deploy_remoteUnknown"),
|
|
9932
|
+
ext: {
|
|
9933
|
+
...result.error.ext,
|
|
9934
|
+
commandExecution: {
|
|
9935
|
+
...state,
|
|
9936
|
+
reason: state?.reason ?? "exit",
|
|
9937
|
+
localTermination: state?.localTermination ?? "unknown",
|
|
9938
|
+
remoteExecution: "unknown"
|
|
9939
|
+
}
|
|
9940
|
+
}
|
|
9941
|
+
});
|
|
9409
9942
|
};
|
|
9410
9943
|
const upload = async (localPath, remotePath) => {
|
|
9411
9944
|
const args = buildScpUploadArgs(config, localPath, remotePath);
|
|
9412
9945
|
const result = await runAuthenticated("scp", args, { timeoutMs: 6e5 });
|
|
9413
9946
|
if (!result.success) {
|
|
9414
|
-
return err({
|
|
9947
|
+
return err({
|
|
9948
|
+
...HaiDeployError.IMAGE_TRANSFER_FAILED,
|
|
9949
|
+
message: deployM("deploy_imageTransferFailed", {
|
|
9950
|
+
params: { error: result.error.message }
|
|
9951
|
+
}),
|
|
9952
|
+
ext: result.error.ext
|
|
9953
|
+
});
|
|
9415
9954
|
}
|
|
9416
9955
|
return ok(void 0);
|
|
9417
9956
|
};
|
|
@@ -9419,7 +9958,13 @@ ${removed.error.message}` });
|
|
|
9419
9958
|
logger4.debug("SSH preflight", { host: config.host, port: config.port });
|
|
9420
9959
|
const result = await exec("true", { timeoutMs: 2e4 });
|
|
9421
9960
|
if (!result.success) {
|
|
9422
|
-
return err({
|
|
9961
|
+
return err({
|
|
9962
|
+
...HaiDeployError.REMOTE_CONNECT_FAILED,
|
|
9963
|
+
message: deployM("deploy_remoteConnectFailed", {
|
|
9964
|
+
params: { host: config.host, error: result.error.message }
|
|
9965
|
+
}),
|
|
9966
|
+
ext: result.error.ext
|
|
9967
|
+
});
|
|
9423
9968
|
}
|
|
9424
9969
|
return ok(void 0);
|
|
9425
9970
|
};
|
|
@@ -9452,8 +9997,11 @@ async function createDeploymentBundle(options) {
|
|
|
9452
9997
|
const nodeRuntime = artifact.primaryService === "serv" || !!compose.services[artifact.primaryService]?.environment?.NODE_OPTIONS;
|
|
9453
9998
|
for (const name of selected) {
|
|
9454
9999
|
const service = compose.services[name];
|
|
9455
|
-
if (!service)
|
|
9456
|
-
throw new Error(deployM("deploy_selectionInvalid", {
|
|
10000
|
+
if (!service) {
|
|
10001
|
+
throw new Error(deployM("deploy_selectionInvalid", {
|
|
10002
|
+
params: { service: name }
|
|
10003
|
+
}));
|
|
10004
|
+
}
|
|
9457
10005
|
if (typeof service.image === "string")
|
|
9458
10006
|
references.add(service.image);
|
|
9459
10007
|
if (service.depends_on) {
|
|
@@ -9467,12 +10015,18 @@ async function createDeploymentBundle(options) {
|
|
|
9467
10015
|
service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace(/^\.\/config(?=:)/, "../../../config").replace(/\.\/(web\.conf|request-access\.yml|tls|proxy-start\.sh|renew-https\.sh|runtime-env\.mjs):/g, `../../${options.deploymentId}/.hai/$1:`) : mount);
|
|
9468
10016
|
}
|
|
9469
10017
|
}
|
|
9470
|
-
put(".hai/compose.json", JSON.stringify({
|
|
10018
|
+
put(".hai/compose.json", JSON.stringify({
|
|
10019
|
+
...compose,
|
|
10020
|
+
services: Object.fromEntries(selected.map((name) => [name, compose.services[name]]))
|
|
10021
|
+
}));
|
|
9471
10022
|
let runtime;
|
|
9472
10023
|
const imageList = [];
|
|
9473
10024
|
for (const reference of references) {
|
|
9474
|
-
if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
|
|
9475
|
-
throw new Error(deployM("deploy_imageVersionRequired", {
|
|
10025
|
+
if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference)) {
|
|
10026
|
+
throw new Error(deployM("deploy_imageVersionRequired", {
|
|
10027
|
+
params: { image: reference }
|
|
10028
|
+
}));
|
|
10029
|
+
}
|
|
9476
10030
|
const built = artifact.images.find((image) => image.imageRef === reference);
|
|
9477
10031
|
const file = built?.file ?? `middleware-${imageList.length + 1}.tar`;
|
|
9478
10032
|
const destination = join(staging, "images", file);
|
|
@@ -9493,10 +10047,20 @@ async function createDeploymentBundle(options) {
|
|
|
9493
10047
|
if (!runtime)
|
|
9494
10048
|
throw new Error(deployM("deploy_bundleRuntimeRequired"));
|
|
9495
10049
|
}
|
|
9496
|
-
let inspected = await runCommand(runtime, [
|
|
10050
|
+
let inspected = await runCommand(runtime, [
|
|
10051
|
+
"image",
|
|
10052
|
+
"inspect",
|
|
10053
|
+
"--format",
|
|
10054
|
+
"{{.Id}}",
|
|
10055
|
+
reference
|
|
10056
|
+
]);
|
|
9497
10057
|
if (!inspected.success) {
|
|
9498
10058
|
const download = resolveImageReference(reference, options.imageSource);
|
|
9499
|
-
const pulled = await runCommand(runtime, ["pull", download], {
|
|
10059
|
+
const pulled = await runCommand(runtime, ["pull", download], {
|
|
10060
|
+
timeoutMs: 6e5,
|
|
10061
|
+
env: proxyEnvironment,
|
|
10062
|
+
secrets: Object.values(proxyEnvironment).filter(Boolean)
|
|
10063
|
+
});
|
|
9500
10064
|
if (!pulled.success)
|
|
9501
10065
|
throw new Error(pulled.error.message);
|
|
9502
10066
|
if (download !== reference) {
|
|
@@ -9504,7 +10068,13 @@ async function createDeploymentBundle(options) {
|
|
|
9504
10068
|
if (!tagged.success)
|
|
9505
10069
|
throw new Error(tagged.error.message);
|
|
9506
10070
|
}
|
|
9507
|
-
inspected = await runCommand(runtime, [
|
|
10071
|
+
inspected = await runCommand(runtime, [
|
|
10072
|
+
"image",
|
|
10073
|
+
"inspect",
|
|
10074
|
+
"--format",
|
|
10075
|
+
"{{.Id}}",
|
|
10076
|
+
reference
|
|
10077
|
+
]);
|
|
9508
10078
|
}
|
|
9509
10079
|
const identity = inspected.success && !inspected.data.stdoutTruncated ? inspected.data.stdout.trim().replace(/^sha256:/, "") : "";
|
|
9510
10080
|
const cacheIdentity = createHash("sha256").update(identity).update("\0").update(reference).digest("hex");
|
|
@@ -9516,7 +10086,13 @@ async function createDeploymentBundle(options) {
|
|
|
9516
10086
|
checksum = copied;
|
|
9517
10087
|
}
|
|
9518
10088
|
if (!checksum) {
|
|
9519
|
-
const saved = await runCommand(runtime, [
|
|
10089
|
+
const saved = await runCommand(runtime, [
|
|
10090
|
+
"save",
|
|
10091
|
+
...runtime === "podman" ? ["--format", "docker-archive"] : [],
|
|
10092
|
+
"-o",
|
|
10093
|
+
destination,
|
|
10094
|
+
reference
|
|
10095
|
+
], { timeoutMs: 6e5 });
|
|
9520
10096
|
if (!saved.success)
|
|
9521
10097
|
throw new Error(saved.error.message);
|
|
9522
10098
|
checksum = await artifactChecksum(destination);
|
|
@@ -9558,7 +10134,12 @@ async function createDeploymentBundle(options) {
|
|
|
9558
10134
|
if (file !== "https")
|
|
9559
10135
|
put(`.hai/${file}`, content);
|
|
9560
10136
|
}
|
|
9561
|
-
const httpsFingerprint = config.expose.https ? createHash("sha256").update(JSON.stringify({
|
|
10137
|
+
const httpsFingerprint = config.expose.https ? createHash("sha256").update(JSON.stringify({
|
|
10138
|
+
...config.expose.https,
|
|
10139
|
+
certificateFile: void 0,
|
|
10140
|
+
privateKeyFile: void 0,
|
|
10141
|
+
certificate: options.httpsAssets?.["tls/fullchain.pem"]
|
|
10142
|
+
})).digest("hex") : "";
|
|
9562
10143
|
const backend = createBackendStateAssets(projectName, compose, selected);
|
|
9563
10144
|
const metadata = {
|
|
9564
10145
|
"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}` : ""}
|
|
@@ -9580,31 +10161,103 @@ async function createDeploymentBundle(options) {
|
|
|
9580
10161
|
// 未选应用时只带容器原生绑定,保持当前发布的应用连接归属。
|
|
9581
10162
|
"managed-bindings": updatesApp ? managed["managed-bindings"] : managed["managed-bindings"].replace(/^.* prefix\n/gm, ""),
|
|
9582
10163
|
"managed-volumes": managed["managed-volumes"],
|
|
9583
|
-
"managed-app-keys": updatesApp ? `${[
|
|
10164
|
+
"managed-app-keys": updatesApp ? `${[
|
|
10165
|
+
...new Set(options.managedAppKeys)
|
|
10166
|
+
].join("\n")}
|
|
9584
10167
|
` : "\n",
|
|
9585
|
-
"unmanaged-app-keys": updatesApp ? `${[
|
|
10168
|
+
"unmanaged-app-keys": updatesApp ? `${[
|
|
10169
|
+
...new Set(options.unmanagedAppKeys)
|
|
10170
|
+
].join("\n")}
|
|
9586
10171
|
` : "\n",
|
|
9587
10172
|
"state-backends": backend["state-backends"]
|
|
9588
10173
|
};
|
|
9589
|
-
put("manifest.json", `${JSON.stringify({
|
|
10174
|
+
put("manifest.json", `${JSON.stringify({
|
|
10175
|
+
...artifact,
|
|
10176
|
+
bundleFormat: 2,
|
|
10177
|
+
images: artifact.images.filter((image) => references.has(image.imageRef)),
|
|
10178
|
+
deploymentId: options.deploymentId,
|
|
10179
|
+
selectedServices: selected,
|
|
10180
|
+
partial: options.partial,
|
|
10181
|
+
metadata
|
|
10182
|
+
}, null, 2)}
|
|
9590
10183
|
`);
|
|
9591
10184
|
const serverDirectory = `${config.remote.baseDir}/${projectName}`;
|
|
9592
|
-
put("apply.sh", createApplyScript({
|
|
9593
|
-
|
|
10185
|
+
put("apply.sh", createApplyScript({
|
|
10186
|
+
deploymentId: options.deploymentId,
|
|
10187
|
+
projectName,
|
|
10188
|
+
environment,
|
|
10189
|
+
directory: serverDirectory,
|
|
10190
|
+
runtime: config.remote.runtime,
|
|
10191
|
+
partial: options.partial,
|
|
10192
|
+
selected,
|
|
10193
|
+
primaryService: artifact.primaryService,
|
|
10194
|
+
hostPort: config.expose.hostPort,
|
|
10195
|
+
healthPaths: options.healthPaths
|
|
10196
|
+
}));
|
|
10197
|
+
const instructionParams = {
|
|
10198
|
+
file: basename(output),
|
|
10199
|
+
images: [...references].join("\n")
|
|
10200
|
+
};
|
|
9594
10201
|
const mode = deployM(options.partial ? "deploy_bundlePartial" : "deploy_bundleFull");
|
|
9595
|
-
const selection = deployM("deploy_bundleSelection", {
|
|
10202
|
+
const selection = deployM("deploy_bundleSelection", {
|
|
10203
|
+
params: {
|
|
10204
|
+
mode,
|
|
10205
|
+
services: selected.join(", ")
|
|
10206
|
+
}
|
|
10207
|
+
});
|
|
9596
10208
|
const instructions = `${selection}
|
|
9597
10209
|
${deployM("deploy_bundleInstructions", { params: instructionParams })}`;
|
|
9598
|
-
const runtimeGuide = deployM("deploy_bundleRuntime", {
|
|
9599
|
-
|
|
10210
|
+
const runtimeGuide = deployM("deploy_bundleRuntime", {
|
|
10211
|
+
params: { directory: serverDirectory, deploymentId: options.deploymentId }
|
|
10212
|
+
});
|
|
10213
|
+
const variableGuide = deployM("deploy_bundleVariables", {
|
|
10214
|
+
params: { environment, keys: Object.keys(core.config.parseEnv(applicationEnvironment)).sort().join(", ") || "\u2014" }
|
|
10215
|
+
});
|
|
9600
10216
|
const https = config.expose.https;
|
|
9601
10217
|
const certificates = https?.type === "files" ? `${serverDirectory}/releases/${options.deploymentId}/.hai/tls` : `${serverDirectory}/https/certificates/${https?.domain}`;
|
|
9602
|
-
const httpsGuide = https ? deployM("deploy_bundleHttps", {
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
|
|
9606
|
-
|
|
9607
|
-
|
|
10218
|
+
const httpsGuide = https ? deployM("deploy_bundleHttps", {
|
|
10219
|
+
params: {
|
|
10220
|
+
domain: https.domain,
|
|
10221
|
+
port: https.port,
|
|
10222
|
+
mode: https.type,
|
|
10223
|
+
certificates,
|
|
10224
|
+
environment
|
|
10225
|
+
}
|
|
10226
|
+
}) + (https.type === "letsencrypt" ? `
|
|
10227
|
+
${deployM("deploy_bundleAcme", {
|
|
10228
|
+
params: { state: `${serverDirectory}/https/acme`, domain: https.domain }
|
|
10229
|
+
})}` : "") : deployM("deploy_bundleHttpOnly", {
|
|
10230
|
+
params: { port: config.expose.hostPort }
|
|
10231
|
+
});
|
|
10232
|
+
const accountGuide = deployM("deploy_bundleAccounts", {
|
|
10233
|
+
params: { sources: options.accountSources || "\u2014" }
|
|
10234
|
+
});
|
|
10235
|
+
const guides = [
|
|
10236
|
+
runtimeGuide,
|
|
10237
|
+
variableGuide,
|
|
10238
|
+
httpsGuide,
|
|
10239
|
+
accountGuide,
|
|
10240
|
+
deployM("deploy_bundleDirectories", {
|
|
10241
|
+
params: { directory: serverDirectory, deploymentId: options.deploymentId }
|
|
10242
|
+
})
|
|
10243
|
+
].join("\n\n");
|
|
10244
|
+
const bundleMetadata = deployM("deploy_bundleMetadata", {
|
|
10245
|
+
params: {
|
|
10246
|
+
version: artifact.appVersion,
|
|
10247
|
+
buildId: artifact.buildId,
|
|
10248
|
+
deploymentId: options.deploymentId,
|
|
10249
|
+
compose: ".hai/compose.json"
|
|
10250
|
+
}
|
|
10251
|
+
});
|
|
10252
|
+
const manualGuide = deployM("deploy_bundleApplyGuide", {
|
|
10253
|
+
params: {
|
|
10254
|
+
file: shellArgument(basename(output)),
|
|
10255
|
+
directory: serverDirectory,
|
|
10256
|
+
target: shellArgument(serverDirectory),
|
|
10257
|
+
runtime: config.remote.runtime,
|
|
10258
|
+
deploymentId: options.deploymentId
|
|
10259
|
+
}
|
|
10260
|
+
});
|
|
9608
10261
|
put("APPLY.txt", `${bundleMetadata}
|
|
9609
10262
|
${selection}
|
|
9610
10263
|
|
|
@@ -9622,7 +10275,13 @@ ${guides}
|
|
|
9622
10275
|
checksums.push(`${fileHashes.get(file) ?? await artifactChecksum(join(staging, file))} ${file}`);
|
|
9623
10276
|
writeFileSync(join(staging, ".hai/SHA256SUMS"), `${checksums.join("\n")}
|
|
9624
10277
|
`);
|
|
9625
|
-
const tar = await runCommand("tar", [
|
|
10278
|
+
const tar = await runCommand("tar", [
|
|
10279
|
+
output.endsWith(".gz") ? "-czf" : "-cf",
|
|
10280
|
+
temporaryTar,
|
|
10281
|
+
"-C",
|
|
10282
|
+
staging,
|
|
10283
|
+
"."
|
|
10284
|
+
]);
|
|
9626
10285
|
if (!tar.success)
|
|
9627
10286
|
throw new Error(tar.error.message);
|
|
9628
10287
|
chmodSync(temporaryTar, 384);
|
|
@@ -9630,7 +10289,10 @@ ${guides}
|
|
|
9630
10289
|
const upload = createUploadScripts();
|
|
9631
10290
|
const scripts = directory;
|
|
9632
10291
|
writeFileSync(join(scripts, "deploy.sh"), upload.sh, { mode: 448 });
|
|
9633
|
-
writeFileSync(join(scripts, "deploy-local.sh"), createLocalInstallScript({
|
|
10292
|
+
writeFileSync(join(scripts, "deploy-local.sh"), createLocalInstallScript({
|
|
10293
|
+
bundle: basename(output),
|
|
10294
|
+
directory: serverDirectory
|
|
10295
|
+
}), { mode: 448 });
|
|
9634
10296
|
writeFileSync(join(scripts, "deploy.ps1"), upload.ps1);
|
|
9635
10297
|
writeFileSync(join(scripts, "read-target.awk"), upload.reader);
|
|
9636
10298
|
const target = {
|
|
@@ -9662,10 +10324,18 @@ ${guides}
|
|
|
9662
10324
|
writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...target, _comments: comments }, null, 2)}
|
|
9663
10325
|
`);
|
|
9664
10326
|
writeFileSync(join(scripts, "README.txt"), readme);
|
|
9665
|
-
for (const file of [
|
|
10327
|
+
for (const file of [
|
|
10328
|
+
`docker-compose-${artifact.appVersion}.yml`,
|
|
10329
|
+
".env.runtime",
|
|
10330
|
+
"middleware-runtime.env",
|
|
10331
|
+
"request-access.yml",
|
|
10332
|
+
...options.hasWebConfig ? ["web.conf"] : []
|
|
10333
|
+
])
|
|
9666
10334
|
rmSync(join(directory, file), { force: true });
|
|
9667
10335
|
return { bundlePath: output, instructions: `${selection}
|
|
9668
|
-
${deployM("deploy_bundleInstructions", {
|
|
10336
|
+
${deployM("deploy_bundleInstructions", {
|
|
10337
|
+
params: { ...instructionParams, file: output }
|
|
10338
|
+
})}
|
|
9669
10339
|
${guides}
|
|
9670
10340
|
${scripts}`, startName: "apply.sh" };
|
|
9671
10341
|
} finally {
|
|
@@ -9694,9 +10364,7 @@ function createHttpsAssets(config, projectDirectory) {
|
|
|
9694
10364
|
try {
|
|
9695
10365
|
const readPem = (file) => {
|
|
9696
10366
|
const path = resolve(projectDirectory, file);
|
|
9697
|
-
|
|
9698
|
-
throw new Error("Invalid PEM file");
|
|
9699
|
-
return readFileSync(path, "utf8");
|
|
10367
|
+
return readLocalFile(path, { maxBytes: 1048576 }).toString("utf8");
|
|
9700
10368
|
};
|
|
9701
10369
|
const certificate = readPem(config.certificateFile);
|
|
9702
10370
|
const key = readPem(config.privateKeyFile);
|
|
@@ -9889,7 +10557,21 @@ function planPostgresDb(plan, projectName, password, net) {
|
|
|
9889
10557
|
restart: "unless-stopped",
|
|
9890
10558
|
env_file: ["middleware-runtime.env"],
|
|
9891
10559
|
volumes: [`${projectName}-db-data:/var/lib/postgresql/data`],
|
|
9892
|
-
healthcheck: {
|
|
10560
|
+
healthcheck: {
|
|
10561
|
+
test: [
|
|
10562
|
+
"CMD",
|
|
10563
|
+
"pg_isready",
|
|
10564
|
+
"-h",
|
|
10565
|
+
"127.0.0.1",
|
|
10566
|
+
"-U",
|
|
10567
|
+
"hai",
|
|
10568
|
+
"-d",
|
|
10569
|
+
"hai"
|
|
10570
|
+
],
|
|
10571
|
+
interval: "2s",
|
|
10572
|
+
timeout: "5s",
|
|
10573
|
+
retries: 60
|
|
10574
|
+
},
|
|
9893
10575
|
networks: net
|
|
9894
10576
|
};
|
|
9895
10577
|
plan.volumes[`${projectName}-db-data`] = null;
|
|
@@ -9908,7 +10590,12 @@ function planMysqlDb(plan, projectName, password, net) {
|
|
|
9908
10590
|
restart: "unless-stopped",
|
|
9909
10591
|
env_file: ["middleware-runtime.env"],
|
|
9910
10592
|
volumes: [`${projectName}-db-data:/var/lib/mysql`],
|
|
9911
|
-
healthcheck: {
|
|
10593
|
+
healthcheck: {
|
|
10594
|
+
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 --silent"],
|
|
10595
|
+
interval: "2s",
|
|
10596
|
+
timeout: "5s",
|
|
10597
|
+
retries: 60
|
|
10598
|
+
},
|
|
9912
10599
|
networks: net
|
|
9913
10600
|
};
|
|
9914
10601
|
plan.volumes[`${projectName}-db-data`] = null;
|
|
@@ -9926,7 +10613,12 @@ function planRedisCache(plan, projectName, net) {
|
|
|
9926
10613
|
image: IMAGES.redis,
|
|
9927
10614
|
container_name: `hai-${projectName}-cache`,
|
|
9928
10615
|
restart: "unless-stopped",
|
|
9929
|
-
healthcheck: {
|
|
10616
|
+
healthcheck: {
|
|
10617
|
+
test: ["CMD", "redis-cli", "ping"],
|
|
10618
|
+
interval: "2s",
|
|
10619
|
+
timeout: "5s",
|
|
10620
|
+
retries: 60
|
|
10621
|
+
},
|
|
9930
10622
|
networks: net
|
|
9931
10623
|
};
|
|
9932
10624
|
plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
|
|
@@ -9938,15 +10630,40 @@ function planS3Storage(plan, projectName, password, net, engine, configured = {}
|
|
|
9938
10630
|
image: IMAGES[engine],
|
|
9939
10631
|
container_name: `hai-${projectName}-storage`,
|
|
9940
10632
|
restart: "unless-stopped",
|
|
9941
|
-
...engine === "minio" ? {
|
|
9942
|
-
|
|
10633
|
+
...engine === "minio" ? {
|
|
10634
|
+
command: ["server", "/data"]
|
|
10635
|
+
} : {},
|
|
10636
|
+
healthcheck: {
|
|
10637
|
+
test: engine === "rustfs" ? [
|
|
10638
|
+
"CMD",
|
|
10639
|
+
"curl",
|
|
10640
|
+
"--noproxy",
|
|
10641
|
+
"*",
|
|
10642
|
+
"-fsS",
|
|
10643
|
+
"http://127.0.0.1:9000/minio/health/ready"
|
|
10644
|
+
] : [
|
|
10645
|
+
"CMD",
|
|
10646
|
+
"mc",
|
|
10647
|
+
"ready",
|
|
10648
|
+
"http://127.0.0.1:9000"
|
|
10649
|
+
],
|
|
10650
|
+
interval: "2s",
|
|
10651
|
+
timeout: "5s",
|
|
10652
|
+
retries: 60
|
|
10653
|
+
},
|
|
9943
10654
|
env_file: ["middleware-runtime.env"],
|
|
9944
10655
|
volumes: [`${projectName}-storage-data:/data`],
|
|
9945
10656
|
networks: net
|
|
9946
10657
|
};
|
|
9947
10658
|
plan.volumes[`${projectName}-storage-data`] = null;
|
|
9948
10659
|
if (engine === "rustfs") {
|
|
9949
|
-
Object.assign(plan.containerEnv, {
|
|
10660
|
+
Object.assign(plan.containerEnv, {
|
|
10661
|
+
RUSTFS_ACCESS_KEY: "hai-storage",
|
|
10662
|
+
RUSTFS_SECRET_KEY: password,
|
|
10663
|
+
RUSTFS_ADDRESS: "0.0.0.0:9000",
|
|
10664
|
+
RUSTFS_VOLUMES: "/data",
|
|
10665
|
+
RUSTFS_CONSOLE_ENABLE: "false"
|
|
10666
|
+
});
|
|
9950
10667
|
} else {
|
|
9951
10668
|
Object.assign(plan.containerEnv, { MINIO_ROOT_USER: "hai-storage", MINIO_ROOT_PASSWORD: password });
|
|
9952
10669
|
}
|
|
@@ -9956,7 +10673,12 @@ function planS3Storage(plan, projectName, password, net, engine, configured = {}
|
|
|
9956
10673
|
bucket: configured.bucket || "hai",
|
|
9957
10674
|
region: configured.region || "us-east-1",
|
|
9958
10675
|
// 自建只替换连接和托管凭据,不能丢失对象前缀、外部签名地址或操作日志策略。
|
|
9959
|
-
...Object.fromEntries([
|
|
10676
|
+
...Object.fromEntries([
|
|
10677
|
+
"prefix",
|
|
10678
|
+
"publicEndpoint",
|
|
10679
|
+
"publicUrl",
|
|
10680
|
+
"operationLog"
|
|
10681
|
+
].filter((name) => configured[name] !== void 0).map((name) => [name, configured[name]])),
|
|
9960
10682
|
accessKeyId: "hai-storage",
|
|
9961
10683
|
secretAccessKey: password,
|
|
9962
10684
|
forcePathStyle: true,
|
|
@@ -10081,11 +10803,30 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
|
|
|
10081
10803
|
const password = secrets.ldapPassword ?? secrets.dbPassword;
|
|
10082
10804
|
plan.services.push("ldap");
|
|
10083
10805
|
plan.dependsOn.push("ldap");
|
|
10084
|
-
plan.composeServices.ldap = {
|
|
10806
|
+
plan.composeServices.ldap = {
|
|
10807
|
+
image: IMAGES.ldap,
|
|
10808
|
+
restart: "unless-stopped",
|
|
10809
|
+
env_file: ["middleware-runtime.env"],
|
|
10810
|
+
networks: net,
|
|
10811
|
+
volumes: [`${projectName}-ldap-data:/var/lib/ldap`, `${projectName}-ldap-config:/etc/ldap/slapd.d`]
|
|
10812
|
+
};
|
|
10085
10813
|
plan.volumes[`${projectName}-ldap-data`] = null;
|
|
10086
10814
|
plan.volumes[`${projectName}-ldap-config`] = null;
|
|
10087
|
-
Object.assign(plan.containerEnv, {
|
|
10088
|
-
|
|
10815
|
+
Object.assign(plan.containerEnv, {
|
|
10816
|
+
LDAP_DOMAIN: "example.org",
|
|
10817
|
+
LDAP_ORGANISATION: "HAI",
|
|
10818
|
+
LDAP_ADMIN_PASSWORD: password,
|
|
10819
|
+
LDAP_TLS: "false"
|
|
10820
|
+
});
|
|
10821
|
+
Object.assign(plan.env, {
|
|
10822
|
+
HAI_IAM_LOGIN_LDAP: "true",
|
|
10823
|
+
HAI_IAM_LDAP: JSON.stringify({
|
|
10824
|
+
url: "ldap://ldap:389",
|
|
10825
|
+
bindDn: "cn=admin,dc=example,dc=org",
|
|
10826
|
+
bindPassword: password,
|
|
10827
|
+
searchBase: "dc=example,dc=org"
|
|
10828
|
+
})
|
|
10829
|
+
});
|
|
10089
10830
|
plan.secrets.push(password);
|
|
10090
10831
|
} else if (service === "email" && (scan.middlewares ? scan.middlewares.some((item) => item.service === "email" && item.backend === "smtp" && (options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email ?? "default") === "default") : backends.email === "smtp") && !provisionEnv.HAI_REACH_PROVIDERS) {
|
|
10091
10832
|
plan.services.push("email");
|
|
@@ -10093,10 +10834,25 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
|
|
|
10093
10834
|
plan.composeServices.email = { image: IMAGES.smtp, restart: "unless-stopped", networks: net };
|
|
10094
10835
|
const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
|
|
10095
10836
|
if (requirements?.length) {
|
|
10096
|
-
for (const requirement of requirements)
|
|
10097
|
-
plan.env[requirement.envPrefix] = JSON.stringify({
|
|
10837
|
+
for (const requirement of requirements) {
|
|
10838
|
+
plan.env[requirement.envPrefix] = JSON.stringify({
|
|
10839
|
+
name: requirement.config.name ?? "email",
|
|
10840
|
+
type: "smtp",
|
|
10841
|
+
host: "email",
|
|
10842
|
+
port: 1025,
|
|
10843
|
+
secure: false,
|
|
10844
|
+
from: requirement.config.from || "noreply@example.com"
|
|
10845
|
+
});
|
|
10846
|
+
}
|
|
10098
10847
|
} else if (!scan.middlewares) {
|
|
10099
|
-
plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({
|
|
10848
|
+
plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({
|
|
10849
|
+
name: "email",
|
|
10850
|
+
type: "smtp",
|
|
10851
|
+
host: "email",
|
|
10852
|
+
port: 1025,
|
|
10853
|
+
secure: false,
|
|
10854
|
+
from: "noreply@example.com"
|
|
10855
|
+
});
|
|
10100
10856
|
}
|
|
10101
10857
|
}
|
|
10102
10858
|
}
|
|
@@ -10111,15 +10867,21 @@ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort,
|
|
|
10111
10867
|
const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
|
|
10112
10868
|
const environment = options?.environment ?? appEnv.HAI_DEPLOY_ENVIRONMENT ?? "prod";
|
|
10113
10869
|
const nodeEnv = environment === "dev" || environment === "development" ? "development" : environment === "test" ? "test" : "production";
|
|
10114
|
-
const env = mergeEnvironmentLayers([
|
|
10115
|
-
|
|
10116
|
-
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10870
|
+
const env = mergeEnvironmentLayers([
|
|
10871
|
+
{
|
|
10872
|
+
NODE_ENV: nodeEnv
|
|
10873
|
+
},
|
|
10874
|
+
Object.fromEntries(Object.entries(appEnv).filter(([name]) => !CONTAINER_ENV_KEYS.has(name))),
|
|
10875
|
+
sidecar.env,
|
|
10876
|
+
provisionEnv,
|
|
10877
|
+
{
|
|
10878
|
+
// 容器监听由本次制品决定,本机回环地址和开发端口不能覆盖部署拓扑。
|
|
10879
|
+
HOST: "0.0.0.0",
|
|
10880
|
+
PORT: String(containerPort),
|
|
10881
|
+
HAI_SERV_SERVER_HOST: "0.0.0.0",
|
|
10882
|
+
HAI_SERV_SERVER_PORT: String(containerPort)
|
|
10883
|
+
}
|
|
10884
|
+
]);
|
|
10123
10885
|
return { env, sidecar };
|
|
10124
10886
|
}
|
|
10125
10887
|
function generateComposeFile(options) {
|
|
@@ -10139,7 +10901,12 @@ function generateComposeFile(options) {
|
|
|
10139
10901
|
};
|
|
10140
10902
|
if (primaryService === "serv") {
|
|
10141
10903
|
app.healthcheck = {
|
|
10142
|
-
test: [
|
|
10904
|
+
test: [
|
|
10905
|
+
"CMD",
|
|
10906
|
+
"node",
|
|
10907
|
+
"-e",
|
|
10908
|
+
"(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config', configSchemas: { serv: ServConfigSchema } }); const { server, http } = ServConfigSchema.parse(core.config.getOrThrow('serv')); if (http.health === false) process.exit(0); const response = await fetch('http://127.0.0.1:' + server.port + http.health.path, { signal: AbortSignal.timeout(4000) }); process.exit(response.ok ? 0 : 1); } catch { process.exit(1); } })()"
|
|
10909
|
+
],
|
|
10143
10910
|
interval: "30s",
|
|
10144
10911
|
timeout: "5s",
|
|
10145
10912
|
start_period: "15s",
|
|
@@ -10157,14 +10924,31 @@ function generateComposeFile(options) {
|
|
|
10157
10924
|
appVolumes.push("./request-access.yml:/run/hai/request-access.yml:ro");
|
|
10158
10925
|
if (options.web || options.gateway)
|
|
10159
10926
|
appEnvironment.HAI_DEPLOY_PRIVATE_PROXY = "1";
|
|
10927
|
+
if (nodeRuntime && primaryService === "web") {
|
|
10928
|
+
if (!options.gateway)
|
|
10929
|
+
throw new Error("SvelteKit SSR requires a private gateway");
|
|
10930
|
+
Object.assign(appEnvironment, {
|
|
10931
|
+
PROTOCOL_HEADER: "x-forwarded-proto",
|
|
10932
|
+
HOST_HEADER: "host",
|
|
10933
|
+
PORT_HEADER: "",
|
|
10934
|
+
ADDRESS_HEADER: "x-forwarded-for",
|
|
10935
|
+
XFF_DEPTH: "1"
|
|
10936
|
+
});
|
|
10937
|
+
}
|
|
10160
10938
|
if (Object.keys(appEnvironment).length)
|
|
10161
10939
|
app.environment = appEnvironment;
|
|
10162
10940
|
if (options.mountWebConfig)
|
|
10163
10941
|
appVolumes.push("./web.conf:/etc/nginx/conf.d/default.conf:ro");
|
|
10164
10942
|
if (appVolumes.length)
|
|
10165
10943
|
app.volumes = appVolumes;
|
|
10166
|
-
if (sidecar.dependsOn.length > 0)
|
|
10167
|
-
app.depends_on = Object.fromEntries(sidecar.dependsOn.map((name) => [
|
|
10944
|
+
if (sidecar.dependsOn.length > 0) {
|
|
10945
|
+
app.depends_on = Object.fromEntries(sidecar.dependsOn.map((name) => [
|
|
10946
|
+
name,
|
|
10947
|
+
{
|
|
10948
|
+
condition: sidecar.composeServices[name]?.healthcheck ? "service_healthy" : "service_started"
|
|
10949
|
+
}
|
|
10950
|
+
]));
|
|
10951
|
+
}
|
|
10168
10952
|
const services = { [primaryService]: app, ...sidecar.composeServices };
|
|
10169
10953
|
if (options.web) {
|
|
10170
10954
|
delete app.ports;
|
|
@@ -10176,7 +10960,21 @@ function generateComposeFile(options) {
|
|
|
10176
10960
|
pull_policy: "never",
|
|
10177
10961
|
ports: [`${hostPort}:${options.web.containerPort}`],
|
|
10178
10962
|
environment: { PUBLIC_API_BASE: "" },
|
|
10179
|
-
healthcheck: {
|
|
10963
|
+
healthcheck: {
|
|
10964
|
+
test: [
|
|
10965
|
+
"CMD",
|
|
10966
|
+
"wget",
|
|
10967
|
+
"-Y",
|
|
10968
|
+
"off",
|
|
10969
|
+
"-q",
|
|
10970
|
+
"-O",
|
|
10971
|
+
"/dev/null",
|
|
10972
|
+
`http://127.0.0.1:${options.web.containerPort}/_hai/web-health`
|
|
10973
|
+
],
|
|
10974
|
+
interval: "30s",
|
|
10975
|
+
timeout: "5s",
|
|
10976
|
+
retries: 3
|
|
10977
|
+
},
|
|
10180
10978
|
volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
|
|
10181
10979
|
depends_on: sidecar.services.includes("storage") ? ["serv", "storage"] : ["serv"],
|
|
10182
10980
|
networks: ["hai-app"]
|
|
@@ -10189,9 +10987,26 @@ function generateComposeFile(options) {
|
|
|
10189
10987
|
container_name: `hai-${projectName}-gateway`,
|
|
10190
10988
|
restart: "unless-stopped",
|
|
10191
10989
|
ports: [`${hostPort}:8080`],
|
|
10192
|
-
healthcheck: {
|
|
10990
|
+
healthcheck: {
|
|
10991
|
+
test: [
|
|
10992
|
+
"CMD",
|
|
10993
|
+
"wget",
|
|
10994
|
+
"-Y",
|
|
10995
|
+
"off",
|
|
10996
|
+
"-q",
|
|
10997
|
+
"-O",
|
|
10998
|
+
"/dev/null",
|
|
10999
|
+
"http://127.0.0.1:8080/_hai/gateway-health"
|
|
11000
|
+
],
|
|
11001
|
+
interval: "30s",
|
|
11002
|
+
timeout: "5s",
|
|
11003
|
+
retries: 3
|
|
11004
|
+
},
|
|
10193
11005
|
volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
|
|
10194
|
-
depends_on: [
|
|
11006
|
+
depends_on: [
|
|
11007
|
+
primaryService,
|
|
11008
|
+
...sidecar.services.includes("storage") ? ["storage"] : []
|
|
11009
|
+
],
|
|
10195
11010
|
networks: ["hai-app"]
|
|
10196
11011
|
};
|
|
10197
11012
|
}
|
|
@@ -10234,7 +11049,10 @@ function generateComposeFile(options) {
|
|
|
10234
11049
|
};
|
|
10235
11050
|
if (Object.keys(sidecar.volumes).length > 0)
|
|
10236
11051
|
compose.volumes = sidecar.volumes;
|
|
10237
|
-
for (const role of [
|
|
11052
|
+
for (const role of [
|
|
11053
|
+
primaryService,
|
|
11054
|
+
...options.web ? ["web"] : []
|
|
11055
|
+
]) {
|
|
10238
11056
|
const hosts = Object.entries(options.compose?.services[role]?.extra_hosts ?? {});
|
|
10239
11057
|
if (hosts.length)
|
|
10240
11058
|
services[role].extra_hosts = hosts.map(([host, ip]) => `${host}:${ip}`);
|
|
@@ -10248,12 +11066,18 @@ function generateWebProxyConfig(port, apiPrefix, options = {}) {
|
|
|
10248
11066
|
const webPort = options.webPort ?? 8080;
|
|
10249
11067
|
const webBasePath = options.webBasePath ?? "";
|
|
10250
11068
|
const apiPath = apiPrefix.replace(/\/+$/, "");
|
|
10251
|
-
const backendPaths = [
|
|
11069
|
+
const backendPaths = [
|
|
11070
|
+
...new Set(options.backendPaths ?? ["/health"])
|
|
11071
|
+
];
|
|
10252
11072
|
if (!/^\/[\w.-]+(?:\/[\w.-]+)*$/.test(apiPrefix) || apiPrefix.split("/").some((part) => part === "." || part === "..") || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535) || backendPaths.includes(`${webBasePath}/`) || backendPaths.some((path) => !/^\/[\w./-]*$/.test(path) || path.includes("//") || path.split("/").some((part) => part === "." || part === "..") || path === `${webBasePath}/runtime-config.js` || path === "/_hai/web-health") || webBasePath && (backendPaths.includes(webBasePath) || webBasePath === apiPath || webBasePath.startsWith(`${apiPath}/`))) {
|
|
10253
11073
|
throw new Error("Invalid web proxy target");
|
|
10254
11074
|
}
|
|
10255
11075
|
const webLocation = renderWebLocation(webBasePath);
|
|
10256
|
-
const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [
|
|
11076
|
+
const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [
|
|
11077
|
+
apiPath,
|
|
11078
|
+
...backendPaths,
|
|
11079
|
+
...options.storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]
|
|
11080
|
+
], options.storageBasePath) : "";
|
|
10257
11081
|
const backendLocations = backendPaths.map((path) => ` location = ${path} {
|
|
10258
11082
|
proxy_pass http://hai_serv_backend;
|
|
10259
11083
|
proxy_http_version 1.1;
|
|
@@ -10345,11 +11169,13 @@ function generateApplicationProxyConfig(options) {
|
|
|
10345
11169
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
10346
11170
|
throw new Error("Invalid application proxy target");
|
|
10347
11171
|
const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
|
|
11172
|
+
const trustedEdges = renderTrustedEdgeProxies(options.trustedEdgeProxies);
|
|
11173
|
+
const forwardedProtocol = options.trustedEdgeProxies?.length ? "$hai_request_scheme" : "$scheme";
|
|
10348
11174
|
return `include /tmp/hai-dns.conf;
|
|
10349
|
-
${renderResolvedUpstream("hai_application_backend", primaryService, port)}${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
|
|
11175
|
+
${renderResolvedUpstream("hai_application_backend", primaryService, port)}${renderTrustedEdgeProtocol(options.trustedEdgeProxies)}${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
|
|
10350
11176
|
listen 8080;
|
|
10351
11177
|
server_name _;
|
|
10352
|
-
${
|
|
11178
|
+
${trustedEdges} large_client_header_buffers 4 32k;
|
|
10353
11179
|
${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
|
|
10354
11180
|
access_log off;
|
|
10355
11181
|
return 200 'ok';
|
|
@@ -10359,8 +11185,11 @@ ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath)
|
|
|
10359
11185
|
proxy_pass http://hai_application_backend;
|
|
10360
11186
|
proxy_http_version 1.1;
|
|
10361
11187
|
proxy_set_header Host $http_host;
|
|
10362
|
-
proxy_set_header X-Forwarded-Proto $
|
|
11188
|
+
proxy_set_header X-Forwarded-Proto ${forwardedProtocol};
|
|
10363
11189
|
proxy_set_header Connection "";
|
|
11190
|
+
proxy_set_header X-Forwarded-Host $http_host;
|
|
11191
|
+
proxy_set_header X-Forwarded-Port "";
|
|
11192
|
+
proxy_set_header Forwarded "";
|
|
10364
11193
|
proxy_set_header X-Forwarded-For $remote_addr;
|
|
10365
11194
|
proxy_set_header X-Real-IP $remote_addr;
|
|
10366
11195
|
proxy_request_buffering off;
|
|
@@ -10388,6 +11217,20 @@ function renderTrustedEdgeProxies(entries = []) {
|
|
|
10388
11217
|
real_ip_recursive on;
|
|
10389
11218
|
`;
|
|
10390
11219
|
}
|
|
11220
|
+
function renderTrustedEdgeProtocol(entries = []) {
|
|
11221
|
+
if (!entries.length)
|
|
11222
|
+
return "";
|
|
11223
|
+
return `geo $realip_remote_addr $hai_trusted_edge {
|
|
11224
|
+
default 0;
|
|
11225
|
+
${entries.map((entry) => ` ${entry} 1;`).join("\n")}
|
|
11226
|
+
}
|
|
11227
|
+
map "$hai_trusted_edge:$http_x_forwarded_proto" $hai_request_scheme {
|
|
11228
|
+
default $scheme;
|
|
11229
|
+
"1:http" http;
|
|
11230
|
+
"1:https" https;
|
|
11231
|
+
}
|
|
11232
|
+
`;
|
|
11233
|
+
}
|
|
10391
11234
|
function renderWebLocation(webBasePath) {
|
|
10392
11235
|
if (webBasePath && (!/^\/[\w.-]+(?:\/[\w.-]+)*$/.test(webBasePath) || webBasePath.split("/").some((segment) => segment === "." || segment === "..") || ["/health", "/_hai/web-health"].includes(webBasePath)))
|
|
10393
11236
|
throw new Error("Invalid web base path");
|
|
@@ -10443,8 +11286,13 @@ function prepareRequestAccess(options) {
|
|
|
10443
11286
|
if (!scope || typeof scope !== "object" || !("access" in scope))
|
|
10444
11287
|
return ok({ settings: options.settings });
|
|
10445
11288
|
const configured = RequestAccessConfigSchema.safeParse(scope.access);
|
|
10446
|
-
if (!configured.success)
|
|
10447
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_requestAccessInvalid", {
|
|
11289
|
+
if (!configured.success) {
|
|
11290
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_requestAccessInvalid", {
|
|
11291
|
+
params: {
|
|
11292
|
+
field: options.module === "serv" ? "serv.http.access" : "kit.access"
|
|
11293
|
+
}
|
|
11294
|
+
}));
|
|
11295
|
+
}
|
|
10448
11296
|
try {
|
|
10449
11297
|
const policy = core.access.loadPolicy(configured.data, options.directory);
|
|
10450
11298
|
const check = core.access.createRequestCheck(policy);
|
|
@@ -10453,10 +11301,20 @@ function prepareRequestAccess(options) {
|
|
|
10453
11301
|
if (configured.data === false || !("file" in configured.data))
|
|
10454
11302
|
return ok({ settings: options.settings });
|
|
10455
11303
|
const access = { file: "/run/hai/request-access.yml" };
|
|
10456
|
-
const settings = options.module === "serv" ? {
|
|
10457
|
-
|
|
11304
|
+
const settings = options.module === "serv" ? {
|
|
11305
|
+
...options.settings,
|
|
11306
|
+
http: { ...scope, access }
|
|
11307
|
+
} : { ...options.settings, access };
|
|
11308
|
+
return ok({
|
|
11309
|
+
settings,
|
|
11310
|
+
snapshot: stringify(policy, { aliasDuplicateObjects: false })
|
|
11311
|
+
});
|
|
10458
11312
|
} catch {
|
|
10459
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_requestAccessInvalid", {
|
|
11313
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_requestAccessInvalid", {
|
|
11314
|
+
params: {
|
|
11315
|
+
field: options.module === "serv" ? "serv.http.access" : "kit.access"
|
|
11316
|
+
}
|
|
11317
|
+
}));
|
|
10460
11318
|
}
|
|
10461
11319
|
}
|
|
10462
11320
|
var HEALTH_MAX_ATTEMPTS = 40;
|
|
@@ -10475,10 +11333,25 @@ function endpointSettings(value) {
|
|
|
10475
11333
|
}
|
|
10476
11334
|
var ServerResultSchema = z.object({
|
|
10477
11335
|
deploymentId: z.string(),
|
|
10478
|
-
stage: z.enum([
|
|
11336
|
+
stage: z.enum([
|
|
11337
|
+
"preflight",
|
|
11338
|
+
"credentials",
|
|
11339
|
+
"recover",
|
|
11340
|
+
"images",
|
|
11341
|
+
"dependencies",
|
|
11342
|
+
"start",
|
|
11343
|
+
"https",
|
|
11344
|
+
"health",
|
|
11345
|
+
"complete"
|
|
11346
|
+
]),
|
|
10479
11347
|
exitCode: z.number().int().min(0).max(255),
|
|
10480
11348
|
durationSeconds: z.number().int().nonnegative(),
|
|
10481
|
-
recoveryStatus: z.enum([
|
|
11349
|
+
recoveryStatus: z.enum([
|
|
11350
|
+
"not-needed",
|
|
11351
|
+
"passed",
|
|
11352
|
+
"failed",
|
|
11353
|
+
"stopped"
|
|
11354
|
+
]).optional()
|
|
10482
11355
|
});
|
|
10483
11356
|
function makeDeploymentId() {
|
|
10484
11357
|
return `${core.time.formatDate(/* @__PURE__ */ new Date(), "YYYYMMDD-HHmmss")}-${randomBytes(3).toString("hex")}`;
|
|
@@ -10505,7 +11378,11 @@ function createDockerSshProvider(config, container) {
|
|
|
10505
11378
|
}
|
|
10506
11379
|
return err(
|
|
10507
11380
|
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
10508
|
-
deployM("deploy_remoteRuntimeMissing", {
|
|
11381
|
+
deployM("deploy_remoteRuntimeMissing", {
|
|
11382
|
+
params: {
|
|
11383
|
+
runtime: candidates.join(" / ")
|
|
11384
|
+
}
|
|
11385
|
+
})
|
|
10509
11386
|
);
|
|
10510
11387
|
}
|
|
10511
11388
|
async function healthCheck(url, certificate) {
|
|
@@ -10513,7 +11390,10 @@ function createDockerSshProvider(config, container) {
|
|
|
10513
11390
|
try {
|
|
10514
11391
|
if (certificate) {
|
|
10515
11392
|
const healthy = await new Promise((resolve9) => {
|
|
10516
|
-
const request$1 = request(url, {
|
|
11393
|
+
const request$1 = request(url, {
|
|
11394
|
+
ca: [...rootCertificates, certificate],
|
|
11395
|
+
timeout: 5e3
|
|
11396
|
+
}, (response) => {
|
|
10517
11397
|
response.resume();
|
|
10518
11398
|
resolve9(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
|
|
10519
11399
|
});
|
|
@@ -10525,7 +11405,9 @@ function createDockerSshProvider(config, container) {
|
|
|
10525
11405
|
if (healthy)
|
|
10526
11406
|
return true;
|
|
10527
11407
|
} else {
|
|
10528
|
-
const response = await fetch(url, {
|
|
11408
|
+
const response = await fetch(url, {
|
|
11409
|
+
signal: AbortSignal.timeout(5e3)
|
|
11410
|
+
});
|
|
10529
11411
|
await response.body?.cancel();
|
|
10530
11412
|
if (response.ok)
|
|
10531
11413
|
return true;
|
|
@@ -10550,10 +11432,16 @@ function createDockerSshProvider(config, container) {
|
|
|
10550
11432
|
const primaryService = artifact.primaryService;
|
|
10551
11433
|
const app = images.find((image) => image.name === primaryService);
|
|
10552
11434
|
if (options?.selectedServices) {
|
|
10553
|
-
const available = [
|
|
11435
|
+
const available = [
|
|
11436
|
+
...images.map((image) => image.name),
|
|
11437
|
+
...scan.requiredServices
|
|
11438
|
+
];
|
|
10554
11439
|
const invalid = options.selectedServices.find((name2) => !available.includes(name2));
|
|
10555
|
-
if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
|
|
10556
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
|
|
11440
|
+
if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid) {
|
|
11441
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
|
|
11442
|
+
params: { service: invalid ?? options.selectedServices.join(", ") }
|
|
11443
|
+
}));
|
|
11444
|
+
}
|
|
10557
11445
|
}
|
|
10558
11446
|
const hasWebProxy = primaryService === "serv" && artifact.hasWeb;
|
|
10559
11447
|
const partial = artifact.partial || options?.selectedServices !== void 0;
|
|
@@ -10568,14 +11456,12 @@ function createDockerSshProvider(config, container) {
|
|
|
10568
11456
|
const plannedRuntime = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
|
|
10569
11457
|
const { sidecar } = plannedRuntime;
|
|
10570
11458
|
let { env } = plannedRuntime;
|
|
10571
|
-
if (https && scan.isSvelteKit && !env.ORIGIN)
|
|
10572
|
-
env.ORIGIN = url;
|
|
10573
11459
|
const managedStorage = sidecar.services.includes("storage") ? JSON.parse(sidecar.env.HAI_STORAGE) : void 0;
|
|
10574
11460
|
let storageBasePath = "";
|
|
10575
11461
|
if (managedStorage?.publicEndpoint !== void 0) {
|
|
10576
11462
|
try {
|
|
10577
11463
|
if (typeof managedStorage.publicEndpoint !== "string")
|
|
10578
|
-
throw new
|
|
11464
|
+
throw new TypeError("Invalid public endpoint type");
|
|
10579
11465
|
const unsafeCharacters = /[\u0000-\u0020\u007F\\%]/.test(managedStorage.publicEndpoint);
|
|
10580
11466
|
const endpoint = new URL(managedStorage.publicEndpoint);
|
|
10581
11467
|
if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.search || endpoint.hash || !/^\/(?:[\w.-]+(?:\/[\w.-]+)*\/?)?$/.test(endpoint.pathname) || unsafeCharacters || /(?:^|\/)\.{1,2}(?:\/|$)/.test(managedStorage.publicEndpoint)) {
|
|
@@ -10587,7 +11473,7 @@ function createDockerSshProvider(config, container) {
|
|
|
10587
11473
|
}
|
|
10588
11474
|
}
|
|
10589
11475
|
const storageRouting = managedStorage ? createHash("sha256").update(JSON.stringify({ ...managedStorage, secretAccessKey: void 0 })).digest("hex") : "";
|
|
10590
|
-
const gateway = !hasWebProxy && (!!managedStorage || !!https && !standaloneWebConfig);
|
|
11476
|
+
const gateway = !hasWebProxy && (scan.isSvelteKit || !!managedStorage || !!https && !standaloneWebConfig);
|
|
10591
11477
|
if (gateway && primaryService === "web" && !scan.isSvelteKit)
|
|
10592
11478
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
|
|
10593
11479
|
const proxyService = gateway ? "gateway" : "web";
|
|
@@ -10649,8 +11535,14 @@ function createDockerSshProvider(config, container) {
|
|
|
10649
11535
|
return prepared2;
|
|
10650
11536
|
Object.assign(server.data, prepared2.data.settings);
|
|
10651
11537
|
requestAccessSnapshot = prepared2.data.snapshot;
|
|
10652
|
-
if (requestAccessSnapshot)
|
|
10653
|
-
env = mergeEnvironmentLayers([
|
|
11538
|
+
if (requestAccessSnapshot) {
|
|
11539
|
+
env = mergeEnvironmentLayers([
|
|
11540
|
+
env,
|
|
11541
|
+
{
|
|
11542
|
+
HAI_SERV: JSON.stringify(server.data)
|
|
11543
|
+
}
|
|
11544
|
+
]);
|
|
11545
|
+
}
|
|
10654
11546
|
}
|
|
10655
11547
|
let feedbackTransportBytes;
|
|
10656
11548
|
if (scan.haiModules?.includes("@h-ai/feedback")) {
|
|
@@ -10676,8 +11568,14 @@ function createDockerSshProvider(config, container) {
|
|
|
10676
11568
|
return prepared2;
|
|
10677
11569
|
Object.assign(kit.data, prepared2.data.settings);
|
|
10678
11570
|
requestAccessSnapshot = prepared2.data.snapshot;
|
|
10679
|
-
if (requestAccessSnapshot)
|
|
10680
|
-
env = mergeEnvironmentLayers([
|
|
11571
|
+
if (requestAccessSnapshot) {
|
|
11572
|
+
env = mergeEnvironmentLayers([
|
|
11573
|
+
env,
|
|
11574
|
+
{
|
|
11575
|
+
HAI_KIT: JSON.stringify(kit.data)
|
|
11576
|
+
}
|
|
11577
|
+
]);
|
|
11578
|
+
}
|
|
10681
11579
|
}
|
|
10682
11580
|
const transport = kit.data.transport;
|
|
10683
11581
|
if (transport && typeof transport === "object") {
|
|
@@ -10693,8 +11591,13 @@ function createDockerSshProvider(config, container) {
|
|
|
10693
11591
|
}
|
|
10694
11592
|
}
|
|
10695
11593
|
}
|
|
10696
|
-
if (updatesApp && protectedTransport && scan.haiModules?.includes("@h-ai/crypto") && !parseTransportServerKeyRing(env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING).success)
|
|
10697
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_transportKeyRingInvalid", {
|
|
11594
|
+
if (updatesApp && protectedTransport && scan.haiModules?.includes("@h-ai/crypto") && !parseTransportServerKeyRing(env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING).success) {
|
|
11595
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_transportKeyRingInvalid", {
|
|
11596
|
+
params: {
|
|
11597
|
+
path: join(scan.envDir ?? appDir, `.env.${options?.environment ?? "prod"}.local`)
|
|
11598
|
+
}
|
|
11599
|
+
}));
|
|
11600
|
+
}
|
|
10698
11601
|
let webProxy;
|
|
10699
11602
|
if (hasWebProxy) {
|
|
10700
11603
|
const http = server.data.http;
|
|
@@ -10709,11 +11612,30 @@ function createDockerSshProvider(config, container) {
|
|
|
10709
11612
|
try {
|
|
10710
11613
|
const configured = transport ? transport.maxPlaintextBytes : void 0;
|
|
10711
11614
|
const limits = resolveTransportBodyLimits(Math.max(createTransportBodyPolicy(configured === "auto" ? void 0 : configured, transport ? transport.routePlaintextBytes : void 0).maximum.plaintextBytes, feedbackTransportBytes ?? 0));
|
|
10712
|
-
webProxy = generateWebProxyConfig(containerPort, apiPrefix, {
|
|
11615
|
+
webProxy = generateWebProxyConfig(containerPort, apiPrefix, {
|
|
11616
|
+
webPort: webContainerPort,
|
|
11617
|
+
maxPlaintextBytes: limits.plaintextBytes,
|
|
11618
|
+
webBasePath,
|
|
11619
|
+
backendPaths,
|
|
11620
|
+
storageBucket: managedStorage?.bucket,
|
|
11621
|
+
storageBasePath,
|
|
11622
|
+
trustedEdgeProxies: config.trustedEdgeProxies
|
|
11623
|
+
});
|
|
10713
11624
|
} catch (error) {
|
|
10714
11625
|
return err(HaiDeployError.CONFIG_ERROR, error instanceof RangeError ? error.message : deployM(managedStorage ? "deploy_storageProxyInvalid" : "deploy_transportBodyLimitInvalid"));
|
|
10715
11626
|
}
|
|
10716
|
-
env = mergeEnvironmentLayers([
|
|
11627
|
+
env = mergeEnvironmentLayers([
|
|
11628
|
+
env,
|
|
11629
|
+
{
|
|
11630
|
+
HAI_SERV: JSON.stringify({
|
|
11631
|
+
...server.data,
|
|
11632
|
+
cors: {
|
|
11633
|
+
...cors,
|
|
11634
|
+
origin: (!cors?.origin || cors.origin === "*") && (config.ssh || https) ? url : cors?.origin ?? "*"
|
|
11635
|
+
}
|
|
11636
|
+
})
|
|
11637
|
+
}
|
|
11638
|
+
]);
|
|
10717
11639
|
} else if (gateway) {
|
|
10718
11640
|
try {
|
|
10719
11641
|
const http = server.data.http;
|
|
@@ -10721,7 +11643,18 @@ function createDockerSshProvider(config, container) {
|
|
|
10721
11643
|
const transport = server.data.transport;
|
|
10722
11644
|
applicationPlaintextBytes = Math.max(createTransportBodyPolicy(transport?.maxPlaintextBytes === "auto" ? void 0 : transport?.maxPlaintextBytes, transport?.routePlaintextBytes).maximum.plaintextBytes, feedbackTransportBytes ?? 0);
|
|
10723
11645
|
}
|
|
10724
|
-
webProxy = generateApplicationProxyConfig({
|
|
11646
|
+
webProxy = generateApplicationProxyConfig({
|
|
11647
|
+
primaryService,
|
|
11648
|
+
port: containerPort,
|
|
11649
|
+
bucket: managedStorage?.bucket,
|
|
11650
|
+
storageBasePath,
|
|
11651
|
+
reservedPaths: [
|
|
11652
|
+
http?.apiPrefix ?? "/api/v1",
|
|
11653
|
+
...storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]
|
|
11654
|
+
],
|
|
11655
|
+
maxPlaintextBytes: applicationPlaintextBytes,
|
|
11656
|
+
trustedEdgeProxies: config.trustedEdgeProxies
|
|
11657
|
+
});
|
|
10725
11658
|
} catch {
|
|
10726
11659
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageProxyInvalid"));
|
|
10727
11660
|
}
|
|
@@ -10730,11 +11663,23 @@ function createDockerSshProvider(config, container) {
|
|
|
10730
11663
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
|
|
10731
11664
|
webProxy = generateStaticWebConfig(containerPort, webBasePath);
|
|
10732
11665
|
}
|
|
10733
|
-
if (https && webProxy)
|
|
11666
|
+
if (https && webProxy) {
|
|
10734
11667
|
webProxy = renderHttpsProxy(webProxy, https, hasWebProxy ? webContainerPort : gateway ? 8080 : containerPort);
|
|
11668
|
+
}
|
|
10735
11669
|
if (https && primaryService === "serv" && !hasWebProxy) {
|
|
10736
11670
|
const cors = server.data.cors;
|
|
10737
|
-
env = mergeEnvironmentLayers([
|
|
11671
|
+
env = mergeEnvironmentLayers([
|
|
11672
|
+
env,
|
|
11673
|
+
{
|
|
11674
|
+
HAI_SERV: JSON.stringify({
|
|
11675
|
+
...server.data,
|
|
11676
|
+
cors: {
|
|
11677
|
+
...cors,
|
|
11678
|
+
origin: !cors?.origin || cors.origin === "*" ? url : cors.origin
|
|
11679
|
+
}
|
|
11680
|
+
})
|
|
11681
|
+
}
|
|
11682
|
+
]);
|
|
10738
11683
|
}
|
|
10739
11684
|
const requestedOutput = options?.output ?? join(deploymentRoot, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar.gz`);
|
|
10740
11685
|
const compressed = requestedOutput.endsWith(".tar.gz");
|
|
@@ -10744,20 +11689,43 @@ function createDockerSshProvider(config, container) {
|
|
|
10744
11689
|
if (!/^[\w.+-]+$/.test(name))
|
|
10745
11690
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleNameInvalid"));
|
|
10746
11691
|
const artifactDir = resolve(dirname(requestedOutput), name);
|
|
10747
|
-
if (existsSync(artifactDir))
|
|
10748
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleDirectoryExists", {
|
|
11692
|
+
if (existsSync(artifactDir)) {
|
|
11693
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleDirectoryExists", {
|
|
11694
|
+
params: { path: artifactDir }
|
|
11695
|
+
}));
|
|
11696
|
+
}
|
|
10749
11697
|
mkdirSync(dirname(artifactDir), { recursive: true, mode: 448 });
|
|
10750
11698
|
mkdirSync(artifactDir, { mode: 448 });
|
|
10751
11699
|
chmodSync(artifactDir, 448);
|
|
10752
11700
|
const envPath = join(artifactDir, ".env.runtime");
|
|
10753
11701
|
const nodeRuntime = primaryService === "serv" || scan.isSvelteKit;
|
|
10754
11702
|
const startupOverrides = nodeRuntime ? { NODE_OPTIONS: "", HAI_DEPLOY_PRIVATE_PROXY: "" } : {};
|
|
11703
|
+
if (scan.isSvelteKit) {
|
|
11704
|
+
for (const name2 of [
|
|
11705
|
+
"ORIGIN",
|
|
11706
|
+
"PROTOCOL_HEADER",
|
|
11707
|
+
"HOST_HEADER",
|
|
11708
|
+
"PORT_HEADER",
|
|
11709
|
+
"ADDRESS_HEADER",
|
|
11710
|
+
"XFF_DEPTH"
|
|
11711
|
+
])
|
|
11712
|
+
startupOverrides[name2] = "";
|
|
11713
|
+
}
|
|
10755
11714
|
const runtimeEnvironment = serializeEnvFile(env, startupOverrides, nodeRuntime ? "node" : "compose");
|
|
10756
|
-
const envVarsSet = [
|
|
10757
|
-
|
|
10758
|
-
|
|
10759
|
-
|
|
10760
|
-
|
|
11715
|
+
const envVarsSet = [
|
|
11716
|
+
.../* @__PURE__ */ new Set([
|
|
11717
|
+
...Object.keys(parseEnv(runtimeEnvironment)),
|
|
11718
|
+
...nodeRuntime ? ["NODE_OPTIONS"] : [],
|
|
11719
|
+
...hasWebProxy || gateway ? ["HAI_DEPLOY_PRIVATE_PROXY"] : [],
|
|
11720
|
+
...scan.isSvelteKit ? [
|
|
11721
|
+
"PROTOCOL_HEADER",
|
|
11722
|
+
"HOST_HEADER",
|
|
11723
|
+
"PORT_HEADER",
|
|
11724
|
+
"ADDRESS_HEADER",
|
|
11725
|
+
"XFF_DEPTH"
|
|
11726
|
+
] : []
|
|
11727
|
+
])
|
|
11728
|
+
];
|
|
10761
11729
|
writeFileSync(envPath, runtimeEnvironment, { mode: 384 });
|
|
10762
11730
|
if (requestAccessSnapshot)
|
|
10763
11731
|
writeFileSync(join(artifactDir, "request-access.yml"), requestAccessSnapshot, { mode: 384 });
|
|
@@ -10766,7 +11734,12 @@ function createDockerSshProvider(config, container) {
|
|
|
10766
11734
|
const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
|
|
10767
11735
|
if (!environment.success)
|
|
10768
11736
|
return environment;
|
|
10769
|
-
const saved = saveDeploymentEnvironment(environment.data, {
|
|
11737
|
+
const saved = saveDeploymentEnvironment(environment.data, {
|
|
11738
|
+
HAI_DEPLOY_ENVIRONMENT: environment.data.environment,
|
|
11739
|
+
HAI_CORE_ENV: environment.data.values.HAI_CORE_ENV,
|
|
11740
|
+
...sidecar.env,
|
|
11741
|
+
...provisionEnv
|
|
11742
|
+
});
|
|
10770
11743
|
if (!saved.success)
|
|
10771
11744
|
return saved;
|
|
10772
11745
|
}
|
|
@@ -10792,16 +11765,27 @@ function createDockerSshProvider(config, container) {
|
|
|
10792
11765
|
writeFileSync(composePath, composeContent);
|
|
10793
11766
|
if (webProxy)
|
|
10794
11767
|
writeFileSync(join(artifactDir, "web.conf"), webProxy);
|
|
10795
|
-
const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [
|
|
11768
|
+
const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [
|
|
11769
|
+
...images.map((image) => image.name),
|
|
11770
|
+
...Object.keys(sidecar.composeServices)
|
|
11771
|
+
];
|
|
10796
11772
|
if (gateway && !partial)
|
|
10797
11773
|
selected.push("gateway");
|
|
10798
11774
|
if (https?.type === "letsencrypt" && !partial)
|
|
10799
11775
|
selected.push("certbot");
|
|
10800
|
-
if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length)
|
|
10801
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
|
|
11776
|
+
if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length) {
|
|
11777
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
|
|
11778
|
+
params: {
|
|
11779
|
+
service: selected.join(", ")
|
|
11780
|
+
}
|
|
11781
|
+
}));
|
|
11782
|
+
}
|
|
10802
11783
|
for (const name2 of selected) {
|
|
10803
|
-
if (["serv", "web"].includes(name2) && !images.some((image) => image.name === name2))
|
|
10804
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
|
|
11784
|
+
if (["serv", "web"].includes(name2) && !images.some((image) => image.name === name2)) {
|
|
11785
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", {
|
|
11786
|
+
params: { service: name2 }
|
|
11787
|
+
}));
|
|
11788
|
+
}
|
|
10805
11789
|
}
|
|
10806
11790
|
options?.onProgress?.("packaging");
|
|
10807
11791
|
const bundle = await createDeploymentBundle({
|
|
@@ -10823,14 +11807,28 @@ function createDockerSshProvider(config, container) {
|
|
|
10823
11807
|
containerPort,
|
|
10824
11808
|
managedAppKeys: Object.keys(sidecar.env),
|
|
10825
11809
|
unmanagedAppKeys: scan.middlewares?.filter((item) => !Object.hasOwn(sidecar.env, item.envPrefix)).map((item) => item.envPrefix),
|
|
10826
|
-
accountSources: describeDeploymentVariableSources(options?.envDir ?? scan.envDir ?? appDir, options?.environment ?? "prod", env, [
|
|
11810
|
+
accountSources: describeDeploymentVariableSources(options?.envDir ?? scan.envDir ?? appDir, options?.environment ?? "prod", env, [
|
|
11811
|
+
"HAI_ADMIN_DEFAULT_PASSWORD",
|
|
11812
|
+
"HAI_AUDIT_ADMIN_PASSWORD",
|
|
11813
|
+
"HAI_CONFIG_ADMIN_PASSWORD",
|
|
11814
|
+
"HAI_BUSINESS_OPERATOR_PASSWORD"
|
|
11815
|
+
]),
|
|
10827
11816
|
healthPaths,
|
|
10828
11817
|
httpsAssets,
|
|
10829
11818
|
output: join(artifactDir, `${name}.tar${compressed ? ".gz" : ""}`),
|
|
10830
11819
|
cacheDirectory: join(resolveProjectDirectory(appDir), ".hai", "cache", "images")
|
|
10831
11820
|
});
|
|
10832
|
-
if (options?.bundleOnly)
|
|
10833
|
-
return ok({
|
|
11821
|
+
if (options?.bundleOnly) {
|
|
11822
|
+
return ok({
|
|
11823
|
+
url: "",
|
|
11824
|
+
deploymentId,
|
|
11825
|
+
status: "packaged",
|
|
11826
|
+
provider: "docker-ssh",
|
|
11827
|
+
healthCheck: "skipped",
|
|
11828
|
+
envVarsSet,
|
|
11829
|
+
...bundle
|
|
11830
|
+
});
|
|
11831
|
+
}
|
|
10834
11832
|
if (!ssh)
|
|
10835
11833
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
10836
11834
|
const ping = await ssh.ping();
|
|
@@ -10850,19 +11848,23 @@ function createDockerSshProvider(config, container) {
|
|
|
10850
11848
|
let pendingOutput = "";
|
|
10851
11849
|
const started = await ssh.exec(
|
|
10852
11850
|
`mkdir -p ${shellArgument(`${remoteTar}.contents`)} && chmod 600 ${shellArgument(remoteTar)} && tar -xf ${shellArgument(remoteTar)} -C ${shellArgument(`${remoteTar}.contents`)} && sh ${shellArgument(`${remoteTar}.contents/${bundle.startName}`)} ${shellArgument(remoteDir)} && { rm -rf -- ${shellArgument(`${remoteTar}.contents`)} && rm -f -- ${shellArgument(remoteTar)} || printf '[WARN] Deployment passed; incoming archive cleanup failed\\n' >&2; }`,
|
|
10853
|
-
{
|
|
10854
|
-
|
|
10855
|
-
|
|
10856
|
-
|
|
10857
|
-
|
|
10858
|
-
|
|
10859
|
-
|
|
10860
|
-
|
|
10861
|
-
|
|
10862
|
-
|
|
10863
|
-
|
|
11851
|
+
{
|
|
11852
|
+
timeoutMs: 6e5,
|
|
11853
|
+
secrets: sidecar.secrets,
|
|
11854
|
+
onOutput: (output) => {
|
|
11855
|
+
const parts = output.split("\n");
|
|
11856
|
+
for (let index = 0; index < parts.length; index++) {
|
|
11857
|
+
pendingOutput += parts[index].slice(0, 512 - pendingOutput.length);
|
|
11858
|
+
if (index === parts.length - 1)
|
|
11859
|
+
break;
|
|
11860
|
+
const line = pendingOutput;
|
|
11861
|
+
pendingOutput = "";
|
|
11862
|
+
const stage = /^\[(preflight|images|dependencies|start|https|health|complete)\]/.exec(line)?.[1];
|
|
11863
|
+
if (stage)
|
|
11864
|
+
options?.onProgress?.(stage);
|
|
11865
|
+
}
|
|
10864
11866
|
}
|
|
10865
|
-
}
|
|
11867
|
+
}
|
|
10866
11868
|
);
|
|
10867
11869
|
if (!started.success) {
|
|
10868
11870
|
const resultFile = `${remoteDir}/result-${deploymentId}.json`;
|
|
@@ -10877,16 +11879,34 @@ function createDockerSshProvider(config, container) {
|
|
|
10877
11879
|
} catch {
|
|
10878
11880
|
}
|
|
10879
11881
|
}
|
|
10880
|
-
return err({
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
10885
|
-
|
|
10886
|
-
|
|
10887
|
-
|
|
10888
|
-
|
|
10889
|
-
|
|
11882
|
+
return err({
|
|
11883
|
+
...started.error,
|
|
11884
|
+
message: `${started.error.message}
|
|
11885
|
+
${terminal ? deployM("deploy_remoteFinished", {
|
|
11886
|
+
params: {
|
|
11887
|
+
stage: terminal.stage,
|
|
11888
|
+
code: String(terminal.exitCode)
|
|
11889
|
+
}
|
|
11890
|
+
}) : deployM("deploy_remoteUnknown")}`,
|
|
11891
|
+
suggestion: terminal ? deployM("deploy_remoteFinished", {
|
|
11892
|
+
params: {
|
|
11893
|
+
stage: terminal.stage,
|
|
11894
|
+
code: String(terminal.exitCode)
|
|
11895
|
+
}
|
|
11896
|
+
}) : deployM("deploy_remoteUnknown"),
|
|
11897
|
+
ext: {
|
|
11898
|
+
...started.error.ext,
|
|
11899
|
+
commandExecution: {
|
|
11900
|
+
...state,
|
|
11901
|
+
reason: state?.reason ?? "exit",
|
|
11902
|
+
localTermination: state?.localTermination ?? "unknown",
|
|
11903
|
+
remoteExecution: terminal ? "finished" : "unknown",
|
|
11904
|
+
deploymentId,
|
|
11905
|
+
resultFile,
|
|
11906
|
+
...terminal ? { remoteExitCode: terminal.exitCode, remoteStage: terminal.stage, remoteRecovery: terminal.recoveryStatus } : {}
|
|
11907
|
+
}
|
|
11908
|
+
}
|
|
11909
|
+
});
|
|
10890
11910
|
}
|
|
10891
11911
|
logger5.info("Running health check", { url: applicationUrl });
|
|
10892
11912
|
let healthy = true;
|
|
@@ -10898,16 +11918,24 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
|
|
|
10898
11918
|
}
|
|
10899
11919
|
if (!healthy) {
|
|
10900
11920
|
return err(
|
|
10901
|
-
{
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
11921
|
+
{
|
|
11922
|
+
...HaiDeployError.HEALTH_CHECK_FAILED,
|
|
11923
|
+
message: deployM("deploy_healthCheckFailed", {
|
|
11924
|
+
params: { url: applicationUrl }
|
|
11925
|
+
}),
|
|
11926
|
+
ext: {
|
|
11927
|
+
commandExecution: {
|
|
11928
|
+
reason: "exit",
|
|
11929
|
+
localTermination: "confirmed",
|
|
11930
|
+
remoteExecution: "finished",
|
|
11931
|
+
deploymentId,
|
|
11932
|
+
resultFile: `${remoteDir}/result-${deploymentId}.json`,
|
|
11933
|
+
remoteExitCode: 0,
|
|
11934
|
+
remoteStage: "complete",
|
|
11935
|
+
remoteRecovery: "not-needed"
|
|
11936
|
+
}
|
|
11937
|
+
}
|
|
11938
|
+
}
|
|
10911
11939
|
);
|
|
10912
11940
|
}
|
|
10913
11941
|
const result = {
|
|
@@ -10929,11 +11957,16 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
|
|
|
10929
11957
|
const reason = error instanceof Error ? error.message : String(error);
|
|
10930
11958
|
let safeReason = "Deployment bundle failed";
|
|
10931
11959
|
try {
|
|
10932
|
-
safeReason = maskSecrets(reason, [
|
|
11960
|
+
safeReason = maskSecrets(reason, [
|
|
11961
|
+
...Object.values(context.appEnv ?? {}),
|
|
11962
|
+
config.ssh?.password ?? ""
|
|
11963
|
+
].filter(Boolean));
|
|
10933
11964
|
} catch {
|
|
10934
11965
|
}
|
|
10935
11966
|
logger5.error("Deployment bundle failed", { reason: safeReason });
|
|
10936
|
-
return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", {
|
|
11967
|
+
return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", {
|
|
11968
|
+
params: { reason: safeReason }
|
|
11969
|
+
}));
|
|
10937
11970
|
}
|
|
10938
11971
|
};
|
|
10939
11972
|
return { name: "docker-ssh", deploy: deploySafely };
|
|
@@ -10944,7 +11977,12 @@ var POLL_INTERVAL = 3e3;
|
|
|
10944
11977
|
var MAX_POLL_ATTEMPTS = 60;
|
|
10945
11978
|
var VercelApiError = class extends Error {
|
|
10946
11979
|
constructor(status) {
|
|
10947
|
-
super(deployM("deploy_apiError", {
|
|
11980
|
+
super(deployM("deploy_apiError", {
|
|
11981
|
+
params: {
|
|
11982
|
+
service: "Vercel",
|
|
11983
|
+
status: String(status)
|
|
11984
|
+
}
|
|
11985
|
+
}));
|
|
10948
11986
|
this.status = status;
|
|
10949
11987
|
}
|
|
10950
11988
|
status;
|
|
@@ -10953,7 +11991,9 @@ function toDeployError(codeDef, messageKey, error) {
|
|
|
10953
11991
|
return {
|
|
10954
11992
|
...codeDef,
|
|
10955
11993
|
message: deployM(messageKey, {
|
|
10956
|
-
params: {
|
|
11994
|
+
params: {
|
|
11995
|
+
error: error instanceof Error ? error.message : String(error)
|
|
11996
|
+
}
|
|
10957
11997
|
}),
|
|
10958
11998
|
cause: error
|
|
10959
11999
|
};
|
|
@@ -11163,7 +12203,11 @@ async function uploadFile(token, content, sha) {
|
|
|
11163
12203
|
body: new Blob([uploadContent])
|
|
11164
12204
|
});
|
|
11165
12205
|
if (!response.ok && response.status !== 409) {
|
|
11166
|
-
throw new Error(deployM("deploy_fileUploadFailed", {
|
|
12206
|
+
throw new Error(deployM("deploy_fileUploadFailed", {
|
|
12207
|
+
params: {
|
|
12208
|
+
status: String(response.status)
|
|
12209
|
+
}
|
|
12210
|
+
}));
|
|
11167
12211
|
}
|
|
11168
12212
|
}
|
|
11169
12213
|
async function pollDeploymentStatus(token, deploymentId) {
|
|
@@ -11196,7 +12240,9 @@ function createAliyunProvisioner() {
|
|
|
11196
12240
|
const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
|
|
11197
12241
|
const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
|
|
11198
12242
|
if (!akId || !akSecret || !credentials.signName) {
|
|
11199
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12243
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12244
|
+
params: { fields: "accessKeyId, accessKeySecret, signName" }
|
|
12245
|
+
}));
|
|
11200
12246
|
}
|
|
11201
12247
|
accessKey = akId;
|
|
11202
12248
|
secretKey = akSecret;
|
|
@@ -11208,7 +12254,9 @@ function createAliyunProvisioner() {
|
|
|
11208
12254
|
return err(
|
|
11209
12255
|
HaiDeployError.AUTH_FAILED,
|
|
11210
12256
|
deployM("deploy_authFailed", {
|
|
11211
|
-
params: {
|
|
12257
|
+
params: {
|
|
12258
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12259
|
+
}
|
|
11212
12260
|
}),
|
|
11213
12261
|
error
|
|
11214
12262
|
);
|
|
@@ -11227,7 +12275,15 @@ function createAliyunProvisioner() {
|
|
|
11227
12275
|
provisionerName: "aliyun",
|
|
11228
12276
|
resourceStatus: "reused",
|
|
11229
12277
|
envVars: {
|
|
11230
|
-
HAI_REACH_PROVIDERS: JSON.stringify([
|
|
12278
|
+
HAI_REACH_PROVIDERS: JSON.stringify([
|
|
12279
|
+
{
|
|
12280
|
+
name: "sms",
|
|
12281
|
+
type: "aliyun-sms",
|
|
12282
|
+
accessKeyId: accessKey,
|
|
12283
|
+
accessKeySecret: secretKey,
|
|
12284
|
+
signName
|
|
12285
|
+
}
|
|
12286
|
+
])
|
|
11231
12287
|
},
|
|
11232
12288
|
resourceInfo: "aliyun:verify-only"
|
|
11233
12289
|
});
|
|
@@ -11249,7 +12305,12 @@ async function neonFetch(token, path, options) {
|
|
|
11249
12305
|
}
|
|
11250
12306
|
});
|
|
11251
12307
|
if (!response.ok) {
|
|
11252
|
-
throw new Error(deployM("deploy_apiError", {
|
|
12308
|
+
throw new Error(deployM("deploy_apiError", {
|
|
12309
|
+
params: {
|
|
12310
|
+
service: "Neon",
|
|
12311
|
+
status: String(response.status)
|
|
12312
|
+
}
|
|
12313
|
+
}));
|
|
11253
12314
|
}
|
|
11254
12315
|
return response.json();
|
|
11255
12316
|
}
|
|
@@ -11269,10 +12330,14 @@ async function getNeonConnectionUri(token, projectId) {
|
|
|
11269
12330
|
const data = await neonFetch(
|
|
11270
12331
|
token,
|
|
11271
12332
|
`/projects/${projectId}/connection_uri?${query.toString()}`,
|
|
11272
|
-
{
|
|
12333
|
+
{
|
|
12334
|
+
headers: { "Content-Type": "application/json" }
|
|
12335
|
+
}
|
|
11273
12336
|
);
|
|
11274
12337
|
if (!data.uri) {
|
|
11275
|
-
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12338
|
+
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12339
|
+
params: { service: "Neon" }
|
|
12340
|
+
}));
|
|
11276
12341
|
}
|
|
11277
12342
|
return data.uri;
|
|
11278
12343
|
}
|
|
@@ -11282,7 +12347,11 @@ function buildNeonProvisionResult(projectId, connectionUri, resourceStatus) {
|
|
|
11282
12347
|
provisionerName: "neon",
|
|
11283
12348
|
resourceStatus,
|
|
11284
12349
|
envVars: {
|
|
11285
|
-
HAI_DB: JSON.stringify({
|
|
12350
|
+
HAI_DB: JSON.stringify({
|
|
12351
|
+
type: "postgresql",
|
|
12352
|
+
url: connectionUri,
|
|
12353
|
+
database: decodeURIComponent(new URL(connectionUri).pathname.slice(1))
|
|
12354
|
+
})
|
|
11286
12355
|
},
|
|
11287
12356
|
resourceInfo: `neon-project:${projectId}`
|
|
11288
12357
|
};
|
|
@@ -11297,7 +12366,9 @@ function createNeonProvisioner() {
|
|
|
11297
12366
|
try {
|
|
11298
12367
|
const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
|
|
11299
12368
|
if (!apiToken) {
|
|
11300
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12369
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12370
|
+
params: { fields: "token" }
|
|
12371
|
+
}));
|
|
11301
12372
|
}
|
|
11302
12373
|
await listNeonProjects(apiToken);
|
|
11303
12374
|
token = apiToken;
|
|
@@ -11308,7 +12379,9 @@ function createNeonProvisioner() {
|
|
|
11308
12379
|
return err(
|
|
11309
12380
|
HaiDeployError.AUTH_FAILED,
|
|
11310
12381
|
deployM("deploy_authFailed", {
|
|
11311
|
-
params: {
|
|
12382
|
+
params: {
|
|
12383
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12384
|
+
}
|
|
11312
12385
|
}),
|
|
11313
12386
|
error
|
|
11314
12387
|
);
|
|
@@ -11349,7 +12422,9 @@ function createNeonProvisioner() {
|
|
|
11349
12422
|
resource = { resourceInfo: `neon-project:${projectId}`, resourceStatus: "created" };
|
|
11350
12423
|
const connectionUri = data.connection_uris?.[0]?.connection_uri ?? "";
|
|
11351
12424
|
if (!projectId || !connectionUri) {
|
|
11352
|
-
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12425
|
+
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12426
|
+
params: { service: "Neon" }
|
|
12427
|
+
}));
|
|
11353
12428
|
}
|
|
11354
12429
|
logger8.info("Neon database provisioned", { projectId });
|
|
11355
12430
|
return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
|
|
@@ -11359,7 +12434,9 @@ function createNeonProvisioner() {
|
|
|
11359
12434
|
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
11360
12435
|
projectName: appName,
|
|
11361
12436
|
stage: "provision:db",
|
|
11362
|
-
resources: [
|
|
12437
|
+
resources: [
|
|
12438
|
+
{ serviceType: "db", provisionerName: "neon", ...resource }
|
|
12439
|
+
]
|
|
11363
12440
|
});
|
|
11364
12441
|
}
|
|
11365
12442
|
return err(
|
|
@@ -11392,13 +12469,20 @@ function createR2Provisioner() {
|
|
|
11392
12469
|
const acctId = credentials.accountId ?? credentials.account_id ?? "";
|
|
11393
12470
|
const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
|
|
11394
12471
|
if (!acctId || !apiTok || !credentials.accessKeyId || !credentials.secretAccessKey) {
|
|
11395
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12472
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12473
|
+
params: { fields: "accountId, apiToken, accessKeyId, secretAccessKey" }
|
|
12474
|
+
}));
|
|
11396
12475
|
}
|
|
11397
12476
|
const res = await fetch(`${CF_API}/accounts/${acctId}/r2/buckets`, {
|
|
11398
12477
|
headers: { Authorization: `Bearer ${apiTok}` }
|
|
11399
12478
|
});
|
|
11400
12479
|
if (!res.ok) {
|
|
11401
|
-
throw new Error(deployM("deploy_apiError", {
|
|
12480
|
+
throw new Error(deployM("deploy_apiError", {
|
|
12481
|
+
params: {
|
|
12482
|
+
service: "Cloudflare",
|
|
12483
|
+
status: String(res.status)
|
|
12484
|
+
}
|
|
12485
|
+
}));
|
|
11402
12486
|
}
|
|
11403
12487
|
accountId = acctId;
|
|
11404
12488
|
token = apiTok;
|
|
@@ -11411,7 +12495,9 @@ function createR2Provisioner() {
|
|
|
11411
12495
|
return err(
|
|
11412
12496
|
HaiDeployError.AUTH_FAILED,
|
|
11413
12497
|
deployM("deploy_authFailed", {
|
|
11414
|
-
params: {
|
|
12498
|
+
params: {
|
|
12499
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12500
|
+
}
|
|
11415
12501
|
}),
|
|
11416
12502
|
error
|
|
11417
12503
|
);
|
|
@@ -11439,7 +12525,12 @@ function createR2Provisioner() {
|
|
|
11439
12525
|
}
|
|
11440
12526
|
);
|
|
11441
12527
|
if (!createRes.ok && createRes.status !== 409) {
|
|
11442
|
-
throw new Error(deployM("deploy_apiError", {
|
|
12528
|
+
throw new Error(deployM("deploy_apiError", {
|
|
12529
|
+
params: {
|
|
12530
|
+
service: "Cloudflare",
|
|
12531
|
+
status: String(createRes.status)
|
|
12532
|
+
}
|
|
12533
|
+
}));
|
|
11443
12534
|
}
|
|
11444
12535
|
logger9.info("R2 bucket provisioned", { bucketName });
|
|
11445
12536
|
return ok({
|
|
@@ -11487,13 +12578,20 @@ function createResendProvisioner() {
|
|
|
11487
12578
|
try {
|
|
11488
12579
|
const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
11489
12580
|
if (!apiToken || !credentials.from) {
|
|
11490
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12581
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12582
|
+
params: { fields: "apiKey, from" }
|
|
12583
|
+
}));
|
|
11491
12584
|
}
|
|
11492
12585
|
const res = await fetch(`${RESEND_API}/domains`, {
|
|
11493
12586
|
headers: { Authorization: `Bearer ${apiToken}` }
|
|
11494
12587
|
});
|
|
11495
12588
|
if (!res.ok) {
|
|
11496
|
-
throw new Error(deployM("deploy_apiError", {
|
|
12589
|
+
throw new Error(deployM("deploy_apiError", {
|
|
12590
|
+
params: {
|
|
12591
|
+
service: "Resend",
|
|
12592
|
+
status: String(res.status)
|
|
12593
|
+
}
|
|
12594
|
+
}));
|
|
11497
12595
|
}
|
|
11498
12596
|
token = apiToken;
|
|
11499
12597
|
from = credentials.from;
|
|
@@ -11504,7 +12602,9 @@ function createResendProvisioner() {
|
|
|
11504
12602
|
return err(
|
|
11505
12603
|
HaiDeployError.AUTH_FAILED,
|
|
11506
12604
|
deployM("deploy_authFailed", {
|
|
11507
|
-
params: {
|
|
12605
|
+
params: {
|
|
12606
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12607
|
+
}
|
|
11508
12608
|
}),
|
|
11509
12609
|
error
|
|
11510
12610
|
);
|
|
@@ -11523,7 +12623,18 @@ function createResendProvisioner() {
|
|
|
11523
12623
|
provisionerName: "resend",
|
|
11524
12624
|
resourceStatus: "reused",
|
|
11525
12625
|
envVars: {
|
|
11526
|
-
HAI_REACH_PROVIDERS: JSON.stringify([
|
|
12626
|
+
HAI_REACH_PROVIDERS: JSON.stringify([
|
|
12627
|
+
{
|
|
12628
|
+
name: "email",
|
|
12629
|
+
type: "smtp",
|
|
12630
|
+
host: "smtp.resend.com",
|
|
12631
|
+
port: 465,
|
|
12632
|
+
secure: true,
|
|
12633
|
+
user: "resend",
|
|
12634
|
+
pass: token,
|
|
12635
|
+
from
|
|
12636
|
+
}
|
|
12637
|
+
])
|
|
11527
12638
|
},
|
|
11528
12639
|
resourceInfo: "resend:verify-only"
|
|
11529
12640
|
});
|
|
@@ -11545,7 +12656,12 @@ async function upstashFetch(email, apiKey, path, options) {
|
|
|
11545
12656
|
}
|
|
11546
12657
|
});
|
|
11547
12658
|
if (!response.ok) {
|
|
11548
|
-
throw new Error(deployM("deploy_apiError", {
|
|
12659
|
+
throw new Error(deployM("deploy_apiError", {
|
|
12660
|
+
params: {
|
|
12661
|
+
service: "Upstash",
|
|
12662
|
+
status: String(response.status)
|
|
12663
|
+
}
|
|
12664
|
+
}));
|
|
11549
12665
|
}
|
|
11550
12666
|
return response.json();
|
|
11551
12667
|
}
|
|
@@ -11562,7 +12678,9 @@ async function getUpstashDatabase(email, apiKey, databaseId) {
|
|
|
11562
12678
|
function buildUpstashProvisionResult(database, resourceStatus) {
|
|
11563
12679
|
const databaseId = database.database_id ?? "";
|
|
11564
12680
|
if (!databaseId || !database.endpoint || !database.password || !database.port) {
|
|
11565
|
-
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12681
|
+
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12682
|
+
params: { service: "Upstash" }
|
|
12683
|
+
}));
|
|
11566
12684
|
}
|
|
11567
12685
|
return {
|
|
11568
12686
|
serviceType: "cache",
|
|
@@ -11586,7 +12704,9 @@ function createUpstashProvisioner() {
|
|
|
11586
12704
|
const userEmail = credentials.email ?? "";
|
|
11587
12705
|
const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
11588
12706
|
if (!userEmail || !userKey) {
|
|
11589
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12707
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12708
|
+
params: { fields: "email, api_key" }
|
|
12709
|
+
}));
|
|
11590
12710
|
}
|
|
11591
12711
|
await listUpstashDatabases(userEmail, userKey);
|
|
11592
12712
|
email = userEmail;
|
|
@@ -11598,7 +12718,9 @@ function createUpstashProvisioner() {
|
|
|
11598
12718
|
return err(
|
|
11599
12719
|
HaiDeployError.AUTH_FAILED,
|
|
11600
12720
|
deployM("deploy_authFailed", {
|
|
11601
|
-
params: {
|
|
12721
|
+
params: {
|
|
12722
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12723
|
+
}
|
|
11602
12724
|
}),
|
|
11603
12725
|
error
|
|
11604
12726
|
);
|
|
@@ -11619,12 +12741,17 @@ function createUpstashProvisioner() {
|
|
|
11619
12741
|
if (existingDatabase !== null) {
|
|
11620
12742
|
const databaseId = existingDatabase.database_id ?? "";
|
|
11621
12743
|
if (!databaseId) {
|
|
11622
|
-
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12744
|
+
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12745
|
+
params: { service: "Upstash" }
|
|
12746
|
+
}));
|
|
11623
12747
|
}
|
|
11624
12748
|
resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
|
|
11625
12749
|
const detail = await getUpstashDatabase(email, apiKey, databaseId);
|
|
11626
12750
|
logger11.info("Upstash Redis reused", { databaseId, databaseName });
|
|
11627
|
-
return ok(buildUpstashProvisionResult({
|
|
12751
|
+
return ok(buildUpstashProvisionResult({
|
|
12752
|
+
...existingDatabase,
|
|
12753
|
+
...detail
|
|
12754
|
+
}, "reused"));
|
|
11628
12755
|
}
|
|
11629
12756
|
const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
|
|
11630
12757
|
method: "POST",
|
|
@@ -11644,7 +12771,9 @@ function createUpstashProvisioner() {
|
|
|
11644
12771
|
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
11645
12772
|
projectName: appName,
|
|
11646
12773
|
stage: "provision:cache",
|
|
11647
|
-
resources: [
|
|
12774
|
+
resources: [
|
|
12775
|
+
{ serviceType: "cache", provisionerName: "upstash", ...resource }
|
|
12776
|
+
]
|
|
11648
12777
|
});
|
|
11649
12778
|
}
|
|
11650
12779
|
return err(
|
|
@@ -11678,7 +12807,9 @@ function createProviderByType(type) {
|
|
|
11678
12807
|
case "vercel":
|
|
11679
12808
|
return createVercelProvider();
|
|
11680
12809
|
default:
|
|
11681
|
-
throw new Error(deployM("deploy_unsupportedProviderType", {
|
|
12810
|
+
throw new Error(deployM("deploy_unsupportedProviderType", {
|
|
12811
|
+
params: { type }
|
|
12812
|
+
}));
|
|
11682
12813
|
}
|
|
11683
12814
|
}
|
|
11684
12815
|
function createProvisionerByName(provisioner) {
|
|
@@ -11694,7 +12825,9 @@ function createProvisionerByName(provisioner) {
|
|
|
11694
12825
|
case "aliyun":
|
|
11695
12826
|
return createAliyunProvisioner();
|
|
11696
12827
|
default:
|
|
11697
|
-
throw new Error(deployM("deploy_unsupportedProvisionerType", {
|
|
12828
|
+
throw new Error(deployM("deploy_unsupportedProvisionerType", {
|
|
12829
|
+
params: { type: provisioner }
|
|
12830
|
+
}));
|
|
11698
12831
|
}
|
|
11699
12832
|
}
|
|
11700
12833
|
function extractCredentials(serviceConfig) {
|
|
@@ -11727,8 +12860,12 @@ function mergeProvisionEnv(results, scan, options) {
|
|
|
11727
12860
|
return item.service === prov.serviceType && (!mode || mode === "default");
|
|
11728
12861
|
});
|
|
11729
12862
|
if (targets.length) {
|
|
11730
|
-
for (const target of targets)
|
|
11731
|
-
reachProviders[reachRequirements.indexOf(target)] = {
|
|
12863
|
+
for (const target of targets) {
|
|
12864
|
+
reachProviders[reachRequirements.indexOf(target)] = {
|
|
12865
|
+
...mergeManagedMiddlewareConfig(target, provider),
|
|
12866
|
+
name: target.config.name ?? provider.name
|
|
12867
|
+
};
|
|
12868
|
+
}
|
|
11732
12869
|
} else {
|
|
11733
12870
|
reachProviders.push(provider);
|
|
11734
12871
|
}
|
|
@@ -11750,7 +12887,9 @@ var deploy = {
|
|
|
11750
12887
|
logger12.warn("Deploy init already in progress, skipping concurrent call");
|
|
11751
12888
|
return err(
|
|
11752
12889
|
HaiDeployError.CONFIG_ERROR,
|
|
11753
|
-
deployM("deploy_configError", {
|
|
12890
|
+
deployM("deploy_configError", {
|
|
12891
|
+
params: { error: "init already in progress" }
|
|
12892
|
+
})
|
|
11754
12893
|
);
|
|
11755
12894
|
}
|
|
11756
12895
|
initInProgress = true;
|
|
@@ -11765,7 +12904,9 @@ var deploy = {
|
|
|
11765
12904
|
logger12.error("Deploy config validation failed", { error: parseResult.error.message });
|
|
11766
12905
|
return err(
|
|
11767
12906
|
HaiDeployError.CONFIG_ERROR,
|
|
11768
|
-
deployM("deploy_configError", {
|
|
12907
|
+
deployM("deploy_configError", {
|
|
12908
|
+
params: { error: parseResult.error.message }
|
|
12909
|
+
}),
|
|
11769
12910
|
parseResult.error
|
|
11770
12911
|
);
|
|
11771
12912
|
}
|
|
@@ -11813,7 +12954,9 @@ var deploy = {
|
|
|
11813
12954
|
currentConfig = parsed;
|
|
11814
12955
|
logger12.info("Deploy module initialized", {
|
|
11815
12956
|
provider: parsed.provider.type,
|
|
11816
|
-
provisioners: [
|
|
12957
|
+
provisioners: [
|
|
12958
|
+
...currentProvisioners.keys()
|
|
12959
|
+
]
|
|
11817
12960
|
});
|
|
11818
12961
|
return ok(void 0);
|
|
11819
12962
|
} catch (error) {
|
|
@@ -11825,7 +12968,9 @@ var deploy = {
|
|
|
11825
12968
|
return err(
|
|
11826
12969
|
HaiDeployError.CONFIG_ERROR,
|
|
11827
12970
|
deployM("deploy_configError", {
|
|
11828
|
-
params: {
|
|
12971
|
+
params: {
|
|
12972
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12973
|
+
}
|
|
11829
12974
|
}),
|
|
11830
12975
|
error
|
|
11831
12976
|
);
|
|
@@ -11879,7 +13024,12 @@ var deploy = {
|
|
|
11879
13024
|
resources: [
|
|
11880
13025
|
...results.map(({ envVars: _envVars, ...resource }) => resource),
|
|
11881
13026
|
...getDeployRecovery(provResult.error)?.resources ?? [
|
|
11882
|
-
{
|
|
13027
|
+
{
|
|
13028
|
+
serviceType,
|
|
13029
|
+
provisionerName: provisioner.name,
|
|
13030
|
+
resourceInfo: projectName,
|
|
13031
|
+
resourceStatus: "unknown"
|
|
13032
|
+
}
|
|
11883
13033
|
]
|
|
11884
13034
|
]
|
|
11885
13035
|
});
|
|
@@ -11917,7 +13067,9 @@ var deploy = {
|
|
|
11917
13067
|
connectionVariables.add(match[1]);
|
|
11918
13068
|
}
|
|
11919
13069
|
} catch {
|
|
11920
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
13070
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
13071
|
+
params: { path: projectFile, line: "1" }
|
|
13072
|
+
}));
|
|
11921
13073
|
}
|
|
11922
13074
|
}
|
|
11923
13075
|
const appEnv = Object.fromEntries(Object.entries(environment.data.values).filter(([key]) => key !== "HAI_DEPLOY" && !connectionVariables.has(key) && !key.startsWith("HAI_CLI_") && (!key.startsWith("HAI_DEPLOY_") || key === "HAI_DEPLOY_ENVIRONMENT")));
|
|
@@ -11925,7 +13077,9 @@ var deploy = {
|
|
|
11925
13077
|
options = {
|
|
11926
13078
|
...options,
|
|
11927
13079
|
...scanOptions.environment ? { environment: environment.data.environment } : {},
|
|
11928
|
-
...options?.skipMiddleware ? {
|
|
13080
|
+
...options?.skipMiddleware ? {
|
|
13081
|
+
serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"]))
|
|
13082
|
+
} : {}
|
|
11929
13083
|
};
|
|
11930
13084
|
if (options.selectedServices) {
|
|
11931
13085
|
options.serviceModes = {
|
|
@@ -11961,7 +13115,10 @@ var deploy = {
|
|
|
11961
13115
|
return err(provResults.error);
|
|
11962
13116
|
}
|
|
11963
13117
|
recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
|
|
11964
|
-
allEnvVars = {
|
|
13118
|
+
allEnvVars = {
|
|
13119
|
+
...allEnvVars,
|
|
13120
|
+
...mergeProvisionEnv(provResults.data, scan, options)
|
|
13121
|
+
};
|
|
11965
13122
|
if (options?.environment) {
|
|
11966
13123
|
const saved = saveDeploymentEnvironment(environment.data, mergeProvisionEnv(provResults.data, scan, options));
|
|
11967
13124
|
if (!saved.success)
|
|
@@ -12037,7 +13194,16 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
|
|
|
12037
13194
|
}
|
|
12038
13195
|
provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
|
|
12039
13196
|
}
|
|
12040
|
-
return provider.deploy({
|
|
13197
|
+
return provider.deploy({
|
|
13198
|
+
appDir,
|
|
13199
|
+
artifact,
|
|
13200
|
+
artifactDir,
|
|
13201
|
+
projectName,
|
|
13202
|
+
scan,
|
|
13203
|
+
provisionEnv,
|
|
13204
|
+
appEnv,
|
|
13205
|
+
options
|
|
13206
|
+
});
|
|
12041
13207
|
}
|
|
12042
13208
|
z.enum(["sqlite", "postgresql", "mysql"]);
|
|
12043
13209
|
var ReldbOperationLogConfigSchema = z.object({
|
|
@@ -12070,7 +13236,12 @@ var MysqlPoolConfigSchema = z.object({
|
|
|
12070
13236
|
}).strict();
|
|
12071
13237
|
var SslConfigSchema = z.union([
|
|
12072
13238
|
z.boolean(),
|
|
12073
|
-
z.enum([
|
|
13239
|
+
z.enum([
|
|
13240
|
+
"require",
|
|
13241
|
+
"prefer",
|
|
13242
|
+
"allow",
|
|
13243
|
+
"disable"
|
|
13244
|
+
]),
|
|
12074
13245
|
z.record(z.string(), z.unknown())
|
|
12075
13246
|
]);
|
|
12076
13247
|
var MysqlSslConfigSchema = z.union([
|
|
@@ -12086,7 +13257,9 @@ var MysqlSslConfigSchema = z.union([
|
|
|
12086
13257
|
cert: z.string().min(1).optional(),
|
|
12087
13258
|
/** MySQL 客户端私钥,PEM 内容;必须同时提供 cert */
|
|
12088
13259
|
key: z.string().min(1).optional()
|
|
12089
|
-
}).strict().refine((ssl) => Boolean(ssl.cert) === Boolean(ssl.key), {
|
|
13260
|
+
}).strict().refine((ssl) => Boolean(ssl.cert) === Boolean(ssl.key), {
|
|
13261
|
+
path: ["key"]
|
|
13262
|
+
})
|
|
12090
13263
|
]);
|
|
12091
13264
|
var SqliteOptionsSchema = z.object({
|
|
12092
13265
|
/** 是否启用 WAL 模式(默认 true),可提高并发读写性能 */
|
|
@@ -12148,7 +13321,10 @@ var PostgresqlConfigSchema = NetworkConfigSchema.extend({
|
|
|
12148
13321
|
var MysqlConfigSchema = NetworkConfigSchema.extend({
|
|
12149
13322
|
type: z.literal("mysql"),
|
|
12150
13323
|
/** 网络数据库URL;MySQL空串表示使用分字段,非空仅mysql://且拒绝query/fragment,优先于host/port/database/user/password */
|
|
12151
|
-
url: z.union([
|
|
13324
|
+
url: z.union([
|
|
13325
|
+
z.literal("").transform(() => void 0),
|
|
13326
|
+
z.url({ protocol: /^mysql$/ }).refine((value) => !value.includes("?") && !value.includes("#"))
|
|
13327
|
+
]).optional(),
|
|
12152
13328
|
/** 数据库:SQLite文件路径或:memory:;PostgreSQL必填名称;MySQL无URL时必填,URL模式从URL读取 */
|
|
12153
13329
|
database: z.string().min(1).optional(),
|
|
12154
13330
|
/** TLS:PostgreSQL使用其模式;MySQL仅false或严格对象,空对象默认校验证书链和DNS身份 */
|
|
@@ -12158,7 +13334,9 @@ var MysqlConfigSchema = NetworkConfigSchema.extend({
|
|
|
12158
13334
|
/** MySQL 特定选项 */
|
|
12159
13335
|
mysql: MysqlOptionsSchema.optional(),
|
|
12160
13336
|
...OperationLogConfigFieldSchema
|
|
12161
|
-
}).refine((config) => Boolean(config.url) || Boolean(config.database), {
|
|
13337
|
+
}).refine((config) => Boolean(config.url) || Boolean(config.database), {
|
|
13338
|
+
path: ["database"]
|
|
13339
|
+
});
|
|
12162
13340
|
var ReldbConfigSchema = z.discriminatedUnion("type", [
|
|
12163
13341
|
SqliteConfigSchema,
|
|
12164
13342
|
PostgresqlConfigSchema,
|
|
@@ -12168,8 +13346,12 @@ function canKeepMiddleware(requirement, backend, environment) {
|
|
|
12168
13346
|
const privatePath = join(environment.envDir, `.env.${environment.environment}.local`);
|
|
12169
13347
|
let targetValues = environment.targetValues;
|
|
12170
13348
|
try {
|
|
12171
|
-
if (existsSync(privatePath))
|
|
12172
|
-
targetValues = core.config.mergeEnv([
|
|
13349
|
+
if (existsSync(privatePath)) {
|
|
13350
|
+
targetValues = core.config.mergeEnv([
|
|
13351
|
+
targetValues,
|
|
13352
|
+
core.config.parseEnv(readLocalFile(privatePath).toString("utf8"))
|
|
13353
|
+
]);
|
|
13354
|
+
}
|
|
12173
13355
|
} catch {
|
|
12174
13356
|
return false;
|
|
12175
13357
|
}
|
|
@@ -12179,19 +13361,31 @@ function canKeepMiddleware(requirement, backend, environment) {
|
|
|
12179
13361
|
function empty(value2) {
|
|
12180
13362
|
if (Array.isArray(value2))
|
|
12181
13363
|
return value2.map(empty);
|
|
12182
|
-
if (value2 && typeof value2 === "object")
|
|
12183
|
-
return Object.fromEntries(Object.entries(value2).map(([name, child]) => [
|
|
13364
|
+
if (value2 && typeof value2 === "object") {
|
|
13365
|
+
return Object.fromEntries(Object.entries(value2).map(([name, child]) => [
|
|
13366
|
+
name,
|
|
13367
|
+
empty(child)
|
|
13368
|
+
]));
|
|
13369
|
+
}
|
|
12184
13370
|
return void 0;
|
|
12185
13371
|
}
|
|
12186
13372
|
let seed = empty(requirement.config);
|
|
12187
|
-
for (const part of [...requirement.configPath].reverse())
|
|
13373
|
+
for (const part of [...requirement.configPath].reverse()) {
|
|
12188
13374
|
seed = /^\d+$/.test(part) ? Array.from({ length: Number(part) + 1 }, (_, index) => index === Number(part) ? seed : null) : { [part]: seed };
|
|
12189
|
-
|
|
13375
|
+
}
|
|
13376
|
+
const resolved = core.config.resolve({
|
|
13377
|
+
name: requirement.configName,
|
|
13378
|
+
value: seed,
|
|
13379
|
+
environment: targetValues,
|
|
13380
|
+
shape: structures[requirement.configName],
|
|
13381
|
+
missing: "empty"
|
|
13382
|
+
});
|
|
12190
13383
|
if (!resolved.success)
|
|
12191
13384
|
return false;
|
|
12192
13385
|
let value = resolved.data;
|
|
12193
|
-
for (const part of requirement.configPath)
|
|
13386
|
+
for (const part of requirement.configPath) {
|
|
12194
13387
|
value = value && typeof value === "object" ? value[part] : void 0;
|
|
13388
|
+
}
|
|
12195
13389
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12196
13390
|
return false;
|
|
12197
13391
|
const target = value;
|
|
@@ -12208,30 +13402,39 @@ function validateMiddlewareField(options) {
|
|
|
12208
13402
|
}
|
|
12209
13403
|
function middlewareFieldProblem(options) {
|
|
12210
13404
|
const { value, field: field2, backend } = options;
|
|
12211
|
-
if (value === "")
|
|
13405
|
+
if (value === "") {
|
|
12212
13406
|
return field2.required ? deployM("deploy_middlewareRequired") : void 0;
|
|
12213
|
-
|
|
12214
|
-
|
|
13407
|
+
}
|
|
13408
|
+
if (field2.kind === "port") {
|
|
13409
|
+
return /^\d+$/.test(value) && Number(value) >= 1 && Number(value) <= 65535 ? void 0 : deployM("deploy_middlewareInteger", {
|
|
13410
|
+
params: { min: 1, max: 65535 }
|
|
13411
|
+
});
|
|
13412
|
+
}
|
|
12215
13413
|
if (field2.kind === "number") {
|
|
12216
13414
|
const min = field2.minimum ?? 0;
|
|
12217
13415
|
const max = field2.maximum ?? Number.MAX_SAFE_INTEGER;
|
|
12218
|
-
return /^\d+$/.test(value) && Number(value) >= min && Number(value) <= max ? void 0 : deployM("deploy_middlewareInteger", {
|
|
13416
|
+
return /^\d+$/.test(value) && Number(value) >= min && Number(value) <= max ? void 0 : deployM("deploy_middlewareInteger", {
|
|
13417
|
+
params: { min, max }
|
|
13418
|
+
});
|
|
12219
13419
|
}
|
|
12220
|
-
if (field2.kind === "boolean")
|
|
13420
|
+
if (field2.kind === "boolean") {
|
|
12221
13421
|
return ["true", "false"].includes(value) ? void 0 : deployM("deploy_middlewareBoolean");
|
|
13422
|
+
}
|
|
12222
13423
|
if (backend === "s3" && field2.name === "endpoint") {
|
|
12223
13424
|
const endpoint = URL.parse(value);
|
|
12224
13425
|
return endpoint?.hostname && ["http:", "https:"].includes(endpoint.protocol) ? void 0 : deployM("deploy_middlewareS3Endpoint");
|
|
12225
13426
|
}
|
|
12226
|
-
if (field2.kind === "url")
|
|
13427
|
+
if (field2.kind === "url") {
|
|
12227
13428
|
return URL.canParse(value) ? void 0 : deployM("deploy_middlewareUrl");
|
|
13429
|
+
}
|
|
12228
13430
|
if (field2.name === "ssl") {
|
|
12229
13431
|
try {
|
|
12230
13432
|
const document = parseDocument(value);
|
|
12231
13433
|
if (document.errors.length)
|
|
12232
13434
|
return deployM("deploy_middlewareTlsSyntax");
|
|
12233
|
-
if (!(backend === "mysql" ? MysqlSslConfigSchema : SslConfigSchema).safeParse(document.toJS()).success)
|
|
13435
|
+
if (!(backend === "mysql" ? MysqlSslConfigSchema : SslConfigSchema).safeParse(document.toJS()).success) {
|
|
12234
13436
|
return deployM(backend === "mysql" ? "deploy_middlewareMysqlTls" : "deploy_middlewarePostgresTls");
|
|
13437
|
+
}
|
|
12235
13438
|
} catch {
|
|
12236
13439
|
return deployM("deploy_middlewareTlsSyntax");
|
|
12237
13440
|
}
|
|
@@ -12253,8 +13456,11 @@ function parseMiddlewareField(options) {
|
|
|
12253
13456
|
if (options.managed && (options.field.secret || options.field.managed) && options.value === "")
|
|
12254
13457
|
return ok(void 0);
|
|
12255
13458
|
const problem = middlewareFieldProblem(options);
|
|
12256
|
-
if (problem)
|
|
12257
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_middlewareFieldError", {
|
|
13459
|
+
if (problem) {
|
|
13460
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_middlewareFieldError", {
|
|
13461
|
+
params: { field: options.field.name, reason: problem }
|
|
13462
|
+
}));
|
|
13463
|
+
}
|
|
12258
13464
|
const { value, field: field2 } = options;
|
|
12259
13465
|
if (value === "")
|
|
12260
13466
|
return ok(void 0);
|
|
@@ -12272,8 +13478,13 @@ function saveMiddlewareEnvironment(environment, options) {
|
|
|
12272
13478
|
const configuration = requirement.service === "ldap" ? options.configuration : { ...options.configuration, type: backend };
|
|
12273
13479
|
if (requirement.service === "db") {
|
|
12274
13480
|
const validated = ReldbConfigSchema.safeParse(configuration);
|
|
12275
|
-
if (!validated.success)
|
|
12276
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_middlewareFieldInvalid", {
|
|
13481
|
+
if (!validated.success) {
|
|
13482
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_middlewareFieldInvalid", {
|
|
13483
|
+
params: {
|
|
13484
|
+
fields: validated.error.issues.map((issue) => `db.${issue.path.join(".")}`).join(", ")
|
|
13485
|
+
}
|
|
13486
|
+
}));
|
|
13487
|
+
}
|
|
12277
13488
|
}
|
|
12278
13489
|
if (requirement.service === "ldap") {
|
|
12279
13490
|
updates.HAI_IAM_LOGIN_LDAP = backend !== "disabled";
|
|
@@ -12287,17 +13498,26 @@ function saveMiddlewareEnvironment(environment, options) {
|
|
|
12287
13498
|
mkdirSync(directory, { recursive: true });
|
|
12288
13499
|
const path = join(directory, `_${requirement.configName}.yml`);
|
|
12289
13500
|
const lineCounter = new LineCounter();
|
|
12290
|
-
const document = parseDocument(existsSync(path) ?
|
|
12291
|
-
if (document.errors.length)
|
|
12292
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
12293
|
-
|
|
13501
|
+
const document = parseDocument(existsSync(path) ? readLocalFile(path).toString("utf8") : "{}\n", { lineCounter, prettyErrors: false });
|
|
13502
|
+
if (document.errors.length) {
|
|
13503
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
13504
|
+
params: {
|
|
13505
|
+
path,
|
|
13506
|
+
line: String(lineCounter.linePos(document.errors[0].pos[0]).line)
|
|
13507
|
+
}
|
|
13508
|
+
}));
|
|
13509
|
+
}
|
|
13510
|
+
if (backend !== "disabled" && requirement.configPath.length && !document.hasIn(requirement.configPath)) {
|
|
12294
13511
|
document.setIn(requirement.configPath, document.createNode(requirement.config.name ? { name: requirement.config.name, type: backend } : {}));
|
|
13512
|
+
}
|
|
12295
13513
|
if (requirement.service === "ldap" && !document.hasIn(["login", "ldap"]))
|
|
12296
13514
|
document.setIn(["login", "ldap"], false);
|
|
12297
13515
|
const content = document.toString();
|
|
12298
|
-
if (!existsSync(path) ||
|
|
12299
|
-
|
|
12300
|
-
return saveDeploymentEnvironment(environment, updates, {
|
|
13516
|
+
if (!existsSync(path) || readLocalFile(path).toString("utf8") !== content)
|
|
13517
|
+
writeLocalFile(path, content);
|
|
13518
|
+
return saveDeploymentEnvironment(environment, updates, {
|
|
13519
|
+
replace: options.configuration && backend !== "disabled" ? [requirement.envPrefix] : []
|
|
13520
|
+
});
|
|
12301
13521
|
} catch {
|
|
12302
13522
|
return err(HaiDeployError.ENV_VAR_FAILED, deployM("deploy_environmentWriteFailed"));
|
|
12303
13523
|
}
|
|
@@ -12305,46 +13525,253 @@ function saveMiddlewareEnvironment(environment, options) {
|
|
|
12305
13525
|
|
|
12306
13526
|
// src/deploy-middleware.ts
|
|
12307
13527
|
var field = (name, defaultValue, kind, secret = false) => ({ name, defaultValue, kind, secret });
|
|
12308
|
-
|
|
13528
|
+
function required(name, defaultValue, kind) {
|
|
13529
|
+
return {
|
|
13530
|
+
...field(name, defaultValue, kind),
|
|
13531
|
+
required: true
|
|
13532
|
+
};
|
|
13533
|
+
}
|
|
12309
13534
|
var DEPLOY_MIDDLEWARE = {
|
|
12310
13535
|
db: [
|
|
12311
|
-
{
|
|
13536
|
+
{
|
|
13537
|
+
backend: "sqlite",
|
|
13538
|
+
label: "SQLite",
|
|
13539
|
+
purpose: "deploy_purposeSqlite",
|
|
13540
|
+
fields: [
|
|
13541
|
+
required("database", "./data/app.db")
|
|
13542
|
+
]
|
|
13543
|
+
},
|
|
12312
13544
|
...["postgresql", "mysql"].map((backend) => ({
|
|
12313
13545
|
backend,
|
|
12314
13546
|
label: backend === "mysql" ? "MySQL" : "PostgreSQL",
|
|
12315
13547
|
purpose: "deploy_purposeDatabase",
|
|
12316
13548
|
provisioner: backend === "postgresql" ? "neon" : void 0,
|
|
12317
13549
|
cloudFields: backend === "postgresql" ? provisionerFields("neon") : void 0,
|
|
12318
|
-
fields: [
|
|
13550
|
+
fields: [
|
|
13551
|
+
field("url", "", "url", true),
|
|
13552
|
+
required("host", "localhost"),
|
|
13553
|
+
required("port", backend === "mysql" ? "3306" : "5432", "port"),
|
|
13554
|
+
required("database", "hai"),
|
|
13555
|
+
field("user", "hai"),
|
|
13556
|
+
field("password", "", void 0, true),
|
|
13557
|
+
field("ssl", "false")
|
|
13558
|
+
]
|
|
12319
13559
|
}))
|
|
12320
13560
|
],
|
|
12321
13561
|
cache: [
|
|
12322
|
-
{
|
|
12323
|
-
|
|
13562
|
+
{
|
|
13563
|
+
backend: "memory",
|
|
13564
|
+
label: "Memory",
|
|
13565
|
+
purpose: "deploy_purposeMemory",
|
|
13566
|
+
fields: []
|
|
13567
|
+
},
|
|
13568
|
+
{
|
|
13569
|
+
backend: "redis",
|
|
13570
|
+
label: "Redis",
|
|
13571
|
+
purpose: "deploy_purposeCache",
|
|
13572
|
+
provisioner: "upstash",
|
|
13573
|
+
cloudFields: provisionerFields("upstash"),
|
|
13574
|
+
fields: [
|
|
13575
|
+
field("url", "", "url", true),
|
|
13576
|
+
required("host", "localhost"),
|
|
13577
|
+
required("port", "6379", "port"),
|
|
13578
|
+
field("password", "", void 0, true),
|
|
13579
|
+
{
|
|
13580
|
+
...field("db", "0", "number"),
|
|
13581
|
+
maximum: 15
|
|
13582
|
+
},
|
|
13583
|
+
field("tls", "false", "boolean"),
|
|
13584
|
+
field("keyPrefix"),
|
|
13585
|
+
field("connectTimeout", "10000", "number"),
|
|
13586
|
+
field("commandTimeout", "5000", "number")
|
|
13587
|
+
]
|
|
13588
|
+
}
|
|
12324
13589
|
],
|
|
12325
13590
|
storage: [
|
|
12326
|
-
{
|
|
12327
|
-
|
|
13591
|
+
{
|
|
13592
|
+
backend: "local",
|
|
13593
|
+
label: "Local filesystem",
|
|
13594
|
+
purpose: "deploy_purposeLocal",
|
|
13595
|
+
fields: [
|
|
13596
|
+
required("root", "./data/storage")
|
|
13597
|
+
]
|
|
13598
|
+
},
|
|
13599
|
+
{
|
|
13600
|
+
backend: "s3",
|
|
13601
|
+
label: "S3 / RustFS / MinIO / Cloudflare R2",
|
|
13602
|
+
purpose: "deploy_purposeStorage",
|
|
13603
|
+
provisioner: "cloudflare-r2",
|
|
13604
|
+
cloudFields: provisionerFields("cloudflare-r2"),
|
|
13605
|
+
containerOptions: [
|
|
13606
|
+
{
|
|
13607
|
+
field: "s3Engine",
|
|
13608
|
+
variable: "HAI_DEPLOY_CONTAINER_S3ENGINE",
|
|
13609
|
+
purpose: "deploy_s3Engine",
|
|
13610
|
+
defaultValue: "rustfs",
|
|
13611
|
+
choices: [
|
|
13612
|
+
{ label: "RustFS", value: "rustfs" },
|
|
13613
|
+
{ label: "MinIO", value: "minio" }
|
|
13614
|
+
]
|
|
13615
|
+
}
|
|
13616
|
+
],
|
|
13617
|
+
fields: [
|
|
13618
|
+
field("endpoint", "", "url"),
|
|
13619
|
+
required("bucket", "hai"),
|
|
13620
|
+
required("region", "us-east-1"),
|
|
13621
|
+
{
|
|
13622
|
+
...required("accessKeyId"),
|
|
13623
|
+
managed: true
|
|
13624
|
+
},
|
|
13625
|
+
{
|
|
13626
|
+
...required("secretAccessKey"),
|
|
13627
|
+
secret: true
|
|
13628
|
+
},
|
|
13629
|
+
field("forcePathStyle", "true", "boolean")
|
|
13630
|
+
]
|
|
13631
|
+
}
|
|
12328
13632
|
],
|
|
12329
13633
|
vecdb: [
|
|
12330
|
-
{
|
|
12331
|
-
|
|
12332
|
-
|
|
12333
|
-
|
|
13634
|
+
{
|
|
13635
|
+
backend: "lancedb",
|
|
13636
|
+
label: "LanceDB",
|
|
13637
|
+
purpose: "deploy_purposeLocal",
|
|
13638
|
+
fields: [
|
|
13639
|
+
required("path", "./data/vectors")
|
|
13640
|
+
]
|
|
13641
|
+
},
|
|
13642
|
+
{
|
|
13643
|
+
backend: "qdrant",
|
|
13644
|
+
label: "Qdrant",
|
|
13645
|
+
purpose: "deploy_purposeVector",
|
|
13646
|
+
fields: [
|
|
13647
|
+
required("url", "http://localhost:6333", "url"),
|
|
13648
|
+
field("apiKey", "", void 0, true)
|
|
13649
|
+
]
|
|
13650
|
+
},
|
|
13651
|
+
{
|
|
13652
|
+
backend: "pgvector",
|
|
13653
|
+
label: "PostgreSQL / pgvector",
|
|
13654
|
+
purpose: "deploy_purposeVector",
|
|
13655
|
+
fields: [
|
|
13656
|
+
field("url", "", "url", true),
|
|
13657
|
+
required("host", "localhost"),
|
|
13658
|
+
required("port", "5432", "port"),
|
|
13659
|
+
required("database", "hai"),
|
|
13660
|
+
field("user", "hai"),
|
|
13661
|
+
field("password", "", void 0, true)
|
|
13662
|
+
]
|
|
13663
|
+
},
|
|
13664
|
+
{
|
|
13665
|
+
backend: "chroma",
|
|
13666
|
+
label: "Chroma",
|
|
13667
|
+
purpose: "deploy_purposeVector",
|
|
13668
|
+
fields: [
|
|
13669
|
+
required("url", "http://localhost:8000", "url")
|
|
13670
|
+
]
|
|
13671
|
+
}
|
|
12334
13672
|
],
|
|
12335
13673
|
ldap: [
|
|
12336
|
-
{
|
|
12337
|
-
|
|
13674
|
+
{
|
|
13675
|
+
backend: "disabled",
|
|
13676
|
+
label: "Disabled",
|
|
13677
|
+
purpose: "deploy_purposeDisabled",
|
|
13678
|
+
fields: []
|
|
13679
|
+
},
|
|
13680
|
+
{
|
|
13681
|
+
backend: "ldap",
|
|
13682
|
+
label: "LDAP / OpenLDAP",
|
|
13683
|
+
purpose: "deploy_purposeLdap",
|
|
13684
|
+
fields: [
|
|
13685
|
+
required("url", "ldap://localhost:389", "url"),
|
|
13686
|
+
required("bindDn", "cn=admin,dc=example,dc=org"),
|
|
13687
|
+
{
|
|
13688
|
+
...required("bindPassword"),
|
|
13689
|
+
secret: true
|
|
13690
|
+
},
|
|
13691
|
+
required("searchBase", "dc=example,dc=org"),
|
|
13692
|
+
field("searchFilter", "(uid={{username}})"),
|
|
13693
|
+
field("usernameAttribute", "uid"),
|
|
13694
|
+
field("emailAttribute", "mail"),
|
|
13695
|
+
field("displayNameAttribute", "cn"),
|
|
13696
|
+
field("useTls", "false", "boolean"),
|
|
13697
|
+
{
|
|
13698
|
+
...field("connectTimeout", "5000", "number"),
|
|
13699
|
+
minimum: 1e3
|
|
13700
|
+
}
|
|
13701
|
+
]
|
|
13702
|
+
}
|
|
12338
13703
|
],
|
|
12339
13704
|
email: [
|
|
12340
|
-
{
|
|
12341
|
-
|
|
12342
|
-
|
|
13705
|
+
{
|
|
13706
|
+
backend: "console",
|
|
13707
|
+
label: "Console",
|
|
13708
|
+
purpose: "deploy_purposeConsole",
|
|
13709
|
+
fields: []
|
|
13710
|
+
},
|
|
13711
|
+
{
|
|
13712
|
+
backend: "smtp",
|
|
13713
|
+
label: "SMTP",
|
|
13714
|
+
purpose: "deploy_purposeEmail",
|
|
13715
|
+
provisioner: "resend",
|
|
13716
|
+
cloudFields: provisionerFields("resend"),
|
|
13717
|
+
fields: [
|
|
13718
|
+
required("host", "localhost"),
|
|
13719
|
+
required("port", "465", "port"),
|
|
13720
|
+
field("secure", "true", "boolean"),
|
|
13721
|
+
field("user"),
|
|
13722
|
+
field("pass", "", void 0, true),
|
|
13723
|
+
required("from", "noreply@example.com")
|
|
13724
|
+
]
|
|
13725
|
+
},
|
|
13726
|
+
{
|
|
13727
|
+
backend: "api",
|
|
13728
|
+
containerSupported: false,
|
|
13729
|
+
label: "HTTP notification API",
|
|
13730
|
+
purpose: "deploy_purposeEmail",
|
|
13731
|
+
fields: [
|
|
13732
|
+
required("url", "", "url"),
|
|
13733
|
+
field("method", "POST"),
|
|
13734
|
+
field("headers", "", void 0, true),
|
|
13735
|
+
field("timeout", "10000", "number")
|
|
13736
|
+
]
|
|
13737
|
+
}
|
|
12343
13738
|
],
|
|
12344
13739
|
sms: [
|
|
12345
|
-
{
|
|
12346
|
-
|
|
12347
|
-
|
|
13740
|
+
{
|
|
13741
|
+
backend: "console",
|
|
13742
|
+
label: "Console",
|
|
13743
|
+
purpose: "deploy_purposeConsole",
|
|
13744
|
+
fields: []
|
|
13745
|
+
},
|
|
13746
|
+
{
|
|
13747
|
+
backend: "aliyun-sms",
|
|
13748
|
+
containerSupported: false,
|
|
13749
|
+
label: "Aliyun SMS",
|
|
13750
|
+
purpose: "deploy_purposeSms",
|
|
13751
|
+
provisioner: "aliyun",
|
|
13752
|
+
cloudFields: provisionerFields("aliyun"),
|
|
13753
|
+
fields: [
|
|
13754
|
+
required("accessKeyId"),
|
|
13755
|
+
{
|
|
13756
|
+
...required("accessKeySecret"),
|
|
13757
|
+
secret: true
|
|
13758
|
+
},
|
|
13759
|
+
required("signName"),
|
|
13760
|
+
field("endpoint", "dysmsapi.aliyuncs.com")
|
|
13761
|
+
]
|
|
13762
|
+
},
|
|
13763
|
+
{
|
|
13764
|
+
backend: "api",
|
|
13765
|
+
containerSupported: false,
|
|
13766
|
+
label: "HTTP notification API",
|
|
13767
|
+
purpose: "deploy_purposeSms",
|
|
13768
|
+
fields: [
|
|
13769
|
+
required("url", "", "url"),
|
|
13770
|
+
field("method", "POST"),
|
|
13771
|
+
field("headers", "", void 0, true),
|
|
13772
|
+
field("timeout", "10000", "number")
|
|
13773
|
+
]
|
|
13774
|
+
}
|
|
12348
13775
|
]
|
|
12349
13776
|
};
|
|
12350
13777
|
function describeMiddlewareBackend(backend) {
|
|
@@ -12354,8 +13781,15 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12354
13781
|
if ("projectName" in options && options.projectName !== void 0)
|
|
12355
13782
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
|
|
12356
13783
|
const parsed = DeployConfigSchema.safeParse(input);
|
|
12357
|
-
if (!parsed.success)
|
|
12358
|
-
return err({
|
|
13784
|
+
if (!parsed.success) {
|
|
13785
|
+
return err({
|
|
13786
|
+
...HaiDeployError.CONFIG_ERROR,
|
|
13787
|
+
message: deployM("deploy_artifactInvalid"),
|
|
13788
|
+
ext: {
|
|
13789
|
+
fields: parsed.error.issues.map((issue) => issue.path.join("."))
|
|
13790
|
+
}
|
|
13791
|
+
});
|
|
13792
|
+
}
|
|
12359
13793
|
const config = parsed.data;
|
|
12360
13794
|
const scan = await scanApp(appDir, { environment: options.environment ?? config.environment, envDir: options.envDir });
|
|
12361
13795
|
if (!scan.success)
|
|
@@ -12382,21 +13816,51 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12382
13816
|
const compose = validateComposeServices(config.container, artifact);
|
|
12383
13817
|
if (!compose.success)
|
|
12384
13818
|
return compose;
|
|
12385
|
-
images = [
|
|
13819
|
+
images = [
|
|
13820
|
+
.../* @__PURE__ */ new Set([
|
|
13821
|
+
artifact.primaryService,
|
|
13822
|
+
...artifact.hasWeb ? ["web"] : []
|
|
13823
|
+
])
|
|
13824
|
+
];
|
|
12386
13825
|
} else {
|
|
12387
13826
|
const file = join(root, "hai.json");
|
|
12388
|
-
const project = existsSync(file) ? z.object({
|
|
12389
|
-
|
|
12390
|
-
|
|
12391
|
-
|
|
12392
|
-
|
|
12393
|
-
|
|
13827
|
+
const project = existsSync(file) ? z.object({
|
|
13828
|
+
build: z.object({
|
|
13829
|
+
images: z.array(z.object({ name: DeploymentArtifactSchema.shape.primaryService })).min(1).max(2).refine((items) => new Set(items.map((item) => item.name)).size === items.length).optional()
|
|
13830
|
+
}).optional()
|
|
13831
|
+
}).parse(JSON.parse(readFileSync(file, "utf8"))) : void 0;
|
|
13832
|
+
images = project?.build?.images?.map((image) => image.name) ?? [
|
|
13833
|
+
app.isSvelteKit ? "web" : "serv"
|
|
13834
|
+
];
|
|
13835
|
+
}
|
|
13836
|
+
const middlewares = (app.middlewares ?? []).filter((item) => ![
|
|
13837
|
+
"sqlite",
|
|
13838
|
+
"local",
|
|
13839
|
+
"memory",
|
|
13840
|
+
"disabled",
|
|
13841
|
+
"console",
|
|
13842
|
+
"lancedb"
|
|
13843
|
+
].includes(item.backend));
|
|
13844
|
+
const available = [
|
|
13845
|
+
.../* @__PURE__ */ new Set([
|
|
13846
|
+
...images,
|
|
13847
|
+
...app.requiredServices
|
|
13848
|
+
])
|
|
13849
|
+
];
|
|
13850
|
+
const selected = [
|
|
13851
|
+
...options.selectedServices ?? (artifact?.partial ? artifact.images.map((image) => image.name) : available)
|
|
13852
|
+
];
|
|
12394
13853
|
if (!selected.length || new Set(selected).size !== selected.length || selected.some((name) => !available.includes(name)))
|
|
12395
13854
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
12396
13855
|
if (artifact && selected.some((name) => images.includes(name) && !artifact.images.some((image) => image.name === name)))
|
|
12397
13856
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
12398
13857
|
const remoteDirectory = provider.type === "docker-ssh" ? `${provider.remote.baseDir}/${projectName}` : void 0;
|
|
12399
|
-
const baseline = {
|
|
13858
|
+
const baseline = {
|
|
13859
|
+
required: Boolean(artifact?.partial || options.selectedServices),
|
|
13860
|
+
state: "not-inspected",
|
|
13861
|
+
current: remoteDirectory ? `${remoteDirectory}/current` : void 0,
|
|
13862
|
+
release: void 0
|
|
13863
|
+
};
|
|
12400
13864
|
if (options.inspectRemote) {
|
|
12401
13865
|
if (provider.type !== "docker-ssh" || !provider.ssh || !remoteDirectory)
|
|
12402
13866
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
@@ -12412,13 +13876,27 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12412
13876
|
baseline.release = release || void 0;
|
|
12413
13877
|
}
|
|
12414
13878
|
}
|
|
12415
|
-
const configFiles = [
|
|
13879
|
+
const configFiles = [
|
|
13880
|
+
".env",
|
|
13881
|
+
".env.local",
|
|
13882
|
+
`.env.${environment.data.environment}`,
|
|
13883
|
+
`.env.${environment.data.environment}.local`
|
|
13884
|
+
].map((name) => join(environment.data.envDir, name)).filter(existsSync);
|
|
12416
13885
|
const configDirectory = join(directory, "config");
|
|
12417
13886
|
if (existsSync(configDirectory))
|
|
12418
13887
|
configFiles.push(...readdirSync(configDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name)).map((entry) => join(configDirectory, entry.name)).sort());
|
|
12419
|
-
const serviceModes = {
|
|
13888
|
+
const serviceModes = {
|
|
13889
|
+
...options.skipMiddleware ? {} : options.serviceModes,
|
|
13890
|
+
...Object.fromEntries(app.requiredServices.filter((service) => options.skipMiddleware || !selected.includes(service)).map((service) => [service, "keep"]))
|
|
13891
|
+
};
|
|
12420
13892
|
const effective = resolveSidecarOptions(app, { ...options, selectedServices: selected, serviceModes });
|
|
12421
|
-
const provisionKeys = {
|
|
13893
|
+
const provisionKeys = {
|
|
13894
|
+
db: "HAI_DB",
|
|
13895
|
+
cache: "HAI_CACHE",
|
|
13896
|
+
storage: "HAI_STORAGE",
|
|
13897
|
+
email: "HAI_REACH_PROVIDERS",
|
|
13898
|
+
sms: "HAI_REACH_PROVIDERS"
|
|
13899
|
+
};
|
|
12422
13900
|
const provisionEnv = {};
|
|
12423
13901
|
const provisioners = /* @__PURE__ */ new Map();
|
|
12424
13902
|
for (const service of Object.keys(provisionKeys)) {
|
|
@@ -12430,7 +13908,7 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12430
13908
|
}
|
|
12431
13909
|
const sidecar = provider.type === "docker-ssh" ? planSidecars(directory, projectName, app, provisionEnv, effective, config.container?.s3Engine, { dbPassword: "", storagePassword: "", ldapPassword: "" }) : void 0;
|
|
12432
13910
|
const https = provider.type === "docker-ssh" ? provider.expose.https : void 0;
|
|
12433
|
-
const gateway = !!artifact && !(artifact.primaryService === "serv" && artifact.hasWeb) && (sidecar?.services.includes("storage") || !!https &&
|
|
13911
|
+
const gateway = !!artifact && !(artifact.primaryService === "serv" && artifact.hasWeb) && (app.isSvelteKit || sidecar?.services.includes("storage") || !!https && artifact.primaryService === "serv");
|
|
12434
13912
|
if (gateway && artifact.primaryService === "web" && !app.isSvelteKit)
|
|
12435
13913
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
|
|
12436
13914
|
if (gateway) {
|
|
@@ -12449,17 +13927,54 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12449
13927
|
root,
|
|
12450
13928
|
serviceDir: directory,
|
|
12451
13929
|
environment: environment.data.environment,
|
|
12452
|
-
target: provider.type === "docker-ssh" ? {
|
|
12453
|
-
|
|
13930
|
+
target: provider.type === "docker-ssh" ? {
|
|
13931
|
+
provider: provider.type,
|
|
13932
|
+
host: provider.ssh?.host,
|
|
13933
|
+
port: provider.ssh?.port,
|
|
13934
|
+
directory: remoteDirectory,
|
|
13935
|
+
runtime: provider.remote.runtime,
|
|
13936
|
+
hostPort: provider.expose.hostPort,
|
|
13937
|
+
https: https ? { type: https.type, domain: https.domain, port: https.port } : void 0
|
|
13938
|
+
} : { provider: provider.type },
|
|
13939
|
+
artifact: artifact ? {
|
|
13940
|
+
directory: artifactDir,
|
|
13941
|
+
version: artifact.appVersion,
|
|
13942
|
+
buildId: artifact.buildId,
|
|
13943
|
+
verified: true,
|
|
13944
|
+
images: artifact.images.map((image) => ({ name: image.name, reference: image.imageRef, sha256: image.sha256 }))
|
|
13945
|
+
} : void 0,
|
|
12454
13946
|
selected,
|
|
12455
13947
|
retained: available.filter((name) => !selected.includes(name)),
|
|
12456
|
-
containers: provider.type === "docker-ssh" ? [
|
|
12457
|
-
|
|
13948
|
+
containers: provider.type === "docker-ssh" ? [
|
|
13949
|
+
...selected.filter((name) => images.includes(name) || name === "gateway").map((name) => `hai-${projectName}-${name}`),
|
|
13950
|
+
...services.flatMap(([, service]) => typeof service.container_name === "string" ? [service.container_name] : [])
|
|
13951
|
+
] : [],
|
|
13952
|
+
composeManagedServices: [
|
|
13953
|
+
...services.filter(([, service]) => typeof service.container_name !== "string").map(([name]) => name),
|
|
13954
|
+
...selected.includes("certbot") ? ["certbot"] : []
|
|
13955
|
+
],
|
|
12458
13956
|
logicalVolumes: Object.keys(sidecar?.volumes ?? {}),
|
|
12459
|
-
middleware: middlewares.map((item) => ({
|
|
12460
|
-
|
|
13957
|
+
middleware: middlewares.map((item) => ({
|
|
13958
|
+
service: item.service,
|
|
13959
|
+
backend: item.backend,
|
|
13960
|
+
configuration: item.envPrefix,
|
|
13961
|
+
action: selected.includes(item.service) ? effective.serviceModes?.[item.envPrefix] ?? effective.serviceModes?.[item.service] ?? "default" : "keep",
|
|
13962
|
+
provisioner: provisioners.get(item.service)
|
|
13963
|
+
})),
|
|
13964
|
+
configuration: {
|
|
13965
|
+
files: [
|
|
13966
|
+
join(root, "hai.json"),
|
|
13967
|
+
...configFiles
|
|
13968
|
+
].filter(existsSync),
|
|
13969
|
+
environmentKeys: Object.keys(environment.data.values).sort()
|
|
13970
|
+
},
|
|
12461
13971
|
baseline,
|
|
12462
|
-
recovery: {
|
|
13972
|
+
recovery: {
|
|
13973
|
+
releases: remoteDirectory ? `${remoteDirectory}/releases` : void 0,
|
|
13974
|
+
point: baseline.release ?? "previous-successful-release",
|
|
13975
|
+
preservesVolumes: true,
|
|
13976
|
+
databaseRollback: false
|
|
13977
|
+
}
|
|
12463
13978
|
});
|
|
12464
13979
|
} catch {
|
|
12465
13980
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
@@ -12470,7 +13985,11 @@ var passwordSchema = z.string().min(1).max(1024).refine((value) => Buffer.from(v
|
|
|
12470
13985
|
var importSchema = z.strictObject({
|
|
12471
13986
|
version: z.literal(1),
|
|
12472
13987
|
projectName: nameSchema,
|
|
12473
|
-
credentials: z.strictObject({
|
|
13988
|
+
credentials: z.strictObject({
|
|
13989
|
+
DB_PASSWORD: passwordSchema.optional(),
|
|
13990
|
+
STORAGE_PASSWORD: passwordSchema.optional(),
|
|
13991
|
+
LDAP_PASSWORD: passwordSchema.optional()
|
|
13992
|
+
}),
|
|
12474
13993
|
services: z.array(z.strictObject({
|
|
12475
13994
|
service: z.enum(["db", "vecdb", "storage", "ldap"]),
|
|
12476
13995
|
container: nameSchema,
|
|
@@ -12484,7 +14003,12 @@ var importSchema = z.strictObject({
|
|
|
12484
14003
|
bindDn: z.string().min(1).max(512).default("cn=admin,dc=example,dc=org")
|
|
12485
14004
|
})).min(1).max(4)
|
|
12486
14005
|
});
|
|
12487
|
-
var groups2 = {
|
|
14006
|
+
var groups2 = {
|
|
14007
|
+
db: "DB_PASSWORD",
|
|
14008
|
+
vecdb: "DB_PASSWORD",
|
|
14009
|
+
storage: "STORAGE_PASSWORD",
|
|
14010
|
+
ldap: "LDAP_PASSWORD"
|
|
14011
|
+
};
|
|
12488
14012
|
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;
|
|
12489
14013
|
try{process.stdout.write('LOCKED\n');let input='';for await(const chunk of process.stdin){input+=chunk;if(input.length>65536)throw Error()}
|
|
12490
14014
|
if(!input)return;const {content}=JSON.parse(input);if(typeof content!=='string'||!content.startsWith('FORMAT_VERSION=2\n'))throw Error();
|
|
@@ -12516,11 +14040,24 @@ async function importDeploymentSecrets(options) {
|
|
|
12516
14040
|
throw new Error("input");
|
|
12517
14041
|
const input = privateFile(resolve(options.inputFile));
|
|
12518
14042
|
const parsed = importSchema.safeParse(JSON.parse(input));
|
|
12519
|
-
if (!parsed.success)
|
|
12520
|
-
return err({
|
|
14043
|
+
if (!parsed.success) {
|
|
14044
|
+
return err({
|
|
14045
|
+
...HaiDeployError.CONFIG_ERROR,
|
|
14046
|
+
message: deployM("deploy_secretImportFailed", {
|
|
14047
|
+
params: { phase }
|
|
14048
|
+
}),
|
|
14049
|
+
ext: {
|
|
14050
|
+
phase,
|
|
14051
|
+
state: "unchanged",
|
|
14052
|
+
fields: parsed.error.issues.map((issue) => issue.path.join("."))
|
|
14053
|
+
}
|
|
14054
|
+
});
|
|
14055
|
+
}
|
|
12521
14056
|
const config = parsed.data;
|
|
12522
14057
|
const project = `hai-${config.projectName}`;
|
|
12523
|
-
const selectedGroups = [
|
|
14058
|
+
const selectedGroups = [
|
|
14059
|
+
...new Set(config.services.map((service) => groups2[service.service]))
|
|
14060
|
+
].sort();
|
|
12524
14061
|
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])) {
|
|
12525
14062
|
throw new Error("input");
|
|
12526
14063
|
}
|
|
@@ -12560,23 +14097,89 @@ async function importDeploymentSecrets(options) {
|
|
|
12560
14097
|
const application = await inspectContainer(options.runtime, helper2, secrets);
|
|
12561
14098
|
if (application.project !== project || application.service !== "app" || !application.running || !application.networks.some((network) => identity.networks.includes(network)))
|
|
12562
14099
|
throw new Error("identity");
|
|
12563
|
-
const result = await runCommand(options.runtime, [
|
|
14100
|
+
const result = await runCommand(options.runtime, [
|
|
14101
|
+
"exec",
|
|
14102
|
+
"-i",
|
|
14103
|
+
helper2,
|
|
14104
|
+
"node",
|
|
14105
|
+
"-e",
|
|
14106
|
+
s3VerificationScript
|
|
14107
|
+
], {
|
|
14108
|
+
input: JSON.stringify({
|
|
14109
|
+
endpoint: `http://${service.container}:9000`,
|
|
14110
|
+
bucket: service.bucket,
|
|
14111
|
+
accessKeyId: service.accessKeyId,
|
|
14112
|
+
secret: password,
|
|
14113
|
+
region: service.region
|
|
14114
|
+
}),
|
|
14115
|
+
secrets,
|
|
14116
|
+
timeoutMs: 15e3,
|
|
14117
|
+
maxCaptureChars: 1024
|
|
14118
|
+
});
|
|
12564
14119
|
verified = result.success && result.data.stdout.trim() === "VERIFIED";
|
|
12565
14120
|
} else {
|
|
12566
|
-
const args = service.kind === "ldap" ? [
|
|
12567
|
-
|
|
12568
|
-
|
|
14121
|
+
const args = service.kind === "ldap" ? [
|
|
14122
|
+
"exec",
|
|
14123
|
+
"-i",
|
|
14124
|
+
service.container,
|
|
14125
|
+
"ldapwhoami",
|
|
14126
|
+
"-x",
|
|
14127
|
+
"-H",
|
|
14128
|
+
"ldap://127.0.0.1:389",
|
|
14129
|
+
"-D",
|
|
14130
|
+
service.bindDn,
|
|
14131
|
+
"-y",
|
|
14132
|
+
"/dev/stdin"
|
|
14133
|
+
] : [
|
|
14134
|
+
"exec",
|
|
14135
|
+
"-i",
|
|
14136
|
+
service.container,
|
|
14137
|
+
"sh",
|
|
14138
|
+
"-c",
|
|
14139
|
+
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"',
|
|
14140
|
+
"hai-secret-check",
|
|
14141
|
+
service.username,
|
|
14142
|
+
service.database,
|
|
14143
|
+
service.container
|
|
14144
|
+
];
|
|
14145
|
+
const result = await runCommand(options.runtime, args, {
|
|
14146
|
+
input: service.kind === "ldap" ? password : `${password}
|
|
14147
|
+
`,
|
|
14148
|
+
secrets,
|
|
14149
|
+
timeoutMs: 15e3,
|
|
14150
|
+
maxCaptureChars: 1024
|
|
14151
|
+
});
|
|
12569
14152
|
verified = result.success && !result.data.stdoutTruncated && (service.kind === "ldap" ? result.data.stdout.trim().startsWith("dn:") : result.data.stdout.trim() === "1");
|
|
12570
14153
|
if (verified && service.kind === "mysql") {
|
|
12571
|
-
const rootCheck = await runCommand(options.runtime, [
|
|
12572
|
-
|
|
14154
|
+
const rootCheck = await runCommand(options.runtime, [
|
|
14155
|
+
...args.slice(0, -3),
|
|
14156
|
+
"root",
|
|
14157
|
+
service.database,
|
|
14158
|
+
service.container
|
|
14159
|
+
], {
|
|
14160
|
+
input: `${password}
|
|
14161
|
+
`,
|
|
14162
|
+
secrets,
|
|
14163
|
+
timeoutMs: 15e3,
|
|
14164
|
+
maxCaptureChars: 1024
|
|
14165
|
+
});
|
|
12573
14166
|
verified = rootCheck.success && !rootCheck.data.stdoutTruncated && rootCheck.data.stdout.trim() === "1";
|
|
12574
14167
|
const invalidPassword = `hai-import-invalid-${randomUUID()}`;
|
|
12575
14168
|
for (const username of [service.username, "root"]) {
|
|
12576
14169
|
if (!verified)
|
|
12577
14170
|
break;
|
|
12578
|
-
const negative = await runCommand(options.runtime, [
|
|
12579
|
-
|
|
14171
|
+
const negative = await runCommand(options.runtime, [
|
|
14172
|
+
...args.slice(0, -3),
|
|
14173
|
+
username,
|
|
14174
|
+
service.database,
|
|
14175
|
+
service.container
|
|
14176
|
+
], {
|
|
14177
|
+
input: `${invalidPassword}
|
|
14178
|
+
`,
|
|
14179
|
+
secrets: [...secrets, invalidPassword],
|
|
14180
|
+
timeoutMs: 15e3,
|
|
14181
|
+
maxCaptureChars: 1024
|
|
14182
|
+
});
|
|
12580
14183
|
verified = !negative.success && /ERROR 1045 \(28000\)/.test(negative.error.message);
|
|
12581
14184
|
}
|
|
12582
14185
|
}
|
|
@@ -12586,8 +14189,15 @@ async function importDeploymentSecrets(options) {
|
|
|
12586
14189
|
phase = "identity";
|
|
12587
14190
|
}
|
|
12588
14191
|
phase = "state";
|
|
12589
|
-
const uniqueVolumes = [
|
|
12590
|
-
|
|
14192
|
+
const uniqueVolumes = [
|
|
14193
|
+
...new Map(volumes.map((volume) => [`${volume.group}:${volume.name}`, volume])).values()
|
|
14194
|
+
].sort((a, b) => `${a.group}:${a.name}`.localeCompare(`${b.group}:${b.name}`));
|
|
14195
|
+
const lines = [
|
|
14196
|
+
"FORMAT_VERSION=2",
|
|
14197
|
+
`PROJECT=${project}`,
|
|
14198
|
+
...selectedGroups.map((group) => `${group}=${Buffer.from(config.credentials[group], "utf8").toString("hex")}`),
|
|
14199
|
+
...uniqueVolumes.map((volume, index) => `VOLUME_${index + 1}=${volume.group}:${volume.name}`)
|
|
14200
|
+
];
|
|
12591
14201
|
let changed = false;
|
|
12592
14202
|
if (existsSync(stateFile) || isSymbolicLink(stateFile)) {
|
|
12593
14203
|
const existing = readImportedState(privateFile(stateFile));
|
|
@@ -12600,9 +14210,24 @@ async function importDeploymentSecrets(options) {
|
|
|
12600
14210
|
throw new Error("lock");
|
|
12601
14211
|
changed = true;
|
|
12602
14212
|
}
|
|
12603
|
-
return ok({
|
|
14213
|
+
return ok({
|
|
14214
|
+
projectName: config.projectName,
|
|
14215
|
+
stateVersion: 2,
|
|
14216
|
+
groups: selectedGroups,
|
|
14217
|
+
volumes: uniqueVolumes.map((volume) => volume.name),
|
|
14218
|
+
changed
|
|
14219
|
+
});
|
|
12604
14220
|
} catch {
|
|
12605
|
-
return err({
|
|
14221
|
+
return err({
|
|
14222
|
+
...HaiDeployError.CONFIG_ERROR,
|
|
14223
|
+
message: deployM("deploy_secretImportFailed", {
|
|
14224
|
+
params: { phase }
|
|
14225
|
+
}),
|
|
14226
|
+
ext: {
|
|
14227
|
+
phase,
|
|
14228
|
+
state: commitAttempted ? "unknown" : "unchanged"
|
|
14229
|
+
}
|
|
14230
|
+
});
|
|
12606
14231
|
} finally {
|
|
12607
14232
|
await lock?.release();
|
|
12608
14233
|
}
|
|
@@ -12638,19 +14263,26 @@ function isSymbolicLink(path) {
|
|
|
12638
14263
|
}
|
|
12639
14264
|
}
|
|
12640
14265
|
function privateFile(path) {
|
|
12641
|
-
|
|
12642
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 65536 || process.platform !== "win32" && (stat.mode & 63 || process.getuid?.() !== 0 && stat.uid !== process.getuid?.()))
|
|
12643
|
-
throw new Error("private-file");
|
|
12644
|
-
return readFileSync(path, "utf8");
|
|
14266
|
+
return readLocalFile(path, { maxBytes: 65536, private: true }).toString("utf8");
|
|
12645
14267
|
}
|
|
12646
14268
|
async function inspectContainer(runtime, container, secrets) {
|
|
12647
|
-
const result = await runCommand(runtime, [
|
|
14269
|
+
const result = await runCommand(runtime, [
|
|
14270
|
+
"inspect",
|
|
14271
|
+
"--format",
|
|
14272
|
+
"{{json .Config.Labels}}\n{{json .Mounts}}\n{{json .State.Running}}\n{{json .NetworkSettings.Networks}}",
|
|
14273
|
+
container
|
|
14274
|
+
], { secrets, timeoutMs: 1e4, maxCaptureChars: 32768 });
|
|
12648
14275
|
if (!result.success || result.data.stdoutTruncated)
|
|
12649
14276
|
throw new Error("identity");
|
|
12650
14277
|
const [labelsText, mountsText, runningText, networksText] = result.data.stdout.trim().split("\n");
|
|
12651
14278
|
const labels = z.record(z.string(), z.string()).parse(JSON.parse(labelsText));
|
|
12652
|
-
const mounts = z.array(z.object({
|
|
12653
|
-
|
|
14279
|
+
const mounts = z.array(z.object({
|
|
14280
|
+
Type: z.string(),
|
|
14281
|
+
Name: z.string().optional()
|
|
14282
|
+
})).parse(JSON.parse(mountsText));
|
|
14283
|
+
const networks = z.record(z.string(), z.object({
|
|
14284
|
+
NetworkID: z.string()
|
|
14285
|
+
})).parse(JSON.parse(networksText) ?? {});
|
|
12654
14286
|
const dockerProject = labels["com.docker.compose.project"] || void 0;
|
|
12655
14287
|
const podmanProject = labels["io.podman.compose.project"] || void 0;
|
|
12656
14288
|
const dockerService = labels["com.docker.compose.service"] || void 0;
|
|
@@ -12658,11 +14290,27 @@ async function inspectContainer(runtime, container, secrets) {
|
|
|
12658
14290
|
if (Boolean(dockerProject) !== Boolean(dockerService) || Boolean(podmanProject) !== Boolean(podmanService) || dockerProject && podmanProject && dockerProject !== podmanProject || dockerService && podmanService && dockerService !== podmanService) {
|
|
12659
14291
|
throw new Error("identity");
|
|
12660
14292
|
}
|
|
12661
|
-
return {
|
|
14293
|
+
return {
|
|
14294
|
+
project: dockerProject ?? podmanProject,
|
|
14295
|
+
service: dockerService ?? podmanService,
|
|
14296
|
+
running: runningText === "true",
|
|
14297
|
+
volumes: mounts.flatMap((mount) => mount.Type === "volume" && mount.Name ? [mount.Name] : []),
|
|
14298
|
+
networks: Object.values(networks).map((network) => network.NetworkID)
|
|
14299
|
+
};
|
|
12662
14300
|
}
|
|
12663
14301
|
async function acquireImportLock(path, root) {
|
|
12664
14302
|
return new Promise((resolveLock, reject) => {
|
|
12665
|
-
const child = spawn("flock", [
|
|
14303
|
+
const child = spawn("flock", [
|
|
14304
|
+
"-n",
|
|
14305
|
+
path,
|
|
14306
|
+
process.execPath,
|
|
14307
|
+
"-e",
|
|
14308
|
+
importCommitScript,
|
|
14309
|
+
root
|
|
14310
|
+
], {
|
|
14311
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
14312
|
+
windowsHide: true
|
|
14313
|
+
});
|
|
12666
14314
|
let acquired = false;
|
|
12667
14315
|
let exited = false;
|
|
12668
14316
|
let output = "";
|