fix(swarm): ask the stack for the gate's service instead of guessing from metadata

The converge gate polls STACK_NAME_PRIMARY_SERVICE_KEY. Four roles never
create that name and each was given a static `workload: node-local` to make
the gate skip itself:

  21fe9e984  web-svc-cdn      ships only cdn_build, never cdn_cdn
  2c6c8b763  web-app-matrix   names its services matrix_synapse and friends
  76508c946  svc-runner       deploys node-local compose projects per node
  cb52a72f2  bigbluebutton    phantom service, and the DR drill must still run

Four hand-written claims for one observable fact. The claim is also static
where the truth is not: web-app-discourse renders `services: {}` only while
its sidecars are shared, and job 94965456921 of run 31864448027 polled
discourse_discourse ninety times for a stack that had none. Declaring the
role node-local would have blinded the gate in the variants where its
sidecars are local and a stack does exist.

has_swarm_service asks the deployed stack for the exact name instead. Every
caller runs behind `if rc == 0`, so an absent service is the role's shape and
not a broken deploy. Exact-line matching, not docker's name filter, which
matches substrings.

No coverage is lost: 03_wait_converge cannot pass without that service, and
05/06/07 plus the backup drill only run once it has. The four roles keep the
skip they were given; discourse and every future case get it without a
declaration. The teardown branch in the drill keeps its meaning - stack rm
where the service exists, compose stop per node where it does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kevinveenbirkenbachandClaude Opus 5 committed 2026-08-16 03:38:46 +02:00
1 parent 18c9738efb
commit 246558a13e
7 files changed
+20 -42

No files matched your search

