diff --git a/src/confcom/HISTORY.rst b/src/confcom/HISTORY.rst index 5e4a4f7f403..4b8d456e332 100644 --- a/src/confcom/HISTORY.rst +++ b/src/confcom/HISTORY.rst @@ -3,6 +3,13 @@ Release History =============== +1.7.2 +++++++ +* Fix ORAS fragment discovery for ORAS CLI >= 1.3.0 +* Fix StatefulSet/Deployment volume mount and securityContext resolution in `containers from_vn2` command to use pod template spec +* Fix empty command array in `containers from_vn2` command overwriting image entrypoint when no command/args specified in YAML +* Fix phantom exec_processes entries generated for non-exec probes (httpGet/tcpSocket) in `containers from_vn2` command + 1.7.1 ++++++ * Replace deprecated pkg_resources with packaging for Python 3.13 compatibility diff --git a/src/confcom/azext_confcom/command/containers_from_vn2.py b/src/confcom/azext_confcom/command/containers_from_vn2.py index b07786781b5..1d4998b8183 100644 --- a/src/confcom/azext_confcom/command/containers_from_vn2.py +++ b/src/confcom/azext_confcom/command/containers_from_vn2.py @@ -26,6 +26,18 @@ ) +def _get_pod_spec(template: dict) -> dict: + """Return the pod spec from a Kubernetes resource. + + For templated resources (Deployment, StatefulSet, DaemonSet, Job, etc.) + the pod spec lives at spec.template.spec. For bare Pods it is at spec. + """ + return ( + template.get("spec", {}).get("template", {}).get("spec", {}) + or template.get("spec", {}) + ) + + def find_vn2_containers(vn2_template): for key, value in vn2_template.items(): if key in ("containers", "initContainers"): @@ -111,9 +123,11 @@ def vn2_container_mounts(template: dict, container: dict) -> list[dict]: v["metadata"]["name"]: v.get("spec", {}).get("accessModes", []) for v in template.get("spec", {}).get("volumeClaimTemplates", []) } + # For Deployment/StatefulSet/etc., volumes are at spec.template.spec.volumes + pod_spec = _get_pod_spec(template) volume_defs = { v["name"]: [k for k in v.keys() if k != "name"][0] - for v in template.get("spec", {}).get("volumes", []) + for v in pod_spec.get("volumes", []) } return [ @@ -204,8 +218,11 @@ def containers_from_vn2( } # Parse security context + # For Deployment/StatefulSet/etc., pod-level securityContext is at + # spec.template.spec.securityContext, not spec.securityContext. + pod_spec = _get_pod_spec(template_doc) security_context = ( - template_doc.get("spec", {}).get("securityContext", {}) + pod_spec.get("securityContext", {}) | template_container.get("securityContext", {}) ) if security_context.get("privileged", False): @@ -252,7 +269,7 @@ def containers_from_vn2( template_container.get("lifecycle", {}).get("postStart"), template_container.get("lifecycle", {}).get("preStop"), ] - if process is not None + if process is not None and process.get("exec") is not None ] if exec_processes: template_container_def["exec_processes"] = exec_processes diff --git a/src/confcom/azext_confcom/lib/containers.py b/src/confcom/azext_confcom/lib/containers.py index f2ca46dab4f..4a0523ff3f0 100644 --- a/src/confcom/azext_confcom/lib/containers.py +++ b/src/confcom/azext_confcom/lib/containers.py @@ -24,6 +24,11 @@ def merge_containers(*args) -> dict: }: existing = merged_container.get(key) or [] merged_container[key] = list(existing) + list(value or []) + elif key in { + "command", + }: + if value or key not in merged_container: + merged_container[key] = value else: merged_container[key] = value diff --git a/src/confcom/azext_confcom/lib/images.py b/src/confcom/azext_confcom/lib/images.py index 34eb5f8ece2..810c12ae259 100644 --- a/src/confcom/azext_confcom/lib/images.py +++ b/src/confcom/azext_confcom/lib/images.py @@ -4,11 +4,14 @@ # -------------------------------------------------------------------------------------------- import functools +import logging import subprocess import docker from pathlib import Path +logger = logging.getLogger(__name__) + @functools.lru_cache() def get_image(image_ref: str) -> docker.models.images.Image: @@ -37,7 +40,17 @@ def get_image_layers(image: str) -> list[str]: text=True, ) - return [line.split("hash: ")[-1] for line in result.stdout.splitlines()] + layers = [] + for line in result.stdout.splitlines(): + if "hash: " in line: + layers.append(line.split("hash: ")[-1]) + else: + # dmverity-vhd may print warnings to stdout (e.g. OCI format + # parsing errors). Log them so they are visible but don't treat them + # as layer hashes. + logger.warning("Unexpected dmverity-vhd output: %s", line) + + return layers def get_image_config(image: str) -> dict: diff --git a/src/confcom/azext_confcom/oras_proxy.py b/src/confcom/azext_confcom/oras_proxy.py index 8dd452a78e0..1814ce9eaa5 100644 --- a/src/confcom/azext_confcom/oras_proxy.py +++ b/src/confcom/azext_confcom/oras_proxy.py @@ -75,7 +75,8 @@ def discover( logger.info("Discovering fragments for %s: %s", image, item.stdout.decode('utf-8')) if item.returncode == 0: json_output = json.loads(item.stdout.decode("utf-8")) - manifests = json_output.get("manifests", []) + # ORAS >= 1.3.0 renamed "manifests" to "referrers" + manifests = json_output.get("referrers") or json_output.get("manifests", []) if manifests is not None: for manifest in manifests: hashes.append(manifest["digest"]) diff --git a/src/confcom/setup.py b/src/confcom/setup.py index 2a7ebce00be..e0e76481204 100644 --- a/src/confcom/setup.py +++ b/src/confcom/setup.py @@ -19,7 +19,7 @@ logger.warn("Wheel is not available, disabling bdist_wheel hook") -VERSION = "1.7.1" +VERSION = "1.7.2" # The full list of classifiers is available at # https://pypi.python.org/pypi?%3Aaction=list_classifiers