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

257 statements  

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

1#!/usr/bin/env python3 

2"""Preload the images a rendered Helm chart runs into a kind node. 

3 

4``helm install --wait`` in the kind jobs leaves every image pull to the 

5kubelet, which retries a failed pull on its own exponential backoff (10 s, 

620 s, ... up to 5 min between attempts), so an eight-minute wait gets about six 

7tries. That is not enough for Amazon ECR Public. Anonymous clients get one 

8image-pull request per second (the documented "Rate of unauthenticated image 

9pulls" quota, which is not adjustable), and ECR Public answers the next request 

10with 429 Too Many Requests. A pull by tag sends its manifest requests 

11back-to-back (the tag, the image index, then the platform manifest), so the 

12second is often refused even from an idle client. GitHub-hosted runners also 

13share their egress addresses with other jobs. Argo CD's chart runs Redis from 

14ECR Public, and ``integration:kind:platform-addons`` kept failing on exactly 

15that 429 (``argocd-redis`` stuck in ImagePullBackOff until the wait expired). 

16 

17For every image the rendered manifests run, this pulls the image on the runner 

18with retry and then ``kind load``s it, so the kubelet (the charts pull 

19``IfNotPresent``) finds it on the node and never contacts a registry. Helm test 

20hooks are skipped because ``helm install`` never starts them. A Docker Official 

21Image served from ECR Public (``<ecr-public>/docker/library/<name>``) is first 

22resolved to its digest with paced requests to ECR Public. It is then pulled by 

23that digest from Docker Hub, which publishes the same index, and tagged back to 

24the chart's own reference. The bytes are the ones ECR Public serves, and nothing 

25the chart renders changes. If Docker Hub does not serve that digest, the image is 

26pulled from ECR Public by digest, with more attempts. If ECR Public never answers 

27the digest lookup (every request throttled), Docker Hub's copy of the same tag is 

28used, with a warning; a definitive answer, such as a tag ECR Public does not 

29have, still fails the step. Any other image is pulled as written. An image pinned 

30by digest cannot be addressed by ``kind load``, so it is left to the kubelet, 

31with a warning. 

32 

33``--pinned IMAGE=LOCAL`` covers a pod the job writes itself that must run 

34digest-pinned bytes. Anonymous ECR Public pulls also run into its data limit 

35(429 "Data limit exceeded") from shared runner addresses, which kept failing 

36``integration:kind:examples-smoke`` on the MLflow TLS sidecar's pinned Python 

37image. The image is pulled by its digest (Docker Hub's copy first for a Docker 

38Official Image on ECR Public, then ECR Public), tagged ``LOCAL`` and loaded. The 

39pod names ``LOCAL`` with ``imagePullPolicy: Never``, so it never reaches a 

40registry. 

41 

42``--image NAME`` loads an image a previous step already put in the runner's 

43Docker (a service image built with ``load: true``, or an example's image pulled 

44with retry) without pulling anything. 

45 

46Every load goes through one path. ``kind load docker-image`` runs ``docker 

47save`` for the whole image and imports the archive with ``ctr images import 

48--all-platforms``. Docker 29 (the Ubuntu 26.04 runner image) stores images in 

49containerd, where a pulled or built image is recorded under its multi-platform 

50index while only the daemon's own platform has content, so that import fails 

51with ``content digest sha256:...: not found`` (kubernetes-sigs/kind#4224). This 

52script instead saves each image with ``docker save --platform`` for the 

53daemon's platform (``docker version`` reports it), so the archive carries one 

54manifest and the blobs behind it, and loads that archive with ``kind load 

55image-archive``. One archive at a time keeps the runner's disk peak at one 

56image. The archive lives under ``TMPDIR``, which the workflows point at the 

57runner's work disk. 

58 

59Usage:: 

60 

61 python3 .github/scripts/preload_kind_images.py --cluster NAME RENDERED.yaml [...] 

62 python3 .github/scripts/preload_kind_images.py --cluster NAME --pinned IMAGE@DIGEST=LOCAL:TAG 

63 python3 .github/scripts/preload_kind_images.py --cluster NAME --image NAME:TAG [--image ...] 

64""" 

65 

66from __future__ import annotations 

67 

68import argparse 

69import json 

70import re 

71import subprocess 

72import sys 

73import tempfile 

74import time 

75import urllib.error 

76import urllib.request 

