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:
@@ -125,19 +125,28 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
|
||||
- чистый хост проходит;
|
||||
- **каждый** маркер по отдельности останавливает установку;
|
||||
- список покрывает состояние, юниты, бинарник Hysteria и наследие 0.x;
|
||||
- пути из конфигурации (`HY2XS_INSTALL_DIR`, `HY2XS_DATA_DIR`) попадают в
|
||||
список, а не только значения по умолчанию;
|
||||
- `/usr/local/lib/hy2xs/package` — маркер в PHASE 0, но не в PHASE 1: между
|
||||
фазами его создаёт сам `install.sh`;
|
||||
- пути из конфигурации (`HY2XS_INSTALL_DIR`, `HY2XS_DATA_DIR`, `HY2XS_LOG_DIR`)
|
||||
попадают в список, а не только значения по умолчанию;
|
||||
- всё, что удаляет `purge-v0.sh`, покрыто маркерами clean-host: два списка
|
||||
описывают одну границу и не имеют права разъезжаться;
|
||||
- пути, созданные `install.sh` между фазами (`/usr/local/lib/hy2xs`,
|
||||
`/usr/local/lib/hy2xs/package`, `/usr/local/bin/hy2xs-orchestrator`), —
|
||||
маркеры в PHASE 0, но не в PHASE 1;
|
||||
- сообщение перечисляет найденные маркеры и говорит, что хост не изменён.
|
||||
|
||||
`orchestrator/test/install-boundary.test.ts`:
|
||||
|
||||
- под read-only guard недоступны `writeText`, `writeTextAtomic`, `runVisible`,
|
||||
`runHidden`, `runRawVisible`;
|
||||
- под read-only guard недоступны `writeText`, `writeTextAtomic` и все
|
||||
`runMutating*`-раннеры;
|
||||
- read-only раннеры под guard'ом продолжают работать: разделение API — это не
|
||||
запрет наблюдения, а запрет мутации;
|
||||
- классификация отказа зависит от ownership-флагов и фазы, а **не** от текста
|
||||
ошибки;
|
||||
- пока операция ничего не применила, отказ — `fatal_pre_apply`.
|
||||
- `fatal_pre_apply` недостижим ни при одном взведённом флаге, включая
|
||||
`stateWritten`: записанный `install-state.json` уже делает хост изменённым;
|
||||
- начатая (не обязательно завершённая) установка пакетов уже даёт
|
||||
`fatal_post_apply` — регрессия на сценарий «PHASE 0 прошла, apt-get упал,
|
||||
установщик заявил, что ничего не тронул».
|
||||
|
||||
`orchestrator/test/install-state.test.ts`:
|
||||
|
||||
@@ -160,7 +169,51 @@ HYSTERIA_BIN=/usr/local/bin/hysteria ./tools/test/e2e-hysteria.sh
|
||||
- `acme.dns.config` вырезается целиком;
|
||||
- невалидный YAML не роняет редакцию и всё равно чистится;
|
||||
- секрет внутри URL-значения в env вырезается, даже если имя ключа несекретное
|
||||
(`HY2_AUTH_URL`).
|
||||
(`HY2_AUTH_URL`);
|
||||
- URL под **произвольным** именем ключа теряет встроенные учётные данные и
|
||||
секретные query-параметры, но сохраняет адрес; то же для URL внутри списка.
|
||||
|
||||
## A8. Инвариант публичного endpoint (unit)
|
||||
|
||||
`orchestrator/test/network-endpoint.test.ts` — проба подменяет и DNS, и список
|
||||
локальных адресов, поэтому тест не зависит ни от сети, ни от интерфейсов машины
|
||||
разработчика.
|
||||
|
||||
| Сценарий | Результат |
|
||||
| --- | --- |
|
||||
| A-запись == текущий публичный IPv4 | PASS |
|
||||
| A-запись == старый IPv4 | FAIL, в тексте оба адреса |
|
||||
| A-запись отсутствует | FAIL |
|
||||
| A == текущий + чужой | FAIL |
|
||||
| у сервера 2 публичных IP, DNS использует один | PASS |
|
||||
| `PUBLIC_HOST` — правильный IPv4-литерал | PASS |
|
||||
| `PUBLIC_HOST` — устаревший IPv4-литерал | FAIL |
|
||||
| `DOMAIN` совпадает, отдельный `PUBLIC_HOST` устарел | FAIL |
|
||||
| `PUBLIC_HOST` совпадает, отдельный TLS-домен устарел | FAIL |
|
||||
| нет ни одного локального публичного IPv4 | FAIL |
|
||||
| `HY2XS_PUBLIC_ENDPOINT_POLICY` = strict / warn / off | fail / warn / skip |
|
||||
| отсутствие A-записи при любой политике | FAIL |
|
||||
|
||||
Отдельно проверяется классификация IPv4: приватные, CGNAT, link-local,
|
||||
multicast и reserved диапазоны не считаются публичным адресом сервера, а
|
||||
`172.32.0.0` и `172.15.255.255` — считаются (границы `172.16/12`).
|
||||
|
||||
## A9. Импорт пиров (unit)
|
||||
|
||||
`apps/service/peer_import_test.go`:
|
||||
|
||||
- выгрузка, сделанная `ExportPeer`, принимается без правок;
|
||||
- имя проверяется теми же правилами, что и при обычном создании пира: длина,
|
||||
набор символов, отсутствие пробелов и переводов строки;
|
||||
- `bootstrap-admin-peer` не может быть импортирован ни по имени, ни по `authId`:
|
||||
его секрет продублирован в `/etc/hy2xs/bootstrap-admin.secret`;
|
||||
- диапазоны `quotaBytes`, `expiresAt`, `maxDevices`, `disabled`, `bannedUntil`,
|
||||
счётчиков трафика и длины секрета проверяются;
|
||||
- sentinel-значения (`quotaBytes = -1`, `maxDevices = 0`) остаются валидными;
|
||||
- дубликаты имени и `authId` внутри одной партии отклоняются;
|
||||
- партия сверх лимита отклоняется;
|
||||
- невалидная **последняя** запись отклоняет весь файл: импорт применяется
|
||||
целиком или не применяется вовсе.
|
||||
|
||||
## A7. Контракт версий (build)
|
||||
|
||||
@@ -248,7 +301,7 @@ trafficStats:
|
||||
|
||||
auth:
|
||||
type == http
|
||||
url == http://127.0.0.1:<UI_PORT>/hui/hysteria2/auth?access_token=<machine token>
|
||||
url == http://127.0.0.1:<UI_PORT>/internal/hysteria/auth?access_token=<machine token>
|
||||
insecure == (tlsMode == self_signed_dev)
|
||||
|
||||
TLS:
|
||||
@@ -332,8 +385,15 @@ idle timeout проходил семантическую проверку. То
|
||||
- операционные поля остаются читаемыми;
|
||||
- вырезаются: obfs-пароль, `trafficStats.secret`, `access_token`, `auth.userpass`, учётные данные ACME DNS, пароли outbound;
|
||||
- вырезается **неизвестное** поле с секретным именем;
|
||||
- URL под произвольным именем ключа (`endpoint:`) теряет учётные данные и
|
||||
`access_token`, но сохраняет адрес; то же для URL внутри списка;
|
||||
- не-URL скаляры (`50 mbps`, `0.0.0.0:443`, `10.0.0.1:1080`, `30s`, числа)
|
||||
проходят санитайзер без изменений;
|
||||
- пути к файлам (`tls.key`, `ech.keyPath`, `clientCA`) остаются видимыми.
|
||||
|
||||
Го- и TS-санитайзеры описывают один контракт и покрыты зеркальными тестами:
|
||||
граница определяется значением, а не именем ключа.
|
||||
|
||||
## D0. Граница установки на живом сервере
|
||||
|
||||
Проверяется на хосте, где уже стоит предыдущая установка:
|
||||
@@ -350,6 +410,47 @@ idle timeout проходил семантическую проверку. То
|
||||
`/usr/local/lib/hy2xs` и `install-state.json`, а затем откатом останавливал и
|
||||
выключал работающие службы старой установки.
|
||||
|
||||
## D1. Отказ сразу после успешной PHASE 0 (fault injection)
|
||||
|
||||
Проверяется на чистом хосте. Это узкая щель между «PHASE 0 прошла» и «первая
|
||||
мутирующая операция упала» — место, где установщик раньше врал.
|
||||
|
||||
1. PHASE 0 проходит успешно;
|
||||
2. `installDeps` ломается искусственно (например, недоступный apt-репозиторий
|
||||
или временно испорченный `/etc/apt/sources.list.d/`);
|
||||
3. установка завершается отказом;
|
||||
4. в выводе **нет** `fatal_pre_apply` и нет фразы про «ничего не применялось»;
|
||||
5. `/var/lib/hy2xs/install-state.json` существует и честно показывает
|
||||
`phase: failed` с текстом ошибки;
|
||||
6. diagnostics-бандл собран;
|
||||
7. `hy2xs-orchestrator status` не заявляет установку успешной.
|
||||
|
||||
До исправления шаги 4–6 давали противоположный результат: `install-state.json`
|
||||
уже лежал на диске, но отказ классифицировался как pre-apply, обработка
|
||||
состояния пропускалась, а следующая установка на этой машине отказывалась по
|
||||
clean-host контракту из-за оставшегося маркера.
|
||||
|
||||
## D2. Устаревший DNS после смены IPv4 провайдером
|
||||
|
||||
Проверяется на рабочей установке.
|
||||
|
||||
```text
|
||||
сервер: текущий публичный IPv4 = B
|
||||
DNS: A-запись = A (старый адрес)
|
||||
|
||||
hy2xs-orchestrator doctor
|
||||
→ FAIL
|
||||
→ в выводе присутствуют и A, и B
|
||||
|
||||
обновить A-запись на B, дождаться TTL
|
||||
|
||||
hy2xs-orchestrator doctor
|
||||
→ PASS
|
||||
```
|
||||
|
||||
Дополнительно: `reconfigure --apply` при устаревшей A-записи тоже обязан
|
||||
отказать — инвариант живёт в общем `preflight`, а не в одном `doctor`.
|
||||
|
||||
## D. Negative tests
|
||||
|
||||
1. не Debian 13
|
||||
@@ -365,6 +466,12 @@ idle timeout проходил семантическую проверку. То
|
||||
11. неизвестный `HY2XS_HYSTERIA_OBFS_TYPE`
|
||||
12. конфигурация со схемой `HY2XS_CONFIG_SCHEMA_VERSION` из линейки `0.x`
|
||||
13. upstream `latest` несовместим с шаблоном HY2XS — падает сборка, не установка
|
||||
14. `HY2XS_PUBLIC_HOST` резолвится не на этот сервер
|
||||
15. `HY2XS_DOMAIN` резолвится не на этот сервер при отличном от него `PUBLIC_HOST`
|
||||
16. A-запись содержит правильный адрес и чужой одновременно
|
||||
17. неизвестное значение `HY2XS_PUBLIC_ENDPOINT_POLICY`
|
||||
18. импорт пиров с невалидной записью — файл не применяется частично
|
||||
19. импорт пиров, пытающийся перезаписать `bootstrap-admin-peer`
|
||||
|
||||
## E. Fix20 production matrix (обязательные сценарии)
|
||||
|
||||
@@ -397,6 +504,14 @@ idle timeout проходил семантическую проверку. То
|
||||
- install не падает на race после restart;
|
||||
- readiness waiters дожидаются listener/healthz.
|
||||
|
||||
8. **Отказ между PHASE 0 и первой мутацией** (сценарий D1):
|
||||
- `install-state.json` честно показывает `failed`;
|
||||
- установщик не заявляет, что хост не изменён.
|
||||
|
||||
9. **Устаревший DNS после смены IPv4** (сценарий D2):
|
||||
- `doctor` и `reconfigure` отказывают;
|
||||
- в выводе присутствуют оба адреса.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
Система принимается, если:
|
||||
|
||||
Reference in New Issue
Block a user