@h-ai/deploy 1.0.0-beta.18 → 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 +17 -2
- package/README.ja.md +18 -3
- package/README.md +24 -10
- package/dist/deploy-runtime-env.mjs +37 -10
- package/dist/index.d.ts +6 -5
- package/dist/index.js +2030 -403
- 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,13 +17,15 @@ 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 字符。 */
|
|
@@ -33,7 +36,7 @@ var DeploymentArtifactSchema = z.object({
|
|
|
33
36
|
source: z.object({
|
|
34
37
|
/** 工程 package.json 所在仓库的提交 SHA;无法确定时为 unknown。 */
|
|
35
38
|
commit: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64}|unknown)$/),
|
|
36
|
-
/**
|
|
39
|
+
/** 工程及所选镜像上下文/构建定义有未提交改动时为 true;跨仓库或无法检查时为 null。 */
|
|
37
40
|
dirty: z.boolean().nullable()
|
|
38
41
|
}).optional(),
|
|
39
42
|
/** 只包含所选镜像的构建不能用于首次安装。 */
|
|
@@ -561,8 +564,13 @@ var SshConfigSchema = z.object({
|
|
|
561
564
|
}).superRefine((ssh, context) => {
|
|
562
565
|
if (ssh.auth === "key" || ssh.auth === void 0 && !ssh.password)
|
|
563
566
|
return;
|
|
564
|
-
if (!ssh.password || /[\r\n\0]/.test(ssh.password) || new TextEncoder().encode(ssh.password).length > 1023)
|
|
565
|
-
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
|
+
}
|
|
566
574
|
});
|
|
567
575
|
var HttpsDomainSchema = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i).refine((value) => !value.includes(".."));
|
|
568
576
|
var httpsOptions = {
|
|
@@ -599,8 +607,13 @@ var PortExposeSchema = z.object({
|
|
|
599
607
|
/** 可选 HTTPS:已有 PEM 文件或官方 Certbot 自动申请续期;省略保留 HTTP。 */
|
|
600
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")
|
|
601
609
|
}).superRefine((expose, context) => {
|
|
602
|
-
if (expose.https && (expose.https.port === expose.hostPort || expose.https.type === "letsencrypt" && expose.hostPort !== 80))
|
|
603
|
-
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
|
+
}
|
|
604
617
|
});
|
|
605
618
|
var ExposeSchema = PortExposeSchema;
|
|
606
619
|
var DockerSshProviderConfigSchema = z.object({
|
|
@@ -617,7 +630,12 @@ var DockerSshProviderConfigSchema = z.object({
|
|
|
617
630
|
/** 应用暴露方式 */
|
|
618
631
|
expose: ExposeSchema,
|
|
619
632
|
/** Nginx 前方可信负载均衡的 IP/CIDR;省略时不接受外部转发头。 */
|
|
620
|
-
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()
|
|
621
639
|
});
|
|
622
640
|
var ProviderConfigSchema = z.discriminatedUnion("type", [
|
|
623
641
|
VercelProviderConfigSchema,
|
|
@@ -658,23 +676,52 @@ var ServicesConfigSchema = z.object({
|
|
|
658
676
|
sms: z.discriminatedUnion("provisioner", [AliyunSmsServiceSchema]).optional()
|
|
659
677
|
}).optional();
|
|
660
678
|
function provisionerFields(provisioner) {
|
|
661
|
-
const schema = [
|
|
679
|
+
const schema = [
|
|
680
|
+
NeonServiceSchema,
|
|
681
|
+
UpstashServiceSchema,
|
|
682
|
+
R2ServiceSchema,
|
|
683
|
+
ResendServiceSchema,
|
|
684
|
+
AliyunSmsServiceSchema
|
|
685
|
+
].find((item) => item.shape.provisioner.value === provisioner);
|
|
662
686
|
return schema ? Object.keys(schema.shape).filter((name) => name !== "provisioner") : [];
|
|
663
687
|
}
|
|
664
|
-
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
|
+
]);
|
|
665
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;
|
|
666
703
|
var ComposeServiceConfigSchema = z.strictObject({
|
|
667
704
|
/** 容器内的主机名到 IPv4 或 IPv6 地址映射,不能覆盖框架内部服务名。 */
|
|
668
|
-
extra_hosts: z.record(z.string(), z.union([
|
|
705
|
+
extra_hosts: z.record(z.string(), z.union([
|
|
706
|
+
z.ipv4(),
|
|
707
|
+
z.ipv6()
|
|
708
|
+
], {
|
|
669
709
|
error: () => deployM("deploy_composeHostAddressInvalid")
|
|
670
710
|
})).superRefine((hosts, ctx) => {
|
|
671
711
|
const seen = /* @__PURE__ */ new Set();
|
|
672
712
|
for (const host of Object.keys(hosts)) {
|
|
673
713
|
const key = host.toLowerCase();
|
|
674
714
|
if (host.length > 253 || !composeHostname.test(host)) {
|
|
675
|
-
ctx.addIssue({
|
|
715
|
+
ctx.addIssue({
|
|
716
|
+
code: "custom",
|
|
717
|
+
message: deployM("deploy_composeHostnameInvalid")
|
|
718
|
+
});
|
|
676
719
|
} else if (reservedComposeHosts.has(key) || seen.has(key)) {
|
|
677
|
-
ctx.addIssue({
|
|
720
|
+
ctx.addIssue({
|
|
721
|
+
code: "custom",
|
|
722
|
+
path: [host],
|
|
723
|
+
message: deployM("deploy_composeHostnameConflict")
|
|
724
|
+
});
|
|
678
725
|
}
|
|
679
726
|
seen.add(key);
|
|
680
727
|
}
|
|
@@ -712,8 +759,12 @@ function validateComposeServices(container, artifact) {
|
|
|
712
759
|
const field2 = `deploy.container.compose.services.${unavailable}`;
|
|
713
760
|
return err({
|
|
714
761
|
...HaiDeployError.CONFIG_ERROR,
|
|
715
|
-
message: deployM("deploy_composeServiceUnavailable", {
|
|
716
|
-
|
|
762
|
+
message: deployM("deploy_composeServiceUnavailable", {
|
|
763
|
+
params: { field: field2 }
|
|
764
|
+
}),
|
|
765
|
+
ext: {
|
|
766
|
+
fields: [field2]
|
|
767
|
+
}
|
|
717
768
|
});
|
|
718
769
|
}
|
|
719
770
|
function resolveImageReference(reference, source) {
|
|
@@ -730,8 +781,11 @@ function resolveImageReference(reference, source) {
|
|
|
730
781
|
parts.unshift("library");
|
|
731
782
|
const mappings = source.type === "cn" ? { "docker.io": "docker.1ms.run", "ghcr.io": "ghcr.1ms.run" } : source.registries;
|
|
732
783
|
const replacement = mappings?.[registry];
|
|
733
|
-
if (!replacement || typeof replacement !== "string")
|
|
734
|
-
throw new Error(deployM("deploy_imageSourceMissing", {
|
|
784
|
+
if (!replacement || typeof replacement !== "string") {
|
|
785
|
+
throw new Error(deployM("deploy_imageSourceMissing", {
|
|
786
|
+
params: { registry }
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
735
789
|
if (replacement.split("/").some((part) => part === "." || part === ".."))
|
|
736
790
|
throw new Error(deployM("deploy_downloadInvalid"));
|
|
737
791
|
let url;
|
|
@@ -748,20 +802,32 @@ function resolveImageReference(reference, source) {
|
|
|
748
802
|
function systemProxy() {
|
|
749
803
|
if (process.platform === "win32") {
|
|
750
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";
|
|
751
|
-
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 }));
|
|
752
811
|
if (value.pac)
|
|
753
812
|
throw new Error(deployM("deploy_proxySystemUnsupported"));
|
|
754
813
|
if (!value.enabled || !value.server)
|
|
755
814
|
return {};
|
|
756
815
|
const entries = Object.fromEntries(value.server.split(";").map((item) => item.includes("=") ? item.split("=", 2) : ["all", item]));
|
|
757
|
-
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
|
+
};
|
|
758
821
|
}
|
|
759
822
|
if (process.platform === "darwin") {
|
|
760
823
|
const output = execFileSync("/usr/sbin/scutil", ["--proxy"], { encoding: "utf8", timeout: 5e3 });
|
|
761
824
|
const get = (key) => output.match(new RegExp(`^\\s*${key}\\s*:\\s*(.+)$`, "m"))?.[1]?.trim();
|
|
762
825
|
if (get("ProxyAutoConfigEnable") === "1" || get("ProxyAutoDiscoveryEnable") === "1")
|
|
763
826
|
throw new Error(deployM("deploy_proxySystemUnsupported"));
|
|
764
|
-
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
|
+
};
|
|
765
831
|
}
|
|
766
832
|
throw new Error(deployM("deploy_proxySystemUnsupported"));
|
|
767
833
|
}
|
|
@@ -789,7 +855,18 @@ function resolveDownloadProxy(config) {
|
|
|
789
855
|
const bypass = mode === "none" ? "" : config?.bypass ?? values.bypass ?? "";
|
|
790
856
|
if (/[\r\n\0]/.test(bypass))
|
|
791
857
|
throw new Error(deployM("deploy_downloadInvalid"));
|
|
792
|
-
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
|
+
};
|
|
793
870
|
}
|
|
794
871
|
|
|
795
872
|
// src/deploy-config-shapes.json
|
|
@@ -6129,13 +6206,95 @@ var deploy_config_shapes_default = {
|
|
|
6129
6206
|
}
|
|
6130
6207
|
}
|
|
6131
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
|
+
}
|
|
6132
6291
|
|
|
6133
6292
|
// src/deploy-environment.ts
|
|
6134
6293
|
var shapes = deploy_config_shapes_default;
|
|
6135
6294
|
function environmentFile(path) {
|
|
6136
6295
|
const values = {};
|
|
6137
6296
|
if (existsSync(path)) {
|
|
6138
|
-
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")))) {
|
|
6139
6298
|
if (value !== void 0)
|
|
6140
6299
|
values[name] = value;
|
|
6141
6300
|
}
|
|
@@ -6146,7 +6305,15 @@ function mergeEnvironmentLayers(layers, prefix) {
|
|
|
6146
6305
|
return core.config.mergeEnv(layers.map((layer) => Object.fromEntries(Object.entries(layer).filter(([name]) => !prefix || name === prefix || name.startsWith(`${prefix}_`)))));
|
|
6147
6306
|
}
|
|
6148
6307
|
function describeDeploymentVariableSources(directory, environment, values, names) {
|
|
6149
|
-
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
|
+
}));
|
|
6150
6317
|
return names.filter((name) => values[name]).map((name) => {
|
|
6151
6318
|
const source = process.env[name] !== void 0 ? "process.env" : files.find((file) => Object.hasOwn(file.values, name))?.path;
|
|
6152
6319
|
return `${name}: ${source ?? ".env.runtime"}`;
|
|
@@ -6161,32 +6328,65 @@ function loadDeploymentConfig(name, path, values) {
|
|
|
6161
6328
|
const project = JSON.parse(content);
|
|
6162
6329
|
input = project && typeof project === "object" && !Array.isArray(project) ? project[name] : void 0;
|
|
6163
6330
|
} catch {
|
|
6164
|
-
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
|
+
}));
|
|
6165
6339
|
}
|
|
6166
|
-
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
6167
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", { params: { path, line: "1" } }));
|
|
6168
6340
|
} else {
|
|
6169
6341
|
const lineCounter = new LineCounter();
|
|
6170
6342
|
const document = parseDocument(content, { lineCounter, prettyErrors: false });
|
|
6171
|
-
if (document.errors.length)
|
|
6172
|
-
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
|
+
}
|
|
6173
6351
|
input = document.toJS();
|
|
6174
6352
|
}
|
|
6175
|
-
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
|
+
});
|
|
6176
6360
|
if (!resolved.success) {
|
|
6177
6361
|
const cause = resolved.error.cause;
|
|
6178
6362
|
const fields = cause && typeof cause === "object" && "fields" in cause ? cause.fields : void 0;
|
|
6179
6363
|
const safeFields = Array.isArray(fields) ? fields.filter((field2) => typeof field2 === "string" && /^HAI_[A-Z0-9_]+$/.test(field2)).join(", ") : "";
|
|
6180
|
-
if (name === "reach" && /^HAI_REACH_PROVIDERS_\d+(?:_|$)/.test(safeFields) && /^\s*\[\s*\]\s*$/.test(values.HAI_REACH_PROVIDERS ?? ""))
|
|
6181
|
-
return err({
|
|
6182
|
-
|
|
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
|
+
});
|
|
6183
6378
|
}
|
|
6184
6379
|
const config = resolved.data;
|
|
6185
|
-
if (config !== null && (typeof config !== "object" || Array.isArray(config)))
|
|
6186
|
-
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
|
+
}
|
|
6187
6385
|
return ok(config && typeof config === "object" && !Array.isArray(config) ? config : {});
|
|
6188
6386
|
} catch {
|
|
6189
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", {
|
|
6387
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileUnreadable", {
|
|
6388
|
+
params: { path }
|
|
6389
|
+
}));
|
|
6190
6390
|
}
|
|
6191
6391
|
}
|
|
6192
6392
|
function loadDeploymentEnvironment(options) {
|
|
@@ -6195,8 +6395,11 @@ function loadDeploymentEnvironment(options) {
|
|
|
6195
6395
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_invalidEnvironment"));
|
|
6196
6396
|
const envDir = resolve(options.envDir);
|
|
6197
6397
|
const target = join(envDir, `.env.${environment}`);
|
|
6198
|
-
if (options.requireFile !== false && !existsSync(target))
|
|
6199
|
-
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
|
+
}
|
|
6200
6403
|
const loaded = core.config.loadEnvFiles({ mode: environment, envDir });
|
|
6201
6404
|
if (!loaded.success)
|
|
6202
6405
|
return loaded;
|
|
@@ -6210,25 +6413,40 @@ function loadDeploymentEnvironment(options) {
|
|
|
6210
6413
|
values.HAI_DEPLOY_ENVIRONMENT = environment;
|
|
6211
6414
|
if (values.HAI_CORE_ENV === void 0) {
|
|
6212
6415
|
const fallback = environment === "prod" ? "production" : environment === "dev" ? "development" : environment === "test" ? "test" : "staging";
|
|
6213
|
-
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
|
+
});
|
|
6214
6422
|
const settings = configured.success && configured.data && typeof configured.data === "object" ? configured.data : void 0;
|
|
6215
6423
|
values.HAI_CORE_ENV = typeof settings?.env === "string" ? settings.env : fallback;
|
|
6216
6424
|
}
|
|
6217
|
-
return ok({
|
|
6425
|
+
return ok({
|
|
6426
|
+
environment,
|
|
6427
|
+
envDir,
|
|
6428
|
+
values,
|
|
6429
|
+
targetValues: selected
|
|
6430
|
+
});
|
|
6218
6431
|
} catch {
|
|
6219
6432
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_environmentReadFailed"));
|
|
6220
6433
|
}
|
|
6221
6434
|
}
|
|
6222
6435
|
var reachGatewayEnvironment = /^HAI_REACH_PROVIDERS_\d+_GATEWAY$/i;
|
|
6223
6436
|
function isSecret(name, value) {
|
|
6437
|
+
if (name === "headers" || /^HAI_REACH_PROVIDERS_\d+_HEADERS(?:_|$)/i.test(name))
|
|
6438
|
+
return true;
|
|
6224
6439
|
if (/^HAI_REACH_PROVIDERS_\d+_GATEWAY_(?:HEADERS|BODY)(?:_|$)/i.test(name))
|
|
6225
6440
|
return true;
|
|
6226
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"))
|
|
6227
6442
|
return true;
|
|
6228
6443
|
try {
|
|
6229
6444
|
const url = new URL(value);
|
|
6230
|
-
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))) {
|
|
6231
6448
|
return true;
|
|
6449
|
+
}
|
|
6232
6450
|
} catch {
|
|
6233
6451
|
}
|
|
6234
6452
|
try {
|
|
@@ -6270,7 +6488,13 @@ function generatedVariables(values) {
|
|
|
6270
6488
|
const structuredNames = Object.keys(group).filter((name) => structures.has(name));
|
|
6271
6489
|
if (!structuredNames.length)
|
|
6272
6490
|
continue;
|
|
6273
|
-
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
|
+
});
|
|
6274
6498
|
if (resolved.success) {
|
|
6275
6499
|
const flattened = core.config.toEnv({ prefix: `HAI_${module.toUpperCase()}`, value: resolved.data, shape: shapes[module] });
|
|
6276
6500
|
const represented = structuredNames.every((name) => Object.entries(flattened).some(([key, value]) => key === name || key.startsWith(`${name}_`) || name.startsWith(`${key}_`) && value.startsWith("[")));
|
|
@@ -6308,12 +6532,12 @@ function generatedVariables(values) {
|
|
|
6308
6532
|
return generated;
|
|
6309
6533
|
}
|
|
6310
6534
|
function updateFile(path, values, privateFile2) {
|
|
6311
|
-
let content = existsSync(path) ?
|
|
6535
|
+
let content = existsSync(path) ? readLocalFile(path).toString("utf8") : "# Local private deployment configuration; do not commit.\n";
|
|
6312
6536
|
for (const [name, value] of Object.entries(values)) {
|
|
6313
6537
|
if (!/^[A-Z][A-Z0-9_]*$/.test(name))
|
|
6314
6538
|
throw new Error("Invalid environment variable name");
|
|
6315
6539
|
const line = core.config.stringifyEnv({ [name]: value }).trimEnd();
|
|
6316
|
-
const pattern = new RegExp(`^(?:export\\
|
|
6540
|
+
const pattern = new RegExp(`^[\\t ]*(?:export[\\t ]+)?${name}[\\t ]*=[\\t ]*(?:'[^']*'|"(?:[^"\\\\]|\\\\.)*"|\`[^\`]*\`|[^\\r\\n]*)[^\\r\\n]*`, "gm");
|
|
6317
6541
|
if (pattern.test(content))
|
|
6318
6542
|
content = content.replace(pattern, () => line);
|
|
6319
6543
|
else
|
|
@@ -6324,25 +6548,22 @@ ${line}
|
|
|
6324
6548
|
for (const [name, value] of Object.entries(core.config.parseEnv(content)).sort(([left], [right]) => left.length - right.length)) {
|
|
6325
6549
|
if (!name.startsWith("HAI_") || !/^\s*[[{]/.test(value))
|
|
6326
6550
|
continue;
|
|
6327
|
-
const parent = new RegExp(`^(?:export\\
|
|
6328
|
-
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);
|
|
6329
6553
|
if (parent && child && parent.index > child.index) {
|
|
6330
6554
|
const withoutParent = content.slice(0, parent.index) + content.slice(parent.index + parent[0].length);
|
|
6331
6555
|
content = withoutParent.slice(0, child.index) + parent[0].replace(/\r?\n?$/, "\n") + withoutParent.slice(child.index);
|
|
6332
6556
|
}
|
|
6333
6557
|
}
|
|
6334
|
-
|
|
6335
|
-
if (privateFile2)
|
|
6336
|
-
chmodSync(path, 384);
|
|
6558
|
+
writeLocalFile(path, content, privateFile2);
|
|
6337
6559
|
}
|
|
6338
6560
|
function removeOverrides(path, names, privateFile2 = true) {
|
|
6339
6561
|
if (!existsSync(path) || names.length === 0)
|
|
6340
6562
|
return;
|
|
6341
|
-
let content =
|
|
6563
|
+
let content = readLocalFile(path).toString("utf8");
|
|
6342
6564
|
for (const name of names)
|
|
6343
|
-
content = content.replace(new RegExp(`^(?:export\\
|
|
6344
|
-
|
|
6345
|
-
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);
|
|
6346
6567
|
}
|
|
6347
6568
|
function saveDeploymentEnvironment(environment, values, options = {}) {
|
|
6348
6569
|
try {
|
|
@@ -6352,8 +6573,12 @@ function saveDeploymentEnvironment(environment, values, options = {}) {
|
|
|
6352
6573
|
return value;
|
|
6353
6574
|
if (Array.isArray(value))
|
|
6354
6575
|
return value.map((item, index) => splitArrayValue2(`${name}_${index}`, item) ?? null);
|
|
6355
|
-
if (value && typeof value === "object" && !reachGatewayEnvironment.test(name) && !isSecret(name, "") && Object.keys(value).every((key) => /^[a-z][a-zA-Z0-9]*$/.test(key)))
|
|
6356
|
-
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
|
+
}
|
|
6357
6582
|
privateValues[name] = literal;
|
|
6358
6583
|
return void 0;
|
|
6359
6584
|
};
|
|
@@ -6407,10 +6632,10 @@ function saveDeploymentEnvironment(environment, values, options = {}) {
|
|
|
6407
6632
|
removeOverrides(privatePath, Object.keys(shared).filter((name) => !(name in privateValues)));
|
|
6408
6633
|
if (Object.keys(privateValues).length) {
|
|
6409
6634
|
const ignorePath = join(environment.envDir, ".gitignore");
|
|
6410
|
-
const ignore = existsSync(ignorePath) ?
|
|
6635
|
+
const ignore = existsSync(ignorePath) ? readLocalFile(ignorePath).toString("utf8") : "";
|
|
6411
6636
|
const missing = [".env.local", ".env.*.local", ".hai/deploy/"].filter((line) => !ignore.split(/\r?\n/).includes(line));
|
|
6412
6637
|
if (missing.length)
|
|
6413
|
-
|
|
6638
|
+
writeLocalFile(ignorePath, `${ignore.trimEnd()}
|
|
6414
6639
|
${missing.join("\n")}
|
|
6415
6640
|
`);
|
|
6416
6641
|
}
|
|
@@ -6420,7 +6645,14 @@ ${missing.join("\n")}
|
|
|
6420
6645
|
}
|
|
6421
6646
|
}
|
|
6422
6647
|
function normalizeProjectName(name) {
|
|
6423
|
-
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);
|
|
6424
6656
|
if (!normalized)
|
|
6425
6657
|
throw new Error(deployM("deploy_projectNameRequired"));
|
|
6426
6658
|
return normalized;
|
|
@@ -6473,7 +6705,9 @@ function normalizeCredentialEntries(entries) {
|
|
|
6473
6705
|
}
|
|
6474
6706
|
function assertAllowedCredentialKey(key) {
|
|
6475
6707
|
if (!isAllowedCredentialKey(key)) {
|
|
6476
|
-
throw new Error(deployM("deploy_credentialInvalidKey", {
|
|
6708
|
+
throw new Error(deployM("deploy_credentialInvalidKey", {
|
|
6709
|
+
params: { key }
|
|
6710
|
+
}));
|
|
6477
6711
|
}
|
|
6478
6712
|
}
|
|
6479
6713
|
function assertAllowedCredentialEntries(entries) {
|
|
@@ -6525,7 +6759,9 @@ function loadCredentials() {
|
|
|
6525
6759
|
return err(
|
|
6526
6760
|
HaiDeployError.CREDENTIAL_ERROR,
|
|
6527
6761
|
deployM("deploy_credentialError", {
|
|
6528
|
-
params: {
|
|
6762
|
+
params: {
|
|
6763
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6764
|
+
}
|
|
6529
6765
|
}),
|
|
6530
6766
|
error
|
|
6531
6767
|
);
|
|
@@ -6545,7 +6781,9 @@ function saveCredential(key, value) {
|
|
|
6545
6781
|
return err(
|
|
6546
6782
|
HaiDeployError.CREDENTIAL_ERROR,
|
|
6547
6783
|
deployM("deploy_credentialError", {
|
|
6548
|
-
params: {
|
|
6784
|
+
params: {
|
|
6785
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6786
|
+
}
|
|
6549
6787
|
}),
|
|
6550
6788
|
error
|
|
6551
6789
|
);
|
|
@@ -6567,7 +6805,9 @@ function saveCredentials(entries) {
|
|
|
6567
6805
|
return err(
|
|
6568
6806
|
HaiDeployError.CREDENTIAL_ERROR,
|
|
6569
6807
|
deployM("deploy_credentialError", {
|
|
6570
|
-
params: {
|
|
6808
|
+
params: {
|
|
6809
|
+
error: error instanceof Error ? error.message : String(error)
|
|
6810
|
+
}
|
|
6571
6811
|
}),
|
|
6572
6812
|
error
|
|
6573
6813
|
);
|
|
@@ -6576,12 +6816,59 @@ function saveCredentials(entries) {
|
|
|
6576
6816
|
|
|
6577
6817
|
// src/deploy-middleware-config.ts
|
|
6578
6818
|
var connectionFields = {
|
|
6579
|
-
db: [
|
|
6580
|
-
|
|
6581
|
-
|
|
6582
|
-
|
|
6583
|
-
|
|
6584
|
-
|
|
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
|
+
]
|
|
6585
6872
|
};
|
|
6586
6873
|
function mergeManagedMiddlewareConfig(requirement, supplied) {
|
|
6587
6874
|
if (!requirement || requirement.service !== "ldap" && requirement.backend !== supplied.type)
|
|
@@ -6589,7 +6876,10 @@ function mergeManagedMiddlewareConfig(requirement, supplied) {
|
|
|
6589
6876
|
const configured = { ...requirement.config };
|
|
6590
6877
|
for (const field2 of connectionFields[requirement.service] ?? [])
|
|
6591
6878
|
delete configured[field2];
|
|
6592
|
-
const merged = {
|
|
6879
|
+
const merged = {
|
|
6880
|
+
...configured,
|
|
6881
|
+
...supplied
|
|
6882
|
+
};
|
|
6593
6883
|
if (requirement.service === "cache" && typeof configured.db === "number" && typeof supplied.url === "string") {
|
|
6594
6884
|
const url = new URL(supplied.url);
|
|
6595
6885
|
if (configured.db !== 0 || url.pathname !== "" && url.pathname !== "/") {
|
|
@@ -6604,21 +6894,39 @@ var RecoverySchema = z.object({
|
|
|
6604
6894
|
stage: z.string(),
|
|
6605
6895
|
platformProjectId: z.string().optional(),
|
|
6606
6896
|
resources: z.array(z.object({
|
|
6607
|
-
serviceType: z.enum([
|
|
6897
|
+
serviceType: z.enum([
|
|
6898
|
+
"db",
|
|
6899
|
+
"cache",
|
|
6900
|
+
"storage",
|
|
6901
|
+
"email",
|
|
6902
|
+
"sms"
|
|
6903
|
+
]),
|
|
6608
6904
|
provisionerName: z.string(),
|
|
6609
6905
|
resourceInfo: z.string(),
|
|
6610
6906
|
resourceStatus: z.enum(["created", "reused", "unknown"])
|
|
6611
6907
|
}))
|
|
6612
6908
|
});
|
|
6613
6909
|
var CommandStateSchema = z.object({
|
|
6614
|
-
reason: z.enum([
|
|
6910
|
+
reason: z.enum([
|
|
6911
|
+
"timeout",
|
|
6912
|
+
"output",
|
|
6913
|
+
"input",
|
|
6914
|
+
"spawn",
|
|
6915
|
+
"exit",
|
|
6916
|
+
"configuration"
|
|
6917
|
+
]),
|
|
6615
6918
|
localTermination: z.enum(["confirmed", "unknown", "not-started"]),
|
|
6616
6919
|
remoteExecution: z.enum(["unknown", "finished"]).optional(),
|
|
6617
6920
|
deploymentId: z.string().optional(),
|
|
6618
6921
|
resultFile: z.string().optional(),
|
|
6619
6922
|
remoteExitCode: z.number().int().optional(),
|
|
6620
6923
|
remoteStage: z.string().optional(),
|
|
6621
|
-
remoteRecovery: z.enum([
|
|
6924
|
+
remoteRecovery: z.enum([
|
|
6925
|
+
"not-needed",
|
|
6926
|
+
"passed",
|
|
6927
|
+
"failed",
|
|
6928
|
+
"stopped"
|
|
6929
|
+
]).optional()
|
|
6622
6930
|
});
|
|
6623
6931
|
function getDeployCommandState(error) {
|
|
6624
6932
|
const parsed = CommandStateSchema.safeParse(error.ext?.commandExecution);
|
|
@@ -6634,7 +6942,9 @@ function recoveryFailure(error, recovery) {
|
|
|
6634
6942
|
httpStatus: error.httpStatus,
|
|
6635
6943
|
system: error.system,
|
|
6636
6944
|
module: error.module,
|
|
6637
|
-
message: deployM("deploy_recoveryFailure", {
|
|
6945
|
+
message: deployM("deploy_recoveryFailure", {
|
|
6946
|
+
params: { stage: recovery.stage }
|
|
6947
|
+
}),
|
|
6638
6948
|
suggestion: deployM("deploy_recoverySuggestion"),
|
|
6639
6949
|
ext: { recovery }
|
|
6640
6950
|
});
|
|
@@ -6669,12 +6979,15 @@ function runtimeDependencies(appDir) {
|
|
|
6669
6979
|
visited.add(id);
|
|
6670
6980
|
const metadata = importer === void 0 ? record(packages[id.split("(")[0]]) : {};
|
|
6671
6981
|
const peers = record(metadata.peerDependencies);
|
|
6672
|
-
for (const [name, reference] of Object.entries({
|
|
6982
|
+
for (const [name, reference] of Object.entries({
|
|
6983
|
+
...record(node.dependencies),
|
|
6984
|
+
...record(node.optionalDependencies)
|
|
6985
|
+
})) {
|
|
6673
6986
|
if (name in peers)
|
|
6674
6987
|
continue;
|
|
6675
6988
|
const version = typeof reference === "string" ? reference : record(reference).version;
|
|
6676
6989
|
if (typeof version !== "string")
|
|
6677
|
-
throw new
|
|
6990
|
+
throw new TypeError("Invalid pnpm dependency reference");
|
|
6678
6991
|
if (version.startsWith("link:")) {
|
|
6679
6992
|
if (importer === void 0)
|
|
6680
6993
|
throw new Error("Invalid workspace dependency");
|
|
@@ -6695,7 +7008,14 @@ function runtimeDependencies(appDir) {
|
|
|
6695
7008
|
}
|
|
6696
7009
|
};
|
|
6697
7010
|
visit(`workspace:${importerKey}`, record(importers[importerKey]), importerKey);
|
|
6698
|
-
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
|
+
};
|
|
6699
7019
|
}
|
|
6700
7020
|
async function scanApp(inputDir, options) {
|
|
6701
7021
|
try {
|
|
@@ -6717,7 +7037,14 @@ async function scanApp(inputDir, options) {
|
|
|
6717
7037
|
const deps = graph.runtime;
|
|
6718
7038
|
const configs = {};
|
|
6719
7039
|
const enabledConfigs = /* @__PURE__ */ new Set();
|
|
6720
|
-
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
|
+
})) {
|
|
6721
7048
|
configs[name] = {};
|
|
6722
7049
|
if (!deps.has(`@h-ai/${owner}`))
|
|
6723
7050
|
continue;
|
|
@@ -6731,7 +7058,15 @@ async function scanApp(inputDir, options) {
|
|
|
6731
7058
|
}
|
|
6732
7059
|
const middlewares = [];
|
|
6733
7060
|
const add = (service, backend, configName = service, configPath = [], config = configs[configName] ?? {}, source = `_${configName}.yml`) => {
|
|
6734
|
-
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
|
+
});
|
|
6735
7070
|
};
|
|
6736
7071
|
const iam = enabledConfigs.has("iam");
|
|
6737
7072
|
if (enabledConfigs.has("db"))
|
|
@@ -6752,8 +7087,12 @@ async function scanApp(inputDir, options) {
|
|
|
6752
7087
|
const provider = record(item);
|
|
6753
7088
|
const type = String(provider.type ?? "");
|
|
6754
7089
|
const service = type === "aliyun-sms" || ["console", "api"].includes(type) && provider.name === "sms" ? "sms" : ["smtp", "api", "console"].includes(type) ? "email" : void 0;
|
|
6755
|
-
if (service)
|
|
6756
|
-
add(service, type, "reach", [
|
|
7090
|
+
if (service) {
|
|
7091
|
+
add(service, type, "reach", [
|
|
7092
|
+
"providers",
|
|
7093
|
+
String(index)
|
|
7094
|
+
], provider, "@h-ai/reach");
|
|
7095
|
+
}
|
|
6757
7096
|
}
|
|
6758
7097
|
}
|
|
6759
7098
|
if (enabledConfigs.has("reach") && providers === void 0)
|
|
@@ -6768,19 +7107,33 @@ async function scanApp(inputDir, options) {
|
|
|
6768
7107
|
isSvelteKit,
|
|
6769
7108
|
adapterInstalled: graph.build.has("@sveltejs/adapter-vercel"),
|
|
6770
7109
|
nodeAdapterInstalled: graph.build.has("@sveltejs/adapter-node"),
|
|
6771
|
-
requiredServices: [
|
|
7110
|
+
requiredServices: [
|
|
7111
|
+
...new Set(middlewares.map((item) => item.service))
|
|
7112
|
+
],
|
|
6772
7113
|
serviceBackends: Object.fromEntries(middlewares.map((item) => [item.service, item.backend]))
|
|
6773
7114
|
};
|
|
6774
|
-
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
|
+
});
|
|
6775
7121
|
return ok(scan);
|
|
6776
7122
|
} catch {
|
|
6777
|
-
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
|
+
}));
|
|
6778
7126
|
}
|
|
6779
7127
|
}
|
|
6780
7128
|
|
|
6781
7129
|
// src/container/deploy-compose-identity.ts
|
|
6782
7130
|
function createComposeContainerLookupScript() {
|
|
6783
|
-
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("|");
|
|
6784
7137
|
return String.raw`compose_container_ids() (
|
|
6785
7138
|
if [ "$#" -ne 4 ]; then printf 'Compose container lookup requires runtime, project, service and selector.\n' >&2; exit 1; fi
|
|
6786
7139
|
lookup_runtime=$1
|
|
@@ -7168,11 +7521,28 @@ deployment_forget_metadata() {
|
|
|
7168
7521
|
|
|
7169
7522
|
// src/container/deploy-managed-secrets.ts
|
|
7170
7523
|
var groups = [
|
|
7171
|
-
{
|
|
7172
|
-
|
|
7173
|
-
|
|
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
|
+
}
|
|
7174
7539
|
];
|
|
7175
|
-
var appPrefixes = {
|
|
7540
|
+
var appPrefixes = {
|
|
7541
|
+
db: "HAI_DB",
|
|
7542
|
+
vecdb: "HAI_VECDB",
|
|
7543
|
+
storage: "HAI_STORAGE",
|
|
7544
|
+
ldap: "HAI_IAM_LDAP"
|
|
7545
|
+
};
|
|
7176
7546
|
function resolveComposeNamedVolumes(projectName, mounts, declarations = {}) {
|
|
7177
7547
|
if (!Array.isArray(mounts))
|
|
7178
7548
|
return [];
|
|
@@ -7198,7 +7568,9 @@ function createManagedSecretAssets(projectName, nativeEnvironment, compose) {
|
|
|
7198
7568
|
const bindings = /* @__PURE__ */ new Set();
|
|
7199
7569
|
const volumes = /* @__PURE__ */ new Set();
|
|
7200
7570
|
for (const group of groups) {
|
|
7201
|
-
const values = [
|
|
7571
|
+
const values = [
|
|
7572
|
+
...new Set(group.keys.flatMap((key) => native[key] === void 0 ? [] : [native[key]]))
|
|
7573
|
+
];
|
|
7202
7574
|
if (!values.length)
|
|
7203
7575
|
continue;
|
|
7204
7576
|
if (values.length !== 1 || !/^[\w-]{32}$/.test(values[0]))
|
|
@@ -7400,7 +7772,7 @@ function createBackendStateAssets(projectName, compose, selected) {
|
|
|
7400
7772
|
const service = compose.services[serviceName];
|
|
7401
7773
|
const image = service?.image;
|
|
7402
7774
|
if (typeof image !== "string")
|
|
7403
|
-
throw new
|
|
7775
|
+
throw new TypeError(deployM("deploy_artifactInvalid"));
|
|
7404
7776
|
const imageFamily = image.slice(image.lastIndexOf("/") + 1).split(":")[0];
|
|
7405
7777
|
const family = imageFamily === "postgres" ? "postgresql" : imageFamily;
|
|
7406
7778
|
const namedVolumes = resolveComposeNamedVolumes(projectName, service.volumes, compose.volumes);
|
|
@@ -7753,7 +8125,13 @@ public static class HaiCommandJob {
|
|
|
7753
8125
|
}
|
|
7754
8126
|
`;
|
|
7755
8127
|
function powershell(script) {
|
|
7756
|
-
return [
|
|
8128
|
+
return [
|
|
8129
|
+
"-NoLogo",
|
|
8130
|
+
"-NoProfile",
|
|
8131
|
+
"-NonInteractive",
|
|
8132
|
+
"-EncodedCommand",
|
|
8133
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
8134
|
+
];
|
|
7757
8135
|
}
|
|
7758
8136
|
function windowsCommand(command, args) {
|
|
7759
8137
|
const name = `Local\\hai-deploy-${randomUUID()}`;
|
|
@@ -7767,7 +8145,13 @@ ${nativeJob}
|
|
|
7767
8145
|
[Environment]::SetEnvironmentVariable('${key}', $null)
|
|
7768
8146
|
exit ([HaiCommandJob]::Run([string]$inputConfig.name, [string]$inputConfig.command, [string[]]$inputConfig.args))
|
|
7769
8147
|
} catch { [Console]::Error.WriteLine('Windows command job failed'); exit 125 }`;
|
|
7770
|
-
return {
|
|
8148
|
+
return {
|
|
8149
|
+
name,
|
|
8150
|
+
args: powershell(script),
|
|
8151
|
+
env: {
|
|
8152
|
+
[key]: JSON.stringify({ name, command, args })
|
|
8153
|
+
}
|
|
8154
|
+
};
|
|
7771
8155
|
}
|
|
7772
8156
|
function windowsJobProbe(name) {
|
|
7773
8157
|
return powershell(`$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'; $InformationPreference='SilentlyContinue'
|
|
@@ -7837,7 +8221,10 @@ function spawnCommand(command, args, options) {
|
|
|
7837
8221
|
const job = process.platform === "win32" ? windowsCommand(command, args) : void 0;
|
|
7838
8222
|
const child = spawn(job ? "powershell.exe" : command, job?.args ?? args, {
|
|
7839
8223
|
...options,
|
|
7840
|
-
env: job ? {
|
|
8224
|
+
env: job ? {
|
|
8225
|
+
...options.env,
|
|
8226
|
+
...job.env
|
|
8227
|
+
} : options.env,
|
|
7841
8228
|
windowsHide: true,
|
|
7842
8229
|
shell: false,
|
|
7843
8230
|
detached: !job
|
|
@@ -7864,7 +8251,12 @@ function spawnCommand(command, args, options) {
|
|
|
7864
8251
|
if (child.pid) {
|
|
7865
8252
|
if (job) {
|
|
7866
8253
|
if (!exited) {
|
|
7867
|
-
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()));
|
|
7868
8260
|
child.kill("SIGKILL");
|
|
7869
8261
|
}
|
|
7870
8262
|
} else {
|
|
@@ -7906,7 +8298,9 @@ function spawnCommand(command, args, options) {
|
|
|
7906
8298
|
|
|
7907
8299
|
// src/container/deploy-secret-masker.ts
|
|
7908
8300
|
function createSecretMasker(secrets = []) {
|
|
7909
|
-
const values = [
|
|
8301
|
+
const values = [
|
|
8302
|
+
...new Set(secrets.filter(Boolean))
|
|
8303
|
+
].sort((a, b) => b.length - a.length);
|
|
7910
8304
|
if (values.reduce((total, value) => total + value.length, 0) > 1048576)
|
|
7911
8305
|
throw new RangeError("Secret redaction input is too large");
|
|
7912
8306
|
const pattern = values.length ? new RegExp(values.map((value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|"), "g") : void 0;
|
|
@@ -7978,11 +8372,19 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
7978
8372
|
throw new RangeError("Invalid command termination timeout");
|
|
7979
8373
|
if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
|
|
7980
8374
|
throw new RangeError("Invalid command timeout");
|
|
7981
|
-
const secrets = [
|
|
8375
|
+
const secrets = [
|
|
8376
|
+
...options.secrets ?? []
|
|
8377
|
+
];
|
|
7982
8378
|
const safeCommand = maskSecrets(command, secrets);
|
|
7983
8379
|
const outputMasker = createSecretMasker(secrets);
|
|
7984
8380
|
const errorMasker = createSecretMasker(secrets);
|
|
7985
|
-
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
|
+
});
|
|
7986
8388
|
const child = control.child;
|
|
7987
8389
|
child.stdout.setEncoding("utf8");
|
|
7988
8390
|
child.stderr.setEncoding("utf8");
|
|
@@ -8021,7 +8423,12 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8021
8423
|
try {
|
|
8022
8424
|
await options.onOutput?.(text);
|
|
8023
8425
|
} catch {
|
|
8024
|
-
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
|
+
}));
|
|
8025
8432
|
}
|
|
8026
8433
|
};
|
|
8027
8434
|
const captureError = (text) => {
|
|
@@ -8031,7 +8438,9 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8031
8438
|
if (options.timeoutMs && options.timeoutMs > 0) {
|
|
8032
8439
|
timer = setTimeout(() => {
|
|
8033
8440
|
logger3.error("Command timed out", { command: safeCommand, timeoutMs: options.timeoutMs });
|
|
8034
|
-
void interrupt("timeout", deployM("deploy_commandTimeout", {
|
|
8441
|
+
void interrupt("timeout", deployM("deploy_commandTimeout", {
|
|
8442
|
+
params: { command: safeCommand }
|
|
8443
|
+
}));
|
|
8035
8444
|
}, options.timeoutMs);
|
|
8036
8445
|
}
|
|
8037
8446
|
const outputTask = (async () => {
|
|
@@ -8046,7 +8455,12 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8046
8455
|
if (!settled && !interrupted)
|
|
8047
8456
|
await publish(outputMasker.end());
|
|
8048
8457
|
} catch {
|
|
8049
|
-
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
|
+
}));
|
|
8050
8464
|
}
|
|
8051
8465
|
})();
|
|
8052
8466
|
const errorTask = (async () => {
|
|
@@ -8061,16 +8475,28 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8061
8475
|
if (!settled && !interrupted)
|
|
8062
8476
|
captureError(errorMasker.end());
|
|
8063
8477
|
} catch {
|
|
8064
|
-
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
|
+
}));
|
|
8065
8484
|
}
|
|
8066
8485
|
})();
|
|
8067
8486
|
child.stdin.on("error", () => {
|
|
8068
|
-
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
|
+
}));
|
|
8069
8493
|
});
|
|
8070
8494
|
child.on("error", (error) => {
|
|
8071
8495
|
const safeError = maskSecrets(error.message, secrets);
|
|
8072
8496
|
logger3.error("Command failed to start", { command: safeCommand, error: safeError });
|
|
8073
|
-
void interrupt("spawn", deployM("deploy_commandFailed", {
|
|
8497
|
+
void interrupt("spawn", deployM("deploy_commandFailed", {
|
|
8498
|
+
params: { command: safeCommand, error: safeError }
|
|
8499
|
+
}));
|
|
8074
8500
|
});
|
|
8075
8501
|
child.on("close", async (code) => {
|
|
8076
8502
|
await Promise.all([outputTask, errorTask]);
|
|
@@ -8079,24 +8505,52 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
8079
8505
|
if (timer)
|
|
8080
8506
|
clearTimeout(timer);
|
|
8081
8507
|
if (!await control.treeExited()) {
|
|
8082
|
-
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
|
+
}));
|
|
8083
8514
|
return;
|
|
8084
8515
|
}
|
|
8085
8516
|
const exitCode = code ?? -1;
|
|
8086
8517
|
if (exitCode === 0) {
|
|
8087
|
-
finish(ok({
|
|
8518
|
+
finish(ok({
|
|
8519
|
+
code: exitCode,
|
|
8520
|
+
stdout,
|
|
8521
|
+
stderr,
|
|
8522
|
+
stdoutTruncated,
|
|
8523
|
+
stderrTruncated
|
|
8524
|
+
}));
|
|
8088
8525
|
return;
|
|
8089
8526
|
}
|
|
8090
|
-
logger3.error("Command exited with non-zero code", {
|
|
8091
|
-
|
|
8092
|
-
|
|
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" }));
|
|
8093
8542
|
});
|
|
8094
8543
|
if (options.input !== void 0)
|
|
8095
8544
|
child.stdin.write(options.input);
|
|
8096
8545
|
child.stdin.end();
|
|
8097
8546
|
});
|
|
8098
8547
|
} catch {
|
|
8099
|
-
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" });
|
|
8100
8554
|
}
|
|
8101
8555
|
}
|
|
8102
8556
|
|
|
@@ -8339,7 +8793,13 @@ printf '%s\n' "$HAI_DEPLOY_ASKPASS_PASSWORD"
|
|
|
8339
8793
|
`.replaceAll("\\${", "${");
|
|
8340
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";
|
|
8341
8795
|
function helperFailure(key, reason = "configuration") {
|
|
8342
|
-
return err({
|
|
8796
|
+
return err({
|
|
8797
|
+
...HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
8798
|
+
message: deployM(key),
|
|
8799
|
+
ext: {
|
|
8800
|
+
commandExecution: { reason, localTermination: "not-started" }
|
|
8801
|
+
}
|
|
8802
|
+
});
|
|
8343
8803
|
}
|
|
8344
8804
|
async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
|
|
8345
8805
|
let directory;
|
|
@@ -8370,7 +8830,12 @@ async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
|
|
|
8370
8830
|
const source = join(directory, "askpass.cs");
|
|
8371
8831
|
writeFileSync(source, windowsAskpassSource, { mode: 384 });
|
|
8372
8832
|
const compiler = join(process.env.SystemRoot ?? "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
|
|
8373
|
-
const compiled = await runCommand(compiler, [
|
|
8833
|
+
const compiled = await runCommand(compiler, [
|
|
8834
|
+
"-NoProfile",
|
|
8835
|
+
"-NonInteractive",
|
|
8836
|
+
"-Command",
|
|
8837
|
+
compileAskpassScript
|
|
8838
|
+
], {
|
|
8374
8839
|
env: { HAI_DEPLOY_ASKPASS_SOURCE: source, HAI_DEPLOY_ASKPASS_OUTPUT: helper2, HAI_DEPLOY_ASKPASS_PASSWORD: "" },
|
|
8375
8840
|
timeoutMs: Math.max(1, Math.min(3e4, Math.ceil(deadline - performance.now()))),
|
|
8376
8841
|
terminationTimeoutMs,
|
|
@@ -8391,7 +8856,16 @@ async function prepareSshAskpass(password, deadline, terminationTimeoutMs) {
|
|
|
8391
8856
|
const removed = cleanup();
|
|
8392
8857
|
return removed.success ? helperFailure("deploy_sshAskpassFailed", "timeout") : removed;
|
|
8393
8858
|
}
|
|
8394
|
-
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
|
+
});
|
|
8395
8869
|
} catch {
|
|
8396
8870
|
const removed = cleanup();
|
|
8397
8871
|
return removed.success ? helperFailure("deploy_sshAskpassFailed") : removed;
|
|
@@ -9352,7 +9826,10 @@ function usesSshPassword(config) {
|
|
|
9352
9826
|
}
|
|
9353
9827
|
function buildConnectOptions(config) {
|
|
9354
9828
|
const password = usesSshPassword(config);
|
|
9355
|
-
const args = [
|
|
9829
|
+
const args = [
|
|
9830
|
+
"-o",
|
|
9831
|
+
password ? "BatchMode=no" : "BatchMode=yes"
|
|
9832
|
+
];
|
|
9356
9833
|
if (password)
|
|
9357
9834
|
args.push("-o", "PreferredAuthentications=password", "-o", "PasswordAuthentication=yes", "-o", "PubkeyAuthentication=no", "-o", "NumberOfPasswordPrompts=1");
|
|
9358
9835
|
args.push("-o", `ConnectTimeout=${config.connectTimeoutSeconds}`);
|
|
@@ -9389,8 +9866,15 @@ function createSshExecutor(config) {
|
|
|
9389
9866
|
const runAuthenticated = async (command, args, options) => {
|
|
9390
9867
|
if (!usesSshPassword(config))
|
|
9391
9868
|
return runCommand(command, args, options);
|
|
9392
|
-
if (!config.password || options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0 || options.timeoutMs > 2147483647))
|
|
9393
|
-
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
|
+
}
|
|
9394
9878
|
const deadline = options.timeoutMs === void 0 ? Number.POSITIVE_INFINITY : performance.now() + options.timeoutMs;
|
|
9395
9879
|
const prepared = await prepareSshAskpass(config.password, deadline, options.terminationTimeoutMs);
|
|
9396
9880
|
if (!prepared.success)
|
|
@@ -9399,20 +9883,36 @@ function createSshExecutor(config) {
|
|
|
9399
9883
|
let removed = ok(void 0);
|
|
9400
9884
|
try {
|
|
9401
9885
|
const remaining = deadline - performance.now();
|
|
9402
|
-
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, {
|
|
9403
9893
|
...options,
|
|
9404
9894
|
env: prepared.data.env,
|
|
9405
9895
|
timeoutMs: Number.isFinite(deadline) ? Math.ceil(remaining) : void 0,
|
|
9406
|
-
secrets: [
|
|
9896
|
+
secrets: [
|
|
9897
|
+
...options.secrets ?? [],
|
|
9898
|
+
config.password
|
|
9899
|
+
]
|
|
9407
9900
|
});
|
|
9408
9901
|
} catch {
|
|
9409
|
-
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
|
+
});
|
|
9410
9909
|
} finally {
|
|
9411
9910
|
removed = prepared.data.cleanup();
|
|
9412
9911
|
}
|
|
9413
|
-
if (!removed.success)
|
|
9912
|
+
if (!removed.success) {
|
|
9414
9913
|
return result.success ? removed : err({ ...result.error, message: `${result.error.message}
|
|
9415
9914
|
${removed.error.message}` });
|
|
9915
|
+
}
|
|
9416
9916
|
return result;
|
|
9417
9917
|
};
|
|
9418
9918
|
const exec = async (remoteCommand, options) => {
|
|
@@ -9426,13 +9926,31 @@ ${removed.error.message}` });
|
|
|
9426
9926
|
if (result.success)
|
|
9427
9927
|
return result;
|
|
9428
9928
|
const state = getDeployCommandState(result.error);
|
|
9429
|
-
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
|
+
});
|
|
9430
9942
|
};
|
|
9431
9943
|
const upload = async (localPath, remotePath) => {
|
|
9432
9944
|
const args = buildScpUploadArgs(config, localPath, remotePath);
|
|
9433
9945
|
const result = await runAuthenticated("scp", args, { timeoutMs: 6e5 });
|
|
9434
9946
|
if (!result.success) {
|
|
9435
|
-
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
|
+
});
|
|
9436
9954
|
}
|
|
9437
9955
|
return ok(void 0);
|
|
9438
9956
|
};
|
|
@@ -9440,7 +9958,13 @@ ${removed.error.message}` });
|
|
|
9440
9958
|
logger4.debug("SSH preflight", { host: config.host, port: config.port });
|
|
9441
9959
|
const result = await exec("true", { timeoutMs: 2e4 });
|
|
9442
9960
|
if (!result.success) {
|
|
9443
|
-
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
|
+
});
|
|
9444
9968
|
}
|
|
9445
9969
|
return ok(void 0);
|
|
9446
9970
|
};
|
|
@@ -9473,8 +9997,11 @@ async function createDeploymentBundle(options) {
|
|
|
9473
9997
|
const nodeRuntime = artifact.primaryService === "serv" || !!compose.services[artifact.primaryService]?.environment?.NODE_OPTIONS;
|
|
9474
9998
|
for (const name of selected) {
|
|
9475
9999
|
const service = compose.services[name];
|
|
9476
|
-
if (!service)
|
|
9477
|
-
throw new Error(deployM("deploy_selectionInvalid", {
|
|
10000
|
+
if (!service) {
|
|
10001
|
+
throw new Error(deployM("deploy_selectionInvalid", {
|
|
10002
|
+
params: { service: name }
|
|
10003
|
+
}));
|
|
10004
|
+
}
|
|
9478
10005
|
if (typeof service.image === "string")
|
|
9479
10006
|
references.add(service.image);
|
|
9480
10007
|
if (service.depends_on) {
|
|
@@ -9488,12 +10015,18 @@ async function createDeploymentBundle(options) {
|
|
|
9488
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);
|
|
9489
10016
|
}
|
|
9490
10017
|
}
|
|
9491
|
-
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
|
+
}));
|
|
9492
10022
|
let runtime;
|
|
9493
10023
|
const imageList = [];
|
|
9494
10024
|
for (const reference of references) {
|
|
9495
|
-
if (!/:[^/:]+$/.test(reference) || /:latest$/i.test(reference))
|
|
9496
|
-
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
|
+
}
|
|
9497
10030
|
const built = artifact.images.find((image) => image.imageRef === reference);
|
|
9498
10031
|
const file = built?.file ?? `middleware-${imageList.length + 1}.tar`;
|
|
9499
10032
|
const destination = join(staging, "images", file);
|
|
@@ -9514,10 +10047,20 @@ async function createDeploymentBundle(options) {
|
|
|
9514
10047
|
if (!runtime)
|
|
9515
10048
|
throw new Error(deployM("deploy_bundleRuntimeRequired"));
|
|
9516
10049
|
}
|
|
9517
|
-
let inspected = await runCommand(runtime, [
|
|
10050
|
+
let inspected = await runCommand(runtime, [
|
|
10051
|
+
"image",
|
|
10052
|
+
"inspect",
|
|
10053
|
+
"--format",
|
|
10054
|
+
"{{.Id}}",
|
|
10055
|
+
reference
|
|
10056
|
+
]);
|
|
9518
10057
|
if (!inspected.success) {
|
|
9519
10058
|
const download = resolveImageReference(reference, options.imageSource);
|
|
9520
|
-
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
|
+
});
|
|
9521
10064
|
if (!pulled.success)
|
|
9522
10065
|
throw new Error(pulled.error.message);
|
|
9523
10066
|
if (download !== reference) {
|
|
@@ -9525,7 +10068,13 @@ async function createDeploymentBundle(options) {
|
|
|
9525
10068
|
if (!tagged.success)
|
|
9526
10069
|
throw new Error(tagged.error.message);
|
|
9527
10070
|
}
|
|
9528
|
-
inspected = await runCommand(runtime, [
|
|
10071
|
+
inspected = await runCommand(runtime, [
|
|
10072
|
+
"image",
|
|
10073
|
+
"inspect",
|
|
10074
|
+
"--format",
|
|
10075
|
+
"{{.Id}}",
|
|
10076
|
+
reference
|
|
10077
|
+
]);
|
|
9529
10078
|
}
|
|
9530
10079
|
const identity = inspected.success && !inspected.data.stdoutTruncated ? inspected.data.stdout.trim().replace(/^sha256:/, "") : "";
|
|
9531
10080
|
const cacheIdentity = createHash("sha256").update(identity).update("\0").update(reference).digest("hex");
|
|
@@ -9537,7 +10086,13 @@ async function createDeploymentBundle(options) {
|
|
|
9537
10086
|
checksum = copied;
|
|
9538
10087
|
}
|
|
9539
10088
|
if (!checksum) {
|
|
9540
|
-
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 });
|
|
9541
10096
|
if (!saved.success)
|
|
9542
10097
|
throw new Error(saved.error.message);
|
|
9543
10098
|
checksum = await artifactChecksum(destination);
|
|
@@ -9579,7 +10134,12 @@ async function createDeploymentBundle(options) {
|
|
|
9579
10134
|
if (file !== "https")
|
|
9580
10135
|
put(`.hai/${file}`, content);
|
|
9581
10136
|
}
|
|
9582
|
-
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") : "";
|
|
9583
10143
|
const backend = createBackendStateAssets(projectName, compose, selected);
|
|
9584
10144
|
const metadata = {
|
|
9585
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}` : ""}
|
|
@@ -9601,31 +10161,103 @@ async function createDeploymentBundle(options) {
|
|
|
9601
10161
|
// 未选应用时只带容器原生绑定,保持当前发布的应用连接归属。
|
|
9602
10162
|
"managed-bindings": updatesApp ? managed["managed-bindings"] : managed["managed-bindings"].replace(/^.* prefix\n/gm, ""),
|
|
9603
10163
|
"managed-volumes": managed["managed-volumes"],
|
|
9604
|
-
"managed-app-keys": updatesApp ? `${[
|
|
10164
|
+
"managed-app-keys": updatesApp ? `${[
|
|
10165
|
+
...new Set(options.managedAppKeys)
|
|
10166
|
+
].join("\n")}
|
|
9605
10167
|
` : "\n",
|
|
9606
|
-
"unmanaged-app-keys": updatesApp ? `${[
|
|
10168
|
+
"unmanaged-app-keys": updatesApp ? `${[
|
|
10169
|
+
...new Set(options.unmanagedAppKeys)
|
|
10170
|
+
].join("\n")}
|
|
9607
10171
|
` : "\n",
|
|
9608
10172
|
"state-backends": backend["state-backends"]
|
|
9609
10173
|
};
|
|
9610
|
-
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)}
|
|
9611
10183
|
`);
|
|
9612
10184
|
const serverDirectory = `${config.remote.baseDir}/${projectName}`;
|
|
9613
|
-
put("apply.sh", createApplyScript({
|
|
9614
|
-
|
|
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
|
+
};
|
|
9615
10201
|
const mode = deployM(options.partial ? "deploy_bundlePartial" : "deploy_bundleFull");
|
|
9616
|
-
const selection = deployM("deploy_bundleSelection", {
|
|
10202
|
+
const selection = deployM("deploy_bundleSelection", {
|
|
10203
|
+
params: {
|
|
10204
|
+
mode,
|
|
10205
|
+
services: selected.join(", ")
|
|
10206
|
+
}
|
|
10207
|
+
});
|
|
9617
10208
|
const instructions = `${selection}
|
|
9618
10209
|
${deployM("deploy_bundleInstructions", { params: instructionParams })}`;
|
|
9619
|
-
const runtimeGuide = deployM("deploy_bundleRuntime", {
|
|
9620
|
-
|
|
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
|
+
});
|
|
9621
10216
|
const https = config.expose.https;
|
|
9622
10217
|
const certificates = https?.type === "files" ? `${serverDirectory}/releases/${options.deploymentId}/.hai/tls` : `${serverDirectory}/https/certificates/${https?.domain}`;
|
|
9623
|
-
const httpsGuide = https ? deployM("deploy_bundleHttps", {
|
|
9624
|
-
|
|
9625
|
-
|
|
9626
|
-
|
|
9627
|
-
|
|
9628
|
-
|
|
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
|
+
});
|
|
9629
10261
|
put("APPLY.txt", `${bundleMetadata}
|
|
9630
10262
|
${selection}
|
|
9631
10263
|
|
|
@@ -9643,7 +10275,13 @@ ${guides}
|
|
|
9643
10275
|
checksums.push(`${fileHashes.get(file) ?? await artifactChecksum(join(staging, file))} ${file}`);
|
|
9644
10276
|
writeFileSync(join(staging, ".hai/SHA256SUMS"), `${checksums.join("\n")}
|
|
9645
10277
|
`);
|
|
9646
|
-
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
|
+
]);
|
|
9647
10285
|
if (!tar.success)
|
|
9648
10286
|
throw new Error(tar.error.message);
|
|
9649
10287
|
chmodSync(temporaryTar, 384);
|
|
@@ -9651,7 +10289,10 @@ ${guides}
|
|
|
9651
10289
|
const upload = createUploadScripts();
|
|
9652
10290
|
const scripts = directory;
|
|
9653
10291
|
writeFileSync(join(scripts, "deploy.sh"), upload.sh, { mode: 448 });
|
|
9654
|
-
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 });
|
|
9655
10296
|
writeFileSync(join(scripts, "deploy.ps1"), upload.ps1);
|
|
9656
10297
|
writeFileSync(join(scripts, "read-target.awk"), upload.reader);
|
|
9657
10298
|
const target = {
|
|
@@ -9683,10 +10324,18 @@ ${guides}
|
|
|
9683
10324
|
writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...target, _comments: comments }, null, 2)}
|
|
9684
10325
|
`);
|
|
9685
10326
|
writeFileSync(join(scripts, "README.txt"), readme);
|
|
9686
|
-
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
|
+
])
|
|
9687
10334
|
rmSync(join(directory, file), { force: true });
|
|
9688
10335
|
return { bundlePath: output, instructions: `${selection}
|
|
9689
|
-
${deployM("deploy_bundleInstructions", {
|
|
10336
|
+
${deployM("deploy_bundleInstructions", {
|
|
10337
|
+
params: { ...instructionParams, file: output }
|
|
10338
|
+
})}
|
|
9690
10339
|
${guides}
|
|
9691
10340
|
${scripts}`, startName: "apply.sh" };
|
|
9692
10341
|
} finally {
|
|
@@ -9715,9 +10364,7 @@ function createHttpsAssets(config, projectDirectory) {
|
|
|
9715
10364
|
try {
|
|
9716
10365
|
const readPem = (file) => {
|
|
9717
10366
|
const path = resolve(projectDirectory, file);
|
|
9718
|
-
|
|
9719
|
-
throw new Error("Invalid PEM file");
|
|
9720
|
-
return readFileSync(path, "utf8");
|
|
10367
|
+
return readLocalFile(path, { maxBytes: 1048576 }).toString("utf8");
|
|
9721
10368
|
};
|
|
9722
10369
|
const certificate = readPem(config.certificateFile);
|
|
9723
10370
|
const key = readPem(config.privateKeyFile);
|
|
@@ -9910,7 +10557,21 @@ function planPostgresDb(plan, projectName, password, net) {
|
|
|
9910
10557
|
restart: "unless-stopped",
|
|
9911
10558
|
env_file: ["middleware-runtime.env"],
|
|
9912
10559
|
volumes: [`${projectName}-db-data:/var/lib/postgresql/data`],
|
|
9913
|
-
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
|
+
},
|
|
9914
10575
|
networks: net
|
|
9915
10576
|
};
|
|
9916
10577
|
plan.volumes[`${projectName}-db-data`] = null;
|
|
@@ -9929,7 +10590,12 @@ function planMysqlDb(plan, projectName, password, net) {
|
|
|
9929
10590
|
restart: "unless-stopped",
|
|
9930
10591
|
env_file: ["middleware-runtime.env"],
|
|
9931
10592
|
volumes: [`${projectName}-db-data:/var/lib/mysql`],
|
|
9932
|
-
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
|
+
},
|
|
9933
10599
|
networks: net
|
|
9934
10600
|
};
|
|
9935
10601
|
plan.volumes[`${projectName}-db-data`] = null;
|
|
@@ -9947,7 +10613,12 @@ function planRedisCache(plan, projectName, net) {
|
|
|
9947
10613
|
image: IMAGES.redis,
|
|
9948
10614
|
container_name: `hai-${projectName}-cache`,
|
|
9949
10615
|
restart: "unless-stopped",
|
|
9950
|
-
healthcheck: {
|
|
10616
|
+
healthcheck: {
|
|
10617
|
+
test: ["CMD", "redis-cli", "ping"],
|
|
10618
|
+
interval: "2s",
|
|
10619
|
+
timeout: "5s",
|
|
10620
|
+
retries: 60
|
|
10621
|
+
},
|
|
9951
10622
|
networks: net
|
|
9952
10623
|
};
|
|
9953
10624
|
plan.env.HAI_CACHE = JSON.stringify({ type: "redis", url: "redis://cache:6379" });
|
|
@@ -9959,15 +10630,40 @@ function planS3Storage(plan, projectName, password, net, engine, configured = {}
|
|
|
9959
10630
|
image: IMAGES[engine],
|
|
9960
10631
|
container_name: `hai-${projectName}-storage`,
|
|
9961
10632
|
restart: "unless-stopped",
|
|
9962
|
-
...engine === "minio" ? {
|
|
9963
|
-
|
|
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
|
+
},
|
|
9964
10654
|
env_file: ["middleware-runtime.env"],
|
|
9965
10655
|
volumes: [`${projectName}-storage-data:/data`],
|
|
9966
10656
|
networks: net
|
|
9967
10657
|
};
|
|
9968
10658
|
plan.volumes[`${projectName}-storage-data`] = null;
|
|
9969
10659
|
if (engine === "rustfs") {
|
|
9970
|
-
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
|
+
});
|
|
9971
10667
|
} else {
|
|
9972
10668
|
Object.assign(plan.containerEnv, { MINIO_ROOT_USER: "hai-storage", MINIO_ROOT_PASSWORD: password });
|
|
9973
10669
|
}
|
|
@@ -9977,7 +10673,12 @@ function planS3Storage(plan, projectName, password, net, engine, configured = {}
|
|
|
9977
10673
|
bucket: configured.bucket || "hai",
|
|
9978
10674
|
region: configured.region || "us-east-1",
|
|
9979
10675
|
// 自建只替换连接和托管凭据,不能丢失对象前缀、外部签名地址或操作日志策略。
|
|
9980
|
-
...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]])),
|
|
9981
10682
|
accessKeyId: "hai-storage",
|
|
9982
10683
|
secretAccessKey: password,
|
|
9983
10684
|
forcePathStyle: true,
|
|
@@ -10102,11 +10803,30 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
|
|
|
10102
10803
|
const password = secrets.ldapPassword ?? secrets.dbPassword;
|
|
10103
10804
|
plan.services.push("ldap");
|
|
10104
10805
|
plan.dependsOn.push("ldap");
|
|
10105
|
-
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
|
+
};
|
|
10106
10813
|
plan.volumes[`${projectName}-ldap-data`] = null;
|
|
10107
10814
|
plan.volumes[`${projectName}-ldap-config`] = null;
|
|
10108
|
-
Object.assign(plan.containerEnv, {
|
|
10109
|
-
|
|
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
|
+
});
|
|
10110
10830
|
plan.secrets.push(password);
|
|
10111
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) {
|
|
10112
10832
|
plan.services.push("email");
|
|
@@ -10114,10 +10834,25 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
|
|
|
10114
10834
|
plan.composeServices.email = { image: IMAGES.smtp, restart: "unless-stopped", networks: net };
|
|
10115
10835
|
const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
|
|
10116
10836
|
if (requirements?.length) {
|
|
10117
|
-
for (const requirement of requirements)
|
|
10118
|
-
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
|
+
}
|
|
10119
10847
|
} else if (!scan.middlewares) {
|
|
10120
|
-
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
|
+
});
|
|
10121
10856
|
}
|
|
10122
10857
|
}
|
|
10123
10858
|
}
|
|
@@ -10132,15 +10867,21 @@ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort,
|
|
|
10132
10867
|
const sidecar = planSidecars(appDir, projectName, scan, provisionEnv, options, s3Engine);
|
|
10133
10868
|
const environment = options?.environment ?? appEnv.HAI_DEPLOY_ENVIRONMENT ?? "prod";
|
|
10134
10869
|
const nodeEnv = environment === "dev" || environment === "development" ? "development" : environment === "test" ? "test" : "production";
|
|
10135
|
-
const env = mergeEnvironmentLayers([
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
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
|
+
]);
|
|
10144
10885
|
return { env, sidecar };
|
|
10145
10886
|
}
|
|
10146
10887
|
function generateComposeFile(options) {
|
|
@@ -10160,7 +10901,12 @@ function generateComposeFile(options) {
|
|
|
10160
10901
|
};
|
|
10161
10902
|
if (primaryService === "serv") {
|
|
10162
10903
|
app.healthcheck = {
|
|
10163
|
-
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
|
+
],
|
|
10164
10910
|
interval: "30s",
|
|
10165
10911
|
timeout: "5s",
|
|
10166
10912
|
start_period: "15s",
|
|
@@ -10178,14 +10924,31 @@ function generateComposeFile(options) {
|
|
|
10178
10924
|
appVolumes.push("./request-access.yml:/run/hai/request-access.yml:ro");
|
|
10179
10925
|
if (options.web || options.gateway)
|
|
10180
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
|
+
}
|
|
10181
10938
|
if (Object.keys(appEnvironment).length)
|
|
10182
10939
|
app.environment = appEnvironment;
|
|
10183
10940
|
if (options.mountWebConfig)
|
|
10184
10941
|
appVolumes.push("./web.conf:/etc/nginx/conf.d/default.conf:ro");
|
|
10185
10942
|
if (appVolumes.length)
|
|
10186
10943
|
app.volumes = appVolumes;
|
|
10187
|
-
if (sidecar.dependsOn.length > 0)
|
|
10188
|
-
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
|
+
}
|
|
10189
10952
|
const services = { [primaryService]: app, ...sidecar.composeServices };
|
|
10190
10953
|
if (options.web) {
|
|
10191
10954
|
delete app.ports;
|
|
@@ -10197,7 +10960,21 @@ function generateComposeFile(options) {
|
|
|
10197
10960
|
pull_policy: "never",
|
|
10198
10961
|
ports: [`${hostPort}:${options.web.containerPort}`],
|
|
10199
10962
|
environment: { PUBLIC_API_BASE: "" },
|
|
10200
|
-
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
|
+
},
|
|
10201
10978
|
volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
|
|
10202
10979
|
depends_on: sidecar.services.includes("storage") ? ["serv", "storage"] : ["serv"],
|
|
10203
10980
|
networks: ["hai-app"]
|
|
@@ -10210,9 +10987,26 @@ function generateComposeFile(options) {
|
|
|
10210
10987
|
container_name: `hai-${projectName}-gateway`,
|
|
10211
10988
|
restart: "unless-stopped",
|
|
10212
10989
|
ports: [`${hostPort}:8080`],
|
|
10213
|
-
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
|
+
},
|
|
10214
11005
|
volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
|
|
10215
|
-
depends_on: [
|
|
11006
|
+
depends_on: [
|
|
11007
|
+
primaryService,
|
|
11008
|
+
...sidecar.services.includes("storage") ? ["storage"] : []
|
|
11009
|
+
],
|
|
10216
11010
|
networks: ["hai-app"]
|
|
10217
11011
|
};
|
|
10218
11012
|
}
|
|
@@ -10255,7 +11049,10 @@ function generateComposeFile(options) {
|
|
|
10255
11049
|
};
|
|
10256
11050
|
if (Object.keys(sidecar.volumes).length > 0)
|
|
10257
11051
|
compose.volumes = sidecar.volumes;
|
|
10258
|
-
for (const role of [
|
|
11052
|
+
for (const role of [
|
|
11053
|
+
primaryService,
|
|
11054
|
+
...options.web ? ["web"] : []
|
|
11055
|
+
]) {
|
|
10259
11056
|
const hosts = Object.entries(options.compose?.services[role]?.extra_hosts ?? {});
|
|
10260
11057
|
if (hosts.length)
|
|
10261
11058
|
services[role].extra_hosts = hosts.map(([host, ip]) => `${host}:${ip}`);
|
|
@@ -10269,12 +11066,18 @@ function generateWebProxyConfig(port, apiPrefix, options = {}) {
|
|
|
10269
11066
|
const webPort = options.webPort ?? 8080;
|
|
10270
11067
|
const webBasePath = options.webBasePath ?? "";
|
|
10271
11068
|
const apiPath = apiPrefix.replace(/\/+$/, "");
|
|
10272
|
-
const backendPaths = [
|
|
11069
|
+
const backendPaths = [
|
|
11070
|
+
...new Set(options.backendPaths ?? ["/health"])
|
|
11071
|
+
];
|
|
10273
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}/`))) {
|
|
10274
11073
|
throw new Error("Invalid web proxy target");
|
|
10275
11074
|
}
|
|
10276
11075
|
const webLocation = renderWebLocation(webBasePath);
|
|
10277
|
-
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) : "";
|
|
10278
11081
|
const backendLocations = backendPaths.map((path) => ` location = ${path} {
|
|
10279
11082
|
proxy_pass http://hai_serv_backend;
|
|
10280
11083
|
proxy_http_version 1.1;
|
|
@@ -10366,11 +11169,13 @@ function generateApplicationProxyConfig(options) {
|
|
|
10366
11169
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
10367
11170
|
throw new Error("Invalid application proxy target");
|
|
10368
11171
|
const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
|
|
11172
|
+
const trustedEdges = renderTrustedEdgeProxies(options.trustedEdgeProxies);
|
|
11173
|
+
const forwardedProtocol = options.trustedEdgeProxies?.length ? "$hai_request_scheme" : "$scheme";
|
|
10369
11174
|
return `include /tmp/hai-dns.conf;
|
|
10370
|
-
${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 {
|
|
10371
11176
|
listen 8080;
|
|
10372
11177
|
server_name _;
|
|
10373
|
-
${
|
|
11178
|
+
${trustedEdges} large_client_header_buffers 4 32k;
|
|
10374
11179
|
${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
|
|
10375
11180
|
access_log off;
|
|
10376
11181
|
return 200 'ok';
|
|
@@ -10380,8 +11185,11 @@ ${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath)
|
|
|
10380
11185
|
proxy_pass http://hai_application_backend;
|
|
10381
11186
|
proxy_http_version 1.1;
|
|
10382
11187
|
proxy_set_header Host $http_host;
|
|
10383
|
-
proxy_set_header X-Forwarded-Proto $
|
|
11188
|
+
proxy_set_header X-Forwarded-Proto ${forwardedProtocol};
|
|
10384
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 "";
|
|
10385
11193
|
proxy_set_header X-Forwarded-For $remote_addr;
|
|
10386
11194
|
proxy_set_header X-Real-IP $remote_addr;
|
|
10387
11195
|
proxy_request_buffering off;
|
|
@@ -10409,6 +11217,20 @@ function renderTrustedEdgeProxies(entries = []) {
|
|
|
10409
11217
|
real_ip_recursive on;
|
|
10410
11218
|
`;
|
|
10411
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
|
+
}
|
|
10412
11234
|
function renderWebLocation(webBasePath) {
|
|
10413
11235
|
if (webBasePath && (!/^\/[\w.-]+(?:\/[\w.-]+)*$/.test(webBasePath) || webBasePath.split("/").some((segment) => segment === "." || segment === "..") || ["/health", "/_hai/web-health"].includes(webBasePath)))
|
|
10414
11236
|
throw new Error("Invalid web base path");
|
|
@@ -10464,8 +11286,13 @@ function prepareRequestAccess(options) {
|
|
|
10464
11286
|
if (!scope || typeof scope !== "object" || !("access" in scope))
|
|
10465
11287
|
return ok({ settings: options.settings });
|
|
10466
11288
|
const configured = RequestAccessConfigSchema.safeParse(scope.access);
|
|
10467
|
-
if (!configured.success)
|
|
10468
|
-
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
|
+
}
|
|
10469
11296
|
try {
|
|
10470
11297
|
const policy = core.access.loadPolicy(configured.data, options.directory);
|
|
10471
11298
|
const check = core.access.createRequestCheck(policy);
|
|
@@ -10474,10 +11301,20 @@ function prepareRequestAccess(options) {
|
|
|
10474
11301
|
if (configured.data === false || !("file" in configured.data))
|
|
10475
11302
|
return ok({ settings: options.settings });
|
|
10476
11303
|
const access = { file: "/run/hai/request-access.yml" };
|
|
10477
|
-
const settings = options.module === "serv" ? {
|
|
10478
|
-
|
|
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
|
+
});
|
|
10479
11312
|
} catch {
|
|
10480
|
-
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
|
+
}));
|
|
10481
11318
|
}
|
|
10482
11319
|
}
|
|
10483
11320
|
var HEALTH_MAX_ATTEMPTS = 40;
|
|
@@ -10496,10 +11333,25 @@ function endpointSettings(value) {
|
|
|
10496
11333
|
}
|
|
10497
11334
|
var ServerResultSchema = z.object({
|
|
10498
11335
|
deploymentId: z.string(),
|
|
10499
|
-
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
|
+
]),
|
|
10500
11347
|
exitCode: z.number().int().min(0).max(255),
|
|
10501
11348
|
durationSeconds: z.number().int().nonnegative(),
|
|
10502
|
-
recoveryStatus: z.enum([
|
|
11349
|
+
recoveryStatus: z.enum([
|
|
11350
|
+
"not-needed",
|
|
11351
|
+
"passed",
|
|
11352
|
+
"failed",
|
|
11353
|
+
"stopped"
|
|
11354
|
+
]).optional()
|
|
10503
11355
|
});
|
|
10504
11356
|
function makeDeploymentId() {
|
|
10505
11357
|
return `${core.time.formatDate(/* @__PURE__ */ new Date(), "YYYYMMDD-HHmmss")}-${randomBytes(3).toString("hex")}`;
|
|
@@ -10526,7 +11378,11 @@ function createDockerSshProvider(config, container) {
|
|
|
10526
11378
|
}
|
|
10527
11379
|
return err(
|
|
10528
11380
|
HaiDeployError.REMOTE_COMMAND_FAILED,
|
|
10529
|
-
deployM("deploy_remoteRuntimeMissing", {
|
|
11381
|
+
deployM("deploy_remoteRuntimeMissing", {
|
|
11382
|
+
params: {
|
|
11383
|
+
runtime: candidates.join(" / ")
|
|
11384
|
+
}
|
|
11385
|
+
})
|
|
10530
11386
|
);
|
|
10531
11387
|
}
|
|
10532
11388
|
async function healthCheck(url, certificate) {
|
|
@@ -10534,7 +11390,10 @@ function createDockerSshProvider(config, container) {
|
|
|
10534
11390
|
try {
|
|
10535
11391
|
if (certificate) {
|
|
10536
11392
|
const healthy = await new Promise((resolve9) => {
|
|
10537
|
-
const request$1 = request(url, {
|
|
11393
|
+
const request$1 = request(url, {
|
|
11394
|
+
ca: [...rootCertificates, certificate],
|
|
11395
|
+
timeout: 5e3
|
|
11396
|
+
}, (response) => {
|
|
10538
11397
|
response.resume();
|
|
10539
11398
|
resolve9(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
|
|
10540
11399
|
});
|
|
@@ -10546,7 +11405,9 @@ function createDockerSshProvider(config, container) {
|
|
|
10546
11405
|
if (healthy)
|
|
10547
11406
|
return true;
|
|
10548
11407
|
} else {
|
|
10549
|
-
const response = await fetch(url, {
|
|
11408
|
+
const response = await fetch(url, {
|
|
11409
|
+
signal: AbortSignal.timeout(5e3)
|
|
11410
|
+
});
|
|
10550
11411
|
await response.body?.cancel();
|
|
10551
11412
|
if (response.ok)
|
|
10552
11413
|
return true;
|
|
@@ -10571,10 +11432,16 @@ function createDockerSshProvider(config, container) {
|
|
|
10571
11432
|
const primaryService = artifact.primaryService;
|
|
10572
11433
|
const app = images.find((image) => image.name === primaryService);
|
|
10573
11434
|
if (options?.selectedServices) {
|
|
10574
|
-
const available = [
|
|
11435
|
+
const available = [
|
|
11436
|
+
...images.map((image) => image.name),
|
|
11437
|
+
...scan.requiredServices
|
|
11438
|
+
];
|
|
10575
11439
|
const invalid = options.selectedServices.find((name2) => !available.includes(name2));
|
|
10576
|
-
if (!options.selectedServices.length || new Set(options.selectedServices).size !== options.selectedServices.length || invalid)
|
|
10577
|
-
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
|
+
}
|
|
10578
11445
|
}
|
|
10579
11446
|
const hasWebProxy = primaryService === "serv" && artifact.hasWeb;
|
|
10580
11447
|
const partial = artifact.partial || options?.selectedServices !== void 0;
|
|
@@ -10589,14 +11456,12 @@ function createDockerSshProvider(config, container) {
|
|
|
10589
11456
|
const plannedRuntime = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
|
|
10590
11457
|
const { sidecar } = plannedRuntime;
|
|
10591
11458
|
let { env } = plannedRuntime;
|
|
10592
|
-
if (https && scan.isSvelteKit && !env.ORIGIN)
|
|
10593
|
-
env.ORIGIN = url;
|
|
10594
11459
|
const managedStorage = sidecar.services.includes("storage") ? JSON.parse(sidecar.env.HAI_STORAGE) : void 0;
|
|
10595
11460
|
let storageBasePath = "";
|
|
10596
11461
|
if (managedStorage?.publicEndpoint !== void 0) {
|
|
10597
11462
|
try {
|
|
10598
11463
|
if (typeof managedStorage.publicEndpoint !== "string")
|
|
10599
|
-
throw new
|
|
11464
|
+
throw new TypeError("Invalid public endpoint type");
|
|
10600
11465
|
const unsafeCharacters = /[\u0000-\u0020\u007F\\%]/.test(managedStorage.publicEndpoint);
|
|
10601
11466
|
const endpoint = new URL(managedStorage.publicEndpoint);
|
|
10602
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)) {
|
|
@@ -10608,7 +11473,7 @@ function createDockerSshProvider(config, container) {
|
|
|
10608
11473
|
}
|
|
10609
11474
|
}
|
|
10610
11475
|
const storageRouting = managedStorage ? createHash("sha256").update(JSON.stringify({ ...managedStorage, secretAccessKey: void 0 })).digest("hex") : "";
|
|
10611
|
-
const gateway = !hasWebProxy && (!!managedStorage || !!https && !standaloneWebConfig);
|
|
11476
|
+
const gateway = !hasWebProxy && (scan.isSvelteKit || !!managedStorage || !!https && !standaloneWebConfig);
|
|
10612
11477
|
if (gateway && primaryService === "web" && !scan.isSvelteKit)
|
|
10613
11478
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
|
|
10614
11479
|
const proxyService = gateway ? "gateway" : "web";
|
|
@@ -10670,8 +11535,14 @@ function createDockerSshProvider(config, container) {
|
|
|
10670
11535
|
return prepared2;
|
|
10671
11536
|
Object.assign(server.data, prepared2.data.settings);
|
|
10672
11537
|
requestAccessSnapshot = prepared2.data.snapshot;
|
|
10673
|
-
if (requestAccessSnapshot)
|
|
10674
|
-
env = mergeEnvironmentLayers([
|
|
11538
|
+
if (requestAccessSnapshot) {
|
|
11539
|
+
env = mergeEnvironmentLayers([
|
|
11540
|
+
env,
|
|
11541
|
+
{
|
|
11542
|
+
HAI_SERV: JSON.stringify(server.data)
|
|
11543
|
+
}
|
|
11544
|
+
]);
|
|
11545
|
+
}
|
|
10675
11546
|
}
|
|
10676
11547
|
let feedbackTransportBytes;
|
|
10677
11548
|
if (scan.haiModules?.includes("@h-ai/feedback")) {
|
|
@@ -10697,8 +11568,14 @@ function createDockerSshProvider(config, container) {
|
|
|
10697
11568
|
return prepared2;
|
|
10698
11569
|
Object.assign(kit.data, prepared2.data.settings);
|
|
10699
11570
|
requestAccessSnapshot = prepared2.data.snapshot;
|
|
10700
|
-
if (requestAccessSnapshot)
|
|
10701
|
-
env = mergeEnvironmentLayers([
|
|
11571
|
+
if (requestAccessSnapshot) {
|
|
11572
|
+
env = mergeEnvironmentLayers([
|
|
11573
|
+
env,
|
|
11574
|
+
{
|
|
11575
|
+
HAI_KIT: JSON.stringify(kit.data)
|
|
11576
|
+
}
|
|
11577
|
+
]);
|
|
11578
|
+
}
|
|
10702
11579
|
}
|
|
10703
11580
|
const transport = kit.data.transport;
|
|
10704
11581
|
if (transport && typeof transport === "object") {
|
|
@@ -10714,8 +11591,13 @@ function createDockerSshProvider(config, container) {
|
|
|
10714
11591
|
}
|
|
10715
11592
|
}
|
|
10716
11593
|
}
|
|
10717
|
-
if (updatesApp && protectedTransport && scan.haiModules?.includes("@h-ai/crypto") && !parseTransportServerKeyRing(env.HAI_CRYPTO_TRANSPORT_SERVERKEYRING).success)
|
|
10718
|
-
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
|
+
}
|
|
10719
11601
|
let webProxy;
|
|
10720
11602
|
if (hasWebProxy) {
|
|
10721
11603
|
const http = server.data.http;
|
|
@@ -10730,11 +11612,30 @@ function createDockerSshProvider(config, container) {
|
|
|
10730
11612
|
try {
|
|
10731
11613
|
const configured = transport ? transport.maxPlaintextBytes : void 0;
|
|
10732
11614
|
const limits = resolveTransportBodyLimits(Math.max(createTransportBodyPolicy(configured === "auto" ? void 0 : configured, transport ? transport.routePlaintextBytes : void 0).maximum.plaintextBytes, feedbackTransportBytes ?? 0));
|
|
10733
|
-
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
|
+
});
|
|
10734
11624
|
} catch (error) {
|
|
10735
11625
|
return err(HaiDeployError.CONFIG_ERROR, error instanceof RangeError ? error.message : deployM(managedStorage ? "deploy_storageProxyInvalid" : "deploy_transportBodyLimitInvalid"));
|
|
10736
11626
|
}
|
|
10737
|
-
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
|
+
]);
|
|
10738
11639
|
} else if (gateway) {
|
|
10739
11640
|
try {
|
|
10740
11641
|
const http = server.data.http;
|
|
@@ -10742,7 +11643,18 @@ function createDockerSshProvider(config, container) {
|
|
|
10742
11643
|
const transport = server.data.transport;
|
|
10743
11644
|
applicationPlaintextBytes = Math.max(createTransportBodyPolicy(transport?.maxPlaintextBytes === "auto" ? void 0 : transport?.maxPlaintextBytes, transport?.routePlaintextBytes).maximum.plaintextBytes, feedbackTransportBytes ?? 0);
|
|
10744
11645
|
}
|
|
10745
|
-
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
|
+
});
|
|
10746
11658
|
} catch {
|
|
10747
11659
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageProxyInvalid"));
|
|
10748
11660
|
}
|
|
@@ -10751,11 +11663,23 @@ function createDockerSshProvider(config, container) {
|
|
|
10751
11663
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
|
|
10752
11664
|
webProxy = generateStaticWebConfig(containerPort, webBasePath);
|
|
10753
11665
|
}
|
|
10754
|
-
if (https && webProxy)
|
|
11666
|
+
if (https && webProxy) {
|
|
10755
11667
|
webProxy = renderHttpsProxy(webProxy, https, hasWebProxy ? webContainerPort : gateway ? 8080 : containerPort);
|
|
11668
|
+
}
|
|
10756
11669
|
if (https && primaryService === "serv" && !hasWebProxy) {
|
|
10757
11670
|
const cors = server.data.cors;
|
|
10758
|
-
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
|
+
]);
|
|
10759
11683
|
}
|
|
10760
11684
|
const requestedOutput = options?.output ?? join(deploymentRoot, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar.gz`);
|
|
10761
11685
|
const compressed = requestedOutput.endsWith(".tar.gz");
|
|
@@ -10765,20 +11689,43 @@ function createDockerSshProvider(config, container) {
|
|
|
10765
11689
|
if (!/^[\w.+-]+$/.test(name))
|
|
10766
11690
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_bundleNameInvalid"));
|
|
10767
11691
|
const artifactDir = resolve(dirname(requestedOutput), name);
|
|
10768
|
-
if (existsSync(artifactDir))
|
|
10769
|
-
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
|
+
}
|
|
10770
11697
|
mkdirSync(dirname(artifactDir), { recursive: true, mode: 448 });
|
|
10771
11698
|
mkdirSync(artifactDir, { mode: 448 });
|
|
10772
11699
|
chmodSync(artifactDir, 448);
|
|
10773
11700
|
const envPath = join(artifactDir, ".env.runtime");
|
|
10774
11701
|
const nodeRuntime = primaryService === "serv" || scan.isSvelteKit;
|
|
10775
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
|
+
}
|
|
10776
11714
|
const runtimeEnvironment = serializeEnvFile(env, startupOverrides, nodeRuntime ? "node" : "compose");
|
|
10777
|
-
const envVarsSet = [
|
|
10778
|
-
|
|
10779
|
-
|
|
10780
|
-
|
|
10781
|
-
|
|
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
|
+
];
|
|
10782
11729
|
writeFileSync(envPath, runtimeEnvironment, { mode: 384 });
|
|
10783
11730
|
if (requestAccessSnapshot)
|
|
10784
11731
|
writeFileSync(join(artifactDir, "request-access.yml"), requestAccessSnapshot, { mode: 384 });
|
|
@@ -10787,7 +11734,12 @@ function createDockerSshProvider(config, container) {
|
|
|
10787
11734
|
const environment = loadDeploymentEnvironment({ environment: options.environment, envDir: options.envDir ?? scan.envDir ?? appDir });
|
|
10788
11735
|
if (!environment.success)
|
|
10789
11736
|
return environment;
|
|
10790
|
-
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
|
+
});
|
|
10791
11743
|
if (!saved.success)
|
|
10792
11744
|
return saved;
|
|
10793
11745
|
}
|
|
@@ -10813,16 +11765,27 @@ function createDockerSshProvider(config, container) {
|
|
|
10813
11765
|
writeFileSync(composePath, composeContent);
|
|
10814
11766
|
if (webProxy)
|
|
10815
11767
|
writeFileSync(join(artifactDir, "web.conf"), webProxy);
|
|
10816
|
-
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
|
+
];
|
|
10817
11772
|
if (gateway && !partial)
|
|
10818
11773
|
selected.push("gateway");
|
|
10819
11774
|
if (https?.type === "letsencrypt" && !partial)
|
|
10820
11775
|
selected.push("certbot");
|
|
10821
|
-
if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length)
|
|
10822
|
-
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
|
+
}
|
|
10823
11783
|
for (const name2 of selected) {
|
|
10824
|
-
if (["serv", "web"].includes(name2) && !images.some((image) => image.name === name2))
|
|
10825
|
-
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
|
+
}
|
|
10826
11789
|
}
|
|
10827
11790
|
options?.onProgress?.("packaging");
|
|
10828
11791
|
const bundle = await createDeploymentBundle({
|
|
@@ -10844,14 +11807,28 @@ function createDockerSshProvider(config, container) {
|
|
|
10844
11807
|
containerPort,
|
|
10845
11808
|
managedAppKeys: Object.keys(sidecar.env),
|
|
10846
11809
|
unmanagedAppKeys: scan.middlewares?.filter((item) => !Object.hasOwn(sidecar.env, item.envPrefix)).map((item) => item.envPrefix),
|
|
10847
|
-
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
|
+
]),
|
|
10848
11816
|
healthPaths,
|
|
10849
11817
|
httpsAssets,
|
|
10850
11818
|
output: join(artifactDir, `${name}.tar${compressed ? ".gz" : ""}`),
|
|
10851
11819
|
cacheDirectory: join(resolveProjectDirectory(appDir), ".hai", "cache", "images")
|
|
10852
11820
|
});
|
|
10853
|
-
if (options?.bundleOnly)
|
|
10854
|
-
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
|
+
}
|
|
10855
11832
|
if (!ssh)
|
|
10856
11833
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
10857
11834
|
const ping = await ssh.ping();
|
|
@@ -10871,19 +11848,23 @@ function createDockerSshProvider(config, container) {
|
|
|
10871
11848
|
let pendingOutput = "";
|
|
10872
11849
|
const started = await ssh.exec(
|
|
10873
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; }`,
|
|
10874
|
-
{
|
|
10875
|
-
|
|
10876
|
-
|
|
10877
|
-
|
|
10878
|
-
|
|
10879
|
-
|
|
10880
|
-
|
|
10881
|
-
|
|
10882
|
-
|
|
10883
|
-
|
|
10884
|
-
|
|
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
|
+
}
|
|
10885
11866
|
}
|
|
10886
|
-
}
|
|
11867
|
+
}
|
|
10887
11868
|
);
|
|
10888
11869
|
if (!started.success) {
|
|
10889
11870
|
const resultFile = `${remoteDir}/result-${deploymentId}.json`;
|
|
@@ -10898,16 +11879,34 @@ function createDockerSshProvider(config, container) {
|
|
|
10898
11879
|
} catch {
|
|
10899
11880
|
}
|
|
10900
11881
|
}
|
|
10901
|
-
return err({
|
|
10902
|
-
|
|
10903
|
-
|
|
10904
|
-
|
|
10905
|
-
|
|
10906
|
-
|
|
10907
|
-
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
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
|
+
});
|
|
10911
11910
|
}
|
|
10912
11911
|
logger5.info("Running health check", { url: applicationUrl });
|
|
10913
11912
|
let healthy = true;
|
|
@@ -10919,16 +11918,24 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
|
|
|
10919
11918
|
}
|
|
10920
11919
|
if (!healthy) {
|
|
10921
11920
|
return err(
|
|
10922
|
-
{
|
|
10923
|
-
|
|
10924
|
-
|
|
10925
|
-
|
|
10926
|
-
|
|
10927
|
-
|
|
10928
|
-
|
|
10929
|
-
|
|
10930
|
-
|
|
10931
|
-
|
|
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
|
+
}
|
|
10932
11939
|
);
|
|
10933
11940
|
}
|
|
10934
11941
|
const result = {
|
|
@@ -10950,11 +11957,16 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
|
|
|
10950
11957
|
const reason = error instanceof Error ? error.message : String(error);
|
|
10951
11958
|
let safeReason = "Deployment bundle failed";
|
|
10952
11959
|
try {
|
|
10953
|
-
safeReason = maskSecrets(reason, [
|
|
11960
|
+
safeReason = maskSecrets(reason, [
|
|
11961
|
+
...Object.values(context.appEnv ?? {}),
|
|
11962
|
+
config.ssh?.password ?? ""
|
|
11963
|
+
].filter(Boolean));
|
|
10954
11964
|
} catch {
|
|
10955
11965
|
}
|
|
10956
11966
|
logger5.error("Deployment bundle failed", { reason: safeReason });
|
|
10957
|
-
return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", {
|
|
11967
|
+
return err(HaiDeployError.DEPLOY_FAILED, deployM("deploy_bundleFailed", {
|
|
11968
|
+
params: { reason: safeReason }
|
|
11969
|
+
}));
|
|
10958
11970
|
}
|
|
10959
11971
|
};
|
|
10960
11972
|
return { name: "docker-ssh", deploy: deploySafely };
|
|
@@ -10965,7 +11977,12 @@ var POLL_INTERVAL = 3e3;
|
|
|
10965
11977
|
var MAX_POLL_ATTEMPTS = 60;
|
|
10966
11978
|
var VercelApiError = class extends Error {
|
|
10967
11979
|
constructor(status) {
|
|
10968
|
-
super(deployM("deploy_apiError", {
|
|
11980
|
+
super(deployM("deploy_apiError", {
|
|
11981
|
+
params: {
|
|
11982
|
+
service: "Vercel",
|
|
11983
|
+
status: String(status)
|
|
11984
|
+
}
|
|
11985
|
+
}));
|
|
10969
11986
|
this.status = status;
|
|
10970
11987
|
}
|
|
10971
11988
|
status;
|
|
@@ -10974,7 +11991,9 @@ function toDeployError(codeDef, messageKey, error) {
|
|
|
10974
11991
|
return {
|
|
10975
11992
|
...codeDef,
|
|
10976
11993
|
message: deployM(messageKey, {
|
|
10977
|
-
params: {
|
|
11994
|
+
params: {
|
|
11995
|
+
error: error instanceof Error ? error.message : String(error)
|
|
11996
|
+
}
|
|
10978
11997
|
}),
|
|
10979
11998
|
cause: error
|
|
10980
11999
|
};
|
|
@@ -11184,7 +12203,11 @@ async function uploadFile(token, content, sha) {
|
|
|
11184
12203
|
body: new Blob([uploadContent])
|
|
11185
12204
|
});
|
|
11186
12205
|
if (!response.ok && response.status !== 409) {
|
|
11187
|
-
throw new Error(deployM("deploy_fileUploadFailed", {
|
|
12206
|
+
throw new Error(deployM("deploy_fileUploadFailed", {
|
|
12207
|
+
params: {
|
|
12208
|
+
status: String(response.status)
|
|
12209
|
+
}
|
|
12210
|
+
}));
|
|
11188
12211
|
}
|
|
11189
12212
|
}
|
|
11190
12213
|
async function pollDeploymentStatus(token, deploymentId) {
|
|
@@ -11217,7 +12240,9 @@ function createAliyunProvisioner() {
|
|
|
11217
12240
|
const akId = credentials.accessKeyId ?? credentials.access_key_id ?? credentials.access_key ?? "";
|
|
11218
12241
|
const akSecret = credentials.accessKeySecret ?? credentials.access_key_secret ?? credentials.secret_key ?? "";
|
|
11219
12242
|
if (!akId || !akSecret || !credentials.signName) {
|
|
11220
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12243
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12244
|
+
params: { fields: "accessKeyId, accessKeySecret, signName" }
|
|
12245
|
+
}));
|
|
11221
12246
|
}
|
|
11222
12247
|
accessKey = akId;
|
|
11223
12248
|
secretKey = akSecret;
|
|
@@ -11229,7 +12254,9 @@ function createAliyunProvisioner() {
|
|
|
11229
12254
|
return err(
|
|
11230
12255
|
HaiDeployError.AUTH_FAILED,
|
|
11231
12256
|
deployM("deploy_authFailed", {
|
|
11232
|
-
params: {
|
|
12257
|
+
params: {
|
|
12258
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12259
|
+
}
|
|
11233
12260
|
}),
|
|
11234
12261
|
error
|
|
11235
12262
|
);
|
|
@@ -11248,7 +12275,15 @@ function createAliyunProvisioner() {
|
|
|
11248
12275
|
provisionerName: "aliyun",
|
|
11249
12276
|
resourceStatus: "reused",
|
|
11250
12277
|
envVars: {
|
|
11251
|
-
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
|
+
])
|
|
11252
12287
|
},
|
|
11253
12288
|
resourceInfo: "aliyun:verify-only"
|
|
11254
12289
|
});
|
|
@@ -11270,7 +12305,12 @@ async function neonFetch(token, path, options) {
|
|
|
11270
12305
|
}
|
|
11271
12306
|
});
|
|
11272
12307
|
if (!response.ok) {
|
|
11273
|
-
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
|
+
}));
|
|
11274
12314
|
}
|
|
11275
12315
|
return response.json();
|
|
11276
12316
|
}
|
|
@@ -11290,10 +12330,14 @@ async function getNeonConnectionUri(token, projectId) {
|
|
|
11290
12330
|
const data = await neonFetch(
|
|
11291
12331
|
token,
|
|
11292
12332
|
`/projects/${projectId}/connection_uri?${query.toString()}`,
|
|
11293
|
-
{
|
|
12333
|
+
{
|
|
12334
|
+
headers: { "Content-Type": "application/json" }
|
|
12335
|
+
}
|
|
11294
12336
|
);
|
|
11295
12337
|
if (!data.uri) {
|
|
11296
|
-
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12338
|
+
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12339
|
+
params: { service: "Neon" }
|
|
12340
|
+
}));
|
|
11297
12341
|
}
|
|
11298
12342
|
return data.uri;
|
|
11299
12343
|
}
|
|
@@ -11303,7 +12347,11 @@ function buildNeonProvisionResult(projectId, connectionUri, resourceStatus) {
|
|
|
11303
12347
|
provisionerName: "neon",
|
|
11304
12348
|
resourceStatus,
|
|
11305
12349
|
envVars: {
|
|
11306
|
-
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
|
+
})
|
|
11307
12355
|
},
|
|
11308
12356
|
resourceInfo: `neon-project:${projectId}`
|
|
11309
12357
|
};
|
|
@@ -11318,7 +12366,9 @@ function createNeonProvisioner() {
|
|
|
11318
12366
|
try {
|
|
11319
12367
|
const apiToken = credentials.apiKey ?? credentials.token ?? credentials.api_key ?? "";
|
|
11320
12368
|
if (!apiToken) {
|
|
11321
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12369
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12370
|
+
params: { fields: "token" }
|
|
12371
|
+
}));
|
|
11322
12372
|
}
|
|
11323
12373
|
await listNeonProjects(apiToken);
|
|
11324
12374
|
token = apiToken;
|
|
@@ -11329,7 +12379,9 @@ function createNeonProvisioner() {
|
|
|
11329
12379
|
return err(
|
|
11330
12380
|
HaiDeployError.AUTH_FAILED,
|
|
11331
12381
|
deployM("deploy_authFailed", {
|
|
11332
|
-
params: {
|
|
12382
|
+
params: {
|
|
12383
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12384
|
+
}
|
|
11333
12385
|
}),
|
|
11334
12386
|
error
|
|
11335
12387
|
);
|
|
@@ -11370,7 +12422,9 @@ function createNeonProvisioner() {
|
|
|
11370
12422
|
resource = { resourceInfo: `neon-project:${projectId}`, resourceStatus: "created" };
|
|
11371
12423
|
const connectionUri = data.connection_uris?.[0]?.connection_uri ?? "";
|
|
11372
12424
|
if (!projectId || !connectionUri) {
|
|
11373
|
-
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12425
|
+
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12426
|
+
params: { service: "Neon" }
|
|
12427
|
+
}));
|
|
11374
12428
|
}
|
|
11375
12429
|
logger8.info("Neon database provisioned", { projectId });
|
|
11376
12430
|
return ok(buildNeonProvisionResult(projectId, connectionUri, "created"));
|
|
@@ -11380,7 +12434,9 @@ function createNeonProvisioner() {
|
|
|
11380
12434
|
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
11381
12435
|
projectName: appName,
|
|
11382
12436
|
stage: "provision:db",
|
|
11383
|
-
resources: [
|
|
12437
|
+
resources: [
|
|
12438
|
+
{ serviceType: "db", provisionerName: "neon", ...resource }
|
|
12439
|
+
]
|
|
11384
12440
|
});
|
|
11385
12441
|
}
|
|
11386
12442
|
return err(
|
|
@@ -11413,13 +12469,20 @@ function createR2Provisioner() {
|
|
|
11413
12469
|
const acctId = credentials.accountId ?? credentials.account_id ?? "";
|
|
11414
12470
|
const apiTok = credentials.apiToken ?? credentials.api_token ?? credentials.token ?? "";
|
|
11415
12471
|
if (!acctId || !apiTok || !credentials.accessKeyId || !credentials.secretAccessKey) {
|
|
11416
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12472
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12473
|
+
params: { fields: "accountId, apiToken, accessKeyId, secretAccessKey" }
|
|
12474
|
+
}));
|
|
11417
12475
|
}
|
|
11418
12476
|
const res = await fetch(`${CF_API}/accounts/${acctId}/r2/buckets`, {
|
|
11419
12477
|
headers: { Authorization: `Bearer ${apiTok}` }
|
|
11420
12478
|
});
|
|
11421
12479
|
if (!res.ok) {
|
|
11422
|
-
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
|
+
}));
|
|
11423
12486
|
}
|
|
11424
12487
|
accountId = acctId;
|
|
11425
12488
|
token = apiTok;
|
|
@@ -11432,7 +12495,9 @@ function createR2Provisioner() {
|
|
|
11432
12495
|
return err(
|
|
11433
12496
|
HaiDeployError.AUTH_FAILED,
|
|
11434
12497
|
deployM("deploy_authFailed", {
|
|
11435
|
-
params: {
|
|
12498
|
+
params: {
|
|
12499
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12500
|
+
}
|
|
11436
12501
|
}),
|
|
11437
12502
|
error
|
|
11438
12503
|
);
|
|
@@ -11460,7 +12525,12 @@ function createR2Provisioner() {
|
|
|
11460
12525
|
}
|
|
11461
12526
|
);
|
|
11462
12527
|
if (!createRes.ok && createRes.status !== 409) {
|
|
11463
|
-
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
|
+
}));
|
|
11464
12534
|
}
|
|
11465
12535
|
logger9.info("R2 bucket provisioned", { bucketName });
|
|
11466
12536
|
return ok({
|
|
@@ -11508,13 +12578,20 @@ function createResendProvisioner() {
|
|
|
11508
12578
|
try {
|
|
11509
12579
|
const apiToken = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
11510
12580
|
if (!apiToken || !credentials.from) {
|
|
11511
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12581
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12582
|
+
params: { fields: "apiKey, from" }
|
|
12583
|
+
}));
|
|
11512
12584
|
}
|
|
11513
12585
|
const res = await fetch(`${RESEND_API}/domains`, {
|
|
11514
12586
|
headers: { Authorization: `Bearer ${apiToken}` }
|
|
11515
12587
|
});
|
|
11516
12588
|
if (!res.ok) {
|
|
11517
|
-
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
|
+
}));
|
|
11518
12595
|
}
|
|
11519
12596
|
token = apiToken;
|
|
11520
12597
|
from = credentials.from;
|
|
@@ -11525,7 +12602,9 @@ function createResendProvisioner() {
|
|
|
11525
12602
|
return err(
|
|
11526
12603
|
HaiDeployError.AUTH_FAILED,
|
|
11527
12604
|
deployM("deploy_authFailed", {
|
|
11528
|
-
params: {
|
|
12605
|
+
params: {
|
|
12606
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12607
|
+
}
|
|
11529
12608
|
}),
|
|
11530
12609
|
error
|
|
11531
12610
|
);
|
|
@@ -11544,7 +12623,18 @@ function createResendProvisioner() {
|
|
|
11544
12623
|
provisionerName: "resend",
|
|
11545
12624
|
resourceStatus: "reused",
|
|
11546
12625
|
envVars: {
|
|
11547
|
-
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
|
+
])
|
|
11548
12638
|
},
|
|
11549
12639
|
resourceInfo: "resend:verify-only"
|
|
11550
12640
|
});
|
|
@@ -11566,7 +12656,12 @@ async function upstashFetch(email, apiKey, path, options) {
|
|
|
11566
12656
|
}
|
|
11567
12657
|
});
|
|
11568
12658
|
if (!response.ok) {
|
|
11569
|
-
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
|
+
}));
|
|
11570
12665
|
}
|
|
11571
12666
|
return response.json();
|
|
11572
12667
|
}
|
|
@@ -11583,7 +12678,9 @@ async function getUpstashDatabase(email, apiKey, databaseId) {
|
|
|
11583
12678
|
function buildUpstashProvisionResult(database, resourceStatus) {
|
|
11584
12679
|
const databaseId = database.database_id ?? "";
|
|
11585
12680
|
if (!databaseId || !database.endpoint || !database.password || !database.port) {
|
|
11586
|
-
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12681
|
+
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12682
|
+
params: { service: "Upstash" }
|
|
12683
|
+
}));
|
|
11587
12684
|
}
|
|
11588
12685
|
return {
|
|
11589
12686
|
serviceType: "cache",
|
|
@@ -11607,7 +12704,9 @@ function createUpstashProvisioner() {
|
|
|
11607
12704
|
const userEmail = credentials.email ?? "";
|
|
11608
12705
|
const userKey = credentials.apiKey ?? credentials.api_key ?? credentials.token ?? "";
|
|
11609
12706
|
if (!userEmail || !userKey) {
|
|
11610
|
-
throw new Error(deployM("deploy_credentialMissing", {
|
|
12707
|
+
throw new Error(deployM("deploy_credentialMissing", {
|
|
12708
|
+
params: { fields: "email, api_key" }
|
|
12709
|
+
}));
|
|
11611
12710
|
}
|
|
11612
12711
|
await listUpstashDatabases(userEmail, userKey);
|
|
11613
12712
|
email = userEmail;
|
|
@@ -11619,7 +12718,9 @@ function createUpstashProvisioner() {
|
|
|
11619
12718
|
return err(
|
|
11620
12719
|
HaiDeployError.AUTH_FAILED,
|
|
11621
12720
|
deployM("deploy_authFailed", {
|
|
11622
|
-
params: {
|
|
12721
|
+
params: {
|
|
12722
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12723
|
+
}
|
|
11623
12724
|
}),
|
|
11624
12725
|
error
|
|
11625
12726
|
);
|
|
@@ -11640,12 +12741,17 @@ function createUpstashProvisioner() {
|
|
|
11640
12741
|
if (existingDatabase !== null) {
|
|
11641
12742
|
const databaseId = existingDatabase.database_id ?? "";
|
|
11642
12743
|
if (!databaseId) {
|
|
11643
|
-
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12744
|
+
throw new Error(deployM("deploy_provisionNoResult", {
|
|
12745
|
+
params: { service: "Upstash" }
|
|
12746
|
+
}));
|
|
11644
12747
|
}
|
|
11645
12748
|
resource = { resourceInfo: `upstash-db:${databaseId}`, resourceStatus: "reused" };
|
|
11646
12749
|
const detail = await getUpstashDatabase(email, apiKey, databaseId);
|
|
11647
12750
|
logger11.info("Upstash Redis reused", { databaseId, databaseName });
|
|
11648
|
-
return ok(buildUpstashProvisionResult({
|
|
12751
|
+
return ok(buildUpstashProvisionResult({
|
|
12752
|
+
...existingDatabase,
|
|
12753
|
+
...detail
|
|
12754
|
+
}, "reused"));
|
|
11649
12755
|
}
|
|
11650
12756
|
const data = await upstashFetch(email, apiKey, "/v2/redis/database", {
|
|
11651
12757
|
method: "POST",
|
|
@@ -11665,7 +12771,9 @@ function createUpstashProvisioner() {
|
|
|
11665
12771
|
return recoveryFailure({ ...HaiDeployError.PROVISION_FAILED}, {
|
|
11666
12772
|
projectName: appName,
|
|
11667
12773
|
stage: "provision:cache",
|
|
11668
|
-
resources: [
|
|
12774
|
+
resources: [
|
|
12775
|
+
{ serviceType: "cache", provisionerName: "upstash", ...resource }
|
|
12776
|
+
]
|
|
11669
12777
|
});
|
|
11670
12778
|
}
|
|
11671
12779
|
return err(
|
|
@@ -11699,7 +12807,9 @@ function createProviderByType(type) {
|
|
|
11699
12807
|
case "vercel":
|
|
11700
12808
|
return createVercelProvider();
|
|
11701
12809
|
default:
|
|
11702
|
-
throw new Error(deployM("deploy_unsupportedProviderType", {
|
|
12810
|
+
throw new Error(deployM("deploy_unsupportedProviderType", {
|
|
12811
|
+
params: { type }
|
|
12812
|
+
}));
|
|
11703
12813
|
}
|
|
11704
12814
|
}
|
|
11705
12815
|
function createProvisionerByName(provisioner) {
|
|
@@ -11715,7 +12825,9 @@ function createProvisionerByName(provisioner) {
|
|
|
11715
12825
|
case "aliyun":
|
|
11716
12826
|
return createAliyunProvisioner();
|
|
11717
12827
|
default:
|
|
11718
|
-
throw new Error(deployM("deploy_unsupportedProvisionerType", {
|
|
12828
|
+
throw new Error(deployM("deploy_unsupportedProvisionerType", {
|
|
12829
|
+
params: { type: provisioner }
|
|
12830
|
+
}));
|
|
11719
12831
|
}
|
|
11720
12832
|
}
|
|
11721
12833
|
function extractCredentials(serviceConfig) {
|
|
@@ -11748,8 +12860,12 @@ function mergeProvisionEnv(results, scan, options) {
|
|
|
11748
12860
|
return item.service === prov.serviceType && (!mode || mode === "default");
|
|
11749
12861
|
});
|
|
11750
12862
|
if (targets.length) {
|
|
11751
|
-
for (const target of targets)
|
|
11752
|
-
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
|
+
}
|
|
11753
12869
|
} else {
|
|
11754
12870
|
reachProviders.push(provider);
|
|
11755
12871
|
}
|
|
@@ -11771,7 +12887,9 @@ var deploy = {
|
|
|
11771
12887
|
logger12.warn("Deploy init already in progress, skipping concurrent call");
|
|
11772
12888
|
return err(
|
|
11773
12889
|
HaiDeployError.CONFIG_ERROR,
|
|
11774
|
-
deployM("deploy_configError", {
|
|
12890
|
+
deployM("deploy_configError", {
|
|
12891
|
+
params: { error: "init already in progress" }
|
|
12892
|
+
})
|
|
11775
12893
|
);
|
|
11776
12894
|
}
|
|
11777
12895
|
initInProgress = true;
|
|
@@ -11786,7 +12904,9 @@ var deploy = {
|
|
|
11786
12904
|
logger12.error("Deploy config validation failed", { error: parseResult.error.message });
|
|
11787
12905
|
return err(
|
|
11788
12906
|
HaiDeployError.CONFIG_ERROR,
|
|
11789
|
-
deployM("deploy_configError", {
|
|
12907
|
+
deployM("deploy_configError", {
|
|
12908
|
+
params: { error: parseResult.error.message }
|
|
12909
|
+
}),
|
|
11790
12910
|
parseResult.error
|
|
11791
12911
|
);
|
|
11792
12912
|
}
|
|
@@ -11834,7 +12954,9 @@ var deploy = {
|
|
|
11834
12954
|
currentConfig = parsed;
|
|
11835
12955
|
logger12.info("Deploy module initialized", {
|
|
11836
12956
|
provider: parsed.provider.type,
|
|
11837
|
-
provisioners: [
|
|
12957
|
+
provisioners: [
|
|
12958
|
+
...currentProvisioners.keys()
|
|
12959
|
+
]
|
|
11838
12960
|
});
|
|
11839
12961
|
return ok(void 0);
|
|
11840
12962
|
} catch (error) {
|
|
@@ -11846,7 +12968,9 @@ var deploy = {
|
|
|
11846
12968
|
return err(
|
|
11847
12969
|
HaiDeployError.CONFIG_ERROR,
|
|
11848
12970
|
deployM("deploy_configError", {
|
|
11849
|
-
params: {
|
|
12971
|
+
params: {
|
|
12972
|
+
error: error instanceof Error ? error.message : String(error)
|
|
12973
|
+
}
|
|
11850
12974
|
}),
|
|
11851
12975
|
error
|
|
11852
12976
|
);
|
|
@@ -11900,7 +13024,12 @@ var deploy = {
|
|
|
11900
13024
|
resources: [
|
|
11901
13025
|
...results.map(({ envVars: _envVars, ...resource }) => resource),
|
|
11902
13026
|
...getDeployRecovery(provResult.error)?.resources ?? [
|
|
11903
|
-
{
|
|
13027
|
+
{
|
|
13028
|
+
serviceType,
|
|
13029
|
+
provisionerName: provisioner.name,
|
|
13030
|
+
resourceInfo: projectName,
|
|
13031
|
+
resourceStatus: "unknown"
|
|
13032
|
+
}
|
|
11904
13033
|
]
|
|
11905
13034
|
]
|
|
11906
13035
|
});
|
|
@@ -11938,7 +13067,9 @@ var deploy = {
|
|
|
11938
13067
|
connectionVariables.add(match[1]);
|
|
11939
13068
|
}
|
|
11940
13069
|
} catch {
|
|
11941
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
13070
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
13071
|
+
params: { path: projectFile, line: "1" }
|
|
13072
|
+
}));
|
|
11942
13073
|
}
|
|
11943
13074
|
}
|
|
11944
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")));
|
|
@@ -11946,7 +13077,9 @@ var deploy = {
|
|
|
11946
13077
|
options = {
|
|
11947
13078
|
...options,
|
|
11948
13079
|
...scanOptions.environment ? { environment: environment.data.environment } : {},
|
|
11949
|
-
...options?.skipMiddleware ? {
|
|
13080
|
+
...options?.skipMiddleware ? {
|
|
13081
|
+
serviceModes: Object.fromEntries(scan.requiredServices.map((service) => [service, "keep"]))
|
|
13082
|
+
} : {}
|
|
11950
13083
|
};
|
|
11951
13084
|
if (options.selectedServices) {
|
|
11952
13085
|
options.serviceModes = {
|
|
@@ -11982,7 +13115,10 @@ var deploy = {
|
|
|
11982
13115
|
return err(provResults.error);
|
|
11983
13116
|
}
|
|
11984
13117
|
recovery.resources = provResults.data.map(({ envVars: _envVars, ...resource }) => resource);
|
|
11985
|
-
allEnvVars = {
|
|
13118
|
+
allEnvVars = {
|
|
13119
|
+
...allEnvVars,
|
|
13120
|
+
...mergeProvisionEnv(provResults.data, scan, options)
|
|
13121
|
+
};
|
|
11986
13122
|
if (options?.environment) {
|
|
11987
13123
|
const saved = saveDeploymentEnvironment(environment.data, mergeProvisionEnv(provResults.data, scan, options));
|
|
11988
13124
|
if (!saved.success)
|
|
@@ -12058,7 +13194,16 @@ async function deployContainerApp(provider, appDir, scan, rawProjectName, option
|
|
|
12058
13194
|
}
|
|
12059
13195
|
provisionEnv = mergeProvisionEnv(provResults.data, scan, options);
|
|
12060
13196
|
}
|
|
12061
|
-
return provider.deploy({
|
|
13197
|
+
return provider.deploy({
|
|
13198
|
+
appDir,
|
|
13199
|
+
artifact,
|
|
13200
|
+
artifactDir,
|
|
13201
|
+
projectName,
|
|
13202
|
+
scan,
|
|
13203
|
+
provisionEnv,
|
|
13204
|
+
appEnv,
|
|
13205
|
+
options
|
|
13206
|
+
});
|
|
12062
13207
|
}
|
|
12063
13208
|
z.enum(["sqlite", "postgresql", "mysql"]);
|
|
12064
13209
|
var ReldbOperationLogConfigSchema = z.object({
|
|
@@ -12091,7 +13236,12 @@ var MysqlPoolConfigSchema = z.object({
|
|
|
12091
13236
|
}).strict();
|
|
12092
13237
|
var SslConfigSchema = z.union([
|
|
12093
13238
|
z.boolean(),
|
|
12094
|
-
z.enum([
|
|
13239
|
+
z.enum([
|
|
13240
|
+
"require",
|
|
13241
|
+
"prefer",
|
|
13242
|
+
"allow",
|
|
13243
|
+
"disable"
|
|
13244
|
+
]),
|
|
12095
13245
|
z.record(z.string(), z.unknown())
|
|
12096
13246
|
]);
|
|
12097
13247
|
var MysqlSslConfigSchema = z.union([
|
|
@@ -12107,7 +13257,9 @@ var MysqlSslConfigSchema = z.union([
|
|
|
12107
13257
|
cert: z.string().min(1).optional(),
|
|
12108
13258
|
/** MySQL 客户端私钥,PEM 内容;必须同时提供 cert */
|
|
12109
13259
|
key: z.string().min(1).optional()
|
|
12110
|
-
}).strict().refine((ssl) => Boolean(ssl.cert) === Boolean(ssl.key), {
|
|
13260
|
+
}).strict().refine((ssl) => Boolean(ssl.cert) === Boolean(ssl.key), {
|
|
13261
|
+
path: ["key"]
|
|
13262
|
+
})
|
|
12111
13263
|
]);
|
|
12112
13264
|
var SqliteOptionsSchema = z.object({
|
|
12113
13265
|
/** 是否启用 WAL 模式(默认 true),可提高并发读写性能 */
|
|
@@ -12169,7 +13321,10 @@ var PostgresqlConfigSchema = NetworkConfigSchema.extend({
|
|
|
12169
13321
|
var MysqlConfigSchema = NetworkConfigSchema.extend({
|
|
12170
13322
|
type: z.literal("mysql"),
|
|
12171
13323
|
/** 网络数据库URL;MySQL空串表示使用分字段,非空仅mysql://且拒绝query/fragment,优先于host/port/database/user/password */
|
|
12172
|
-
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(),
|
|
12173
13328
|
/** 数据库:SQLite文件路径或:memory:;PostgreSQL必填名称;MySQL无URL时必填,URL模式从URL读取 */
|
|
12174
13329
|
database: z.string().min(1).optional(),
|
|
12175
13330
|
/** TLS:PostgreSQL使用其模式;MySQL仅false或严格对象,空对象默认校验证书链和DNS身份 */
|
|
@@ -12179,7 +13334,9 @@ var MysqlConfigSchema = NetworkConfigSchema.extend({
|
|
|
12179
13334
|
/** MySQL 特定选项 */
|
|
12180
13335
|
mysql: MysqlOptionsSchema.optional(),
|
|
12181
13336
|
...OperationLogConfigFieldSchema
|
|
12182
|
-
}).refine((config) => Boolean(config.url) || Boolean(config.database), {
|
|
13337
|
+
}).refine((config) => Boolean(config.url) || Boolean(config.database), {
|
|
13338
|
+
path: ["database"]
|
|
13339
|
+
});
|
|
12183
13340
|
var ReldbConfigSchema = z.discriminatedUnion("type", [
|
|
12184
13341
|
SqliteConfigSchema,
|
|
12185
13342
|
PostgresqlConfigSchema,
|
|
@@ -12189,8 +13346,12 @@ function canKeepMiddleware(requirement, backend, environment) {
|
|
|
12189
13346
|
const privatePath = join(environment.envDir, `.env.${environment.environment}.local`);
|
|
12190
13347
|
let targetValues = environment.targetValues;
|
|
12191
13348
|
try {
|
|
12192
|
-
if (existsSync(privatePath))
|
|
12193
|
-
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
|
+
}
|
|
12194
13355
|
} catch {
|
|
12195
13356
|
return false;
|
|
12196
13357
|
}
|
|
@@ -12200,19 +13361,31 @@ function canKeepMiddleware(requirement, backend, environment) {
|
|
|
12200
13361
|
function empty(value2) {
|
|
12201
13362
|
if (Array.isArray(value2))
|
|
12202
13363
|
return value2.map(empty);
|
|
12203
|
-
if (value2 && typeof value2 === "object")
|
|
12204
|
-
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
|
+
}
|
|
12205
13370
|
return void 0;
|
|
12206
13371
|
}
|
|
12207
13372
|
let seed = empty(requirement.config);
|
|
12208
|
-
for (const part of [...requirement.configPath].reverse())
|
|
13373
|
+
for (const part of [...requirement.configPath].reverse()) {
|
|
12209
13374
|
seed = /^\d+$/.test(part) ? Array.from({ length: Number(part) + 1 }, (_, index) => index === Number(part) ? seed : null) : { [part]: seed };
|
|
12210
|
-
|
|
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
|
+
});
|
|
12211
13383
|
if (!resolved.success)
|
|
12212
13384
|
return false;
|
|
12213
13385
|
let value = resolved.data;
|
|
12214
|
-
for (const part of requirement.configPath)
|
|
13386
|
+
for (const part of requirement.configPath) {
|
|
12215
13387
|
value = value && typeof value === "object" ? value[part] : void 0;
|
|
13388
|
+
}
|
|
12216
13389
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
12217
13390
|
return false;
|
|
12218
13391
|
const target = value;
|
|
@@ -12229,30 +13402,39 @@ function validateMiddlewareField(options) {
|
|
|
12229
13402
|
}
|
|
12230
13403
|
function middlewareFieldProblem(options) {
|
|
12231
13404
|
const { value, field: field2, backend } = options;
|
|
12232
|
-
if (value === "")
|
|
13405
|
+
if (value === "") {
|
|
12233
13406
|
return field2.required ? deployM("deploy_middlewareRequired") : void 0;
|
|
12234
|
-
|
|
12235
|
-
|
|
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
|
+
}
|
|
12236
13413
|
if (field2.kind === "number") {
|
|
12237
13414
|
const min = field2.minimum ?? 0;
|
|
12238
13415
|
const max = field2.maximum ?? Number.MAX_SAFE_INTEGER;
|
|
12239
|
-
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
|
+
});
|
|
12240
13419
|
}
|
|
12241
|
-
if (field2.kind === "boolean")
|
|
13420
|
+
if (field2.kind === "boolean") {
|
|
12242
13421
|
return ["true", "false"].includes(value) ? void 0 : deployM("deploy_middlewareBoolean");
|
|
13422
|
+
}
|
|
12243
13423
|
if (backend === "s3" && field2.name === "endpoint") {
|
|
12244
13424
|
const endpoint = URL.parse(value);
|
|
12245
13425
|
return endpoint?.hostname && ["http:", "https:"].includes(endpoint.protocol) ? void 0 : deployM("deploy_middlewareS3Endpoint");
|
|
12246
13426
|
}
|
|
12247
|
-
if (field2.kind === "url")
|
|
13427
|
+
if (field2.kind === "url") {
|
|
12248
13428
|
return URL.canParse(value) ? void 0 : deployM("deploy_middlewareUrl");
|
|
13429
|
+
}
|
|
12249
13430
|
if (field2.name === "ssl") {
|
|
12250
13431
|
try {
|
|
12251
13432
|
const document = parseDocument(value);
|
|
12252
13433
|
if (document.errors.length)
|
|
12253
13434
|
return deployM("deploy_middlewareTlsSyntax");
|
|
12254
|
-
if (!(backend === "mysql" ? MysqlSslConfigSchema : SslConfigSchema).safeParse(document.toJS()).success)
|
|
13435
|
+
if (!(backend === "mysql" ? MysqlSslConfigSchema : SslConfigSchema).safeParse(document.toJS()).success) {
|
|
12255
13436
|
return deployM(backend === "mysql" ? "deploy_middlewareMysqlTls" : "deploy_middlewarePostgresTls");
|
|
13437
|
+
}
|
|
12256
13438
|
} catch {
|
|
12257
13439
|
return deployM("deploy_middlewareTlsSyntax");
|
|
12258
13440
|
}
|
|
@@ -12274,8 +13456,11 @@ function parseMiddlewareField(options) {
|
|
|
12274
13456
|
if (options.managed && (options.field.secret || options.field.managed) && options.value === "")
|
|
12275
13457
|
return ok(void 0);
|
|
12276
13458
|
const problem = middlewareFieldProblem(options);
|
|
12277
|
-
if (problem)
|
|
12278
|
-
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
|
+
}
|
|
12279
13464
|
const { value, field: field2 } = options;
|
|
12280
13465
|
if (value === "")
|
|
12281
13466
|
return ok(void 0);
|
|
@@ -12293,8 +13478,13 @@ function saveMiddlewareEnvironment(environment, options) {
|
|
|
12293
13478
|
const configuration = requirement.service === "ldap" ? options.configuration : { ...options.configuration, type: backend };
|
|
12294
13479
|
if (requirement.service === "db") {
|
|
12295
13480
|
const validated = ReldbConfigSchema.safeParse(configuration);
|
|
12296
|
-
if (!validated.success)
|
|
12297
|
-
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
|
+
}
|
|
12298
13488
|
}
|
|
12299
13489
|
if (requirement.service === "ldap") {
|
|
12300
13490
|
updates.HAI_IAM_LOGIN_LDAP = backend !== "disabled";
|
|
@@ -12308,17 +13498,26 @@ function saveMiddlewareEnvironment(environment, options) {
|
|
|
12308
13498
|
mkdirSync(directory, { recursive: true });
|
|
12309
13499
|
const path = join(directory, `_${requirement.configName}.yml`);
|
|
12310
13500
|
const lineCounter = new LineCounter();
|
|
12311
|
-
const document = parseDocument(existsSync(path) ?
|
|
12312
|
-
if (document.errors.length)
|
|
12313
|
-
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_configFileInvalid", {
|
|
12314
|
-
|
|
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)) {
|
|
12315
13511
|
document.setIn(requirement.configPath, document.createNode(requirement.config.name ? { name: requirement.config.name, type: backend } : {}));
|
|
13512
|
+
}
|
|
12316
13513
|
if (requirement.service === "ldap" && !document.hasIn(["login", "ldap"]))
|
|
12317
13514
|
document.setIn(["login", "ldap"], false);
|
|
12318
13515
|
const content = document.toString();
|
|
12319
|
-
if (!existsSync(path) ||
|
|
12320
|
-
|
|
12321
|
-
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
|
+
});
|
|
12322
13521
|
} catch {
|
|
12323
13522
|
return err(HaiDeployError.ENV_VAR_FAILED, deployM("deploy_environmentWriteFailed"));
|
|
12324
13523
|
}
|
|
@@ -12326,46 +13525,253 @@ function saveMiddlewareEnvironment(environment, options) {
|
|
|
12326
13525
|
|
|
12327
13526
|
// src/deploy-middleware.ts
|
|
12328
13527
|
var field = (name, defaultValue, kind, secret = false) => ({ name, defaultValue, kind, secret });
|
|
12329
|
-
|
|
13528
|
+
function required(name, defaultValue, kind) {
|
|
13529
|
+
return {
|
|
13530
|
+
...field(name, defaultValue, kind),
|
|
13531
|
+
required: true
|
|
13532
|
+
};
|
|
13533
|
+
}
|
|
12330
13534
|
var DEPLOY_MIDDLEWARE = {
|
|
12331
13535
|
db: [
|
|
12332
|
-
{
|
|
13536
|
+
{
|
|
13537
|
+
backend: "sqlite",
|
|
13538
|
+
label: "SQLite",
|
|
13539
|
+
purpose: "deploy_purposeSqlite",
|
|
13540
|
+
fields: [
|
|
13541
|
+
required("database", "./data/app.db")
|
|
13542
|
+
]
|
|
13543
|
+
},
|
|
12333
13544
|
...["postgresql", "mysql"].map((backend) => ({
|
|
12334
13545
|
backend,
|
|
12335
13546
|
label: backend === "mysql" ? "MySQL" : "PostgreSQL",
|
|
12336
13547
|
purpose: "deploy_purposeDatabase",
|
|
12337
13548
|
provisioner: backend === "postgresql" ? "neon" : void 0,
|
|
12338
13549
|
cloudFields: backend === "postgresql" ? provisionerFields("neon") : void 0,
|
|
12339
|
-
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
|
+
]
|
|
12340
13559
|
}))
|
|
12341
13560
|
],
|
|
12342
13561
|
cache: [
|
|
12343
|
-
{
|
|
12344
|
-
|
|
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
|
+
}
|
|
12345
13589
|
],
|
|
12346
13590
|
storage: [
|
|
12347
|
-
{
|
|
12348
|
-
|
|
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
|
+
}
|
|
12349
13632
|
],
|
|
12350
13633
|
vecdb: [
|
|
12351
|
-
{
|
|
12352
|
-
|
|
12353
|
-
|
|
12354
|
-
|
|
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
|
+
}
|
|
12355
13672
|
],
|
|
12356
13673
|
ldap: [
|
|
12357
|
-
{
|
|
12358
|
-
|
|
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
|
+
}
|
|
12359
13703
|
],
|
|
12360
13704
|
email: [
|
|
12361
|
-
{
|
|
12362
|
-
|
|
12363
|
-
|
|
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
|
+
}
|
|
12364
13738
|
],
|
|
12365
13739
|
sms: [
|
|
12366
|
-
{
|
|
12367
|
-
|
|
12368
|
-
|
|
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
|
+
}
|
|
12369
13775
|
]
|
|
12370
13776
|
};
|
|
12371
13777
|
function describeMiddlewareBackend(backend) {
|
|
@@ -12375,8 +13781,15 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12375
13781
|
if ("projectName" in options && options.projectName !== void 0)
|
|
12376
13782
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_projectNameOverrideRemoved"));
|
|
12377
13783
|
const parsed = DeployConfigSchema.safeParse(input);
|
|
12378
|
-
if (!parsed.success)
|
|
12379
|
-
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
|
+
}
|
|
12380
13793
|
const config = parsed.data;
|
|
12381
13794
|
const scan = await scanApp(appDir, { environment: options.environment ?? config.environment, envDir: options.envDir });
|
|
12382
13795
|
if (!scan.success)
|
|
@@ -12403,21 +13816,51 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12403
13816
|
const compose = validateComposeServices(config.container, artifact);
|
|
12404
13817
|
if (!compose.success)
|
|
12405
13818
|
return compose;
|
|
12406
|
-
images = [
|
|
13819
|
+
images = [
|
|
13820
|
+
.../* @__PURE__ */ new Set([
|
|
13821
|
+
artifact.primaryService,
|
|
13822
|
+
...artifact.hasWeb ? ["web"] : []
|
|
13823
|
+
])
|
|
13824
|
+
];
|
|
12407
13825
|
} else {
|
|
12408
13826
|
const file = join(root, "hai.json");
|
|
12409
|
-
const project = existsSync(file) ? z.object({
|
|
12410
|
-
|
|
12411
|
-
|
|
12412
|
-
|
|
12413
|
-
|
|
12414
|
-
|
|
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
|
+
];
|
|
12415
13853
|
if (!selected.length || new Set(selected).size !== selected.length || selected.some((name) => !available.includes(name)))
|
|
12416
13854
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
12417
13855
|
if (artifact && selected.some((name) => images.includes(name) && !artifact.images.some((image) => image.name === name)))
|
|
12418
13856
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
12419
13857
|
const remoteDirectory = provider.type === "docker-ssh" ? `${provider.remote.baseDir}/${projectName}` : void 0;
|
|
12420
|
-
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
|
+
};
|
|
12421
13864
|
if (options.inspectRemote) {
|
|
12422
13865
|
if (provider.type !== "docker-ssh" || !provider.ssh || !remoteDirectory)
|
|
12423
13866
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_sshRequired"));
|
|
@@ -12433,13 +13876,27 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12433
13876
|
baseline.release = release || void 0;
|
|
12434
13877
|
}
|
|
12435
13878
|
}
|
|
12436
|
-
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);
|
|
12437
13885
|
const configDirectory = join(directory, "config");
|
|
12438
13886
|
if (existsSync(configDirectory))
|
|
12439
13887
|
configFiles.push(...readdirSync(configDirectory, { withFileTypes: true }).filter((entry) => entry.isFile() && /\.ya?ml$/i.test(entry.name)).map((entry) => join(configDirectory, entry.name)).sort());
|
|
12440
|
-
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
|
+
};
|
|
12441
13892
|
const effective = resolveSidecarOptions(app, { ...options, selectedServices: selected, serviceModes });
|
|
12442
|
-
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
|
+
};
|
|
12443
13900
|
const provisionEnv = {};
|
|
12444
13901
|
const provisioners = /* @__PURE__ */ new Map();
|
|
12445
13902
|
for (const service of Object.keys(provisionKeys)) {
|
|
@@ -12451,7 +13908,7 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12451
13908
|
}
|
|
12452
13909
|
const sidecar = provider.type === "docker-ssh" ? planSidecars(directory, projectName, app, provisionEnv, effective, config.container?.s3Engine, { dbPassword: "", storagePassword: "", ldapPassword: "" }) : void 0;
|
|
12453
13910
|
const https = provider.type === "docker-ssh" ? provider.expose.https : void 0;
|
|
12454
|
-
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");
|
|
12455
13912
|
if (gateway && artifact.primaryService === "web" && !app.isSvelteKit)
|
|
12456
13913
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
|
|
12457
13914
|
if (gateway) {
|
|
@@ -12470,17 +13927,54 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
12470
13927
|
root,
|
|
12471
13928
|
serviceDir: directory,
|
|
12472
13929
|
environment: environment.data.environment,
|
|
12473
|
-
target: provider.type === "docker-ssh" ? {
|
|
12474
|
-
|
|
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,
|
|
12475
13946
|
selected,
|
|
12476
13947
|
retained: available.filter((name) => !selected.includes(name)),
|
|
12477
|
-
containers: provider.type === "docker-ssh" ? [
|
|
12478
|
-
|
|
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
|
+
],
|
|
12479
13956
|
logicalVolumes: Object.keys(sidecar?.volumes ?? {}),
|
|
12480
|
-
middleware: middlewares.map((item) => ({
|
|
12481
|
-
|
|
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
|
+
},
|
|
12482
13971
|
baseline,
|
|
12483
|
-
recovery: {
|
|
13972
|
+
recovery: {
|
|
13973
|
+
releases: remoteDirectory ? `${remoteDirectory}/releases` : void 0,
|
|
13974
|
+
point: baseline.release ?? "previous-successful-release",
|
|
13975
|
+
preservesVolumes: true,
|
|
13976
|
+
databaseRollback: false
|
|
13977
|
+
}
|
|
12484
13978
|
});
|
|
12485
13979
|
} catch {
|
|
12486
13980
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_artifactInvalid"));
|
|
@@ -12491,7 +13985,11 @@ var passwordSchema = z.string().min(1).max(1024).refine((value) => Buffer.from(v
|
|
|
12491
13985
|
var importSchema = z.strictObject({
|
|
12492
13986
|
version: z.literal(1),
|
|
12493
13987
|
projectName: nameSchema,
|
|
12494
|
-
credentials: z.strictObject({
|
|
13988
|
+
credentials: z.strictObject({
|
|
13989
|
+
DB_PASSWORD: passwordSchema.optional(),
|
|
13990
|
+
STORAGE_PASSWORD: passwordSchema.optional(),
|
|
13991
|
+
LDAP_PASSWORD: passwordSchema.optional()
|
|
13992
|
+
}),
|
|
12495
13993
|
services: z.array(z.strictObject({
|
|
12496
13994
|
service: z.enum(["db", "vecdb", "storage", "ldap"]),
|
|
12497
13995
|
container: nameSchema,
|
|
@@ -12505,7 +14003,12 @@ var importSchema = z.strictObject({
|
|
|
12505
14003
|
bindDn: z.string().min(1).max(512).default("cn=admin,dc=example,dc=org")
|
|
12506
14004
|
})).min(1).max(4)
|
|
12507
14005
|
});
|
|
12508
|
-
var groups2 = {
|
|
14006
|
+
var groups2 = {
|
|
14007
|
+
db: "DB_PASSWORD",
|
|
14008
|
+
vecdb: "DB_PASSWORD",
|
|
14009
|
+
storage: "STORAGE_PASSWORD",
|
|
14010
|
+
ldap: "LDAP_PASSWORD"
|
|
14011
|
+
};
|
|
12509
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;
|
|
12510
14013
|
try{process.stdout.write('LOCKED\n');let input='';for await(const chunk of process.stdin){input+=chunk;if(input.length>65536)throw Error()}
|
|
12511
14014
|
if(!input)return;const {content}=JSON.parse(input);if(typeof content!=='string'||!content.startsWith('FORMAT_VERSION=2\n'))throw Error();
|
|
@@ -12537,11 +14040,24 @@ async function importDeploymentSecrets(options) {
|
|
|
12537
14040
|
throw new Error("input");
|
|
12538
14041
|
const input = privateFile(resolve(options.inputFile));
|
|
12539
14042
|
const parsed = importSchema.safeParse(JSON.parse(input));
|
|
12540
|
-
if (!parsed.success)
|
|
12541
|
-
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
|
+
}
|
|
12542
14056
|
const config = parsed.data;
|
|
12543
14057
|
const project = `hai-${config.projectName}`;
|
|
12544
|
-
const selectedGroups = [
|
|
14058
|
+
const selectedGroups = [
|
|
14059
|
+
...new Set(config.services.map((service) => groups2[service.service]))
|
|
14060
|
+
].sort();
|
|
12545
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])) {
|
|
12546
14062
|
throw new Error("input");
|
|
12547
14063
|
}
|
|
@@ -12581,23 +14097,89 @@ async function importDeploymentSecrets(options) {
|
|
|
12581
14097
|
const application = await inspectContainer(options.runtime, helper2, secrets);
|
|
12582
14098
|
if (application.project !== project || application.service !== "app" || !application.running || !application.networks.some((network) => identity.networks.includes(network)))
|
|
12583
14099
|
throw new Error("identity");
|
|
12584
|
-
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
|
+
});
|
|
12585
14119
|
verified = result.success && result.data.stdout.trim() === "VERIFIED";
|
|
12586
14120
|
} else {
|
|
12587
|
-
const args = service.kind === "ldap" ? [
|
|
12588
|
-
|
|
12589
|
-
|
|
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
|
+
});
|
|
12590
14152
|
verified = result.success && !result.data.stdoutTruncated && (service.kind === "ldap" ? result.data.stdout.trim().startsWith("dn:") : result.data.stdout.trim() === "1");
|
|
12591
14153
|
if (verified && service.kind === "mysql") {
|
|
12592
|
-
const rootCheck = await runCommand(options.runtime, [
|
|
12593
|
-
|
|
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
|
+
});
|
|
12594
14166
|
verified = rootCheck.success && !rootCheck.data.stdoutTruncated && rootCheck.data.stdout.trim() === "1";
|
|
12595
14167
|
const invalidPassword = `hai-import-invalid-${randomUUID()}`;
|
|
12596
14168
|
for (const username of [service.username, "root"]) {
|
|
12597
14169
|
if (!verified)
|
|
12598
14170
|
break;
|
|
12599
|
-
const negative = await runCommand(options.runtime, [
|
|
12600
|
-
|
|
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
|
+
});
|
|
12601
14183
|
verified = !negative.success && /ERROR 1045 \(28000\)/.test(negative.error.message);
|
|
12602
14184
|
}
|
|
12603
14185
|
}
|
|
@@ -12607,8 +14189,15 @@ async function importDeploymentSecrets(options) {
|
|
|
12607
14189
|
phase = "identity";
|
|
12608
14190
|
}
|
|
12609
14191
|
phase = "state";
|
|
12610
|
-
const uniqueVolumes = [
|
|
12611
|
-
|
|
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
|
+
];
|
|
12612
14201
|
let changed = false;
|
|
12613
14202
|
if (existsSync(stateFile) || isSymbolicLink(stateFile)) {
|
|
12614
14203
|
const existing = readImportedState(privateFile(stateFile));
|
|
@@ -12621,9 +14210,24 @@ async function importDeploymentSecrets(options) {
|
|
|
12621
14210
|
throw new Error("lock");
|
|
12622
14211
|
changed = true;
|
|
12623
14212
|
}
|
|
12624
|
-
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
|
+
});
|
|
12625
14220
|
} catch {
|
|
12626
|
-
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
|
+
});
|
|
12627
14231
|
} finally {
|
|
12628
14232
|
await lock?.release();
|
|
12629
14233
|
}
|
|
@@ -12659,19 +14263,26 @@ function isSymbolicLink(path) {
|
|
|
12659
14263
|
}
|
|
12660
14264
|
}
|
|
12661
14265
|
function privateFile(path) {
|
|
12662
|
-
|
|
12663
|
-
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > 65536 || process.platform !== "win32" && (stat.mode & 63 || process.getuid?.() !== 0 && stat.uid !== process.getuid?.()))
|
|
12664
|
-
throw new Error("private-file");
|
|
12665
|
-
return readFileSync(path, "utf8");
|
|
14266
|
+
return readLocalFile(path, { maxBytes: 65536, private: true }).toString("utf8");
|
|
12666
14267
|
}
|
|
12667
14268
|
async function inspectContainer(runtime, container, secrets) {
|
|
12668
|
-
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 });
|
|
12669
14275
|
if (!result.success || result.data.stdoutTruncated)
|
|
12670
14276
|
throw new Error("identity");
|
|
12671
14277
|
const [labelsText, mountsText, runningText, networksText] = result.data.stdout.trim().split("\n");
|
|
12672
14278
|
const labels = z.record(z.string(), z.string()).parse(JSON.parse(labelsText));
|
|
12673
|
-
const mounts = z.array(z.object({
|
|
12674
|
-
|
|
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) ?? {});
|
|
12675
14286
|
const dockerProject = labels["com.docker.compose.project"] || void 0;
|
|
12676
14287
|
const podmanProject = labels["io.podman.compose.project"] || void 0;
|
|
12677
14288
|
const dockerService = labels["com.docker.compose.service"] || void 0;
|
|
@@ -12679,11 +14290,27 @@ async function inspectContainer(runtime, container, secrets) {
|
|
|
12679
14290
|
if (Boolean(dockerProject) !== Boolean(dockerService) || Boolean(podmanProject) !== Boolean(podmanService) || dockerProject && podmanProject && dockerProject !== podmanProject || dockerService && podmanService && dockerService !== podmanService) {
|
|
12680
14291
|
throw new Error("identity");
|
|
12681
14292
|
}
|
|
12682
|
-
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
|
+
};
|
|
12683
14300
|
}
|
|
12684
14301
|
async function acquireImportLock(path, root) {
|
|
12685
14302
|
return new Promise((resolveLock, reject) => {
|
|
12686
|
-
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
|
+
});
|
|
12687
14314
|
let acquired = false;
|
|
12688
14315
|
let exited = false;
|
|
12689
14316
|
let output = "";
|