77from collections.abc import Callable, Iterable, Iterator, Mapping, Sequence 

78from dataclasses import dataclass 

79from email.message import Message 

80from pathlib import Path 

81from typing import Any 

82 

83import yaml 

84 

85#: Amazon ECR Public's registry hosts: the gallery name and the newer endpoint 

86#: charts reference (Argo CD's runs ``ecr-public.aws.com/docker/library/redis``). 

87ECR_PUBLIC_HOSTS = frozenset({"public.ecr.aws", "ecr-public.aws.com"}) 

88#: ECR Public's service name in its token realm, the same for both hosts. 

89ECR_PUBLIC_SERVICE = "public.ecr.aws" 

90#: ECR Public's mirror of the Docker Official Images, published by Docker. 

91DOCKER_LIBRARY = "docker/library/" 

92DOCKER_HUB_LIBRARY = "docker.io/library/" 

93 

94MANIFEST_ACCEPT = ", ".join( 

95 ( 

96 "application/vnd.oci.image.index.v1+json", 

97 "application/vnd.docker.distribution.manifest.list.v2+json", 

98 "application/vnd.oci.image.manifest.v1+json", 

99 "application/vnd.docker.distribution.manifest.v2+json", 

100 ) 

101) 

102DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$") 

103#: ``os/arch`` as ``docker version`` prints it (``linux/amd64``, ``linux/arm64``). 

104PLATFORM_RE = re.compile(r"^[a-z0-9]+/[a-z0-9]+$") 

105#: The daemon's own platform, the one an image it built or pulled has content for. 

106DAEMON_PLATFORM_ARGV = ("docker", "version", "--format", "{{.Server.Os}}/{{.Server.Arch}}") 

107 

108#: Pod-spec keys whose entries carry an ``image``. 

109CONTAINER_KEYS = ("initContainers", "containers", "ephemeralContainers") 

110#: ``helm.sh/hook`` values ``helm install`` never runs (``helm test`` does). 

111TEST_HOOKS = frozenset({"test", "test-success", "test-failure"}) 

112 

113#: ECR Public admits one anonymous request per second: space them further apart. 

114REQUEST_SPACING_SECONDS = 1.5 

115REGISTRY_ATTEMPTS = 8 

116REGISTRY_BACKOFF_CAP_SECONDS = 20.0 

117REQUEST_TIMEOUT_SECONDS = 20.0 

118PULL_ATTEMPTS = 4 

119PULL_DELAY_SECONDS = 15.0 

120#: The ECR Public fallback pull trips the same throttle, so it gets more tries. 

121ECR_PUBLIC_PULL_ATTEMPTS = 12 

122ECR_PUBLIC_PULL_DELAY_SECONDS = 5.0 

123 

124Runner = Callable[[Sequence[str]], int] 

125#: Runs a command and returns its stdout; an empty string when it fails. 

126Capturer = Callable[[Sequence[str]], str] 

127Opener = Callable[[urllib.request.Request, float], Any] 

128Sleeper = Callable[[float], None] 

129 

130 

131class PreloadError(RuntimeError): 

132 """An image could not be resolved, pulled, tagged or loaded.""" 

133 

134 

135class RegistryUnreachable(PreloadError): 

136 """Every attempt was throttled (429) or lost in transport: no answer about the image.""" 

137 

138 

139@dataclass(frozen=True) 

140class ImageReference: 

141 """One image reference, normalized the way Docker and containerd read it.""" 

142 

143 registry: str 

144 repository: str 

145 tag: str | None 

146 digest: str | None 

147 

148 @classmethod 

149 def parse(cls, reference: str) -> ImageReference: 

150 """Split ``[registry/]repository[:tag][@digest]``; Docker Hub is the default registry.""" 

151 name, _, digest = reference.strip().partition("@") 

152 first, slash, rest = name.partition("/") 

153 if slash and ("." in first or ":" in first or first == "localhost"): 

154 registry, path = first, rest 

155 else: 

156 registry, path = "docker.io", name 

157 if "/" not in path: 

158 path = f"library/{path}" 

159 tag: str | None = None 

160 if ":" in path.rsplit("/", 1)[-1]: 

161 path, tag = path.rsplit(":", 1) 

162 if not all(path.split("/")) or tag == "" or (digest and not DIGEST_RE.match(digest)): 

163 raise PreloadError(f"cannot parse the image reference {reference!r}") 

164 if not digest and tag is None: 