-1
View File
@@ -7,7 +7,6 @@ runner:
swarm:
enabled: true
lifecycle: beta
workload: node-local
proxy:
service_key: runner-1
github_owner: infinito-nexus
@@ -34,7 +34,6 @@ bigbluebutton:
bond: 1
enabled: true
shared: true
workload: node-local
repository: https://github.com/bigbluebutton/docker.git
ref: v3.0.23
recording:
-1
View File
@@ -113,7 +113,6 @@ matrix:
name: matrix
# nocheck: default-placement-manager inner docker-in-docker storage; node-local in swarm
placement: manager
workload: node-local
flavor: ansible
upstream:
repo: https://github.com/spantaleev/matrix-docker-ansible-deploy
-1
View File
@@ -7,7 +7,6 @@ dashboard:
cdn:
# nocheck: default-placement-manager the one-shot npm-mirror build must write the served CDN root, a node-local host bind openresty (manager-pinned) reads
placement: manager
workload: node-local
modes:
host:
enabled: true
@@ -111,10 +111,10 @@ flowchart TB
`utils/tests/swarm/backup_repos.py` derives both the manager's inventory
groups and the `remote-2-local` provider list from that closure; apps
without those roles prove the chain through the nfs repository.
- The workload form only changes step 6: stack workloads get
`docker stack rm`, `workload: node-local` roles get their compose
project stopped on every node. Every other step operates on volumes,
the NFS export and the deployed backup units, independent of the
orchestrator.
- The workload form only changes step 6: a stack that carries services gets
`docker stack rm`, a role whose compose renders none gets its compose
project stopped on every node. `has_swarm_service` decides by asking the
deployed stack. Every other step operates on volumes, the NFS export and
the deployed backup units, independent of the orchestrator.
- Runs once per app, in the variant-0 CI job, between the matrix's first
deploy and its update pass.
@@ -215,7 +215,7 @@ fi
echo " marker present on encrypted USB"
echo "==> [6/9] tear the stack down completely (full disaster) before recovery"
if [ "${HAS_SWARM_SERVICE}" = true ]; then
if has_swarm_service; then
docker exec "${MGR}" bash "${BKP_IN_NODE}/04_stack_rm_wait.sh" "${STACK_NAME}"
else
for _node in "${MGR}" "${WRK1}" "${WRK2}"; do
+14 -32
View File
@@ -51,33 +51,6 @@ if [ -f "${ROLE_DIR}/meta/services.yml" ] &&
DEFAULT_PLACEMENT_MANAGER=true
fi
HAS_SWARM_SERVICE="$(PYTHONPATH="${_REPO_ROOT}" "${PYTHON}" -c "
import os, yaml
svc = os.path.join('${ROLE_DIR}', 'meta', 'services.yml')
data = {}
if os.path.exists(svc):
data = yaml.safe_load(open(svc)) or {}
if not isinstance(data, dict):
data = {}
entity = data.get('${ENTITY}')
workload = entity.get('workload') if isinstance(entity, dict) else None
if isinstance(workload, str):
workload = workload.strip().lower()
if workload == 'node-local':
print('false')
raise SystemExit
if workload == 'stack':
print('true')
raise SystemExit
try:
from utils.roles.meta_lookup import get_role_skip
swarm_skipped = 'swarm' in get_role_skip('${APP_ID}')
except Exception:
swarm_skipped = False
has_image = any(isinstance(v, dict) and 'image' in v for v in data.values())
has_tpl = any(os.path.exists(os.path.join('${ROLE_DIR}', 'templates', t)) for t in ('compose.yml.j2', 'service.yml.j2'))
print('false' if (swarm_skipped or not (has_image or has_tpl)) else 'true')
")"
PROBE_PORT="$(PYTHONPATH="${_REPO_ROOT}" "${PYTHON}" -c "
import os, yaml
@@ -121,7 +94,7 @@ PRIMARY_NFS_VOLUME="${NFS_VOLUMES%%$'\n'*}"
NFS_CHECK_MOUNTPOINT="/mnt/nfs-check"
export APP_ID ENTITY STACK_NAME SERVICE_NAME PRIMARY_SERVICE_KEY CUSTOM_IMAGE_REPO DB_DEP NFS_VOLUMES PRIMARY_NFS_VOLUME DEFAULT_PLACEMENT_MANAGER HAS_SWARM_SERVICE PROBE_PORT NFS_CHECK_MOUNTPOINT
export APP_ID ENTITY STACK_NAME SERVICE_NAME PRIMARY_SERVICE_KEY CUSTOM_IMAGE_REPO DB_DEP NFS_VOLUMES PRIMARY_NFS_VOLUME DEFAULT_PLACEMENT_MANAGER PROBE_PORT NFS_CHECK_MOUNTPOINT
# Exits the calling chaos step (09/10/11) with success when the role is
# manager-pinned (no worker task exists to drain).
@@ -132,11 +105,21 @@ skip_chaos_if_manager_pinned() {
fi
}
# Exits the calling gate (07/09/10/11) with success when the role deploys no
has_swarm_service() {
local _names=""
[ -n "${MGR:-}" ] || return 1
if ! _names="$(docker exec "${MGR}" docker stack services \
--format '{{.Name}}' "${STACK_NAME}" 2>/dev/null)"; then
_names=""
fi
printf '%s\n' "${_names}" | grep -qxF "${SERVICE_NAME}"
}
# Exits the calling gate (03/05/06/07) with success when the role deploys no
# swarm service to converge or drain.
skip_if_no_swarm_service() {
if [ "${HAS_SWARM_SERVICE}" != true ]; then
echo "SKIP: ${ENTITY} (${APP_ID}) deploys no swarm service — converge/chaos gate does not apply"
if ! has_swarm_service; then
echo "SKIP: ${ENTITY} (${APP_ID}) deployed no swarm service — converge/chaos gate does not apply"
exit 0
fi
}
@@ -158,6 +141,5 @@ if [ "${SWARM_NFS_PILOT_VERBOSE:-0}" = "1" ]; then
echo " DB_DEP=${DB_DEP}"
echo " PRIMARY_NFS_VOLUME=${PRIMARY_NFS_VOLUME}"
echo " DEFAULT_PLACEMENT_MANAGER=${DEFAULT_PLACEMENT_MANAGER}"
echo " HAS_SWARM_SERVICE=${HAS_SWARM_SERVICE}"
echo " PROBE_PORT=${PROBE_PORT}"
fi