docs: clean-install-only, versions.env и очистка предыдущего поколения
Новый docs/14-legacy-cleanup.md: как выглядит отказ установщика, полный список маркеров чужой установки, что сохранить перед очисткой, работа purge-v0.sh, ручная процедура и отдельно - случай незавершённой установки текущего поколения, где нужен repair, а не очистка. Обновлено под фактическое поведение: - README и package/docs: установка описана как две фазы, PHASE 0 ничего не меняет; добавлен troubleshooting по отказу clean-host; версии toolchain больше не передаются через окружение; - 02-build-layer: раздел про versions.env (что в нём есть и чего нет и почему), verify_versions_contract, проверка происхождения артефакта по upstream hashes.txt; - 08-orchestrator-spec: двухфазный контракт, read-only guard, идентификация поколения в install-state, ownership-aware rollback, расширенная семантическая проверка конфига, структурная редакция; - 04-admin-panel: таблица удалённых маршрутов и почему они удалены, а не оставлены заглушками; сужена формулировка гарантии санитайза; - 11-testing: новые unit-наборы, полный список инвариантов конфига, раздел про одну реализацию URI вместо двух, сценарий проверки границы установки на живом сервере; - 12-operations и 13-runbook: диагностика отказов по поколению, поведение diagnostics-бандла; - tools/build/README: контракт версий, обе суммы Bun, hashes.txt. CHANGELOG: раздел Unreleased с разбором каждого исправленного дефекта.
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
## Главная роль оркестратора
|
||||
|
||||
Оркестратор работает **только на target machine** и умеет:
|
||||
- выполнить read-only проверку чистоты хоста (`preflight-install`)
|
||||
- выполнить первичную установку (`install`)
|
||||
- выполнить явную реконфигурацию (`reconfigure --dry-run|--apply`)
|
||||
- разложить bundled UI
|
||||
@@ -48,6 +49,93 @@
|
||||
|
||||
Если машина уже «жила своей жизнью», baseline не обещает корректной автоадаптации.
|
||||
|
||||
## Двухфазный контракт установки
|
||||
|
||||
Установка разделена на две фазы с жёсткой границей между ними:
|
||||
|
||||
```text
|
||||
PHASE 0 — READ ONLY
|
||||
проверка прав
|
||||
sha256sum -c metadata/checksums.txt
|
||||
./orchestrator/hy2xs-orchestrator preflight-install --package-dir <распакованный пакет>
|
||||
├── платформа Debian 13 amd64
|
||||
├── clean-host контракт
|
||||
└── валидация конфигурации
|
||||
↓ ноль persistent writes
|
||||
PHASE 0 PASSED
|
||||
↓
|
||||
PHASE 1 — MUTATION
|
||||
install -d /usr/local/lib/hy2xs
|
||||
раскладка оркестратора и runtime-пакета
|
||||
hy2xs-orchestrator install
|
||||
```
|
||||
|
||||
Ключевые свойства:
|
||||
|
||||
- `preflight-install` запускается **из распакованного пакета**, а не из
|
||||
установленного `/usr/local/lib/hy2xs`: до PHASE 1 этого каталога может не
|
||||
существовать, и создавать его нельзя.
|
||||
- Граница держится не соглашением, а **read-only guard** (`lib/guard.ts`):
|
||||
под ним `writeText`/`writeTextAtomic` и мутирующие раннеры `lib/process`
|
||||
кидают ошибку. Это проверяется тестами.
|
||||
- Внутри `install` **`preflight()` выполняется раньше первой записи
|
||||
`install-state.json`**. Отказ на этом этапе означает, что на сервере не
|
||||
изменено ничего.
|
||||
|
||||
Полный список маркеров чужой установки и порядок очистки —
|
||||
[14-legacy-cleanup.md](14-legacy-cleanup.md).
|
||||
|
||||
## Маркер состояния установки
|
||||
|
||||
`/var/lib/hy2xs/install-state.json` отвечает на вопрос «эта машина — установка
|
||||
**текущего поколения** HY2XS, и в каком она состоянии». Поэтому кроме фазы он
|
||||
несёт идентификацию поколения:
|
||||
|
||||
```json
|
||||
{
|
||||
"product": "hy2xs",
|
||||
"release_line": 1,
|
||||
"config_schema_version": 2,
|
||||
"product_version": "1.0.0",
|
||||
"installed": true,
|
||||
"phase": "installed"
|
||||
}
|
||||
```
|
||||
|
||||
`reconfigure` и `repair` проверяют `product` / `release_line` /
|
||||
`config_schema_version` **до** всего остального. Флага `installed: true`
|
||||
недостаточно: такой же маркер мог остаться от 0.x.
|
||||
|
||||
`repair` дополнительно требует явного `--allow-partial-state`, чтобы работать
|
||||
поверх незавершённой установки. Разрешение не подразумевается: молчаливое
|
||||
согласие на произвольный partial marker и позволяло «чинить» чужое состояние.
|
||||
|
||||
## Ownership и rollback
|
||||
|
||||
Операция ведёт учёт того, что она реально успела применить:
|
||||
|
||||
```text
|
||||
depsInstalled
|
||||
filesystemPrepared
|
||||
unitsDeployed
|
||||
firewallTouched
|
||||
postInstallWritten
|
||||
servicesStarted
|
||||
```
|
||||
|
||||
Классификация отказа строится **по этим флагам и фазе**, а не по тексту
|
||||
сообщения об ошибке. Ранее классификация шла по подстрокам, из-за чего
|
||||
preflight-ошибка со словом `nftables` приводила к откату чужого firewall.
|
||||
|
||||
Инварианты rollback:
|
||||
|
||||
- `fatal_pre_apply` по определению означает «ничего не применялось»:
|
||||
system rollback не выполняется, `install-state.json` не пишется,
|
||||
diagnostics-бандл не собирается (его сбор сам создал бы каталоги в
|
||||
`/var/log/hy2xs`).
|
||||
- `systemctl stop/disable` выполняется **только если текущая операция сама
|
||||
развернула эти unit-файлы**.
|
||||
|
||||
## Что приходит на target
|
||||
|
||||
На target должен попадать уже готовый package, содержащий:
|
||||
@@ -74,7 +162,7 @@
|
||||
|
||||
## Что делает оркестратор по шагам
|
||||
|
||||
1. Проверяет, что ОС — Debian 13.
|
||||
1. Проверяет, что ОС — Debian 13, и что хост чист (**до любой мутации**).
|
||||
2. Проверяет базовые зависимости и install context.
|
||||
3. Создаёт каталоги установки.
|
||||
4. Разворачивает bundled HY2XS admin.
|
||||
@@ -115,19 +203,70 @@
|
||||
## CLI baseline
|
||||
|
||||
Команды:
|
||||
- `preflight-install --package-dir <path> [--config <source-env>]`
|
||||
- `install --package-dir <path> [--config <source-env>]`
|
||||
- `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --dry-run`
|
||||
- `reconfigure --package-dir <path> --config /etc/hy2xs/hy2xs.env --apply`
|
||||
- `repair --package-dir <path> --config /etc/hy2xs/hy2xs.env [--allow-partial-state]`
|
||||
- `redact-config --config <path> (--in-place | --out <path>) [--format auto|env|yaml]`
|
||||
|
||||
`preflight-install` не принимает `--skip-*`: эти флаги влияют на мутацию, а
|
||||
PHASE 0 ничего не меняет.
|
||||
|
||||
`--allow-partial-state` допустим только для `repair`.
|
||||
|
||||
Инварианты:
|
||||
- только IPv4 bind/listen;
|
||||
- TLS modes: `acme | file | self_signed_dev`;
|
||||
- `trafficStats.secret` отдельный от `JWT_SECRET`;
|
||||
- `HY2XS_CONFIG_SCHEMA_VERSION` — обязательное поле; его отсутствие трактуется
|
||||
как legacy-конфигурация и отклоняется, а не заменяется значением по умолчанию;
|
||||
- install flow фиксирует фактически установленную версию Hysteria в snapshot;
|
||||
- версия/URL/SHA256 Hysteria берутся из metadata install package;
|
||||
- `reconfigure` не обновляет бинарник Hysteria, только runtime-слой.
|
||||
- `reconfigure` не обновляет бинарник Hysteria, только runtime-слой;
|
||||
- при `reconfigure --apply`: backup -> staged apply -> smoke -> rollback on fail.
|
||||
|
||||
## Семантическая проверка сгенерированного конфига
|
||||
|
||||
`assertHysteriaConfigMatchesProfile` разбирает YAML и сверяет его с
|
||||
production-профилем, а не ищет подстроки. Проверяются, в частности:
|
||||
|
||||
- `listen`, ровно один подтип `obfs` и его соответствие `obfs.type`;
|
||||
- размеры пакетов Gecko;
|
||||
- `bandwidth`, `disableLossCompensation`, `ignoreClientBandwidth`;
|
||||
- `congestion.type` / `bbrProfile`;
|
||||
- весь QUIC baseline, **включая `maxIdleTimeout`**;
|
||||
- `trafficStats.listen` и непустой `secret`;
|
||||
- `auth.type`, **точный** `auth.http.url` (host/port/path/token) и
|
||||
`auth.http.insecure`;
|
||||
- ACME: `type`, `email`, `ca`, `dir`, `listenHost`, первый домен;
|
||||
- отсутствие посторонних секций верхнего уровня.
|
||||
|
||||
Сообщение об ошибке для `auth.http.url` намеренно не печатает сам токен: текст
|
||||
уходит в логи и в diagnostics-бандл.
|
||||
|
||||
## Редактирование секретов
|
||||
|
||||
`redact-config` и diagnostics-бандл используют **структурную** редакцию: YAML
|
||||
разбирается и обходится как дерево.
|
||||
|
||||
Это не косметика. Построчное правило `auth:\s*(.*)` подставляло маркер в
|
||||
заголовок mapping'а и оставляло нетронутым вложенный
|
||||
`auth.http.url` с `access_token=<секрет>`, то есть бандл уносил machine token
|
||||
наружу. Значение может лежать где угодно в дереве, поэтому обходить нужно
|
||||
дерево.
|
||||
|
||||
Редактируются:
|
||||
- поля с секретоподобным именем (`password`, `secret`, `token`, `apiKey`,
|
||||
`privateKey`, `authorization`, `cookie`, `bearer`, `signature`, …);
|
||||
- карты, где секретны все значения (`auth.userpass`, `acme.dns.config`);
|
||||
- учётные данные и секретные query-параметры внутри URL — в том числе в
|
||||
env-файлах, где имя ключа (`HY2_AUTH_URL`) ни под один маркер не подходит.
|
||||
|
||||
Гарантия формулируется честно: **known secrets + secret-shaped unknown
|
||||
fields**. Обобщённый sanitizer не может пообещать, что под правило попадёт
|
||||
любой будущий секрет.
|
||||
|
||||
## Что не реализовывать
|
||||
|
||||
- update subcommands
|
||||
|
||||
Reference in New Issue
Block a user