|
5 | 5 |
|
6 | 6 | import copy |
7 | 7 | import json |
8 | | -import subprocess |
9 | 8 | import warnings |
10 | 9 | from enum import Enum, auto |
11 | 10 | from typing import Any, Dict, List, Optional, Tuple, Union |
|
44 | 43 | process_mounts_from_config, |
45 | 44 | readable_diff, |
46 | 45 | find_value_in_params_and_vars) |
47 | | -from azext_confcom.lib.images import get_image_platform |
| 46 | +from azext_confcom.lib.images import get_image_platform # pylint: disable=unused-import |
48 | 47 | from azext_confcom.lib.defaults import get_debug_mode_exec_procs |
49 | 48 | from knack.log import get_logger |
50 | 49 | from tqdm import tqdm |
@@ -671,75 +670,59 @@ def set_images(self, images: List[ContainerImage]) -> None: |
671 | 670 | self._images = images |
672 | 671 |
|
673 | 672 |
|
674 | | -def _get_image_platforms_from_docker(image_name: str) -> List[str]: |
675 | | - """Detect all platforms an image supports using docker manifest inspect. |
| 673 | +def validate_image_platform(image_name: str, platform: str) -> None: |
| 674 | + """Validate that the image's platform matches --platform. |
676 | 675 |
|
677 | | - If the image reference is a digest (@sha256:...), strip it back to the |
678 | | - tag so that multi-platform manifest list lookups work correctly. |
| 676 | + Checks the local Docker image first, then attempts to pull with the |
| 677 | + specified platform if not found locally. Verifies the image's |
| 678 | + Os/Architecture attrs match the requested platform. |
679 | 679 | """ |
680 | | - # Strip digest references — manifest inspect needs a tag, not a digest |
681 | | - if "@sha256:" in image_name: |
682 | | - image_name = image_name.split("@sha256:")[0] |
683 | | - if ":" not in image_name.split("/")[-1]: |
684 | | - image_name += ":latest" |
685 | | - |
| 680 | + import docker as docker_module |
686 | 681 | try: |
687 | | - result = subprocess.run( |
688 | | - ["docker", "manifest", "inspect", image_name], |
689 | | - capture_output=True, text=True, timeout=30, check=True, |
690 | | - ) |
691 | | - data = json.loads(result.stdout) |
692 | | - platforms = [] |
693 | | - for manifest in data.get("manifests", []): |
694 | | - plat = manifest.get("platform", {}) |
695 | | - os_name = plat.get("os", "unknown") |
696 | | - arch = plat.get("architecture", "unknown") |
697 | | - if os_name != "unknown" and arch != "unknown": |
698 | | - platforms.append(f"{os_name}/{arch}") |
699 | | - if platforms: |
700 | | - return platforms |
701 | | - except (subprocess.CalledProcessError, subprocess.TimeoutExpired, |
702 | | - json.JSONDecodeError, FileNotFoundError): |
703 | | - pass |
704 | | - return [] |
| 682 | + client = docker_module.from_env() |
| 683 | + except docker_module.errors.DockerException: |
| 684 | + eprint("Docker is not running. Please start Docker.") |
| 685 | + return |
705 | 686 |
|
| 687 | + image = None |
706 | 688 |
|
707 | | -def validate_image_platform(image_name: str, platform: str) -> None: |
708 | | - """Validate that the image supports the specified platform. |
709 | | -
|
710 | | - Uses docker manifest inspect for multi-platform detection, |
711 | | - then falls back to get_image_platform (docker pull) for single-platform. |
712 | | - """ |
713 | | - supported = _get_image_platforms_from_docker(image_name) |
| 689 | + # Try local image first |
| 690 | + try: |
| 691 | + image = client.images.get(image_name) |
| 692 | + except (docker_module.errors.ImageNotFound, docker_module.errors.NullResource): |
| 693 | + pass |
714 | 694 |
|
715 | | - # Fall back to single-platform detection via docker pull |
716 | | - if not supported: |
| 695 | + # If not local, try pulling with the specified platform |
| 696 | + if image is None: |
717 | 697 | try: |
718 | | - detected = get_image_platform(image_name) |
719 | | - if detected: |
720 | | - supported = [detected] |
721 | | - except (ValueError, KeyError, AttributeError): |
722 | | - pass |
723 | | - |
724 | | - if not supported: |
725 | | - logger.warning( |
726 | | - "Could not detect supported platforms for image '%s'. " |
727 | | - "Skipping platform validation.", image_name, |
728 | | - ) |
729 | | - return |
| 698 | + image = client.images.pull(image_name, platform=platform) |
| 699 | + except (docker_module.errors.ImageNotFound, docker_module.errors.NotFound): |
| 700 | + eprint( |
| 701 | + f'Image "{image_name}" is not found. ' |
| 702 | + f'Please check the image name and repository.' |
| 703 | + ) |
| 704 | + except docker_module.errors.APIError as e: |
| 705 | + error_msg = str(e).lower() |
| 706 | + if "not supported" in error_msg or "no matching manifest" in error_msg: |
| 707 | + eprint( |
| 708 | + f'Image "{image_name}" could not be pulled for platform "{platform}". ' |
| 709 | + f'Docker Desktop must be in the correct container mode ' |
| 710 | + f'(Linux containers for linux/amd64, ' |
| 711 | + f'Windows containers for windows/amd64).' |
| 712 | + ) |
| 713 | + else: |
| 714 | + eprint( |
| 715 | + f'Image "{image_name}" could not be pulled for platform ' |
| 716 | + f'"{platform}": {e}' |
| 717 | + ) |
730 | 718 |
|
731 | | - if len(supported) == 1 and supported[0] != platform: |
| 719 | + detected = f"{image.attrs.get('Os')}/{image.attrs.get('Architecture')}" |
| 720 | + if detected != platform: |
732 | 721 | eprint( |
733 | | - f'Image "{image_name}" only supports platform "{supported[0]}", ' |
| 722 | + f'Image "{image_name}" has platform "{detected}", ' |
734 | 723 | f'which does not match the specified platform "{platform}".' |
735 | 724 | ) |
736 | 725 |
|
737 | | - if len(supported) > 1 and platform not in supported: |
738 | | - eprint( |
739 | | - f'Image "{image_name}" supports platforms {supported}, ' |
740 | | - f'which does not include the specified platform "{platform}".' |
741 | | - ) |
742 | | - |
743 | 726 |
|
744 | 727 | # pylint: disable=R0914, |
745 | 728 | def load_policy_from_arm_template_str( |
|
0 commit comments