diff --git a/base/images/images.toml b/base/images/images.toml index b454c4237ec..ec1bd77c930 100644 --- a/base/images/images.toml +++ b/base/images/images.toml @@ -95,14 +95,35 @@ runtime-package-management = true [images.container-distroless-minimal] description = "Container Distroless Minimal Image" definition = { type = "kiwi", path = "container-base/container-base.kiwi", profile = "distroless-minimal" } +tests.test-suites = [{ name = "static-image-checks" }] + +[images.container-distroless-minimal.capabilities] +machine-bootable = false +container = true +systemd = false +runtime-package-management = false [images.container-distroless-base] description = "Container Distroless Base Image" definition = { type = "kiwi", path = "container-base/container-base.kiwi", profile = "distroless-base" } +tests.test-suites = [{ name = "static-image-checks" }] + +[images.container-distroless-base.capabilities] +machine-bootable = false +container = true +systemd = false +runtime-package-management = false [images.container-distroless-debug] description = "Container Distroless Debug Image" definition = { type = "kiwi", path = "container-base/container-base.kiwi", profile = "distroless-debug" } +tests.test-suites = [{ name = "static-image-checks" }] + +[images.container-distroless-debug.capabilities] +machine-bootable = false +container = true +systemd = false +runtime-package-management = false # ---- wsl --------------------------------------------------------------- diff --git a/base/images/tests/README.md b/base/images/tests/README.md index ea1202d6e68..3ef265035b6 100644 --- a/base/images/tests/README.md +++ b/base/images/tests/README.md @@ -93,6 +93,7 @@ base/images/ │ └── tools.py # Native-tool registry └── cases/ # Test cases ├── test_os_release.py # Shared: /etc/os-release + ├── test_oci_config.py # Shared (container): OCI Config.User unset ├── test_packages.py # Shared: rpm-db checks (capability-gated) ├── vm-base/ # VM-specific tests (auto-restricted to the vm-base family — vm-base, vm-base-dev) │ ├── test_kernel.py @@ -111,6 +112,7 @@ base/images/ | `capabilities` | session | `set[str]` | Parsed `--capabilities` | | `workdir` | session | `Path` | Working directory for mounts/extractions | | `rootfs` | session | `Path` | Mounted/extracted root filesystem | +| `oci_image_config` | session | `dict[str, object]` | Parsed `skopeo inspect --config` output (use with `@pytest.mark.require_capability("container")`) | | `os_release` | session | `dict[str, str]` | Parsed `/etc/os-release` | | `installed_packages` | session | `set[str]` | Installed RPM names (`rpm --root`) | | `disk_info` | session | `DiskInfo \| None` | VM only | diff --git a/base/images/tests/cases/test_oci_config.py b/base/images/tests/cases/test_oci_config.py new file mode 100644 index 00000000000..67a6a124af8 --- /dev/null +++ b/base/images/tests/cases/test_oci_config.py @@ -0,0 +1,36 @@ +# SPDX-License-Identifier: MIT +"""OCI image-config validation (container images). + +Shared test (not under a ``cases//`` directory) so it applies to +every container image family — both ``core`` (container-base) and the +``distroless-*`` variants. Gated on the ``container`` capability via +``@pytest.mark.require_capability`` so it only runs for container images +(VM images, which declare ``container = false``, are skipped). +""" + +from __future__ import annotations + +import pytest + + +@pytest.mark.require_capability("container") +def test_no_explicit_config_user(oci_image_config: dict[str, object]) -> None: + """OCI ``Config.User`` must be unset. + + Azure Linux base/distroless images intentionally leave ``Config.User`` + unset (matching AZL 3.0 and mainstream base images such as Debian, + Ubuntu, Alpine, UBI, Fedora). The OCI runtime default for an unset + user is uid 0, so this does not change effective runtime behavior, but + explicitly declaring a user diverges from that convention. An explicit + empty string still counts as "set" and must therefore also fail. + """ + config = oci_image_config.get("config") + assert isinstance(config, dict), ( + f"OCI image config has no 'config' object (got {type(config).__name__}); " + f"cannot validate Config.User. Full inspect output: {oci_image_config!r}" + ) + assert "User" not in config, ( + "OCI Config.User must be unset, but the image explicitly declares " + f"User={config['User']!r}. Remove the user attribute from the kiwi " + " so the published manifest leaves Config.User unset." + ) diff --git a/base/images/tests/conftest.py b/base/images/tests/conftest.py index 66f06ba29b3..9907f1d750e 100644 --- a/base/images/tests/conftest.py +++ b/base/images/tests/conftest.py @@ -18,6 +18,7 @@ from utils.disk import inspect_disk from utils.extract import ( + inspect_oci_config, mount_container_image, mount_vm_image, unmount_container_image, @@ -160,6 +161,19 @@ def rootfs(image_path: Path, image_type: str, workdir: Path) -> Path: unmount_container_image(container_dir) +@pytest.fixture(scope="session") +def oci_image_config(image_path: Path) -> dict[str, object]: + """Return the parsed OCI image config. + + Only meaningful for container images; tests using this fixture gate on + the ``container`` capability via + ``@pytest.mark.require_capability("container")``. + """ + config = inspect_oci_config(image_path) + logger.info("OCI image config.config keys: %s", sorted(config.get("config", {}))) + return config + + @pytest.fixture(scope="session") def disk_info(image_path: Path, image_type: str) -> DiskInfo | None: """Partition/filesystem info — ``None`` for container images.""" diff --git a/base/images/tests/utils/extract.py b/base/images/tests/utils/extract.py index 7a99fa953b4..7b616b99d54 100644 --- a/base/images/tests/utils/extract.py +++ b/base/images/tests/utils/extract.py @@ -7,10 +7,12 @@ from __future__ import annotations +import json import logging import os import subprocess from pathlib import Path +from typing import Any from .tools import NativeTool @@ -202,3 +204,24 @@ def unmount_container_image(extract_dir: Path) -> None: result.returncode, result.stderr.strip(), ) + + +def inspect_oci_config(image_path: Path) -> dict[str, Any]: + """Return the OCI image configuration for a container archive. + + Runs ``skopeo inspect --config`` against the OCI archive and parses + the resulting JSON (the OCI image config, which carries the + ``config`` object with ``User``, ``Cmd``, ``WorkingDir``, etc.). + Unlike rootfs extraction this needs only ``skopeo`` — no umoci unpack. + """ + image_path = image_path.resolve() + logger.info("Inspecting OCI image config: %s", image_path) + result = _run( + [ + SKOPEO.name, + "inspect", + "--config", + f"oci-archive:{image_path}", + ] + ) + return json.loads(result.stdout)