Coverage for .github / scripts / validate_helm_charts.py: 100.00%

608 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-10-28 16:19 +0000

1"""Validate every Helm chart (name, version) pinned in charts.yaml. 

2 

3Confirms that each chart entry in ``lambda/helm-installer/charts.yaml`` is a 

4real, installable Helm chart at the pinned version — i.e. the 

5``(chart, version)`` combination is something Helm can actually pull and 

6render for a cluster, not a typo or a tag that never shipped. The 

7helm-installer Lambda trusts these pins blindly at deploy time, so a bad pin 

8today only surfaces as a failed ``helm upgrade --install`` mid-deploy; this 

9check moves that failure left into CI. 

10 

11Two layers, matching what each CI stage can afford: 

12 

13 Structural (offline, always): pure-Python checks on the parsed YAML — no 

14 network, no ``helm`` binary. Every entry needs a ``chart``, a SemVer-ish 

15 ``version`` and a ``repo_url``; an ``oci://`` ``repo_url`` must set 

16 ``use_oci: true`` (and vice-versa); a classic HTTP(S) repo needs a 

17 ``repo_name`` for ``helm repo add``. Catches the obvious mistakes before 

18 spending a network round-trip. 

19 

20 Online (needs ``helm`` + network): for every chart, build the *same* 

21 reference the installer Lambda builds (see ``handler.install_chart``), 

22 then 

23 

24 * ``helm show chart <ref> --version <ver>`` — proves Helm can pull the 

25 chart metadata at exactly the pinned version; and 

26 * ``helm template <ref> --version <ver> --values <configured>`` — 

27 proves the chart renders to Kubernetes manifests (installable) with 

28 the values ``charts.yaml`` ships. 

29 

30 Two retry layers ride out intermittent registry failures instead of 

31 forcing a manual job rerun. Inner: every network-touching helm call 

32 (repo add/update, show chart, template) goes through ``_run_with_retry`` 

33 — a fixed number of attempts with exponential backoff, first success 

34 wins. Outer: charts still failing after the first sweep get a bounded 

35 number of re-passes (default one more, ~30s later) behind a fresh 

36 ``helm repo add`` + index refresh, covering outages that outlast a 

37 single command's retry window. A genuinely bad pin fails every attempt 

38 of every pass and still surfaces. 

39 

40By default *every* chart in the file is validated, including entries with 

41``enabled: false``: those are toggled on via ``cdk.json``, so their pinned 

42``(name, version)`` must be valid too. 

43 

44Three lockstep contracts ride along (each with an offline half that always 

45runs and an online half): the AWS Load Balancer Controller chart against the 

46Gateway API CRD bundles the helm-installer pins, the Kubeflow Trainer chart 

47against the shipped ``torch-distributed`` runtime, and the ``slinky-slurm`` 

48slurmrestd image against ``SLURM_API_VERSION`` in ``gco/slurm_config.py`` 

49(the online half runs ``slurmrestd -d list`` inside the pinned image, so it 

50needs a container runtime: ``docker`` by default, ``--container-runtime`` 

51for podman/finch). 

52 

53Usage:: 

54 

55 # Structural only (no helm needed): 

56 python3 .github/scripts/validate_helm_charts.py --mode offline 

57 

58 # Full check (requires helm + docker on PATH, and network) — the CI gate: 

59 python3 .github/scripts/validate_helm_charts.py --mode online 

60 

61 # Same, with another container runtime for the slurmrestd image (the 

62 # repo-wide GCO_CONTAINER_RUNTIME variable sets the default): 

63 python3 .github/scripts/validate_helm_charts.py --mode online --container-runtime finch 

64 

65 # Auto: structural always, online when helm happens to be installed: 

66 python3 .github/scripts/validate_helm_charts.py 

67 

68 # Point at a non-default charts.yaml (used by the test suite): 

69 python3 .github/scripts/validate_helm_charts.py --charts /path/to/charts.yaml 

70 

71 # Emit one chart's pinned reference / shipped values (consumed by the 

72 # integration:kind:examples-smoke job so its `helm install` uses the 

73 # exact pins and values the installer Lambda would — no copies in CI): 

74 python3 .github/scripts/validate_helm_charts.py --emit-ref mlflow 

75 python3 .github/scripts/validate_helm_charts.py --emit-values mlflow 

76 

77Exit codes:: 

78 

79 0 all validated charts are well-formed (and, when online, resolvable 

80 + renderable at their pinned versions) 

81 1 one or more charts failed validation 

82 2 unexpected I/O / argument error (charts.yaml missing or unparseable, 

83 or --mode online requested without a helm binary or container runtime) 

84 

85The module is importable from the test suite — call ``validate_structure()``, 

86``build_refs()`` or ``validate_online()`` directly to exercise the logic 

87against fixtures. 

88""" 

89 

90from __future__ import annotations 

91 

92import argparse 

93import contextlib 

94import copy 

95import os 

96import re 

97import shutil 

98import subprocess # used only to invoke the pinned `helm` binary with fixed argv 

99import sys 

100import tempfile 

101import time 

102from dataclasses import dataclass 

103from pathlib import Path 

104from typing import Any 

105 

106import yaml 

107 

108# charts.yaml lives next to the helm-installer Lambda handler. This script is 

109# .github/scripts/validate_helm_charts.py, so the repo root is two parents up. 

110_REPO_ROOT = Path(__file__).resolve().parents[2] 

111_DEFAULT_CHARTS = _REPO_ROOT / "lambda" / "helm-installer" / "charts.yaml" 

112 

113# Lenient SemVer-ish matcher for the pinned chart ``version``. Helm requires 

114# SemVer2 chart versions, so anything failing this is guaranteed to fail a real 

115# ``helm ... --version`` resolve too — we just catch it offline first. Accepts 

116# an optional leading "v" (cert-manager / aws-efa tag their charts that way) 

117# and an optional pre-release / build-metadata suffix. 

118_VERSION_RE = re.compile(r"^v?\d+(?:\.\d+){1,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$") 

119 

120 

121@dataclass(frozen=True) 

122class ChartRef: 

123 """Everything needed to resolve one ``charts.yaml`` entry with Helm.""" 

124 

125 name: str 

126 chart: str 

127 version: str 

128 repo_name: str 

129 repo_url: str 

130 use_oci: bool 

131 namespace: str 

132 enabled: bool 

133 values: dict[str, Any] 

134 

135 def reference(self) -> str: 

136 """Build the Helm chart reference exactly like ``handler.install_chart``. 

137 

138 OCI charts are addressed by their full ``oci://.../<chart>`` URL; 

139 classic HTTP(S) repos are addressed as ``<repo_name>/<chart>`` after 

140 the repo has been added with ``helm repo add``. 

141 """ 

142 if self.use_oci: 

143 return f"{self.repo_url}/{self.chart}" 

144 return f"{self.repo_name}/{self.chart}" 

145 

146 

147def load_charts(path: Path) -> dict[str, Any]: 

148 """Parse ``charts.yaml`` and return the ``charts:`` mapping. 

149 

150 Raises ``FileNotFoundError`` when the file is absent and ``ValueError`` 

151 when the top-level ``charts:`` mapping is missing or malformed; ``main()`` 

152 turns both into exit code 2. 

153 """ 

154 with open(path, encoding="utf-8") as fh: 

155 data = yaml.safe_load(fh) or {} 

156 charts = data.get("charts") 

157 if not isinstance(charts, dict): 

158 raise ValueError("missing or malformed top-level 'charts:' mapping") 

159 return charts 

160 

161 

162def validate_structure(charts: dict[str, Any], *, enabled_only: bool = False) -> list[str]: 

163 """Return a list of structural problems (empty == all well-formed). 

164 

165 These are the offline, pure-Python checks: enough to guarantee we can even 

166 build a Helm reference for each entry and that OCI/classic metadata is 

167 self-consistent. Every returned string names the offending chart so the 

168 failure is actionable from the CI log alone. 

169 """ 

170 errors: list[str] = [] 

171 if not charts: 

172 return ["charts.yaml contains no chart entries under 'charts:'"] 

173 

174 for name, cfg in charts.items(): 

175 if not isinstance(cfg, dict): 

176 errors.append(f"{name}: entry is not a mapping") 

177 continue 

178 if enabled_only and not cfg.get("enabled", False): 

179 continue 

180 

181 chart = cfg.get("chart") 

182 version = cfg.get("version") 

183 repo_url = cfg.get("repo_url") 

184 repo_name = cfg.get("repo_name") 

185 use_oci = bool(cfg.get("use_oci", False)) 

186 

187 if not (isinstance(chart, str) and chart.strip()): 

188 errors.append(f"{name}: missing or empty 'chart'") 

189 

190 if not (isinstance(version, str) and version.strip()): 

191 errors.append(f"{name}: missing or empty 'version'") 

192 elif not _VERSION_RE.match(version.strip()): 

193 errors.append(f"{name}: version {version!r} is not a valid SemVer chart version") 

