Files
computer-playbook/plugins/lookup/compose_replicas.py
T
kevinveenbirkenbachandClaude Opus 4.7 d194c1fa26 feat(sys-svc-container): SPOT compose_replicas lookup + replica-handler lint
Add the compose_replicas lookup plugin as the single source of truth for
swarm replica calculation. Default emits `replicas: len(groups[application_id])`
(topology-driven); override term forces an explicit value. In compose mode
the lookup returns the empty string because docker-compose ignores
deploy.replicas.

Refactor deploy.yml.j2 to use the lookup. The previous service_replicas
default in defaults/main.yml is no longer needed and is dropped.

Add a lint test that walks every roles/*/templates/compose*.yml.j2 and
verifies each service entry under the top-level services: declares
replica handling via one of three valid paths:

  1. Include sys-svc-container/templates/base.yml.j2 (full inheritance).
  2. Include sys-svc-container/templates/deploy.yml.j2 directly (escape
     hatch for services that own their restart/logging/env_file and
     therefore can't use base.yml.j2 without duplicate-key conflicts).
  3. Call lookup('compose_replicas', ...) explicitly for handcrafted
     deploy: blocks.

Fix the 7 web-app compose templates flagged by the new lint (bluesky,
dashboard, magento, matrix, moodle, nextcloud, opentalk) by including
deploy.yml.j2 directly and setting service_name as a SPOT before each
service entry, so the service name in the yaml key and the resource_filter
lookups in deploy.yml.j2 both flow from one variable.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 18:19:03 +02:00

75 lines
2.5 KiB
Python

"""Lookup `compose_replicas`: emit `replicas: <N>` only in swarm mode.
Single SPOT for swarm replica calculation across the repo.
Default value: `len(groups[application_id])` - one task per host in the
application's inventory group. Single-host inventories yield 1; multi-
host swarm inventories yield N matching the topology.
Override: pass an explicit value as the first term.
Examples:
{{ lookup('compose_replicas') }} # topology default
{{ lookup('compose_replicas', 1) }} # force replicas: 1
{{ lookup('compose_replicas', N) }} # force replicas: N
In compose mode the lookup returns the empty string because
docker-compose ignores `deploy.replicas`. Wrapping the emit in the
`deploy:` block remains the caller's responsibility - typically
`roles/sys-svc-container/templates/deploy.yml.j2`.
"""
from __future__ import annotations
import contextlib
from typing import Any
from ansible.errors import AnsibleError
from ansible.plugins.lookup import LookupBase
class LookupModule(LookupBase):
def run(
self,
terms: list[Any] | None,
variables: dict[str, Any] | None = None,
**kwargs: Any,
) -> list[str]:
if terms and len(terms) > 1:
raise AnsibleError(
"compose_replicas lookup accepts at most one term: an explicit replicas override"
)
vars_ = variables or getattr(self._templar, "available_variables", {}) or {}
templar = getattr(self, "_templar", None)
raw_mode = vars_.get("DEPLOYMENT_MODE", "compose")
if templar is not None:
with contextlib.suppress(Exception):
raw_mode = templar.template(raw_mode)
if str(raw_mode).strip() != "swarm":
return [""]
if terms:
replicas = terms[0]
else:
app_id = vars_.get("application_id")
if templar is not None and app_id is not None:
with contextlib.suppress(Exception):
app_id = templar.template(app_id)
groups = vars_.get("groups", {}) or {}
hosts = groups.get(app_id, []) if app_id else []
replicas = len(hosts or [])
try:
n = int(replicas)
except (TypeError, ValueError) as exc:
raise AnsibleError(
f"compose_replicas: cannot coerce '{replicas}' to int: {exc}"
) from exc
if n < 1:
n = 1
return [f"replicas: {n}"]