cf094f6e6f
Проход по операциям, которые делают не то, что обещает их имя. P0. Удаление bootstrap-admin-peer не было отзывом доступа. Признаком «создавать пир или нет» служило наличие строки в таблице, а HY2XS_ADMIN_CON_PASS продолжает жить в /etc/hy2xs/hy2xs.env — его читает systemd-юнит. Оператор удалял пира, доступ исчезал, и ближайший restart возвращал того же пира с тем же секретом. Молча. Признаком стала отметка BOOTSTRAP_PEER_SEEDED в таблице config: «создавался когда-либо», а не «существует сейчас». Отметка и пир пишутся одной транзакцией. P1. Резервная копия с includeSecrets=true проглатывала и ошибку расшифровки, и отсутствие шифртекста, отдавая пира с пустым secret и успешный ответ. Теперь недоступный секрет любого пира отклоняет весь запрос с указанием имени. P1. DecryptPeerSecret возвращала содержимое колонки как расшифрованный секрет, если оно не начиналось с v1: — остаток поколения с открытыми секретами. P1. doctor перезапускал hysteria-server и hy2xs-admin: диагностика подозрения на проблему обрывала все живые соединения. P1. Админка сама генерировала HYSTERIA2_TRAFFIC_STATS_SECRET, записать который в /etc/hysteria/config.yaml она не может. Сервис объявлял себя здоровым, а machine auth переставал совпадать. P1. Обходы проверки зависимостей (accepted-risk/skipped) не могли произвести артефакт: приёмка требует dependency_security_gate=true. Удалены из сборки и документации, отсутствие проверяется приёмкой. P2. UPDATE по отсутствующей строке config считался успехом, и cron перепланировался при несохранённом значении. Решение по RowsAffected. P2. Слой данных не отличал «записи нет» от «база не ответила»: sentinel-значения ErrPeerNotFound / ErrAdminUserNotFound / ErrConfigNotFound / ErrStorage. P2. Удалены алиасы /:id/client-url и /:id/qr. Контракт разработки: apps/go.mod объявляет toolchain go1.26.7 (директива go — языковой baseline, а не выбор компилятора), tools/dev/doctor.sh|.ps1 сверяют среду с versions.env.
315 lines
9.2 KiB
TypeScript
315 lines
9.2 KiB
TypeScript
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";
|
|
import { diagnosticsCollect } from "./commands/diagnostics";
|
|
import { redactConfig, type RedactConfigFormat } from "./commands/redact-config";
|
|
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>] [--allow-partial-state] [--skip-firewall] [--skip-service-start] [--skip-smoke]");
|
|
console.error(" hy2xs-orchestrator doctor --package-dir <path> [--config <path>] [--skip-firewall] [--skip-smoke]");
|
|
console.error(" note: doctor never restarts services — it diagnoses the running installation");
|
|
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]");
|
|
console.error(" hy2xs-orchestrator redact-config --config <path> [--in-place | --out <path>] [--format auto|env|yaml]");
|
|
console.error(" note: --skip-start is deprecated alias for --skip-service-start --skip-smoke");
|
|
process.exit(2);
|
|
}
|
|
|
|
function parseRedactConfigOptions(args: string[]): {
|
|
configPath: string;
|
|
outPath: string;
|
|
inPlace: boolean;
|
|
format: RedactConfigFormat;
|
|
} {
|
|
let configPath = "";
|
|
let outPath = "";
|
|
let inPlace = false;
|
|
let format: RedactConfigFormat = "auto";
|
|
|
|
for (let i = 0; i < args.length; i += 1) {
|
|
const arg = args[i];
|
|
switch (arg) {
|
|
case "--config":
|
|
configPath = takeValue(args, i, arg);
|
|
i += 1;
|
|
break;
|
|
case "--out":
|
|
outPath = takeValue(args, i, arg);
|
|
i += 1;
|
|
break;
|
|
case "--in-place":
|
|
inPlace = true;
|
|
break;
|
|
case "--format": {
|
|
const value = takeValue(args, i, arg);
|
|
if (value !== "auto" && value !== "env" && value !== "yaml") {
|
|
console.error(`invalid --format value: ${value}`);
|
|
usage();
|
|
}
|
|
format = value;
|
|
i += 1;
|
|
break;
|
|
}
|
|
default:
|
|
console.error(`Unknown argument: ${arg}`);
|
|
usage();
|
|
}
|
|
}
|
|
|
|
if (!configPath) {
|
|
console.error("Missing --config");
|
|
usage();
|
|
}
|
|
|
|
if (inPlace === (outPath !== "")) {
|
|
console.error("Specify exactly one of --in-place or --out <path>");
|
|
usage();
|
|
}
|
|
|
|
return { configPath, outPath, inPlace, format };
|
|
}
|
|
|
|
function takeValue(args: string[], index: number, flag: string): string {
|
|
const value = args[index + 1];
|
|
if (!value || value.startsWith("--")) {
|
|
console.error(`Missing value for ${flag}`);
|
|
usage();
|
|
}
|
|
return value;
|
|
}
|
|
|
|
function parseInstallOptions(args: string[]): InstallOptions {
|
|
const options: InstallOptions = {
|
|
packageDir: "",
|
|
sourceConfigPath: "",
|
|
runtimeConfigPath: "/etc/hy2xs/hy2xs.env",
|
|
nonInteractive: false,
|
|
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 "--non-interactive":
|
|
options.nonInteractive = true;
|
|
break;
|
|
case "--skip-firewall":
|
|
options.skipFirewall = true;
|
|
break;
|
|
case "--skip-service-start":
|
|
options.skipServiceStart = true;
|
|
break;
|
|
case "--skip-smoke":
|
|
options.skipSmoke = true;
|
|
break;
|
|
case "--skip-start":
|
|
console.error("warning: --skip-start is deprecated; use --skip-service-start and/or --skip-smoke");
|
|
options.skipServiceStart = true;
|
|
options.skipSmoke = true;
|
|
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();
|
|
}
|
|
|
|
if (options.skipSmoke && process.env.HY2XS_BREAK_GLASS !== "1") {
|
|
console.error("--skip-smoke is break-glass only. Set HY2XS_BREAK_GLASS=1 to continue.");
|
|
usage();
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
function parseReconfigureOptions(args: string[]): ReconfigureOptions {
|
|
const options: ReconfigureOptions = {
|
|
packageDir: "",
|
|
sourceConfigPath: "/etc/hy2xs/hy2xs.env",
|
|
runtimeConfigPath: "/etc/hy2xs/hy2xs.env",
|
|
nonInteractive: false,
|
|
dryRun: false,
|
|
apply: false,
|
|
allowPartialState: false,
|
|
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;
|
|
case "--allow-partial-state":
|
|
options.allowPartialState = true;
|
|
break;
|
|
case "--dry-run":
|
|
options.dryRun = true;
|
|
break;
|
|
case "--apply":
|
|
options.apply = true;
|
|
break;
|
|
case "--skip-firewall":
|
|
options.skipFirewall = true;
|
|
break;
|
|
case "--skip-service-start":
|
|
options.skipServiceStart = true;
|
|
break;
|
|
case "--skip-smoke":
|
|
options.skipSmoke = true;
|
|
break;
|
|
case "--skip-start":
|
|
console.error("warning: --skip-start is deprecated; use --skip-service-start and/or --skip-smoke");
|
|
options.skipServiceStart = true;
|
|
options.skipSmoke = true;
|
|
break;
|
|
default:
|
|
console.error(`Unknown argument: ${arg}`);
|
|
usage();
|
|
}
|
|
}
|
|
|
|
if (!options.packageDir) {
|
|
console.error("Missing --package-dir");
|
|
usage();
|
|
}
|
|
|
|
if (options.dryRun === options.apply) {
|
|
console.error("Specify exactly one of --dry-run or --apply");
|
|
usage();
|
|
}
|
|
|
|
if (options.skipSmoke && process.env.HY2XS_BREAK_GLASS !== "1") {
|
|
console.error("--skip-smoke is break-glass only. Set HY2XS_BREAK_GLASS=1 to continue.");
|
|
usage();
|
|
}
|
|
|
|
return options;
|
|
}
|
|
|
|
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") {
|
|
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") {
|
|
const options = parseReconfigureOptions(["--apply", ...args]);
|
|
await repair(options);
|
|
return;
|
|
}
|
|
if (command === "doctor") {
|
|
const options = parseReconfigureOptions(["--dry-run", ...args]);
|
|
await doctor(options);
|
|
return;
|
|
}
|
|
if (command === "status") {
|
|
await status(parseCommonOptions(args));
|
|
return;
|
|
}
|
|
if (command === "diagnostics") {
|
|
const [subcommand, ...rest] = args;
|
|
if (subcommand !== "collect") {
|
|
usage();
|
|
}
|
|
await diagnosticsCollect(parseCommonOptions(rest));
|
|
return;
|
|
}
|
|
if (command === "redact-config") {
|
|
await redactConfig(parseRedactConfigOptions(args));
|
|
return;
|
|
}
|
|
usage();
|
|
}
|
|
|
|
main().catch((error: unknown) => {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.error(`[hy2xs] ERROR: ${message}`);
|
|
process.exit(1);
|
|
});
|