194 

195 if not (isinstance(repo_url, str) and repo_url.strip()): 

196 errors.append(f"{name}: missing or empty 'repo_url'") 

197 else: 

198 is_oci_url = repo_url.startswith("oci://") 

199 if is_oci_url and not use_oci: 

200 errors.append(f"{name}: repo_url is oci:// but use_oci is not set to true") 

201 if use_oci and not is_oci_url: 

202 errors.append( 

203 f"{name}: use_oci is true but repo_url {repo_url!r} is not an oci:// URL" 

204 ) 

205 if not use_oci and not is_oci_url: 

206 if not repo_url.startswith(("http://", "https://")): 

207 errors.append( 

208 f"{name}: classic repo_url {repo_url!r} must be http(s):// or oci://" 

209 ) 

210 if not (isinstance(repo_name, str) and repo_name.strip()): 

211 errors.append(f"{name}: non-OCI chart needs a 'repo_name' for 'helm repo add'") 

212 

213 return errors 

214 

215 

216def build_refs(charts: dict[str, Any], *, enabled_only: bool = False) -> list[ChartRef]: 

217 """Build a ``ChartRef`` for every entry well-formed enough to resolve. 

218 

219 Entries too malformed to build a reference (no chart / version / repo_url, 

220 or a classic repo with no repo_name) are skipped here — ``validate_structure`` 

221 already reports them, so there is no point trying to hit Helm for them. 

222 """ 

223 refs: list[ChartRef] = [] 

224 for name, cfg in charts.items(): 

225 if not isinstance(cfg, dict): 

226 continue 

227 if enabled_only and not cfg.get("enabled", False): 

228 continue 

229 

230 chart = cfg.get("chart") 

231 version = cfg.get("version") 

232 repo_url = cfg.get("repo_url") 

233 use_oci = bool(cfg.get("use_oci", False)) 

234 repo_name = cfg.get("repo_name") or "" 

235 

236 if not (isinstance(chart, str) and chart.strip()): 

237 continue 

238 if not (isinstance(version, str) and version.strip()): 

239 continue 

240 if not (isinstance(repo_url, str) and repo_url.strip()): 

241 continue 

242 if not use_oci and not str(repo_name).strip(): 

243 continue 

244 

245 values = cfg.get("values") 

246 refs.append( 

247 ChartRef( 

248 name=str(name), 

249 chart=chart.strip(), 

250 version=version.strip(), 

251 repo_name=str(repo_name).strip(), 

252 repo_url=repo_url.strip(), 

253 use_oci=use_oci, 

254 namespace=str(cfg.get("namespace", "default")), 

255 enabled=bool(cfg.get("enabled", False)), 

256 values=values if isinstance(values, dict) else {}, 

257 ) 

258 ) 

259 return refs 

260 

261 

262def _run(cmd: list[str], env: dict[str, str], *, timeout: int = 120) -> tuple[int, str, str]: 

263 """Run a command with a fixed argv (never a shell), returning (rc, out, err). 

264 

265 A subprocess timeout maps to ``(-1, "", "timeout: ...")`` so callers get a 

266 uniform failure contract instead of an exception — mirrors the same pattern 

267 in the helm-installer Lambda's ``run_helm``. 

268 """ 

269 try: 

270 proc = ( 

271 subprocess.run( # nosemgrep: dangerous-subprocess-use-audit - fixed argv, no shell=True 

272 cmd, 

273 capture_output=True, 

274 text=True, 

275 env=env, 

276 timeout=timeout, 

277 check=False, 

278 ) 

279 ) 

280 except subprocess.TimeoutExpired as exc: 

281 return -1, "", f"timeout: command exceeded {exc.timeout}s" 

282 return proc.returncode, proc.stdout, proc.stderr 

283 

284 

285def _run_with_retry( 

286 cmd: list[str], 

287 env: dict[str, str], 

288 *, 

289 attempts: int = 4, 

290 base_delay: float = 2.0, 

291 max_delay: float = 20.0, 

292 timeout: int = 120, 

293 verbose: bool = False, 

294 description: str = "", 

295) -> tuple[int, str, str]: 

296 """Run a network-touching helm command up to ``attempts`` times; first success wins. 

297 

298 The command is retried on any non-zero exit, regardless of why it failed: 

299 the first attempt that succeeds (rc 0) is returned immediately, and if none 

300 do, the last failure is returned. Between attempts we sleep an exponentially 

301 growing delay (``base_delay`` doubling each round, capped at ``max_delay``) 

302 to give a blipping registry a moment to recover. 

303 

304 This is the guard against intermittent registry failures (timeouts, resets, 

305 5xx) that otherwise force a manual rerun of the ``integration:helm:charts-valid`` 

306 job. A genuinely bad pin fails on every attempt and still surfaces, just a 

307 few seconds later. 

308 """ 

309 result: tuple[int, str, str] = (1, "", "") 

310 for attempt in range(1, attempts + 1): 

311 result = _run(cmd, env, timeout=timeout) 

312 if result[0] == 0: 

313 return result 

314 if attempt < attempts: 

315 delay = min(base_delay * (2 ** (attempt - 1)), max_delay) 

316 if verbose: 

317 label = description or " ".join(cmd) 

318 print( 

319 f" attempt {attempt}/{attempts} failed, retrying in " 

320 f"{delay:.1f}s — {label}: {_tail(result[2], 200)}" 

321 ) 

322 time.sleep(delay) 

323 return result 

324 

325 

326def _helm_env(helm_home: Path) -> dict[str, str]: 

327 """Return an environment that isolates Helm's cache/config/data in a temp dir. 

328 

329 Keeps the check hermetic: it never reads or clobbers a developer's real 

330 ``helm repo`` list, and CI starts from a clean slate every run. 

331 """ 

332 env = os.environ.copy() 

333 env["HELM_CACHE_HOME"] = str(helm_home / "cache") 

334 env["HELM_CONFIG_HOME"] = str(helm_home / "config") 

335 env["HELM_DATA_HOME"] = str(helm_home / "data") 

336 return env 

337 

338 

339def _tail(text: str, limit: int = 400) -> str: 

340 """Trim helm stderr to the last ``limit`` chars so reports stay readable.""" 

341 text = (text or "").strip() 

342 if len(text) <= limit: 

343 return text 

344 return "..." + text[-limit:] 

345 

346 

347def _chart_version_from_show(show_output: str) -> str | None: 

348 """Extract the ``version`` field from ``helm show chart`` YAML output.""" 

349 try: 

350 meta = yaml.safe_load(show_output) 

351 except yaml.YAMLError: 

352 return None 

353 if isinstance(meta, dict): 

354 version = meta.get("version") 

355 if isinstance(version, str): 

356 return version 

357 return None 

358 

359 

360def _versions_match(resolved: str, requested: str) -> bool: 

361 """Compare versions ignoring a leading ``v`` on either side.""" 

362 return resolved.lstrip("v") == requested.lstrip("v") 

363 

364 

365def _render_chart( 

366 ref: ChartRef, 

367 ref_str: str, 

368 helm_binary: str, 

369 env: dict[str, str], 

370 *, 

371 verbose: bool = False, 

372) -> str | None: 

373 """``helm template`` the chart with its shipped values; return an error or None. 

374 

375 Rendering with the exact ``values`` block from ``charts.yaml`` proves the 

376 chart is installable *as GCO configures it*, not just with upstream 

377 defaults. No cluster is contacted, but templating a remote ``--version`` 

378 ref still pulls the chart from its registry, so it goes through 

379 ``_run_with_retry`` to ride out the same transient blips as the resolve 

380 step. 

381 """ 

382 args = [ 

383 helm_binary, 

384 "template", 

385 ref.name, 

386 ref_str, 

387 "--version", 

388 ref.version, 

389 "--namespace", 

390 ref.namespace, 

391 ] 

392 

393 values_path: str | None = None 

394 if ref.values: 

395 fd, values_path = tempfile.mkstemp(suffix=".yaml", prefix=f"{ref.name}-values-") 

396 try: 

397 # fdopen takes ownership of fd; the with-block closes it on any exit, 

398 # so a failing dump must NOT close it again (that raised EBADF and 

399 # masked the real error). Remove the half-written file instead. 

400 with os.fdopen(fd, "w", encoding="utf-8") as fh: 

401 yaml.safe_dump(ref.values, fh) 

402 except Exception: 

403 with contextlib.suppress(OSError): 

404 os.remove(values_path) 

405 raise 

406 args.extend(["--values", values_path]) 

407 

408 try: 

409 rc, _out, err = _run_with_retry( 

410 args, 

411 env, 

412 timeout=180, 

413 verbose=verbose, 

414 description=f"helm template {ref.name}", 

415 ) 

416 finally: 

417 if values_path: 

418 with contextlib.suppress(OSError): 

419 os.remove(values_path) 

420 

421 if rc != 0: 

422 return f"chart failed to render (helm template): {_tail(err)}" 

423 return None 

