diff --git a/pithead b/pithead index 45d7328b..9cd93256 100755 --- a/pithead +++ b/pithead @@ -176,10 +176,33 @@ resolve_pull_policy() { else printf 'missing'; fi } +# #795: `compose up --remove-orphans` never removes the container of a service whose profile just +# went inactive — the service is still in the compose file, so compose does not count it as an +# orphan and leaves it running (a tari local→remote switch left the old minotari_node up, offline +# and re-syncing, against a remote-mode config). Reconcile from the committed .env instead: every +# profile-gated service whose profile token is absent gets its container stopped and removed. +# `compose rm` resolves an explicitly named service whatever the active profiles, and exits 0 when +# no container exists, so running this before every up is a cheap no-op in the steady state and +# also heals a box already stuck with a stale node container. +remove_deactivated_profile_containers() { + local profiles gone=() + profiles=",$(env_get COMPOSE_PROFILES)," + [[ "$profiles" == *,local_node,* ]] || gone+=(monerod) + [[ "$profiles" == *,local_tari,* ]] || gone+=(tari) + [[ "$profiles" == *,payout_confirm,* ]] || gone+=(wallet-rpc) + [[ "$profiles" == *,tari_payout_confirm,* ]] || gone+=(tari-wallet) + [ "${#gone[@]}" -eq 0 ] && return 0 + docker compose rm -sf "${gone[@]}" >/dev/null 2>&1 || + warn "Could not remove the deactivated container(s): ${gone[*]} — remove them manually with 'docker rm -f ${gone[*]}'." +} + # Run `docker compose up` with live output; on failure, explain a bridge-subnet collision (#180) if # that's what Docker rejected. Returns compose's own exit code. compose_up_checked() { local tmp out rc + # Deactivated-profile containers go BEFORE the up (#795): the old local node must stop before + # p2pool (re)starts against the remote one, not linger beside it. + remove_deactivated_profile_containers tmp="$(mktemp)" docker compose up --pull "$(resolve_pull_policy)" "$@" 2>&1 | tee "$tmp" rc=${PIPESTATUS[0]} @@ -5224,8 +5247,9 @@ apply() { # commit above (never before the operator said yes) and under the marker, so a failed move is # retried; the recreate below then mounts the migrated directory. migrate_dashboard_data - # Compose recreates only the services whose resolved config changed; --remove-orphans - # drops monerod when a local→remote switch deactivates the local_node profile. + # Compose recreates only the services whose resolved config changed. --remove-orphans covers + # services that left the compose file entirely; a profile-deactivated service is NOT an orphan + # to compose, so compose_up_checked removes those containers itself before the up (#795). if ! compose_up_checked -d --remove-orphans; then warn "Config files were updated but containers were NOT recreated ('docker compose up' failed)." warn "Fix the cause shown above, then re-run '$0 apply' (it will retry the recreate) — or '$0 up'." diff --git a/tests/stack/run.sh b/tests/stack/run.sh index 611bce22..7caa6698 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -3384,6 +3384,39 @@ out="$(cd "$V" && PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" assert_rc "invalid tari.mode rejected" "$?" "1" assert_contains "invalid tari.mode message" "$out" "tari.mode must be" +echo "== black-box: profile deactivation removes the old container (#795) ==" +# `compose up --remove-orphans` does not remove a profile-deactivated service's container — the +# service is still in the compose file, so compose does not count it as an orphan — and a tari +# local→remote switch left the old node running (offline, re-syncing) against a remote-mode +# config. Apply must issue an explicit `compose rm -sf` for every profile-gated service whose +# profile is off, BEFORE the recreate up. Asserted through the docker-stub call log. +# (1) Baseline, both nodes local: the reconcile list is exactly the (off) payout-confirm wallets — +# neither node container is touched. +seed_env +printf '{ "monero": {"mode":"local","wallet_address":"%s","node_username":"u","node_password":"p"}, "tari":{"wallet_address":"T"}, "p2pool":{"pool":"main"}, "dashboard":{"secure":true,"host":"box.lan"} }\n' "$WALLET" >"$V/config.json" +SWLOG="$V/docker-switch.log" +: >"$SWLOG" +out="$(cd "$V" && DOCKER_LOG="$SWLOG" PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "baseline local-nodes apply exits 0" "$?" "0" +assert_contains "local nodes: only the off payout wallets reconcile" "$(cat "$SWLOG")" "compose rm -sf wallet-rpc tari-wallet" +# (2) tari local→remote (the #795 reproduction): the tari container joins the removal list. No +# re-seed — the committed local-mode .env from (1) makes this a real transition. +printf '{ "monero": {"mode":"local","wallet_address":"%s","node_username":"u","node_password":"p"}, "tari":{"wallet_address":"T","mode":"remote","remote":{"host":"tari.example.com"}}, "p2pool":{"pool":"main"}, "dashboard":{"secure":true,"host":"box.lan"} }\n' "$WALLET" >"$V/config.json" +: >"$SWLOG" +out="$(cd "$V" && DOCKER_LOG="$SWLOG" PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "tari local-to-remote switch applies cleanly" "$?" "0" +assert_contains "switch removes the deactivated tari container" "$(cat "$SWLOG")" "compose rm -sf tari wallet-rpc tari-wallet" +# The removal must precede the recreate up — the point is the old node never runs beside the +# remote-mode p2pool, not that it eventually disappears. +assert_contains "tari removal happens before the recreate up" "$(sed '/compose up --pull never -d --remove-orphans/q' "$SWLOG")" "compose rm -sf tari wallet-rpc tari-wallet" +# (3) monerod rides the same guard on a monero local→remote switch. +seed_env +printf '{ "monero": {"mode":"remote","remote":{"host":"node.example"},"wallet_address":"%s","node_username":"u","node_password":"p"}, "tari":{"wallet_address":"T"}, "p2pool":{"pool":"main"}, "dashboard":{"secure":true,"host":"box.lan"} }\n' "$WALLET" >"$V/config.json" +: >"$SWLOG" +out="$(cd "$V" && DOCKER_LOG="$SWLOG" PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "monero local-to-remote switch applies cleanly" "$?" "0" +assert_contains "switch removes the deactivated monerod container" "$(cat "$SWLOG")" "compose rm -sf monerod wallet-rpc tari-wallet" + echo "== black-box: monero.rpc_lan_access + prep_blocks_threads reflect into .env (#523) ==" # The rendered .env must match the config input. rpc_lan_access gates the monerod RPC bind: default # (unset) keeps it localhost-only; true opens it to the LAN. prep_blocks_threads overrides the