refactor: consolidate migration installer
This commit is contained in:
@@ -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.
|
||||
|
||||
## Совместимость
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.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` |
|
||||
|
||||
Внутри 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
@@ -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"
|
||||
|
||||
@@ -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 "$@"
|
||||
@@ -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():
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user