Files
HY2XS_flamy/orchestrator/src/commands/install.ts
T

113 lines
4.5 KiB
TypeScript

import type { InstallContext, InstallOptions } from "../types/context";
import { exists, readText, writeText } from "../lib/fs";
import { runVisible } from "../lib/process";
import { step } from "../lib/log";
import { readPackageValue } from "../lib/packageMeta";
import { parseRuntimeEnv, renderRuntimeEnv } from "../config/env";
import { preflight } from "../steps/preflight";
import { installDeps } from "../steps/deps";
import { prepareFilesystem } from "../steps/filesystem";
import { deployUi } from "../steps/ui";
import { installHysteria } from "../steps/hysteria";
import { generateConfig } from "../steps/config";
import { deploySystemd } from "../steps/systemd";
import { applyFirewall, cancelFirewallRollback, rollbackFirewallNow } from "../steps/firewall";
import { writeBootstrapAdminSecret, writePostInstallEnv } from "../steps/env";
import { smoke } from "../steps/smoke";
const INSTALL_STATE_PATH = "/var/lib/hy2xs/install-state.json";
async function markInstallSuccessful(context: InstallContext): Promise<void> {
await runVisible`install -d -m 0755 -o root -g root /var/lib/hy2xs`;
const state = JSON.stringify(
{
installed: true,
version: context.packageVersion,
build_id: context.packageBuildId,
installed_at: new Date().toISOString()
},
null,
2
);
await writeText(INSTALL_STATE_PATH, `${state}\n`, 0o644);
await runVisible`chown root:root ${INSTALL_STATE_PATH}`;
}
async function rollbackFailedInstall(context: InstallContext, state: { firewallTouched: boolean }): Promise<void> {
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`;
}
export async function install(options: InstallOptions): Promise<void> {
const hasSourceConfig = options.sourceConfigPath ? await exists(options.sourceConfigPath) : false;
if (options.sourceConfigPath && !hasSourceConfig) {
throw new Error(`config source not found: ${options.sourceConfigPath}`);
}
const sourceConfigPath = hasSourceConfig ? options.sourceConfigPath : `${options.packageDir}/config/hy2xs.env`;
const sourceConfigRaw = await readText(sourceConfigPath);
const config = parseRuntimeEnv(sourceConfigRaw);
const context: InstallContext = {
mode: "install",
options,
config,
packageVersion: await readPackageValue(options.packageDir, "package.version", "unknown"),
packageBuildId: await readPackageValue(options.packageDir, "package.build_id", "unknown"),
installDate: new Date().toISOString(),
hysteriaVersion: "unknown",
hysteriaTargetVersion: await readPackageValue(options.packageDir, "hysteria.version", ""),
hysteriaArtifactUrl: await readPackageValue(options.packageDir, "hysteria.url", ""),
hysteriaArtifactSha256: await readPackageValue(options.packageDir, "hysteria.sha256", "")
};
if (!context.hysteriaTargetVersion || !context.hysteriaArtifactUrl || !context.hysteriaArtifactSha256) {
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);
step("system dependencies");
await installDeps(context);
step("filesystem");
await prepareFilesystem(context);
step("write runtime env");
await runVisible`mkdir -p /etc/hy2xs`;
await writeText(options.runtimeConfigPath, renderRuntimeEnv(config), 0o600);
await runVisible`chown root:root ${options.runtimeConfigPath}`;
await runVisible`chmod 0600 ${options.runtimeConfigPath}`;
step("bundled UI");
await deployUi(context);
step("Hysteria2 upstream install");
await installHysteria(context);
step("config generation");
await generateConfig(context);
step("systemd units");
await deploySystemd(context);
step("firewall");
await applyFirewall(context);
state.firewallTouched = true;
step("post-install env");
await writePostInstallEnv(context);
step("bootstrap admin secret");
await writeBootstrapAdminSecret(context);
step("smoke checks");
await smoke(context);
step("finalize firewall rollback guard");
await cancelFirewallRollback(context);
step("mark install successful");
await markInstallSuccessful(context);
} catch (error) {
await rollbackFailedInstall(context, state);
throw error;
}
}