424 

425 

426def _sync_classic_repos( 

427 classic: dict[str, str], 

428 helm_binary: str, 

429 env: dict[str, str], 

430 *, 

431 verbose: bool = False, 

432) -> list[str]: 

433 """``helm repo add`` every classic repo and refresh the index once. 

434 

435 Returns error strings for repos that could not be added. The index 

436 refresh is network-bound too — it goes through the same retry so a blip 

437 there doesn't cascade into spurious "cannot resolve" errors downstream. 

438 """ 

439 errors: list[str] = [] 

440 for repo_name, repo_url in classic.items(): 

441 rc, _out, err = _run_with_retry( 

442 [helm_binary, "repo", "add", repo_name, repo_url, "--force-update"], 

443 env, 

444 verbose=verbose, 

445 description=f"helm repo add {repo_name}", 

446 ) 

447 if rc != 0: 

448 errors.append(f"helm repo add {repo_name} ({repo_url}) failed: {_tail(err)}") 

449 if classic: 

450 _run_with_retry( 

451 [helm_binary, "repo", "update"], 

452 env, 

453 timeout=180, 

454 verbose=verbose, 

455 description="helm repo update", 

456 ) 

457 return errors 

458 

459 

460def _validate_refs( 

461 refs: list[ChartRef], 

462 helm_binary: str, 

463 env: dict[str, str], 

464 *, 

465 skip_template: bool = False, 

466 verbose: bool = False, 

467) -> dict[str, list[str]]: 

468 """Resolve + render each chart; return ``{chart name: [errors]}`` for failures.""" 

469 failures: dict[str, list[str]] = {} 

470 for ref in refs: 

471 ref_str = ref.reference() 

472 label = f"{ref.name} {ref.version} ({ref_str})" 

473 chart_errors: list[str] = [] 

474 

475 rc, out, err = _run_with_retry( 

476 [helm_binary, "show", "chart", ref_str, "--version", ref.version], 

477 env, 

478 verbose=verbose, 

479 description=f"helm show chart {ref.name}", 

480 ) 

481 if rc != 0: 

482 chart_errors.append( 

483 f"{ref.name}: helm cannot resolve {ref_str} at version " 

484 f"{ref.version!r}: {_tail(err)}" 

485 ) 

486 if verbose: 

487 print(f"FAIL resolve {label}") 

488 failures[ref.name] = chart_errors 

489 continue 

490 

491 resolved = _chart_version_from_show(out) 

492 if resolved and not _versions_match(resolved, ref.version): 

493 chart_errors.append( 

494 f"{ref.name}: requested version {ref.version!r} but helm resolved {resolved!r}" 

495 ) 

496 if verbose: 

497 print(f"ok resolve {label}") 

498 

499 if not skip_template: 

500 render_error = _render_chart(ref, ref_str, helm_binary, env, verbose=verbose) 

501 if render_error: 

502 chart_errors.append(f"{ref.name}: {render_error}") 

503 if verbose: 

504 print(f"FAIL render {label}") 

505 elif verbose: 

506 print(f"ok render {label}") 

507 

508 if chart_errors: 

509 failures[ref.name] = chart_errors 

510 return failures 

511 

512 

513def validate_online( 

514 refs: list[ChartRef], 

515 *, 

516 helm_binary: str = "helm", 

517 skip_template: bool = False, 

518 verbose: bool = False, 

519 passes: int = 2, 

520 repass_delay: float = 30.0, 

521) -> list[str]: 

522 """Resolve (and, unless skipped, render) each chart at its pinned version. 

523 

524 Returns a list of human-readable error strings (empty == every chart is 

525 resolvable and renderable). Runs against an isolated Helm home so it is 

526 safe to invoke on a developer machine. 

527 

528 Retry model, outer layer: the per-command retry in ``_run_with_retry`` 

529 rides out blips that clear within one command's ~40-second attempt 

530 window, but a registry outage lasting a few minutes fails several charts 

531 on every inner attempt and used to fail the job (observed live: a rerun 

532 of the unchanged job passed). So after the first sweep, the charts that 

533 failed get up to ``passes - 1`` additional sweeps, each preceded by a 

534 ``repass_delay`` pause and a fresh repo add + index refresh. Only 

535 failures that survive every pass are reported; a genuinely bad pin fails 

536 every pass and still surfaces. 

537 """ 

538 errors: list[str] = [] 

539 if not refs: 

540 return errors 

541 

542 with tempfile.TemporaryDirectory(prefix="gco-helm-validate-") as tmp: 

543 env = _helm_env(Path(tmp)) 

544 classic = {ref.repo_name: ref.repo_url for ref in refs if not ref.use_oci} 

545 

546 repo_errors = _sync_classic_repos(classic, helm_binary, env, verbose=verbose) 

547 failures = _validate_refs( 

548 refs, helm_binary, env, skip_template=skip_template, verbose=verbose 

549 ) 

550 

551 for extra_pass in range(2, max(passes, 1) + 1): 

552 if not failures and not repo_errors: 

553 break 

554 if verbose: 

555 print( 

556 f"re-pass {extra_pass}/{passes}: retrying " 

557 f"{len(failures)} failed chart(s) in {repass_delay:.0f}s " 

558 "(fresh repo index)" 

559 ) 

560 time.sleep(repass_delay) 

561 repo_errors = _sync_classic_repos(classic, helm_binary, env, verbose=verbose) 

562 retry_refs = [ref for ref in refs if ref.name in failures] 

563 failures = _validate_refs( 

564 retry_refs, helm_binary, env, skip_template=skip_template, verbose=verbose 

565 ) 

566 

567 errors.extend(repo_errors) 

568 for ref in refs: 

569 errors.extend(failures.get(ref.name, [])) 

570 

571 return errors 

572 

573 

574# --------------------------------------------------------------------------- 

575# Gateway API / aws-load-balancer-controller lockstep 

576# 

577# The controller is built against an exact ``sigs.k8s.io/gateway-api`` release 

578# (declared in its go.mod) and its own gateway CRDs ship per controller tag. 

579# GCO pins three coupled artifacts in two different files: 

580# 

581# * the aws-load-balancer-controller chart version (charts.yaml, this file's 

582# usual input), 

583# * the ``gateway-api-standard-vX.Y.Z`` CRD bundle, and 

584# * the ``aws-lbc-gateway-vX.Y.Z`` CRD bundle 

585# (both in lambda/helm-installer/handler.py PINNED_GATEWAY_CRD_BUNDLES). 

586# 

587# When the chart moved to 3.5.0 while the standard bundle stayed at v1.5.0, 

588# the controller silently stopped reconciling gateways — nothing failed until 

589# the live release validation deployed the pair. These checks encode the 

590# contract so the drift fails CI instead: 

591# 

592# offline: the aws-lbc-gateway bundle version must equal the pinned chart 

593# version (they ship from the same controller tag). 

594# online: the controller tag's go.mod names its required gateway-api 

595# release; the pinned standard bundle must be at least that (major.minor). 

596# --------------------------------------------------------------------------- 

597 

598_HANDLER_PATH = _REPO_ROOT / "lambda" / "helm-installer" / "handler.py" 

599_LBC_CHART_KEY = "aws-load-balancer-controller" 

600_GATEWAY_API_BUNDLE_RE = re.compile(r'name="gateway-api-standard-v(\d+\.\d+\.\d+)"') 

601_LBC_BUNDLE_RE = re.compile(r'name="aws-lbc-gateway-v(\d+\.\d+\.\d+)"') 

602_GO_MOD_GATEWAY_API_RE = re.compile(r"^\s*sigs\.k8s\.io/gateway-api\s+v(\d+\.\d+\.\d+)", re.M) 

603_LBC_GO_MOD_URL_TEMPLATE = ( 

604 "https://raw.githubusercontent.com/kubernetes-sigs/" 

605 "aws-load-balancer-controller/v{version}/go.mod" 

606) 

607_GO_MOD_FETCH_ATTEMPTS = 3 

608_GO_MOD_FETCH_TIMEOUT_SECONDS = 15 

609 

610 

611def parse_pinned_gateway_bundles(handler_source: str) -> tuple[str | None, str | None]: 

612 """Return (gateway-api standard bundle version, aws-lbc bundle version).""" 

613 gateway_api = _GATEWAY_API_BUNDLE_RE.search(handler_source) 

614 lbc = _LBC_BUNDLE_RE.search(handler_source) 

615 return ( 

616 gateway_api.group(1) if gateway_api else None, 

617 lbc.group(1) if lbc else None, 

618 ) 

619 

620 

621def gateway_api_requirement_from_go_mod(go_mod_text: str) -> str | None: 

622 """Return the gateway-api release the controller's go.mod declares.""" 

623 match = _GO_MOD_GATEWAY_API_RE.search(go_mod_text) 

624 return match.group(1) if match else None 

625 

626 

627def _version_tuple(version: str) -> tuple[int, ...]: 

628 return tuple(int(part) for part in version.split(".")) 

