diff --git a/doc/leader_follower.md b/doc/leader_follower.md index 9319640..632e805 100644 --- a/doc/leader_follower.md +++ b/doc/leader_follower.md @@ -51,6 +51,77 @@ These leader-side actions enqueue async snapshot push: 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 If remote must become writable after sync phase: diff --git a/docker-compose.blackbox-follow.yml b/docker-compose.blackbox-follow.yml index debad6a..525d8c6 100644 --- a/docker-compose.blackbox-follow.yml +++ b/docker-compose.blackbox-follow.yml @@ -12,6 +12,8 @@ services: retries: 20 volumes: - turniere-blackbox-follow-source-postgres:/var/lib/postgresql/data + networks: + - source-db follower-postgres: image: ${TURNIERE_BLACKBOX_POSTGRES_IMAGE:-postgres:16} @@ -26,6 +28,8 @@ services: retries: 20 volumes: - turniere-blackbox-follow-follower-postgres:/var/lib/postgresql/data + networks: + - follower-db source-app: image: ${TURNIERE_BLACKBOX_APP_IMAGE:-registry.gitlab.com/turniere/turniere-backend/production/commits:local} @@ -44,6 +48,9 @@ services: RAILS_SERVE_STATIC_FILES: "1" ports: - "${TURNIERE_BLACKBOX_SOURCE_HOST_PORT:-3002}:3000" + networks: + - source-db + - sync follower-app: image: ${TURNIERE_BLACKBOX_APP_IMAGE:-registry.gitlab.com/turniere/turniere-backend/production/commits:local} @@ -62,6 +69,9 @@ services: RAILS_SERVE_STATIC_FILES: "1" ports: - "${TURNIERE_BLACKBOX_FOLLOWER_HOST_PORT:-3003}:3000" + networks: + - follower-db + - sync e2e: 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_PASSWORD: ${TURNIERE_E2E_ALT_PASSWORD:-password123} TURNIERE_E2E_ALT_USERNAME: ${TURNIERE_E2E_ALT_USERNAME:-e2e-alt-user} + networks: + - sync volumes: turniere-blackbox-follow-source-postgres: turniere-blackbox-follow-follower-postgres: + +networks: + source-db: + internal: true + follower-db: + internal: true + sync: diff --git a/tasks.py b/tasks.py index 206f382..ed68ca1 100644 --- a/tasks.py +++ b/tasks.py @@ -994,6 +994,55 @@ def docker_blackbox_follow_up( 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") def docker_blackbox_follow_down( c,