165 tag = "latest" 

166 return cls(registry=registry, repository=path, tag=tag, digest=digest or None) 

167 

168 def docker_hub_twin(self) -> str | None: 

169 """Docker Hub's name for a Docker Official Image mirrored on ECR Public, else None.""" 

170 if self.registry in ECR_PUBLIC_HOSTS and self.repository.startswith(DOCKER_LIBRARY): 

171 return DOCKER_HUB_LIBRARY + self.repository.removeprefix(DOCKER_LIBRARY) 

172 return None 

173 

174 

175def _is_test_hook(document: Mapping[str, Any]) -> bool: 

176 metadata = document.get("metadata") 

177 annotations = metadata.get("annotations") if isinstance(metadata, Mapping) else None 

178 hooks = annotations.get("helm.sh/hook", "") if isinstance(annotations, Mapping) else "" 

179 return any(hook.strip() in TEST_HOOKS for hook in str(hooks).split(",")) 

180 

181 

182def _container_images(node: Any) -> Iterator[str]: 

183 """Every container image under ``node``, wherever a pod template sits.""" 

184 if isinstance(node, Mapping): 

185 for key, value in node.items(): 

186 if key in CONTAINER_KEYS and isinstance(value, list): 

187 for container in value: 

188 image = container.get("image") if isinstance(container, Mapping) else None 

189 if isinstance(image, str) and image.strip(): 

190 yield image.strip() 

191 else: 

192 yield from _container_images(value) 

193 elif isinstance(node, list): 

194 for item in node: 

195 yield from _container_images(item) 

196 

197 

198def collect_images(texts: Iterable[str]) -> list[str]: 

199 """The images the rendered manifests run, in first-seen order, test hooks excluded.""" 

200 images: dict[str, None] = {} 

201 for text in texts: 

202 for document in yaml.safe_load_all(text): 

203 if isinstance(document, Mapping) and not _is_test_hook(document): 

204 for image in _container_images(document): 

205 images.setdefault(image, None) 

206 return list(images) 

207 

208 

209def _open(request: urllib.request.Request, timeout: float) -> Any: 

210 return urllib.request.urlopen( # nosec B310 # nosemgrep: dynamic-urllib-use-detected - every URL is https:// on a host from the fixed ECR_PUBLIC_HOSTS set; a reference on any other registry never reaches this function 

211 request, timeout=timeout 

212 ) 

213 

214 

215def _run(argv: Sequence[str]) -> int: 

216 return subprocess.run( # nosemgrep: dangerous-subprocess-use-audit - fixed docker/kind argv, no shell=True 

217 list(argv), check=False 

218 ).returncode 

219 

220 

221def _capture(argv: Sequence[str]) -> str: 

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

223 list(argv), check=False, capture_output=True, text=True 

224 ) 

225 return completed.stdout if completed.returncode == 0 else "" 

226 

227 

228def _registry_request( 

229 url: str, 

230 *, 

231 method: str = "GET", 

232 headers: Mapping[str, str] | None = None, 

233 opener: Opener = _open, 

234 sleep: Sleeper = time.sleep, 

235 attempts: int = REGISTRY_ATTEMPTS, 

236 spacing: float = REQUEST_SPACING_SECONDS, 

237) -> tuple[Message, bytes]: 

238 """One ECR Public request, retried on 429 and transport errors with growing waits. 

239 

240 Any other HTTP status is an answer (a missing tag is a 404) and raises 

241 :class:`PreloadError` at once; running out of attempts raises 

242 :class:`RegistryUnreachable`. 

243 """ 

244 last_error = "" 

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

246 retry_after = "" 

247 request = urllib.request.Request(url, method=method, headers=dict(headers or {})) 

248 try: 

249 with opener(request, REQUEST_TIMEOUT_SECONDS) as response: 

250 return response.headers, response.read() 

251 except urllib.error.HTTPError as exc: 

252 if exc.code != 429: 

253 raise PreloadError(f"{method} {url} answered HTTP {exc.code}") from exc 

254 last_error = "HTTP 429 Too Many Requests" 

255 retry_after = str((exc.headers.get("Retry-After") if exc.headers else None) or "") 

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

257 last_error = f"{type(exc).__name__}: {exc}" 

258 if attempt < attempts: 

259 wait = min(spacing * 2 ** (attempt - 1), REGISTRY_BACKOFF_CAP_SECONDS) 

