fix: продакшен-фиксы install-flow и шаблонов

- исправлен рендер post-install.env: прокинут HYSTERIA_API_SECRET

- добавлен fail-fast при неразрешенных {{...}} в renderTemplate

- HY2XS_FORCE_PASSWORD_CHANGE приведён к production default=false

- docs синхронизированы: sudo bootstrap на deps-стадии

- builder: VERIFY_TOOLCHAIN_CHECKSUMS=true по умолчанию

- acceptance расширен новыми инвариантами
This commit is contained in:
2026-05-09 03:03:46 +05:00
parent 78da898ba5
commit 8de1719aa4
10 changed files with 29 additions and 8 deletions
+2 -2
View File
@@ -44,9 +44,9 @@ apt-get update
apt-get install -y sudo ca-certificates curl iproute2 tar openssl nftables systemd apt-get install -y sudo ca-certificates curl iproute2 tar openssl nftables systemd
``` ```
`sudo` является обязательной зависимостью target-хоста. Он используется не для интерактивной установки, а для smoke-проверок прав доступа от имени runtime-пользователей. `sudo` используется для smoke-проверок прав доступа от имени runtime-пользователей и автоматически устанавливается на шаге deps в install-flow.
`sudo` должен быть установлен до запуска `./install.sh`: preflight проверяет его до шага `installDeps`, поэтому deps-стадия не является bootstrap-слоем для `sudo`. Предварительная ручная установка `sudo` перед `./install.sh` не требуется: preflight допускает его отсутствие на clean-host и install deps bootstrap'ит его до smoke-проверок.
```sh ```sh
./install.sh --non-interactive ./install.sh --non-interactive
+2 -2
View File
@@ -8,14 +8,14 @@
### 0. Target prerequisites обязательны ### 0. Target prerequisites обязательны
На target-хосте до запуска install должны быть доступны системные зависимости: На target-хосте install-flow сам обеспечивает системные зависимости (deps stage):
```bash ```bash
apt-get update apt-get update
apt-get install -y sudo ca-certificates curl iproute2 tar openssl nftables systemd apt-get install -y sudo ca-certificates curl iproute2 tar openssl nftables systemd
``` ```
`sudo` обязателен: используется smoke-проверками прав от имени runtime-пользователей (`hysteria`, `hy2xs-admin`). `sudo` обязателен для smoke-проверок прав от имени runtime-пользователей (`hysteria`, `hy2xs-admin`), но его не нужно ставить вручную заранее: на clean-host он устанавливается на deps-стадии до smoke.
### 1. Builder и target — разные миры ### 1. Builder и target — разные миры
Если нужно изменить состав install package, это делается в локальном builder layer, а не на target server. Если нужно изменить состав install package, это делается в локальном builder layer, а не на target server.
+1 -1
View File
@@ -15,7 +15,7 @@ apt-get install -y sudo ca-certificates curl iproute2 tar openssl nftables syste
`sudo` обязателен для permission smoke-checks от имени runtime-пользователей. `sudo` обязателен для permission smoke-checks от имени runtime-пользователей.
`sudo` должен быть установлен до запуска `./install.sh`: preflight проверяет его до шага установки зависимостей. Предварительная ручная установка `sudo` до запуска `./install.sh` не требуется: на clean-host install-flow ставит его на стадии deps до выполнения smoke-checks.
## 3. Required open ports ## 3. Required open ports
+1 -1
View File
@@ -208,7 +208,7 @@ export function parseRuntimeEnv(content: string): RuntimeConfig {
adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"), adminUser: requireValue("HY2XS_ADMIN_USER", env.HY2XS_ADMIN_USER || "admin"),
adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD), adminInitialPassword: valueOrGenerate(env.HY2XS_ADMIN_INITIAL_PASSWORD),
adminConPass: requireValue("HY2XS_ADMIN_CON_PASS", valueOrGenerate(env.HY2XS_ADMIN_CON_PASS)), adminConPass: requireValue("HY2XS_ADMIN_CON_PASS", valueOrGenerate(env.HY2XS_ADMIN_CON_PASS)),
forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, true), forcePasswordChange: parseBool("HY2XS_FORCE_PASSWORD_CHANGE", env.HY2XS_FORCE_PASSWORD_CHANGE, false),
allowSelfSignedDev: parseBool("HY2XS_ALLOW_SELF_SIGNED_DEV", env.HY2XS_ALLOW_SELF_SIGNED_DEV, false), allowSelfSignedDev: parseBool("HY2XS_ALLOW_SELF_SIGNED_DEV", env.HY2XS_ALLOW_SELF_SIGNED_DEV, false),
tlsMode, tlsMode,
acmeType, acmeType,
+7
View File
@@ -87,5 +87,12 @@ export function renderTemplate(template: string, values: Record<string, string |
for (const [key, value] of Object.entries(values)) { for (const [key, value] of Object.entries(values)) {
rendered = rendered.replaceAll(`{{${key}}}`, String(value)); rendered = rendered.replaceAll(`{{${key}}}`, String(value));
} }
const unresolved = rendered.match(/\{\{\s*[A-Z0-9_]+\s*\}\}/g) ?? [];
if (unresolved.length > 0) {
const unique = [...new Set(unresolved.map((item) => item.replace(/\s+/g, "")))].sort();
throw new Error(`template render failed: unresolved placeholders: ${unique.join(", ")}`);
}
return rendered; return rendered;
} }
+1
View File
@@ -26,6 +26,7 @@ export async function writePostInstallEnv(context: RuntimeContext): Promise<void
IGNORE_CLIENT_BANDWIDTH: context.config.hysteriaIgnoreClientBandwidth ? "true" : "false", IGNORE_CLIENT_BANDWIDTH: context.config.hysteriaIgnoreClientBandwidth ? "true" : "false",
HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost, HYSTERIA_API_HOST: context.config.hysteriaTrafficStatsHost,
HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort, HYSTERIA_API_PORT: context.config.hysteriaTrafficStatsPort,
HYSTERIA_API_SECRET: context.config.hysteriaTrafficStatsSecret,
UI_BIND_HOST: context.config.uiBindHost, UI_BIND_HOST: context.config.uiBindHost,
UI_PORT: context.config.uiPort, UI_PORT: context.config.uiPort,
INSTALL_DIR: context.config.installDir, INSTALL_DIR: context.config.installDir,
+1 -1
View File
@@ -13,7 +13,7 @@ HY2XS_UI_PORT=8080
HY2XS_ADMIN_USER=hy2xsadmin HY2XS_ADMIN_USER=hy2xsadmin
HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__ HY2XS_ADMIN_INITIAL_PASSWORD=__GENERATE__
HY2XS_ADMIN_CON_PASS=__GENERATE__ HY2XS_ADMIN_CON_PASS=__GENERATE__
HY2XS_FORCE_PASSWORD_CHANGE=true HY2XS_FORCE_PASSWORD_CHANGE=false
HY2XS_ALLOW_SELF_SIGNED_DEV=false HY2XS_ALLOW_SELF_SIGNED_DEV=false
HY2XS_TLS_MODE=acme HY2XS_TLS_MODE=acme
HY2XS_ACME_TYPE=http HY2XS_ACME_TYPE=http
+6
View File
@@ -180,6 +180,12 @@ If frozen install fails, update dependencies intentionally in source control and
- `TOOLCHAIN_DIR=/custom/path/.toolchain` - `TOOLCHAIN_DIR=/custom/path/.toolchain`
- `VERIFY_TOOLCHAIN_CHECKSUMS=true` - `VERIFY_TOOLCHAIN_CHECKSUMS=true`
По умолчанию `VERIFY_TOOLCHAIN_CHECKSUMS=true` в [`tools/build/lib/deps.sh`](tools/build/lib/deps.sh), поэтому для production-сборки обязательно передавать контрольные суммы:
- `GO_ARCHIVE_SHA256=<sha256>`
- `NODE_ARCHIVE_SHA256=<sha256>`
- `BUN_ARCHIVE_SHA256=<sha256>`
## Frontend memory policy ## Frontend memory policy
Builder applies a safe default Node.js heap limit for frontend build inside [`bundle_ui()`](lib/package.sh): Builder applies a safe default Node.js heap limit for frontend build inside [`bundle_ui()`](lib/package.sh):
+7
View File
@@ -15,6 +15,9 @@ run_fix20_acceptance_subset() {
log_step "Acceptance: firewall mode defaults in config" log_step "Acceptance: firewall mode defaults in config"
grep -q '^HY2XS_FIREWALL_MODE=' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_FIREWALL_MODE missing in runtime config" grep -q '^HY2XS_FIREWALL_MODE=' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_FIREWALL_MODE missing in runtime config"
log_step "Acceptance: force password change production default"
grep -q '^HY2XS_FORCE_PASSWORD_CHANGE=false$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_FORCE_PASSWORD_CHANGE must default to false"
log_step "Acceptance: systemd unit production env" log_step "Acceptance: systemd unit production env"
grep -q '^Environment=GIN_MODE=release$' "$package_dir/systemd/hy2xs-admin.service" || fail "acceptance: GIN_MODE=release missing" grep -q '^Environment=GIN_MODE=release$' "$package_dir/systemd/hy2xs-admin.service" || fail "acceptance: GIN_MODE=release missing"
@@ -28,4 +31,8 @@ run_fix20_acceptance_subset() {
log_step "Acceptance: smoke auth checks are tokenized" log_step "Acceptance: smoke auth checks are tokenized"
grep -q 'unexpected auth status without machine token' orchestrator/src/steps/smoke.ts || fail "acceptance: missing 403 negative smoke for auth without machine token" grep -q 'unexpected auth status without machine token' orchestrator/src/steps/smoke.ts || fail "acceptance: missing 403 negative smoke for auth without machine token"
grep -q 'hysteria2/auth?access_token=\${context.config.hysteriaTrafficStatsSecret}' orchestrator/src/steps/smoke.ts || fail "acceptance: smoke auth URL is not tokenized" grep -q 'hysteria2/auth?access_token=\${context.config.hysteriaTrafficStatsSecret}' orchestrator/src/steps/smoke.ts || fail "acceptance: smoke auth URL is not tokenized"
log_step "Acceptance: env rendering maps machine token and fails on unresolved placeholders"
grep -q 'HYSTERIA_API_SECRET: context.config.hysteriaTrafficStatsSecret' orchestrator/src/steps/env.ts || fail "acceptance: writePostInstallEnv must pass HYSTERIA_API_SECRET"
grep -q 'template render failed: unresolved placeholders' orchestrator/src/lib/fs.ts || fail "acceptance: renderTemplate must fail on unresolved placeholders"
} }
+1 -1
View File
@@ -6,7 +6,7 @@ BUN_REQUIRED="${BUN_REQUIRED:-1.3.13}"
NODE_REQUIRED="${NODE_REQUIRED:-20.19.0}" NODE_REQUIRED="${NODE_REQUIRED:-20.19.0}"
PNPM_REQUIRED="${PNPM_REQUIRED:-9.15.9}" PNPM_REQUIRED="${PNPM_REQUIRED:-9.15.9}"
TOOLCHAIN_DIR="${TOOLCHAIN_DIR:-$ROOT_DIR/.toolchain}" TOOLCHAIN_DIR="${TOOLCHAIN_DIR:-$ROOT_DIR/.toolchain}"
VERIFY_TOOLCHAIN_CHECKSUMS="${VERIFY_TOOLCHAIN_CHECKSUMS:-false}" VERIFY_TOOLCHAIN_CHECKSUMS="${VERIFY_TOOLCHAIN_CHECKSUMS:-true}"
GO_ARCHIVE_SHA256="${GO_ARCHIVE_SHA256:-}" GO_ARCHIVE_SHA256="${GO_ARCHIVE_SHA256:-}"
NODE_ARCHIVE_SHA256="${NODE_ARCHIVE_SHA256:-}" NODE_ARCHIVE_SHA256="${NODE_ARCHIVE_SHA256:-}"
BUN_ARCHIVE_SHA256="${BUN_ARCHIVE_SHA256:-}" BUN_ARCHIVE_SHA256="${BUN_ARCHIVE_SHA256:-}"