Add local manual leader follower stack

This commit is contained in:
Daniel Schädler 2026-04-16 10:40:28 +02:00
parent fee9ec0bef
commit 763ff710f0
3 changed files with 139 additions and 0 deletions

View File

@ -51,6 +51,77 @@ These leader-side actions enqueue async snapshot push:
Queue retries with backoff until follower accepts snapshot. Queue retries with backoff until follower accepts snapshot.
## Local Manual Testing
Backend now has local manual setup target:
```bash
inv docker-blackbox-follow-manual-up
```
This starts:
- leader production image on `http://127.0.0.1:3002`
- follower production image on `http://127.0.0.1:3003`
- separate leader and follower Postgres containers
- bootstrapped owner users on both backends
Teardown:
```bash
inv docker-blackbox-follow-down
```
### Local topology
Compose setup now separates DB traffic:
- `source-postgres` only on `source-db` internal network
- `follower-postgres` only on `follower-db` internal network
- both apps share only `sync` network
This gives:
- separate production app containers
- separate production DB containers
- DB isolation between leader and follower
Important limit:
- Docker Compose does **not** enforce true one-way HTTP reachability here
- local setup still allows follower container to reach leader container over shared `sync` network
- real production trust model still comes from backend design:
- leader pushes
- follower never pulls
- follower has no code path that contacts leader
So local setup is good for frontend/manual testing of behavior, not strict network-policy simulation.
### Manual frontend testing flow
1. Run `inv docker-blackbox-follow-manual-up`
2. Open leader backend at `http://127.0.0.1:3002`
3. Open follower backend at `http://127.0.0.1:3003`
4. Log in with:
- email: `e2e@example.com`
- password: `password123`
5. On follower backend, create empty read-only follower tournament
6. On leader backend, create normal tournament
7. On leader tournament, set:
- `sync_target_url` to follower `PATCH /tournaments/:id/sync_state`
- `sync_auth_token` to same shared token used on follower
8. Point local frontend leader instance to `http://127.0.0.1:3002`
9. Point local frontend follower/remote instance to `http://127.0.0.1:3003`
10. Progress tournament on leader, verify follower mirrors state and stays read only
Frontend team can use this setup to build:
- owner sync configuration UI
- follower read-only UX
- status display for `sync_last_pushed_at`
- status display for `sync_last_push_error`
- takeover flow by disabling `read_only_mode`
## Takeover ## Takeover
If remote must become writable after sync phase: If remote must become writable after sync phase:

View File

@ -12,6 +12,8 @@ services:
retries: 20 retries: 20
volumes: volumes:
- turniere-blackbox-follow-source-postgres:/var/lib/postgresql/data - turniere-blackbox-follow-source-postgres:/var/lib/postgresql/data
networks:
- source-db
follower-postgres: follower-postgres:
image: ${TURNIERE_BLACKBOX_POSTGRES_IMAGE:-postgres:16} image: ${TURNIERE_BLACKBOX_POSTGRES_IMAGE:-postgres:16}
@ -26,6 +28,8 @@ services:
retries: 20 retries: 20
volumes: volumes:
- turniere-blackbox-follow-follower-postgres:/var/lib/postgresql/data - turniere-blackbox-follow-follower-postgres:/var/lib/postgresql/data
networks:
- follower-db
source-app: source-app:
image: ${TURNIERE_BLACKBOX_APP_IMAGE:-registry.gitlab.com/turniere/turniere-backend/production/commits:local} image: ${TURNIERE_BLACKBOX_APP_IMAGE:-registry.gitlab.com/turniere/turniere-backend/production/commits:local}
@ -44,6 +48,9 @@ services:
RAILS_SERVE_STATIC_FILES: "1" RAILS_SERVE_STATIC_FILES: "1"
ports: ports:
- "${TURNIERE_BLACKBOX_SOURCE_HOST_PORT:-3002}:3000" - "${TURNIERE_BLACKBOX_SOURCE_HOST_PORT:-3002}:3000"
networks:
- source-db
- sync
follower-app: follower-app:
image: ${TURNIERE_BLACKBOX_APP_IMAGE:-registry.gitlab.com/turniere/turniere-backend/production/commits:local} image: ${TURNIERE_BLACKBOX_APP_IMAGE:-registry.gitlab.com/turniere/turniere-backend/production/commits:local}
@ -62,6 +69,9 @@ services:
RAILS_SERVE_STATIC_FILES: "1" RAILS_SERVE_STATIC_FILES: "1"
ports: ports:
- "${TURNIERE_BLACKBOX_FOLLOWER_HOST_PORT:-3003}:3000" - "${TURNIERE_BLACKBOX_FOLLOWER_HOST_PORT:-3003}:3000"
networks:
- follower-db
- sync
e2e: e2e:
image: ${TURNIERE_BLACKBOX_RUNNER_IMAGE:-registry.gitlab.com/turniere/turniere-backend/test/commits:local} image: ${TURNIERE_BLACKBOX_RUNNER_IMAGE:-registry.gitlab.com/turniere/turniere-backend/test/commits:local}
@ -82,7 +92,16 @@ services:
TURNIERE_E2E_ALT_EMAIL: ${TURNIERE_E2E_ALT_EMAIL:-e2e-alt@example.com} TURNIERE_E2E_ALT_EMAIL: ${TURNIERE_E2E_ALT_EMAIL:-e2e-alt@example.com}
TURNIERE_E2E_ALT_PASSWORD: ${TURNIERE_E2E_ALT_PASSWORD:-password123} TURNIERE_E2E_ALT_PASSWORD: ${TURNIERE_E2E_ALT_PASSWORD:-password123}
TURNIERE_E2E_ALT_USERNAME: ${TURNIERE_E2E_ALT_USERNAME:-e2e-alt-user} TURNIERE_E2E_ALT_USERNAME: ${TURNIERE_E2E_ALT_USERNAME:-e2e-alt-user}
networks:
- sync
volumes: volumes:
turniere-blackbox-follow-source-postgres: turniere-blackbox-follow-source-postgres:
turniere-blackbox-follow-follower-postgres: turniere-blackbox-follow-follower-postgres:
networks:
source-db:
internal: true
follower-db:
internal: true
sync:

