refactor: consolidate migration installer

This commit is contained in:
3252a8
2026-06-02 10:57:28 +03:00
parent 0473829f7a
commit 2ccead9b49
7 changed files with 162 additions and 814 deletions
+2 -2
View File
@@ -43,8 +43,8 @@ Remnawave Minishop - Telegram-бот и Web App (Mini App) для продажи
- [Telegram-авторизация](docs/features/telegram-auth.md) и [вход по email](docs/features/email-login.md) - настройка BotFather/OAuth и SMTP-логина.
- [Поддержка пользователей / тикеты](docs/features/support.md) - тикеты в Mini App, входящий список админки, уведомления, лимиты и внешняя ссылка поддержки.
- [Темы Web App](docs/features/webapp-themes.md) - кастомные темы, настройка внешнего вида, логотипы, CSS/ассеты и пайплайн создания новой темы.
- [Миграции](docs/migrations/index.md) - готовые сценарии переноса с других ботов; сейчас описан `remnawave-tg-shop`.
- [Миграция с remnawave-tg-shop](docs/migrations/remnawave-tg-shop.md) - готовый сценарий для legacy-стека.
- [Миграции](docs/migrations/index.md) - готовые сценарии переноса с `remnawave-tg-shop` и Remnashop.
- [Миграция с remnawave-tg-shop](docs/migrations/remnawave-tg-shop.md) и [Remnashop](docs/migrations/remnashop.md) - сценарии через общий install wizard.
## Совместимость
+4 -4
View File
@@ -1,6 +1,6 @@
# Миграция из Remnashop
Remnashop импортируется через общий legacy-importer `backend/scripts/import_legacy.py`.
Remnashop импортируется через общий скрипт импорта `backend/scripts/import_legacy.py`.
Самый удобный путь - интерактивный install wizard:
```bash
@@ -8,8 +8,8 @@ curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scri
sh install.sh
```
В меню выберите `Install new stack and run legacy migration` для нового сервера
или `Run legacy migration only`, если compose-папка и `.env` уже готовы.
В меню выберите `Install new stack and run migration` для нового сервера
или `Run migration only`, если compose-папка и `.env` уже готовы.
## Что переносится
@@ -21,7 +21,7 @@ sh install.sh
- служебные mappings, чтобы повторный запуск мог работать в режиме `merge`;
- настройки совместимости Remnashop в админке: старые ref-ссылки и promo codes.
Данные, которые не имеют прямого аналога, сохраняются в legacy mappings или
Данные, которые не имеют прямого аналога, сохраняются в служебных таблицах миграции или
message logs как заметки, чтобы администратор мог проверить их после переноса.
## Flow wizard
+94 -353
View File
@@ -1,117 +1,60 @@
# Миграция с `remnawave-tg-shop` (≤ v2.7.0) на `remnawave-minishop` (v3.4+)
# Миграция с `remnawave-tg-shop` на `remnawave-minishop`
Эта страница - готовый сценарий для legacy-стека `remnawave-tg-shop`. Это единственная миграция с другого бота, которая сейчас описана в документации. Для других Telegram-ботов, самописных панелей и ручных таблиц готового сценария пока нет: их нельзя переносить по этой инструкции без отдельного анализа схемы БД, тарифов, платежей и связи с Remnawave Panel.
Автоматический скрипт ниже рассчитан именно на родственный стек `remnawave-tg-shop`, где структура БД и Docker volumes известны заранее. Для других ботов нужен отдельный адаптер экспорта/импорта.
## Новый install wizard
Для нового сервера или переноса без клонирования репозитория используйте общий
`sh` wizard:
Для переноса со старого родственного стека используйте общий install wizard:
```bash
curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/install.sh -o install.sh
sh install.sh
```
В меню выберите `Install new stack and run legacy migration` или
`Run legacy migration only`, затем источник `Old remnawave-tg-shop`.
Wizard поддерживает два режима:
В меню выберите `Install new stack and run migration` для нового
сервера или `Run migration only`, если compose-папка уже готова. Затем
выберите источник `Old remnawave-tg-shop`.
- `Copy old Docker volumes` - тот же безопасный сценарий, что и старый
`migrate_to_minishop.sh`: старый volume `remnawave-tg-shop-db-data`
копируется в `remnawave-minishop-db-data`, затем новый stack запускает
сервис `migrate` и накатывает все схемные миграции;
- `Dump from a source PostgreSQL DSN` - новый режим для случаев, когда старая
БД доступна как внешний PostgreSQL DSN. Wizard поднимает целевой `postgres`,
сбрасывает целевую БД, делает `pg_dump` из старой БД, восстанавливает дамп в
compose-БД и затем запускает `migrate`.
Wizard поддерживает два способа переноса:
Старый helper `scripts/migrate_to_minishop.sh` ниже всё ещё полезен для
in-place обновления уже клонированного репозитория: он переключает git-ветку,
переносит volumes и стартует новый stack. Прямой source DSN он не поддерживал.
- `Copy old Docker volumes` - для старого compose-стека на том же Docker host.
Скрипт подготавливает новый stack, копирует
`remnawave-tg-shop-db-data` в `remnawave-minishop-db-data`, опционально
переносит Caddy volumes и запускает новый stack.
- `Dump from a source PostgreSQL DSN` - для старой БД, доступной по DSN.
Скрипт поднимает целевой `postgres`, сбрасывает целевую БД, делает
`pg_dump` из старой БД, восстанавливает дамп в compose-БД и запускает
сервис `migrate`.
## Короткий путь без смены ветки и сборки
В обоих режимах старые volumes и старая БД не удаляются автоматически.
Если вы используете только готовые Docker-образы и не собираете проект
локально, git-команды из ручного способа не нужны. Достаточно обновить
compose-файл до одного из готовых примеров в `deploy/examples` и
перенести/обновить БД. Самый прямой вариант без встроенного обратного прокси -
`deploy/examples/no-proxy/docker-compose.yml`; для Caddy, Nginx и Newt есть
такие же самостоятельные папки.
## Как работает перенос
Минимальная последовательность:
`remnawave-tg-shop` и `remnawave-minishop` имеют совместимую историю схемы.
После переноса старой PostgreSQL-БД сервис `migrate` накатывает недостающие
миграции из `backend/db/migrator.py`: сначала применяются `Base.metadata`,
затем последовательные записи `schema_migrations`. Это one-shot сервис: он
должен завершиться с кодом `0`, после чего стартуют `backend` и `worker`.
```bash
docker compose down
При volume-миграции wizard:
# Скопируйте старый .env в выбранную папку примера и обновите значения там.
cp .env deploy/examples/no-proxy/.env
nano deploy/examples/no-proxy/.env
1. Останавливает известные контейнеры старого и переходного стеков, если вы
подтверждаете этот шаг.
2. Запускает `docker compose up --no-start`, чтобы Docker Compose создал новые
volumes.
3. Копирует старый volume БД:
# Подготовьте стек из готовых образов.
IMAGE_TAG=3.4.0 docker compose \
--env-file deploy/examples/no-proxy/.env \
-f deploy/examples/no-proxy/docker-compose.yml \
up --no-start
```bash
docker run --rm \
-v remnawave-tg-shop-db-data:/from:ro \
-v remnawave-minishop-db-data:/to \
alpine sh -c "cd /from && cp -a . /to"
```
# Нужно только при переходе со старого имени volume remnawave-tg-shop-db-data.
# Если у вас уже есть remnawave-minishop-db-data, этот шаг пропустите.
docker run --rm \
-v remnawave-tg-shop-db-data:/from:ro \
-v remnawave-minishop-db-data:/to \
alpine sh -c "cd /from && cp -a . /to"
4. Если старые Caddy volumes существуют, переносит
`remnawave-tg-shop-caddy-data` -> `remnawave-minishop-caddy-data` и
`remnawave-tg-shop-caddy-config` -> `remnawave-minishop-caddy-config`.
5. Запускает новый stack через Docker Compose.
IMAGE_TAG=3.4.0 docker compose \
--env-file deploy/examples/no-proxy/.env \
-f deploy/examples/no-proxy/docker-compose.yml \
up -d
docker compose \
--env-file deploy/examples/no-proxy/.env \
-f deploy/examples/no-proxy/docker-compose.yml \
logs migrate
```
Сервис `migrate` сам применит недостающие схемные миграции к перенесённому
тому PostgreSQL. Новые тома `remnawave-minishop-redis-data` и
`remnawave-minishop-shop-data` переносить не нужно: они создаются пустыми.
Этот документ описывает обновление стека, поднятого по `remnawave-tg-shop`
(включая последний релиз `v2.7.0` форка `kavore/remnawave-tg-shop`), до
текущей версии `remnawave-minishop` (v3.4+). Между этими версиями произошли
две независимые перетряски, и скрипт пытается отработать обе одной командой:
1. **Переименование стека** (v3.1.0): контейнеры и тома `remnawave-tg-shop-*`
стали `remnawave-minishop-*`. Простой `docker compose up -d` после
`git pull` создаёт пустую БД — без переноса тома данные теряются.
2. **Разделение бота на сервисы** (v3.4.0): из одного контейнера выделены
`backend`, `worker`, `frontend`, `migrate` + новые `postgres`, `redis`.
Появились новые volumes `redis-data` и `shop-data`, новые обязательные
переменные окружения, а схема БД обновляется автоматически one-shot
сервисом `migrate`.
После миграции `docker compose ps` должен показать как минимум: `backend`,
`worker`, `frontend`, `postgres`, `redis` (running) и `migrate` (exited 0).
Логи: `docker compose logs -f backend worker frontend`.
Доступные пути:
- [Автоматический](#автоматический-способ-через-скрипт) — скрипт-обёртка
останавливает старый стек, накатывает свежий код, переносит том БД,
поднимает новые сервисы. Идемпотентный.
- [Ручной](#ручной-способ) — те же шаги командами, для тех, кому нужно
понимать каждое действие или выполнить выборочно.
В обоих случаях:
- старые тома **не удаляются** автоматически — это безопасный бэкап на случай
отката;
- сертификаты Caddy (если используется `deploy/examples/caddy/docker-compose.yml`)
тоже переносятся, чтобы Let's Encrypt не выписывал их заново и не упереться
в rate limit;
- схема БД обновляется автоматически: при первом `docker compose up -d` сервис
`migrate` накатывает на перенесённый том все недостающие миграции (от
alembic-схемы v2.7.0 до текущей).
Если целевой DB volume уже непустой, wizard не перетирает его молча: он
останавливается и просит отдельное подтверждение на продолжение без копирования
старой БД.
## Что меняется в архитектуре
@@ -120,256 +63,55 @@ docker compose \
| Версия | Сервисы |
| --- | --- |
| `v2.7.0` | `remnawave-tg-shop`, `remnawave-tg-shop-db` |
| `v3.1.xv3.3.x` | `remnawave-minishop`, `remnawave-minishop-db` |
| `v3.4+` (текущая) | `remnawave-minishop-backend`, `remnawave-minishop-worker`, `remnawave-minishop-frontend`, `remnawave-minishop-migrate`, `remnawave-minishop-postgres`, `remnawave-minishop-redis` |
Внутри Docker-сети сервисы доступны по коротким DNS-именам (`backend`, `worker`,
`frontend`, `postgres`, `redis`), а не по полному `container_name`. Это важно
для внешнего reverse-proxy — см. раздел [Внешний reverse-proxy](#внешний-reverse-proxy) ниже.
| `v3.1.x-v3.3.x` | `remnawave-minishop`, `remnawave-minishop-db` |
| `v3.4+` | `remnawave-minishop-backend`, `remnawave-minishop-worker`, `remnawave-minishop-frontend`, `remnawave-minishop-migrate`, `remnawave-minishop-postgres`, `remnawave-minishop-redis` |
**Volumes**:
| Volume | v2.7.0 | v3.4+ | Что внутри |
| --- | --- | --- | --- |
| `remnawave-minishop-db-data` | переименовать из `remnawave-tg-shop-db-data` | переносится скриптом | PostgreSQL |
| `remnawave-minishop-redis-data` | — | создаётся пустым | Redis (FSM, rate-limit, cache, очередь вебхуков, distributed locks) |
| `remnawave-minishop-shop-data` | — | создаётся пустым | `/app/data`: `tariffs.json`, темы Web App, кэш логотипа/emoji |
| `remnawave-minishop-caddy-data` / `remnawave-minishop-caddy-config` | переименовать из `remnawave-tg-shop-caddy-*` | переносится скриптом | только при Caddy-варианте |
| Volume | Что происходит |
| --- | --- |
| `remnawave-minishop-db-data` | переносится из `remnawave-tg-shop-db-data` или восстанавливается из source DSN |
| `remnawave-minishop-redis-data` | создается пустым |
| `remnawave-minishop-shop-data` | создается пустым; runtime-файлы в `/app/data` дальше настраиваются через админку или вручную |
| `remnawave-minishop-caddy-data` / `remnawave-minishop-caddy-config` | переносятся из `remnawave-tg-shop-caddy-*`, если старый стек использовал Caddy |
`redis-data` и `shop-data` стартуют пустыми — это нормально. Redis ничего
долгоживущего не хранит (всё либо FSM, либо кеш с TTL), а `data/` инициализируется
из образа при первом старте (`tariffs.json` пуст пока вы не сконфигурируете
тарифы через админ-панель).
Доступные compose-профили: `docker-compose.yml`,
`deploy/examples/caddy/docker-compose.yml`,
`deploy/examples/nginx/docker-compose.yml`,
`deploy/examples/newt/docker-compose.yml`,
`deploy/examples/no-proxy/docker-compose.yml`.
## Переменные окружения, которые могли исчезнуть или переехать
## Переменные окружения
Перед запуском нового стека проверьте `.env`. Ниже — только то, что точно
менялось между v2.7.0 и v3.4+:
Перед запуском нового стека проверьте `.env`. Самые важные изменения:
| Было (v2.7.0) | Стало (v3.4+) | Действие |
| Было | Стало | Действие |
| --- | --- | --- |
| `TELEGRAM_WEBHOOK_SECRET` | `WEBHOOK_SECRET_TOKEN` | Переименовать. Если пусто — будет сгенерирован при старте, но тогда Telegram переустановит webhook (на это не реагирует существующий запрос). |
| `TELEGRAM_WEBHOOK_PATH` | удалена | Путь вебхука теперь генерируется из `BOT_TOKEN` автоматически. |
| `REQUIRED_CHANNEL_SUBSCRIBE_TO_USE` | удалена | Гейт включается автоматически, как только задан `REQUIRED_CHANNEL_ID`. |
| `STARS_PROVIDER_TOKEN` | удалена | Telegram Stars (XTR) используются напрямую. |
| `REFERRAL_ENABLED` | удалена | Реферальная программа активна по умолчанию. В legacy-режиме без JSON-каталога отключайте платежные бонусы через нули в `REFERRAL_BONUS_DAYS_*` и `REFEREE_BONUS_DAYS_*`; в JSON-тарифах обнуляйте или удаляйте `referral_bonus_days_inviter` и `referral_bonus_days_referee` у period-тарифов. |
| `POSTGRES_HOST=remnawave-tg-shop-db` | в `.env``remnawave-minishop-db` или пусто | Под compose значение всё равно переопределяется на сервисное имя `postgres` (см. `environment:` в compose-файлах), поэтому скрипт правит `.env` только для bare-metal сценариев. |
| `WEBHOOK_BASE_URL` | **обязательна** | Polling-режим удалён, без публичного URL бот не стартует. |
| | `REDIS_URL=redis://redis:6379/0` | Обязательна для воркера, очередей и rate-limit. По умолчанию в compose-файлах уже задана. |
| — | `WEBAPP_SESSION_SECRET`, `WEBAPP_ENABLED`, `WEBAPP_SERVER_PORT`, `WEBAPP_THEMES_DIR`, `TARIFFS_CONFIG_PATH` | Новые настройки Web App / тарифного каталога. Безопасные дефолты есть в `.env.example`. |
| `TELEGRAM_WEBHOOK_SECRET` | `WEBHOOK_SECRET_TOKEN` | Перенести значение или сгенерировать новый stable secret. |
| `TELEGRAM_WEBHOOK_PATH` | удалена | Путь вебхука теперь рассчитывается автоматически. |
| `REQUIRED_CHANNEL_SUBSCRIBE_TO_USE` | удалена | Гейт включается, когда задан `REQUIRED_CHANNEL_ID`. |
| `STARS_PROVIDER_TOKEN` | удалена | Telegram Stars используются напрямую. |
| `POSTGRES_HOST=remnawave-tg-shop-db` | `postgres` внутри Compose | В compose-файлах `POSTGRES_HOST` переопределяется service name `postgres`. |
| `WEBHOOK_BASE_URL` | обязательна | Без публичного URL backend не стартует корректно. |
| - | `REDIS_URL=redis://redis:6379/0` | В compose-профилях задано автоматически. |
| - | `WEBAPP_SESSION_SECRET`, `WEBAPP_ENABLED`, `TARIFFS_CONFIG_PATH` | Новые настройки Web App и каталога тарифов. |
Полный референс — [docs/getting-started/configuration.md](../getting-started/configuration.md). Скрипт миграции
эти переменные **не правит** автоматически (только `POSTGRES_HOST`), потому
что у каждой инсталляции свой шаблон `.env` с кастомными значениями. Лучше
сравнить свой `.env` с `.env.example` глазами один раз, чем получить
несовместимый шаблон автоматом.
Остальные продуктовые настройки удобнее проверить после первого входа в
админку.
## Автоматический способ (через скрипт)
## Reverse Proxy
Если helper ещё не лежит у вас локально, запускайте его прямо из `raw` из
корня старого репозитория:
В старом стеке часто был один upstream `remnawave-tg-shop:8000`. В текущем
split-arch stack маршруты разделены:
```bash
bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh)
```
> Команда выше рассчитана на `bash` / Git Bash / WSL. Если вы запускаете из
> PowerShell, удобнее сначала открыть Git Bash.
Если вы уже подтянули новую версию и файл есть локально, можно запускать так:
```bash
bash scripts/migrate_to_minishop.sh
```
По умолчанию скрипт работает с `docker-compose.yml` и переключается на ветку
`main`. Можно переопределить через переменные окружения:
| Переменная | Назначение | По умолчанию |
| ----------------- | ----------------------------------------------------------------------- | ---------------------- |
| `PROJECT_ROOT` | Явный путь к корню старого репозитория, если запуск не из него | текущая директория |
| `COMPOSE_FILE` | Какой compose-файл стартовать в конце | `docker-compose.yml` |
| `TARGET_BRANCH` | На какую ветку переключаться и подтягивать обновления | `main` |
| `GIT_REMOTE` | Какой remote использовать для `fetch`/`pull` | `origin` |
| `NEW_ORIGIN_URL` | Если задано и не совпадает с URL выбранного remote — он будет обновлён | (не меняется) |
| `ASSUME_YES` | `1` — не задавать интерактивных вопросов | `0` |
Примеры:
```bash
# Caddy-вариант из raw-файла.
# Перед запуском скопируйте старый .env в deploy/examples/caddy/.env
# и заполните WEBHOOK_HOST / MINIAPP_HOST.
COMPOSE_FILE=deploy/examples/caddy/docker-compose.yml \
bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh)
# С переключением origin на форк 3252a8
NEW_ORIGIN_URL=https://github.com/3252a8/remnawave-minishop.git \
bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh)
# Без интерактива
ASSUME_YES=1 \
bash <(curl -fsSL https://raw.githubusercontent.com/3252a8/remnawave-minishop/main/scripts/migrate_to_minishop.sh)
```
Что делает скрипт:
1. **Останавливает текущий стек**: ищет известные контейнеры старой схемы
(`remnawave-tg-shop`, `…-db`, `…-caddy`), переходного периода
(`remnawave-minishop`, `…-db`, `…-caddy`) и новой схемы
(`…-backend`, `…-worker`, `…-frontend`, `…-migrate`, `…-postgres`, `…-redis`)
и останавливает их, если запущены. Безопасно при повторном запуске.
2. **Переключает `origin`**, если задана переменная `NEW_ORIGIN_URL`, иначе
оставляет как есть.
3. **Подтягивает целевую ветку** (`git fetch` + `git switch` + `git pull --ff-only`).
Прерывается, если в рабочем дереве есть незакоммиченные изменения.
4. **Правит `POSTGRES_HOST` в `.env`** (только для bare-metal сценариев — в
compose это значение перебивает `environment:` блок).
5. **Подготавливает новый стек в режиме `--no-start`**, чтобы Compose сам
создал тома `db-data`, `redis-data`, `shop-data` и не ругался на уже
существующий volume.
6. **Переносит том БД** `remnawave-tg-shop-db-data``remnawave-minishop-db-data`
(и Caddy-тома, если применимо) через одноразовый `alpine`-контейнер. Если
новый том уже непустой — копирование пропускается. Новые volumes
`redis-data` и `shop-data` остаются пустыми (их и не должно быть в старом
стеке).
7. **Стартует новый стек** (`docker compose up -d --remove-orphans` плюс
`--build` для локальной сборки). `migrate` отработает первым, накатит
на перенесённый том все недостающие миграции (от alembic-схемы v2.7.0 до
текущей) и завершится. Затем стартуют `backend`, `worker`, `frontend`.
Скрипт идемпотентен: повторный запуск ничего не сломает, просто пропустит уже
выполненные шаги.
После того как убедитесь, что бот работает и данные на месте, удалите старые
тома:
```bash
docker volume rm remnawave-tg-shop-db-data
docker volume rm remnawave-tg-shop-caddy-data remnawave-tg-shop-caddy-config 2>/dev/null || true
```
## Ручной способ
1. **Остановите старый стек и обновите код:**
```bash
docker compose down
git fetch origin
git checkout main
git pull --ff-only origin main
```
2. **(Только для bare-metal без compose)** обновите `.env`, если в нём ещё
жёстко прописан старый контейнер БД:
```bash
sed -i.bak 's/^POSTGRES_HOST=remnawave-tg-shop-db$/POSTGRES_HOST=remnawave-minishop-db/' .env
```
Под `docker compose up` это не нужно: compose сам выставляет
`POSTGRES_HOST: postgres` (имя сервиса) в `environment:` и `.env`-значение
не используется.
3. **Проверьте `.env`** на наличие переменных, которые исчезли или
переименовались — см. раздел
[Переменные окружения](#переменные-окружения-которые-могли-исчезнуть-или-переехать)
выше. Главное: `WEBHOOK_SECRET_TOKEN` (бывший `TELEGRAM_WEBHOOK_SECRET`),
обязательный `WEBHOOK_BASE_URL` и наличие `REDIS_URL` (по умолчанию задано
в compose).
4. **Подготовьте новый стек без запуска**, чтобы Compose создал новые volumes
(`db-data`, `redis-data`, `shop-data`) и контейнеры:
```bash
# Локальная сборка
docker compose up --no-start --build
# Или готовый Caddy-вариант из GHCR-образов
cp .env deploy/examples/caddy/.env
nano deploy/examples/caddy/.env
docker compose \
--env-file deploy/examples/caddy/.env \
-f deploy/examples/caddy/docker-compose.yml \
up --no-start
# Другие готовые варианты:
# deploy/examples/nginx/docker-compose.yml
# deploy/examples/newt/docker-compose.yml
# deploy/examples/no-proxy/docker-compose.yml
```
5. **Перенесите том БД в новое имя:**
```bash
docker run --rm \
-v remnawave-tg-shop-db-data:/from:ro \
-v remnawave-minishop-db-data:/to \
alpine sh -c "cd /from && cp -a . /to"
```
`remnawave-minishop-redis-data` и `remnawave-minishop-shop-data` — новые,
переносить нечего. Они инициализируются на лету: Redis пуст, а `data/`
наполняется при первом обращении к настройкам Web App / каталогу тарифов.
6. **(Только для Caddy)** перенесите тома Caddy с TLS-сертификатами и
состоянием ACME:
```bash
for v in caddy-data caddy-config; do
docker run --rm \
-v "remnawave-tg-shop-$v":/from:ro \
-v "remnawave-minishop-$v":/to \
alpine sh -c "cd /from && cp -a . /to"
done
```
7. **Запустите новый стек:**
```bash
docker compose up -d
# или
docker compose \
--env-file deploy/examples/caddy/.env \
-f deploy/examples/caddy/docker-compose.yml \
up -d
```
Сервис `migrate` запустится первым, обнаружит перенесённый том,
применит недостающие схемные миграции (`Base.metadata.create_all` +
последовательные миграции `0001..00NN` из `backend/db/migrator.py`) и
выйдет с кодом 0. Только после этого стартуют `backend` и `worker`.
8. **Проверьте состояние:**
```bash
docker compose ps
docker compose logs -f backend worker frontend
docker compose logs migrate # должен закончиться "Migrator: migration 00NN applied successfully"
```
9. **(Опционально) удалите старые тома**, когда убедитесь, что новый стек
стабилен:
```bash
docker volume rm remnawave-tg-shop-db-data
docker volume rm remnawave-tg-shop-caddy-data remnawave-tg-shop-caddy-config 2>/dev/null || true
```
## Внешний reverse-proxy
В v2.7.0 был один upstream — `remnawave-tg-shop:8000`. В v3.4+ функциональность
разнесена по портам и сервисам:
| Назначение | DNS-имя сервиса | Порт |
| Назначение | Service | Port |
| --- | --- | --- |
| Telegram / платежные / вебхуки панели | `backend` | `8080` |
| Health-чек | `backend` | `8080` (`/healthz`) |
| Web App API (`/api/*`, `/auth/*`, ассеты тем и логотипов) | `backend` | `8081` (доступен только из Docker-сети) |
| Статический фронт Web App | `frontend` | `80` (внутри `frontend` уже проксирует `/api/*` и `/auth/*` на `backend:8081`) |
| Telegram, платежные и panel webhooks | `backend` | `8080` |
| Health-check | `backend` | `8080` (`/healthz`) |
| Web App API и auth | `backend` | `8081` внутри Docker-сети |
| Статический Web App frontend | `frontend` | `80` |
Минимальная замена для внешнего Nginx, который раньше слал всё на один
upstream:
Минимальная схема для внешнего Nginx:
```nginx
upstream remnawave_backend_webhooks { server backend:8080; }
@@ -378,33 +120,32 @@ upstream remnawave_frontend { server frontend:80; }
server {
server_name app.domain.com;
listen 443 ssl;
http2 on;
# ssl_certificate / ssl_certificate_key — без изменений
location /webhook/ { proxy_pass http://remnawave_backend_webhooks; }
location /healthz { proxy_pass http://remnawave_backend_webhooks; }
location / { proxy_pass http://remnawave_frontend; }
location / { proxy_pass http://remnawave_frontend; }
}
```
Полные примеры (Caddy, Nginx, Newt/Pangolin и запуск без обратного прокси) — в
[docs/getting-started/deployment.md](../getting-started/deployment.md) и [docs/features/web-app.md](../features/web-app.md). Если раньше прокси указывал на
`remnawave-tg-shop:8000` напрямую, после миграции нужно либо переключиться на
`backend:8080` / `frontend:80`, либо использовать готовый Caddy/Nginx/Newt
пример, который уже знает правильную маршрутизацию.
Готовые Caddy, Nginx, Pangolin/Newt и no-proxy профили уже содержат нужную
маршрутизацию.
## Если что-то пошло не так
## Проверка
`migrate` упал → читайте `docker compose logs migrate`. Том БД остался
не тронут, можно откатиться, переключив compose-файл обратно на старый
коммит и подняв старый стек на старом томе `remnawave-tg-shop-db-data`
(пока вы его не удалили).
После переноса:
`backend` не стартует → чаще всего `WEBHOOK_BASE_URL` пуст, либо
`WEBHOOK_SECRET_TOKEN` отличается от того, что Telegram ждёт. Поставьте
свежий секрет в `.env` и перезапустите — Telegram переустановит webhook
автоматически.
```bash
docker compose ps
docker compose logs migrate
docker compose logs -f backend worker frontend
```
Web App пуст / 502 → проверьте, что `frontend` живёт (`docker compose ps`),
а внешний прокси шлёт на `frontend:80`, а не на старый
`remnawave-tg-shop:8000`.
`migrate` должен завершиться успешно, а `backend`, `worker`, `frontend`,
`postgres` и `redis` должны быть running/healthy.
Когда убедитесь, что новый stack работает, старые volumes можно удалить вручную:
```bash
docker volume rm remnawave-tg-shop-db-data
docker volume rm remnawave-tg-shop-caddy-data remnawave-tg-shop-caddy-config 2>/dev/null || true
```
+4 -5
View File
@@ -939,7 +939,7 @@ run_remnashop_migration() {
if confirm "Restart backend and worker so setting overrides are reloaded?" 1; then
(cd "$TARGET_DIR" && run_compose restart backend worker) || true
fi
ok "Legacy migration completed."
ok "Migration completed."
}
run_target_schema_migrations() {
@@ -980,11 +980,10 @@ run_tgshop_volume_migration() {
run_tgshop_dsn_migration() {
section "Old remnawave-tg-shop DSN migration"
warn "The old standalone helper did not support direct DSN import."
warn "This wizard path dumps the old PostgreSQL database, restores it into target Compose PostgreSQL, then runs Minishop schema migrations."
warn "The target database will be dropped and recreated before restore."
if ! confirm "Replace target database with the legacy dump?" 0; then
if ! confirm "Replace target database with the source dump?" 0; then
warn "Migration not applied."
return 0
fi
@@ -1141,8 +1140,8 @@ main_menu() {
banner
choose "Main menu" "1" "1|2|3|4|5|6" \
"1. Install new stack" \
"2. Install new stack and run legacy migration" \
"3. Run legacy migration only" \
"2. Install new stack and run migration" \
"3. Run migration only" \
"4. Download/update deployment files only" \
"5. Validate current stack" \
"6. Exit"
-318
View File
@@ -1,318 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT=""
OLD_PREFIX="remnawave-tg-shop"
NEW_PREFIX="remnawave-minishop"
OLD_DB_VOLUME="${OLD_PREFIX}-db-data"
NEW_DB_VOLUME="${NEW_PREFIX}-db-data"
OLD_CADDY_VOLUMES=("${OLD_PREFIX}-caddy-data" "${OLD_PREFIX}-caddy-config")
NEW_CADDY_VOLUMES=("${NEW_PREFIX}-caddy-data" "${NEW_PREFIX}-caddy-config")
# Container names span three eras: original ``remnawave-tg-shop*`` (≤ v2.7.0),
# the renamed but still single-container ``remnawave-minishop*`` (v3.1.0
# v3.3.x), and the split-arch stack introduced in v3.4.0 (backend / worker /
# frontend / migrate / postgres / redis, plus optional caddy). The list is
# used only to stop existing containers before migration, so it is safe —
# and idempotent — to include every known name from every era.
KNOWN_CONTAINERS=(
# v2.7.0 (upstream remnawave-tg-shop):
"${OLD_PREFIX}"
"${OLD_PREFIX}-db"
"${OLD_PREFIX}-caddy"
# v3.1.0 v3.2.x (renamed but still one container):
"${NEW_PREFIX}"
"${NEW_PREFIX}-db"
"${NEW_PREFIX}-caddy"
# v3.4.0+ (split architecture):
"${NEW_PREFIX}-backend"
"${NEW_PREFIX}-worker"
"${NEW_PREFIX}-frontend"
"${NEW_PREFIX}-migrate"
"${NEW_PREFIX}-postgres"
"${NEW_PREFIX}-redis"
)
log() {
printf '%s\n' "$*"
}
die() {
printf 'Ошибка: %s\n' "$*" >&2
exit 1
}
require_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Не найдено обязательное средство \`$1\` в PATH."
}
resolve_root() {
if [[ -n "${PROJECT_ROOT:-}" ]]; then
[[ -d "$PROJECT_ROOT" ]] || die "PROJECT_ROOT не существует: $PROJECT_ROOT"
(cd -- "$PROJECT_ROOT" >/dev/null && pwd -P)
return
fi
local git_root
if git_root="$(git rev-parse --show-toplevel 2>/dev/null)"; then
printf '%s\n' "$git_root"
return
fi
pwd -P
}
run() {
log "+ $*"
"$@"
}
container_exists() {
docker inspect "$1" >/dev/null 2>&1
}
container_running() {
[[ "$(docker inspect -f '{{.State.Running}}' "$1" 2>/dev/null || true)" == "true" ]]
}
stop_container() {
local name="$1"
if ! container_exists "$name"; then
return 1
fi
if container_running "$name"; then
docker stop "$name" >/dev/null
fi
docker rm "$name" >/dev/null
}
volume_exists() {
docker volume inspect "$1" >/dev/null 2>&1
}
volume_is_empty() {
docker run --rm -v "$1:/data" alpine sh -c 'test -z "$(find /data -mindepth 1 -print -quit)"' >/dev/null 2>&1
}
copy_volume() {
local source="$1"
local target="$2"
if ! volume_exists "$source"; then
log " - Пропускаю том \`$source\`: исходный том не найден."
return 1
fi
if volume_exists "$target" && ! volume_is_empty "$target"; then
log " - Пропускаю том \`$target\`: он уже не пустой."
return 1
fi
if ! volume_exists "$target"; then
die "Целевой том \`$target\` не создан Compose. Сначала нужно подготовить новый стек в режиме \`--no-start\`."
fi
docker run --rm -v "$source:/from:ro" -v "$target:/to" alpine sh -c 'cd /from && cp -a . /to/'
}
is_old_postgres_host() {
grep -Eq "^[[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*${OLD_PREFIX}-db[[:space:]]*(#.*)?$" "$ROOT/.env"
}
is_new_postgres_host() {
grep -Eq "^[[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*${NEW_PREFIX}-db[[:space:]]*(#.*)?$" "$ROOT/.env"
}
update_postgres_host() {
if is_old_postgres_host; then
sed -i.bak -E "s|^([[:space:]]*POSTGRES_HOST[[:space:]]*=[[:space:]]*)${OLD_PREFIX}-db([[:space:]]*(#.*)?)$|\\1${NEW_PREFIX}-db\\2|" "$ROOT/.env"
log " - \`.env\` обновлён, резервная копия сохранена в \`.env.bak\`."
elif is_new_postgres_host; then
log " - \`POSTGRES_HOST\` уже указывает на новый контейнер, ничего менять не нужно."
else
log " - \`POSTGRES_HOST\` не похож на старую схему, пропускаю изменение."
fi
}
main() {
require_cmd git
require_cmd docker
docker info >/dev/null
ROOT="$(resolve_root)"
local compose_file="${COMPOSE_FILE:-docker-compose.yml}"
local target_branch="${TARGET_BRANCH:-main}"
local git_remote="${GIT_REMOTE:-origin}"
local new_origin_url="${NEW_ORIGIN_URL:-}"
local assume_yes="${ASSUME_YES:-0}"
local current_origin
local current_branch
local remote_ref
local head_commit
local compose_has_build=0
local compose_has_caddy=0
local -a compose_cmd
local -a running_containers=()
local -a summary=()
local -a up_args
local name
local source
local target
local answer
if [[ $compose_file != /* ]]; then
compose_file="$ROOT/$compose_file"
fi
[[ -f "$compose_file" ]] || die "Compose-файл не найден: $compose_file"
[[ -e "$ROOT/.git" ]] || die "Скрипт нужно запускать из корня git-репозитория."
[[ -f "$ROOT/.env" ]] || die "Не найден \`.env\` в корне репозитория."
if docker compose version >/dev/null 2>&1; then
compose_cmd=(docker compose)
elif command -v docker-compose >/dev/null 2>&1; then
compose_cmd=(docker-compose)
else
die "Не найден ни \`docker compose\`, ни \`docker-compose\`."
fi
if [[ -n "$(git -C "$ROOT" status --porcelain=v1)" ]]; then
die "В рабочем дереве есть незакоммиченные изменения. Сначала сохраните их, чтобы миграция не затёрла чужие правки."
fi
if grep -Eq '^[[:space:]]*build:[[:space:]]*' "$compose_file"; then
compose_has_build=1
fi
if grep -Eq '^[[:space:]]*caddy:[[:space:]]*$' "$compose_file"; then
compose_has_caddy=1
fi
for name in "${KNOWN_CONTAINERS[@]}"; do
if container_exists "$name"; then
running_containers+=("$name")
fi
done
current_origin="$(git -C "$ROOT" remote get-url "$git_remote")"
if [[ -n "$new_origin_url" && "$current_origin" != "$new_origin_url" ]]; then
summary+=( "обновить $git_remote с \`$current_origin\` на \`$new_origin_url\`" )
fi
summary+=( "скачать ветку \`$target_branch\` из \`$git_remote\`" )
if volume_exists "$OLD_DB_VOLUME"; then
summary+=( "проверить том БД \`$OLD_DB_VOLUME\` и перенести в \`$NEW_DB_VOLUME\` при необходимости" )
fi
if ((compose_has_caddy)); then
for i in 0 1; do
source="${OLD_CADDY_VOLUMES[$i]}"
target="${NEW_CADDY_VOLUMES[$i]}"
if volume_exists "$source"; then
summary+=( "проверить том \`$source\` и перенести в \`$target\` при необходимости" )
fi
done
fi
if is_old_postgres_host; then
summary+=( "обновить \`POSTGRES_HOST\` в \`.env\`" )
fi
summary+=( "подготовить новый стек через Compose в режиме \`--no-start\`" )
summary+=( "запустить compose-файл \`$(basename "$compose_file")\`" )
if [[ "$assume_yes" != "1" ]]; then
if [[ ! -t 0 ]]; then
die "Скрипт ожидает интерактивное подтверждение. Запустите с \`ASSUME_YES=1\` для неинтерактивного режима."
fi
log "План миграции:"
for name in "${summary[@]}"; do
log " - $name"
done
read -r -p "Продолжить? [y/N]: " answer
case "$answer" in
y|Y|yes|YES|Yes)
;;
*)
die "Миграция отменена пользователем."
;;
esac
fi
log "1. Останавливаю старый стек"
if ((${#running_containers[@]})); then
for name in "${running_containers[@]}"; do
stop_container "$name"
log " - контейнер \`$name\` остановлен/удалён"
done
else
log " - запущенных контейнеров старой схемы не найдено"
fi
if [[ -n "$new_origin_url" && "$current_origin" != "$new_origin_url" ]]; then
log "2. Обновляю origin"
run git -C "$ROOT" remote set-url "$git_remote" "$new_origin_url"
else
log "2. Origin уже актуален, пропускаю"
fi
log "3. Обновляю git до ветки \`$target_branch\`"
run git -C "$ROOT" fetch "$git_remote" "$target_branch"
current_branch="$(git -C "$ROOT" branch --show-current || true)"
if [[ -z "$current_branch" ]]; then
current_branch="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
fi
if [[ "$current_branch" != "$target_branch" ]]; then
if git -C "$ROOT" show-ref --verify --quiet "refs/heads/$target_branch"; then
run git -C "$ROOT" switch "$target_branch"
else
run git -C "$ROOT" switch -c "$target_branch" --track "$git_remote/$target_branch"
fi
else
log " - уже на ветке \`$target_branch\`"
fi
remote_ref="$(git -C "$ROOT" rev-parse "$git_remote/$target_branch")"
head_commit="$(git -C "$ROOT" rev-parse HEAD)"
if [[ "$head_commit" != "$remote_ref" ]]; then
run git -C "$ROOT" pull --ff-only "$git_remote" "$target_branch"
else
log " - локальная ветка уже совпадает с удалённой, \`git pull\` не нужен"
fi
log "4. Обновляю \`.env\`"
update_postgres_host
log "5. Подготавливаю новый стек через Compose"
if ((compose_has_build)); then
run "${compose_cmd[@]}" -f "$compose_file" up --no-start --build
else
run "${compose_cmd[@]}" -f "$compose_file" up --no-start
fi
log "6. Переношу тома"
if copy_volume "$OLD_DB_VOLUME" "$NEW_DB_VOLUME"; then
log " - БД перенесена в \`$NEW_DB_VOLUME\`"
fi
if ((compose_has_caddy)); then
for i in 0 1; do
source="${OLD_CADDY_VOLUMES[$i]}"
target="${NEW_CADDY_VOLUMES[$i]}"
if copy_volume "$source" "$target"; then
log " - \`$source\` перенесён в \`$target\`"
fi
done
fi
log "7. Запускаю новый стек"
if ((compose_has_build)); then
up_args=(up -d --build --remove-orphans)
else
up_args=(up -d --remove-orphans)
fi
run "${compose_cmd[@]}" -f "$compose_file" "${up_args[@]}"
run "${compose_cmd[@]}" -f "$compose_file" ps
log "Готово."
}
main "$@"
+2 -1
View File
@@ -37,7 +37,8 @@ def test_shell_installer_downloads_raw_files_and_runs_import_in_container():
assert "git clone" not in script
assert "backend python backend/scripts/import_legacy.py" in script
assert "--dry-run" in script
assert "Install new stack and run legacy migration" in script
assert "Install new stack and run migration" in script
assert "Run migration only" in script
def test_shell_installer_supports_legacy_tgshop_volume_and_dsn_paths():
+56 -131
View File
@@ -1,25 +1,15 @@
"""Pin facts that ``docs/migrations/remnawave-tg-shop.md`` and
``scripts/migrate_to_minishop.sh`` rely on.
Both documents are written for a user upgrading from ``remnawave-tg-shop``
(v2.7.0 era) to the current split-arch ``remnawave-minishop`` (v3.4+). They
make concrete claims about:
* the set of container names produced by today's compose files;
* the set of volume names produced by today's compose files;
* the eras the migration script is allowed to stop containers from.
If any of these drift apart from reality, the migration document silently
goes stale. These tests fail loudly instead.
"""
"""Pin facts shared by the migration docs and the unified install wizard."""
import re
import shutil
import subprocess
import unittest
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[1]
DOC_PATH = REPO_ROOT / "docs" / "migrations" / "remnawave-tg-shop.md"
SCRIPT_PATH = REPO_ROOT / "scripts" / "migrate_to_minishop.sh"
INSTALL_SCRIPT_PATH = REPO_ROOT / "scripts" / "install.sh"
REMOVED_SCRIPT_PATH = REPO_ROOT / "scripts" / "migrate_to_minishop.sh"
COMPOSE_FILES = (
REPO_ROOT / "docker-compose.yml",
REPO_ROOT / "deploy" / "examples" / "caddy" / "docker-compose.yml",
@@ -28,7 +18,6 @@ COMPOSE_FILES = (
REPO_ROOT / "deploy" / "examples" / "no-proxy" / "docker-compose.yml",
)
# Names that the current architecture must produce in at least one compose file.
EXPECTED_CONTAINER_NAMES = {
"remnawave-minishop-backend",
"remnawave-minishop-worker",
@@ -52,15 +41,27 @@ def _all_compose_text() -> str:
return "\n".join(_read(path) for path in COMPOSE_FILES if path.is_file())
def _known_containers_from_install_script() -> set[str]:
text = _read(INSTALL_SCRIPT_PATH)
match = re.search(r'^KNOWN_LEGACY_CONTAINERS="([^"]+)"', text, flags=re.MULTILINE)
if not match:
return set()
return set(match.group(1).split())
class MigrationDocumentationFactsTests(unittest.TestCase):
def setUp(self) -> None:
self.doc = _read(DOC_PATH)
self.script = _read(SCRIPT_PATH)
self.script = _read(INSTALL_SCRIPT_PATH)
self.compose = _all_compose_text()
def test_unified_install_script_is_the_only_migration_entrypoint(self):
self.assertTrue(INSTALL_SCRIPT_PATH.is_file())
self.assertFalse(REMOVED_SCRIPT_PATH.exists())
self.assertIn("scripts/install.sh", self.doc)
self.assertNotIn("migrate_to_minishop.sh", self.doc)
def test_doc_lists_every_running_container_in_current_compose(self):
"""The architecture table must reflect what ``docker compose up``
actually produces today."""
missing = sorted(name for name in EXPECTED_CONTAINER_NAMES if name not in self.doc)
self.assertFalse(
missing,
@@ -77,152 +78,94 @@ class MigrationDocumentationFactsTests(unittest.TestCase):
)
def test_doc_warns_about_renamed_telegram_webhook_secret(self):
# This is the single rename most likely to bite a v2.7.0 → HEAD user.
self.assertIn("TELEGRAM_WEBHOOK_SECRET", self.doc)
self.assertIn("WEBHOOK_SECRET_TOKEN", self.doc)
def test_doc_says_webhook_base_url_is_required(self):
# Polling mode was dropped — without WEBHOOK_BASE_URL the bot refuses
# to start. A user migrating from v2.7.0 (where it was optional) must
# be told this explicitly.
block = self.doc.lower()
self.assertIn("webhook_base_url", block)
# "обязательна" is the marker text in the env-vars table.
self.assertIn("обязательн", block)
def test_doc_mentions_migrate_one_shot_service(self):
# The migrate sidecar is what makes schema migrations transparent on
# the second-stage upgrade. Don't bury it.
self.assertIn("migrate", self.doc)
# "one-shot" or "разовый" / "однораз" text variants accepted.
normalized = self.doc.lower()
self.assertIn("migrate", normalized)
self.assertTrue(
"one-shot" in normalized or "однораз" in normalized,
"migration doc must describe `migrate` as a one-shot service",
)
def test_doc_mentions_postgres_host_compose_override_caveat(self):
# Otherwise users follow the sed-fix step blindly and then panic
# because their .env still has the "wrong" hostname under compose.
self.assertIn("POSTGRES_HOST", self.doc)
# Russian: "переопределя…" / "перебивает" indicate the override is documented.
text = self.doc.lower()
self.assertTrue(
"переопредел" in text or "перебивает" in text,
"переопредел" in text or "service name" in text,
"migration doc must explain that compose overrides POSTGRES_HOST",
)
def test_doc_describes_redis_data_and_shop_data_as_fresh(self):
# We do not migrate redis-data or shop-data — make sure that's said
# so users don't try to copy them from the old stack.
text = self.doc.lower()
self.assertIn("redis-data", text)
self.assertIn("shop-data", text)
# Some phrasing variant must say it's empty / fresh / new on purpose.
self.assertTrue(
any(marker in text for marker in ("пустым", "создаётся пуст", "новые,", "пуст —"))
)
self.assertTrue(any(marker in text for marker in ("пустым", "создается пуст")))
def test_doc_explains_reverse_proxy_no_longer_single_upstream(self):
# The note "rename remnawave-tg-shop → remnawave-minishop in your
# proxy config" used to be enough; after the split it's wrong.
# Make sure both backend:8080 and frontend:80 are documented.
self.assertIn("backend:8080", self.doc)
self.assertIn("frontend:80", self.doc)
def _known_containers_from_script() -> set[str]:
"""Parse the literal ``KNOWN_CONTAINERS`` array from the script source.
The array is defined with ``${OLD_PREFIX}`` / ``${NEW_PREFIX}`` placeholders
that we substitute here. Doing this statically (rather than sourcing the
script in bash) avoids running ``main`` and keeps the test independent of
a bash interpreter being available at runtime.
"""
text = _read(SCRIPT_PATH)
prefix_match = re.search(r'^OLD_PREFIX="([^"]+)"', text, flags=re.MULTILINE)
new_match = re.search(r'^NEW_PREFIX="([^"]+)"', text, flags=re.MULTILINE)
if not prefix_match or not new_match:
return set()
old_prefix = prefix_match.group(1)
new_prefix = new_match.group(1)
# ``\n)`` as a boundary: a non-greedy ``.*?\)`` would stop at the first
# close-paren in a comment like ``# (v2.7.0 upstream remnawave-tg-shop)``.
array_match = re.search(
r"KNOWN_CONTAINERS=\((.*?)\n\)",
text,
flags=re.DOTALL,
)
if not array_match:
return set()
body = array_match.group(1)
# Strip line comments and quotes, then expand the two placeholders.
names: set[str] = set()
for raw_line in body.splitlines():
line = raw_line.split("#", 1)[0].strip()
if not line:
continue
for token in re.findall(r'"([^"]+)"', line):
expanded = token.replace("${OLD_PREFIX}", old_prefix).replace(
"${NEW_PREFIX}", new_prefix
)
names.add(expanded)
return names
def test_doc_mentions_both_supported_tgshop_migration_methods(self):
self.assertIn("Copy old Docker volumes", self.doc)
self.assertIn("Dump from a source PostgreSQL DSN", self.doc)
self.assertIn("pg_dump", self.doc)
class MigrationScriptCoverageTests(unittest.TestCase):
class InstallWizardCoverageTests(unittest.TestCase):
def setUp(self) -> None:
self.script = _read(SCRIPT_PATH)
self.known = _known_containers_from_script()
self.script = _read(INSTALL_SCRIPT_PATH)
self.known = _known_containers_from_install_script()
def test_known_containers_covers_split_arch(self):
"""A re-run on a partially migrated stack must be able to stop the new
containers, otherwise ``docker compose up`` later fails with name
conflicts."""
missing = sorted(EXPECTED_CONTAINER_NAMES - self.known)
self.assertFalse(
missing,
f"KNOWN_CONTAINERS missing split-arch entries: {missing}\nactual: {sorted(self.known)}",
f"KNOWN_LEGACY_CONTAINERS missing split-arch entries: {missing}",
)
def test_known_containers_still_covers_legacy_eras(self):
# We must also keep stopping the original (v2.7.0) and intermediate
# (v3.1.x v3.2.x) container names.
def test_known_containers_still_covers_old_eras(self):
for legacy in ("remnawave-tg-shop", "remnawave-tg-shop-db", "remnawave-minishop-db"):
with self.subTest(container=legacy):
self.assertIn(legacy, self.known)
def test_script_is_syntactically_valid_bash(self):
# The script is curl|bash'ed from raw.githubusercontent in the docs,
# so a syntax break is a hard regression.
import shutil
import subprocess
def test_installer_contains_tgshop_volume_and_dsn_paths(self):
self.assertIn("run_tgshop_volume_migration", self.script)
self.assertIn("run_tgshop_dsn_migration", self.script)
self.assertIn("remnawave-tg-shop-db-data", self.script)
self.assertIn("pg_dump --clean --if-exists", self.script)
self.assertIn("run_compose run --rm migrate", self.script)
bash = shutil.which("bash")
if not bash: # pragma: no cover
self.skipTest("bash not available in PATH")
def test_script_is_syntactically_valid_sh_and_bash(self):
sh = shutil.which("sh")
if not sh: # pragma: no cover
self.skipTest("sh not available in PATH")
result = subprocess.run(
[bash, "-n", str(SCRIPT_PATH)],
[sh, "-n", str(INSTALL_SCRIPT_PATH)],
check=False,
capture_output=True,
text=True,
)
self.assertEqual(
result.returncode,
0,
(
"bash -n flagged migrate_to_minishop.sh:\n"
f"stdout={result.stdout}\nstderr={result.stderr}"
),
)
self.assertEqual(result.returncode, 0, result.stderr)
bash = shutil.which("bash")
if bash:
result = subprocess.run(
[bash, "-n", str(INSTALL_SCRIPT_PATH)],
check=False,
capture_output=True,
text=True,
)
self.assertEqual(result.returncode, 0, result.stderr)
class DocComposeFileReferencesTests(unittest.TestCase):
"""The doc links the user to specific compose files — they must exist."""
def test_referenced_compose_files_exist(self):
doc = _read(DOC_PATH)
for relpath in (
@@ -234,40 +177,22 @@ class DocComposeFileReferencesTests(unittest.TestCase):
):
with self.subTest(path=relpath):
self.assertIn(relpath, doc)
self.assertTrue(
(REPO_ROOT / relpath).is_file(),
f"{relpath} is referenced in migrations/remnawave-tg-shop.md "
"but missing on disk",
)
self.assertTrue((REPO_ROOT / relpath).is_file())
def test_doc_references_migrator_module_path(self):
# The doc tells users to expect ``backend/db/migrator.py`` migrations
# to apply via the migrate service. If the file moves, the doc lies.
doc = _read(DOC_PATH)
self.assertIn("backend/db/migrator.py", doc)
self.assertTrue((REPO_ROOT / "backend" / "db" / "migrator.py").is_file())
class MigrationFootprintRegexTests(unittest.TestCase):
"""Spot-check that what compose actually defines matches what we documented."""
def test_every_compose_volume_documented(self):
"""If a future compose file introduces a new ``remnawave-minishop-*``
named volume, the migration doc must call out whether it carries data
from the old stack or starts fresh."""
doc = _read(DOC_PATH)
compose_text = _all_compose_text()
# Find every named volume of the form ``remnawave-minishop-<key>-data``.
defined = set(re.findall(r"remnawave-minishop-[\w-]+-data", compose_text))
# Caddy-only volumes only ship in the caddy compose file but are still
# documented; allow them either way.
for volume in defined:
with self.subTest(volume=volume):
self.assertIn(
volume,
doc,
f"new volume {volume} is defined in compose but missing from migration doc",
)
self.assertIn(volume, doc)
if __name__ == "__main__": # pragma: no cover