260 if retry_after.isdigit(): 

261 wait = max(wait, float(retry_after)) 

262 print(f"{method} {url}: {last_error}; retrying in {wait:.1f}s", flush=True) 

263 sleep(wait) 

264 raise RegistryUnreachable(f"{method} {url} failed after {attempts} attempts ({last_error})") 

265 

266 

267def ecr_public_digest( 

268 reference: ImageReference, 

269 *, 

270 opener: Opener = _open, 

271 sleep: Sleeper = time.sleep, 

272) -> str: 

273 """Resolve an ECR Public tag to the digest it serves: one token and one HEAD request.""" 

274 if reference.digest is not None: 

275 return reference.digest 

276 scope = f"repository:{reference.repository}:pull" 

277 token_url = f"https://{reference.registry}/token/?service={ECR_PUBLIC_SERVICE}&scope={scope}" 

278 _headers, body = _registry_request(token_url, opener=opener, sleep=sleep) 

279 try: 

280 token = json.loads(body)["token"] 

281 except (ValueError, KeyError, TypeError) as exc: 

282 raise PreloadError(f"{token_url} returned no anonymous token") from exc 

283 sleep(REQUEST_SPACING_SECONDS) 

284 manifest_url = ( 

285 f"https://{reference.registry}/v2/{reference.repository}/manifests/{reference.tag}" 

286 ) 

287 headers, _body = _registry_request( 

288 manifest_url, 

289 method="HEAD", 

290 headers={"Authorization": f"Bearer {token}", "Accept": MANIFEST_ACCEPT}, 

291 opener=opener, 

292 sleep=sleep, 

293 ) 

294 digest = str(headers.get("Docker-Content-Digest") or "").strip() 

295 if not DIGEST_RE.match(digest): 

296 raise PreloadError(f"{manifest_url} returned no usable Docker-Content-Digest ({digest!r})") 

297 return digest 

298 

299 

300def pull_with_retry( 

301 image: str, 

302 *, 

303 run: Runner = _run, 

304 sleep: Sleeper = time.sleep, 

305 attempts: int = PULL_ATTEMPTS, 

306 delay: float = PULL_DELAY_SECONDS, 

307) -> bool: 

308 """``docker pull`` with a fixed wait between attempts; False once they run out.""" 

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

310 print(f"::group::docker pull {image} (attempt {attempt}/{attempts})", flush=True) 

311 code = run(["docker", "pull", image]) 

312 print("::endgroup::", flush=True) 

313 if code == 0: 

314 return True 

315 if attempt < attempts: 

316 print( 

317 f"::warning::docker pull {image} failed (attempt {attempt}/{attempts}); " 

318 f"retrying in {delay:.0f}s", 

319 flush=True, 

320 ) 

321 sleep(delay) 

322 return False 

323 

324 

325def _tag(source: str, target: str, run: Runner) -> None: 

326 if run(["docker", "tag", source, target]) != 0: 

327 raise PreloadError(f"docker tag {source} {target} failed") 

328 

329 

330def stage_image( 

331 image: str, 

332 *, 

333 run: Runner = _run, 

334 sleep: Sleeper = time.sleep, 

335 resolve: Callable[[ImageReference], str] = ecr_public_digest, 

336) -> bool: 

337 """Put ``image`` in the runner's Docker under the chart's own name; False when skipped.""" 

338 reference = ImageReference.parse(image) 

339 if reference.digest is not None: 

340 print( 

341 f"::warning::{image} is pinned by digest, which kind load cannot address; " 

342 "the kubelet pulls it", 

343 flush=True, 

344 ) 

345 return False 

346 twin = reference.docker_hub_twin() 

347 if twin is None: 

348 if not pull_with_retry(image, run=run, sleep=sleep): 

349 raise PreloadError(f"could not pull {image} after {PULL_ATTEMPTS} attempts") 

350 return True 

351 

352 try: 

353 digest = resolve(reference) 

354 except RegistryUnreachable as exc: 

355 # ECR Public never answered, so its digest cannot be compared. The 

356 # tag is the Docker Official Image either way. A definitive answer 

357 # (a tag ECR Public does not have) is a PreloadError and is not caught. 

358 source = f"{twin}:{reference.tag}" 

359 print(f"::warning::{exc}; pulling {source} without the digest check", flush=True) 

360 if not pull_with_retry(source, run=run, sleep=sleep): 

