fix(install): двухфазная установка, clean-host контракт и проверка поколения
Установщик мог повредить работающий сервер до того, как откажется его
трогать: install.sh переписывал /usr/local/lib/hy2xs, раскладывал
runtime-пакет и перезаписывал install-state.json, и только потом
запускал clean-host preflight. При ошибочном запуске поверх старой
установки rollback дополнительно делал stop и disable для работающих
hysteria-server и hy2xs-admin.
Установка разделена на две фазы с жёсткой границей:
PHASE 0 - read only: права, checksums пакета, clean-host preflight
из распакованного архива (новая команда preflight-install)
PHASE 1 - mutation: раскладка оркестратора и сама установка
Граница держится не соглашением, а read-only guard: под ним writeText,
writeTextAtomic и мутирующие раннеры lib/process кидают ошибку.
Внутри install() preflight выполняется раньше первой записи состояния.
Остальное в этом же инварианте:
- clean-host контракт расширен с двух маркеров до четырнадцати, пути
установки и данных берутся из конфигурации, а не захардкожены;
- отсутствие HY2XS_CONFIG_SCHEMA_VERSION трактуется как legacy, а не
как текущая схема: до v1 этого поля не существовало. Тест,
закреплявший прежнее поведение, инвертирован;
- install-state несёт идентификацию поколения (product, release_line,
config_schema_version); reconfigure и repair проверяют её до всего
остального, потому что installed: true мог остаться и от 0.x;
- repair требует явного --allow-partial-state;
- классификация отказа опирается на ownership-флаги, а не на текст
ошибки: раньше сообщение со словом nftables приводило к откату
чужого firewall. stop/disable выполняется только для юнитов,
развёрнутых текущей операцией, а fatal_pre_apply не делает
системного отката и не собирает diagnostics-бандл.
This commit is contained in:
+57
-2
@@ -1,4 +1,5 @@
|
||||
import { install } from "./commands/install";
|
||||
import { preflightInstall } from "./commands/preflight-install";
|
||||
import { reconfigure, repair } from "./commands/reconfigure";
|
||||
import { doctor } from "./commands/doctor";
|
||||
import { status } from "./commands/status";
|
||||
@@ -8,9 +9,10 @@ import type { InstallOptions, ReconfigureOptions } from "./types/context";
|
||||
|
||||
function usage(): never {
|
||||
console.error("Usage:");
|
||||
console.error(" hy2xs-orchestrator preflight-install --package-dir <path> [--config <path>]");
|
||||
console.error(" hy2xs-orchestrator install --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke] [--non-interactive]");
|
||||
console.error(" hy2xs-orchestrator reconfigure --package-dir <path> [--config <path>] [--dry-run|--apply] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||
console.error(" hy2xs-orchestrator repair --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||
console.error(" hy2xs-orchestrator repair --package-dir <path> [--config <path>] [--allow-partial-state] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||
console.error(" hy2xs-orchestrator doctor --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||
console.error(" hy2xs-orchestrator status --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||
console.error(" hy2xs-orchestrator diagnostics collect --package-dir <path> [--config <path>] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
||||
@@ -148,6 +150,7 @@ function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
||||
nonInteractive: false,
|
||||
dryRun: false,
|
||||
apply: false,
|
||||
allowPartialState: false,
|
||||
skipFirewall: false,
|
||||
skipServiceStart: false,
|
||||
skipSmoke: false
|
||||
@@ -164,6 +167,9 @@ function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
||||
options.sourceConfigPath = takeValue(args, i, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--allow-partial-state":
|
||||
options.allowPartialState = true;
|
||||
break;
|
||||
case "--dry-run":
|
||||
options.dryRun = true;
|
||||
break;
|
||||
@@ -212,14 +218,63 @@ function parseCommonOptions(args: string[]): InstallOptions {
|
||||
return parseInstallOptions(args);
|
||||
}
|
||||
|
||||
/**
|
||||
* PHASE 0 не принимает ничего, что могло бы повлиять на мутацию: только
|
||||
* расположение пакета и источник конфигурации.
|
||||
*/
|
||||
function parsePreflightInstallOptions(args: string[]): InstallOptions {
|
||||
const options: InstallOptions = {
|
||||
packageDir: "",
|
||||
sourceConfigPath: "",
|
||||
runtimeConfigPath: "/etc/hy2xs/hy2xs.env",
|
||||
nonInteractive: true,
|
||||
skipFirewall: false,
|
||||
skipServiceStart: false,
|
||||
skipSmoke: false
|
||||
};
|
||||
|
||||
for (let i = 0; i < args.length; i += 1) {
|
||||
const arg = args[i];
|
||||
switch (arg) {
|
||||
case "--package-dir":
|
||||
options.packageDir = takeValue(args, i, arg);
|
||||
i += 1;
|
||||
break;
|
||||
case "--config":
|
||||
options.sourceConfigPath = takeValue(args, i, arg);
|
||||
i += 1;
|
||||
break;
|
||||
default:
|
||||
console.error(`Unknown argument: ${arg}`);
|
||||
usage();
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.packageDir) {
|
||||
console.error("Missing --package-dir");
|
||||
usage();
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const [command, ...args] = Bun.argv.slice(2);
|
||||
if (command === "preflight-install") {
|
||||
await preflightInstall(parsePreflightInstallOptions(args));
|
||||
return;
|
||||
}
|
||||
if (command === "install") {
|
||||
await install(parseInstallOptions(args));
|
||||
return;
|
||||
}
|
||||
if (command === "reconfigure") {
|
||||
await reconfigure(parseReconfigureOptions(args));
|
||||
const options = parseReconfigureOptions(args);
|
||||
if (options.allowPartialState) {
|
||||
console.error("--allow-partial-state is only valid for `repair`");
|
||||
usage();
|
||||
}
|
||||
await reconfigure(options);
|
||||
return;
|
||||
}
|
||||
if (command === "repair") {
|
||||
|
||||
Reference in New Issue
Block a user