629 

630 

631def fetch_lbc_go_mod(controller_version: str) -> str: 

632 """Fetch the controller tag's go.mod, retrying transient HTTP failures.""" 

633 import urllib.error 

634 import urllib.request 

635 

636 if not re.fullmatch(r"\d+\.\d+\.\d+", controller_version): 

637 raise RuntimeError( 

638 f"refusing go.mod fetch for non-semver controller version {controller_version!r}" 

639 ) 

640 url = _LBC_GO_MOD_URL_TEMPLATE.format(version=controller_version) 

641 last_error: Exception | None = None 

642 for attempt in range(1, _GO_MOD_FETCH_ATTEMPTS + 1): 

643 try: 

644 with urllib.request.urlopen( # nosec B310 # nosemgrep: dynamic-urllib-use-detected - fixed https://raw.githubusercontent.com template; the only variable is a strictly semver-validated version segment, so no scheme or host injection is possible 

645 url, timeout=_GO_MOD_FETCH_TIMEOUT_SECONDS 

646 ) as response: 

647 return str(response.read().decode("utf-8")) 

648 except (urllib.error.URLError, TimeoutError, OSError) as exc: 

649 last_error = exc 

650 if attempt < _GO_MOD_FETCH_ATTEMPTS: 

651 time.sleep(2**attempt) 

652 raise RuntimeError(f"could not fetch {url}: {last_error}") 

653 

654 

655def validate_gateway_lockstep( 

656 charts: dict[str, Any], 

657 *, 

658 handler_source: str | None = None, 

659 go_mod_fetcher: Any = None, 

660 online: bool = False, 

661 require_entry: bool = True, 

662) -> list[str]: 

663 """Check the chart / CRD-bundle lockstep contract; return error strings. 

664 

665 Offline: the ``aws-lbc-gateway`` bundle version must equal the pinned 

666 chart version. Online (adds one HTTPS fetch): the pinned standard 

667 gateway-api bundle must satisfy the requirement in the controller tag's 

668 go.mod. With ``require_entry`` (the default, used for the repository's 

669 real charts.yaml) a missing chart entry or renamed handler constants are 

670 reported rather than skipped — this guard exists precisely for refactors 

671 that move them. ``main()`` disables ``require_entry`` for explicitly 

672 supplied ``--charts`` fixture files that legitimately omit the entry. 

673 """ 

674 errors: list[str] = [] 

675 entry = charts.get(_LBC_CHART_KEY) 

676 if not isinstance(entry, dict) or not entry.get("version"): 

677 if require_entry: 

678 return [f"gateway lockstep: no {_LBC_CHART_KEY!r} entry with a version in charts.yaml"] 

679 return [] 

680 chart_version = str(entry["version"]).lstrip("v") 

681 

682 if handler_source is None: 

683 try: 

684 handler_source = _HANDLER_PATH.read_text(encoding="utf-8") 

685 except OSError as exc: 

686 return [f"gateway lockstep: cannot read {_HANDLER_PATH}: {exc}"] 

687 

688 gateway_api_pin, lbc_pin = parse_pinned_gateway_bundles(handler_source) 

689 if gateway_api_pin is None or lbc_pin is None: 

690 return [ 

691 "gateway lockstep: PINNED_GATEWAY_CRD_BUNDLES in " 

692 "lambda/helm-installer/handler.py no longer names a " 

693 "'gateway-api-standard-vX.Y.Z' and an 'aws-lbc-gateway-vX.Y.Z' " 

694 "bundle; update this check alongside any rename" 

695 ] 

696 

697 if lbc_pin != chart_version: 

698 errors.append( 

699 f"gateway lockstep: aws-lbc-gateway CRD bundle v{lbc_pin} does not match " 

700 f"the pinned {_LBC_CHART_KEY} chart {chart_version} — the controller's " 

701 "gateway CRDs ship per controller tag; bump both together " 

702 "(charts.yaml + PINNED_GATEWAY_CRD_BUNDLES in lambda/helm-installer/handler.py)" 

703 ) 

704 

705 if not online: 

706 return errors 

707 

708 fetcher = go_mod_fetcher or fetch_lbc_go_mod 

709 try: 

710 go_mod_text = fetcher(chart_version) 

711 except RuntimeError as exc: 

712 errors.append(f"gateway lockstep: {exc}") 

713 return errors 

714 

715 required = gateway_api_requirement_from_go_mod(go_mod_text) 

716 if required is None: 

717 errors.append( 

718 f"gateway lockstep: go.mod for {_LBC_CHART_KEY} v{chart_version} does not " 

719 "declare sigs.k8s.io/gateway-api — upstream layout changed; update this check" 

720 ) 

721 return errors 

722 

723 if _version_tuple(gateway_api_pin)[:2] < _version_tuple(required)[:2]: 

724 errors.append( 

725 f"gateway lockstep: {_LBC_CHART_KEY} {chart_version} is built against " 

726 f"gateway-api v{required} (its go.mod), but the pinned standard CRD bundle " 

727 f"is v{gateway_api_pin}. Upgrading the controller without its Gateway API " 

728 "CRDs silently stops gateway reconciliation (caught live, 2026-08). Bump " 

729 "'gateway-api-standard' in lambda/helm-installer/handler.py to at least " 

730 f"v{required} in the same change" 

731 ) 

732 return errors 

733 

734 

735# --------------------------------------------------------------------------- 

736# Kubeflow Trainer runtime / example / docs lockstep 

737# 

738# The kubeflow-trainer chart delivers its built-in ClusterTrainingRuntime 

739# blueprints through a post-install hook Job (an unpinned run-time network 

740# fetch), so GCO disables that hook and ships the ``torch-distributed`` 

741# runtime itself — extracted verbatim from the pinned chart into 

742# ``post-helm-kubeflow-trainer-runtimes.yaml`` with two documented 

743# deviations (``automountServiceAccountToken: false`` and NoNewPrivs on the 

744# ``node`` container). Three more surfaces 

745# repeat the runtime's pinned trainer image so users see exactly what runs: 

746# 

747# * the shipped runtime manifest (the source of truth in-repo), 

748# * ``examples/kubeflow-trainjob.yaml`` ``spec.trainer.image`` (listed 

749# explicitly so the image-trust gate validates it), and 

750# * the ``docs/DISTRIBUTED_TRAINING.md`` TrainJob snippet. 

751# 

752# A chart version bump that skips re-extraction would silently run a stale 

753# runtime (or torchrun wiring) against a newer controller. These checks 

754# encode the contract so the drift fails CI instead: 

755# 

756# offline: the shipped runtime manifest, the example and every 

757# ``pytorch/pytorch`` image mentioned in the distributed-training doc 

758# agree on one image. 

759# online: rendering the *pinned* chart with its runtime delivery enabled 

760# must reproduce the shipped runtime — image called out explicitly, 

761# full spec compared after applying the documented deviations, and the 

762# upstream ``trainer.kubeflow.org/*`` labels preserved (the 

763# ``webhook-validation: disabled`` label is what keeps webhook warm-up 

764# from flaking the apply). 

765# --------------------------------------------------------------------------- 

766 

767_TRAINER_CHART_KEY = "kubeflow-trainer" 

768_TRAINER_RUNTIME_NAME = "torch-distributed" 

769_TRAINER_RUNTIME_MANIFEST = ( 

770 _REPO_ROOT 

771 / "lambda" 

772 / "kubectl-applier-simple" 

773 / "manifests" 

774 / "post-helm-kubeflow-trainer-runtimes.yaml" 

775) 

776_TRAINJOB_EXAMPLE = _REPO_ROOT / "examples" / "kubeflow-trainjob.yaml" 

777_DISTRIBUTED_TRAINING_DOC = _REPO_ROOT / "docs" / "DISTRIBUTED_TRAINING.md" 

778# Only pytorch/pytorch mentions are lockstep-bound: the doc may legitimately 

779# show other registries' images, but a pytorch/pytorch tag that differs from 

780# the shipped runtime is exactly the stale-doc drift this check exists for. 

781_DOC_PYTORCH_IMAGE_RE = re.compile(r"image:\s*(pytorch/pytorch:\S+)") 

782# The runtime labels GCO must carry verbatim: framework selection and the 

783# upstream pre-validation marker that exempts the built-in runtime from 

784# webhook admission (losing it reintroduces webhook warm-up flakes). 

785_TRAINER_RUNTIME_LOCKSTEP_LABELS = ( 

786 "trainer.kubeflow.org/framework", 

787 "trainer.kubeflow.org/webhook-validation", 

788) 

789 

790 

791def parse_shipped_torch_runtime(manifest_text: str) -> dict[str, Any] | None: 

792 """Return the one ``torch-distributed`` runtime in the shipped manifest. 

793 

794 ``None`` when the manifest does not contain exactly one 

795 ``ClusterTrainingRuntime`` named ``torch-distributed`` — the caller turns 

796 that into an "update this check" error rather than guessing. 

797 """ 