361 raise PreloadError(f"could not pull {source} after {PULL_ATTEMPTS} attempts") from exc 

362 _tag(source, image, run) 

363 return True 

364 source = f"{twin}@{digest}" 

365 if pull_with_retry(source, run=run, sleep=sleep): 

366 _tag(source, image, run) 

367 print(f"{image}: pulled {source}, Docker Hub's copy of the same index", flush=True) 

368 return True 

369 print(f"::warning::Docker Hub does not serve {source}; pulling it from ECR Public", flush=True) 

370 fallback = f"{reference.registry}/{reference.repository}@{digest}" 

371 if not pull_with_retry( 

372 fallback, 

373 run=run, 

374 sleep=sleep, 

375 attempts=ECR_PUBLIC_PULL_ATTEMPTS, 

376 delay=ECR_PUBLIC_PULL_DELAY_SECONDS, 

377 ): 

378 raise PreloadError( 

379 f"could not pull {image} ({digest}) from Docker Hub or, after " 

380 f"{ECR_PUBLIC_PULL_ATTEMPTS} attempts, from ECR Public" 

381 ) 

382 _tag(fallback, image, run) 

383 return True 

384 

385 

386def stage_pinned_image( 

387 image: str, 

388 local: str, 

389 *, 

390 run: Runner = _run, 

391 sleep: Sleeper = time.sleep, 

392) -> None: 

393 """Put the digest-pinned ``image`` in the runner's Docker under the local tag ``local``. 

394 

395 ``kind load`` cannot address a digest reference, so a pod that must run 

396 pinned bytes without a registry names a local tag and pulls ``Never``. 

397 Docker checks a pull by digest against that digest, so the bytes are the 

398 pinned ones whichever registry serves them. A Docker Official Image on ECR 

399 Public comes from Docker Hub first, which publishes the same index, then 

400 from ECR Public with more attempts. Any other image is pulled by digest 

401 from its own registry. 

402 """ 

403 reference = ImageReference.parse(image) 

404 if reference.digest is None: 

405 raise PreloadError(f"{image} is not pinned by digest") 

406 if ImageReference.parse(local).digest is not None: 

407 raise PreloadError(f"the local name {local} must be a tag, not a digest") 

408 own = f"{reference.registry}/{reference.repository}@{reference.digest}" 

409 twin = reference.docker_hub_twin() 

410 sources = ( 

411 [ 

412 (f"{twin}@{reference.digest}", PULL_ATTEMPTS, PULL_DELAY_SECONDS), 

413 (own, ECR_PUBLIC_PULL_ATTEMPTS, ECR_PUBLIC_PULL_DELAY_SECONDS), 

414 ] 

415 if twin is not None 

416 else [(own, PULL_ATTEMPTS, PULL_DELAY_SECONDS)] 

417 ) 

418 for source, attempts, delay in sources: 

419 if pull_with_retry(source, run=run, sleep=sleep, attempts=attempts, delay=delay): 

420 _tag(source, local, run) 

421 print(f"{image}: pulled {source} and tagged it {local}", flush=True) 

422 return 

423 print(f"::warning::could not pull {source}", flush=True) 

424 raise PreloadError( 

425 f"could not pull {image} from " + " or ".join(source for source, _, _ in sources) 

426 ) 

427 

428 

429def _pinned_argument(value: str) -> tuple[str, str]: 

430 """Parse ``--pinned IMAGE=LOCAL``.""" 

431 image, separator, local = value.partition("=") 

432 if not separator or not image.strip() or not local.strip(): 

433 raise argparse.ArgumentTypeError(f"expected IMAGE=LOCAL, got {value!r}") 

434 return image.strip(), local.strip() 

435 

436 

437def daemon_platform(*, capture: Capturer = _capture) -> str: 

438 """The Docker daemon's ``os/arch``: the one platform its images have content for.""" 

439 platform = capture(DAEMON_PLATFORM_ARGV).strip() 

440 if not PLATFORM_RE.match(platform): 

441 raise PreloadError(f"docker version reported no usable daemon platform ({platform!r})") 

442 return platform 

443 

444 

445def load_into_kind( 

446 images: Sequence[str], 

447 cluster: str, 

448 *, 

449 run: Runner = _run, 

450 capture: Capturer = _capture, 

451) -> None: 

