mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 19:01:54 +00:00
Refactors four roles that previously used `# nocheck: compose-chdir-in-task` /
`compose-verb-in-task` / `container-exec-resolver` markers to follow the
canonical swarm-aware pattern:
- web-app-bigbluebutton: adds BBB_POSTGRES_CONTAINER_ADDRESS /
BBB_GREENLIGHT_CONTAINER_ADDRESS vars; 02a_postgres_password_sync.yml +
03_administrator.yml use `container exec -i {{ ..._CONTAINER_ADDRESS | quote }}`;
02_compose.yml's compose-build-phase tasks (generate-compose +
prebuild etherpad) move into the new
02b_bootstrap_compose_project.yml (>3 entries cannot live in a block)
under a single `when: DEPLOYMENT_MODE \!= "swarm"` include guard.
- web-app-baserow: BASEROW_APP_CONTAINER_ADDRESS var + 02_manager_ops.yml
converted to container exec + container restart canonical pattern.
- web-app-matomo: MATOMO_APP_CONTAINER_ADDRESS var; 07_patch_database.yml
swaps MATOMO_LOCAL_CID for the proper resolver-backed address.
- web-app-matrix: 01_docker.yml's two consecutive `compose pull` /
`compose up` tasks extract into 03_compose_pull_and_up.yml (with the new
`# include-gated: when: DEPLOYMENT_MODE \!= "swarm"` header marker) and
the previous 03_webserver.yml renames to 04_webserver.yml to keep the
numeric file sequence contiguous.
Supporting lint infrastructure changes so file-level include-gated
sub-task lists survive the swarm-compat lints:
- utils/annotations/task_gate.py: `is_task_compose_only_gated` now walks
up nested parent blocks (a single `when: DEPLOYMENT_MODE \!= 'swarm'`
on `- block:` exempts every child task underneath) and a new
`is_file_compose_only_by_header` recognises files declaring
`# include-gated: when: DEPLOYMENT_MODE \!= "swarm"` in their first
five lines as compose-only-by-include.
- tests/lint/ansible/shell/test_no_compose_chdir_in_shell_tasks.py and
test_no_compose_verb_in_shell_tasks.py call the header helper before
the per-line scan, so include-only sub-files are exempt.
- tests/unit/utils/annotations/test_task_gate.py covers both new
behaviours (recursive parent walk plus file header) end-to-end.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
105 lines
3.8 KiB
Python
105 lines
3.8 KiB
Python
"""Detect Ansible task-level `when:` gates that exempt compose-only
|
|
shell calls from swarm-compat lints.
|
|
|
|
A task whose `when:` evaluates compose-only at deploy time (e.g.
|
|
`when: DEPLOYMENT_MODE != 'swarm'` or `when: DEPLOYMENT_MODE == 'compose'`)
|
|
never reaches a swarm host, so `compose <verb>` / `chdir: directories.instance`
|
|
inside that task body is legitimate. This module gives the lints a
|
|
heuristic to recognise that gate without parsing YAML.
|
|
|
|
The detection is line-based: the task block is the span between the
|
|
nearest `- name:` (at indent N) above the offending line and the next
|
|
top-level structural marker (`- name:` at indent <= N, or end of
|
|
file). Within that span we look for any `when:` clause whose textual
|
|
value matches a compose-only DEPLOYMENT_MODE expression.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from collections.abc import Sequence
|
|
|
|
_TASK_NAME_RE = re.compile(
|
|
r"^(?P<indent>\s*)-\s+(?:name|block|include_tasks|import_tasks|hosts)\s*:"
|
|
)
|
|
_WHEN_RE = re.compile(r"^\s*when\s*:\s*(?P<expr>.+?)\s*$")
|
|
_COMPOSE_ONLY_EXPR = re.compile(
|
|
r"DEPLOYMENT_MODE\s*!=\s*['\"]swarm['\"]"
|
|
r"|DEPLOYMENT_MODE\s*==\s*['\"]compose['\"]"
|
|
)
|
|
_FILE_COMPOSE_ONLY_HEADER = re.compile(
|
|
r"^\s*#\s*include-gated\s*:\s*when\s*:\s*DEPLOYMENT_MODE\s*"
|
|
r"(?:!=\s*['\"]swarm['\"]|==\s*['\"]compose['\"])"
|
|
)
|
|
|
|
|
|
def is_file_compose_only_by_header(lines: Sequence[str]) -> bool:
|
|
"""True iff the file declares a top-of-file marker
|
|
|
|
# include-gated: when: DEPLOYMENT_MODE != "swarm"
|
|
|
|
that documents the entire file is meant to be `include_tasks:` from
|
|
a parent that already carries the compose-only `when:` guard. The
|
|
marker MUST sit in the first 5 lines.
|
|
"""
|
|
return any(_FILE_COMPOSE_ONLY_HEADER.search(raw) for raw in lines[:5])
|
|
|
|
|
|
def _task_block_bounds(lines: Sequence[str], idx: int) -> tuple[int, int]:
|
|
"""Return (start, end_exclusive) of the task block containing *idx*.
|
|
|
|
*start* is the index of the nearest `- name:` (or `- block:`/`- include_tasks:`)
|
|
above *idx*. *end* is the index of the next sibling `- name:` at the same
|
|
indent, or `len(lines)` if none.
|
|
"""
|
|
start = -1
|
|
indent = ""
|
|
for i in range(idx, -1, -1):
|
|
m = _TASK_NAME_RE.match(lines[i])
|
|
if m:
|
|
start = i
|
|
indent = m.group("indent")
|
|
break
|
|
if start < 0:
|
|
return (0, len(lines))
|
|
|
|
for j in range(start + 1, len(lines)):
|
|
m = _TASK_NAME_RE.match(lines[j])
|
|
if m and len(m.group("indent")) <= len(indent):
|
|
return (start, j)
|
|
return (start, len(lines))
|
|
|
|
|
|
def is_task_compose_only_gated(lines: Sequence[str], idx: int) -> bool:
|
|
"""True iff the task body containing *idx* — or any enclosing parent
|
|
`- block:` / `- name:` block — carries a `when:` clause whose textual
|
|
expression evaluates compose-only at deploy time. Walks upwards to
|
|
recognise block-level gates: a single `when: DEPLOYMENT_MODE !=
|
|
'swarm'` on a parent `- block:` covers every child task underneath.
|
|
"""
|
|
cur_idx = idx
|
|
while True:
|
|
start, end = _task_block_bounds(lines, cur_idx)
|
|
for k in range(start, end):
|
|
m = _WHEN_RE.match(lines[k])
|
|
if m and _COMPOSE_ONLY_EXPR.search(m.group("expr")):
|
|
return True
|
|
if start <= 0:
|
|
return False
|
|
marker = _TASK_NAME_RE.match(lines[start])
|
|
if not marker:
|
|
return False
|
|
cur_indent = len(marker.group("indent"))
|
|
parent_idx = None
|
|
for i in range(start - 1, -1, -1):
|
|
m = _TASK_NAME_RE.match(lines[i])
|
|
if m and len(m.group("indent")) < cur_indent:
|
|
parent_idx = i
|
|
break
|
|
if parent_idx is None:
|
|
return False
|
|
cur_idx = parent_idx
|