Skip to content

Commit fa1c0e2

Browse files
committed
[confcom] Derive image platform and add --platform validation
1 parent b6816c4 commit fa1c0e2

File tree

1 file changed

+84
-2
lines changed

1 file changed

+84
-2
lines changed

src/confcom/azext_confcom/security_policy.py

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import copy
77
import json
8+
import subprocess
89
import warnings
910
from enum import Enum, auto
1011
from typing import Any, Dict, List, Optional, Tuple, Union
@@ -41,8 +42,9 @@
4142
process_fragment_imports,
4243
process_mounts,
4344
process_mounts_from_config,
44-
readable_diff)
45-
from azext_confcom.lib.images import get_image_platform # pylint: disable=unused-import
45+
readable_diff,
46+
find_value_in_params_and_vars)
47+
from azext_confcom.lib.images import get_image_platform
4648
from azext_confcom.lib.defaults import get_debug_mode_exec_procs
4749
from knack.log import get_logger
4850
from tqdm import tqdm
@@ -669,6 +671,76 @@ def set_images(self, images: List[ContainerImage]) -> None:
669671
self._images = images
670672

671673

674+
def _get_image_platforms_from_docker(image_name: str) -> List[str]:
675+
"""Detect all platforms an image supports using docker manifest inspect.
676+
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.
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+
686+
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 []
705+
706+
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)
714+
715+
# Fall back to single-platform detection via docker pull
716+
if not supported:
717+
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
730+
731+
if len(supported) == 1 and supported[0] != platform:
732+
eprint(
733+
f'Image "{image_name}" only supports platform "{supported[0]}", '
734+
f'which does not match the specified platform "{platform}".'
735+
)
736+
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+
672744
# pylint: disable=R0914,
673745
def load_policy_from_arm_template_str(
674746
template_data: str,
@@ -830,6 +902,10 @@ def load_policy_from_arm_template_str(
830902
f'Field ["{config.ACI_FIELD_TEMPLATE_IMAGE}"] is empty or cannot be found'
831903
)
832904

905+
# Resolve ARM parameters/variables to get the real image name for validation
906+
resolved_image = find_value_in_params_and_vars(all_params, all_vars, image_name)
907+
validate_image_platform(resolved_image, platform)
908+
833909
exec_processes = []
834910
extract_probe(exec_processes, image_properties, config.ACI_FIELD_CONTAINERS_READINESS_PROBE)
835911
extract_probe(exec_processes, image_properties, config.ACI_FIELD_CONTAINERS_LIVENESS_PROBE)
@@ -920,6 +996,8 @@ def load_policy_from_image_name(
920996

921997
containers = []
922998
for image_name in image_names:
999+
validate_image_platform(image_name, platform)
1000+
9231001
container = {}
9241002
# assign just the fields that are expected
9251003
# the values will come when calling
@@ -1037,6 +1115,8 @@ def load_policy_from_json(
10371115
f'Field ["{config.ACI_FIELD_TEMPLATE_IMAGE}"] is empty or cannot be found'
10381116
)
10391117

1118+
validate_image_platform(image_name, platform)
1119+
10401120
container_name = case_insensitive_dict_get(
10411121
container, config.ACI_FIELD_CONTAINERS_NAME
10421122
) or image_name
@@ -1250,6 +1330,8 @@ def load_policy_from_virtual_node_yaml_str(
12501330
if not image:
12511331
eprint("Container does not have an image field")
12521332

1333+
validate_image_platform(image, platform)
1334+
12531335
# env vars
12541336
envs = process_env_vars_from_yaml(
12551337
container,

0 commit comments

Comments
 (0)