diff --git a/README.md b/README.md index c461a24..3bef914 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,9 @@ HY2XS — production installer/runtime-manager для развёртывания - vanilla Hysteria2 устанавливается как pinned binary (version/url/sha256 из metadata install package); - runtime-конфиг управляется через `/etc/hy2xs/hy2xs.env` и команду `reconfigure`. -В baseline намеренно не входят target-side build, rollback/uninstall orchestration, Telegram bot delivery, port hopping и generic server-manager функции. +В baseline намеренно не входят target-side build, standalone rollback/update/uninstall subcommands, Telegram bot delivery, port hopping и generic server-manager функции. + +При этом install/reconfigure содержат bounded rollback для failure-сценариев firewall/systemd/config/smoke. ## Структура репозитория @@ -64,6 +66,8 @@ apt-get install -y sudo ca-certificates curl iproute2 tar openssl nftables syste `sudo` является обязательной зависимостью target-хоста. Он используется не для интерактивной установки, а для smoke-проверок прав доступа от имени runtime-пользователей. +`sudo` должен быть установлен до запуска `./install.sh`: preflight проверяет его до шага `installDeps`, поэтому deps-стадия не является bootstrap-слоем для `sudo`. + ```sh ./install.sh --non-interactive ``` diff --git a/docs/01-architecture-baseline.md b/docs/01-architecture-baseline.md index f886b33..d8886ce 100644 --- a/docs/01-architecture-baseline.md +++ b/docs/01-architecture-baseline.md @@ -109,7 +109,9 @@ Telegram-бот, backend выдачи ключей, remote profile publishing, b - target-side `bun install` / transpile / compile - Telegram-бот - backend выдачи remote profiles -- update / rollback / uninstall +- standalone update / rollback / uninstall subcommands + +При этом в install/reconfigure допускается bounded rollback для failure-сценариев firewall/systemd/config/smoke. - Docker как основной способ поставки - multi-node deployment - сложный control plane diff --git a/docs/08-orchestrator-spec.md b/docs/08-orchestrator-spec.md index 8b79ec0..b700c9e 100644 --- a/docs/08-orchestrator-spec.md +++ b/docs/08-orchestrator-spec.md @@ -31,7 +31,7 @@ ## Оркестратор не умеет - upgrade -- rollback +- standalone rollback subcommands - uninstall - repair старых неизвестных состояний - target-side build @@ -110,6 +110,7 @@ - Любой конфликт неизвестного старого состояния = stop with error. - Никакой сложной автомиграции. - Ошибки должны быть текстовыми и пригодными для диагностики. +- Для install/reconfigure допустим bounded rollback при failure-сценариях firewall/systemd/config/smoke. ## CLI baseline diff --git a/docs/11-testing-and-acceptance.md b/docs/11-testing-and-acceptance.md index 95d956c..e09d138 100644 --- a/docs/11-testing-and-acceptance.md +++ b/docs/11-testing-and-acceptance.md @@ -79,9 +79,10 @@ 4. UI поставлен из bundled fork 5. `post-install.env` отражает фактическое deploy-состояние 6. оркестратор зафиксирован как Bun/TypeScript stack и поставляется как готовый install-артефакт -7. оркестратор не требует update / rollback / uninstall логики -8. Telegram/access layer не требуется для прохождения install acceptance -9. отсутствует production path для port hopping -10. UI не запускается от root -11. клиентские endpoint не зависят от request `Host`/`hostname` -12. production build verify падает, если `config/hy2xs.env` содержит placeholder-значения +7. оркестратор не требует standalone update / rollback / uninstall subcommands +8. bounded rollback в install/reconfigure корректно отрабатывает failure-сценарии firewall/systemd/config/smoke +9. Telegram/access layer не требуется для прохождения install acceptance +10. отсутствует production path для port hopping +11. UI не запускается от root +12. клиентские endpoint не зависят от request `Host`/`hostname` +13. production build verify падает, если `config/hy2xs.env` содержит placeholder-значения diff --git a/docs/13-production-runbook.md b/docs/13-production-runbook.md index ac5f9d5..98536aa 100644 --- a/docs/13-production-runbook.md +++ b/docs/13-production-runbook.md @@ -15,6 +15,8 @@ apt-get install -y sudo ca-certificates curl iproute2 tar openssl nftables syste `sudo` обязателен для permission smoke-checks от имени runtime-пользователей. +`sudo` должен быть установлен до запуска `./install.sh`: preflight проверяет его до шага установки зависимостей. + ## 3. Required open ports - UDP `${HY2XS_HYSTERIA_PORT}` diff --git a/docs/README.md b/docs/README.md index 10c5144..24a49c4 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ - оркестратор: **install-only**, только первичная установка и базовая настройка - стек оркестратора: **Bun + TypeScript** - target-side build: **запрещён** -- update / rollback / uninstall: **вне scope** +- standalone update / rollback / uninstall subcommands: **вне scope** - сборка и упаковка: **отдельный локальный build layer** - post-install state: **`/etc/hysteria/post-install.env`** - клиентский delivery/access layer: **вне baseline этого пакета docs** @@ -34,8 +34,9 @@ 2. HY2XS admin форкается к себе и поставляется вместе с пакетом. 3. Оркестратор пишется на **Bun + TypeScript**. 4. На target нет `npm` / `pnpm` / `yarn` / `bun install` / transpile step. -5. На target нет логики update / rollback / uninstall. -6. Выдача доступа пользователям, Telegram-бот, billing, backend профилей и похожие контуры **не входят** в этот baseline. +5. На target нет standalone логики update / rollback / uninstall. +6. В install/reconfigure есть bounded rollback для failure-сценариев firewall/systemd/config/smoke. +7. Выдача доступа пользователям, Telegram-бот, billing, backend профилей и похожие контуры **не входят** в этот baseline. ## Состав документов diff --git a/orchestrator/src/commands/install.ts b/orchestrator/src/commands/install.ts index a31e001..e2fa799 100644 --- a/orchestrator/src/commands/install.ts +++ b/orchestrator/src/commands/install.ts @@ -33,8 +33,10 @@ async function markInstallSuccessful(context: InstallContext): Promise { await runVisible`chown root:root ${INSTALL_STATE_PATH}`; } -async function rollbackFailedInstall(context: InstallContext): Promise { - await rollbackFirewallNow(context); +async function rollbackFailedInstall(context: InstallContext, state: { firewallTouched: boolean }): Promise { + if (state.firewallTouched) { + await rollbackFirewallNow(context); + } await runVisible`systemctl stop hysteria-server hy2xs-admin || true`; await runVisible`systemctl disable hysteria-server hy2xs-admin || true`; await runVisible`systemctl reset-failed hysteria-server hy2xs-admin || true`; @@ -66,6 +68,10 @@ export async function install(options: InstallOptions): Promise { throw new Error("missing Hysteria lock metadata in package: hysteria.version/hysteria.url/hysteria.sha256"); } + const state = { + firewallTouched: false + }; + try { step("preflight"); await preflight(context); @@ -88,6 +94,7 @@ export async function install(options: InstallOptions): Promise { await deploySystemd(context); step("firewall"); await applyFirewall(context); + state.firewallTouched = true; step("post-install env"); await writePostInstallEnv(context); step("bootstrap admin secret"); @@ -99,7 +106,7 @@ export async function install(options: InstallOptions): Promise { step("mark install successful"); await markInstallSuccessful(context); } catch (error) { - await rollbackFailedInstall(context); + await rollbackFailedInstall(context, state); throw error; } } diff --git a/orchestrator/src/lib/process.ts b/orchestrator/src/lib/process.ts index 71d81f8..43b69af 100644 --- a/orchestrator/src/lib/process.ts +++ b/orchestrator/src/lib/process.ts @@ -49,6 +49,18 @@ export async function runVisible(command: TemplateStringsArray, ...args: unknown } } +export async function runRawVisible(command: string): Promise { + info(`running script:\n${command}`); + const process = Bun.spawn(["sh", "-eu", "-c", command], { + stdout: "inherit", + stderr: "inherit" + }); + const exitCode = await process.exited; + if (exitCode !== 0) { + throw new Error(`script failed (${exitCode})`); + } +} + export async function runHidden(command: TemplateStringsArray, ...args: unknown[]): Promise { const rendered = renderCommand(command, args); const process = Bun.spawn(["sh", "-c", rendered], { diff --git a/orchestrator/src/steps/filesystem.ts b/orchestrator/src/steps/filesystem.ts index 4a19893..5a8bb7e 100644 --- a/orchestrator/src/steps/filesystem.ts +++ b/orchestrator/src/steps/filesystem.ts @@ -1,5 +1,5 @@ import type { InstallContext } from "../types/context"; -import { runVisible } from "../lib/process"; +import { runRawVisible, runVisible } from "../lib/process"; async function ensureRuntimeIdentity(user: string, expectedHome: string): Promise { const checkCmd = ` @@ -23,7 +23,7 @@ else useradd --system --home ${expectedHome} --shell /usr/sbin/nologin ${user} fi `; - await runVisible`${checkCmd}`; + await runRawVisible(checkCmd); } export async function prepareFilesystem(context: InstallContext): Promise { diff --git a/orchestrator/src/steps/firewall.ts b/orchestrator/src/steps/firewall.ts index 17d2994..1c70c76 100644 --- a/orchestrator/src/steps/firewall.ts +++ b/orchestrator/src/steps/firewall.ts @@ -124,6 +124,11 @@ export async function rollbackFirewallNow(context: RuntimeContext): Promise/dev/null || true; else rm -f /etc/nftables.conf; fi`; await runVisible`if [ -f /etc/nftables.d/hy2xs.nft.existed ]; then cp -a /etc/nftables.d/hy2xs.nft.bak /etc/nftables.d/hy2xs.nft 2>/dev/null || true; else rm -f /etc/nftables.d/hy2xs.nft; fi`; - await runVisible`nft -f /etc/nftables.conf >/dev/null 2>&1 || true`; + await runVisible`if [ -f /etc/nftables.d/nftables.conf.existed ]; then nft -f /etc/nftables.conf >/dev/null 2>&1 || true; else nft flush ruleset >/dev/null 2>&1 || true; fi`; await runVisible`rm -f ${FW_BACKUP_FILES}`; }