798 try: 

799 docs = [doc for doc in yaml.safe_load_all(manifest_text) if isinstance(doc, dict)] 

800 except yaml.YAMLError: 

801 return None 

802 runtimes = [ 

803 doc 

804 for doc in docs 

805 if doc.get("kind") == "ClusterTrainingRuntime" 

806 and (doc.get("metadata") or {}).get("name") == _TRAINER_RUNTIME_NAME 

807 ] 

808 return runtimes[0] if len(runtimes) == 1 else None 

809 

810 

811def upstream_torch_runtime_from_render(render_text: str) -> dict[str, Any] | None: 

812 """Extract the ``torch-distributed`` runtime from a rendered chart. 

813 

814 The chart ships its runtimes as multi-doc YAML inside the 

815 ``*runtimes-installer`` ConfigMap's ``runtimes.yaml`` key (the payload its 

816 hook Job would kubectl-apply); this digs the runtime out of that payload. 

817 """ 

818 try: 

819 docs = [doc for doc in yaml.safe_load_all(render_text) if isinstance(doc, dict)] 

820 except yaml.YAMLError: 

821 return None 

822 for doc in docs: 

823 if doc.get("kind") != "ConfigMap": 

824 continue 

825 name = str((doc.get("metadata") or {}).get("name") or "") 

826 if not name.endswith("runtimes-installer"): 

827 continue 

828 payload = (doc.get("data") or {}).get("runtimes.yaml") 

829 if not isinstance(payload, str): 

830 continue 

831 try: 

832 runtimes = [item for item in yaml.safe_load_all(payload) if isinstance(item, dict)] 

833 except yaml.YAMLError: 

834 return None 

835 for runtime in runtimes: 

836 if ( 

837 runtime.get("kind") == "ClusterTrainingRuntime" 

838 and (runtime.get("metadata") or {}).get("name") == _TRAINER_RUNTIME_NAME 

839 ): 

840 return runtime 

841 return None 

842 

843 

844def trainer_node_image(runtime: dict[str, Any]) -> str | None: 

845 """Return the trainer image of a runtime's ``node`` replicated Job.""" 

846 template_spec = ((runtime.get("spec") or {}).get("template") or {}).get("spec") or {} 

847 jobs = template_spec.get("replicatedJobs") 

848 if not isinstance(jobs, list): 

849 return None 

850 for job in jobs: 

851 if not isinstance(job, dict) or job.get("name") != "node": 

852 continue 

853 pod_spec = (((job.get("template") or {}).get("spec") or {}).get("template") or {}).get( 

854 "spec" 

855 ) or {} 

856 containers = pod_spec.get("containers") 

857 if not isinstance(containers, list): 

858 return None 

859 for container in containers: 

860 if isinstance(container, dict) and container.get("name") == "node": 

861 image = container.get("image") 

862 return image if isinstance(image, str) else None 

863 return None 

864 

865 

866def example_trainer_image(example_text: str) -> str | None: 

867 """Return ``spec.trainer.image`` from the TrainJob example manifest.""" 

868 try: 

869 docs = [doc for doc in yaml.safe_load_all(example_text) if isinstance(doc, dict)] 

870 except yaml.YAMLError: 

871 return None 

872 for doc in docs: 

873 if doc.get("kind") == "TrainJob": 

874 image = ((doc.get("spec") or {}).get("trainer") or {}).get("image") 

875 return image if isinstance(image, str) else None 

876 return None 

877 

878 

879def doc_pytorch_images(doc_text: str) -> list[str]: 

880 """Return every ``pytorch/pytorch`` image the doc's snippets mention.""" 

881 return _DOC_PYTORCH_IMAGE_RE.findall(doc_text) 

882 

883 

884def _apply_documented_runtime_deviations(upstream_spec: dict[str, Any]) -> dict[str, Any]: 

885 """Return the upstream runtime spec with GCO's sanctioned deviations applied. 

886 

887 Exactly two deviations from verbatim extraction are documented in the 

888 manifest header, both on the ``node`` pod template: 

889 

890 - ``automountServiceAccountToken: false`` (the same security default 

891 both submission paths inject into user Jobs), and 

892 - ``securityContext.allowPrivilegeEscalation: false`` on the ``node`` 

893 container (NoNewPrivs; the platform's manifest policy already rejects 

894 an explicit ``true``). 

895 

896 Anything else that differs from upstream is drift and must fail — a new 

897 deliberate deviation belongs here *and* in the manifest header, in the 

898 same change. 

899 """ 

900 adjusted = copy.deepcopy(upstream_spec) 

901 jobs = ((adjusted.get("template") or {}).get("spec") or {}).get("replicatedJobs") 

902 for job in jobs if isinstance(jobs, list) else []: 

903 if not isinstance(job, dict) or job.get("name") != "node": 

904 continue 

905 pod_spec = (((job.get("template") or {}).get("spec") or {}).get("template") or {}).get( 

906 "spec" 

907 ) 

908 if not isinstance(pod_spec, dict): 

909 continue 

910 pod_spec["automountServiceAccountToken"] = False 

911 for container in pod_spec.get("containers") or []: 

912 if isinstance(container, dict) and container.get("name") == "node": 

913 security = container.setdefault("securityContext", {}) 

914 if isinstance(security, dict): 

915 security["allowPrivilegeEscalation"] = False 

916 return adjusted 

917 

918 

919def fetch_upstream_torch_runtime( 

920 entry: dict[str, Any], helm_binary: str = "helm" 

921) -> dict[str, Any]: 

922 """Render the pinned chart with runtime delivery enabled; return the runtime. 

923 

924 Runs against an isolated Helm home (same hermetic setup as the resolve / 

925 render pass) and rides the shared retry ladder, so registry blips do not 

926 fail the job. Raises ``RuntimeError`` with an actionable message when the 

927 chart cannot be rendered or no longer ships the runtime where this check 

928 expects it. 

929 """ 

930 refs = build_refs({_TRAINER_CHART_KEY: entry}) 

931 if not refs: 

932 raise RuntimeError( 

933 f"cannot build a Helm reference from the {_TRAINER_CHART_KEY!r} charts.yaml entry" 

934 ) 

935 ref = refs[0] 

936 with tempfile.TemporaryDirectory(prefix="gco-trainer-lockstep-") as tmp: 

937 env = _helm_env(Path(tmp)) 

938 if not ref.use_oci: 

939 _sync_classic_repos({ref.repo_name: ref.repo_url}, helm_binary, env) 

940 rc, out, err = _run_with_retry( 

941 [ 

942 helm_binary, 

943 "template", 

944 ref.name, 

945 ref.reference(), 

946 "--version", 

947 ref.version, 

948 "--namespace", 

949 ref.namespace, 

950 "--set", 

951 "runtimes.torchDistributed.enabled=true", 

952 ], 

953 env, 

954 timeout=180, 

955 description="helm template kubeflow-trainer (runtime delivery enabled)", 

956 ) 

957 if rc != 0: 

958 raise RuntimeError( 

959 f"helm template {ref.reference()} at {ref.version!r} (with " 

960 f"runtimes.torchDistributed.enabled=true) failed: {_tail(err)}" 

961 ) 

962 runtime = upstream_torch_runtime_from_render(out) 

963 if runtime is None: 

964 raise RuntimeError( 

965 f"chart {ref.version} no longer ships a 'runtimes-installer' ConfigMap " 

966 f"containing ClusterTrainingRuntime {_TRAINER_RUNTIME_NAME!r} — upstream " 

967 "runtime delivery changed; update this check alongside the re-extraction" 

968 ) 

969 return runtime 

970 

971 

972def validate_trainer_runtime_lockstep( 

973 charts: dict[str, Any], 

974 *, 

975 manifest_text: str | None = None, 

976 example_text: str | None = None, 

977 doc_text: str | None = None, 

978 online: bool = False, 

979 runtime_fetcher: Any = None, 

980 helm_binary: str = "helm", 

981 require_entry: bool = True, 

982) -> list[str]: 

983 """Check the trainer runtime / example / docs lockstep; return error strings. 

984 

985 Offline: the shipped runtime manifest, the TrainJob example and the 

986 distributed-training doc must agree on the trainer image. Online (adds 

987 one chart render): the shipped runtime must reproduce what the pinned 

988 chart ships — byte-identical spec after the documented deviations, with 

989 the trainer image compared explicitly for an actionable message. With 

990 ``require_entry`` (the default, used for the repository's real 

991 charts.yaml) a missing chart entry is reported rather than skipped; 

992 ``main()`` disables it for ``--charts`` fixture files. 

993 """ 

994 errors: list[str] = [] 

995 entry = charts.get(_TRAINER_CHART_KEY) 

996 if not isinstance(entry, dict) or not entry.get("version"): 

997 if require_entry: 

998 return [ 

999 f"trainer runtime lockstep: no {_TRAINER_CHART_KEY!r} entry with a " 

1000 "version in charts.yaml" 

1001 ] 

