build: закрепить новые инварианты приёмкой и документацией
verify_versions_contract получил сверку API namespace. Путь machine-auth
записывается в /etc/hysteria/config.yaml и в post-install.env, то есть по нему
Hysteria обращается к админке. Пока строка была продублирована в шаблонах,
smoke, тестах, приёмке и e2e, расхождение обнаруживалось только на живом
сервере. Теперь Go-константы, API_BASE фронтенда и оба шаблона сверяются
против значений, скомпилированных в оркестратор.
Приёмка проверяет, что:
- fatal_pre_apply недостижим после записи install-state;
- каждый ownership-флаг взводится раньше своего шага;
- у read-only фазы нет универсального раннера, через который можно
проскользнуть;
- инвариант публичного endpoint живёт в preflight и не обращается к внешним
сервисам определения IP;
- purge-v0.sh и clean-host описывают одну границу;
- секреты не попадают в персистентный файл экспорта;
- импорт пиров валидируется так же строго, как их создание;
- удалённые exportConfig/importConfig не вернулись.
Захардкоженная схема =2 в приёмке заменена на значение из versions.env: при
переходе на schema 3 пришлось бы помнить ещё и про эту строку.
Документация: контракт раннеров и ownership в 08, инвариант публичного
endpoint в 08/09/12/13 и README, сетевая идентичность панели и удалённые
export/import в 04, сценарии D1 (отказ сразу после PHASE 0) и D2 (устаревший
DNS после смены IPv4) в 11, версии package.json как не-версия продукта в 02.
This commit is contained in:
@@ -31,8 +31,15 @@ run_fix20_acceptance_subset() {
|
||||
grep -q '^HY2XS_FORCE_PASSWORD_CHANGE=false$' "$package_dir/config/hy2xs.env" || fail "acceptance: HY2XS_FORCE_PASSWORD_CHANGE must default to false"
|
||||
|
||||
log_step "Acceptance: config schema version is declared"
|
||||
grep -q '^HY2XS_CONFIG_SCHEMA_VERSION=2$' "$package_dir/config/hy2xs.env" \
|
||||
|| fail "acceptance: HY2XS_CONFIG_SCHEMA_VERSION must be 2 in the packaged baseline"
|
||||
# Значение берётся из versions.env, а не пишется числом: захардкоженная
|
||||
# двойка означала бы, что при переходе на schema 3 нужно помнить ещё и про
|
||||
# эту строку. Источник истины у схемы ровно один.
|
||||
grep -q "^HY2XS_CONFIG_SCHEMA_VERSION=${HY2XS_CONFIG_SCHEMA_VERSION}\$" "$package_dir/config/hy2xs.env" \
|
||||
|| fail "acceptance: packaged baseline must declare HY2XS_CONFIG_SCHEMA_VERSION=${HY2XS_CONFIG_SCHEMA_VERSION}"
|
||||
|
||||
log_step "Acceptance: public endpoint policy is declared and strict by default"
|
||||
grep -q '^HY2XS_PUBLIC_ENDPOINT_POLICY=strict$' "$package_dir/config/hy2xs.env" \
|
||||
|| fail "acceptance: packaged baseline must default to HY2XS_PUBLIC_ENDPOINT_POLICY=strict"
|
||||
|
||||
log_step "Acceptance: fresh install defaults to Gecko obfuscation"
|
||||
grep -q '^HY2XS_HYSTERIA_OBFS_TYPE=gecko$' "$package_dir/config/hy2xs.env" \
|
||||
@@ -118,8 +125,8 @@ run_fix20_acceptance_subset() {
|
||||
grep -q 'Fix20 production matrix' docs/11-testing-and-acceptance.md || fail "acceptance: fix20 matrix section missing"
|
||||
|
||||
log_step "Acceptance: machine auth URL in templates"
|
||||
grep -q '/hui/hysteria2/auth?access_token={{HYSTERIA_API_SECRET}}' "$package_dir/templates/hysteria/config.yaml.tpl" || fail "acceptance: machine token missing in hysteria auth URL template"
|
||||
grep -q '^HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/hui/hysteria2/auth?access_token={{HYSTERIA_API_SECRET}}$' "$package_dir/templates/env/post-install.env.tpl" || fail "acceptance: machine token missing in post-install HY2_AUTH_URL"
|
||||
grep -q '/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}' "$package_dir/templates/hysteria/config.yaml.tpl" || fail "acceptance: machine token missing in hysteria auth URL template"
|
||||
grep -q '^HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}$' "$package_dir/templates/env/post-install.env.tpl" || fail "acceptance: machine token missing in post-install HY2_AUTH_URL"
|
||||
|
||||
log_step "Acceptance: smoke auth checks are tokenized"
|
||||
grep -q 'unexpected auth status without machine token' orchestrator/src/steps/smoke.ts || fail "acceptance: missing 403 negative smoke for auth without machine token"
|
||||
@@ -177,6 +184,105 @@ run_clean_install_acceptance() {
|
||||
grep -q 'systemd units were not deployed by this operation' orchestrator/src/commands/install.ts \
|
||||
|| fail "acceptance: rollback must never stop services it did not deploy"
|
||||
|
||||
log_step "Acceptance: a written install-state already makes the failure post-apply"
|
||||
# Регрессия: classifyFailure не учитывал stateWritten, поэтому падение
|
||||
# apt-get объявлялось «на сервере ничего не изменено», rollback пропускался,
|
||||
# а install-state.json оставался на хосте и ломал следующую установку.
|
||||
grep -q 'ownership.stateWritten' orchestrator/src/commands/install.ts \
|
||||
|| fail "acceptance: classifyFailure must account for a written install-state"
|
||||
"$BUN_BIN" -e '
|
||||
const source = require("node:fs").readFileSync("orchestrator/src/commands/install.ts", "utf8");
|
||||
const body = source.slice(source.indexOf("export function classifyFailure"));
|
||||
const preApply = body.indexOf("return \"fatal_pre_apply\"");
|
||||
const stateWritten = body.indexOf("ownership.stateWritten");
|
||||
if (preApply < 0 || stateWritten < 0) {
|
||||
throw new Error("could not locate classifyFailure branches");
|
||||
}
|
||||
if (stateWritten > preApply) {
|
||||
throw new Error("stateWritten is checked after the fatal_pre_apply fallback");
|
||||
}
|
||||
' || fail "acceptance: fatal_pre_apply must be unreachable once install-state was written"
|
||||
|
||||
log_step "Acceptance: mutating ownership flags are raised before the step, not after"
|
||||
# Флаг «шаг завершился» отвечает не на тот вопрос: apt-get умеет изменить
|
||||
# систему и упасть. Каждый флаг обязан стоять ПЕРЕД своим await.
|
||||
"$BUN_BIN" -e '
|
||||
const source = require("node:fs").readFileSync("orchestrator/src/commands/install.ts", "utf8");
|
||||
const steps = [
|
||||
["depsTouched", "await installDeps("],
|
||||
["filesystemTouched", "await prepareFilesystem("],
|
||||
["uiTouched", "await deployUi("],
|
||||
["hysteriaTouched", "await installHysteria("],
|
||||
["configTouched", "await generateConfig("],
|
||||
["unitsTouched", "await deploySystemd("],
|
||||
["firewallTouched", "await applyFirewall("],
|
||||
["postInstallTouched", "await writePostInstallEnv("],
|
||||
["bootstrapSecretTouched", "await ensureBootstrapAdminSecret("]
|
||||
];
|
||||
for (const [flag, call] of steps) {
|
||||
const flagAt = source.indexOf("ownership." + flag + " = true");
|
||||
const callAt = source.indexOf(call);
|
||||
if (flagAt < 0) throw new Error("missing ownership flag: " + flag);
|
||||
if (callAt < 0) throw new Error("missing step call: " + call);
|
||||
if (flagAt > callAt) throw new Error(flag + " is raised after " + call);
|
||||
}
|
||||
' || fail "acceptance: ownership flags must be raised before the mutating step they cover"
|
||||
|
||||
log_step "Acceptance: the read-only phase has no universal runner to slip through"
|
||||
# Пока существовал один `run`, под которым жили и `ss -ltn`, и `useradd`,
|
||||
# guard держался на внимательности автора правки.
|
||||
grep -q 'export async function runReadOnly' orchestrator/src/lib/process.ts \
|
||||
|| fail "acceptance: process.ts must expose an explicit read-only runner"
|
||||
grep -q 'export async function runMutating' orchestrator/src/lib/process.ts \
|
||||
|| fail "acceptance: process.ts must expose an explicit mutating runner"
|
||||
! grep -rEq '(^|[^A-Za-z0-9_])(run|runVisible|runHidden|runSecret|runRawVisible)`' orchestrator/src \
|
||||
|| fail "acceptance: the pre-split runner names must not come back"
|
||||
local unguarded_runner
|
||||
unguarded_runner="$(grep -c 'assertMutationAllowed' orchestrator/src/lib/process.ts || true)"
|
||||
[ "$unguarded_runner" -ge 4 ] \
|
||||
|| fail "acceptance: every mutating runner must ask the read-only guard for permission"
|
||||
|
||||
log_step "Acceptance: the public endpoint invariant lives in preflight, not only in doctor"
|
||||
grep -q 'assertPublicEndpoint' orchestrator/src/steps/preflight.ts \
|
||||
|| fail "acceptance: preflight must verify that the public endpoint resolves to this server"
|
||||
[ -f orchestrator/src/steps/networkEndpoint.ts ] \
|
||||
|| fail "acceptance: the public endpoint invariant module is missing"
|
||||
! grep -rqE 'ifconfig\.me|ipify|checkip\.amazonaws' orchestrator/src \
|
||||
|| fail "acceptance: the server address must be resolved locally, not via an external service"
|
||||
grep -q 'networkInterfaces' orchestrator/src/steps/networkEndpoint.ts \
|
||||
|| fail "acceptance: local public IPv4 set must come from the host interfaces"
|
||||
|
||||
log_step "Acceptance: purge and clean-host describe the same boundary"
|
||||
local purged_path
|
||||
for purged_path in /var/lib/hysteria /usr/local/lib/hy2xs /usr/local/bin/hy2xs-orchestrator /var/log/hy2xs; do
|
||||
grep -qF "$purged_path" tools/legacy/purge-v0.sh \
|
||||
|| fail "acceptance: purge-v0.sh no longer removes $purged_path"
|
||||
done
|
||||
grep -qF '/var/lib/hysteria' orchestrator/src/steps/cleanHost.ts \
|
||||
|| fail "acceptance: clean-host must treat leftover Hysteria runtime state as a legacy marker"
|
||||
grep -qF '/usr/local/bin/hy2xs-orchestrator' orchestrator/src/steps/cleanHost.ts \
|
||||
|| fail "acceptance: clean-host must treat a leftover orchestrator symlink as a legacy marker"
|
||||
! grep -q 'keep-hysteria-binary' tools/legacy/purge-v0.sh \
|
||||
|| fail "acceptance: --keep-hysteria-binary contradicts the installer clean-host contract"
|
||||
|
||||
log_step "Acceptance: admin secrets never reach a persistent export file"
|
||||
# Экспорт формируется в памяти: os.Create в /var/lib/hy2xs-admin/export
|
||||
# оставлял на диске JSON с расшифрованными секретами пиров.
|
||||
[ ! -f apps/util/export.go ] \
|
||||
|| fail "acceptance: the file-based export helper came back"
|
||||
! grep -rq 'ExportPathDir' apps/cmd apps/controller apps/service apps/dao apps/model apps/util \
|
||||
|| fail "acceptance: the persistent export directory came back"
|
||||
grep -q 'c.Data(200, "application/octet-stream", payload)' apps/controller/peer.go \
|
||||
|| fail "acceptance: peer export must be served from memory"
|
||||
|
||||
log_step "Acceptance: peer import is validated as strictly as peer creation"
|
||||
grep -q 'ValidatePeerImportBatch' apps/service/peer.go \
|
||||
|| fail "acceptance: peer import must validate the whole batch before writing"
|
||||
grep -q 'DisallowUnknownFields' apps/controller/peer.go \
|
||||
|| fail "acceptance: peer import must reject unknown fields instead of silently defaulting"
|
||||
grep -q 'ReservedBootstrapPeerName' apps/service/peer_import.go \
|
||||
|| fail "acceptance: peer import must refuse to overwrite the installer-owned bootstrap peer"
|
||||
|
||||
log_step "Acceptance: missing config schema marker is treated as legacy"
|
||||
grep -q 'HY2XS_CONFIG_SCHEMA_VERSION отсутствует' orchestrator/src/config/env.ts \
|
||||
|| fail "acceptance: a missing HY2XS_CONFIG_SCHEMA_VERSION must be rejected as legacy, not defaulted"
|
||||
@@ -199,7 +305,7 @@ run_clean_install_acceptance() {
|
||||
# Ищется регистрация маршрута (имя в кавычках), а не любое упоминание:
|
||||
# комментарий, объясняющий, почему маршрута нет, должен быть разрешён.
|
||||
local dead_route
|
||||
for dead_route in hysteria2ChangeVersion listRelease updateHysteria2Config importHysteria2Config restartServer uploadCertFile hysteria2AcmePath; do
|
||||
for dead_route in hysteria2ChangeVersion listRelease updateHysteria2Config importHysteria2Config restartServer uploadCertFile hysteria2AcmePath exportConfig importConfig; do
|
||||
! grep -rqF "${dead_route}\"" apps/router apps/controller \
|
||||
|| fail "acceptance: removed route ${dead_route} came back"
|
||||
! grep -rqF "${dead_route}\"" apps/frontend/src/api \
|
||||
|
||||
@@ -111,9 +111,9 @@ bundle_ui() {
|
||||
verify_admin_version_contract "$ADMIN_BUILD_DIR/hy2xs-admin"
|
||||
|
||||
install -m 0755 "$ADMIN_BUILD_DIR/hy2xs-admin" "$STAGE_DIR/ui/hy2xs-admin/hy2xs-admin"
|
||||
if [ -f "$ui_src/docs/sql/h_ui_db.sql" ]; then
|
||||
if [ -f "$ui_src/docs/sql/schema.sql" ]; then
|
||||
mkdir -p "$STAGE_DIR/ui/hy2xs-admin/docs/sql"
|
||||
install -m 0644 "$ui_src/docs/sql/h_ui_db.sql" "$STAGE_DIR/ui/hy2xs-admin/docs/sql/h_ui_db.sql"
|
||||
install -m 0644 "$ui_src/docs/sql/schema.sql" "$STAGE_DIR/ui/hy2xs-admin/docs/sql/schema.sql"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ verify_archive() {
|
||||
|
||||
local hysteria_tpl
|
||||
hysteria_tpl="$(tar -xOzf "$archive" hy2xs-install/templates/hysteria/config.yaml.tpl)"
|
||||
printf '%s\n' "$hysteria_tpl" | grep -q '/hui/hysteria2/auth?access_token={{HYSTERIA_API_SECRET}}' || fail "hysteria auth template must include machine access_token"
|
||||
printf '%s\n' "$hysteria_tpl" | grep -q '/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}' || fail "hysteria auth template must include machine access_token"
|
||||
printf '%s\n' "$hysteria_tpl" | grep -q '{{OBFS_BLOCK}}' || fail "hysteria template must render a typed obfs block"
|
||||
|
||||
# Замороженная версия обязана совпадать во всех местах пакета.
|
||||
@@ -120,7 +120,7 @@ verify_archive() {
|
||||
|
||||
local post_install_tpl
|
||||
post_install_tpl="$(tar -xOzf "$archive" hy2xs-install/templates/env/post-install.env.tpl)"
|
||||
printf '%s\n' "$post_install_tpl" | grep -q '^HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/hui/hysteria2/auth?access_token={{HYSTERIA_API_SECRET}}$' || fail "post-install env template must include machine access_token in HY2_AUTH_URL"
|
||||
printf '%s\n' "$post_install_tpl" | grep -q '^HY2_AUTH_URL=http://127.0.0.1:{{UI_PORT}}/internal/hysteria/auth?access_token={{HYSTERIA_API_SECRET}}$' || fail "post-install env template must include machine access_token in HY2_AUTH_URL"
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp -d)"
|
||||
|
||||
@@ -164,11 +164,64 @@ verify_orchestrator_contract() {
|
||||
HY2XS_TARGET_ARCH)
|
||||
expect_equal "orchestrator target arch" "$value" "$HY2XS_TARGET_ARCH"
|
||||
;;
|
||||
HY2XS_ADMIN_API_BASE)
|
||||
HY2XS_ADMIN_API_BASE="$value"
|
||||
;;
|
||||
HY2XS_HYSTERIA_MACHINE_AUTH_PATH)
|
||||
HY2XS_HYSTERIA_MACHINE_AUTH_PATH="$value"
|
||||
;;
|
||||
*)
|
||||
fail "versions contract: unexpected orchestrator contract line: $line"
|
||||
;;
|
||||
esac
|
||||
done <<<"$output"
|
||||
|
||||
[ -n "${HY2XS_ADMIN_API_BASE:-}" ] \
|
||||
|| fail "versions contract: orchestrator did not report HY2XS_ADMIN_API_BASE"
|
||||
[ -n "${HY2XS_HYSTERIA_MACHINE_AUTH_PATH:-}" ] \
|
||||
|| fail "versions contract: orchestrator did not report HY2XS_HYSTERIA_MACHINE_AUTH_PATH"
|
||||
export HY2XS_ADMIN_API_BASE HY2XS_HYSTERIA_MACHINE_AUTH_PATH
|
||||
}
|
||||
|
||||
# API namespace — это runtime-контракт между тремя компонентами.
|
||||
#
|
||||
# Путь machine-auth уезжает в /etc/hysteria/config.yaml и в post-install.env,
|
||||
# то есть по нему Hysteria обращается к админке. Пока строка была размазана по
|
||||
# шаблонам, smoke, тестам и e2e, любое расхождение обнаруживалось только на
|
||||
# живом сервере. Здесь она сверяется во всех местах сразу — против константы,
|
||||
# скомпилированной в оркестратор.
|
||||
verify_api_namespace_contract() {
|
||||
local go_auth_path go_api_base
|
||||
|
||||
go_auth_path="$(grep -Eo 'HysteriaMachineAuthPath[[:space:]]*=[[:space:]]*"[^"]*"' apps/model/constant/api.go \
|
||||
| head -n1 | sed -E 's/.*"([^"]*)"/\1/')"
|
||||
go_api_base="$(grep -Eo 'AdminAPIBase[[:space:]]*=[[:space:]]*"[^"]*"' apps/model/constant/api.go \
|
||||
| head -n1 | sed -E 's/.*"([^"]*)"/\1/')"
|
||||
|
||||
expect_equal "apps constant.HysteriaMachineAuthPath" "$go_auth_path" "$HY2XS_HYSTERIA_MACHINE_AUTH_PATH"
|
||||
expect_equal "apps constant.AdminAPIBase" "$go_api_base" "$HY2XS_ADMIN_API_BASE"
|
||||
|
||||
local frontend_base
|
||||
frontend_base="$(grep -Eo 'const API_BASE = "[^"]*"' apps/frontend/src/utils/request.ts \
|
||||
| head -n1 | sed -E 's/.*"([^"]*)"/\1/')"
|
||||
expect_equal "frontend API_BASE" "$frontend_base" "$HY2XS_ADMIN_API_BASE"
|
||||
|
||||
grep -qF "${HY2XS_HYSTERIA_MACHINE_AUTH_PATH}?access_token={{HYSTERIA_API_SECRET}}" \
|
||||
package/templates/hysteria/config.yaml.tpl \
|
||||
|| fail "versions contract: hysteria template does not use ${HY2XS_HYSTERIA_MACHINE_AUTH_PATH}"
|
||||
grep -qF "${HY2XS_HYSTERIA_MACHINE_AUTH_PATH}?access_token={{HYSTERIA_API_SECRET}}" \
|
||||
package/templates/env/post-install.env.tpl \
|
||||
|| fail "versions contract: post-install env template does not use ${HY2XS_HYSTERIA_MACHINE_AUTH_PATH}"
|
||||
|
||||
# Старое пространство имён не имеет права вернуться ни в один компонент.
|
||||
# Историческое имя допустимо только в docs/14-legacy-cleanup.md и в
|
||||
# legacy-маркерах clean-host: там это имя чужого артефакта, а не наше.
|
||||
local legacy_hits
|
||||
legacy_hits="$(grep -rlF '/hui' \
|
||||
apps/model apps/router apps/controller apps/service apps/frontend/src \
|
||||
orchestrator/src orchestrator/test package/templates tools/test 2>/dev/null || true)"
|
||||
[ -z "$legacy_hits" ] \
|
||||
|| fail "versions contract: legacy /hui namespace came back in: $legacy_hits"
|
||||
}
|
||||
|
||||
verify_go_toolchain_contract() {
|
||||
@@ -206,6 +259,7 @@ verify_versions_contract() {
|
||||
"$HY2XS_CONFIG_SCHEMA_VERSION"
|
||||
|
||||
verify_orchestrator_contract
|
||||
verify_api_namespace_contract
|
||||
verify_go_toolchain_contract
|
||||
|
||||
expect_equal "build host OS" "$HY2XS_BUILD_OS" "debian"
|
||||
|
||||
@@ -143,7 +143,7 @@ wait_for_port() {
|
||||
die "$label did not start listening on port $port"
|
||||
}
|
||||
|
||||
# Mock HY2XS auth endpoint повторяет контракт /hui/hysteria2/auth:
|
||||
# Mock HY2XS auth endpoint повторяет контракт /internal/hysteria/auth:
|
||||
# проверку machine access_token и отказ неразрешённому секрету.
|
||||
start_auth_endpoint() {
|
||||
cat >"$WORK_DIR/auth-server.ts" <<EOF
|
||||
@@ -242,7 +242,7 @@ render_server_config() {
|
||||
|
||||
grep -q "type: $obfs_type" "$WORK_DIR/server.yaml" \
|
||||
|| die "rendered server config does not use obfs type $obfs_type"
|
||||
grep -q "127.0.0.1:${AUTH_PORT}/hui/hysteria2/auth?access_token=" "$WORK_DIR/server.yaml" \
|
||||
grep -q "127.0.0.1:${AUTH_PORT}/internal/hysteria/auth?access_token=" "$WORK_DIR/server.yaml" \
|
||||
|| die "server config lost the HY2XS machine auth token"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user