Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 34 additions & 2 deletions pithead
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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'."
Expand Down
53 changes: 53 additions & 0 deletions tests/stack/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down