diff --git a/CHANGELOG.md b/CHANGELOG.md index fbaa05e9..28a9e41f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,18 @@ per the process in [`docs/dev/releasing.md`](docs/dev/releasing.md). than its labelled span never reads as a complete one. Both surfaces read one shared roll-up, so they cannot drift apart. +### Fixed + +- **A local→remote node switch now actually retires the node container (#795).** Switching + `tari.mode` (or `monero.mode`) to `remote` and running `apply` dropped the node's compose + profile as promised — but compose never stops the running container of a profile-disabled + service, and while it survived, any other change in the same apply (the Tari memory cap + re-renders on every switch) recreated the very node the switch had just turned off, leaving it + running offline against a remote-mode config. `apply` and `upgrade` now stop and remove every + container whose profile the new configuration switched off — the bundled nodes and the two + payout-confirmation wallets — before recreating anything. On-disk chain data is untouched, as + the preview always said. + ### Dependencies - Dashboard Python group (#788): `aiohttp` 3.14.3, `grpcio` 1.83.0 (floors still satisfy the diff --git a/pithead b/pithead index 45d7328b..c510dfe6 100755 --- a/pithead +++ b/pithead @@ -189,6 +189,31 @@ compose_up_checked() { return "$rc" } +# Stop and remove the containers of services whose compose profile is NOT active in the committed +# .env (#795). Compose does not retire these on its own: a profile-disabled service is not an +# orphan (`up --remove-orphans` ignores it), and a service whose container still exists is treated +# as enabled again by newer compose — so after a local→remote node switch, the next resolved-config +# diff (e.g. a memory-cap change riding in the SAME apply) recreated the very node the switch had +# just turned off, leaving it running offline against a remote-mode config. Called after the .env +# commit and before `compose up` on the paths that re-render .env (apply, upgrade); keyed off the +# committed .env rather than this run's diff so an interrupted apply's retry (the #125 marker +# path) still converges. Removing a container never touches its bind-mounted on-disk data. +remove_deactivated_profile_containers() { + local profiles pair svc token + profiles=$(env_get COMPOSE_PROFILES) + # Every profile-gated service in docker-compose.yml, as service:profile. + for pair in monerod:local_node tari:local_tari wallet-rpc:payout_confirm tari-wallet:tari_payout_confirm; do + svc=${pair%%:*} + token=${pair##*:} + case ",$profiles," in *",$token,"*) continue ;; esac + # --profile makes the switched-off service visible to compose for these two commands. + [ -n "$(docker compose --profile "$token" ps -aq "$svc" 2>/dev/null)" ] || continue + log "Removing the $svc container — it is switched off in this configuration (its on-disk data is kept)." + docker compose --profile "$token" rm -sf "$svc" >/dev/null 2>&1 || + warn "Could not remove the $svc container — remove it by hand with 'docker rm -f $svc', then re-run '$0 apply'." + done +} + stack_up() { log "Starting stack..." warn_missing_data_dirs @@ -431,6 +456,9 @@ stack_upgrade() { # One-time move of the dashboard data out of the install dir (#455) — after the .env commit # (a failed move is retried on re-run) and before the recreate mounts the new location. migrate_dashboard_data + # upgrade re-renders .env from config.json (above), so a mode switch staged in config.json can + # land here instead of apply — retire any node the new profile set switched off (#795). + remove_deactivated_profile_containers # Source checkout: rebuild the images from build/. Release install: pull the new published images # instead — force a re-pull so a moved tag is refreshed (#44). if is_source_checkout; then @@ -5224,8 +5252,12 @@ 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. + # A node (or payout-wallet) whose profile this config switched off must be retired HERE — + # compose won't do it (#795): it is not an orphan, and its surviving container would make the + # `up` below recreate it off an unrelated env change (a memory cap) in this same apply. + remove_deactivated_profile_containers + # Compose recreates only the services whose resolved config changed; --remove-orphans still + # covers services dropped from docker-compose.yml itself across releases. 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..56921565 100755 --- a/tests/stack/run.sh +++ b/tests/stack/run.sh @@ -59,6 +59,14 @@ case "$*" in "exec tor cat /var/lib/tor/tari/hostname") echo "taria.onion" ;; "exec tor cat /var/lib/tor/p2pool/hostname") echo "p2pa.onion" ;; "exec p2pool cat /proc/1/cmdline") printf '%s' "${P2POOL_PROC1:-}" ;; # #273: tests set the running p2pool argv + # Fake per-service container state (#795): a flag file per service under $FAKE_CONTAINERS. + # `compose --profile X ps -aq svc` answers a cid while the file exists; `rm -sf svc` deletes it. + "compose --profile "*" ps -aq "*) + _svc="$*"; _svc="${_svc##* }" + if [ -n "${FAKE_CONTAINERS:-}" ] && [ -e "$FAKE_CONTAINERS/$_svc" ]; then echo "cid-$_svc"; fi ;; + "compose --profile "*" rm -sf "*) + _svc="$*"; _svc="${_svc##* }" + [ -z "${FAKE_CONTAINERS:-}" ] || rm -f "$FAKE_CONTAINERS/$_svc" ;; *hash-password*) # Fake `caddy hash-password` (#8): a per-password digest so enable/change paths differ, and it # never echoes the plaintext back (real bcrypt doesn't either) — keeps the leak checks honest. @@ -3384,6 +3392,51 @@ 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: local→remote switch retires the deactivated node container (#795) ==" +# A mode switch drops the node's profile token from COMPOSE_PROFILES, but compose never stops the +# running container of a profile-disabled service: it is not an orphan (`up --remove-orphans` +# ignores it), and while the container survives, newer compose treats the service as enabled +# again — live, the TARI_MEM_LIMIT diff that rides every tari switch (auto-calc → the remote-mode +# placeholder) recreated the very node the switch had just removed, leaving a full minotari node +# running offline against a remote-mode config. apply must end the run with the container ABSENT. +# The docker stub models container state as flag files under $FAKE_CONTAINERS (see make_stubs). +FAKECT="$V/containers-795" +SWLOG="$V/switch-docker.log" + +# Baseline: both nodes local, both node containers "running". +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" +out="$(cd "$V" && PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "both-local baseline applies cleanly" "$?" "0" +mkdir -p "$FAKECT" +touch "$FAKECT/monerod" "$FAKECT/tari" + +# (1) tari local→remote: the run must end with the tari container gone and monerod untouched. +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" && FAKE_CONTAINERS="$FAKECT" DOCKER_LOG="$SWLOG" PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "tari local→remote switch applies cleanly" "$?" "0" +[ ! -e "$FAKECT/tari" ] && ok "tari container is ABSENT after the switch" || bad "tari container is ABSENT after the switch" "container survived apply" +[ -e "$FAKECT/monerod" ] && ok "monerod container untouched by the tari switch" || bad "monerod container untouched by the tari switch" "monerod was removed" +assert_contains "operator is told the tari container is removed" "$out" "Removing the tari container" +# Ordering: the removal must land BEFORE the recreate `up`, or compose resurrects the node off +# the TARI_MEM_LIMIT diff riding this same apply. +seq="$(grep -oE 'compose --profile local_tari rm -sf tari|compose up' "$SWLOG" | tr '\n' ',')" +assert_eq "tari removal precedes the recreate up" "$seq" "compose --profile local_tari rm -sf tari,compose up," + +# (2) monerod has the same hole on ITS local→remote switch — same guard, same end state. +printf '{ "monero": {"mode":"remote","remote":{"host":"node.example"},"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" +out="$(cd "$V" && FAKE_CONTAINERS="$FAKECT" PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "monero local→remote switch applies cleanly" "$?" "0" +[ ! -e "$FAKECT/monerod" ] && ok "monerod container is ABSENT after the switch" || bad "monerod container is ABSENT after the switch" "container survived apply" +assert_contains "operator is told the monerod container is removed" "$out" "Removing the monerod container" + +# (3) Steady remote state: a later unrelated apply finds no deactivated container and stays quiet. +printf '{ "monero": {"mode":"remote","remote":{"host":"node.example"},"wallet_address":"%s","node_username":"u","node_password":"p"}, "tari":{"wallet_address":"T","mode":"remote","remote":{"host":"tari.example.com"}}, "p2pool":{"pool":"main"}, "proxy":{"donate_level":1}, "dashboard":{"secure":true,"host":"box.lan"} }\n' "$WALLET" >"$V/config.json" +out="$(cd "$V" && FAKE_CONTAINERS="$FAKECT" PATH="$V/bin:$PATH" ./pithead apply -y 2>&1)" +assert_rc "steady remote-mode apply stays clean" "$?" "0" +assert_not_contains "no removal message when nothing is running" "$out" "Removing the " + 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