From 61b7d2a96a08f1b802f43882d41bbaac7c691e2d Mon Sep 17 00:00:00 2001 From: Gmer4Lfe Date: Sat, 22 Aug 2026 14:03:57 -0400 Subject: [PATCH] Partnership stacks are the owner's declaration, so a mirror must read them from the owner's conf --- Plugin/unraid/api/fallback_coverage.php | 81 ++++++++++++++++--------- 1 file changed, 51 insertions(+), 30 deletions(-) diff --git a/Plugin/unraid/api/fallback_coverage.php b/Plugin/unraid/api/fallback_coverage.php index 02ac2be..1d07765 100644 --- a/Plugin/unraid/api/fallback_coverage.php +++ b/Plugin/unraid/api/fallback_coverage.php @@ -88,27 +88,38 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { } sort($containers, SORT_NATURAL | SORT_FLAG_CASE); - // Containers the partner already runs continuously, because onboard deployed them there. - // They cannot be failover material: there is nothing to start during an outage that is not - // already up, and a tier assignment for one would be a delay applied to a running container. + // Stacks are declared by the PARTNERSHIP OWNER and deployed to everyone, so on a mirror they + // are not in this host's conf at all — HOST2_PARTNERSHIP_AUTH_STACK is the shipped template, + // still commented out, while the eight auth containers it describes run there permanently + // because the owner put them there. Reading "this host's" stacks left the mirror's card + // showing every one of them as ordinary, selectable, uncovered. // - // The stacks store XML template filenames, and their case does not have to agree with the - // container's — this conf ships "my-prowlarr.xml" against a container named "Prowlarr" — so - // the mapping is done case-insensitively, once, here. + // Owner's conf first, then this host's, unioned: on the owner the two are the same file, and + // a host that declares extras of its own still has them honoured. The owner's copy reaches a + // mirror through the conf_sync RAM cache, which vv_read_host_conf_raw() knows how to find. + $ownerSlot = strtolower(vv_parse_conf_scalar(vv_read_conf_raw('master.conf'), 'PARTNERSHIP_OWNER_HOST')); + $stackSrc = []; + foreach (array_unique(array_filter([$ownerSlot, $hostId])) as $slot) { + $stackSrc[strtoupper($slot)] = vv_read_host_conf_raw($slot); + } + $stackOf = []; $byLower = []; foreach ($containers as $n) $byLower[strtolower($n)] = $n; - foreach ([ - 'auth' => "{$hostUp}_PARTNERSHIP_AUTH_STACK", - 'arrs' => "{$hostUp}_PARTNERSHIP_ARR_STACK", - 'services' => "{$hostUp}_PARTNERSHIP_SERVICES_STACK", - ] as $label => $var) { - foreach (vv_parse_conf_list($raw, $var) as $xml) { - $n = preg_replace('/^my-|\.xml$/', '', trim($xml)); - if ($n === '') continue; - // The container's real name where this host has one, the template's otherwise: a - // stack entry for something not installed here is still a stack entry. - $stackOf[$byLower[strtolower($n)] ?? $n] = $label; + foreach ($stackSrc as $id => $srcRaw) { + foreach ([ + 'auth' => "{$id}_PARTNERSHIP_AUTH_STACK", + 'arrs' => "{$id}_PARTNERSHIP_ARR_STACK", + 'services' => "{$id}_PARTNERSHIP_SERVICES_STACK", + ] as $label => $var) { + foreach (vv_parse_conf_list($srcRaw, $var) as $xml) { + $n = preg_replace('/^my-|\.xml$/', '', trim($xml)); + if ($n === '') continue; + // Only what this host actually runs. The owner's stack lists everything it + // deploys mesh-wide; a name with no container here is not "always up" here. + if (!isset($byLower[strtolower($n)])) continue; + $stackOf[$byLower[strtolower($n)]] = $label; + } } } @@ -151,19 +162,26 @@ foreach ($TIERS as $t) { } -// Stack containers refused here, not only greyed out in the picker. The card disables their -// selects, but a disabled input is a courtesy to the operator, not a constraint on the endpoint — -// and a tier assignment for a container the partner already runs permanently is a delay attached -// to something that never stops. +// Stack containers refused here, not only greyed out in the picker. A disabled select is a +// courtesy to the operator, not a constraint on the endpoint. +// +// Sourced from the OWNER's conf as well as this host's, for the same reason the read path is: on +// a mirror the stack it runs is the owner's declaration, and checking only the local conf would +// have let a mirror assign a fallback tier to a container that never stops. +$ownerSlotW = strtolower(vv_parse_conf_scalar(vv_read_conf_raw('master.conf'), 'PARTNERSHIP_OWNER_HOST')); $stackNames = []; -foreach ([ - "{$hostUp}_PARTNERSHIP_AUTH_STACK", - "{$hostUp}_PARTNERSHIP_ARR_STACK", - "{$hostUp}_PARTNERSHIP_SERVICES_STACK", -] as $var) { - foreach (vv_parse_conf_list($existingRaw, $var) as $xml) { - $n = preg_replace('/^my-|\.xml$/', '', trim($xml)); - if ($n !== '') $stackNames[strtolower($n)] = true; +foreach (array_unique(array_filter([$ownerSlotW, $hostId])) as $slot) { + $srcRaw = vv_read_host_conf_raw($slot); + $id = strtoupper($slot); + foreach ([ + "{$id}_PARTNERSHIP_AUTH_STACK", + "{$id}_PARTNERSHIP_ARR_STACK", + "{$id}_PARTNERSHIP_SERVICES_STACK", + ] as $var) { + foreach (vv_parse_conf_list($srcRaw, $var) as $xml) { + $n = preg_replace('/^my-|\.xml$/', '', trim($xml)); + if ($n !== '') $stackNames[strtolower($n)] = true; + } } } $map = json_decode((string)($_POST['tiers'] ?? ''), true); @@ -179,8 +197,11 @@ foreach ($map as $name => $tier) { echo json_encode(['ok' => false, 'error' => "No container named $name on this host"]); exit; } if (isset($stackNames[strtolower((string)$name)])) { + // Direction-neutral wording: on the owner this container is deployed TO the partner, on a + // mirror it was deployed HERE by the owner. Both mean the same thing for coverage — it + // runs on both nodes continuously, so there is nothing for a tier to start. echo json_encode(['ok' => false, - 'error' => "$name is deployed to the partner as part of a stack — it already runs there, so it cannot be given a fallback tier"]); exit; + 'error' => "$name belongs to a partnership stack — it runs on both nodes continuously, so it cannot be given a fallback tier"]); exit; } $byTier[$t][] = $known[strtolower((string)$name)]; }