|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
| 3 | +import json |
3 | 4 | import logging |
4 | 5 | import socket |
| 6 | +import sys |
5 | 7 | from dataclasses import dataclass, field |
6 | 8 | from functools import lru_cache |
7 | 9 | from math import ceil |
|
15 | 17 | import docker.models.volumes |
16 | 18 | import docker.types |
17 | 19 | from dataclasses_json import dataclass_json |
| 20 | +from docker.utils import parse_repository_tag |
18 | 21 |
|
19 | 22 | from .. import envs |
20 | 23 | from .__types__ import ( |
@@ -397,8 +400,43 @@ def _pull_image(self, image: str) -> docker.models.images.Image: |
397 | 400 | except docker.errors.ImageNotFound: |
398 | 401 | logger.info(f"Pulling image {image}") |
399 | 402 | try: |
400 | | - # TODO(thxCode): display pull progress |
401 | | - return self._client.images.pull(image) |
| 403 | + repo, tag = parse_repository_tag(image) |
| 404 | + tag = tag or "latest" |
| 405 | + pull_log = self._client.api.pull( |
| 406 | + repo, |
| 407 | + tag=tag, |
| 408 | + stream=True, |
| 409 | + ) |
| 410 | + |
| 411 | + layers: dict[str, int] = {} |
| 412 | + is_tty = sys.stdout.isatty() |
| 413 | + for line in pull_log: |
| 414 | + line_str = ( |
| 415 | + line.decode("utf-8", errors="replace") |
| 416 | + if isinstance(line, bytes) |
| 417 | + else line |
| 418 | + ) |
| 419 | + for log_str in line_str.splitlines(): |
| 420 | + log = json.loads(log_str) |
| 421 | + if "id" not in log: |
| 422 | + print(log["status"]) |
| 423 | + continue |
| 424 | + log_id = log["id"] |
| 425 | + if log_id not in layers: |
| 426 | + layers[log_id] = len(layers) |
| 427 | + if is_tty: |
| 428 | + print(f"\033[{layers[log_id] + 1};0H", end="") |
| 429 | + print("\033[K", end="") |
| 430 | + if "progress" in log: |
| 431 | + print(f"{log_id}: {log['progress']}", flush=True) |
| 432 | + else: |
| 433 | + print(f"{log_id}: {log['status']}", flush=True) |
| 434 | + |
| 435 | + sep = "@" if tag.startswith("sha256:") else ":" |
| 436 | + return self._client.images.get(f"{repo}{sep}{tag}") |
| 437 | + except json.decoder.JSONDecodeError as e: |
| 438 | + msg = f"Failed to pull image {image}, invalid response" |
| 439 | + raise OperationError(msg) from e |
402 | 440 | except docker.errors.APIError as e: |
403 | 441 | msg = f"Failed to pull image {image}" |
404 | 442 | raise OperationError(msg) from e |
|
0 commit comments