1002 return [] 

1003 

1004 if manifest_text is None: 

1005 try: 

1006 manifest_text = _TRAINER_RUNTIME_MANIFEST.read_text(encoding="utf-8") 

1007 except OSError as exc: 

1008 return [f"trainer runtime lockstep: cannot read {_TRAINER_RUNTIME_MANIFEST}: {exc}"] 

1009 if example_text is None: 

1010 try: 

1011 example_text = _TRAINJOB_EXAMPLE.read_text(encoding="utf-8") 

1012 except OSError as exc: 

1013 return [f"trainer runtime lockstep: cannot read {_TRAINJOB_EXAMPLE}: {exc}"] 

1014 if doc_text is None: 

1015 try: 

1016 doc_text = _DISTRIBUTED_TRAINING_DOC.read_text(encoding="utf-8") 

1017 except OSError as exc: 

1018 return [f"trainer runtime lockstep: cannot read {_DISTRIBUTED_TRAINING_DOC}: {exc}"] 

1019 

1020 shipped = parse_shipped_torch_runtime(manifest_text) 

1021 if shipped is None: 

1022 return [ 

1023 "trainer runtime lockstep: post-helm-kubeflow-trainer-runtimes.yaml no " 

1024 f"longer contains exactly one ClusterTrainingRuntime named " 

1025 f"{_TRAINER_RUNTIME_NAME!r}; update this check alongside any restructure" 

1026 ] 

1027 shipped_image = trainer_node_image(shipped) 

1028 if not shipped_image: 

1029 return [ 

1030 "trainer runtime lockstep: the shipped torch-distributed runtime has no " 

1031 "containers[name=node] image; update this check alongside any restructure" 

1032 ] 

1033 

1034 example_image = example_trainer_image(example_text) 

1035 if example_image != shipped_image: 

1036 errors.append( 

1037 f"trainer runtime lockstep: examples/kubeflow-trainjob.yaml pins " 

1038 f"spec.trainer.image {example_image!r} but the shipped torch-distributed " 

1039 f"runtime pins {shipped_image!r} — the example deliberately lists the " 

1040 "runtime's image so the image-trust gate validates exactly what runs; " 

1041 "bump both together" 

1042 ) 

1043 for doc_image in dict.fromkeys(doc_pytorch_images(doc_text)): 

1044 if doc_image != shipped_image: 

1045 errors.append( 

1046 f"trainer runtime lockstep: docs/DISTRIBUTED_TRAINING.md shows " 

1047 f"{doc_image!r} but the shipped torch-distributed runtime pins " 

1048 f"{shipped_image!r} — update the doc snippet in the same change" 

1049 ) 

1050 

1051 if not online: 

1052 return errors 

1053 

1054 fetcher = runtime_fetcher or fetch_upstream_torch_runtime 

1055 try: 

1056 upstream = fetcher(entry, helm_binary) 

1057 except RuntimeError as exc: 

1058 errors.append(f"trainer runtime lockstep: {exc}") 

1059 return errors 

1060 

1061 upstream_image = trainer_node_image(upstream) 

1062 if upstream_image != shipped_image: 

1063 errors.append( 

1064 f"trainer runtime lockstep: chart {entry.get('version')} ships " 

1065 f"torch-distributed with image {upstream_image!r} but " 

1066 f"post-helm-kubeflow-trainer-runtimes.yaml pins {shipped_image!r} — " 

1067 "re-extract the runtime from the pinned chart (helm template --set " 

1068 "runtimes.torchDistributed.enabled=true) and bump the example + doc " 

1069 "images in the same change" 

1070 ) 

1071 expected_spec = _apply_documented_runtime_deviations(upstream.get("spec") or {}) 

1072 if expected_spec != (shipped.get("spec") or {}): 

1073 errors.append( 

1074 f"trainer runtime lockstep: the shipped torch-distributed runtime spec " 

1075 f"differs from what chart {entry.get('version')} ships (beyond the " 

1076 "documented deviations in _apply_documented_runtime_deviations) — the manifest " 

1077 "header's contract is 'same bytes'; re-extract it from the pinned chart " 

1078 "or record a new sanctioned deviation in both the manifest header and " 

1079 "_apply_documented_runtime_deviations" 

1080 ) 

1081 shipped_labels = (shipped.get("metadata") or {}).get("labels") or {} 

1082 upstream_labels = (upstream.get("metadata") or {}).get("labels") or {} 

1083 for label in _TRAINER_RUNTIME_LOCKSTEP_LABELS: 

1084 if shipped_labels.get(label) != upstream_labels.get(label): 

1085 errors.append( 

1086 f"trainer runtime lockstep: label {label!r} is " 

1087 f"{shipped_labels.get(label)!r} in the shipped runtime but " 

1088 f"{upstream_labels.get(label)!r} upstream — these labels carry " 

1089 "upstream semantics (framework selection / webhook pre-validation) " 

1090 "and must be preserved verbatim" 

1091 ) 

1092 return errors 

1093 

1094 

1095# --------------------------------------------------------------------------- 

1096# Slurm REST API version lockstep 

1097# 

1098# Every GCO surface that talks to slurmrestd addresses one OpenAPI plugin 

1099# version (``/slurm/<version>/...``): the API client behind ``/api/v1/slurm``, 

1100# the ``gco slurm`` CLI through it, ``examples/slurm-cluster-job.yaml`` and the 

1101# live-validation probe (both call slurmrestd directly from inside the 

1102# cluster) and the API doc's sample payload. The pin is 

1103# ``SLURM_API_VERSION`` in ``gco/slurm_config.py``, beside the image tag it 

1104# moves with. 

1105# 

1106# The version is a property of the Slurm release the ``slinky-slurm`` images 

1107# are built from: each release ships its newest ``data_parser`` plugin plus 

1108# the three before it and drops the oldest, so bumping the image tag in 

1109# charts.yaml without re-checking the pin eventually leaves the API addressing 

1110# a version the image no longer serves — every request 404s. Negotiating the 

1111# version at runtime is deliberately not the fix: request and response bodies 

1112# differ between plugin versions, so the client is written against exactly 

1113# one. These checks encode the contract so the drift fails CI instead: 

1114# 

1115# offline: every surface that repeats the pin names the same version as 

1116# ``gco/slurm_config.py``. 

1117# online: ``slurmrestd -d list`` inside the pinned slurmrestd image names 

1118# the plugin versions it ships; the pin must be one of them. Needs a 

1119# container runtime (``docker`` on the CI runner; ``--container-runtime`` 

1120# for podman/finch). A newer served version is noted, not an error: 

1121# moving to it is a deliberate re-check of the body shapes. 

1122# --------------------------------------------------------------------------- 

1123 

1124_SLURM_CHART_KEY = "slinky-slurm" 

1125_SLURM_CONFIG_MODULE = _REPO_ROOT / "gco" / "slurm_config.py" 

1126# The files that repeat the pin outside gco/slurm_config.py. The route module 

1127# is included for the comment that records which version's job_desc_msg shape 

1128# the submission body was written against. 

1129_SLURM_API_VERSION_SURFACES: tuple[Path, ...] = ( 

1130 _REPO_ROOT / "examples" / "slurm-cluster-job.yaml", 

1131 _REPO_ROOT / "scripts" / "live_release_validation" / "manifests" / "slurm-smoke-job.yaml", 

1132 _REPO_ROOT / "docs" / "API.md", 

1133 _REPO_ROOT / "gco" / "services" / "api_routes" / "slurm.py", 

1134) 

1135_SLURM_API_VERSION_PIN_RE = re.compile(r'^SLURM_API_VERSION\s*=\s*"(v\d+\.\d+\.\d+)"', re.MULTILINE) 

1136# slurmrestd plugin versions are v0.0.NN; any such token in a Slurm surface is 

1137# a repeat of the pin (nothing else in those files is versioned that way). 

1138_SLURM_API_VERSION_MENTION_RE = re.compile(r"\bv0\.0\.\d+\b") 

1139# One line per plugin in the ``slurmrestd -d list`` output ("data_parser/v0.0.44"). 

1140_SLURMRESTD_PLUGIN_RE = re.compile(r"data_parser/(v\d+\.\d+\.\d+)") 

1141 

1142 

1143def parse_slurm_api_version(config_source: str) -> str | None: 

1144 """Return the ``SLURM_API_VERSION`` pin from ``gco/slurm_config.py`` source.""" 

1145 match = _SLURM_API_VERSION_PIN_RE.search(config_source) 

1146 return match.group(1) if match else None 

1147 

1148 

1149def slurm_api_version_mentions(text: str) -> list[tuple[int, str]]: 

1150 """Return ``(line_number, version)`` for every slurmrestd version token in ``text``.""" 

1151 return [ 

1152 (number, match.group(0)) 

1153 for number, line in enumerate(text.splitlines(), start=1) 

1154 for match in _SLURM_API_VERSION_MENTION_RE.finditer(line) 

1155 ] 

