3"""Validate scoped Buildah and CRI cleanup for deployed runner images."""
5from __future__
import annotations
9MANAGED_IMAGE_LABEL =
"com.ra8-firmware.managed=runner-fleet"
10MANAGED_IMAGE_KIND =
"com.ra8-firmware.image-kind"
11NamedTasks = dict[str, list[tuple[int, dict[str, object]]]]
14def _flatten_tasks(items: list[object]) -> list[dict[str, object]]:
15 """Return top-level and block-nested Ansible tasks in execution order."""
16 flattened: list[dict[str, object]] = []
18 if not isinstance(item, dict):
20 flattened.append(item)
21 for section
in (
"block",
"rescue",
"always"):
22 children = item.get(section)
23 if isinstance(children, list):
24 flattened.extend(_flatten_tasks(children))
28def _named_tasks(source: str) -> tuple[NamedTasks, list[str]]:
29 """Parse the deployed producer into uniquely addressable task candidates."""
31 value = yaml.safe_load(source)
32 except yaml.YAMLError:
33 return {}, [
"ci_runner image cleanup: task file is not valid YAML"]
34 if not isinstance(value, list):
35 return {}, [
"ci_runner image cleanup: task file is not a list"]
36 named: NamedTasks = {}
37 for index, item
in enumerate(_flatten_tasks(value)):
38 if isinstance(item, dict)
and isinstance(item.get(
"name"), str):
39 named.setdefault(item[
"name"], []).append((index, item))
43def _one(named: NamedTasks, name: str, errors: list[str]) -> tuple[int, dict[str, object]] |
None:
44 """Return one named task while making absence or duplication a finding."""
45 matches = named.get(name, [])
47 errors.append(f
"ci_runner image cleanup: expected one task named {name!r}")
52def _argv(task: dict[str, object]) -> object:
53 """Return an Ansible command task's argv value."""
54 command = task.get(
"ansible.builtin.command")
55 return command.get(
"argv")
if isinstance(command, dict)
else None
58def capacity_field_manager_errors(source: str) -> list[str]:
59 """Require temporary ARC capacity changes to share Helm's field manager."""
60 executable =
"\n".join(
61 line
for line
in source.splitlines()
if not line.lstrip().startswith(
"#")
63 normalized =
" ".join(executable.replace(
"\\\n",
" ").split())
64 patches = normalized.count(
"kc patch autoscalingrunnerset")
65 managed = normalized.count(
"--field-manager=helm --type=merge")
66 if patches > 0
and managed == patches:
68 return [
"fleet capacity: ARC patch does not share Helm's field manager"]
71def _build_label_errors(named: NamedTasks) -> tuple[list[str], int]:
72 """Require both producer images to carry ownership and kind labels."""
73 errors: list[str] = []
74 positions: list[int] = []
76 (
"Build the devcontainer toolchain image (single source of truth)",
"devcontainer"),
77 (
"Build the runner image (devcontainer + actions-runner)",
"runner"),
79 for name, kind
in specs:
80 match = _one(named, name, errors)
84 positions.append(index)
85 command = task.get(
"ansible.builtin.command")
86 cmd = command.get(
"cmd",
"")
if isinstance(command, dict)
else ""
87 labels = (MANAGED_IMAGE_LABEL, f
"{MANAGED_IMAGE_KIND}={kind}")
89 f
"ci_runner image cleanup: {name!r} lacks label {label!r}"
91 if f
"--label {label}" not in str(cmd)
93 return errors, max(positions, default=-1)
96def _buildah_kind_errors(named: NamedTasks, kind: str, previous: int) -> tuple[list[str], int]:
97 """Require one dangling-only removal pair after the prior managed step."""
98 errors: list[str] = []
99 find_name = f
"Find superseded managed {kind} images"
100 remove_name = f
"Remove superseded managed {kind} images"
101 register = f
"ci_runner_dangling_{kind}_images"
102 find_match = _one(named, find_name, errors)
103 remove_match = _one(named, remove_name, errors)
104 if find_match
is None or remove_match
is None:
105 return errors, previous
106 find_index, find_task = find_match
107 remove_index, remove_task = remove_match
112 f
"label={MANAGED_IMAGE_LABEL}",
114 f
"label={MANAGED_IMAGE_KIND}={kind}",
120 if _argv(find_task) != expected_find:
121 errors.append(f
"ci_runner image cleanup: {find_name!r} selector is not exact")
122 if find_task.get(
"register") != register
or find_task.get(
"changed_when")
is not False:
123 errors.append(f
"ci_runner image cleanup: {find_name!r} receipt is not exact")
124 if _argv(remove_task) != [
"buildah",
"rmi",
"{{ item }}"]:
125 errors.append(f
"ci_runner image cleanup: {remove_name!r} argv is not exact")
127 remove_task.get(
"loop") != f
"{{{{ {register}.stdout_lines }}}}"
128 or remove_task.get(
"when") !=
"item | length > 0"
129 or remove_task.get(
"changed_when")
is not True
131 errors.append(f
"ci_runner image cleanup: {remove_name!r} loop is not exact")
132 if not previous < find_index < remove_index:
133 errors.append(f
"ci_runner image cleanup: {kind} cleanup order is not exact")
134 return errors, remove_index
137def _cri_inventory_errors(inventory: dict[str, object], reset: dict[str, object]) -> list[str]:
138 """Require an immutable CRI inventory and empty selection receipt."""
139 errors: list[str] = []
141 _argv(inventory) != [
"k3s",
"crictl",
"images",
"-o",
"json"]
142 or inventory.get(
"register") !=
"ci_runner_cri_images"
143 or inventory.get(
"changed_when")
is not False
144 or inventory.get(
"when") !=
"not ansible_check_mode"
146 errors.append(
"ci_runner image cleanup: CRI inventory receipt is not exact")
147 facts = reset.get(
"ansible.builtin.set_fact")
148 expected_repository =
"{{ ci_runner_image | regex_replace(':[^/:]+$', '') }}"
149 if not isinstance(facts, dict)
or (
150 facts.get(
"ci_runner_stale_cri_images") != []
151 or facts.get(
"ci_runner_image_repository") != expected_repository
152 or reset.get(
"changed_when")
is not False
153 or reset.get(
"when") !=
"not ansible_check_mode"
155 errors.append(
"ci_runner image cleanup: CRI selection reset is not exact")
159def _normalized_conditions(task: dict[str, object]) -> set[str]:
160 """Return whitespace-stable conditions from one Ansible task."""
161 conditions = task.get(
"when")
162 if not isinstance(conditions, list):
164 return {
" ".join(str(condition).split())
for condition
in conditions}
167def _cri_selection_errors(select: dict[str, object], remove: dict[str, object]) -> list[str]:
168 """Require exact untagged repository selection and bounded CRI removal."""
169 errors: list[str] = []
170 facts = select.get(
"ansible.builtin.set_fact")
171 selection = facts.get(
"ci_runner_stale_cri_images")
if isinstance(facts, dict)
else None
172 expected_conditions = {
173 "not ansible_check_mode",
174 "item.repoTags | default([]) | length == 0",
175 "item.repoDigests | default([]) | select( 'match', '^' ~ "
176 "(ci_runner_image_repository | regex_escape) ~ "
177 "'@sha256:[0-9a-f]{64}$' ) | list | length > 0",
180 selection !=
"{{ ci_runner_stale_cri_images + [item.id] }}"
181 or select.get(
"loop") !=
"{{ (ci_runner_cri_images.stdout | from_json).images }}"
182 or _normalized_conditions(select) != expected_conditions
183 or select.get(
"changed_when")
is not False
185 errors.append(
"ci_runner image cleanup: CRI ownership selection is not exact")
187 _argv(remove) != [
"k3s",
"crictl",
"rmi",
"{{ item }}"]
188 or remove.get(
"loop") !=
"{{ ci_runner_stale_cri_images }}"
189 or remove.get(
"when") !=
"not ansible_check_mode"
190 or remove.get(
"changed_when")
is not True
192 errors.append(
"ci_runner image cleanup: CRI removal loop is not exact")
196def _cri_errors(named: NamedTasks, previous: int) -> list[str]:
197 """Require a repository-scoped CRI sweep after current-image publication."""
198 errors: list[str] = []
200 "Inventory CRI images after publishing the current runner",
201 "Reset the superseded runner image selection",
202 "Select superseded untagged runner images",
203 "Remove superseded untagged runner images",
205 matches = [_one(named, name, errors)
for name
in names]
206 if any(match
is None for match
in matches):
208 inventory, reset, select, remove = matches
209 positions = [inventory[0], reset[0], select[0], remove[0]]
210 if positions != sorted(positions)
or previous >= positions[0]:
211 errors.append(
"ci_runner image cleanup: CRI cleanup order is not exact")
212 errors.extend(_cri_inventory_errors(inventory[1], reset[1]))
213 errors.extend(_cri_selection_errors(select[1], remove[1]))
217def _helm_ownership_errors(named: NamedTasks) -> list[str]:
218 """Require Helm to reclaim declared runner fields from transient managers."""
219 errors: list[str] = []
220 match = _one(named,
"Install the ra8-ci runner scale set", errors)
223 module = match[1].get(
"kubernetes.core.helm")
224 if not isinstance(module, dict)
or module.get(
"force_conflicts")
is not True:
225 errors.append(
"ci_runner image cleanup: Helm does not reclaim declared field ownership")
229def errors(source: str) -> list[str]:
230 """Require the deployed producer to delete only its own stale images."""
231 named, findings = _named_tasks(source)
234 build_findings, previous = _build_label_errors(named)
235 findings.extend(build_findings)
236 for kind
in (
"runner",
"devcontainer"):
237 kind_findings, previous = _buildah_kind_errors(named, kind, previous)
238 findings.extend(kind_findings)
239 findings.extend(_cri_errors(named, previous))
240 findings.extend(_helm_ownership_errors(named))
244def consumer_errors(source: str) -> list[str]:
245 """Require Docker consumers to remove only superseded managed images."""
246 named, errors = _named_tasks(source)
249 before = _one(named,
"Assert every container uses the validated Docker-native image", errors)
250 find = _one(named,
"Find superseded managed Docker runner images", errors)
251 remove = _one(named,
"Remove superseded managed Docker runner images", errors)
252 after = _one(named,
"Read back the caps the kernel is actually enforcing", errors)
253 if any(task
is None for task
in (before, find, remove, after)):
255 if not before[0] < find[0] < remove[0] < after[0]:
256 errors.append(
"ci_runner_docker image cleanup: task order is not exact")
264 f
"label={MANAGED_IMAGE_LABEL}",
266 f
"label={MANAGED_IMAGE_KIND}=runner",
271 _argv(find[1]) != expected_find
272 or find[1].get(
"register") !=
"ci_runner_docker_dangling_images"
273 or find[1].get(
"when") !=
"not ansible_check_mode"
274 or find[1].get(
"changed_when")
is not False
276 errors.append(
"ci_runner_docker image cleanup: owned dangling selector is not exact")
278 _argv(remove[1]) != [
"docker",
"image",
"rm",
"{{ item }}"]
279 or remove[1].get(
"loop") !=
"{{ ci_runner_docker_dangling_images.stdout_lines }}"
280 or remove[1].get(
"when") !=
"not ansible_check_mode"
281 or remove[1].get(
"changed_when")
is not True
283 errors.append(
"ci_runner_docker image cleanup: bounded removal loop is not exact")