diff --git a/scripts/tests/deploy/swarm/routine/03_wait_converge.sh b/scripts/tests/deploy/swarm/routine/03_wait_converge.sh index e82ba59c69..a4927736b7 100755 --- a/scripts/tests/deploy/swarm/routine/03_wait_converge.sh +++ b/scripts/tests/deploy/swarm/routine/03_wait_converge.sh @@ -1,8 +1,4 @@ #!/usr/bin/env bash -# Wait until every stack service converges. With NFS shared storage the DB -# dep runs as its own single-replica swarm service (volumes on NFS, -# schedulable on any node), named "_" like the app service; the -# check list is empty when APP_ID has no DB dep. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" @@ -11,8 +7,10 @@ source "${SCRIPT_DIR}/../utils/_context.sh" skip_if_no_swarm_service -DB_SERVICE="" -[ "${DB_DEP}" != "none" ] && DB_SERVICE="${DB_DEP}_${DB_DEP}" +DB_SERVICE="$(resolve_db_service)" +if [ "${DB_DEP}" != "none" ] && [ -z "${DB_SERVICE}" ]; then + echo "SKIP db gate: ${ENTITY} declares a ${DB_DEP} dep but no swarm service serves it" +fi converged=false for i in $(seq 1 90); do @@ -40,7 +38,7 @@ for i in $(seq 1 90); do ") fi - echo "[${i}] ${ENTITY}: ${app_replicas} | state: ${app_state} | db(${DB_DEP}): ${db_replicas} ${db_state}" + echo "[${i}] ${ENTITY}: ${app_replicas} | state: ${app_state} | db(${DB_SERVICE}): ${db_replicas} ${db_state}" app_ok="false" if [ -n "${app_replicas}" ] && diff --git a/scripts/tests/deploy/swarm/utils/_context.sh b/scripts/tests/deploy/swarm/utils/_context.sh index 0e198ed515..f77c1492fb 100755 --- a/scripts/tests/deploy/swarm/utils/_context.sh +++ b/scripts/tests/deploy/swarm/utils/_context.sh @@ -114,6 +114,23 @@ has_swarm_service() { printf '%s\n' "${_names}" | grep -qxF "${SERVICE_NAME}" } +# Prints the swarm service carrying APP_ID's database. A role that pins its own +# engine runs it as _database inside its own stack, a central provider +# as _ in its own (plugins/lookup/database.py). +resolve_db_service() { + local _names="" + [ "${DB_DEP}" != "none" ] || return 0 + [ -n "${MGR:-}" ] || return 0 + if ! _names="$(timeout 30 docker exec "${MGR}" docker service ls --format '{{.Name}}' 2>/dev/null)"; then + return 0 + fi + if printf '%s\n' "${_names}" | grep -qxF "${STACK_NAME}_database"; then + printf '%s\n' "${STACK_NAME}_database" + elif printf '%s\n' "${_names}" | grep -qxF "${DB_DEP}_${DB_DEP}"; then + printf '%s\n' "${DB_DEP}_${DB_DEP}" + fi +} + # 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() { diff --git a/scripts/tests/deploy/swarm/utils/collect/diagnostics.sh b/scripts/tests/deploy/swarm/utils/collect/diagnostics.sh index 3e0e8d9777..3fcdc4112a 100755 --- a/scripts/tests/deploy/swarm/utils/collect/diagnostics.sh +++ b/scripts/tests/deploy/swarm/utils/collect/diagnostics.sh @@ -58,14 +58,16 @@ done' if [ "${DB_DEP}" = "mariadb" ]; then sep "mariadb-env" "live mariadb container env (MARIADB* only, value prefix redacted)" - MARIADB_CID=$(dexec "${MGR}" sh -c \ - 'docker ps --filter name=mariadb --format "{{.ID}}" | head -n1') + DB_SERVICE="$(resolve_db_service)" + MARIADB_CID="" + [ -z "${DB_SERVICE}" ] || MARIADB_CID=$(dexec "${MGR}" sh -c \ + "docker ps --filter name=${DB_SERVICE} --format '{{.ID}}' | head -n1") if [ -n "${MARIADB_CID}" ]; then dexec "${MGR}" docker exec "${MARIADB_CID}" sh -c \ 'env | grep -E "^MARIADB|^MYSQL" | sed "s/=\(.\{1,3\}\).*/=\1...(redacted)/"' || echo "(failed to exec into ${MARIADB_CID})" else - echo "(no live mariadb container found)" + echo "(no live container for db service '${DB_SERVICE}')" fi fi diff --git a/tests/unit/python/scripts/tests/deploy/swarm/utils/test_resolve_db_service.py b/tests/unit/python/scripts/tests/deploy/swarm/utils/test_resolve_db_service.py new file mode 100644 index 0000000000..0eb816192d --- /dev/null +++ b/tests/unit/python/scripts/tests/deploy/swarm/utils/test_resolve_db_service.py @@ -0,0 +1,92 @@ +"""Which swarm service the converge gate accepts as a role's database. + +``resolve_db_service`` in ``scripts/tests/deploy/swarm/utils/_context.sh`` picks +between two naming schemes that ``plugins/lookup/database.py`` keeps apart: a +role pinning its own engine runs it as ``_database`` inside its own +stack, a central provider as ``_`` in its own. Guessing one of them +cost run 32630684501 a job - ``mariadb_mariadb`` never existed for Magento - and +the same guess passed vacuously in the all-bonds variant, where an unrelated +role had dragged the central provider in. Pinned here: the local engine wins +when both are deployed, the central name is still used when only it exists, and +a name that is deployed for nobody yields nothing instead of a false match. +""" + +from __future__ import annotations + +import os +import subprocess +import tempfile +import unittest +from pathlib import Path + +from utils.cache.files import PROJECT_ROOT + +CONTEXT = ( + PROJECT_ROOT / "scripts" / "tests" / "deploy" / "swarm" / "utils" / "_context.sh" +) +PINNED_ENGINE_APP = "web-app-magento" +NO_DB_APP = "web-svc-html" +LOCAL_SERVICE = "magento_database" +CENTRAL_SERVICE = "mariadb_mariadb" + + +class TestResolveDbService(unittest.TestCase): + def _resolve(self, app_id: str, deployed: tuple[str, ...]) -> str: + with tempfile.TemporaryDirectory() as tmp: + stub_bin = Path(tmp) / "bin" + stub_bin.mkdir() + docker = stub_bin / "docker" + docker.write_text( + "#!/usr/bin/env bash\nprintf '%s\\n' {}\n".format( + " ".join(f"'{name}'" for name in deployed) or '""' + ) + ) + docker.chmod(0o755) + + env = dict(os.environ) + env.update( + APP_ID=app_id, + MGR="stub-manager", + PATH=f"{stub_bin}:{env['PATH']}", + BASH_ENV="", + ) + proc = subprocess.run( + ["bash", "-c", f'source "{CONTEXT}"; resolve_db_service'], + cwd=PROJECT_ROOT, + env=env, + capture_output=True, + text=True, + check=True, + ) + return proc.stdout.strip() + + def test_the_local_engine_wins_when_the_central_one_is_also_deployed(self) -> None: + self.assertEqual( + self._resolve(PINNED_ENGINE_APP, (LOCAL_SERVICE, CENTRAL_SERVICE)), + LOCAL_SERVICE, + ) + + def test_the_local_engine_is_found_when_it_is_the_only_one(self) -> None: + self.assertEqual( + self._resolve(PINNED_ENGINE_APP, (LOCAL_SERVICE,)), LOCAL_SERVICE + ) + + def test_the_central_provider_is_used_when_no_local_engine_exists(self) -> None: + self.assertEqual( + self._resolve(PINNED_ENGINE_APP, (CENTRAL_SERVICE,)), CENTRAL_SERVICE + ) + + def test_a_substring_of_a_deployed_name_is_not_a_match(self) -> None: + self.assertEqual( + self._resolve(PINNED_ENGINE_APP, ("other_mariadb_mariadb_replica",)), "" + ) + + def test_nothing_is_resolved_when_neither_scheme_is_deployed(self) -> None: + self.assertEqual(self._resolve(PINNED_ENGINE_APP, ("magento_nginx",)), "") + + def test_a_role_without_a_database_dep_resolves_nothing(self) -> None: + self.assertEqual(self._resolve(NO_DB_APP, (CENTRAL_SERVICE,)), "") + + +if __name__ == "__main__": + unittest.main()