1156 

1157 

1158def parse_served_slurm_api_versions(listing: str) -> set[str]: 

1159 """Return the data_parser plugin versions a ``slurmrestd -d list`` listing names.""" 

1160 return set(_SLURMRESTD_PLUGIN_RE.findall(listing)) 

1161 

1162 

1163def slurmrestd_image(entry: dict[str, Any]) -> str | None: 

1164 """Return ``repository:tag`` of the slurmrestd image a ``slinky-slurm`` entry pins.""" 

1165 values = entry.get("values") or {} 

1166 restapi = values.get("restapi") or {} 

1167 image = (restapi.get("slurmrestd") or {}).get("image") 

1168 if not isinstance(image, dict) or not image.get("repository") or not image.get("tag"): 

1169 return None 

1170 return f"{image['repository']}:{image['tag']}" 

1171 

1172 

1173def _plugin_sort_key(version: str) -> tuple[int, ...]: 

1174 return _version_tuple(version.lstrip("v")) 

1175 

1176 

1177def _repo_relative(path: Path) -> str: 

1178 """Display form of a surface path: repo-relative when under the repo root.""" 

1179 try: 

1180 return str(path.relative_to(_REPO_ROOT)) 

1181 except ValueError: 

1182 return str(path) 

1183 

1184 

1185def fetch_served_slurm_api_versions(image: str, container_runtime: str = "docker") -> set[str]: 

1186 """Run ``slurmrestd -d list`` inside the pinned image; return the versions it ships. 

1187 

1188 The pull + run rides the shared retry ladder so a registry blip does not 

1189 fail the job. The container gets no network (the listing is local to the 

1190 binary). Raises ``RuntimeError`` with an actionable message when the image 

1191 cannot run or no longer prints a plugin listing where this check expects 

1192 it. 

1193 """ 

1194 cmd = [ 

1195 container_runtime, 

1196 "run", 

1197 "--rm", 

1198 "--network", 

1199 "none", 

1200 "--entrypoint", 

1201 "slurmrestd", 

1202 image, 

1203 "-d", 

1204 "list", 

1205 ] 

1206 rc, out, err = _run_with_retry( 

1207 cmd, 

1208 os.environ.copy(), 

1209 timeout=600, 

1210 description=f"{container_runtime} run {image} slurmrestd -d list", 

1211 ) 

1212 if rc != 0: 

1213 raise RuntimeError( 

1214 f"{container_runtime} run {image} (slurmrestd -d list) failed: {_tail(err or out)}" 

1215 ) 

1216 versions = parse_served_slurm_api_versions(out + "\n" + err) 

1217 if not versions: 

1218 raise RuntimeError( 

1219 f"slurmrestd -d list in {image} printed no data_parser/vX.Y.Z plugin lines — " 

1220 "upstream output changed; update this check alongside the new format" 

1221 ) 

1222 return versions 

1223 

1224 

1225def validate_slurm_api_version_lockstep( 

1226 charts: dict[str, Any], 

1227 *, 

1228 config_source: str | None = None, 

1229 surface_texts: dict[str, str] | None = None, 

1230 online: bool = False, 

1231 versions_fetcher: Any = None, 

1232 container_runtime: str = "docker", 

1233 require_entry: bool = True, 

1234) -> list[str]: 

1235 """Check the slurmrestd API version lockstep; return error strings. 

1236 

1237 Offline: every surface that repeats ``SLURM_API_VERSION`` (the example 

1238 job, the live-validation probe, the API doc and the route module's shape 

1239 note) names the version pinned in ``gco/slurm_config.py``. Online (adds 

1240 one image pull + run): the slurmrestd image pinned in charts.yaml must 

1241 serve that version. With ``require_entry`` (the default, used for the 

1242 repository's real charts.yaml) a missing chart entry is reported rather 

1243 than skipped; ``main()`` disables it for ``--charts`` fixture files. 

1244 ``surface_texts`` maps a display name to file text (tests); ``None`` 

1245 reads the repository files. 

1246 """ 

1247 errors: list[str] = [] 

1248 entry = charts.get(_SLURM_CHART_KEY) 

1249 if not isinstance(entry, dict) or not entry.get("version"): 

1250 if require_entry: 

1251 return [ 

1252 f"slurm api lockstep: no {_SLURM_CHART_KEY!r} entry with a version in charts.yaml" 

1253 ] 

1254 return [] 

1255 

1256 if config_source is None: 

1257 try: 

1258 config_source = _SLURM_CONFIG_MODULE.read_text(encoding="utf-8") 

1259 except OSError as exc: 

1260 return [f"slurm api lockstep: cannot read {_SLURM_CONFIG_MODULE}: {exc}"] 

1261 pin = parse_slurm_api_version(config_source) 

1262 if pin is None: 

1263 return [ 

1264 'slurm api lockstep: gco/slurm_config.py no longer defines SLURM_API_VERSION = "vX.Y.Z"; ' 

1265 "update this check alongside any rename" 

1266 ] 

1267 

1268 if surface_texts is None: 

1269 surface_texts = {} 

1270 for path in _SLURM_API_VERSION_SURFACES: 

1271 try: 

1272 surface_texts[_repo_relative(path)] = path.read_text(encoding="utf-8") 

1273 except OSError as exc: 

1274 return [f"slurm api lockstep: cannot read {path}: {exc}"] 

1275 for name, text in surface_texts.items(): 

1276 for line, version in slurm_api_version_mentions(text): 

1277 if version != pin: 

1278 errors.append( 

1279 f"slurm api lockstep: {name}:{line} names slurmrestd {version} but " 

1280 f"SLURM_API_VERSION in gco/slurm_config.py is {pin} — every surface " 

1281 "addresses one plugin version; update them in the same change" 

1282 ) 

1283 

1284 if not online: 

1285 return errors 

1286 

1287 image = slurmrestd_image(entry) 

1288 if image is None: 

1289 errors.append( 

1290 f"slurm api lockstep: charts.yaml {_SLURM_CHART_KEY}.values.restapi.slurmrestd.image " 

1291 "no longer pins a repository and tag; update this check alongside any restructure" 

1292 ) 

1293 return errors 

1294 

1295 fetcher = versions_fetcher or fetch_served_slurm_api_versions 

1296 try: 

1297 served = fetcher(image, container_runtime) 

1298 except RuntimeError as exc: 

1299 errors.append(f"slurm api lockstep: {exc}") 

1300 return errors 

1301 

1302 served_list = ", ".join(sorted(served, key=_plugin_sort_key)) 

1303 if pin not in served: 

1304 errors.append( 

1305 f"slurm api lockstep: {image} serves data_parser plugin version(s) {served_list} " 

1306 f"but SLURM_API_VERSION is {pin} — the GCO API would get 404 for every Slurm " 

1307 "request. Bump SLURM_API_VERSION in gco/slurm_config.py (and the surfaces that " 

1308 "repeat it) to a version the image ships, re-checking the request and response " 

1309 "shapes gco/services/slurm_client.py and api_routes/slurm.py rely on" 

1310 ) 

1311 return errors 

1312 

1313 newer = [ 

1314 v 

1315 for v in sorted(served, key=_plugin_sort_key) 

1316 if _plugin_sort_key(v) > _plugin_sort_key(pin) 

1317 ] 

1318 if newer: 

1319 print( 

1320 f"note: {image} serves SLURM_API_VERSION {pin} and also newer plugin version(s) " 

1321 f"{', '.join(newer)}; the newest stays supported longest across Slurm releases, " 

1322 "moving to it is a deliberate re-check of the body shapes (not required now)." 

1323 ) 

1324 return errors 

1325 

1326 

1327def emit_chart_ref(charts: dict[str, Any], chart_name: str) -> tuple[str, str]: 

1328 """Return ``(text, error)`` for --emit-ref. 

1329 

1330 The emitted line is "<helm-ref> <version> <namespace> <repo_url>", 

1331 space-separated so shell callers can consume it with a plain 

1332 ``read -r ref version namespace repo_url``. The reference is built by the 

1333 same ``ChartRef.reference()`` the online validator uses, which mirrors 

1334 ``handler.install_chart`` — the CI job installs exactly what the 

1335 installer Lambda would. ``repo_url`` rides along for classic (non-OCI) 

1336 charts, whose reference is ``<repo_name>/<chart>`` and only resolves 

1337 after ``helm repo add <repo_name> <repo_url>`` (or via 

1338 ``helm pull <chart> --repo <repo_url>``); for OCI charts it is the 

1339 ``oci://`` base already embedded in the reference. 

1340 """ 

1341 refs = {ref.name: ref for ref in build_refs(charts)} 

1342 ref = refs.get(chart_name) 

1343 if ref is None: 

1344 known = ", ".join(sorted(refs)) or "(none)" 

1345 return "", f"chart {chart_name!r} not found in charts.yaml (known: {known})" 

1346 return f"{ref.reference()} {ref.version} {ref.namespace} {ref.repo_url}", "" 

