ra8-firmware 0.1.0
Bare-metal firmware for the Renesas RA8 family (RA8D2 / RA8P1)
Loading...
Searching...
No Matches
runner_image_cleanup_policy.py
Go to the documentation of this file.
1# SPDX-License-Identifier: MIT
2# Copyright (c) 2026 Brighton Sikarskie
3"""Validate scoped Buildah and CRI cleanup for deployed runner images."""
4
5from __future__ import annotations
6
7import yaml
8
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]]]]
12
13
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]] = []
17 for item in items:
18 if not isinstance(item, dict):
19 continue
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))
25 return flattened
26
27
28def _named_tasks(source: str) -> tuple[NamedTasks, list[str]]:
29 """Parse the deployed producer into uniquely addressable task candidates."""
30 try:
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))
40 return named, []
41
42
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, [])
46 if len(matches) != 1:
47 errors.append(f"ci_runner image cleanup: expected one task named {name!r}")
48 return None
49 return matches[0]
50
51
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
56
57
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("#")
62 )
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:
67 return []
68 return ["fleet capacity: ARC patch does not share Helm's field manager"]
69
70
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] = []
75 specs = (
76 ("Build the devcontainer toolchain image (single source of truth)", "devcontainer"),
77 ("Build the runner image (devcontainer + actions-runner)", "runner"),
78 )
79 for name, kind in specs:
80 match = _one(named, name, errors)
81 if match is None:
82 continue
83 index, task = match
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}")
88 errors.extend(
89 f"ci_runner image cleanup: {name!r} lacks label {label!r}"
90 for label in labels
91 if f"--label {label}" not in str(cmd)
92 )
93 return errors, max(positions, default=-1)
94
95
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
108 expected_find = [
109 "buildah",
110 "images",
111 "--filter",
112 f"label={MANAGED_IMAGE_LABEL}",
113 "--filter",
114 f"label={MANAGED_IMAGE_KIND}={kind}",
115 "--filter",
116 "dangling=true",
117 "--quiet",
118 "--no-trunc",
119 ]
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")
126 if (
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
130 ):
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
135
136
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] = []
140 if (
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"
145 ):
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"
154 ):
155 errors.append("ci_runner image cleanup: CRI selection reset is not exact")
156 return errors
157
158
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):
163 return set()
164 return {" ".join(str(condition).split()) for condition in conditions}
165
166
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",
178 }
179 if (
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
184 ):
185 errors.append("ci_runner image cleanup: CRI ownership selection is not exact")
186 if (
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
191 ):
192 errors.append("ci_runner image cleanup: CRI removal loop is not exact")
193 return errors
194
195
196def _cri_errors(named: NamedTasks, previous: int) -> list[str]:
197 """Require a repository-scoped CRI sweep after current-image publication."""
198 errors: list[str] = []
199 names = (
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",
204 )
205 matches = [_one(named, name, errors) for name in names]
206 if any(match is None for match in matches):
207 return errors
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]))
214 return errors
215
216
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)
221 if match is None:
222 return 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")
226 return errors
227
228
229def errors(source: str) -> list[str]:
230 """Require the deployed producer to delete only its own stale images."""
231 named, findings = _named_tasks(source)
232 if findings:
233 return findings
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))
241 return findings
242
243
244def consumer_errors(source: str) -> list[str]:
245 """Require Docker consumers to remove only superseded managed images."""
246 named, errors = _named_tasks(source)
247 if errors:
248 return errors
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)):
254 return errors
255 if not before[0] < find[0] < remove[0] < after[0]:
256 errors.append("ci_runner_docker image cleanup: task order is not exact")
257 expected_find = [
258 "docker",
259 "image",
260 "ls",
261 "--filter",
262 "dangling=true",
263 "--filter",
264 f"label={MANAGED_IMAGE_LABEL}",
265 "--filter",
266 f"label={MANAGED_IMAGE_KIND}=runner",
267 "--quiet",
268 "--no-trunc",
269 ]
270 if (
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
275 ):
276 errors.append("ci_runner_docker image cleanup: owned dangling selector is not exact")
277 if (
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
282 ):
283 errors.append("ci_runner_docker image cleanup: bounded removal loop is not exact")
284 return errors