Files
computer-playbook/cli/meta/roles/applications/dynamic_services.py
kevinveenbirkenbachandClaude Opus 5 7bcba20f9d test(environment): derive the disable set instead of listing it
07_compose_minimal.sh claimed to disable 'all variable (group-conditional)
services' and then named seven by hand. tor was not among them, so a
deploy meant to be minimal pulled svc-net-tor, which hard-depends on
web-svc-mirror, whose group-conditional prometheus flag the closure
resolver treats as true — dragging the whole web stack in. Steps 12 and
13 passed no disable at all, which is why a mariadb swarm test deployed
keycloak, mailu, matomo, prometheus and dashboard.

The set is now derived from the contract the repo already enforces
(tests/integration/roles/meta/services/test_dynamic_flags.py): a key is
group-conditional when enabled OR shared is a Jinja expression. Checking
only enabled would miss mariadb, postgres, redis, memcached, typesense.

Exclusions are load-bearing, not cosmetic: dashboard because disabling
the primary app aborts init; javascript because it aliases cdn, which
web-app-dashboard declares a hard dependency with no runtime fallback;
mariadb and postgres because steps 12 and 13 test them.

variable_services assigns to a local first — disable="$(cmd)" does not
set the exit status of a simple command, so a failing CLI would have
yielded an empty disable set and steps 12/13 would have run the full
deploy and reported green.

roundtrip could not take disable at all; it now reaches both the compose
and the swarm leg.

Not verified by a deploy: 33 of the keys are new to step 07, and the
variant axis of svc-db-mariadb/svc-db-postgres collapses because tor is
its only dimension.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-06 10:31:05 +02:00

73 lines
2.4 KiB
Python

"""Print the group-conditional service keys, comma-separated for ``disable=``.
A service key is group-conditional when some role declares it with a Jinja
``enabled`` expression rather than a literal, i.e. the shape
``tests/integration/roles/meta/services/test_dynamic_flags.py`` enforces.
Those keys are the ones a test deploy may drop without changing what the
role under test is: their providers are co-deployed only because a host
happens to be in the provider's group.
Keys without a provider role are omitted — ``disable=`` cannot act on them.
"""
from __future__ import annotations
import argparse
import sys
from pathlib import Path
from cli.administration.inventory.provision.services_disabler import find_provider_roles
from utils.cache.yaml import load_yaml_any
from utils.roles.mapping import ROLE_FILE_META_SERVICES
def dynamic_service_keys(roles_dir: Path) -> list[str]:
"""Return every group-conditional service key that has a provider role."""
keys: set[str] = set()
for role_dir in sorted(roles_dir.iterdir()):
if not role_dir.is_dir():
continue
services = load_yaml_any(
str(role_dir / ROLE_FILE_META_SERVICES), default_if_missing={}
)
if not isinstance(services, dict):
continue
for key, conf in services.items():
if not isinstance(conf, dict):
continue
if any(
isinstance(conf.get(flag), str) and "{{" in conf[flag]
for flag in ("enabled", "shared")
):
keys.add(str(key))
if not keys:
return []
providers = find_provider_roles(sorted(keys), roles_dir)
return sorted(k for k in keys if k in providers)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--roles-dir",
default="roles",
help="roles directory to scan (default: roles)",
)
parser.add_argument(
"--exclude",
nargs="*",
default=[],
metavar="KEY",
help="service keys to keep enabled, e.g. the key the deploy is testing",
)
args = parser.parse_args(sys.argv[1:] if argv is None else argv)
excluded = {k for k in args.exclude if k}
keys = [k for k in dynamic_service_keys(Path(args.roles_dir)) if k not in excluded]
print(",".join(keys))
return 0
if __name__ == "__main__":
raise SystemExit(main())