452 """Load every staged image into the kind node, one single-platform archive at a time. 

453 

454 ``docker save --platform`` writes only the daemon's platform, so the 

455 archive is complete and ``kind load image-archive`` (``ctr images import 

456 --all-platforms``) accepts it from Docker's containerd image store, where a 

457 plain ``kind load docker-image`` fails on the other platforms' missing 

458 content. Each archive is removed before the next is written. 

459 """ 

460 if not images: 

461 return 

462 platform = daemon_platform(capture=capture) 

463 for image in images: 

464 with tempfile.TemporaryDirectory(prefix="kind-image-") as directory: 

465 archive = str(Path(directory) / "image.tar") 

466 if run(["docker", "save", "--platform", platform, "--output", archive, image]) != 0: 

467 raise PreloadError(f"docker save --platform {platform} {image} failed") 

468 if run(["kind", "load", "image-archive", "--name", cluster, archive]) != 0: 

469 raise PreloadError(f"kind load image-archive of {image} into {cluster} failed") 

470 print(f"loaded {image} ({platform}) into kind cluster {cluster}", flush=True) 

471 

472 

473def _image_argument(value: str) -> str: 

474 """Parse ``--image NAME``: a tag reference already in the runner's Docker.""" 

475 image = value.strip() 

476 try: 

477 reference = ImageReference.parse(image) 

478 except PreloadError as exc: 

479 raise argparse.ArgumentTypeError(str(exc)) from exc 

480 if reference.digest is not None: 

481 raise argparse.ArgumentTypeError( 

482 f"{image} is pinned by digest, which kind cannot address; give the tag it carries" 

483 ) 

484 return image 

485 

486 

487def main( 

488 argv: Sequence[str] | None = None, 

489 *, 

490 run: Runner = _run, 

491 capture: Capturer = _capture, 

492 sleep: Sleeper = time.sleep, 

493 resolve: Callable[[ImageReference], str] = ecr_public_digest, 

494) -> int: 

495 parser = argparse.ArgumentParser( 

496 description="Pull the images rendered Helm manifests run and load them into kind." 

497 ) 

498 parser.add_argument("--cluster", required=True, help="kind cluster name") 

499 parser.add_argument( 

500 "--pinned", 

501 action="append", 

502 default=[], 

503 type=_pinned_argument, 

504 metavar="IMAGE=LOCAL", 

505 help=( 

506 "also stage the digest-pinned IMAGE under the local tag LOCAL, for a pod " 

507 "that runs it with imagePullPolicy: Never (repeatable)" 

508 ), 

509 ) 

510 parser.add_argument( 

511 "--image", 

512 action="append", 

513 default=[], 

514 type=_image_argument, 

515 metavar="NAME", 

516 help=( 

517 "also load NAME, an image a previous step built or pulled into the runner's " 

518 "Docker, without pulling it (repeatable)" 

519 ), 

520 ) 

521 parser.add_argument("manifests", nargs="*", type=Path, help="rendered manifest files") 

522 args = parser.parse_args(argv) 

523 if not args.manifests and not args.pinned and not args.image: 

524 parser.error("give rendered manifests, --pinned IMAGE=LOCAL, --image NAME, or a mix") 

525 

526 images = collect_images(path.read_text(encoding="utf-8") for path in args.manifests) 

527 if args.manifests and not images: 

528 print("::error::the rendered manifests run no container images", flush=True) 

529 return 1 

530 try: 

531 staged = [ 

532 image for image in images if stage_image(image, run=run, sleep=sleep, resolve=resolve) 

533 ] 

534 for image, local in args.pinned: 

535 stage_pinned_image(image, local, run=run, sleep=sleep) 

536 load_into_kind( 

537 [*staged, *(local for _, local in args.pinned), *args.image], 

538 args.cluster, 

539 run=run, 

540 capture=capture, 

541 ) 

542 except PreloadError as exc: 

543 print(f"::error::{exc}", flush=True) 

544 return 1 

545 if args.manifests: 

546 print(f"preloaded {len(staged)} of {len(images)} image(s) into kind cluster {args.cluster}") 

547 if args.pinned: 

548 print( 

549 f"loaded {len(args.pinned)} pinned image(s) under local tags into kind cluster " 

550 f"{args.cluster}" 

551 ) 

552 if args.image: 

553 print(f"loaded {len(args.image)} local image(s) into kind cluster {args.cluster}") 

554 return 0 

555 

556 

557if __name__ == "__main__": 

558 sys.exit(main())