fix, feat, doc: massive build system refactorion, bug fixes and apex documentation (#19)

This commit is contained in:
deytenit authored and GitHub committed 2026-07-12 01:57:40 +03:00
1 parent e8b42c60c2
commit 91f5245823
165 files changed
+8598 -1205

No files matched your search

@@ -0,0 +1,24 @@
---
title: Руководства
description: Практические руководства по сборке репозиториев узлов, действий и композиций.
weight: 20
---
Практические руководства для повседневной работы с apex. Каждое руководство решает одну повторяющуюся задачу: сборка репозитория [узла](/apex/glossary#node), разработка [действий](/apex/glossary#action) для [движка](/apex/glossary#engine), определение [композиций](/apex/glossary#compositions) сервисов и эксплуатация лежащего в основе хоста.
## Доступные руководства {#available-guides}
{{< cards >}}
{{< card link="/apex/guides/node-repository" title="Репозиторий узла" subtitle="Соберите репозиторий узла, который объявляет идентичность узла, его конфигурации и композиции." >}}
{{< card link="/apex/guides/action-development" title="Разработка действий" subtitle="Пишите действия, которые движок диспетчеризует на узле." >}}
{{< card link="/apex/guides/service-composition" title="Композиция сервисов" subtitle="Определяйте композиции проектов и разворачивайте их вместе с базовой композицией." >}}
{{< card link="/apex/guides/host-operations" title="Операции с хостом" subtitle="Выполняйте рутинные операции над работающим хостом через движок." >}}
{{< /cards >}}
---
**См. также:**
- [Docs: Getting started](/apex/getting-started)
- [Docs: Concepts](/apex/concepts)
- [GitHub: apex.ermnvldmr.com](https://github.com/deytenit/apex.ermnvldmr.com)
@@ -0,0 +1,90 @@
---
title: Разработка действий
description: Пишите, переопределяйте и тестируйте действия движка в commons или proprietaries.
weight: 22
---
**Контекст:** вся автоматизация поставляется в виде [действий](/apex/glossary#action) — по одному Python-модулю на каждое, диспетчеризуемых по пути. Общее поведение размещается в [commons](/apex/glossary#commons) `actions/` («commons» — общий репозиторий); всё, что специфично для узла, размещается в `proprietaries/actions/` этого узла. Оба используют идентичный контракт, а запускатель помещает оба дерева в `sys.path`, поэтому проприетарное действие импортирует `engine.*` точно так же, как и действие из commons.
## Минимальное действие {#minimal}
```python {filename="proprietaries/actions/utils/example.py"}
"""Однострочный docstring модуля, описывающий действие."""
import os
from engine.descriptor import Meta, Arg, Flag
METADATA = Meta(
summary="Скопировать отрендеренную конфигурацию на место.",
args=[Arg("target_dir", "Каталог назначения"),
Flag("--dry-run", "Только предпросмотр.")],
)
def run(ctx, args):
if not os.path.isdir(args.target_dir):
ctx.log.error(f"Нет такого каталога: {args.target_dir}")
raise SystemExit(66)
content = ctx.tpl.render("example/config", ctx.vars())
ctx.host.write_file(os.path.join(args.target_dir, "config"), content,
dry_run=args.dry_run, sudo=True)
ctx.log.success("Конфигурация развёрнута.")
```
Путь к файлу — это его имя: модуль выше диспетчеризуется как `apex utils/example`. Верните `0`/`None` для успеха, целое число для конкретного кода выхода или возбудите `SystemExit(code)`; [соглашение о кодах выхода](/apex/concepts/actions-and-overlay#exit-codes) таково: `64` — ошибка использования, `65` — ошибка данных, `66` — отсутствующий ввод, `1` — общая ошибка.
## Соглашения для действий без надзора {#unattended}
Действия, запускаемые из cron, следуют двум правилам, чтобы сбои приводили к оповещению, а не исчезали бесследно:
- **Оберните тело** в `try/except Exception` и отправьте `ctx.notify.error(...)` перед повторным возбуждением как `SystemExit(1)` — Python-эквивалент shell-ловушки ERR.
- **Привяжите код выхода к финальному уведомлению**: `ctx.notify.*` возвращает `False` при неудачной доставке; завершённый запуск, чьё статусное сообщение так и не пришло, всё равно должен завершиться с кодом `1`.
```python
try:
...работа...
log.success("Готово.")
if not ctx.notify.success(TITLE, url, "Готово.", node):
raise SystemExit(1)
except SystemExit:
raise
except Exception as e:
log.error(f"{type(e).__name__}: {e}")
ctx.notify.error(TITLE, url, f"Критическая ошибка: {e}", node)
raise SystemExit(1)
```
## Переопределение, обёртывание, отключение {#override}
Проприетарный файл с тем же относительным путём, что и действие из commons, полностью затеняет его. Внутри затеняющего модуля `ctx.commons.run(args)` вызывает затенённую реализацию — паттерн обёртывания:
```python {filename="proprietaries/actions/sync/packages.py"}
from engine.descriptor import Meta, Arg
METADATA = Meta(summary="sync/packages с локальным для узла предварительным шагом.",
args=[Arg("telegram_bot_url", "Telegram Bot URL")])
def run(ctx, args):
ctx.log.info("Локальная для узла подготовка...")
return ctx.commons.run(args) # делегирование реализации из commons
```
Чтобы вывести действие из эксплуатации на одном узле, присвойте `DISABLED = "reason"` на верхнем уровне модуля: диспетчеризация отклонит его (код выхода `1`), а в списке отобразится `[DISABLED: reason]`.
## Тестирование {#testing}
Набор тестов commons использует stdlib `unittest`:
```bash
./tests/run.sh # python3 -m unittest discover -s tests -p 'test_*.py' -v
```
Изменения движка должны сохранять набор тестов «зелёным». Дешёвая smoke-проверка для любого действия — из commons или проприетарного — это его парсер: `apex <name> --help` должен завершиться с кодом `0`, что доказывает, что модуль разбирается, его `METADATA` корректно сформирован, и его видит механизм обнаружения.
Pre-commit-хук commons (подключаемый `init.sh` через `core.hooksPath`) запускает `apex utils/lint-docker-compose --hook` для staged-файлов compose, поэтому изменения композиций линтуются в момент коммита.
---
**См. также:**
- [Concept: Actions and the overlay](/apex/concepts/actions-and-overlay)
- [Concept: The execution context](/apex/concepts/execution-context)
- [Reference: Engine API](/apex/reference/engine-api)
@@ -0,0 +1,73 @@
---
title: Эксплуатация узла
description: "Операции day-2: конфигурирование, композиции, резервные копии и синхронизация репозитория."
weight: 24
---
**Контекст:** после первоначальной настройки [узел](/apex/glossary#node) эксплуатируется полностью через [движок](/apex/glossary#engine). Это руководство охватывает повторяющиеся операции и то, что каждая из них фактически делает на хосте.
## Конфигурирование хоста {#configure}
`apex configure` запускает полный набор конфигурирования в фиксированном порядке; каждый шаг вызывается индивидуально, когда изменился только один слой:
| Шаг | Эффект |
|---|---|
| `configure/base` | Обеспечивает базу Debian: обновление индекса пакетов, необходимые пакеты (`curl`, `wget`, `git`, `rsyslog`), запущенный rsyslog, установленный `ufw` и вспомогательный скрипт `ufw-docker` в `~/.local/bin/ufw-docker`. |
| `configure/ufw` | Применяет правила хоста, внедряет управляемые блоки правил уровня ядра, применяет правила контейнеров через `ufw-docker`. Поддерживает `--dry-run`. |
| `configure/crowdsec` | Устанавливает CrowdSec, если он отсутствует, разворачивает `configs/crowdsec/` в `/etc/crowdsec/`, устанавливает коллекции и firewall bouncer, если он настроен. Поддерживает `--dry-run`. |
| `configure/cron` | Рендерит `configs/cron/crontab` через слой шаблонов и устанавливает его — с предпросмотром, отказом при пустом рендере (код выхода `65`) и откатом при неудачной установке. Поддерживает `--dry-run`. |
| `configure/systemd` | Рендерит файлы верхнего уровня из `configs/systemd/` в `/etc/systemd/system/`, затем выполняет daemon-reload и включает каждый `.service`. Поддерживает `--dry-run`. |
| `configure/routing` | Включает IP-форвардинг и устанавливает правило и таблицу policy-routing по fwmark, используемые границей анклава; идемпотентно. |
После `configure/ufw` намеренно перезапустите `ufw` (`sudo systemctl restart ufw`) — действие напоминает об этом и предупреждает, что порт SSH уже должен быть разрешён.
## Жизненный цикл композиции {#compose}
```bash
apex compose up # сначала core, затем сервисы по алфавиту
apex compose down # обратный порядок
apex compose restart
apex compose up --dry-run # только напечатать команды
```
Дополнительные аргументы передаются в `docker compose` для каждого проекта. Файлы `.env` и `apex.env` каждого проекта подставляются автоматически, если они присутствуют.
## Резервные копии {#backups}
```bash
apex backup/run "$(cat <path-to-telegram-url-file>)"
```
Запускает [resticontainer](https://github.com/deytenit/resticontainer) в режиме one-shot под профилем compose `manual`: init-if-needed, затем управляемое метками резервное копирование, которое определяет намерение каждого сервиса из его меток compose `restic.*` (`restic.enable`, `restic.backup.paths`, …), делает снимок объединения разрешённых путей хоста в одном запуске restic (`--host` = `APEX_NODE_HOST`, `--compression max`), применяет политику хранения (`forget --prune`) и уведомляет. Он отказывается сообщать об успехе, если ни один сервис с меткой `restic.*` не создал нового снимка. Полный процесс описан в [Concept: Tiered storage](/apex/concepts/tiered-storage#backups). Запланируйте его в `configs/cron/crontab`, чтобы запуск и его результат в Telegram проходили без надзора.
## Синхронизация репозитория (capture-up) {#sync}
```bash
apex sync/repository <telegram-bot-url>
```
Коммитит рабочую копию в `sync/<node>`, выполняет rebase на `origin/main` и force-push с lease; CI сворачивает ветку обратно в `main`. «capture-up» — механизм захвата состояния узла обратно в репозиторий. Действие отказывается выполняться — с постраничной ошибкой — когда сабмодуль commons «грязный» или дерево `compositions/` отсутствует, и аккуратно прерывается при конфликте rebase (требуется ручное вмешательство). См. [Concept: Framework architecture](/apex/concepts/architecture#capture-up) для описания модели.
## Осведомлённость об обновлениях {#updates}
```bash
apex sync/packages <telegram-bot-url>
```
Сообщает об ожидающих apt-обновлениях и устаревших образах контейнеров (локальные дайджесты сравниваются с реестром через `skopeo`) в едином дайджесте Telegram. Он ничего не изменяет на хосте — это плановая проверка осведомлённости.
## Извлечение сертификатов {#certs}
```bash
apex utils/extract-traefik-certs <acme.json> <dest-dir> [telegram-bot-url]
```
Выгружает хранилище ACME граничного прокси в пары `fullchain.pem`/`privkey.pem` по каждому домену (через контейнеризованный dumper), ограничивает права на ключи до `600` и переназначает владельца вывода вызывающему пользователю — для сервисов, которым нужен «сырой» PEM-материал вместо TLS, терминируемого прокси.
---
**См. также:**
- [Getting started with an apex node](/apex/getting-started)
- [Reference: Actions](/apex/reference/actions)
- [Docs: Troubleshooting](/apex/troubleshooting)
@@ -0,0 +1,105 @@
---
title: Сборка репозитория узла
description: Соберите четыре опоры, зафиксируйте commons и подключите поток capture-up.
weight: 21
---
**Контекст:** каждый [узел](/apex/glossary#node) получает ровно один репозиторий. Выбирайте любое имя репозитория — [идентичность](/apex/glossary#node-identity) объявляется явно в `node.env`, поэтому имя репозитория — это соглашение, а не функциональный ввод. Это руководство собирает такой репозиторий с нуля.
{{% steps %}}
### Инициализируйте репозиторий и зафиксируйте commons
```bash
git init <node-repo> && cd <node-repo>
git submodule add <commons-url> commons
( cd commons && git fetch --tags && git checkout <release-tag> )
git add .gitmodules commons
mkdir -p proprietaries/actions proprietaries/lib configs compositions/apex .github/workflows
```
Gitlink фиксирует точную версию движка, которую запускает узел. Обновление в дальнейшем означает checkout более нового тега внутри `commons/` и коммит перемещённого указателя.
### Объявите идентичность
```ini {filename="node.env"}
APEX_NODE_FQDN=node1.example.com
APEX_NODE_HOST=node1.example.com
APEX_SUBNET=198.18.16.0/24
APEX_GIT_AUTHOR_NAME=apex [bot]
APEX_GIT_AUTHOR_EMAIL=apex@localhost
```
`node.env` объявляет идентичность узла явно — не предполагается никакой формы домена. `APEX_NODE_FQDN` — это реальный FQDN, используемый для маршрутизации traefik по `Host()` (fallback: имя хоста ОС, с предупреждением). `APEX_NODE_HOST` — это публичное имя хоста, используемое для `--host` резервного копирования и метки `instance` наблюдаемости/уведомлений (по умолчанию: `APEX_NODE_FQDN`); короткое `name` узла — это его первая метка. `APEX_SUBNET` — это /24 узла для сети `direct` и является **обязательным** — отсутствующее значение приводит к выходу с кодом `66` (см. [Concept: Fleet addressing scheme](/apex/concepts/addressing)). `APEX_GIT_AUTHOR_NAME`/`APEX_GIT_AUTHOR_EMAIL` — это идентичность бота capture-up (общие значения по умолчанию, если не заданы).
### Напишите базовую обёртку и её env-файлы
```yaml {filename="compositions/apex/docker-compose.yml"}
name: apex
include:
- ../../commons/compositions/apex/docker-compose.yml
```
```ini {filename="compositions/apex/apex.env"}
APEX_TRAEFIK_DIRECT_IP=198.18.16.10
APEX_RESTIC_REPOSITORY=s3:<endpoint>/<bucket>/<node>
COMPOSE_PROFILES=adguard,xray,alloy
# цели push-наблюдаемости (только когда включён профиль alloy)
APEX_PROM_PUSH_URL=https://<central-host>/api/v1/push
APEX_LOKI_PUSH_URL=https://<central-host>/loki/api/v1/push
APEX_OBS_BASIC_AUTH_USER=<node-user>
# опциональные скаляры со значениями compose по умолчанию:
# TZ=Etc/UTC
# APEX_RESTIC_COMPRESSION=max
# APEX_RESTIC_AWS_REGION=us-east-1 # задайте для любого не-AWS S3-хранилища
# APEX_RESTIC_AWS_ENDPOINT= # напр., URL S3-совместимого эндпоинта
```
`apex.env` содержит только коммитируемые несекретные скаляры. `COMPOSE_PROFILES` выбирает, какие опциональные базовые сервисы запускает этот узел — `adguard`, `xray`, `alloy` (агент push-наблюдаемости) и `manual` (одноразовые резервные копии) — и может быть пустым. Задокументируйте некоммитируемые секретные ключи в `compositions/apex/SECRETS.md`, чтобы оператор на свежем хосте знал, что заполнить в `.env` (`APEX_RESTIC_PASSWORD`, ключи доступа S3, `APEX_TRAEFIK_CF_DNS_API_TOKEN`, `APEX_TRAEFIK_ACME_EMAIL` и `APEX_OBS_BASIC_AUTH_PASS`, когда наблюдаемость включена).
### Игнорируйте то, чему никогда не место в git
```gitignore {filename=".gitignore"}
**/@tier1
**/@tier2
**/@tier3
**/.env
!compositions/apex/apex.env
__pycache__/
*.pyc
```
### Добавьте bootstrap-скрипт
`init.sh` подготавливает свежий клон на хосте: `git submodule update --init --recursive`, `git config core.hooksPath commons/githooks`, `git config --global --add safe.directory <repo>` и добавляет `<repo>/commons` в начало `PATH` в login-shell, чтобы `apex` разрешался. Пометьте его исполняемым.
### Заведите реестр адресации
Создайте `compositions/ADDRESSING.md` с подсетью узла (`APEX_SUBNET`) и зарезервированными диапазонами (`.10`–`.19` — core, `.20`+ — сервисы). Каждый [якорный IP](/apex/glossary#anchor-ip), выделяемый позднее, записывается здесь.
### Подключите CI capture-up
Добавьте плановый workflow, который сливает `sync/<node>` в `main`, когда ветка существует, коммитя под идентичностью бота. Выполняйте checkout **без** сабмодулей — слияние лишь перемещает указатель commons, а submodule-URL по SSH не извлекается с CI-токеном по умолчанию. См. [Concept: Framework architecture](/apex/concepts/architecture) для полного цикла [capture-up](/apex/glossary#capture-up).
### Проверьте
```bash
./commons/apex --help
```
В списке должно отображаться каждое действие из commons (плюс ваши proprietaries, после их добавления). Вне хоста — до заполнения `node.env` — ожидайте безобидное предупреждение о том, что `APEX_NODE_FQDN` не задан (движок откатывается к имени хоста ОС); `APEX_SUBNET` при этом всё равно должен быть задан, иначе движок завершится с кодом `66`. Закоммитьте собранный репозиторий в `main`.
{{% /steps %}}
## Заметки
- Опора `proprietaries/` может начинаться пустой (сохраняйте её присутствие с помощью файлов `.gitkeep`); движок корректно обрабатывает отсутствующий оверлей.
- Переносите источники конфигурации хоста в `configs/` (`ufw/`, `cron/crontab`, `systemd/`, `crowdsec/`) по мере того как узел принимает каждый шаг `configure/*` — действия потребляют `configs/` дословно через слой шаблонов.
---
**См. также:**
- [Getting started with an apex node](/apex/getting-started)
- [Concept: Framework architecture](/apex/concepts/architecture)
- [Guide: Add a service composition](/apex/guides/service-composition)
@@ -0,0 +1,101 @@
---
title: Добавление композиции сервисов
description: "Подключите новый проект к узлу: якорный IP, сети, уровни и линтинг."
weight: 23
---
**Контекст:** проект присоединяется к [узлу](/apex/glossary#node) как один каталог в `compositions/`, содержащий написанный вручную файл compose. Контракт фреймворка для этого файла невелик: статический [якорный IP](/apex/glossary#anchor-ip) в сети `direct`, ссылки на внешние сети по ключу и пути к данным относительно уровней.
{{% steps %}}
### Выделите якорный адрес
Выберите следующий свободный адрес `.20` или выше в /24 узла и запишите его в `compositions/ADDRESSING.md`. Адреса `.10`–`.19` зарезервированы для [базовой композиции](/apex/glossary#core-composition) (см. [Concept: Fleet addressing scheme](/apex/concepts/addressing)).
### Напишите файл compose
```yaml {filename="compositions/<project>/docker-compose.yml"}
name: <project>
services:
<anchor>:
image: ...
container_name: <project>-<anchor>
user: "${APEX_UID?error}:${APEX_GID?error}"
group_add:
- "${APEX_SHARED_GID?error}"
volumes:
- ./@tier1/<anchor>/config:/config
- ./@tier2/<anchor>/data:/data
networks:
direct:
ipv4_address: 198.18.16.20
enclave: {}
labels:
- "traefik.enable=true"
- "traefik.http.routers.<project>.rule=Host(`...`)"
- "traefik.http.services.<project>.loadbalancer.server.port=8080"
# намерение резервного копирования (resticontainer, определяется из меток restic.*)
- "restic.enable=true"
- "restic.backup.paths=/config"
restart: unless-stopped
<sidecar>:
image: ...
network_mode: "service:<anchor>"
networks:
direct:
external: true
enclave:
external: true
```
Применяемые соглашения:
- **Один якорь на проект** держит статический `ipv4_address` в `direct`; sidecar-контейнеры разделяют его сетевое пространство имён через `network_mode: service:<anchor>`.
- **На сети ссылаются по ключу** (`direct`, `enclave`, `socket`) с `external: true` — их определения принадлежат базовой композиции.
- **Членство в `enclave` плюс метки traefik** публикуют сервис через граничный прокси; прокси обнаруживает его через socket-прокси, никогда через опубликованные порты.
- **Пути к данным относительны уровням** (`./@tierN/...`) и разрешаются через симлинки, создаваемые `tiers/link`.
- **Резервное копирование включается по выбору через метки `restic.*`.** `apex backup/run` управляет [resticontainer](https://github.com/deytenit/resticontainer), который определяет, что снимать, из меток compose: `restic.enable=true` помечает сервис, `restic.backup.paths=<container-path>` называет монтирования для резервного копирования (resticontainer разрешает их в пути хоста). Сервис без меток `restic.*` просто не резервируется. Резервируйте конфигурацию только там, где данные восстановимы.
- Тройка `${APEX_UID...}` берётся из `.env` проекта, поддерживаемого `tiers/useradd`.
### Обеспечьте хранилище и идентичность
```bash
apex tiers/link <tier1> <tier2> <tier3>
apex tiers/useradd
apex tiers/chown
```
Повторный запуск тройки идемпотентен; он подхватывает новый проект, создаёт его каталоги уровней и `.env`, обеспечивает его пользователя `noroot-<project>` и применяет матрицу владения.
### Линтинг
```bash
apex utils/lint-docker-compose
```
Проверяет каждый якорный IP (корректность формы, уникальность, нахождение внутри подсети узла) и запускает линтер compose по репозиторию. Pre-commit-хук обеспечивает тот же линтинг для staged-файлов compose.
### Разверните
```bash
apex compose up
```
Базовая композиция уже поднята; новый проект стартует в своём алфавитном слоте. Проверьте с помощью `docker ps` и, если сервис проксируется, через его маршрут на границе.
{{% /steps %}}
## Заметки
- Внутренние секреты сервиса (например, пароль базы данных) размещаются в собственном `.env` проекта в хранилище уровня 1, рядом с автоматически внедряемым блоком UID.
- Никогда не определяйте `direct`/`enclave`/`socket` внутри файла проекта — второе определение будет конкурировать с базовой композицией за владение сетью.
---
**См. также:**
- [Concept: Compositions and the shared core](/apex/concepts/compositions)
- [Concept: Tiered storage and privilege isolation](/apex/concepts/tiered-storage)
- [Concept: Fleet addressing scheme](/apex/concepts/addressing)