3a4ce9c751
Новый 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 с разбором каждого исправленного дефекта.
277 lines
13 KiB
Markdown
277 lines
13 KiB
Markdown
# Install-only orchestrator spec
|
||
|
||
## Цель документа
|
||
|
||
Зафиксировать ТЗ на оркестратор с учётом двухслойной архитектуры: builder отдельно, target install отдельно.
|
||
|
||
## Технологический стек оркестратора
|
||
|
||
Оркестратор фиксируется как:
|
||
- **Bun + TypeScript** по исходникам
|
||
- локальная сборка builder layer'ом
|
||
- поставка на target в виде **готового install-артефакта**
|
||
|
||
Это означает:
|
||
- на target нет `npm`, `pnpm`, `yarn` или `bun install`
|
||
- на target нет transpile/build step
|
||
- shell на target допустим только как thin wrapper entrypoint
|
||
|
||
## Главная роль оркестратора
|
||
|
||
Оркестратор работает **только на target machine** и умеет:
|
||
- выполнить read-only проверку чистоты хоста (`preflight-install`)
|
||
- выполнить первичную установку (`install`)
|
||
- выполнить явную реконфигурацию (`reconfigure --dry-run|--apply`)
|
||
- разложить bundled UI
|
||
- скачать Hysteria2 из official upstream
|
||
- создать/обновить конфиги
|
||
- создать unit-файлы
|
||
- применить staged firewall
|
||
- создать `post-install.env` и runtime env-файл
|
||
|
||
## Оркестратор не умеет
|
||
|
||
- upgrade
|
||
- standalone rollback subcommands
|
||
- uninstall
|
||
- repair старых неизвестных состояний
|
||
- target-side build
|
||
- target-side git clone исходного кода HY2XS admin
|
||
- Telegram-бот / access delivery
|
||
|
||
## Предусловия
|
||
|
||
Оркестратор рассчитан только на:
|
||
- чистый Debian 13
|
||
- root/sudo install context
|
||
- один сервер
|
||
- одну baseline-схему
|
||
|
||
Если машина уже «жила своей жизнью», 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, содержащий:
|
||
- thin install entrypoint
|
||
- compiled orchestrator artifact
|
||
- bundled HY2XS admin
|
||
- templates
|
||
- unit files
|
||
- docs/examples
|
||
- metadata package version / build id
|
||
|
||
## Логическая модульность
|
||
|
||
Даже если на target приезжает один собранный артефакт, внутри исходников оркестратор должен быть разложен по шагам:
|
||
- preflight
|
||
- deps
|
||
- filesystem
|
||
- hysteria
|
||
- ui
|
||
- systemd
|
||
- firewall
|
||
- env
|
||
- smoke
|
||
|
||
## Что делает оркестратор по шагам
|
||
|
||
1. Проверяет, что ОС — Debian 13, и что хост чист (**до любой мутации**).
|
||
2. Проверяет базовые зависимости и install context.
|
||
3. Создаёт каталоги установки.
|
||
4. Разворачивает bundled HY2XS admin.
|
||
5. Скачивает pinned Hysteria2 binary из package metadata, проверяет SHA256 и выполняет install.
|
||
6. Генерирует Hysteria config.
|
||
7. Создаёт systemd unit для Hysteria.
|
||
8. Создаёт systemd unit для HY2XS admin.
|
||
9. Применяет nftables baseline.
|
||
10. Создаёт `post-install.env`.
|
||
11. Запускает сервисы и выполняет smoke-check.
|
||
|
||
## Модель поставки
|
||
|
||
Рекомендуемая baseline-модель:
|
||
- исходники оркестратора хранятся в `orchestrator/`
|
||
- builder выполняет локальную сборку через Bun
|
||
- в install package кладётся готовый артефакт, который запускается thin wrapper'ом
|
||
|
||
Например:
|
||
- `package/install.sh` — проверка контекста и вызов оркестратора
|
||
- `package/orchestrator/hy2xs-orchestrator` — собранный артефакт
|
||
|
||
## Логирование и коды возврата
|
||
|
||
Оркестратор должен:
|
||
- печатать понятные step-based сообщения
|
||
- завершаться ненулевым кодом при ошибке
|
||
- не скрывать первичный источник падения
|
||
- разделять preflight/config/runtime ошибки хотя бы на уровне текста
|
||
|
||
## Политика ошибок
|
||
|
||
- Любой конфликт неизвестного старого состояния = stop with error.
|
||
- Никакой сложной автомиграции.
|
||
- Ошибки должны быть текстовыми и пригодными для диагностики.
|
||
- Для install/reconfigure допустим bounded rollback при failure-сценариях firewall/systemd/config/smoke.
|
||
|
||
## 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 --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
|
||
- rollback subcommands
|
||
- uninstall subcommands
|
||
- reconcile logic
|
||
- выдачу пользовательских ключей или bot workflow
|