@h-ai/deploy 1.0.0-beta.1 → 1.0.0-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +23 -7
- package/README.ja.md +24 -8
- package/README.md +25 -9
- package/dist/deploy-runtime-env.mjs +58 -0
- package/dist/index.d.ts +71 -4
- package/dist/index.js +567 -100
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,18 +1,23 @@
|
|
|
1
|
-
import { createHash, randomUUID, randomBytes } from 'crypto';
|
|
2
|
-
import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, readdirSync, realpathSync, chmodSync, mkdirSync, mkdtempSync, copyFileSync, renameSync, rmSync,
|
|
1
|
+
import { createHash, randomUUID, randomBytes, X509Certificate, createPublicKey, createPrivateKey } from 'crypto';
|
|
2
|
+
import { createReadStream, readFileSync, existsSync, lstatSync, writeFileSync, readdirSync, realpathSync, chmodSync, mkdirSync, statSync, mkdtempSync, copyFileSync, renameSync, rmSync, createWriteStream } from 'fs';
|
|
3
3
|
import { resolve, extname, join, dirname, relative, basename, sep } from 'path';
|
|
4
4
|
import { z } from 'zod';
|
|
5
|
-
import { core, err, ok, RequestAccessConfigSchema, TrustedProxiesSchema } from '@h-ai/core';
|
|
5
|
+
import { core, err, ok, CoreConfigSchema, RequestAccessConfigSchema, TrustedProxiesSchema } from '@h-ai/core';
|
|
6
6
|
import process6 from 'process';
|
|
7
7
|
import { parseEnv } from 'util';
|
|
8
8
|
import { LineCounter, parseDocument, parse, stringify, parseAllDocuments } from 'yaml';
|
|
9
9
|
import { homedir, tmpdir } from 'os';
|
|
10
|
+
import { request } from 'https';
|
|
11
|
+
import { rootCertificates } from 'tls';
|
|
10
12
|
import { resolveFeedbackBudget } from '@h-ai/api-contract/feedback';
|
|
11
13
|
import { resolveTransportBodyLimits, createTransportBodyPolicy, parseTransportServerKeyRing } from '@h-ai/crypto';
|
|
14
|
+
import { Transform } from 'stream';
|
|
15
|
+
import { pipeline } from 'stream/promises';
|
|
12
16
|
import { spawn } from 'child_process';
|
|
13
17
|
import { readdir, readFile } from 'fs/promises';
|
|
14
18
|
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
15
19
|
import { Buffer } from 'buffer';
|
|
20
|
+
import { isIP } from 'net';
|
|
16
21
|
|
|
17
22
|
// src/deploy-artifact.ts
|
|
18
23
|
var DeploymentArtifactSchema = z.object({
|
|
@@ -117,9 +122,9 @@ var en_US_default = {
|
|
|
117
122
|
deploy_webProxyInvalid: "The API prefix must be an absolute path containing only letters, numbers, underscores, hyphens and slashes.",
|
|
118
123
|
deploy_webBaseConflict: "The website mount path conflicts with an API or health endpoint. Choose a different PUBLIC_BASE_PATH.",
|
|
119
124
|
deploy_storageProxyInvalid: "Cannot generate the storage proxy. Use a valid DNS bucket name whose path does not overlap the website, API, or health endpoints.",
|
|
120
|
-
deploy_storagePublicEndpointInvalid: "Invalid storage.publicEndpoint. Use a browser-accessible HTTP(S)
|
|
125
|
+
deploy_storagePublicEndpointInvalid: "Invalid storage.publicEndpoint. Use a browser-accessible HTTP(S) address with an optional valid proxy path, without a bucket, credentials, query or fragment.",
|
|
121
126
|
deploy_storageBackendRequired: "A static browser app cannot deploy server-side storage credentials. Configure Storage in Serv or SvelteKit and obtain presigned links through its API.",
|
|
122
|
-
deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target
|
|
127
|
+
deploy_artifactInvalid: "Image artifacts are missing or invalid. Run pnpm build --target image, select valid --artifact-dir, or use pnpm run deploy --build.",
|
|
123
128
|
deploy_sshRequired: "Automatic deployment requires SSH settings. Use --bundle-only for manual transfer when the server is unreachable.",
|
|
124
129
|
deploy_sshPasswordInvalid: "SSH passwords must be nonempty, contain no CR/LF/NUL, and be at most 1023 UTF-8 bytes.",
|
|
125
130
|
deploy_sshAskpassFailed: "Could not prepare the managed SSH password helper. Check OpenSSH and Windows PowerShell 5.",
|
|
@@ -169,7 +174,9 @@ var en_US_default = {
|
|
|
169
174
|
deploy_secretImportFailed: "Legacy credential import was rejected during {phase}; check private input permissions, project/volume identity, live credentials and the deployment lock. No services were updated; inspect the protected state file if commit is unconfirmed",
|
|
170
175
|
deploy_projectNameRequired: "The root package.json.name is missing or cannot be normalized into an image and deployment resource identity.",
|
|
171
176
|
deploy_projectNameOverrideRemoved: "Deployment identity comes from the root package.json.name; remove the obsolete projectName override.",
|
|
172
|
-
deploy_requestAccessInvalid: "Invalid request access policy ({field}). Check fields, IP addresses, timezones and time windows. For a file reference, use readable YAML no larger than 1 MiB. Fix the configuration and create a new bundle."
|
|
177
|
+
deploy_requestAccessInvalid: "Invalid request access policy ({field}). Check fields, IP addresses, timezones and time windows. For a file reference, use readable YAML no larger than 1 MiB. Fix the configuration and create a new bundle.",
|
|
178
|
+
deploy_httpsPortsInvalid: "HTTP and HTTPS ports must differ; Let\u2019s Encrypt HTTP-01 requires host port 80.",
|
|
179
|
+
deploy_httpsCertificateInvalid: "Invalid HTTPS certificate. Check readable PEM files, matching unencrypted private key, validity dates and domain, then repackage."
|
|
173
180
|
};
|
|
174
181
|
|
|
175
182
|
// messages/ja-JP.json
|
|
@@ -225,9 +232,9 @@ var ja_JP_default = {
|
|
|
225
232
|
deploy_webProxyInvalid: "API\u30D7\u30EC\u30D5\u30A3\u30C3\u30AF\u30B9\u306F\u3001\u82F1\u6570\u5B57\u3001\u30A2\u30F3\u30C0\u30FC\u30B9\u30B3\u30A2\u3001\u30CF\u30A4\u30D5\u30F3\u3001\u30B9\u30E9\u30C3\u30B7\u30E5\u306E\u307F\u3092\u542B\u3080\u7D76\u5BFE\u30D1\u30B9\u3067\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
226
233
|
deploy_webBaseConflict: "\u30B5\u30A4\u30C8\u306E\u30DE\u30A6\u30F3\u30C8\u30D1\u30B9\u304CAPI\u307E\u305F\u306F\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AF\u3068\u7AF6\u5408\u3057\u3066\u3044\u307E\u3059\u3002\u5225\u306EPUBLIC_BASE_PATH\u3092\u6307\u5B9A\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
227
234
|
deploy_storageProxyInvalid: "\u30B9\u30C8\u30EC\u30FC\u30B8\u30D7\u30ED\u30AD\u30B7\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3002\u6709\u52B9\u306A DNS \u30D0\u30B1\u30C3\u30C8\u540D\u3092\u6307\u5B9A\u3057\u3001\u30B5\u30A4\u30C8\u3001API\u3001\u30D8\u30EB\u30B9\u30C1\u30A7\u30C3\u30AF\u306E\u30D1\u30B9\u3068\u306E\u91CD\u8907\u3092\u907F\u3051\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
228
|
-
deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u304C\u7121\u52B9\u3067\u3059\u3002\
|
|
235
|
+
deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u304C\u7121\u52B9\u3067\u3059\u3002\u30D6\u30E9\u30A6\u30B6\u30FC\u304B\u3089\u5230\u9054\u53EF\u80FD\u306A HTTP(S) \u30A2\u30C9\u30EC\u30B9\u306B\u6709\u52B9\u306A\u30D7\u30ED\u30AD\u30B7\u30D1\u30B9\u3092\u6307\u5B9A\u3067\u304D\u307E\u3059\u3002\u30D0\u30B1\u30C3\u30C8\u3001\u8A8D\u8A3C\u60C5\u5831\u3001\u30AF\u30A8\u30EA\u3001\u30D5\u30E9\u30B0\u30E1\u30F3\u30C8\u306F\u542B\u3081\u306A\u3044\u3067\u304F\u3060\u3055\u3044\u3002",
|
|
229
236
|
deploy_storageBackendRequired: "\u9759\u7684\u30D6\u30E9\u30A6\u30B6\u30A2\u30D7\u30EA\u306B\u306F\u30B5\u30FC\u30D0\u30FC\u7528\u30B9\u30C8\u30EC\u30FC\u30B8\u8A8D\u8A3C\u60C5\u5831\u3092\u914D\u7F6E\u3067\u304D\u307E\u305B\u3093\u3002Serv \u307E\u305F\u306F SvelteKit \u3067 Storage \u3092\u8A2D\u5B9A\u3057\u3001API \u7D4C\u7531\u3067\u7F72\u540D\u4ED8\u304D\u30EA\u30F3\u30AF\u3092\u53D6\u5F97\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
230
|
-
deploy_artifactInvalid: "\u30A4\u30E1\u30FC\u30B8\u6210\u679C\u7269\u304C\u5B58\u5728\u3057\u306A\u3044\u304B\u3001\u7121\u52B9\u3067\u3059\u3002pnpm build --target
|
|
237
|
+
deploy_artifactInvalid: "\u30A4\u30E1\u30FC\u30B8\u6210\u679C\u7269\u304C\u5B58\u5728\u3057\u306A\u3044\u304B\u3001\u7121\u52B9\u3067\u3059\u3002pnpm build --target image \u3092\u5B9F\u884C\u3059\u308B\u304B\u3001--artifact-dir \u3067\u6709\u52B9\u306A\u6210\u679C\u7269\u3092\u9078\u629E\u3059\u308B\u304B\u3001pnpm run deploy --build \u3092\u4F7F\u7528\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
231
238
|
deploy_sshRequired: "\u81EA\u52D5\u30C7\u30D7\u30ED\u30A4\u306B\u306FSSH\u8A2D\u5B9A\u304C\u5FC5\u8981\u3067\u3059\u3002\u30B5\u30FC\u30D0\u30FC\u306B\u63A5\u7D9A\u3067\u304D\u306A\u3044\u5834\u5408\u306F--bundle-only\u3067\u624B\u52D5\u8EE2\u9001\u7528\u306E\u30D0\u30F3\u30C9\u30EB\u3092\u751F\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
232
239
|
deploy_sshPasswordInvalid: "SSH\u30D1\u30B9\u30EF\u30FC\u30C9\u306F\u7A7A\u306B\u3067\u304D\u305A\u3001CR/LF/NUL\u3092\u542B\u3081\u3089\u308C\u307E\u305B\u3093\u3002UTF-8\u30671023\u30D0\u30A4\u30C8\u4EE5\u4E0B\u306B\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
233
240
|
deploy_sshAskpassFailed: "\u7BA1\u7406\u5BFE\u8C61\u306ESSH\u30D1\u30B9\u30EF\u30FC\u30C9helper\u3092\u6E96\u5099\u3067\u304D\u307E\u305B\u3093\u3002OpenSSH\u3068Windows PowerShell 5\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
@@ -277,7 +284,9 @@ var ja_JP_default = {
|
|
|
277
284
|
deploy_secretImportFailed: "\u65E7\u8A8D\u8A3C\u60C5\u5831\u306E\u30A4\u30F3\u30DD\u30FC\u30C8\u306F {phase} \u6BB5\u968E\u3067\u62D2\u5426\u3055\u308C\u307E\u3057\u305F\u3002\u5165\u529B\u6A29\u9650\u3001\u30D7\u30ED\u30B8\u30A7\u30AF\u30C8\u3068\u30DC\u30EA\u30E5\u30FC\u30E0\u3001\u5B9F\u969B\u306E\u8A8D\u8A3C\u60C5\u5831\u3001\u30C7\u30D7\u30ED\u30A4\u30ED\u30C3\u30AF\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30B5\u30FC\u30D3\u30B9\u306F\u66F4\u65B0\u3055\u308C\u3066\u3044\u307E\u305B\u3093\u3002\u78BA\u5B9A\u304C\u4E0D\u660E\u306A\u5834\u5408\u306F\u4FDD\u8B77\u3055\u308C\u305F\u72B6\u614B\u30D5\u30A1\u30A4\u30EB\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044",
|
|
278
285
|
deploy_projectNameRequired: "\u5DE5\u7A0B\u30EB\u30FC\u30C8\u306E package.json.name \u304C\u306A\u3044\u304B\u6B63\u898F\u5316\u3067\u304D\u306A\u3044\u305F\u3081\u3001\u30A4\u30E1\u30FC\u30B8\u3068\u30C7\u30D7\u30ED\u30A4\u30EA\u30BD\u30FC\u30B9\u306E\u8B58\u5225\u540D\u3092\u751F\u6210\u3067\u304D\u307E\u305B\u3093\u3002",
|
|
279
286
|
deploy_projectNameOverrideRemoved: "\u30C7\u30D7\u30ED\u30A4\u8B58\u5225\u540D\u306F\u5DE5\u7A0B\u30EB\u30FC\u30C8\u306E package.json.name \u304B\u3089\u53D6\u5F97\u3057\u307E\u3059\u3002\u65E7 projectName \u4E0A\u66F8\u304D\u5F15\u6570\u3092\u524A\u9664\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
280
|
-
deploy_requestAccessInvalid: "\u30EA\u30AF\u30A8\u30B9\u30C8\u30A2\u30AF\u30BB\u30B9\u8A2D\u5B9A\uFF08{field}\uFF09\u304C\u7121\u52B9\u3067\u3059\u3002\u30D5\u30A3\u30FC\u30EB\u30C9\u3001IP\u3001\u30BF\u30A4\u30E0\u30BE\u30FC\u30F3\u3001\u6642\u9593\u5E2F\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30D5\u30A1\u30A4\u30EB\u306F\u8AAD\u307F\u53D6\u308A\u53EF\u80FD\u306A 1 MiB \u4EE5\u4E0B\u306E YAML \u3092\u6307\u5B9A\u3057\u3001\u4FEE\u6B63\u5F8C\u306B\u518D\u30D1\u30C3\u30B1\u30FC\u30B8\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
287
|
+
deploy_requestAccessInvalid: "\u30EA\u30AF\u30A8\u30B9\u30C8\u30A2\u30AF\u30BB\u30B9\u8A2D\u5B9A\uFF08{field}\uFF09\u304C\u7121\u52B9\u3067\u3059\u3002\u30D5\u30A3\u30FC\u30EB\u30C9\u3001IP\u3001\u30BF\u30A4\u30E0\u30BE\u30FC\u30F3\u3001\u6642\u9593\u5E2F\u3092\u78BA\u8A8D\u3057\u3066\u304F\u3060\u3055\u3044\u3002\u30D5\u30A1\u30A4\u30EB\u306F\u8AAD\u307F\u53D6\u308A\u53EF\u80FD\u306A 1 MiB \u4EE5\u4E0B\u306E YAML \u3092\u6307\u5B9A\u3057\u3001\u4FEE\u6B63\u5F8C\u306B\u518D\u30D1\u30C3\u30B1\u30FC\u30B8\u3057\u3066\u304F\u3060\u3055\u3044\u3002",
|
|
288
|
+
deploy_httpsPortsInvalid: "HTTP \u3068 HTTPS \u306E\u30DD\u30FC\u30C8\u306F\u7570\u306A\u308B\u5FC5\u8981\u304C\u3042\u308A\u307E\u3059\u3002Let\u2019s Encrypt HTTP-01 \u306B\u306F\u30DB\u30B9\u30C8\u306E\u30DD\u30FC\u30C8 80 \u304C\u5FC5\u8981\u3067\u3059\u3002",
|
|
289
|
+
deploy_httpsCertificateInvalid: "HTTPS \u8A3C\u660E\u66F8\u304C\u7121\u52B9\u3067\u3059\u3002PEM \u30D5\u30A1\u30A4\u30EB\u3001\u6697\u53F7\u5316\u3055\u308C\u3066\u3044\u306A\u3044\u79D8\u5BC6\u9375\u3068\u306E\u4E00\u81F4\u3001\u6709\u52B9\u671F\u9593\u3001\u30C9\u30E1\u30A4\u30F3\u3092\u78BA\u8A8D\u3057\u3001\u518D\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044\u3002"
|
|
281
290
|
};
|
|
282
291
|
|
|
283
292
|
// messages/zh-CN.json
|
|
@@ -333,9 +342,9 @@ var zh_CN_default = {
|
|
|
333
342
|
deploy_webProxyInvalid: "API \u524D\u7F00\u5FC5\u987B\u662F\u4EC5\u542B\u5B57\u6BCD\u3001\u6570\u5B57\u3001\u4E0B\u5212\u7EBF\u3001\u8FDE\u5B57\u7B26\u548C\u659C\u6760\u7684\u7EDD\u5BF9\u8DEF\u5F84\u3002",
|
|
334
343
|
deploy_webBaseConflict: "\u7F51\u7AD9\u6302\u8F7D\u76EE\u5F55\u4E0E API \u6216\u5065\u5EB7\u68C0\u67E5\u5165\u53E3\u51B2\u7A81\uFF0C\u8BF7\u4F7F\u7528\u4E0D\u540C\u7684 PUBLIC_BASE_PATH\u3002",
|
|
335
344
|
deploy_storageProxyInvalid: "\u65E0\u6CD5\u751F\u6210\u5BF9\u8C61\u5B58\u50A8\u4EE3\u7406\uFF1A\u8BF7\u4F7F\u7528\u6709\u6548\u7684 DNS \u6876\u540D\uFF0C\u5E76\u786E\u4FDD\u6876\u8DEF\u5F84\u4E0D\u4E0E\u7F51\u7AD9\u76EE\u5F55\u3001API \u6216\u5065\u5EB7\u68C0\u67E5\u5165\u53E3\u91CD\u53E0\u3002",
|
|
336
|
-
deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u65E0\u6548\uFF1A\u8BF7\u586B\u5199\u6D4F\u89C8\u5668\u53EF\u8BBF\u95EE\u7684 HTTP(S) \
|
|
345
|
+
deploy_storagePublicEndpointInvalid: "storage.publicEndpoint \u65E0\u6548\uFF1A\u8BF7\u586B\u5199\u6D4F\u89C8\u5668\u53EF\u8BBF\u95EE\u7684 HTTP(S) \u5730\u5740\uFF0C\u53EF\u5305\u542B\u89C4\u8303\u7684\u4EE3\u7406\u76EE\u5F55\uFF0C\u4E0D\u80FD\u5305\u542B\u6876\u540D\u3001\u51ED\u636E\u3001\u67E5\u8BE2\u6216\u7247\u6BB5\u3002",
|
|
337
346
|
deploy_storageBackendRequired: "\u9759\u6001\u6D4F\u89C8\u5668\u5E94\u7528\u4E0D\u80FD\u90E8\u7F72\u5B58\u50A8\u670D\u52A1\u7AEF\u914D\u7F6E\u3002\u8BF7\u5728 Serv \u6216 SvelteKit \u670D\u52A1\u4E2D\u914D\u7F6E Storage\uFF0C\u518D\u901A\u8FC7 API \u83B7\u53D6\u9884\u7B7E\u540D\u94FE\u63A5\u3002",
|
|
338
|
-
deploy_artifactInvalid: "\u955C\u50CF\u4EA7\u7269\u7F3A\u5931\u6216\u6821\u9A8C\u5931\u8D25\u3002\u8BF7\u5148\u6267\u884C pnpm build --target
|
|
347
|
+
deploy_artifactInvalid: "\u955C\u50CF\u4EA7\u7269\u7F3A\u5931\u6216\u6821\u9A8C\u5931\u8D25\u3002\u8BF7\u5148\u6267\u884C pnpm build --target image\uFF0C\u6216\u7528 --artifact-dir \u6307\u5B9A\u6709\u6548\u4EA7\u7269\uFF1B\u4E5F\u53EF\u7528 pnpm run deploy --build \u4E00\u6B21\u5B8C\u6210\u3002",
|
|
339
348
|
deploy_sshRequired: "\u81EA\u52A8\u90E8\u7F72\u9700\u8981 SSH \u914D\u7F6E\uFF1B\u65E0\u7F51\u7EDC\u8FDE\u63A5\u65F6\u4F7F\u7528 --bundle-only \u751F\u6210\u90E8\u7F72\u5305\u5E76\u624B\u5DE5\u642C\u8FD0\u3002",
|
|
340
349
|
deploy_sshPasswordInvalid: "SSH \u5BC6\u7801\u4E0D\u80FD\u4E3A\u7A7A\u6216\u5305\u542B CR/LF/NUL\uFF0CUTF-8 \u957F\u5EA6\u4E0D\u80FD\u8D85\u8FC7 1023 \u5B57\u8282\u3002",
|
|
341
350
|
deploy_sshAskpassFailed: "\u65E0\u6CD5\u51C6\u5907\u53D7\u63A7 SSH \u5BC6\u7801 helper\uFF1B\u8BF7\u68C0\u67E5\u7CFB\u7EDF OpenSSH \u548C Windows PowerShell 5\u3002",
|
|
@@ -385,7 +394,9 @@ var zh_CN_default = {
|
|
|
385
394
|
deploy_secretImportFailed: "\u65E7\u51ED\u636E\u5BFC\u5165\u5728 {phase} \u9636\u6BB5\u88AB\u62D2\u7EDD\uFF1B\u8BF7\u6838\u5BF9\u79C1\u6709\u8F93\u5165\u6743\u9650\u3001\u9879\u76EE/\u5377\u8EAB\u4EFD\u3001\u5B9E\u9645\u670D\u52A1\u51ED\u636E\u53CA\u90E8\u7F72\u9501\uFF0C\u672A\u6267\u884C\u670D\u52A1\u66F4\u65B0\uFF1B\u63D0\u4EA4\u7ED3\u679C\u4E0D\u660E\u65F6\u8BF7\u6838\u5BF9\u53D7\u4FDD\u62A4\u7684\u72B6\u6001\u6587\u4EF6",
|
|
386
395
|
deploy_projectNameRequired: "\u5DE5\u7A0B\u6839 package.json.name \u7F3A\u5931\u6216\u65E0\u6CD5\u5F52\u4E00\u5316\uFF0C\u4E0D\u80FD\u751F\u6210\u955C\u50CF\u4E0E\u90E8\u7F72\u8D44\u6E90\u8EAB\u4EFD\u3002",
|
|
387
396
|
deploy_projectNameOverrideRemoved: "\u90E8\u7F72\u9879\u76EE\u540D\u7EDF\u4E00\u6765\u81EA\u5DE5\u7A0B\u6839 package.json.name\uFF1B\u8BF7\u79FB\u9664\u65E7 projectName \u8986\u76D6\u53C2\u6570\u3002",
|
|
388
|
-
deploy_requestAccessInvalid: "\u8BF7\u6C42\u8BBF\u95EE\u7B56\u7565\u914D\u7F6E\u65E0\u6548\uFF08{field}\uFF09\u3002\u8BF7\u68C0\u67E5\u5B57\u6BB5\u3001IP\u3001\u65F6\u533A\u53CA\u65F6\u95F4\u7A97\u53E3\uFF1B\u5982\u5F15\u7528\u6587\u4EF6\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u53EF\u8BFB\u3001\u683C\u5F0F\u4E3A YAML \u4E14\u4E0D\u8D85\u8FC7 1 MiB\u3002\u4FEE\u6B63\u540E\u91CD\u65B0\u6253\u5305\u3002"
|
|
397
|
+
deploy_requestAccessInvalid: "\u8BF7\u6C42\u8BBF\u95EE\u7B56\u7565\u914D\u7F6E\u65E0\u6548\uFF08{field}\uFF09\u3002\u8BF7\u68C0\u67E5\u5B57\u6BB5\u3001IP\u3001\u65F6\u533A\u53CA\u65F6\u95F4\u7A97\u53E3\uFF1B\u5982\u5F15\u7528\u6587\u4EF6\uFF0C\u8BF7\u786E\u8BA4\u6587\u4EF6\u53EF\u8BFB\u3001\u683C\u5F0F\u4E3A YAML \u4E14\u4E0D\u8D85\u8FC7 1 MiB\u3002\u4FEE\u6B63\u540E\u91CD\u65B0\u6253\u5305\u3002",
|
|
398
|
+
deploy_httpsPortsInvalid: "HTTP \u4E0E HTTPS \u7AEF\u53E3\u5FC5\u987B\u4E0D\u540C\uFF1BLet\u2019s Encrypt \u7684 HTTP-01 \u5165\u53E3\u5FC5\u987B\u4F7F\u7528\u5BBF\u4E3B\u673A 80 \u7AEF\u53E3\u3002",
|
|
399
|
+
deploy_httpsCertificateInvalid: "HTTPS \u8BC1\u4E66\u65E0\u6548\uFF1A\u8BF7\u6838\u5BF9 PEM \u6587\u4EF6\u53EF\u8BFB\u3001\u8BC1\u4E66\u4E0E\u672A\u52A0\u5BC6\u79C1\u94A5\u5339\u914D\u3001\u8BC1\u4E66\u6709\u6548\u671F\u53CA\u57DF\u540D\uFF0C\u7136\u540E\u91CD\u65B0\u6253\u5305\u3002"
|
|
389
400
|
};
|
|
390
401
|
|
|
391
402
|
// src/deploy-i18n.ts
|
|
@@ -425,12 +436,43 @@ var SshConfigSchema = z.object({
|
|
|
425
436
|
if (!ssh.password || /[\r\n\0]/.test(ssh.password) || new TextEncoder().encode(ssh.password).length > 1023)
|
|
426
437
|
context.addIssue({ code: "custom", path: ["password"], message: deployM("deploy_sshPasswordInvalid") });
|
|
427
438
|
});
|
|
439
|
+
var HttpsDomainSchema = z.string().max(253).regex(/^[a-z0-9](?:[a-z0-9.-]*[a-z0-9])?$/i).refine((value) => !value.includes(".."));
|
|
440
|
+
var httpsOptions = {
|
|
441
|
+
/** 公开访问域名;同时用于证书身份校验和 Nginx 入口。 */
|
|
442
|
+
domain: HttpsDomainSchema.describe("HTTPS \u516C\u5F00\u57DF\u540D\uFF1B\u8BC1\u4E66\u5FC5\u987B\u5339\u914D\u8BE5\u57DF\u540D"),
|
|
443
|
+
/** HTTPS 宿主端口,与 HTTP 端口不同。 */
|
|
444
|
+
port: z.number().int().min(1).max(65535).default(443).describe("HTTPS \u5BBF\u4E3B\u7AEF\u53E3\uFF0C\u9ED8\u8BA4 443\uFF0C\u987B\u4E0E HTTP \u7AEF\u53E3\u4E0D\u540C"),
|
|
445
|
+
/** 是否将普通 HTTP 请求跳转到 HTTPS。 */
|
|
446
|
+
redirectHttp: z.boolean().default(true).describe("\u666E\u901A HTTP \u8BF7\u6C42\u662F\u5426\u4EE5 308 \u8DF3\u8F6C\u81F3 HTTPS\uFF0C\u5065\u5EB7\u68C0\u67E5\u548C ACME \u6311\u6218\u9664\u5916")
|
|
447
|
+
};
|
|
448
|
+
var HttpsConfigSchema = z.discriminatedUnion("type", [
|
|
449
|
+
z.object({
|
|
450
|
+
type: z.literal("files"),
|
|
451
|
+
...httpsOptions,
|
|
452
|
+
/** 相对于工程根或绝对路径的 PEM 完整证书链及未加密私钥。 */
|
|
453
|
+
certificateFile: z.string().min(1).describe("PEM \u5B8C\u6574\u8BC1\u4E66\u94FE\u8DEF\u5F84\uFF0C\u76F8\u5BF9\u5DE5\u7A0B\u6839\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u6253\u5305\u65F6\u590D\u5236\u5230\u79C1\u6709\u53D1\u5E03\u76EE\u5F55"),
|
|
454
|
+
privateKeyFile: z.string().min(1).describe("\u672A\u52A0\u5BC6 PEM \u79C1\u94A5\u8DEF\u5F84\uFF0C\u76F8\u5BF9\u5DE5\u7A0B\u6839\u6216\u7EDD\u5BF9\u8DEF\u5F84\uFF1B\u5FC5\u987B\u4E0E\u8BC1\u4E66\u5339\u914D")
|
|
455
|
+
}),
|
|
456
|
+
z.object({
|
|
457
|
+
type: z.literal("letsencrypt"),
|
|
458
|
+
...httpsOptions,
|
|
459
|
+
domain: HttpsDomainSchema.refine((value) => value.includes(".") && !/^\d+(?:\.\d+){3}$/.test(value)).describe("Let\u2019s Encrypt \u5355\u4E2A\u516C\u7F51\u57DF\u540D\uFF0C\u4F7F\u7528 HTTP-01\uFF0C\u4E0D\u652F\u6301 IP \u6216\u901A\u914D\u7B26"),
|
|
460
|
+
email: z.email().describe("Let\u2019s Encrypt \u8D26\u53F7\u8054\u7CFB\u90AE\u7BB1"),
|
|
461
|
+
/** 使用者须明确同意 Let's Encrypt 的服务条款。 */
|
|
462
|
+
acceptTerms: z.literal(true).describe("\u660E\u786E\u540C\u610F Let\u2019s Encrypt \u670D\u52A1\u6761\u6B3E\u540E\u624D\u53EF\u8BBE\u7F6E\u4E3A true")
|
|
463
|
+
})
|
|
464
|
+
]);
|
|
428
465
|
var PortExposeSchema = z.object({
|
|
429
466
|
type: z.literal("port").default("port"),
|
|
430
467
|
/** 容器内监听端口(adapter-node 默认 3000) */
|
|
431
468
|
containerPort: z.number().int().min(1).max(65535).default(3e3),
|
|
432
469
|
/** 宿主机映射端口 */
|
|
433
|
-
hostPort: z.number().int().min(1).max(65535)
|
|
470
|
+
hostPort: z.number().int().min(1).max(65535),
|
|
471
|
+
/** 可选 HTTPS:已有 PEM 文件或官方 Certbot 自动申请续期;省略保留 HTTP。 */
|
|
472
|
+
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")
|
|
473
|
+
}).superRefine((expose, context) => {
|
|
474
|
+
if (expose.https && (expose.https.port === expose.hostPort || expose.https.type === "letsencrypt" && expose.hostPort !== 80))
|
|
475
|
+
context.addIssue({ code: "custom", path: ["https"], message: deployM("deploy_httpsPortsInvalid") });
|
|
434
476
|
});
|
|
435
477
|
var ExposeSchema = PortExposeSchema;
|
|
436
478
|
var DockerSshProviderConfigSchema = z.object({
|
|
@@ -929,6 +971,32 @@ function saveCredentials(entries) {
|
|
|
929
971
|
);
|
|
930
972
|
}
|
|
931
973
|
}
|
|
974
|
+
|
|
975
|
+
// src/deploy-middleware-config.ts
|
|
976
|
+
var connectionFields = {
|
|
977
|
+
db: ["url", "host", "port", "database", "user", "password", "ssl"],
|
|
978
|
+
cache: ["url", "host", "port", "password", "cluster", "sentinel", "tls"],
|
|
979
|
+
storage: ["endpoint", "bucket", "region", "accessKeyId", "secretAccessKey", "forcePathStyle", "createBucket"],
|
|
980
|
+
vecdb: ["url", "host", "port", "database", "user", "password", "apiKey", "path", "serverCommand", "startupTimeout"],
|
|
981
|
+
ldap: ["url", "bindDn", "bindPassword", "searchBase", "useTls"],
|
|
982
|
+
email: ["host", "port", "secure", "user", "pass"]
|
|
983
|
+
};
|
|
984
|
+
function mergeManagedMiddlewareConfig(requirement, supplied) {
|
|
985
|
+
if (!requirement || requirement.service !== "ldap" && requirement.backend !== supplied.type)
|
|
986
|
+
return supplied;
|
|
987
|
+
const configured = { ...requirement.config };
|
|
988
|
+
for (const field2 of connectionFields[requirement.service] ?? [])
|
|
989
|
+
delete configured[field2];
|
|
990
|
+
const merged = { ...configured, ...supplied };
|
|
991
|
+
if (requirement.service === "cache" && typeof configured.db === "number" && typeof supplied.url === "string") {
|
|
992
|
+
const url = new URL(supplied.url);
|
|
993
|
+
if (configured.db !== 0 || url.pathname !== "" && url.pathname !== "/") {
|
|
994
|
+
url.pathname = String(configured.db);
|
|
995
|
+
merged.url = url.toString();
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
return merged;
|
|
999
|
+
}
|
|
932
1000
|
var RecoverySchema = z.object({
|
|
933
1001
|
projectName: z.string(),
|
|
934
1002
|
stage: z.string(),
|
|
@@ -1796,7 +1864,7 @@ async function groupExited(pid) {
|
|
|
1796
1864
|
async function helper(command, args, timeoutMs) {
|
|
1797
1865
|
if (timeoutMs <= 0)
|
|
1798
1866
|
return false;
|
|
1799
|
-
return new Promise((
|
|
1867
|
+
return new Promise((resolve9) => {
|
|
1800
1868
|
const child = spawn(command, args, { windowsHide: true, shell: false, stdio: "ignore" });
|
|
1801
1869
|
let settled = false;
|
|
1802
1870
|
let timer;
|
|
@@ -1806,7 +1874,7 @@ async function helper(command, args, timeoutMs) {
|
|
|
1806
1874
|
settled = true;
|
|
1807
1875
|
if (timer)
|
|
1808
1876
|
clearTimeout(timer);
|
|
1809
|
-
|
|
1877
|
+
resolve9(success);
|
|
1810
1878
|
};
|
|
1811
1879
|
timer = globalThis.setTimeout(() => {
|
|
1812
1880
|
child.kill("SIGKILL");
|
|
@@ -1949,7 +2017,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
1949
2017
|
ext: { commandExecution: state }
|
|
1950
2018
|
});
|
|
1951
2019
|
try {
|
|
1952
|
-
return await new Promise((
|
|
2020
|
+
return await new Promise((resolve9) => {
|
|
1953
2021
|
if (typeof command !== "string" || !command || command.includes("\0") || args.some((argument) => typeof argument !== "string" || argument.includes("\0")))
|
|
1954
2022
|
throw new TypeError("Invalid command arguments");
|
|
1955
2023
|
if (options.input !== void 0 && typeof options.input !== "string")
|
|
@@ -1984,7 +2052,7 @@ ${deployM(state.localTermination === "confirmed" ? "deploy_localStopped" : state
|
|
|
1984
2052
|
settled = true;
|
|
1985
2053
|
if (timer)
|
|
1986
2054
|
clearTimeout(timer);
|
|
1987
|
-
|
|
2055
|
+
resolve9(result);
|
|
1988
2056
|
};
|
|
1989
2057
|
const interrupt = (reason, message) => {
|
|
1990
2058
|
interrupted = true;
|
|
@@ -2433,7 +2501,7 @@ start_services() {
|
|
|
2433
2501
|
application_services=''
|
|
2434
2502
|
application_orphan_option=$orphan_option
|
|
2435
2503
|
for service in "$@"; do
|
|
2436
|
-
case "$service" in serv|web|gateway) ;; *) middleware_services="$middleware_services $service";; esac
|
|
2504
|
+
case "$service" in serv|web|gateway|certbot) ;; *) middleware_services="$middleware_services $service";; esac
|
|
2437
2505
|
done
|
|
2438
2506
|
if [ -n "$middleware_services" ]; then
|
|
2439
2507
|
compose up -d --no-deps --no-build --pull never "$recreate_option" $orphan_option $middleware_services || return 1
|
|
@@ -2449,6 +2517,9 @@ start_services() {
|
|
|
2449
2517
|
fi
|
|
2450
2518
|
case " $* " in *' gateway '*) compose up -d --no-deps --no-build --pull never "$recreate_option" gateway || return 1;; esac
|
|
2451
2519
|
}
|
|
2520
|
+
start_renewal() {
|
|
2521
|
+
case " $* " in *' certbot '*) compose up -d --no-deps --no-build --pull never --no-recreate certbot || return 1;; esac
|
|
2522
|
+
}
|
|
2452
2523
|
read_proxy_service() {
|
|
2453
2524
|
proxy_directory=$1
|
|
2454
2525
|
if [ -e "$proxy_directory/proxy-service" ] || [ -L "$proxy_directory/proxy-service" ]; then
|
|
@@ -2472,13 +2543,22 @@ reload_retained_proxy() {
|
|
|
2472
2543
|
esac
|
|
2473
2544
|
}
|
|
2474
2545
|
wait_http() {
|
|
2475
|
-
|
|
2476
|
-
|
|
2546
|
+
http_release=$1
|
|
2547
|
+
http_port=$2
|
|
2548
|
+
shift 2
|
|
2549
|
+
https_domain=''
|
|
2550
|
+
if [ -f "$http_release/https" ]; then
|
|
2551
|
+
read -r https_domain https_port https_type < "$http_release/https"
|
|
2552
|
+
https_certificates="$http_release/tls"
|
|
2553
|
+
if [ "$https_type" = letsencrypt ]; then https_certificates="$root/https/certificates/$https_domain"; fi
|
|
2554
|
+
fi
|
|
2477
2555
|
for http_path in "$@"; do
|
|
2478
2556
|
http_ready=false
|
|
2479
2557
|
http_attempt=0
|
|
2480
2558
|
while [ "$http_attempt" -lt 40 ]; do
|
|
2481
|
-
if
|
|
2559
|
+
if [ -n "$https_domain" ]; then
|
|
2560
|
+
if curl --noproxy '*' -fsS --max-time 5 --cacert "$https_certificates/fullchain.pem" --resolve "$https_domain:$https_port:127.0.0.1" "https://$https_domain:$https_port$http_path" >/dev/null 2>&1; then http_ready=true; break; fi
|
|
2561
|
+
elif curl --noproxy '*' -fsS --max-time 5 "http://127.0.0.1:$http_port$http_path" >/dev/null 2>&1; then http_ready=true; break; fi
|
|
2482
2562
|
http_attempt=$((http_attempt + 1))
|
|
2483
2563
|
sleep 3
|
|
2484
2564
|
done
|
|
@@ -2491,7 +2571,6 @@ verify_recovery() {
|
|
|
2491
2571
|
recovery_port=$(awk -F: 'NR == 1 { print $1 }' "$recovery_release/layout") || return 1
|
|
2492
2572
|
recovery_paths=''
|
|
2493
2573
|
if [ -f "$recovery_release/health-paths" ]; then
|
|
2494
|
-
recovery_port=$(cat "$recovery_release/health-port") || return 1
|
|
2495
2574
|
recovery_paths=$(cat "$recovery_release/health-paths") || return 1
|
|
2496
2575
|
else
|
|
2497
2576
|
# 旧格式发布没有独立健康文件;按已存拓扑恢复最小 HTTP 检查。
|
|
@@ -2501,10 +2580,10 @@ verify_recovery() {
|
|
|
2501
2580
|
elif [ "$recovery_primary" = web ]; then recovery_paths='/'; else recovery_paths='/health'; fi
|
|
2502
2581
|
fi
|
|
2503
2582
|
case "$recovery_port" in ''|*[!0-9]*) return 1;; esac
|
|
2504
|
-
test -n "$recovery_paths" || return 1
|
|
2505
2583
|
wait_services $recover_selected || return 1
|
|
2584
|
+
start_renewal $recover_selected || return 1
|
|
2506
2585
|
reload_retained_proxy "$recovery_release" || return 1
|
|
2507
|
-
wait_http "$recovery_port" $recovery_paths
|
|
2586
|
+
wait_http "$recovery_release" "$recovery_port" $recovery_paths
|
|
2508
2587
|
}
|
|
2509
2588
|
finish() {
|
|
2510
2589
|
code=$?
|
|
@@ -2632,7 +2711,7 @@ if [ "$partial" = true ]; then
|
|
|
2632
2711
|
test "$current_storage_routing" = "$previous_storage_routing" || { printf 'Managed storage configuration changed. Create a full deployment to switch its public endpoint, bucket, or access policy consistently.\n' >&2; exit 1; }
|
|
2633
2712
|
if ! cmp -s layout "$root/releases/$previous/layout"; then
|
|
2634
2713
|
# 容量调整允许复用已部署 Web 镜像;端口、路由及其余代理策略仍要求全量部署。
|
|
2635
|
-
test "$(awk -F: '{print $1 ":" $2 ":" $3 ":" $4}' layout)" = "$(awk -F: '{print $1 ":" $2 ":" $3 ":" $4}' "$root/releases/$previous/layout")" || { printf 'Ports or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
|
|
2714
|
+
test "$(awk -F: '{print $1 ":" $2 ":" $3 ":" $4 ":" $6}' layout)" = "$(awk -F: '{print $1 ":" $2 ":" $3 ":" $4 ":" $6}' "$root/releases/$previous/layout")" || { printf 'Ports, HTTPS or application layout changed. Create a full deployment bundle.\n' >&2; exit 1; }
|
|
2636
2715
|
test -f web.conf && test -f proxy-entry.json || { printf 'Proxy configuration missing. Create a full deployment bundle.\n' >&2; exit 1; }
|
|
2637
2716
|
previous_proxy=
|
|
2638
2717
|
while IFS= read -r compose_file; do
|
|
@@ -2650,9 +2729,17 @@ if [ "$partial" = true ]; then
|
|
|
2650
2729
|
fi
|
|
2651
2730
|
fi
|
|
2652
2731
|
mkdir -p "$release"
|
|
2653
|
-
|
|
2732
|
+
# 镜像直接从本次解包目录导入;发布记录只保存运行配置与恢复证据。
|
|
2733
|
+
for entry in "$source_dir"/* "$source_dir"/.[!.]*; do
|
|
2734
|
+
test -e "$entry" || continue
|
|
2735
|
+
test "$entry" != "$source_dir/images" || continue
|
|
2736
|
+
cp -R "$entry" "$release/"
|
|
2737
|
+
done
|
|
2654
2738
|
# Nginx 以非 root 用户运行;只开放不含凭据的代理配置读取权限。
|
|
2655
2739
|
if [ -f "$release/web.conf" ]; then chmod 644 "$release/web.conf"; fi
|
|
2740
|
+
for https_script in https-start.sh renew-https.sh; do
|
|
2741
|
+
if [ -f "$release/$https_script" ]; then chmod 444 "$release/$https_script"; fi
|
|
2742
|
+
done
|
|
2656
2743
|
# 策略不是凭据,发布目录保持700;只读挂载允许镜像中的非root应用读取。
|
|
2657
2744
|
if [ -f "$release/request-access.yml" ]; then chmod 444 "$release/request-access.yml"; fi
|
|
2658
2745
|
: > "$release/compose-files"
|
|
@@ -2666,6 +2753,12 @@ if [ "$partial" = true ]; then
|
|
|
2666
2753
|
fi
|
|
2667
2754
|
step credentials 'Resolving server-managed middleware credentials'
|
|
2668
2755
|
sh "$release/resolve-managed-secrets.sh" "$root" "$release" "$runtime"
|
|
2756
|
+
if [ -f "$release/runtime-env.mjs" ]; then
|
|
2757
|
+
# 上级目录保护宿主秘密;只读文件挂载允许非 root 应用读取,宿主所有者仍可修改配置。
|
|
2758
|
+
chmod 700 "$root" "$release"
|
|
2759
|
+
chmod 644 "$release/.env.runtime" "$release/middleware.env"
|
|
2760
|
+
chmod 444 "$release/runtime-env.mjs"
|
|
2761
|
+
fi
|
|
2669
2762
|
# 在任何可能启动服务(包括增量恢复旧拓扑)之前定稿,失败回滚不删除卷的引擎身份。
|
|
2670
2763
|
sh "$release/resolve-backend-state.sh" "$root" "$release" "$runtime" "$previous" commit
|
|
2671
2764
|
printf '%s\n' "$release/compose-entry.json" >> "$release/compose-files"
|
|
@@ -2703,12 +2796,15 @@ step images 'Loading packaged images (no registry access)'
|
|
|
2703
2796
|
while IFS=' ' read -r archive reference; do
|
|
2704
2797
|
test -n "$archive" || continue
|
|
2705
2798
|
images_changed=true
|
|
2706
|
-
"$runtime" load -i "$
|
|
2799
|
+
"$runtime" load -i "$source_dir/images/$archive"
|
|
2707
2800
|
"$runtime" image inspect "$reference" >/dev/null
|
|
2708
2801
|
architecture=$("$runtime" image inspect --format '{{.Architecture}}' "$reference")
|
|
2709
2802
|
host_arch=$(uname -m)
|
|
2710
2803
|
case "$host_arch:$architecture" in x86_64:amd64|aarch64:arm64) ;; *) printf 'Image architecture %s does not match server %s\n' "$architecture" "$host_arch" >&2; exit 1;; esac
|
|
2711
2804
|
done < "$release/image-list"
|
|
2805
|
+
if [ -f "$release/prepare-https.sh" ]; then
|
|
2806
|
+
sh "$release/prepare-https.sh" "$root" "$release" "$runtime" prepare "$partial"
|
|
2807
|
+
fi
|
|
2712
2808
|
step dependencies 'Checking dependencies of selected services'
|
|
2713
2809
|
if [ "$partial" = true ]; then cat "$release/services" >> "$release/dependencies"; fi
|
|
2714
2810
|
while IFS= read -r dependency; do
|
|
@@ -2720,9 +2816,18 @@ step start 'Updating selected services'
|
|
|
2720
2816
|
changed=true
|
|
2721
2817
|
remove_selected $selected
|
|
2722
2818
|
start_services $selected
|
|
2819
|
+
if [ -f "$release/prepare-https.sh" ] && [ "$partial" = false ]; then
|
|
2820
|
+
step https 'Issuing or installing HTTPS certificate'
|
|
2821
|
+
tls_proxy=$(read_proxy_service "$release")
|
|
2822
|
+
wait_services "$tls_proxy"
|
|
2823
|
+
sh "$release/prepare-https.sh" "$root" "$release" "$runtime" issue "$partial"
|
|
2824
|
+
compose exec -T "$tls_proxy" nginx -t
|
|
2825
|
+
compose exec -T "$tls_proxy" nginx -s reload
|
|
2826
|
+
fi
|
|
2827
|
+
start_renewal $selected
|
|
2723
2828
|
reload_retained_proxy "$release"
|
|
2724
2829
|
step health 'Checking HTTP access'
|
|
2725
|
-
wait_http @@PORT@@ @@HEALTH@@
|
|
2830
|
+
wait_http "$release" @@PORT@@ @@HEALTH@@
|
|
2726
2831
|
step complete 'Deployment passed all checks'
|
|
2727
2832
|
printf '%s\n' "$id" > "$root/current.next"
|
|
2728
2833
|
mv "$root/current.next" "$root/current"
|
|
@@ -2776,7 +2881,7 @@ case "$timeout" in ''|*[!0-9]*|???????????*) invalid_target;; esac
|
|
|
2776
2881
|
test "$timeout" -ge 1 && test "$timeout" -le 2147483647 || invalid_target
|
|
2777
2882
|
case "$remote" in /*) ;; *) invalid_target;; esac
|
|
2778
2883
|
case "$remote" in *[!a-zA-Z0-9/_.\ -]*|*/../*|*/..|/..) invalid_target;; esac
|
|
2779
|
-
case "$bundle" in *.tar) ;; *) invalid_target;; esac
|
|
2884
|
+
case "$bundle" in *.tar|*.tar.gz) ;; *) invalid_target;; esac
|
|
2780
2885
|
case "$bundle" in ''|*[!a-zA-Z0-9_.+-]*) invalid_target;; esac
|
|
2781
2886
|
case "$strict" in true) strict=accept-new;; false) strict=no;; *) invalid_target;; esac
|
|
2782
2887
|
test -f "./$bundle" || { printf 'Bundle missing: %s\n' "$bundle" >&2; exit 1; }
|
|
@@ -2900,7 +3005,7 @@ monitor=''
|
|
|
2900
3005
|
if [ "$upload_code" -ne 0 ]; then printf '[FAIL] Upload failed (exit %s). Check SSH output above.\n' "$upload_code" >&2; exit "$upload_code"; fi
|
|
2901
3006
|
printf '[UPLOAD] 100%% (%s/%s bytes)\n' "$total" "$total"
|
|
2902
3007
|
printf '[3/3] Applying package and checking HTTP access\n'
|
|
2903
|
-
if run_native ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"; then
|
|
3008
|
+
if run_native ssh "$@" -p "$port" "$user@$host" "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote' && { rm -rf -- '$bundle.contents' && rm -f -- '$bundle' || printf '[WARN] Deployment passed; incoming archive cleanup failed\\n' >&2; }"; then
|
|
2904
3009
|
:
|
|
2905
3010
|
else
|
|
2906
3011
|
code=$?
|
|
@@ -2935,7 +3040,7 @@ if ($windowsRuntime) {
|
|
|
2935
3040
|
Set-Location -LiteralPath $PSScriptRoot
|
|
2936
3041
|
$target = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'deploy-target.json') -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
2937
3042
|
if ($target.host -eq 'SERVER_HOST') { throw 'Set the server host in deploy-target.json.' }
|
|
2938
|
-
if ($target.host -isnot [string] -or $target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -isnot [string] -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -isnot [string] -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]*$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -isnot [string] -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar
|
|
3043
|
+
if ($target.host -isnot [string] -or $target.host -notmatch '^[a-zA-Z0-9][a-zA-Z0-9.:-]*$' -or $target.username -isnot [string] -or $target.username -notmatch '^[a-zA-Z_][a-zA-Z0-9_-]*$' -or $target.directory -isnot [string] -or $target.directory -notmatch '^/[a-zA-Z0-9/_. -]*$' -or $target.directory -match '(^|/)\.\.(/|$)' -or $target.bundle -isnot [string] -or $target.bundle -notmatch '^[a-zA-Z0-9_.+-]+\.tar(\.gz)?$') { throw 'Invalid deployment target.' }
|
|
2939
3044
|
$port = if ($null -eq $target.port) { 22 } else { $target.port }
|
|
2940
3045
|
$timeout = if ($null -eq $target.connectTimeoutSeconds) { 10 } else { $target.connectTimeoutSeconds }
|
|
2941
3046
|
if ($port -is [string] -or [string]$port -notmatch '^[0-9]+$' -or $port -lt 1 -or $port -gt 65535 -or $timeout -is [string] -or [string]$timeout -notmatch '^[0-9]+$' -or $timeout -lt 1 -or $timeout -gt 2147483647) { throw 'Invalid deployment port or timeout.' }
|
|
@@ -3094,7 +3199,7 @@ try {
|
|
|
3094
3199
|
Invoke-Step '2/3 Upload' 'scp' ($options + @('-P', [string]$port, "./$bundle", "$($scpDestination):$remote/incoming/$bundle"))
|
|
3095
3200
|
Write-Host "[UPLOAD] 100% ($uploadBytes/$uploadBytes bytes)"
|
|
3096
3201
|
$applying = $true
|
|
3097
|
-
Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote'"))
|
|
3202
|
+
Invoke-Step '3/3 Apply and verify HTTP' 'ssh' ($options + @('-p', [string]$port, $destination, "cd '$remote/incoming' && mkdir -p '$bundle.contents' && tar -xf '$bundle' -C '$bundle.contents' && sh '$bundle.contents/apply.sh' '$remote' && { rm -rf -- '$bundle.contents' && rm -f -- '$bundle' || printf '[WARN] Deployment passed; incoming archive cleanup failed\\n' >&2; }"))
|
|
3098
3203
|
} catch {
|
|
3099
3204
|
if ($applying) {
|
|
3100
3205
|
if ($LASTEXITCODE -gt 0 -and $LASTEXITCODE -lt 128) { [Console]::Error.WriteLine("[FAIL] Remote deployment failed (exit $LASTEXITCODE). Inspect $remote/result-<deploymentId>.json (ID in README.txt) and startup diagnostics.") }
|
|
@@ -3263,13 +3368,14 @@ ${removed.error.message}` });
|
|
|
3263
3368
|
async function createDeploymentBundle(options) {
|
|
3264
3369
|
const { directory, artifactDir, artifact, projectName, environment, config, selected } = options;
|
|
3265
3370
|
const output = resolve(options.output);
|
|
3266
|
-
if (!/^[\w.+-]+\.tar
|
|
3371
|
+
if (!/^[\w.+-]+\.tar(?:\.gz)?$/.test(basename(output)))
|
|
3267
3372
|
throw new Error(deployM("deploy_bundleNameInvalid"));
|
|
3268
3373
|
const staging = mkdtempSync(join(directory, ".bundle-"));
|
|
3269
3374
|
const temporaryTar = `${output}.partial`;
|
|
3270
3375
|
mkdirSync(dirname(output), { recursive: true, mode: 448 });
|
|
3271
3376
|
chmodSync(staging, 448);
|
|
3272
3377
|
const files = [];
|
|
3378
|
+
const fileHashes = /* @__PURE__ */ new Map();
|
|
3273
3379
|
const put = (file, content) => {
|
|
3274
3380
|
mkdirSync(dirname(join(staging, file)), { recursive: true });
|
|
3275
3381
|
writeFileSync(join(staging, file), content, { mode: 384 });
|
|
@@ -3294,7 +3400,7 @@ async function createDeploymentBundle(options) {
|
|
|
3294
3400
|
service.env_file = name === artifact.primaryService ? [`../${options.deploymentId}/.env.runtime`, `../${options.deploymentId}/middleware.env`] : [`../${options.deploymentId}/middleware-runtime.env`];
|
|
3295
3401
|
}
|
|
3296
3402
|
if (Array.isArray(service.volumes))
|
|
3297
|
-
service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace(
|
|
3403
|
+
service.volumes = service.volumes.map((mount) => typeof mount === "string" ? mount.replace(/\.\/(web\.conf|request-access\.yml|tls|https-start\.sh|renew-https\.sh|\.env\.runtime|middleware\.env|runtime-env\.mjs):/g, `../${options.deploymentId}/$1:`) : mount);
|
|
3298
3404
|
}
|
|
3299
3405
|
put("compose.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, compose.services[name]])) }));
|
|
3300
3406
|
put("compose-entry.json", JSON.stringify({ ...compose, services: Object.fromEntries(selected.map((name) => [name, { extends: { file: `../${options.deploymentId}/compose.json`, service: name } }])) }));
|
|
@@ -3305,8 +3411,12 @@ async function createDeploymentBundle(options) {
|
|
|
3305
3411
|
throw new Error(deployM("deploy_imageVersionRequired", { params: { image: reference } }));
|
|
3306
3412
|
const built = artifact.images.find((image) => image.imageRef === reference);
|
|
3307
3413
|
const file = built?.file ?? `middleware-${imageList.length + 1}.tar`;
|
|
3414
|
+
const destination = join(staging, "images", file);
|
|
3308
3415
|
if (built) {
|
|
3309
|
-
|
|
3416
|
+
const checksum = await copyArchive(join(artifactDir, file), destination);
|
|
3417
|
+
if (checksum !== built.sha256)
|
|
3418
|
+
throw new Error(deployM("deploy_artifactInvalid"));
|
|
3419
|
+
fileHashes.set(`images/${file}`, checksum);
|
|
3310
3420
|
} else {
|
|
3311
3421
|
if (!runtime) {
|
|
3312
3422
|
const candidates = options.runtime === "auto" ? ["podman", "docker"] : [options.runtime];
|
|
@@ -3319,14 +3429,41 @@ async function createDeploymentBundle(options) {
|
|
|
3319
3429
|
if (!runtime)
|
|
3320
3430
|
throw new Error(deployM("deploy_bundleRuntimeRequired"));
|
|
3321
3431
|
}
|
|
3322
|
-
|
|
3432
|
+
let inspected = await runCommand(runtime, ["image", "inspect", "--format", "{{.Id}}", reference]);
|
|
3433
|
+
if (!inspected.success) {
|
|
3323
3434
|
const pulled = await runCommand(runtime, ["pull", reference], { timeoutMs: 6e5 });
|
|
3324
3435
|
if (!pulled.success)
|
|
3325
3436
|
throw new Error(pulled.error.message);
|
|
3437
|
+
inspected = await runCommand(runtime, ["image", "inspect", "--format", "{{.Id}}", reference]);
|
|
3326
3438
|
}
|
|
3327
|
-
const
|
|
3328
|
-
|
|
3329
|
-
|
|
3439
|
+
const identity = inspected.success && !inspected.data.stdoutTruncated ? inspected.data.stdout.trim().replace(/^sha256:/, "") : "";
|
|
3440
|
+
const cacheIdentity = createHash("sha256").update(identity).update("\0").update(reference).digest("hex");
|
|
3441
|
+
const cache = options.cacheDirectory && /^[a-f0-9]{64}$/.test(identity) ? join(options.cacheDirectory, `${runtime}-${cacheIdentity}.tar`) : void 0;
|
|
3442
|
+
let checksum;
|
|
3443
|
+
if (cache && existsSync(cache) && existsSync(`${cache}.sha256`) && lstatSync(cache).isFile() && !lstatSync(cache).isSymbolicLink()) {
|
|
3444
|
+
const copied = await copyArchive(cache, destination);
|
|
3445
|
+
if (copied === readFileSync(`${cache}.sha256`, "utf8").trim())
|
|
3446
|
+
checksum = copied;
|
|
3447
|
+
}
|
|
3448
|
+
if (!checksum) {
|
|
3449
|
+
const saved = await runCommand(runtime, ["save", ...runtime === "podman" ? ["--format", "docker-archive"] : [], "-o", destination, reference], { timeoutMs: 6e5 });
|
|
3450
|
+
if (!saved.success)
|
|
3451
|
+
throw new Error(saved.error.message);
|
|
3452
|
+
checksum = await artifactChecksum(destination);
|
|
3453
|
+
if (cache) {
|
|
3454
|
+
mkdirSync(dirname(cache), { recursive: true, mode: 448 });
|
|
3455
|
+
const temporary = `${cache}.${basename(staging)}`;
|
|
3456
|
+
try {
|
|
3457
|
+
copyFileSync(destination, temporary);
|
|
3458
|
+
renameSync(temporary, cache);
|
|
3459
|
+
writeFileSync(`${cache}.sha256`, `${checksum}
|
|
3460
|
+
`, { mode: 384 });
|
|
3461
|
+
} finally {
|
|
3462
|
+
rmSync(temporary, { force: true });
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
}
|
|
3466
|
+
fileHashes.set(`images/${file}`, checksum);
|
|
3330
3467
|
}
|
|
3331
3468
|
files.push(`images/${file}`);
|
|
3332
3469
|
imageList.push(`${file} ${reference}`);
|
|
@@ -3335,6 +3472,8 @@ async function createDeploymentBundle(options) {
|
|
|
3335
3472
|
if (selected.includes(artifact.primaryService) && existsSync(join(directory, "request-access.yml")))
|
|
3336
3473
|
put("request-access.yml", readFileSync(join(directory, "request-access.yml"), "utf8"));
|
|
3337
3474
|
put("middleware.env", options.middlewareEnv);
|
|
3475
|
+
if (selected.includes(artifact.primaryService) && !compose.services[artifact.primaryService]?.env_file)
|
|
3476
|
+
put("runtime-env.mjs", readFileSync(new URL("./deploy-runtime-env.mjs", import.meta.url), "utf8"));
|
|
3338
3477
|
const middlewareRuntime = join(directory, "middleware-runtime.env");
|
|
3339
3478
|
const nativeEnvironment = existsSync(middlewareRuntime) ? readFileSync(middlewareRuntime, "utf8") : "";
|
|
3340
3479
|
put("middleware-runtime.env", nativeEnvironment);
|
|
@@ -3342,18 +3481,17 @@ async function createDeploymentBundle(options) {
|
|
|
3342
3481
|
put(file, content);
|
|
3343
3482
|
if (options.hasWebConfig)
|
|
3344
3483
|
put("web.conf", readFileSync(join(directory, "web.conf"), "utf8"));
|
|
3484
|
+
for (const [file, content] of Object.entries(options.httpsAssets ?? {}))
|
|
3485
|
+
put(file, content);
|
|
3345
3486
|
put("manifest.json", `${JSON.stringify({ ...artifact, images: artifact.images.filter((image) => references.has(image.imageRef)), deploymentId: options.deploymentId, selectedServices: selected, partial: options.partial }, null, 2)}
|
|
3346
3487
|
`);
|
|
3347
3488
|
if (options.hasWebConfig) {
|
|
3348
3489
|
put("proxy-entry.json", JSON.stringify({ services: { [options.proxyService ?? "web"]: { volumes: [`../${options.deploymentId}/web.conf:/etc/nginx/conf.d/default.conf:ro`] } } }));
|
|
3349
3490
|
}
|
|
3350
|
-
put("proxy-service", `${options.hasWebConfig ? options.proxyService ?? "web" : ""}
|
|
3351
|
-
`);
|
|
3352
3491
|
put("storage-routing", `${options.storageRouting ?? ""}
|
|
3353
3492
|
`);
|
|
3354
|
-
|
|
3355
|
-
`
|
|
3356
|
-
put("health-port", `${config.expose.hostPort}
|
|
3493
|
+
const httpsFingerprint = config.expose.https ? createHash("sha256").update(JSON.stringify({ ...config.expose.https, certificateFile: void 0, privateKeyFile: void 0, certificate: options.httpsAssets?.["tls/fullchain.pem"] })).digest("hex") : "";
|
|
3494
|
+
put("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}` : ""}
|
|
3357
3495
|
`);
|
|
3358
3496
|
put("web-base-path", `${artifact.webBasePath ?? ""}
|
|
3359
3497
|
`);
|
|
@@ -3371,15 +3509,15 @@ async function createDeploymentBundle(options) {
|
|
|
3371
3509
|
const selection = deployM("deploy_bundleSelection", { params: { mode, services: selected.join(", ") } });
|
|
3372
3510
|
const instructions = `${selection}
|
|
3373
3511
|
${deployM("deploy_bundleInstructions", { params: instructionParams })}`;
|
|
3374
|
-
|
|
3512
|
+
const readme = `${deployM("deploy_bundleMetadata", { params: { version: artifact.appVersion, buildId: artifact.buildId, deploymentId: options.deploymentId, compose: "compose.json" } })}
|
|
3375
3513
|
${instructions}
|
|
3376
|
-
|
|
3514
|
+
`;
|
|
3377
3515
|
const checksums = [];
|
|
3378
3516
|
for (const file of files)
|
|
3379
|
-
checksums.push(`${await artifactChecksum(join(staging, file))} ${file}`);
|
|
3517
|
+
checksums.push(`${fileHashes.get(file) ?? await artifactChecksum(join(staging, file))} ${file}`);
|
|
3380
3518
|
writeFileSync(join(staging, "SHA256SUMS"), `${checksums.join("\n")}
|
|
3381
3519
|
`);
|
|
3382
|
-
const tar = await runCommand("tar", ["-cf", temporaryTar, "-C", staging, "."]);
|
|
3520
|
+
const tar = await runCommand("tar", [output.endsWith(".gz") ? "-czf" : "-cf", temporaryTar, "-C", staging, "."]);
|
|
3383
3521
|
if (!tar.success)
|
|
3384
3522
|
throw new Error(tar.error.message);
|
|
3385
3523
|
chmodSync(temporaryTar, 384);
|
|
@@ -3418,7 +3556,7 @@ ${instructions}
|
|
|
3418
3556
|
};
|
|
3419
3557
|
writeFileSync(join(scripts, "deploy-target.json"), `${JSON.stringify({ ...target, _comments: comments }, null, 2)}
|
|
3420
3558
|
`);
|
|
3421
|
-
writeFileSync(join(scripts, "README.txt"),
|
|
3559
|
+
writeFileSync(join(scripts, "README.txt"), readme);
|
|
3422
3560
|
for (const file of [`docker-compose-${artifact.appVersion}.yml`, ".env.runtime", "middleware-runtime.env", "request-access.yml", ...options.hasWebConfig ? ["web.conf"] : []])
|
|
3423
3561
|
rmSync(join(directory, file), { force: true });
|
|
3424
3562
|
return { bundlePath: output, instructions: `${selection}
|
|
@@ -3429,6 +3567,178 @@ ${scripts}`, startName: "apply.sh" };
|
|
|
3429
3567
|
rmSync(temporaryTar, { force: true });
|
|
3430
3568
|
}
|
|
3431
3569
|
}
|
|
3570
|
+
async function copyArchive(source, destination) {
|
|
3571
|
+
const hash = createHash("sha256");
|
|
3572
|
+
const digest = new Transform({
|
|
3573
|
+
transform(chunk, _encoding, callback) {
|
|
3574
|
+
hash.update(chunk);
|
|
3575
|
+
callback(null, chunk);
|
|
3576
|
+
}
|
|
3577
|
+
});
|
|
3578
|
+
await pipeline(createReadStream(source), digest, createWriteStream(destination, { mode: 384 }));
|
|
3579
|
+
return hash.digest("hex");
|
|
3580
|
+
}
|
|
3581
|
+
var CERTBOT_IMAGE = "docker.io/certbot/certbot:v5.9.0";
|
|
3582
|
+
function createHttpsAssets(config, projectDirectory) {
|
|
3583
|
+
const assets = {
|
|
3584
|
+
https: `${config.domain} ${config.port} ${config.type}
|
|
3585
|
+
`
|
|
3586
|
+
};
|
|
3587
|
+
if (config.type === "files") {
|
|
3588
|
+
try {
|
|
3589
|
+
const readPem = (file) => {
|
|
3590
|
+
const path = resolve(projectDirectory, file);
|
|
3591
|
+
if (!statSync(path).isFile() || statSync(path).size > 1048576)
|
|
3592
|
+
throw new Error("Invalid PEM file");
|
|
3593
|
+
return readFileSync(path, "utf8");
|
|
3594
|
+
};
|
|
3595
|
+
const certificate = readPem(config.certificateFile);
|
|
3596
|
+
const key = readPem(config.privateKeyFile);
|
|
3597
|
+
const parsed = new X509Certificate(certificate);
|
|
3598
|
+
const publicKey = createPublicKey(createPrivateKey(key)).export({ type: "spki", format: "der" });
|
|
3599
|
+
const now = Date.now();
|
|
3600
|
+
if (!publicKey.equals(parsed.publicKey.export({ type: "spki", format: "der" })) || now < Date.parse(parsed.validFrom) || now >= Date.parse(parsed.validTo) || !(isIP(config.domain) ? parsed.checkIP(config.domain) : parsed.checkHost(config.domain))) {
|
|
3601
|
+
throw new Error("Certificate validation failed");
|
|
3602
|
+
}
|
|
3603
|
+
assets["tls/fullchain.pem"] = certificate;
|
|
3604
|
+
assets["tls/privkey.pem"] = key;
|
|
3605
|
+
} catch {
|
|
3606
|
+
throw new Error(deployM("deploy_httpsCertificateInvalid"));
|
|
3607
|
+
}
|
|
3608
|
+
} else {
|
|
3609
|
+
assets["renew-https.sh"] = createRenewScript(config);
|
|
3610
|
+
}
|
|
3611
|
+
assets["prepare-https.sh"] = createPrepareScript(config);
|
|
3612
|
+
assets["https-start.sh"] = String.raw`#!/bin/sh
|
|
3613
|
+
set -eu
|
|
3614
|
+
# 目录挂载允许原子替换证书;只有新证书通过 nginx -t 才重载。
|
|
3615
|
+
watch_certificate() {
|
|
3616
|
+
previous=$(sha256sum /etc/nginx/tls/fullchain.pem)
|
|
3617
|
+
while sleep 60; do
|
|
3618
|
+
current=$(sha256sum /etc/nginx/tls/fullchain.pem) || continue
|
|
3619
|
+
if [ "$previous" != "$current" ]; then
|
|
3620
|
+
if nginx -t && nginx -s reload; then previous=$current; else printf '[ERROR] HTTPS certificate reload failed\n' >&2; fi
|
|
3621
|
+
fi
|
|
3622
|
+
done
|
|
3623
|
+
}
|
|
3624
|
+
watch_certificate &
|
|
3625
|
+
exec /docker-entrypoint.sh nginx -g 'daemon off;'
|
|
3626
|
+
`;
|
|
3627
|
+
return assets;
|
|
3628
|
+
}
|
|
3629
|
+
function renderHttpsProxy(source, config, httpPort) {
|
|
3630
|
+
const publicPort = config.port === 443 ? "" : `:${config.port}`;
|
|
3631
|
+
const challenge = " location ^~ /.well-known/acme-challenge/ { root /var/www/certbot; }\n";
|
|
3632
|
+
const tls = source.replace(`listen ${httpPort};`, `listen ${httpPort === 8443 ? 8444 : 8443} ssl;
|
|
3633
|
+
ssl_certificate /etc/nginx/tls/fullchain.pem;
|
|
3634
|
+
ssl_certificate_key /etc/nginx/tls/privkey.pem;`).replace("server_name _;", `server_name ${config.domain};`).replaceAll("proxy_set_header X-Forwarded-Proto $scheme;", "proxy_set_header X-Forwarded-Proto https;");
|
|
3635
|
+
const http = config.redirectHttp ? `server {
|
|
3636
|
+
listen ${httpPort};
|
|
3637
|
+
server_name ${config.domain};
|
|
3638
|
+
${challenge} location ~ ^/_hai/(web-health|gateway-health)$ { access_log off; return 200 'ok'; }
|
|
3639
|
+
location / { return 308 https://${config.domain}${publicPort}$request_uri; }
|
|
3640
|
+
}
|
|
3641
|
+
` : source.slice(source.indexOf("server {")).replace("server_name _;", `server_name ${config.domain};
|
|
3642
|
+
${challenge}`);
|
|
3643
|
+
return `${tls}
|
|
3644
|
+
${http}`;
|
|
3645
|
+
}
|
|
3646
|
+
var publishCertificate = String.raw`publish_certificate() {
|
|
3647
|
+
certificate="/etc/letsencrypt/live/$domain"
|
|
3648
|
+
test -s "$certificate/fullchain.pem" && test -s "$certificate/privkey.pem"
|
|
3649
|
+
cp "$certificate/fullchain.pem" /output/fullchain.pem.next
|
|
3650
|
+
cp "$certificate/privkey.pem" /output/privkey.pem.next
|
|
3651
|
+
chmod 444 /output/fullchain.pem.next /output/privkey.pem.next
|
|
3652
|
+
mv /output/privkey.pem.next /output/privkey.pem
|
|
3653
|
+
mv /output/fullchain.pem.next /output/fullchain.pem
|
|
3654
|
+
}
|
|
3655
|
+
`;
|
|
3656
|
+
function createRenewScript(config) {
|
|
3657
|
+
return `#!/bin/sh
|
|
3658
|
+
set -eu
|
|
3659
|
+
domain=${shellArgument(config.domain)}
|
|
3660
|
+
${publishCertificate}${String.raw`
|
|
3661
|
+
trap 'exit 0' TERM INT
|
|
3662
|
+
# Certbot 自己判断到期时间;同一官方客户端同时负责申请与续期,不实现 ACME 协议。
|
|
3663
|
+
while :; do
|
|
3664
|
+
if certbot renew --cert-name "$domain" --non-interactive; then
|
|
3665
|
+
if [ -s "/etc/letsencrypt/live/$domain/fullchain.pem" ]; then
|
|
3666
|
+
current=$(sha256sum "/etc/letsencrypt/live/$domain/fullchain.pem" | awk '{print $1}')
|
|
3667
|
+
installed=$(sha256sum /output/fullchain.pem | awk '{print $1}')
|
|
3668
|
+
if [ "$current" != "$installed" ]; then publish_certificate; fi
|
|
3669
|
+
fi
|
|
3670
|
+
else
|
|
3671
|
+
printf '[ERROR] Certbot renewal failed; the existing certificate remains installed\n' >&2
|
|
3672
|
+
fi
|
|
3673
|
+
sleep 43200 & wait $! || exit 0
|
|
3674
|
+
done
|
|
3675
|
+
`}`;
|
|
3676
|
+
}
|
|
3677
|
+
function createPrepareScript(config) {
|
|
3678
|
+
const header = `#!/bin/sh
|
|
3679
|
+
set -eu
|
|
3680
|
+
umask 077
|
|
3681
|
+
root=$1
|
|
3682
|
+
release=$2
|
|
3683
|
+
runtime=$3
|
|
3684
|
+
mode=$4
|
|
3685
|
+
domain=${shellArgument(config.domain)}
|
|
3686
|
+
`;
|
|
3687
|
+
if (config.type === "files") {
|
|
3688
|
+
return header + String.raw`
|
|
3689
|
+
test "$mode" != prepare || {
|
|
3690
|
+
# 宿主 root/release 保持私密;容器仅可读取独立挂载的证书目录。
|
|
3691
|
+
chmod 700 "$root" "$release"
|
|
3692
|
+
chmod 755 "$release/tls"
|
|
3693
|
+
chmod 444 "$release/tls/fullchain.pem" "$release/tls/privkey.pem"
|
|
3694
|
+
}
|
|
3695
|
+
`;
|
|
3696
|
+
}
|
|
3697
|
+
const python = [
|
|
3698
|
+
"from cryptography import x509",
|
|
3699
|
+
"from cryptography.x509.oid import NameOID",
|
|
3700
|
+
"from cryptography.hazmat.primitives import hashes, serialization",
|
|
3701
|
+
"from cryptography.hazmat.primitives.asymmetric import ec",
|
|
3702
|
+
"from datetime import datetime, timedelta, timezone",
|
|
3703
|
+
"from pathlib import Path",
|
|
3704
|
+
"import sys",
|
|
3705
|
+
"name=sys.argv[1]",
|
|
3706
|
+
"key=ec.generate_private_key(ec.SECP256R1())",
|
|
3707
|
+
"subject=x509.Name([x509.NameAttribute(NameOID.COMMON_NAME,name)])",
|
|
3708
|
+
"now=datetime.now(timezone.utc)",
|
|
3709
|
+
"cert=x509.CertificateBuilder().subject_name(subject).issuer_name(subject).public_key(key.public_key()).serial_number(x509.random_serial_number()).not_valid_before(now-timedelta(minutes=1)).not_valid_after(now+timedelta(days=1)).add_extension(x509.SubjectAlternativeName([x509.DNSName(name)]),False).sign(key,hashes.SHA256())",
|
|
3710
|
+
'Path("/output/privkey.pem").write_bytes(key.private_bytes(serialization.Encoding.PEM,serialization.PrivateFormat.PKCS8,serialization.NoEncryption()))',
|
|
3711
|
+
'Path("/output/fullchain.pem").write_bytes(cert.public_bytes(serialization.Encoding.PEM))'
|
|
3712
|
+
].join("\n");
|
|
3713
|
+
const publish = `set -eu
|
|
3714
|
+
domain=${shellArgument(config.domain)}
|
|
3715
|
+
${publishCertificate}
|
|
3716
|
+
publish_certificate
|
|
3717
|
+
`;
|
|
3718
|
+
return `${header}image=${shellArgument(CERTBOT_IMAGE)}
|
|
3719
|
+
email=${shellArgument(config.email)}
|
|
3720
|
+
bootstrap=${shellArgument(python)}
|
|
3721
|
+
${String.raw`
|
|
3722
|
+
state="$root/https"
|
|
3723
|
+
certificates="$state/certificates/$domain"
|
|
3724
|
+
mkdir -p "$state/acme" "$state/webroot" "$certificates"
|
|
3725
|
+
chmod 700 "$root" "$state" "$state/acme"
|
|
3726
|
+
chmod 755 "$state/webroot" "$certificates"
|
|
3727
|
+
if [ "$mode" = prepare ]; then
|
|
3728
|
+
if [ ! -s "$certificates/fullchain.pem" ] || [ ! -s "$certificates/privkey.pem" ]; then
|
|
3729
|
+
if [ "$5" = true ]; then printf 'HTTPS certificate state missing; create a full deployment.\n' >&2; exit 1; fi
|
|
3730
|
+
# 临时证书只用于启动挑战入口;签发失败不得提交 current。
|
|
3731
|
+
"$runtime" run --rm --pull never --entrypoint python -v "$certificates:/output" "$image" -c "$bootstrap" "$domain"
|
|
3732
|
+
chmod 444 "$certificates/fullchain.pem" "$certificates/privkey.pem"
|
|
3733
|
+
fi
|
|
3734
|
+
elif [ "$mode" = issue ]; then
|
|
3735
|
+
"$runtime" run --rm --pull never -v "$state/acme:/etc/letsencrypt" -v "$state/webroot:/var/www/certbot" "$image" certonly --webroot --webroot-path /var/www/certbot --cert-name "$domain" --domain "$domain" --email "$email" --agree-tos --non-interactive --keep-until-expiring
|
|
3736
|
+
"$runtime" run --rm --pull never --entrypoint sh -v "$state/acme:/etc/letsencrypt:ro" -v "$certificates:/output" "$image" -c `}${shellArgument(publish)}
|
|
3737
|
+
fi
|
|
3738
|
+
`;
|
|
3739
|
+
}
|
|
3740
|
+
|
|
3741
|
+
// src/container/deploy-container-compose.ts
|
|
3432
3742
|
var IMAGES = {
|
|
3433
3743
|
postgres: "docker.io/library/postgres:16.15-alpine",
|
|
3434
3744
|
mysql: "docker.io/library/mysql:8.4.11",
|
|
@@ -3693,12 +4003,17 @@ function planSidecars(_appDir, projectName, scan, provisionEnv, options, s3Engin
|
|
|
3693
4003
|
const requirements = scan.middlewares?.filter((item) => item.service === "email" && item.backend === "smtp" && ["default", void 0].includes(options?.serviceModes?.[item.envPrefix] ?? options?.serviceModes?.email));
|
|
3694
4004
|
if (requirements?.length) {
|
|
3695
4005
|
for (const requirement of requirements)
|
|
3696
|
-
plan.env[requirement.envPrefix] = JSON.stringify({ name: requirement.config.name ?? "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
|
|
4006
|
+
plan.env[requirement.envPrefix] = JSON.stringify({ name: requirement.config.name ?? "email", type: "smtp", host: "email", port: 1025, secure: false, from: requirement.config.from || "noreply@example.com" });
|
|
3697
4007
|
} else if (!scan.middlewares) {
|
|
3698
4008
|
plan.env.HAI_REACH_PROVIDERS_0 = JSON.stringify({ name: "email", type: "smtp", host: "email", port: 1025, secure: false, from: "noreply@example.com" });
|
|
3699
4009
|
}
|
|
3700
4010
|
}
|
|
3701
4011
|
}
|
|
4012
|
+
for (const requirement of scan.middlewares ?? []) {
|
|
4013
|
+
const supplied = plan.env[requirement.envPrefix];
|
|
4014
|
+
if (supplied)
|
|
4015
|
+
plan.env[requirement.envPrefix] = JSON.stringify(mergeManagedMiddlewareConfig(requirement, JSON.parse(supplied)));
|
|
4016
|
+
}
|
|
3702
4017
|
return plan;
|
|
3703
4018
|
}
|
|
3704
4019
|
function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv = {}, s3Engine = "rustfs") {
|
|
@@ -3717,6 +4032,7 @@ function buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort,
|
|
|
3717
4032
|
function generateComposeFile(options) {
|
|
3718
4033
|
const { projectName, imageRef, hostPort, containerPort, sidecar } = options;
|
|
3719
4034
|
const primaryService = options.primaryService ?? "serv";
|
|
4035
|
+
const nodeRuntime = options.nodeRuntime ?? primaryService === "serv";
|
|
3720
4036
|
const app = {
|
|
3721
4037
|
image: imageRef,
|
|
3722
4038
|
labels: { "org.opencontainers.image.version": options.appVersion },
|
|
@@ -3728,11 +4044,28 @@ function generateComposeFile(options) {
|
|
|
3728
4044
|
ports: [`${hostPort}:${containerPort}`],
|
|
3729
4045
|
networks: ["hai-app"]
|
|
3730
4046
|
};
|
|
4047
|
+
if (primaryService === "serv") {
|
|
4048
|
+
app.healthcheck = {
|
|
4049
|
+
test: ["CMD", "node", "-e", "(async () => { try { const { core } = await import('@h-ai/core'); const { ServConfigSchema } = await import('@h-ai/serv'); core.init({ configDir: './config' }); 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); } })()"],
|
|
4050
|
+
interval: "30s",
|
|
4051
|
+
timeout: "5s",
|
|
4052
|
+
start_period: "15s",
|
|
4053
|
+
retries: 3
|
|
4054
|
+
};
|
|
4055
|
+
}
|
|
3731
4056
|
const appVolumes = [...sidecar.appVolumes];
|
|
4057
|
+
const appEnvironment = {};
|
|
4058
|
+
if (nodeRuntime) {
|
|
4059
|
+
delete app.env_file;
|
|
4060
|
+
appVolumes.push("./.env.runtime:/run/hai/.env.runtime:ro", "./middleware.env:/run/hai/middleware.env:ro", "./runtime-env.mjs:/run/hai/runtime-env.mjs:ro");
|
|
4061
|
+
appEnvironment.NODE_OPTIONS = `--import /run/hai/runtime-env.mjs${options.nodeOptions ? ` ${options.nodeOptions}` : ""}`.replaceAll("$", () => "$$");
|
|
4062
|
+
}
|
|
3732
4063
|
if (options.requestAccessPolicy)
|
|
3733
4064
|
appVolumes.push("./request-access.yml:/run/hai/request-access.yml:ro");
|
|
3734
4065
|
if (options.web || options.gateway)
|
|
3735
|
-
|
|
4066
|
+
appEnvironment.HAI_DEPLOY_PRIVATE_PROXY = "1";
|
|
4067
|
+
if (Object.keys(appEnvironment).length)
|
|
4068
|
+
app.environment = appEnvironment;
|
|
3736
4069
|
if (options.mountWebConfig)
|
|
3737
4070
|
appVolumes.push("./web.conf:/etc/nginx/conf.d/default.conf:ro");
|
|
3738
4071
|
if (appVolumes.length)
|
|
@@ -3765,10 +4098,38 @@ function generateComposeFile(options) {
|
|
|
3765
4098
|
ports: [`${hostPort}:8080`],
|
|
3766
4099
|
healthcheck: { test: ["CMD", "wget", "-Y", "off", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/_hai/gateway-health"], interval: "30s", timeout: "5s", retries: 3 },
|
|
3767
4100
|
volumes: ["./web.conf:/etc/nginx/conf.d/default.conf:ro"],
|
|
3768
|
-
depends_on: [primaryService, "storage"],
|
|
4101
|
+
depends_on: [primaryService, ...sidecar.services.includes("storage") ? ["storage"] : []],
|
|
3769
4102
|
networks: ["hai-app"]
|
|
3770
4103
|
};
|
|
3771
4104
|
}
|
|
4105
|
+
if (options.https) {
|
|
4106
|
+
const proxy = services[options.gateway ? "gateway" : options.web ? "web" : primaryService];
|
|
4107
|
+
const https = options.https;
|
|
4108
|
+
const httpPort = options.gateway ? 8080 : options.web?.containerPort ?? containerPort;
|
|
4109
|
+
proxy.ports = [...proxy.ports, `${https.port}:${httpPort === 8443 ? 8444 : 8443}`];
|
|
4110
|
+
const certificates = https.type === "files" ? "./tls" : `../../https/certificates/${https.domain}`;
|
|
4111
|
+
proxy.volumes = [
|
|
4112
|
+
...proxy.volumes,
|
|
4113
|
+
`${certificates}:/etc/nginx/tls:ro`,
|
|
4114
|
+
"./https-start.sh:/run/hai/https-start.sh:ro",
|
|
4115
|
+
...https.type === "letsencrypt" ? ["../../https/webroot:/var/www/certbot:ro"] : []
|
|
4116
|
+
];
|
|
4117
|
+
proxy.entrypoint = ["sh", "/run/hai/https-start.sh"];
|
|
4118
|
+
if (https.type === "letsencrypt") {
|
|
4119
|
+
services.certbot = {
|
|
4120
|
+
image: CERTBOT_IMAGE,
|
|
4121
|
+
restart: "unless-stopped",
|
|
4122
|
+
entrypoint: ["sh", "/run/hai/renew-https.sh"],
|
|
4123
|
+
volumes: [
|
|
4124
|
+
"../../https/acme:/etc/letsencrypt",
|
|
4125
|
+
"../../https/webroot:/var/www/certbot",
|
|
4126
|
+
`../../https/certificates/${https.domain}:/output`,
|
|
4127
|
+
"./renew-https.sh:/run/hai/renew-https.sh:ro"
|
|
4128
|
+
],
|
|
4129
|
+
networks: ["hai-app"]
|
|
4130
|
+
};
|
|
4131
|
+
}
|
|
4132
|
+
}
|
|
3772
4133
|
const compose = {
|
|
3773
4134
|
"x-hai-version": options.appVersion,
|
|
3774
4135
|
services,
|
|
@@ -3787,12 +4148,22 @@ function generateWebProxyConfig(port, apiPrefix, options = {}) {
|
|
|
3787
4148
|
const webPort = options.webPort ?? 8080;
|
|
3788
4149
|
const webBasePath = options.webBasePath ?? "";
|
|
3789
4150
|
const apiPath = apiPrefix.replace(/\/+$/, "");
|
|
3790
|
-
|
|
4151
|
+
const backendPaths = [...new Set(options.backendPaths ?? ["/health"])];
|
|
4152
|
+
if (!/^\/[\w/-]+$/.test(apiPrefix) || ![port, webPort].every((value) => Number.isInteger(value) && value >= 1 && value <= 65535) || backendPaths.some((path) => !/^\/[\w./-]*$/.test(path) || path.startsWith("//") || path.split("/").some((part) => part === "." || part === "..") || path === `${webBasePath}/runtime-config.js` || path === "/_hai/web-health") || webBasePath && (backendPaths.includes(webBasePath) || webBasePath === apiPath || webBasePath.startsWith(`${apiPath}/`))) {
|
|
3791
4153
|
throw new Error("Invalid web proxy target");
|
|
3792
4154
|
}
|
|
3793
4155
|
const webLocation = renderWebLocation(webBasePath);
|
|
3794
|
-
const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [apiPath, webBasePath]) : "";
|
|
3795
|
-
|
|
4156
|
+
const storageLocation = options.storageBucket ? renderStorageLocation(options.storageBucket, [apiPath, ...backendPaths, ...options.storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]], options.storageBasePath) : "";
|
|
4157
|
+
const backendLocations = backendPaths.map((path) => ` location = ${path} {
|
|
4158
|
+
proxy_pass http://serv:${port};
|
|
4159
|
+
proxy_http_version 1.1;
|
|
4160
|
+
proxy_set_header Host $http_host;
|
|
4161
|
+
proxy_set_header X-Forwarded-Proto $scheme;
|
|
4162
|
+
proxy_set_header X-Forwarded-For $remote_addr;
|
|
4163
|
+
proxy_set_header X-Real-IP $remote_addr;
|
|
4164
|
+
}
|
|
4165
|
+
`).join("");
|
|
4166
|
+
return `${options.storageBucket ? renderStorageUpstream(options.storageBucket, options.storageBasePath) : ""}server {
|
|
3796
4167
|
listen ${webPort};
|
|
3797
4168
|
server_name _;
|
|
3798
4169
|
${renderTrustedEdgeProxies(options.trustedEdgeProxies)} root /usr/share/nginx/html;
|
|
@@ -3806,13 +4177,7 @@ ${storageLocation}${webLocation} location = ${webBasePath}/runtime-config.js {
|
|
|
3806
4177
|
access_log off;
|
|
3807
4178
|
return 200 'ok';
|
|
3808
4179
|
}
|
|
3809
|
-
location
|
|
3810
|
-
proxy_pass http://serv:${port};
|
|
3811
|
-
proxy_set_header Host $http_host;
|
|
3812
|
-
proxy_set_header X-Forwarded-For $remote_addr;
|
|
3813
|
-
proxy_set_header X-Real-IP $remote_addr;
|
|
3814
|
-
}
|
|
3815
|
-
location ${apiPrefix.replace(/\/$/, "")}/ {
|
|
4180
|
+
${backendLocations} location ${apiPrefix.replace(/\/$/, "")}/ {
|
|
3816
4181
|
client_max_body_size ${bodyLimits.encryptedPayloadBytes};
|
|
3817
4182
|
proxy_pass http://serv:${port};
|
|
3818
4183
|
proxy_http_version 1.1;
|
|
@@ -3828,9 +4193,11 @@ ${storageLocation}${webLocation} location = ${webBasePath}/runtime-config.js {
|
|
|
3828
4193
|
}
|
|
3829
4194
|
`;
|
|
3830
4195
|
}
|
|
3831
|
-
function renderStorageLocation(bucket, reservedPaths) {
|
|
3832
|
-
|
|
3833
|
-
|
|
4196
|
+
function renderStorageLocation(bucket, reservedPaths, storageBasePath = "") {
|
|
4197
|
+
renderWebLocation(storageBasePath);
|
|
4198
|
+
const path = `${storageBasePath}/${bucket}`;
|
|
4199
|
+
const reserved = ["/health", "/_hai", ...reservedPaths].filter(Boolean);
|
|
4200
|
+
if (!/^[a-z0-9][a-z0-9.-]{1,61}[a-z0-9]$/.test(bucket) || bucket.includes("..") || ["health", "_hai"].includes(bucket) || reserved.some((item) => item === path || item.startsWith(`${path}/`) || path.startsWith(`${item}/`) || storageBasePath && (item === storageBasePath || item.startsWith(`${storageBasePath}/`) || storageBasePath.startsWith(`${item}/`)))) {
|
|
3834
4201
|
throw new Error("Storage bucket conflicts with application routes or is not a valid DNS bucket name");
|
|
3835
4202
|
}
|
|
3836
4203
|
return ` location ^~ ${path}/ {
|
|
@@ -3847,7 +4214,8 @@ function renderStorageLocation(bucket, reservedPaths) {
|
|
|
3847
4214
|
add_header Cache-Control "private, no-store" always;
|
|
3848
4215
|
limit_except GET PUT OPTIONS { deny all; }
|
|
3849
4216
|
client_max_body_size 0;
|
|
3850
|
-
|
|
4217
|
+
${storageBasePath ? ` # map \u4FDD\u7559\u539F\u59CB URI \u4E0E\u67E5\u8BE2\uFF1B\u4E0D\u5728 location \u5185\u521B\u5EFA\u6539\u53D8\u4EE3\u7406\u7EE7\u627F\u7684 if \u914D\u7F6E\u3002
|
|
4218
|
+
proxy_pass http://hai_storage_proxy$hai_storage_uri;` : " proxy_pass http://storage:9000;"}
|
|
3851
4219
|
proxy_http_version 1.1;
|
|
3852
4220
|
proxy_set_header Host $http_host;
|
|
3853
4221
|
proxy_set_header Connection "";
|
|
@@ -3859,16 +4227,27 @@ function renderStorageLocation(bucket, reservedPaths) {
|
|
|
3859
4227
|
}
|
|
3860
4228
|
`;
|
|
3861
4229
|
}
|
|
4230
|
+
function renderStorageUpstream(bucket, storageBasePath = "") {
|
|
4231
|
+
if (!storageBasePath)
|
|
4232
|
+
return "";
|
|
4233
|
+
const path = `${storageBasePath}/${bucket}`.replaceAll(".", "\\\\.");
|
|
4234
|
+
return `upstream hai_storage_proxy { server storage:9000; }
|
|
4235
|
+
map $request_uri $hai_storage_uri {
|
|
4236
|
+
default $request_uri;
|
|
4237
|
+
"~^${path}(/.*)$" /${bucket}$1;
|
|
4238
|
+
}
|
|
4239
|
+
`;
|
|
4240
|
+
}
|
|
3862
4241
|
function generateApplicationProxyConfig(options) {
|
|
3863
4242
|
const { primaryService, port, bucket, reservedPaths, maxPlaintextBytes } = options;
|
|
3864
4243
|
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
3865
4244
|
throw new Error("Invalid application proxy target");
|
|
3866
4245
|
const bodyLimits = resolveTransportBodyLimits(maxPlaintextBytes);
|
|
3867
|
-
return
|
|
4246
|
+
return `${bucket ? renderStorageUpstream(bucket, options.storageBasePath) : ""}server {
|
|
3868
4247
|
listen 8080;
|
|
3869
4248
|
server_name _;
|
|
3870
4249
|
${renderTrustedEdgeProxies(options.trustedEdgeProxies)} large_client_header_buffers 4 32k;
|
|
3871
|
-
${renderStorageLocation(bucket, reservedPaths)} location = /_hai/gateway-health {
|
|
4250
|
+
${bucket ? renderStorageLocation(bucket, reservedPaths, options.storageBasePath) : ""} location = /_hai/gateway-health {
|
|
3872
4251
|
access_log off;
|
|
3873
4252
|
return 200 'ok';
|
|
3874
4253
|
}
|
|
@@ -3976,9 +4355,22 @@ function prepareRequestAccess(options) {
|
|
|
3976
4355
|
}
|
|
3977
4356
|
}
|
|
3978
4357
|
var HEALTH_MAX_ATTEMPTS = 40;
|
|
4358
|
+
function servicePath(value, fallback) {
|
|
4359
|
+
const path = value ?? fallback;
|
|
4360
|
+
if (typeof path !== "string" || !/^\/[\w./-]*$/.test(path) || path.startsWith("//") || path.split("/").some((part) => part === "." || part === ".."))
|
|
4361
|
+
throw new Error("Invalid service endpoint path");
|
|
4362
|
+
return path;
|
|
4363
|
+
}
|
|
4364
|
+
function endpointSettings(value) {
|
|
4365
|
+
if (value === void 0)
|
|
4366
|
+
return {};
|
|
4367
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
4368
|
+
throw new Error("Invalid service endpoint configuration");
|
|
4369
|
+
return value;
|
|
4370
|
+
}
|
|
3979
4371
|
var ServerResultSchema = z.object({
|
|
3980
4372
|
deploymentId: z.string(),
|
|
3981
|
-
stage: z.enum(["preflight", "credentials", "recover", "images", "dependencies", "start", "health", "complete"]),
|
|
4373
|
+
stage: z.enum(["preflight", "credentials", "recover", "images", "dependencies", "start", "https", "health", "complete"]),
|
|
3982
4374
|
exitCode: z.number().int().min(0).max(255),
|
|
3983
4375
|
durationSeconds: z.number().int().nonnegative(),
|
|
3984
4376
|
recoveryStatus: z.enum(["not-needed", "passed", "failed", "stopped"]).optional()
|
|
@@ -3990,7 +4382,7 @@ function makeDeploymentId() {
|
|
|
3990
4382
|
return `${stamp}-${randomBytes(3).toString("hex")}`;
|
|
3991
4383
|
}
|
|
3992
4384
|
function sleep(ms) {
|
|
3993
|
-
return new Promise((
|
|
4385
|
+
return new Promise((resolve9) => setTimeout(resolve9, ms));
|
|
3994
4386
|
}
|
|
3995
4387
|
function createDockerSshProvider(config, container) {
|
|
3996
4388
|
const ssh = config.ssh ? createSshExecutor(config.ssh) : void 0;
|
|
@@ -4014,13 +4406,28 @@ function createDockerSshProvider(config, container) {
|
|
|
4014
4406
|
deployM("deploy_remoteRuntimeMissing", { params: { runtime: candidates.join(" / ") } })
|
|
4015
4407
|
);
|
|
4016
4408
|
}
|
|
4017
|
-
async function healthCheck(url) {
|
|
4409
|
+
async function healthCheck(url, certificate) {
|
|
4018
4410
|
for (let attempt = 0; attempt < HEALTH_MAX_ATTEMPTS; attempt++) {
|
|
4019
4411
|
try {
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4412
|
+
if (certificate) {
|
|
4413
|
+
const healthy = await new Promise((resolve9) => {
|
|
4414
|
+
const request$1 = request(url, { ca: [...rootCertificates, certificate], timeout: 5e3 }, (response) => {
|
|
4415
|
+
response.resume();
|
|
4416
|
+
resolve9(response.statusCode !== void 0 && response.statusCode >= 200 && response.statusCode < 300);
|
|
4417
|
+
});
|
|
4418
|
+
request$1.on("error", () => resolve9(false));
|
|
4419
|
+
request$1.on("close", () => resolve9(false));
|
|
4420
|
+
request$1.on("timeout", () => request$1.destroy());
|
|
4421
|
+
request$1.end();
|
|
4422
|
+
});
|
|
4423
|
+
if (healthy)
|
|
4424
|
+
return true;
|
|
4425
|
+
} else {
|
|
4426
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(5e3) });
|
|
4427
|
+
await response.body?.cancel();
|
|
4428
|
+
if (response.ok)
|
|
4429
|
+
return true;
|
|
4430
|
+
}
|
|
4024
4431
|
} catch {
|
|
4025
4432
|
}
|
|
4026
4433
|
await sleep(HEALTH_POLL_INTERVAL);
|
|
@@ -4031,6 +4438,8 @@ function createDockerSshProvider(config, container) {
|
|
|
4031
4438
|
const { appDir, projectName, scan, provisionEnv, appEnv, options } = context;
|
|
4032
4439
|
const deploymentId = makeDeploymentId();
|
|
4033
4440
|
const hostPort = config.expose.hostPort;
|
|
4441
|
+
const https = config.expose.https;
|
|
4442
|
+
const httpsAssets = https ? createHttpsAssets(https, resolveProjectDirectory(appDir)) : void 0;
|
|
4034
4443
|
const deploymentRoot = join(resolveProjectDirectory(appDir), ".hai", "deploy", options?.environment ?? "prod");
|
|
4035
4444
|
const artifact = context.artifact;
|
|
4036
4445
|
const resolvedBuildDir = context.artifactDir;
|
|
@@ -4049,26 +4458,32 @@ function createDockerSshProvider(config, container) {
|
|
|
4049
4458
|
const containerPort = app?.containerPort ?? (primaryService === "serv" ? artifact.servContainerPort : artifact.webContainerPort) ?? config.expose.containerPort;
|
|
4050
4459
|
const webContainerPort = web?.containerPort ?? artifact.webContainerPort ?? 8080;
|
|
4051
4460
|
const host = config.ssh?.host;
|
|
4052
|
-
const url = host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
|
|
4461
|
+
const url = https ? `https://${https.domain}${https.port === 443 ? "" : `:${https.port}`}` : host ? `http://${host.includes(":") ? `[${host}]` : host}:${hostPort}` : `http://SERVER_HOST:${hostPort}`;
|
|
4053
4462
|
const webBasePath = artifact.webBasePath ?? "";
|
|
4054
4463
|
const applicationUrl = `${url}${webBasePath ? `${webBasePath}/` : ""}`;
|
|
4055
|
-
const standaloneWebConfig = primaryService === "web" && !scan.isSvelteKit && !!webBasePath;
|
|
4056
|
-
const healthPaths = hasWebProxy ? ["/
|
|
4464
|
+
const standaloneWebConfig = primaryService === "web" && !scan.isSvelteKit && (!!webBasePath || !!https);
|
|
4465
|
+
const healthPaths = hasWebProxy ? ["/_hai/web-health", `${webBasePath}/`] : scan.isSvelteKit ? [`${webBasePath}/`] : primaryService === "serv" ? [] : standaloneWebConfig ? ["/health", `${webBasePath}/`] : ["/health"];
|
|
4057
4466
|
const { env, sidecar } = buildRuntimeEnv(appDir, projectName, scan, provisionEnv, containerPort, options, appEnv, container?.s3Engine);
|
|
4467
|
+
if (https && scan.isSvelteKit && !env.ORIGIN)
|
|
4468
|
+
env.ORIGIN = url;
|
|
4058
4469
|
const managedStorage = sidecar.services.includes("storage") ? JSON.parse(sidecar.env.HAI_STORAGE) : void 0;
|
|
4470
|
+
let storageBasePath = "";
|
|
4059
4471
|
if (managedStorage?.publicEndpoint !== void 0) {
|
|
4060
4472
|
try {
|
|
4061
4473
|
if (typeof managedStorage.publicEndpoint !== "string")
|
|
4062
4474
|
throw new Error("Invalid public endpoint type");
|
|
4475
|
+
const unsafeCharacters = /[\u0000-\u0020\u007F\\%]/.test(managedStorage.publicEndpoint);
|
|
4063
4476
|
const endpoint = new URL(managedStorage.publicEndpoint);
|
|
4064
|
-
if (!["http:", "https:"].includes(endpoint.protocol) || endpoint.username || endpoint.password || endpoint.
|
|
4477
|
+
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)) {
|
|
4065
4478
|
throw new Error("Invalid public endpoint origin");
|
|
4479
|
+
}
|
|
4480
|
+
storageBasePath = endpoint.pathname.replace(/\/+$/, "");
|
|
4066
4481
|
} catch {
|
|
4067
4482
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storagePublicEndpointInvalid"));
|
|
4068
4483
|
}
|
|
4069
4484
|
}
|
|
4070
4485
|
const storageRouting = managedStorage ? createHash("sha256").update(JSON.stringify({ ...managedStorage, secretAccessKey: void 0 })).digest("hex") : "";
|
|
4071
|
-
const gateway = !!managedStorage && !
|
|
4486
|
+
const gateway = !hasWebProxy && (!!managedStorage || !!https && !standaloneWebConfig);
|
|
4072
4487
|
if (gateway && primaryService === "web" && !scan.isSvelteKit)
|
|
4073
4488
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
|
|
4074
4489
|
const proxyService = gateway ? "gateway" : "web";
|
|
@@ -4079,6 +4494,35 @@ function createDockerSshProvider(config, container) {
|
|
|
4079
4494
|
const server = existsSync(serverPath) || hasWebProxy ? loadDeploymentConfig("serv", serverPath, env) : ok({});
|
|
4080
4495
|
if (!server.success)
|
|
4081
4496
|
return server;
|
|
4497
|
+
const backendPaths = [];
|
|
4498
|
+
try {
|
|
4499
|
+
const http = endpointSettings(server.data.http);
|
|
4500
|
+
if (primaryService === "serv" && http.health !== false) {
|
|
4501
|
+
const health = endpointSettings(http.health);
|
|
4502
|
+
const path = servicePath(health.path, "/health");
|
|
4503
|
+
healthPaths.unshift(path);
|
|
4504
|
+
backendPaths.push(path, servicePath(health.readyPath, "/ready"));
|
|
4505
|
+
}
|
|
4506
|
+
if (hasWebProxy) {
|
|
4507
|
+
const corePath = join(appDir, "config", "_core.yml");
|
|
4508
|
+
const settings = existsSync(corePath) ? loadDeploymentConfig("core", corePath, env) : ok(env.HAI_CORE ? JSON.parse(env.HAI_CORE) : { env: env.HAI_CORE_ENV });
|
|
4509
|
+
if (!settings.success)
|
|
4510
|
+
return settings;
|
|
4511
|
+
const application = CoreConfigSchema.safeParse(settings.data);
|
|
4512
|
+
if (!application.success)
|
|
4513
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
|
|
4514
|
+
if (application.data.env !== "production") {
|
|
4515
|
+
if (http.openapi !== void 0 && http.openapi !== false)
|
|
4516
|
+
backendPaths.push(servicePath(endpointSettings(http.openapi).path, "/openapi.json"));
|
|
4517
|
+
if (http.docs !== void 0 && http.docs !== false) {
|
|
4518
|
+
const docsPath = servicePath(endpointSettings(http.docs).path, "/docs");
|
|
4519
|
+
backendPaths.push(docsPath, `${docsPath.slice(0, docsPath.lastIndexOf("/") + 1)}_hai/scalar.js`);
|
|
4520
|
+
}
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
} catch {
|
|
4524
|
+
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
|
|
4525
|
+
}
|
|
4082
4526
|
let requestAccessSnapshot;
|
|
4083
4527
|
if (updatesApp && primaryService === "serv") {
|
|
4084
4528
|
const prepared2 = prepareRequestAccess({ settings: server.data, module: "serv", directory: appDir });
|
|
@@ -4140,25 +4584,25 @@ function createDockerSshProvider(config, container) {
|
|
|
4140
4584
|
if (typeof apiPrefix !== "string" || !/^\/[\w/-]+$/.test(apiPrefix)) {
|
|
4141
4585
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webProxyInvalid"));
|
|
4142
4586
|
}
|
|
4143
|
-
if (webBasePath && ([
|
|
4587
|
+
if (webBasePath && ([...backendPaths, "/_hai/web-health"].includes(webBasePath) || webBasePath === apiPrefix.replace(/\/+$/, "") || webBasePath.startsWith(`${apiPrefix.replace(/\/+$/, "")}/`)))
|
|
4144
4588
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
|
|
4145
4589
|
const transport = server.data.transport;
|
|
4146
4590
|
try {
|
|
4147
4591
|
const configured = transport ? transport.maxPlaintextBytes : void 0;
|
|
4148
4592
|
const limits = resolveTransportBodyLimits(Math.max(createTransportBodyPolicy(configured === "auto" ? void 0 : configured, transport ? transport.routePlaintextBytes : void 0).maximum.plaintextBytes, feedbackTransportBytes ?? 0));
|
|
4149
|
-
webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, storageBucket: managedStorage?.bucket, trustedEdgeProxies: config.trustedEdgeProxies });
|
|
4593
|
+
webProxy = generateWebProxyConfig(containerPort, apiPrefix, { webPort: webContainerPort, maxPlaintextBytes: limits.plaintextBytes, webBasePath, backendPaths, storageBucket: managedStorage?.bucket, storageBasePath, trustedEdgeProxies: config.trustedEdgeProxies });
|
|
4150
4594
|
} catch (error) {
|
|
4151
4595
|
return err(HaiDeployError.CONFIG_ERROR, error instanceof RangeError ? error.message : deployM(managedStorage ? "deploy_storageProxyInvalid" : "deploy_transportBodyLimitInvalid"));
|
|
4152
4596
|
}
|
|
4153
|
-
env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && config.ssh ? url : cors?.origin ?? "*" } });
|
|
4154
|
-
} else if (gateway
|
|
4597
|
+
env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: (!cors?.origin || cors.origin === "*") && (config.ssh || https) ? url : cors?.origin ?? "*" } });
|
|
4598
|
+
} else if (gateway) {
|
|
4155
4599
|
try {
|
|
4156
4600
|
const http = server.data.http;
|
|
4157
4601
|
if (primaryService === "serv") {
|
|
4158
4602
|
const transport = server.data.transport;
|
|
4159
4603
|
applicationPlaintextBytes = Math.max(createTransportBodyPolicy(transport?.maxPlaintextBytes === "auto" ? void 0 : transport?.maxPlaintextBytes, transport?.routePlaintextBytes).maximum.plaintextBytes, feedbackTransportBytes ?? 0);
|
|
4160
4604
|
}
|
|
4161
|
-
webProxy = generateApplicationProxyConfig({ primaryService, port: containerPort, bucket: managedStorage
|
|
4605
|
+
webProxy = generateApplicationProxyConfig({ primaryService, port: containerPort, bucket: managedStorage?.bucket, storageBasePath, reservedPaths: [http?.apiPrefix ?? "/api/v1", ...storageBasePath ? [`${webBasePath}/_app`, `${webBasePath}/assets`, `${webBasePath}/runtime-config.js`] : [webBasePath]], maxPlaintextBytes: applicationPlaintextBytes, trustedEdgeProxies: config.trustedEdgeProxies });
|
|
4162
4606
|
} catch {
|
|
4163
4607
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageProxyInvalid"));
|
|
4164
4608
|
}
|
|
@@ -4167,8 +4611,15 @@ function createDockerSshProvider(config, container) {
|
|
|
4167
4611
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_webBaseConflict"));
|
|
4168
4612
|
webProxy = generateStaticWebConfig(containerPort, webBasePath);
|
|
4169
4613
|
}
|
|
4170
|
-
|
|
4171
|
-
|
|
4614
|
+
if (https && webProxy)
|
|
4615
|
+
webProxy = renderHttpsProxy(webProxy, https, hasWebProxy ? webContainerPort : gateway ? 8080 : containerPort);
|
|
4616
|
+
if (https && primaryService === "serv" && !hasWebProxy) {
|
|
4617
|
+
const cors = server.data.cors;
|
|
4618
|
+
env.HAI_SERV = JSON.stringify({ ...server.data, cors: { ...cors, origin: !cors?.origin || cors.origin === "*" ? url : cors.origin } });
|
|
4619
|
+
}
|
|
4620
|
+
const requestedOutput = options?.output ?? join(deploymentRoot, `bundle-${projectName}-${deploymentId}-${artifact.appVersion}.tar.gz`);
|
|
4621
|
+
const compressed = requestedOutput.endsWith(".tar.gz");
|
|
4622
|
+
const stem = basename(requestedOutput).replace(/\.tar(?:\.gz)?$/i, "");
|
|
4172
4623
|
const suffix = `-${artifact.appVersion}`;
|
|
4173
4624
|
const name = stem.endsWith(suffix) ? stem : `${stem}${suffix}`;
|
|
4174
4625
|
if (!/^[\w.+-]+$/.test(name))
|
|
@@ -4201,10 +4652,13 @@ function createDockerSshProvider(config, container) {
|
|
|
4201
4652
|
containerPort,
|
|
4202
4653
|
primaryService,
|
|
4203
4654
|
requestAccessPolicy: requestAccessSnapshot !== void 0,
|
|
4655
|
+
nodeRuntime: primaryService === "serv" || scan.isSvelteKit,
|
|
4656
|
+
nodeOptions: env.NODE_OPTIONS,
|
|
4204
4657
|
sidecar,
|
|
4205
4658
|
web: hasWebProxy ? { imageRef: web?.imageRef ?? "localhost/hai-unselected:0", containerPort: webContainerPort } : void 0,
|
|
4206
4659
|
mountWebConfig: standaloneWebConfig,
|
|
4207
|
-
gateway
|
|
4660
|
+
gateway,
|
|
4661
|
+
https
|
|
4208
4662
|
});
|
|
4209
4663
|
const composePath = join(artifactDir, composeName);
|
|
4210
4664
|
writeFileSync(composePath, composeContent);
|
|
@@ -4213,6 +4667,8 @@ function createDockerSshProvider(config, container) {
|
|
|
4213
4667
|
const selected = options?.selectedServices ? [...options.selectedServices] : artifact.partial ? images.map((image) => image.name) : [...images.map((image) => image.name), ...Object.keys(sidecar.composeServices)];
|
|
4214
4668
|
if (gateway && !partial)
|
|
4215
4669
|
selected.push("gateway");
|
|
4670
|
+
if (https?.type === "letsencrypt" && !partial)
|
|
4671
|
+
selected.push("certbot");
|
|
4216
4672
|
if (!selected.length || selected.some((name2) => !/^[a-z][a-z0-9-]*$/.test(name2)) || new Set(selected).size !== selected.length)
|
|
4217
4673
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_selectionInvalid", { params: { service: selected.join(", ") } }));
|
|
4218
4674
|
for (const name2 of selected) {
|
|
@@ -4237,7 +4693,9 @@ function createDockerSshProvider(config, container) {
|
|
|
4237
4693
|
containerPort,
|
|
4238
4694
|
middlewareEnv: serializeEnvFile(sidecar.env),
|
|
4239
4695
|
healthPaths,
|
|
4240
|
-
|
|
4696
|
+
httpsAssets,
|
|
4697
|
+
output: join(artifactDir, `${name}.tar${compressed ? ".gz" : ""}`),
|
|
4698
|
+
cacheDirectory: join(resolveProjectDirectory(appDir), ".hai", "cache", "images")
|
|
4241
4699
|
});
|
|
4242
4700
|
if (options?.bundleOnly)
|
|
4243
4701
|
return ok({ url: "", deploymentId, status: "packaged", provider: "docker-ssh", healthCheck: "skipped", envVarsSet: Object.keys(env), ...bundle });
|
|
@@ -4259,7 +4717,7 @@ function createDockerSshProvider(config, container) {
|
|
|
4259
4717
|
return uploaded;
|
|
4260
4718
|
let pendingOutput = "";
|
|
4261
4719
|
const started = await ssh.exec(
|
|
4262
|
-
`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)}`,
|
|
4720
|
+
`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; }`,
|
|
4263
4721
|
{ timeoutMs: 6e5, secrets: sidecar.secrets, onOutput: (output) => {
|
|
4264
4722
|
const parts = output.split("\n");
|
|
4265
4723
|
for (let index = 0; index < parts.length; index++) {
|
|
@@ -4268,7 +4726,7 @@ function createDockerSshProvider(config, container) {
|
|
|
4268
4726
|
break;
|
|
4269
4727
|
const line = pendingOutput;
|
|
4270
4728
|
pendingOutput = "";
|
|
4271
|
-
const stage = /^\[(preflight|images|dependencies|start|health|complete)\]/.exec(line)?.[1];
|
|
4729
|
+
const stage = /^\[(preflight|images|dependencies|start|https|health|complete)\]/.exec(line)?.[1];
|
|
4272
4730
|
if (stage)
|
|
4273
4731
|
options?.onProgress?.(stage);
|
|
4274
4732
|
}
|
|
@@ -4301,7 +4759,7 @@ ${terminal ? deployM("deploy_remoteFinished", { params: { stage: terminal.stage,
|
|
|
4301
4759
|
logger5.info("Running health check", { url: applicationUrl });
|
|
4302
4760
|
let healthy = true;
|
|
4303
4761
|
for (const healthPath of healthPaths) {
|
|
4304
|
-
if (!await healthCheck(`${url}${healthPath}
|
|
4762
|
+
if (!await healthCheck(`${url}${healthPath}`, httpsAssets?.["tls/fullchain.pem"])) {
|
|
4305
4763
|
healthy = false;
|
|
4306
4764
|
break;
|
|
4307
4765
|
}
|
|
@@ -4588,7 +5046,7 @@ async function pollDeploymentStatus(token, deploymentId) {
|
|
|
4588
5046
|
if (deployment.readyState === "ERROR" || deployment.readyState === "CANCELED") {
|
|
4589
5047
|
return "error";
|
|
4590
5048
|
}
|
|
4591
|
-
await new Promise((
|
|
5049
|
+
await new Promise((resolve9) => setTimeout(resolve9, POLL_INTERVAL));
|
|
4592
5050
|
}
|
|
4593
5051
|
return "building";
|
|
4594
5052
|
}
|
|
@@ -5123,7 +5581,7 @@ var deployCredentials = {
|
|
|
5123
5581
|
saveAll: (entries) => saveCredentials(entries)
|
|
5124
5582
|
};
|
|
5125
5583
|
function mergeProvisionEnv(results, scan, options) {
|
|
5126
|
-
|
|
5584
|
+
const env = {};
|
|
5127
5585
|
const reachRequirements = scan?.middlewares?.filter((item) => item.configName === "reach") ?? [];
|
|
5128
5586
|
const reachProviders = reachRequirements.map((item) => item.config);
|
|
5129
5587
|
let hasReachOutput = false;
|
|
@@ -5138,13 +5596,16 @@ function mergeProvisionEnv(results, scan, options) {
|
|
|
5138
5596
|
});
|
|
5139
5597
|
if (targets.length) {
|
|
5140
5598
|
for (const target of targets)
|
|
5141
|
-
reachProviders[reachRequirements.indexOf(target)] = { ...provider, name: target.config.name ?? provider.name };
|
|
5599
|
+
reachProviders[reachRequirements.indexOf(target)] = { ...mergeManagedMiddlewareConfig(target, provider), name: target.config.name ?? provider.name };
|
|
5142
5600
|
} else {
|
|
5143
5601
|
reachProviders.push(provider);
|
|
5144
5602
|
}
|
|
5145
5603
|
}
|
|
5146
5604
|
}
|
|
5147
|
-
|
|
5605
|
+
for (const [name, value] of Object.entries(prov.envVars)) {
|
|
5606
|
+
const requirement = scan?.middlewares?.find((item) => item.envPrefix === name);
|
|
5607
|
+
env[name] = requirement ? JSON.stringify(mergeManagedMiddlewareConfig(requirement, JSON.parse(value))) : value;
|
|
5608
|
+
}
|
|
5148
5609
|
}
|
|
5149
5610
|
if (hasReachOutput)
|
|
5150
5611
|
env.HAI_REACH_PROVIDERS = JSON.stringify(reachProviders);
|
|
@@ -5565,7 +6026,8 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
5565
6026
|
}
|
|
5566
6027
|
}
|
|
5567
6028
|
const sidecar = provider.type === "docker-ssh" ? planSidecars(directory, projectName, app, provisionEnv, effective, config.container?.s3Engine, { dbPassword: "", storagePassword: "", ldapPassword: "" }) : void 0;
|
|
5568
|
-
const
|
|
6029
|
+
const https = provider.type === "docker-ssh" ? provider.expose.https : void 0;
|
|
6030
|
+
const gateway = !!artifact && !(artifact.primaryService === "serv" && artifact.hasWeb) && (sidecar?.services.includes("storage") || !!https && (artifact.primaryService === "serv" || app.isSvelteKit));
|
|
5569
6031
|
if (gateway && artifact.primaryService === "web" && !app.isSvelteKit)
|
|
5570
6032
|
return err(HaiDeployError.CONFIG_ERROR, deployM("deploy_storageBackendRequired"));
|
|
5571
6033
|
if (gateway) {
|
|
@@ -5573,18 +6035,23 @@ async function createDeploymentPlan(appDir, input, options = {}) {
|
|
|
5573
6035
|
if (!baseline.required)
|
|
5574
6036
|
selected.push("gateway");
|
|
5575
6037
|
}
|
|
6038
|
+
if (https?.type === "letsencrypt") {
|
|
6039
|
+
available.push("certbot");
|
|
6040
|
+
if (!baseline.required)
|
|
6041
|
+
selected.push("certbot");
|
|
6042
|
+
}
|
|
5576
6043
|
const services = Object.entries(sidecar?.composeServices ?? {}).filter(([name]) => selected.includes(name));
|
|
5577
6044
|
return ok({
|
|
5578
6045
|
projectName,
|
|
5579
6046
|
root,
|
|
5580
|
-
|
|
6047
|
+
serviceDir: directory,
|
|
5581
6048
|
environment: environment.data.environment,
|
|
5582
|
-
target: provider.type === "docker-ssh" ? { provider: provider.type, host: provider.ssh?.host, port: provider.ssh?.port, directory: remoteDirectory, runtime: provider.remote.runtime, hostPort: provider.expose.hostPort } : { provider: provider.type },
|
|
6049
|
+
target: provider.type === "docker-ssh" ? { provider: provider.type, host: provider.ssh?.host, port: provider.ssh?.port, directory: remoteDirectory, runtime: provider.remote.runtime, hostPort: provider.expose.hostPort, https: https ? { type: https.type, domain: https.domain, port: https.port } : void 0 } : { provider: provider.type },
|
|
5583
6050
|
artifact: artifact ? { directory: artifactDir, version: artifact.appVersion, buildId: artifact.buildId, verified: true, images: artifact.images.map((image) => ({ name: image.name, reference: image.imageRef, sha256: image.sha256 })) } : void 0,
|
|
5584
6051
|
selected,
|
|
5585
6052
|
retained: available.filter((name) => !selected.includes(name)),
|
|
5586
6053
|
containers: provider.type === "docker-ssh" ? [...selected.filter((name) => images.includes(name) || name === "gateway").map((name) => `hai-${projectName}-${name}`), ...services.flatMap(([, service]) => typeof service.container_name === "string" ? [service.container_name] : [])] : [],
|
|
5587
|
-
composeManagedServices: services.filter(([, service]) => typeof service.container_name !== "string").map(([name]) => name),
|
|
6054
|
+
composeManagedServices: [...services.filter(([, service]) => typeof service.container_name !== "string").map(([name]) => name), ...selected.includes("certbot") ? ["certbot"] : []],
|
|
5588
6055
|
logicalVolumes: Object.keys(sidecar?.volumes ?? {}),
|
|
5589
6056
|
middleware: middlewares.map((item) => ({ service: item.service, backend: item.backend, configuration: item.envPrefix, action: selected.includes(item.service) ? effective.serviceModes?.[item.envPrefix] ?? effective.serviceModes?.[item.service] ?? "default" : "keep", provisioner: provisioners.get(item.service) })),
|
|
5590
6057
|
configuration: { files: [join(root, "hai.json"), ...configFiles].filter(existsSync), environmentKeys: Object.keys(environment.data.values).sort() },
|