Skip to content

Commit 1492ac5

Browse files
authored
fix: pin privileged Python host-check image by digest (#22)
1 parent 9013a9c commit 1492ac5

3 files changed

Lines changed: 51 additions & 6 deletions

File tree

‎cmax/scripts/1-audit/checks/platform_config.py‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
Environment variables:
3737
3838
* ``CLUSTERMAX_AUDIT_K8S_NAMESPACE`` (default ``default``)
39-
* ``CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE`` (default ``python:3.12-alpine``)
39+
* ``CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE`` (default: digest-pinned
40+
``python:3.12-alpine``; use a digest-pinned mirror for air-gapped clusters)
4041
* ``CLUSTERMAX_AUDIT_K8S_HOST_CHECK_PULL_POLICY`` (default ``IfNotPresent``)
4142
* ``CLUSTERMAX_AUDIT_K8S_MAX_HOST_CHECKS`` (default: all GPU nodes)
4243
* ``CLUSTERMAX_PYXIS_CHECK_IMAGE`` - container the mount check launches.
@@ -68,6 +69,11 @@
6869
from typing import Any
6970

7071
SCHEMA_VERSION = 1
72+
# Pin the multi-architecture index so both x86 and Arm hosts use fixed images.
73+
DEFAULT_K8S_HOST_CHECK_IMAGE = (
74+
"python:3.12-alpine@"
75+
"sha256:c4634f578a412db396771b61b064c6e546c9d6414c7fb5b1b05d5871f1885f7b"
76+
)
7177
CHECK_KEYS = ("vm_iommu", "arm_smmu_virtualization")
7278
ALL_CHECK_KEYS = (*CHECK_KEYS, "nccl_topo_file", "nccl_ib_qps")
7379
CACHE_PATH_ENV = "CLUSTERMAX_PLATFORM_CHECK_CACHE"
@@ -2020,7 +2026,7 @@ def run_k8s_check(
20202026
include_rdma_iommu: bool = True,
20212027
) -> tuple[list[dict[str, Any]], list[str]]:
20222028
namespace = os.environ.get("CLUSTERMAX_AUDIT_K8S_NAMESPACE", "default")
2023-
image = os.environ.get("CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE", "python:3.12-alpine")
2029+
image = os.environ.get("CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE", DEFAULT_K8S_HOST_CHECK_IMAGE)
20242030
max_nodes = as_int(os.environ.get("CLUSTERMAX_AUDIT_K8S_MAX_HOST_CHECKS"), default=len(nodes))
20252031
return load_fanout().fan_out_k8s(
20262032
lambda node: run_k8s_host_check(

‎cmax/scripts/1-audit/checks/system/hbm_memory_exposure.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
Kubernetes harness environment variables:
1111
1212
* ``CLUSTERMAX_AUDIT_K8S_NAMESPACE`` (default ``default``)
13-
* ``CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE`` (default
14-
``python:3.12-alpine``) - must be reachable from GPU nodes; override
15-
for air-gapped clusters.
13+
* ``CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE`` (default: digest-pinned
14+
``python:3.12-alpine``) - must be reachable from GPU nodes; use a
15+
digest-pinned mirror for air-gapped clusters.
1616
* ``CLUSTERMAX_AUDIT_K8S_HOST_CHECK_PULL_POLICY`` (default
1717
``IfNotPresent``)
1818
* ``CLUSTERMAX_AUDIT_K8S_MAX_HOST_CHECKS`` (default: all GPU nodes)
@@ -33,6 +33,11 @@
3333

3434

3535
GIB_KB = 1024 * 1024
36+
# Pin the multi-architecture index so both x86 and Arm hosts use fixed images.
37+
DEFAULT_K8S_HOST_CHECK_IMAGE = (
38+
"python:3.12-alpine@"
39+
"sha256:c4634f578a412db396771b61b064c6e546c9d6414c7fb5b1b05d5871f1885f7b"
40+
)
3641
# Lower bound for what counts as an HBM-sized memory-only NUMA node. 64 GiB
3742
# catches every coherent NVIDIA platform shipping today (GH200 96 GiB, GB200
3843
# 192 GiB per GPU, GB300 288 GiB per GPU, H100 SXM 80 GiB if ever exposed
@@ -735,7 +740,7 @@ def run_k8s_check() -> tuple[list[dict[str, Any]], list[str]]:
735740
return [], [NO_K8S_GPU_NODES]
736741

737742
namespace = os.environ.get("CLUSTERMAX_AUDIT_K8S_NAMESPACE", "default")
738-
image = os.environ.get("CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE", "python:3.12-alpine")
743+
image = os.environ.get("CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE", DEFAULT_K8S_HOST_CHECK_IMAGE)
739744
max_nodes = as_int(os.environ.get("CLUSTERMAX_AUDIT_K8S_MAX_HOST_CHECKS"), default=len(nodes))
740745
return load_fanout().fan_out_k8s(_hbm_per_node(namespace, image), nodes=nodes, max_nodes=max_nodes)
741746

‎tests/audit/test_k8s_utility_image_pins.py‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33

44
from __future__ import annotations
55

6+
import importlib.util
7+
import subprocess
68
import sys
79
import tempfile
810
from pathlib import Path
11+
from unittest import mock
912

13+
import pytest
1014
import yaml
1115

1216
sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -28,6 +32,36 @@
2832
"ubuntu:24.04@"
2933
"sha256:561618e2c15bf2397621dd04f96926663a3b5616c189cf7e38db7e82f5c538ea"
3034
)
35+
PYTHON_IMAGE = (
36+
"python:3.12-alpine@"
37+
"sha256:c4634f578a412db396771b61b064c6e546c9d6414c7fb5b1b05d5871f1885f7b"
38+
)
39+
40+
41+
@pytest.mark.parametrize("check_path", ["platform_config.py", "system/hbm_memory_exposure.py"])
42+
@pytest.mark.parametrize("override", [None, "registry.example/python@" + PYTHON_IMAGE.split("@")[1]])
43+
def test_python_host_checks_use_pinned_default_and_preserve_override(check_path, override):
44+
spec = importlib.util.spec_from_file_location("host_check", SCRIPT.parent / "checks" / check_path)
45+
module = importlib.util.module_from_spec(spec)
46+
spec.loader.exec_module(module)
47+
node = {"name": "gpu-0"}
48+
fanout = mock.Mock()
49+
fanout.k8s_gpu_nodes.return_value = [node]
50+
fanout.fan_out_k8s.side_effect = lambda worker, **kwargs: worker(node)
51+
env = {} if override is None else {"CLUSTERMAX_AUDIT_K8S_HOST_CHECK_IMAGE": override}
52+
with (
53+
mock.patch.dict(module.os.environ, env, clear=True),
54+
mock.patch.object(module, "load_fanout", return_value=fanout),
55+
mock.patch.object(module, "kubectl", return_value=subprocess.CompletedProcess([], 0, "{}")),
56+
mock.patch.object(module, "run_k8s_host_check") as run_host,
57+
):
58+
if check_path == "platform_config.py":
59+
module.run_k8s_check([node])
60+
else:
61+
module.run_k8s_check()
62+
namespace, selected_node, image = run_host.call_args.args
63+
manifest = module.pod_manifest(namespace, selected_node, image, "test-pod")
64+
assert manifest["spec"]["containers"][0]["image"] == (override or PYTHON_IMAGE)
3165

3266

3367
def _applied_manifest(function_name: str, setup: str) -> dict:

0 commit comments

Comments
 (0)