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:
@@ -85,6 +85,25 @@ PHASE 1 — MUTATION
|
||||
Полный список маркеров чужой установки и порядок очистки —
|
||||
[14-legacy-cleanup.md](14-legacy-cleanup.md).
|
||||
|
||||
### Раннеры подпроцессов: два набора, а не один
|
||||
|
||||
Guard умеет останавливать только то, что через него проходит. Поэтому
|
||||
универсального раннера в `lib/process.ts` нет — есть два явных набора:
|
||||
|
||||
| Набор | Guard | Назначение |
|
||||
| --- | --- | --- |
|
||||
| `runReadOnly`, `runReadOnlySecret` | не трогает | наблюдение за системой: `ss`, `systemctl is-active`, `curl`, `getent` |
|
||||
| `runMutating`, `runMutatingVisible`, `runMutatingHidden`, `runMutatingRaw` | спрашивает разрешение | всё, что может изменить хост |
|
||||
|
||||
`*Secret`-варианты не печатают команду в текст ошибки: их аргументы несут
|
||||
machine token или пароль пира, а сообщение уходит в логи и диагностику.
|
||||
|
||||
До разделения существовал один `run`, под которым одинаково жили `ss -ltn` и
|
||||
`useradd`/`install -d`/`mkdir`. Guard стоял только на части раннеров, поэтому
|
||||
утверждение «PHASE 0 ничего не пишет» держалось на внимательности автора
|
||||
следующей правки. Выбор набора теперь — обязательное решение на месте вызова;
|
||||
возвращение старых имён ломает приёмку сборки.
|
||||
|
||||
## Маркер состояния установки
|
||||
|
||||
`/var/lib/hy2xs/install-state.json` отвечает на вопрос «эта машина — установка
|
||||
@@ -112,30 +131,94 @@ PHASE 1 — MUTATION
|
||||
|
||||
## Ownership и rollback
|
||||
|
||||
Операция ведёт учёт того, что она реально успела применить:
|
||||
Операция ведёт учёт того, к чему она **могла прикоснуться**:
|
||||
|
||||
```text
|
||||
depsInstalled
|
||||
filesystemPrepared
|
||||
unitsDeployed
|
||||
stateWritten
|
||||
depsTouched
|
||||
filesystemTouched
|
||||
uiTouched
|
||||
hysteriaTouched
|
||||
configTouched
|
||||
unitsTouched
|
||||
firewallTouched
|
||||
postInstallWritten
|
||||
postInstallTouched
|
||||
bootstrapSecretTouched
|
||||
servicesStarted
|
||||
```
|
||||
|
||||
Формулировка выбрана намеренно. Флаг «шаг успешно завершился» отвечает не на
|
||||
тот вопрос: `apt-get install` умеет распаковать половину пакетов и упасть, и
|
||||
хост уже изменён, хотя шаг не закончился. Поэтому **каждый флаг взводится перед
|
||||
мутирующим вызовом**, а не после него.
|
||||
|
||||
`stateWritten` — полноценный участник классификации. `install-state.json`
|
||||
пишется сразу после успешного preflight, до `installDeps`; пока он в
|
||||
классификации не учитывался, падение `apt-get` объявлялось «на сервере ничего
|
||||
не изменено», rollback пропускался, а маркер оставался на хосте и ломал
|
||||
следующую установку по clean-host контракту.
|
||||
|
||||
Классификация отказа строится **по этим флагам и фазе**, а не по тексту
|
||||
сообщения об ошибке. Ранее классификация шла по подстрокам, из-за чего
|
||||
preflight-ошибка со словом `nftables` приводила к откату чужого firewall.
|
||||
|
||||
Инварианты rollback:
|
||||
|
||||
- `fatal_pre_apply` по определению означает «ничего не применялось»:
|
||||
system rollback не выполняется, `install-state.json` не пишется,
|
||||
- `fatal_pre_apply` по определению означает «ничего не применялось». Попасть в
|
||||
него нельзя ни при одном взведённом флаге, включая `stateWritten`. В этом
|
||||
случае system rollback не выполняется, `install-state.json` не пишется,
|
||||
diagnostics-бандл не собирается (его сбор сам создал бы каталоги в
|
||||
`/var/log/hy2xs`).
|
||||
- `systemctl stop/disable` выполняется **только если текущая операция сама
|
||||
развернула эти unit-файлы**.
|
||||
|
||||
## Инвариант публичного endpoint
|
||||
|
||||
`preflight` проверяет, что публичный endpoint ведёт **на этот сервер**. Так как
|
||||
preflight общий для `install`, `reconfigure` и `doctor`, инвариант действует во
|
||||
всех трёх сценариях.
|
||||
|
||||
Алгоритм:
|
||||
|
||||
```text
|
||||
1. локальные публичные IPv4 из node:os networkInterfaces()
|
||||
(минус 0/8, 10/8, 100.64/10, 127/8, 169.254/16,
|
||||
172.16/12, 192.168/16, 224/4, 240/4)
|
||||
|
||||
2. HY2XS_PUBLIC_HOST
|
||||
IPv4-литерал → обязан быть в локальном множестве
|
||||
домен → все A-записи обязаны быть в локальном множестве
|
||||
|
||||
3. HY2XS_DOMAIN, если задан и отличается от publicHost → та же проверка
|
||||
|
||||
4. AAAA-политика остаётся отдельной
|
||||
```
|
||||
|
||||
Проверяется именно `HY2XS_PUBLIC_HOST`, потому что в `hysteria2://` уезжает он,
|
||||
а не TLS-домен. По умолчанию они совпадают, но архитектурно это разные
|
||||
сущности, и до v1 проверялся только `HY2XS_DOMAIN`.
|
||||
|
||||
Адрес сервера определяется **локально**. Внешние сервисы определения IP не
|
||||
используются: они добавили бы `doctor` сетевую зависимость и превратили бы
|
||||
недоступность стороннего сервиса в ложный отказ установки.
|
||||
|
||||
Несколько публичных IPv4 у сервера — норма: достаточно, чтобы DNS указывал на
|
||||
один из них. Обратное неверно: лишняя A-запись рядом с правильной означает
|
||||
второй, чужой backend за тем же именем. HY2XS — single-host профиль, поэтому
|
||||
это ошибка конфигурации DNS, а не балансировка.
|
||||
|
||||
Строгость управляется `HY2XS_PUBLIC_ENDPOINT_POLICY`:
|
||||
|
||||
| Значение | Поведение |
|
||||
| --- | --- |
|
||||
| `strict` (по умолчанию) | расхождение останавливает операцию |
|
||||
| `warn` | печатается предупреждение, операция продолжается |
|
||||
| `off` | сравнение не выполняется |
|
||||
|
||||
Ослабление предназначено для топологий вне baseline (NAT, floating IP, anycast).
|
||||
Отсутствие A-записи остаётся фатальным при любом значении: имя без A-записи не
|
||||
работает ни в какой топологии.
|
||||
|
||||
## Что приходит на target
|
||||
|
||||
На target должен попадать уже готовый package, содержащий:
|
||||
|
||||
Reference in New Issue
Block a user