View File

@ -994,6 +994,55 @@ def docker_blackbox_follow_up(
print("Follower production apps ready for blackbox E2E.") print("Follower production apps ready for blackbox E2E.")
@task(name="docker-blackbox-follow-manual-up")
def docker_blackbox_follow_manual_up(
c,
source_host_port=BLACKBOX_SOURCE_HOST_PORT,
follower_host_port=BLACKBOX_FOLLOWER_HOST_PORT,
app_image=PRODUCTION_TAG,
runner_image=TEST_TAG,
postgres_image=BLACKBOX_POSTGRES_IMAGE,
build=True,
):
"""Boot local leader/follower production setup for manual testing and print next steps."""
docker_blackbox_follow_up(
c,
source_host_port=source_host_port,
follower_host_port=follower_host_port,
app_image=app_image,
runner_image=runner_image,
postgres_image=postgres_image,
build=build,
)
source_env = _production_env_for("source-postgres", BLACKBOX_SOURCE_DB_NAME)
follower_env = _production_env_for("follower-postgres", BLACKBOX_FOLLOWER_DB_NAME)
_print_header("Running source production database migrations")
c.run("bundle exec rails db:migrate", env=source_env, pty=True)
_print_header("Running follower production database migrations")
c.run("bundle exec rails db:migrate", env=follower_env, pty=True)
_print_header("Bootstrapping confirmed E2E users on source and follower")
_bootstrap_blackbox_users(c, source_env, E2E_EMAIL, E2E_PASSWORD, E2E_USERNAME, E2E_ALT_EMAIL, E2E_ALT_PASSWORD, E2E_ALT_USERNAME)
_bootstrap_blackbox_users(c, follower_env, E2E_EMAIL, E2E_PASSWORD, E2E_USERNAME, E2E_ALT_EMAIL, E2E_ALT_PASSWORD, E2E_ALT_USERNAME)
source_base_url = f"http://127.0.0.1:{source_host_port}"
follower_base_url = f"http://127.0.0.1:{follower_host_port}"
_print_header("Leader/follower manual setup ready")
print(f"Leader URL: {source_base_url}")
print(f"Follower URL: {follower_base_url}")
print(f"E2E owner email: {E2E_EMAIL}")
print(f"E2E owner password: {E2E_PASSWORD}")
print("Next steps:")
print("1. Create read-only follower tournament on follower backend.")
print("2. Create normal leader tournament on leader backend.")
print("3. Configure leader sync_target_url to follower /sync_state endpoint with shared token.")
print("4. Point frontend local leader view at leader URL and remote view at follower URL.")
print("5. Tear down with: inv docker-blackbox-follow-down")
@task(name="docker-blackbox-follow-down") @task(name="docker-blackbox-follow-down")
def docker_blackbox_follow_down( def docker_blackbox_follow_down(
c, c,