NOISSUE - Improve migration scripts

Signed-off-by: dusan <borovcanindusan1@gmail.com>
This commit is contained in:
dusan
2026-06-30 13:50:43 +02:00
parent c405028fc5
commit bc7c9096f9
5 changed files with 554 additions and 1 deletions
+10 -1
View File
@@ -197,7 +197,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES))
all: $(SERVICES)
.PHONY: all $(SERVICES) dockers dockers_dev latest release provision_atom_tokens provision-atom-tokens run_latest run_latest_ci run_tls run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
.PHONY: all $(SERVICES) dockers dockers_dev latest release provision_atom_tokens provision-atom-tokens migrate_atom run_latest run_latest_ci run_tls run_stable run_addons grpc_mtls_certs check_mtls check_certs test_api mocks
clean:
rm -rf ${BUILD_DIR}
@@ -324,6 +324,15 @@ provision_atom_tokens:
provision-atom-tokens:
@:
# Migrate an old Magistrala (v0.30.0 / pre-Atom) deployment into Atom. Runs an
# isolated, collision-free stack, seeds the Atom schema into the run_latest Atom
# volume and loads the data. Default is a dry-run; pass args="--apply" to load,
# args="--verify" to reconcile afterwards.
# make migrate_atom # dry-run
# make migrate_atom args="--apply" # perform the migration
migrate_atom:
DOCKER_PROJECT="$(DOCKER_PROJECT)" tools/atom-migration/migrate.sh $(args)
check_tls:
ifeq ($(GRPC_TLS),true)
@echo "gRPC TLS is enabled"
+104
View File
@@ -3,6 +3,110 @@
Offline, idempotent migrator: Magistrala v0.30.0 (per-service Postgres) → Atom IAM
(single Postgres). See [PLAN.md](./PLAN.md) for the full mapping and runbook.
## One-command migration (recommended)
If you just want to migrate an old deployment and bring it up with
`make run_latest`, use the orchestrator — it handles all the port / volume /
container-name collisions for you:
```bash
make migrate_atom # dry-run: reads + validates, writes nothing
make migrate_atom args="--apply" # perform the migration
make migrate_atom args="--verify" # reconcile source vs Atom afterwards
```
Then:
```bash
# stop the old stack, then:
make run_latest
```
…and the new deployment serves the migrated data.
How it stays collision-free: [`migrate.sh`](./migrate.sh) +
[`docker-compose.migrate.yaml`](./docker-compose.migrate.yaml) run everything in
their own Compose project (`atommig`) on a private network, binding **no host
ports** and using **no fixed container names**, so they never clash with a
running Magistrala (old or `run_latest`) stack. It:
1. mounts the eight old per-service DB volumes
(`magistrala_magistrala-<svc>-db-volume`) into throwaway Postgres containers
(Postgres major version + data-dir layout auto-detected from the volume);
2. brings up an Atom + Postgres on the **same** volume `make run_latest` mounts
(`<DOCKER_PROJECT>_magistrala-atom-db-volume`), so Atom seeds its schema there
and the migrated rows persist for the next `run_latest`;
3. runs the migrator on that private network (reaching every DB by service name);
4. tears the stack down, leaving every volume intact.
All migration volumes are declared `external`, so `down` can never destroy data.
Prerequisites: run this on the machine that hosted the old Magistrala compose
stack. Stop that stack (`docker compose ... down`, **without** `-v`) so the
per-service DB volumes are free but still present locally — the migrator mounts
them directly. On `--apply`, the device-key CSV described below lands in
`tools/atom-migration/report/`.
Env overrides: `SRC_VOL_PREFIX` (old volume prefix, default
`magistrala_magistrala-`), `SRC_DB_USER` / `SRC_DB_PASS` (old Postgres creds,
default `magistrala`), `DOCKER_PROJECT` (run_latest project; default derived like
the Makefile), `MIGRATE_PROJECT` (isolated project name, default `atommig`).
Pass `--keep` to leave the stack up for debugging.
### How volume names are resolved
Names are **derived by convention, not auto-discovered**. There are two sets.
**Source volumes (old deployment, read-only inputs).** Built from a prefix plus
a fixed per-service suffix:
```
<SRC_VOL_PREFIX><svc>-db-volume # svc ∈ domains users clients channels groups auth re reports
```
`SRC_VOL_PREFIX` defaults to `magistrala_magistrala-`, i.e. old Compose project
`magistrala` + Docker Compose's own `magistrala-` volume key. So
`auth``magistrala_magistrala-auth-db-volume`. `migrate.sh` `docker volume
inspect`s all eight up front and aborts loudly if any is missing. The same
`${SRC_VOL_PREFIX}` feeds the `external` volume names in
`docker-compose.migrate.yaml`, so the script and Compose always agree. If your
old deployment used a different Compose project name, set `SRC_VOL_PREFIX`
(e.g. `SRC_VOL_PREFIX=myproj_magistrala-`).
**Atom target volume (where migrated data is written).** Must equal exactly the
volume `make run_latest` mounts, or the new stack would come up on a different,
empty volume. `make run_latest` mounts `magistrala-atom-db-volume`, which Docker
Compose prefixes with the project name `DOCKER_PROJECT`:
```
<DOCKER_PROJECT>_magistrala-atom-db-volume
```
`DOCKER_PROJECT` is itself derived from the git remote, replicating the Makefile
formula:
```sh
repo=$(git remote get-url origin | sed -E 's@.*/([^/]+)/([^/.]+)(\.git)?@\1_\2@') # owner_repo
DOCKER_PROJECT=$(echo "$repo" | sed -E 's/[^a-zA-Z0-9]/_/g' | tr '[:upper:]' '[:lower:]')
ATOM_TARGET_VOLUME="${DOCKER_PROJECT}_magistrala-atom-db-volume"
```
`make migrate_atom` also passes `DOCKER_PROJECT="$(DOCKER_PROJECT)"` straight from
the Makefile, so the two stay in lockstep even if the git derivation would differ.
The target volume is created if it does not yet exist (so the schema-seed step can
write to it); `make run_latest` then reuses the same name. With no usable git
remote, or a remote that does not match the run_latest project, pass
`DOCKER_PROJECT=` explicitly.
**Source Postgres layout** (mount point + `PGDATA` + image major version) is the
one thing actually probed, not assumed: `migrate.sh` mounts the `users` source
volume in a throwaway `alpine` container, locates `PG_VERSION`, and derives the
mount path / `PGDATA` / `postgres:<major>-alpine` image from it (e.g. Postgres 18
keeps data under `/var/lib/postgresql/<major>/docker`). This makes the tool work
regardless of which Postgres version the old deployment ran.
The manual, lower-level steps below are still available if you need finer control.
## Build
Plain binary:
@@ -0,0 +1,219 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
# Isolated, collision-free migration stack driven by migrate.sh.
#
# Everything here lives in its own Compose project (default: atommig) on its own
# private network, binds NO host ports and uses NO fixed container_names, so it
# can never collide with a running Magistrala (old or `make run_latest`) stack.
#
# * source DBs - eight Postgres containers that mount the OLD per-service
# Magistrala volumes (external, read by the migrator).
# * atom-db - Postgres mounting the NEW run_latest Atom volume, so data
# written here is exactly what `make run_latest` later serves.
# * atom - seeds/upgrades the Atom schema in that volume on boot.
# * migrator - the atom-migration binary; run on demand by migrate.sh.
#
# Variables are supplied by migrate.sh (and docker/.env for the ATOM_* / atom
# runtime settings). Source volume layout (mount point + PGDATA) is detected by
# migrate.sh and injected, so this file works regardless of the Postgres major
# version the old deployment used.
x-src-db: &src-db
image: ${SRC_PG_IMAGE:-postgres:18-alpine}
restart: "no"
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${SRC_DB_USER:-magistrala}"]
interval: 3s
timeout: 5s
retries: 20
networks:
- migrate-net
services:
domains-db:
<<: *src-db
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
POSTGRES_DB: domains
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
volumes:
- domains-src:${SRC_MOUNT:-/var/lib/postgresql}
users-db:
<<: *src-db
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
POSTGRES_DB: users
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
volumes:
- users-src:${SRC_MOUNT:-/var/lib/postgresql}
clients-db:
<<: *src-db
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
POSTGRES_DB: clients
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
volumes:
- clients-src:${SRC_MOUNT:-/var/lib/postgresql}
channels-db:
<<: *src-db
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
POSTGRES_DB: channels
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
volumes:
- channels-src:${SRC_MOUNT:-/var/lib/postgresql}
groups-db:
<<: *src-db
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
POSTGRES_DB: groups
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
volumes:
- groups-src:${SRC_MOUNT:-/var/lib/postgresql}
auth-db:
<<: *src-db
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
POSTGRES_DB: auth
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
volumes:
- auth-src:${SRC_MOUNT:-/var/lib/postgresql}
re-db:
<<: *src-db
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
POSTGRES_DB: rules_engine
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
volumes:
- re-src:${SRC_MOUNT:-/var/lib/postgresql}
reports-db:
<<: *src-db
environment:
POSTGRES_USER: ${SRC_DB_USER:-magistrala}
POSTGRES_PASSWORD: ${SRC_DB_PASS:-magistrala}
POSTGRES_DB: reports
PGDATA: ${SRC_PGDATA:-/var/lib/postgresql/18/docker}
volumes:
- reports-src:${SRC_MOUNT:-/var/lib/postgresql}
# Target: the SAME volume `make run_latest` mounts, so migrated data persists.
atom-db:
image: postgres:16-alpine
restart: "no"
environment:
POSTGRES_USER: ${ATOM_DB_USER:-atom}
POSTGRES_PASSWORD: ${ATOM_DB_PASSWORD:-atom}
POSTGRES_DB: ${ATOM_DB_NAME:-atom}
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${ATOM_DB_USER:-atom} -d ${ATOM_DB_NAME:-atom}"]
interval: 3s
timeout: 5s
retries: 20
volumes:
- atom-target:/var/lib/postgresql/data
networks:
- migrate-net
# Seeds / upgrades the Atom schema in the target volume on boot (migrations run
# at startup). Certs disabled so it needs no mounted CA. No host ports.
atom:
image: ghcr.io/absmach/atom:${MG_RELEASE_TAG:-latest}
restart: "no"
depends_on:
atom-db:
condition: service_healthy
env_file:
- ../../docker/.env
environment:
DATABASE_URL: postgres://${ATOM_DB_USER:-atom}:${ATOM_DB_PASSWORD:-atom}@atom-db:5432/${ATOM_DB_NAME:-atom}
LISTEN_ADDR: 0.0.0.0:8080
ATOM_CERTS_ENABLED: "false"
networks:
- migrate-net
# The migration binary. Run on demand: `compose run --rm migrator <flags>`.
migrator:
image: magistrala/atom-migration:dev
depends_on:
atom-db:
condition: service_healthy
domains-db:
condition: service_healthy
users-db:
condition: service_healthy
clients-db:
condition: service_healthy
channels-db:
condition: service_healthy
groups-db:
condition: service_healthy
auth-db:
condition: service_healthy
re-db:
condition: service_healthy
reports-db:
condition: service_healthy
environment:
ATOM_DATABASE_URL: host=atom-db port=5432 user=${ATOM_DB_USER:-atom} password=${ATOM_DB_PASSWORD:-atom} dbname=${ATOM_DB_NAME:-atom} sslmode=disable
user: "${HOST_UID:-0}:${HOST_GID:-0}"
working_dir: /work
volumes:
- ../../:/work
networks:
- migrate-net
# Args (e.g. --apply / --verify) are supplied by migrate.sh via `compose run`.
entrypoint: ["atom-migration", "--env", "tools/atom-migration/migrate.db.env"]
networks:
migrate-net:
driver: bridge
# All source volumes and the Atom target are pre-existing (external): Compose
# never creates or deletes them, so `down` can never destroy migration data.
volumes:
domains-src:
external: true
name: ${SRC_VOL_PREFIX:-magistrala_magistrala-}domains-db-volume
users-src:
external: true
name: ${SRC_VOL_PREFIX:-magistrala_magistrala-}users-db-volume
clients-src:
external: true
name: ${SRC_VOL_PREFIX:-magistrala_magistrala-}clients-db-volume
channels-src:
external: true
name: ${SRC_VOL_PREFIX:-magistrala_magistrala-}channels-db-volume
groups-src:
external: true
name: ${SRC_VOL_PREFIX:-magistrala_magistrala-}groups-db-volume
auth-src:
external: true
name: ${SRC_VOL_PREFIX:-magistrala_magistrala-}auth-db-volume
re-src:
external: true
name: ${SRC_VOL_PREFIX:-magistrala_magistrala-}re-db-volume
reports-src:
external: true
name: ${SRC_VOL_PREFIX:-magistrala_magistrala-}reports-db-volume
atom-target:
external: true
name: ${ATOM_TARGET_VOLUME}
+57
View File
@@ -0,0 +1,57 @@
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
#
# Source DB connection details for the migrator when it runs inside the isolated
# docker-compose.migrate.yaml network. Hosts are the Compose service names, so
# this resolves only from a container attached to that network (which is how
# migrate.sh runs it). The Atom target DSN is passed separately via
# ATOM_DATABASE_URL. Override SRC_DB_USER/SRC_DB_PASS in migrate.sh if the old
# deployment used non-default Postgres credentials.
MG_DOMAINS_DB_HOST=domains-db
MG_DOMAINS_DB_PORT=5432
MG_DOMAINS_DB_USER=magistrala
MG_DOMAINS_DB_PASS=magistrala
MG_DOMAINS_DB_NAME=domains
MG_USERS_DB_HOST=users-db
MG_USERS_DB_PORT=5432
MG_USERS_DB_USER=magistrala
MG_USERS_DB_PASS=magistrala
MG_USERS_DB_NAME=users
MG_CLIENTS_DB_HOST=clients-db
MG_CLIENTS_DB_PORT=5432
MG_CLIENTS_DB_USER=magistrala
MG_CLIENTS_DB_PASS=magistrala
MG_CLIENTS_DB_NAME=clients
MG_CHANNELS_DB_HOST=channels-db
MG_CHANNELS_DB_PORT=5432
MG_CHANNELS_DB_USER=magistrala
MG_CHANNELS_DB_PASS=magistrala
MG_CHANNELS_DB_NAME=channels
MG_GROUPS_DB_HOST=groups-db
MG_GROUPS_DB_PORT=5432
MG_GROUPS_DB_USER=magistrala
MG_GROUPS_DB_PASS=magistrala
MG_GROUPS_DB_NAME=groups
MG_AUTH_DB_HOST=auth-db
MG_AUTH_DB_PORT=5432
MG_AUTH_DB_USER=magistrala
MG_AUTH_DB_PASS=magistrala
MG_AUTH_DB_NAME=auth
MG_RE_DB_HOST=re-db
MG_RE_DB_PORT=5432
MG_RE_DB_USER=magistrala
MG_RE_DB_PASS=magistrala
MG_RE_DB_NAME=rules_engine
MG_REPORTS_DB_HOST=reports-db
MG_REPORTS_DB_PORT=5432
MG_REPORTS_DB_USER=magistrala
MG_REPORTS_DB_PASS=magistrala
MG_REPORTS_DB_NAME=reports
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env bash
# Copyright (c) Abstract Machines
# SPDX-License-Identifier: Apache-2.0
#
# One-command Magistrala v0.30.0 -> Atom migration.
#
# Brings up an isolated, collision-free stack (its own Compose project + private
# network, no host ports, no fixed container names), seeds the Atom schema into
# the SAME volume `make run_latest` uses, runs the migrator, then tears the stack
# down leaving every volume intact. After it finishes you can simply:
#
# make run_latest
#
# and the new deployment serves the migrated data.
#
# Usage:
# tools/atom-migration/migrate.sh # dry-run (reads + validates, writes nothing)
# tools/atom-migration/migrate.sh --apply # perform the migration
# tools/atom-migration/migrate.sh --verify # reconcile source vs Atom after apply
# tools/atom-migration/migrate.sh --apply --keep # leave the stack running for debugging
#
# Env overrides:
# DOCKER_PROJECT run_latest Compose project (default: derived like the Makefile)
# SRC_VOL_PREFIX old DB volume name prefix (default: magistrala_magistrala-)
# SRC_DB_USER/PASS old Postgres credentials (default: magistrala/magistrala)
# MIGRATE_PROJECT isolated Compose project name (default: atommig)
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
COMPOSE_FILE="$SCRIPT_DIR/docker-compose.migrate.yaml"
ENV_FILE="$REPO_ROOT/docker/.env"
MIGRATE_PROJECT="${MIGRATE_PROJECT:-atommig}"
SRC_VOL_PREFIX="${SRC_VOL_PREFIX:-magistrala_magistrala-}"
MIGRATOR_ARGS=()
KEEP=false
for arg in "$@"; do
case "$arg" in
--keep) KEEP=true ;;
--apply|--verify|--dry-run) MIGRATOR_ARGS+=("$arg") ;;
--unmapped-action=*|--report-dir=*) MIGRATOR_ARGS+=("$arg") ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//' | head -40; exit 0 ;;
*) echo "unknown argument: $arg" >&2; exit 2 ;;
esac
done
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
command -v docker >/dev/null || die "docker not found"
[[ -f "$ENV_FILE" ]] || die "missing $ENV_FILE"
# DOCKER_PROJECT: match the Makefile derivation so we target the volume that
# `make run_latest` will mount.
if [[ -z "${DOCKER_PROJECT:-}" ]]; then
repo="$(git -C "$REPO_ROOT" remote get-url origin 2>/dev/null \
| sed -E 's@.*/([^/]+)/([^/.]+)(\.git)?@\1_\2@')"
DOCKER_PROJECT="$(echo "$repo" | sed -E 's/[^a-zA-Z0-9]/_/g' | tr '[:upper:]' '[:lower:]')"
fi
[[ -n "$DOCKER_PROJECT" ]] || die "could not determine DOCKER_PROJECT (set it explicitly)"
ATOM_TARGET_VOLUME="${DOCKER_PROJECT}_magistrala-atom-db-volume"
log "Isolated project : $MIGRATE_PROJECT"
log "Source volumes : ${SRC_VOL_PREFIX}<svc>-db-volume"
log "Atom target vol : $ATOM_TARGET_VOLUME"
# --- preflight: every source volume must exist ---
missing=()
for svc in domains users clients channels groups auth re reports; do
vol="${SRC_VOL_PREFIX}${svc}-db-volume"
docker volume inspect "$vol" >/dev/null 2>&1 || missing+=("$vol")
done
((${#missing[@]} == 0)) || die "missing source volume(s): ${missing[*]}
Run this on the machine whose stopped old Magistrala stack still has these
volumes, or set SRC_VOL_PREFIX if the old Compose project used another name."
# Atom target volume: created here if absent so the seed step can write to it;
# `make run_latest` reuses the same name.
docker volume inspect "$ATOM_TARGET_VOLUME" >/dev/null 2>&1 || {
log "Creating Atom target volume $ATOM_TARGET_VOLUME"
docker volume create "$ATOM_TARGET_VOLUME" >/dev/null
}
# --- detect source Postgres layout (mount point + PGDATA + image major) ---
log "Detecting source Postgres layout"
layout="$(docker run --rm -v "${SRC_VOL_PREFIX}users-db-volume":/d alpine:3.22 sh -c '
if [ -f /d/PG_VERSION ]; then
echo "classic $(cat /d/PG_VERSION) /"
else
p=$(find /d -maxdepth 4 -name PG_VERSION 2>/dev/null | head -1)
[ -n "$p" ] && echo "nested $(cat "$p") ${p#/d/}" || echo "unknown 0 /"
fi')"
read -r kind major rel <<<"$layout"
[[ "$kind" != "unknown" ]] || die "could not find PG_VERSION in source volume"
if [[ "$kind" == "nested" ]]; then
# rel = <major>/docker/PG_VERSION -> PGDATA dir = /var/lib/postgresql/<major>/docker
subdir="$(dirname "/$rel")" # /<major>/docker
SRC_MOUNT="/var/lib/postgresql"
SRC_PGDATA="/var/lib/postgresql${subdir}"
else
SRC_MOUNT="/var/lib/postgresql/data"
SRC_PGDATA="/var/lib/postgresql/data"
fi
SRC_PG_IMAGE="postgres:${major}-alpine"
log "Source Postgres : $SRC_PG_IMAGE (mount $SRC_MOUNT, PGDATA $SRC_PGDATA)"
# --- build the migrator image ---
log "Building migrator image"
docker build -q -f "$SCRIPT_DIR/Dockerfile" -t magistrala/atom-migration:dev "$REPO_ROOT" >/dev/null
# Variables consumed by the compose file. docker/.env is passed via --env-file so
# ATOM_DB_* / MG_RELEASE_TAG interpolate; these exports take precedence.
export SRC_VOL_PREFIX SRC_PG_IMAGE SRC_MOUNT SRC_PGDATA ATOM_TARGET_VOLUME
export SRC_DB_USER="${SRC_DB_USER:-magistrala}" SRC_DB_PASS="${SRC_DB_PASS:-magistrala}"
export HOST_UID="$(id -u)" HOST_GID="$(id -g)"
dc() { docker compose --env-file "$ENV_FILE" -p "$MIGRATE_PROJECT" -f "$COMPOSE_FILE" "$@"; }
cleanup() {
if [[ "$KEEP" == true ]]; then
log "Leaving stack up (--keep). Tear down with:"
echo " docker compose -p $MIGRATE_PROJECT -f $COMPOSE_FILE down"
return
fi
log "Tearing down isolated stack (volumes are external and preserved)"
dc down --remove-orphans >/dev/null 2>&1 || true
}
trap cleanup EXIT
# --- bring up source DBs + Atom DB, seed the Atom schema ---
log "Starting source DBs, Atom DB and Atom schema seeder"
dc up -d --wait domains-db users-db clients-db channels-db groups-db auth-db re-db reports-db atom-db
dc up -d atom
log "Waiting for Atom to apply its schema into the target volume"
for i in $(seq 1 60); do
if dc exec -T atom-db psql -U "${ATOM_DB_USER:-atom}" -d "${ATOM_DB_NAME:-atom}" -tAc \
"SELECT to_regclass('public.tenants') IS NOT NULL AND to_regclass('public.entities') IS NOT NULL;" 2>/dev/null \
| grep -qx t; then
log "Atom schema ready"
break
fi
[[ $i -eq 60 ]] && die "Atom did not initialise its schema in time (check: dc logs atom)"
sleep 2
done
# --- run the migration ---
log "Running migrator ${MIGRATOR_ARGS[*]:-(dry-run)}"
set +e
dc run --rm migrator "${MIGRATOR_ARGS[@]}"
rc=$?
set -e
echo
if [[ $rc -eq 0 ]]; then
log "Migrator finished OK. Report: tools/atom-migration/report/"
if printf '%s\n' "${MIGRATOR_ARGS[@]}" | grep -qx -- --apply; then
log "Next: stop the old stack, then 'make run_latest' to serve migrated data."
fi
else
die "migrator exited $rc (see report in tools/atom-migration/report/)"
fi