fix12: разделение env-артефактов и стабилизация smoke

This commit is contained in:
2026-04-30 23:54:15 +05:00
parent a41f14067b
commit b228928e99
6 changed files with 42 additions and 29 deletions
-4
View File
@@ -12,10 +12,6 @@ func initHysteria2MachineAuthRouter(hysteria2Api *gin.RouterGroup) {
} }
} }
func initHysteria2SubscribeRouter(hysteria2Api *gin.RouterGroup) {
hysteria2Api.GET("/:conPass", controller.Hysteria2Subscribe)
}
func initHysteria2Router(hysteria2Api *gin.RouterGroup) { func initHysteria2Router(hysteria2Api *gin.RouterGroup) {
hysteria2 := hysteria2Api.Group("/hysteria2") hysteria2 := hysteria2Api.Group("/hysteria2")
{ {
+1 -1
View File
@@ -48,7 +48,7 @@
13. `trafficStats` API: корректный secret принимает запрос, неверный secret отклоняется 13. `trafficStats` API: корректный secret принимает запрос, неверный secret отклоняется
14. TLS mode в `config.yaml` соответствует runtime env (`acme|file|self_signed_dev`) 14. TLS mode в `config.yaml` соответствует runtime env (`acme|file|self_signed_dev`)
15. при `HY2XS_TLS_MODE=acme` в `config.yaml` выставлен `acme.type` из `HY2XS_ACME_TYPE` 15. при `HY2XS_TLS_MODE=acme` в `config.yaml` выставлен `acme.type` из `HY2XS_ACME_TYPE`
16. subscribe/node URL в API/QR формируются по `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT` 16. direct `hysteria2://` node URL в API/QR формируется по `HY2XS_PUBLIC_HOST` + `HY2XS_PUBLIC_PORT`; subscription delivery endpoint отключён в baseline и не входит в acceptance
17. `nft -c -f /etc/nftables.conf` проходит после apply 17. `nft -c -f /etc/nftables.conf` проходит после apply
18. пароль admin и `con_pass` не перезаписываются при рестарте `hy2xs-admin` 18. пароль admin и `con_pass` не перезаписываются при рестарте `hy2xs-admin`
19. остановка/рестарт UI не останавливает `hysteria-server` 19. остановка/рестарт UI не останавливает `hysteria-server`
+3 -1
View File
@@ -12,7 +12,7 @@ import { installHysteria } from "../steps/hysteria";
import { generateConfig } from "../steps/config"; import { generateConfig } from "../steps/config";
import { deploySystemd } from "../steps/systemd"; import { deploySystemd } from "../steps/systemd";
import { applyFirewall } from "../steps/firewall"; import { applyFirewall } from "../steps/firewall";
import { writePostInstallEnv } from "../steps/env"; import { writeBootstrapAdminSecret, writePostInstallEnv } from "../steps/env";
import { smoke } from "../steps/smoke"; import { smoke } from "../steps/smoke";
export async function install(options: InstallOptions): Promise<void> { export async function install(options: InstallOptions): Promise<void> {
@@ -64,6 +64,8 @@ export async function install(options: InstallOptions): Promise<void> {
await applyFirewall(context); await applyFirewall(context);
step("post-install env"); step("post-install env");
await writePostInstallEnv(context); await writePostInstallEnv(context);
step("bootstrap admin secret");
await writeBootstrapAdminSecret(context);
step("smoke checks"); step("smoke checks");
await smoke(context); await smoke(context);
} }
+3
View File
@@ -33,6 +33,9 @@ export async function writePostInstallEnv(context: RuntimeContext): Promise<void
await writeText("/etc/hysteria/post-install.env", rendered, 0o600); await writeText("/etc/hysteria/post-install.env", rendered, 0o600);
await runVisible`chown root:root /etc/hysteria/post-install.env`; await runVisible`chown root:root /etc/hysteria/post-install.env`;
await runVisible`chmod 0600 /etc/hysteria/post-install.env`; await runVisible`chmod 0600 /etc/hysteria/post-install.env`;
}
export async function writeBootstrapAdminSecret(context: RuntimeContext): Promise<void> {
await writeText( await writeText(
context.config.bootstrapAdminSecretPath, context.config.bootstrapAdminSecretPath,
[ [
+18 -6
View File
@@ -3,24 +3,31 @@ import { info } from "../lib/log";
import { runHidden, runSecret, runVisible } from "../lib/process"; import { runHidden, runSecret, runVisible } from "../lib/process";
async function retry<T>( async function retry<T>(
label: string,
attempts: number, attempts: number,
delayMs: number, delayMs: number,
action: () => Promise<T>, action: () => Promise<T>,
validate: (value: T) => boolean, validate: (value: T) => boolean,
errorFactory: (value: T) => Error, errorFactory: (value: T | undefined, error: unknown) => Error,
): Promise<T> { ): Promise<T> {
let lastValue: T | undefined; let lastValue: T | undefined;
let lastError: unknown;
for (let i = 0; i < attempts; i += 1) { for (let i = 0; i < attempts; i += 1) {
try {
const value = await action(); const value = await action();
lastValue = value; lastValue = value;
if (validate(value)) { if (validate(value)) {
return value; return value;
} }
} catch (error) {
lastError = error;
}
if (i < attempts - 1) { if (i < attempts - 1) {
info(`${label}: retry ${i + 1}/${attempts}`);
await runHidden`sleep ${Math.max(1, Math.ceil(delayMs / 1000))}`; await runHidden`sleep ${Math.max(1, Math.ceil(delayMs / 1000))}`;
} }
} }
throw errorFactory(lastValue as T); throw errorFactory(lastValue, lastError);
} }
export async function smoke(context: RuntimeContext): Promise<void> { export async function smoke(context: RuntimeContext): Promise<void> {
@@ -62,11 +69,12 @@ export async function smoke(context: RuntimeContext): Promise<void> {
await runVisible`! ss -H -ltn | grep -q '\[::\]:${context.config.uiPort} '`; await runVisible`! ss -H -ltn | grep -q '\[::\]:${context.config.uiPort} '`;
await runVisible`! ss -H -lun | grep -q '\[::\]:${context.config.hysteriaPort} '`; await runVisible`! ss -H -lun | grep -q '\[::\]:${context.config.hysteriaPort} '`;
const invalidAuthResponse = await retry( const invalidAuthResponse = await retry(
"auth invalid credentials",
5, 5,
1000, 1000,
async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`, async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"invalid","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`,
(response) => /"ok"\s*:\s*false/.test(response), (response) => /"ok"\s*:\s*false/.test(response),
(response) => new Error(`unexpected auth response for invalid credentials: ${response}`), (response, error) => new Error(`unexpected auth response for invalid credentials: ${response ?? String(error)}`),
); );
for (let i = 0; i < 10; i += 1) { for (let i = 0; i < 10; i += 1) {
@@ -76,25 +84,29 @@ export async function smoke(context: RuntimeContext): Promise<void> {
} }
} }
if (context.mode === "install") {
const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim(); const adminConPass = (await runSecret`grep '^ADMIN_CON_PASS=' ${context.config.bootstrapAdminSecretPath} | head -n1 | cut -d= -f2-`).trim();
if (!adminConPass) { if (!adminConPass) {
throw new Error("admin connection password is empty in bootstrap secret file"); throw new Error("admin connection password is empty in bootstrap secret file");
} }
const validAuthResponse = await retry( await retry(
"auth valid credentials",
10, 10,
1000, 1000,
async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${adminConPass}","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`, async () => runSecret`curl -sS --max-time 5 -X POST -H 'Content-Type: application/json' --data '{"addr":"127.0.0.1:12345","auth":"${adminConPass}","tx":"0"}' http://127.0.0.1:${context.config.uiPort}/hui/hysteria2/auth`,
(response) => /"ok"\s*:\s*true/.test(response), (response) => /"ok"\s*:\s*true/.test(response),
() => new Error("unexpected auth response for valid credentials"), (response, error) => new Error(`unexpected auth response for valid credentials: ${response ?? String(error)}`),
); );
}
await retry( await retry(
"trafficStats valid secret",
10, 10,
1000, 1000,
async () => runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`, async () => runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: ${context.config.hysteriaTrafficStatsSecret}' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`,
(code) => /^2\d\d$/.test(code.trim()), (code) => /^2\d\d$/.test(code.trim()),
(code) => new Error(`unexpected trafficStats status for valid secret: ${code}`), (code, error) => new Error(`unexpected trafficStats status for valid secret: ${code ?? String(error)}`),
); );
const deniedCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`; const deniedCode = await runSecret`curl -sS --max-time 5 -o /dev/null -w '%{http_code}' -H 'Authorization: invalid-hy2xs-secret' http://127.0.0.1:${context.config.hysteriaTrafficStatsPort}/online`;
if (!/(401|403)/.test(deniedCode)) { if (!/(401|403)/.test(deniedCode)) {