1347 

1348 

1349def emit_chart_values(charts: dict[str, Any], chart_name: str) -> tuple[str, str]: 

1350 """Return ``(yaml_text, error)`` for --emit-values. 

1351 

1352 Fails when the values still carry a ``{{TOKEN}}`` deployment placeholder: 

1353 charts.yaml values are the deploy-time *fallback* and must be 

1354 standalone-installable (the regional stack only ever layers additional 

1355 overrides on top). A token here would mean the fallback contract broke — 

1356 better to fail the emit than install a chart with a literal ``{{...}}``. 

1357 """ 

1358 refs = {ref.name: ref for ref in build_refs(charts)} 

1359 ref = refs.get(chart_name) 

1360 if ref is None: 

1361 known = ", ".join(sorted(refs)) or "(none)" 

1362 return "", f"chart {chart_name!r} not found in charts.yaml (known: {known})" 

1363 text = yaml.safe_dump(ref.values, default_flow_style=False, sort_keys=False) 

1364 if "{{" in text: 

1365 tokens = sorted(set(re.findall(r"\{\{[A-Z0-9_]+\}\}", text))) 

1366 return "", ( 

1367 f"{chart_name}: values contain deployment tokens {tokens} — " 

1368 "charts.yaml values must be standalone-installable fallbacks" 

1369 ) 

1370 return text, "" 

1371 

1372 

1373def _build_parser() -> argparse.ArgumentParser: 

1374 parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0]) 

1375 parser.add_argument( 

1376 "--charts", 

1377 type=Path, 

1378 default=_DEFAULT_CHARTS, 

1379 help="Path to charts.yaml (defaults to lambda/helm-installer/charts.yaml).", 

1380 ) 

1381 parser.add_argument( 

1382 "--mode", 

1383 choices=("auto", "offline", "online"), 

1384 default="auto", 

1385 help=( 

1386 "auto (default): structural checks always, online checks when helm " 

1387 "is on PATH. offline: structural only. online: require helm and run " 

1388 "the resolve/render checks (fail if helm is missing)." 

1389 ), 

1390 ) 

1391 parser.add_argument( 

1392 "--skip-template", 

1393 action="store_true", 

1394 help="Online mode only: resolve each chart but skip the helm template render.", 

1395 ) 

1396 parser.add_argument( 

1397 "--enabled-only", 

1398 action="store_true", 

1399 help="Validate only charts with enabled: true (default: validate every entry).", 

1400 ) 

1401 parser.add_argument( 

1402 "--helm-binary", 

1403 default="helm", 

1404 help="Helm executable to use (default: 'helm' on PATH).", 

1405 ) 

1406 parser.add_argument( 

1407 "--container-runtime", 

1408 default=os.environ.get("GCO_CONTAINER_RUNTIME", "docker"), 

1409 help=( 

1410 "Container CLI used by the online slurmrestd API-version check to run the " 

1411 "pinned slurmrestd image (default: $GCO_CONTAINER_RUNTIME or 'docker'; podman " 

1412 "and finch work too). Required by --mode online; auto mode skips that one check " 

1413 "when it is missing." 

1414 ), 

1415 ) 

1416 parser.add_argument( 

1417 "-v", 

1418 "--verbose", 

1419 action="store_true", 

1420 help="Print a per-chart resolve/render line during the online pass.", 

1421 ) 

1422 emit = parser.add_mutually_exclusive_group() 

1423 emit.add_argument( 

1424 "--emit-ref", 

1425 metavar="CHART", 

1426 help=( 

1427 "Print '<helm-ref> <version> <namespace>' for one charts.yaml entry " 

1428 "and exit (query mode for CI jobs that helm-install the pinned chart)." 

1429 ), 

1430 ) 

1431 emit.add_argument( 

1432 "--emit-values", 

1433 metavar="CHART", 

1434 help=( 

1435 "Print the shipped values block for one charts.yaml entry as YAML " 

1436 "and exit; fails if the values carry {{TOKEN}} placeholders." 

1437 ), 

1438 ) 

1439 return parser 

1440 

1441 

1442def main(argv: list[str] | None = None) -> int: 

1443 args = _build_parser().parse_args(argv) 

1444 

1445 try: 

1446 charts = load_charts(args.charts) 

1447 except FileNotFoundError: 

1448 print(f"ERROR: charts file not found: {args.charts}", file=sys.stderr) 

1449 return 2 

1450 except (yaml.YAMLError, ValueError) as exc: 

1451 print(f"ERROR: could not parse {args.charts}: {exc}", file=sys.stderr) 

1452 return 2 

1453 

1454 if args.emit_ref or args.emit_values: 

1455 emitter = emit_chart_ref if args.emit_ref else emit_chart_values 

1456 text, error = emitter(charts, args.emit_ref or args.emit_values) 

1457 if error: 

1458 print(f"ERROR: {error}", file=sys.stderr) 

1459 return 2 

1460 print(text, end="" if text.endswith("\n") else "\n") 

1461 return 0 

1462 

1463 errors = validate_structure(charts, enabled_only=args.enabled_only) 

1464 

1465 helm_available = shutil.which(args.helm_binary) is not None 

1466 run_online = False 

1467 if args.mode == "online": 

1468 if not helm_available: 

1469 print( 

1470 f"ERROR: --mode online requires the '{args.helm_binary}' binary on PATH.", 

1471 file=sys.stderr, 

1472 ) 

1473 return 2 

1474 run_online = True 

1475 elif args.mode == "auto": 

1476 run_online = helm_available 

1477 if not helm_available: 

1478 print( 

1479 "note: 'helm' not found on PATH; running structural checks only. " 

1480 "Pass --mode online in CI to require the resolve/render checks." 

1481 ) 

1482 

1483 # The slurmrestd API-version check runs the pinned image, so its online 

1484 # half additionally needs a container runtime: required alongside helm in 

1485 # --mode online, skipped with a note in auto mode. 

1486 run_slurm_online = run_online 

1487 if run_online and shutil.which(args.container_runtime) is None: 

1488 if args.mode == "online": 

1489 print( 

1490 f"ERROR: --mode online requires the '{args.container_runtime}' binary on PATH " 

1491 "(the slurmrestd API-version check runs the pinned slurmrestd image; pass " 

1492 "--container-runtime podman|finch for another runtime).", 

1493 file=sys.stderr, 

1494 ) 

1495 return 2 

1496 run_slurm_online = False 

1497 print( 

1498 f"note: '{args.container_runtime}' not found on PATH; skipping the online " 

1499 "slurmrestd API-version check. Pass --mode online in CI to require it " 

1500 "(--container-runtime selects podman/finch)." 

1501 ) 

1502 

1503 # Fixture chart files passed via --charts may legitimately omit the 

1504 # controller / trainer / Slurm entries; the repository's real charts.yaml 

1505 # may not. 

1506 is_default_charts = args.charts.resolve() == _DEFAULT_CHARTS.resolve() 

1507 errors.extend( 

1508 validate_gateway_lockstep( 

1509 charts, 

1510 online=run_online, 

1511 require_entry=is_default_charts, 

1512 ) 

1513 ) 

1514 errors.extend( 

1515 validate_trainer_runtime_lockstep( 

1516 charts, 

1517 online=run_online, 

1518 helm_binary=args.helm_binary, 

1519 require_entry=is_default_charts, 

1520 ) 

1521 ) 

1522 errors.extend( 

1523 validate_slurm_api_version_lockstep( 

1524 charts, 

1525 online=run_slurm_online, 

1526 container_runtime=args.container_runtime, 

1527 require_entry=is_default_charts, 

1528 ) 

1529 ) 

1530 

1531 refs = build_refs(charts, enabled_only=args.enabled_only) 

1532 if run_online: 

1533 errors.extend( 

1534 validate_online( 

1535 refs, 

1536 helm_binary=args.helm_binary, 

1537 skip_template=args.skip_template, 

1538 verbose=args.verbose, 

1539 ) 

1540 ) 

1541 

1542 if errors: 

1543 print() 

1544 print(f"ERROR: {len(errors)} Helm chart validation problem(s) found:") 

1545 for err in errors: 

1546 print(f" - {err}") 

1547 print() 

1548 print( 

1549 "Fix the pinned chart name/version in lambda/helm-installer/charts.yaml " 

1550 "so every entry is a real, installable Helm chart." 

1551 ) 

1552 return 1 

1553 

1554 scope = "enabled" if args.enabled_only else "all" 

1555 if run_online: 

1556 rendered = "" if args.skip_template else " + rendered" 

1557 print( 

1558 f"OK: {len(refs)} Helm chart(s) ({scope}) are well-formed and " 

1559 f"resolvable{rendered} at their pinned versions." 

1560 ) 

1561 else: 

1562 print(f"OK: {len(refs)} Helm chart(s) ({scope}) are structurally valid.") 

1563 return 0 

1564 

1565 

1566if __name__ == "__main__": 

1567 sys.exit(main())