Skip to content

Commit

Permalink
fix: oci engine version check
Browse files Browse the repository at this point in the history
Lower Docker API check to 1.41
Podman versions are not PEP440 compliant, remove distro specific suffixes before parsing.
Add tests with real-world outputs and some made up ones.
  • Loading branch information
mayeut committed Sep 21, 2024
1 parent 4ccfcaf commit 3c8daff
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 14 deletions.
31 changes: 18 additions & 13 deletions cibuildwheel/oci_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import json
import os
import platform
import re
import shlex
import shutil
import subprocess
Expand Down Expand Up @@ -101,22 +100,28 @@ def options_summary(self) -> str | dict[str, str]:

def _check_engine_version(engine: OCIContainerEngineConfig) -> None:
try:
version_string = call(engine.name, "version", "-f", "{{json .}}", capture_stdout=True)
version_info = json.loads(version_string.strip())
if engine.name == "docker":
# --platform support was introduced in 1.32 as experimental
# docker cp, as used by cibuildwheel, has been fixed in v24 => API 1.43
# https://github.com/moby/moby/issues/38995
version_string = call(engine.name, "version", "-f", "{{json .}}", capture_stdout=True)
version_info = json.loads(version_string.strip())
client_api_version = Version(version_info["Client"]["ApiVersion"])
engine_api_version = Version(version_info["Server"]["ApiVersion"])
version_supported = min(client_api_version, engine_api_version) >= Version("1.43")
server_api_version = Version(version_info["Server"]["ApiVersion"])
# --platform support was introduced in 1.32 as experimental, 1.41 removed the experimental flag
version_supported = min(client_api_version, server_api_version) >= Version("1.41")
elif engine.name == "podman":
# Parse the version from the `podman --version` output
pattern = r"(?P<engine_name>\w+)\s+version\s+(?P<version>[0-9.]+)"
match = re.search(pattern, call(engine.name, "--version", capture_stdout=True))
engine_version = Version(match.group("version")) if match else Version("0")
# podman uses the same version string for "Version" & "ApiVersion"
# the version string is not PEP440 compliant here
def _version(version_string: str) -> Version:
for sep in ("-", "~", "^", "+"):
version_string = version_string.split(sep, maxsplit=1)[0]
return Version(version_string)

client_version = _version(version_info["Client"]["Version"])
if "Server" in version_info:
server_version = _version(version_info["Server"]["Version"])
else:
server_version = client_version
# --platform support was introduced in v3
version_supported = engine_version >= Version("3")
version_supported = min(client_version, server_version) >= Version("3")
else:
assert_never(engine.name)
if not version_supported:
Expand Down
71 changes: 70 additions & 1 deletion unit_test/oci_container_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@
import subprocess
import sys
import textwrap
from contextlib import nullcontext
from pathlib import Path, PurePath, PurePosixPath

import pytest
import tomli_w

import cibuildwheel.oci_container
from cibuildwheel.environment import EnvironmentAssignmentBash
from cibuildwheel.oci_container import OCIContainer, OCIContainerEngineConfig, OCIPlatform
from cibuildwheel.errors import OCIEngineTooOldError
from cibuildwheel.oci_container import (
OCIContainer,
OCIContainerEngineConfig,
OCIPlatform,
_check_engine_version,
)
from cibuildwheel.util import CIProvider, detect_ci_provider

# Test utilities
Expand Down Expand Up @@ -569,3 +577,64 @@ def test_multiarch_image(container_engine, platform):
OCIPlatform.S390X: "s390x",
}
assert output_map[platform] == output.strip()


@pytest.mark.parametrize(
("engine_name", "version", "context"),
[
(
"docker",
None, # 17.12.1-ce does supports "docker version --format '{{json . }}'" so a version before that
pytest.raises(OCIEngineTooOldError),
),
(
"docker",
'{"Client":{"Version":"19.03.15","ApiVersion": "1.40"},"Server":{"ApiVersion": "1.40"}}',
pytest.raises(OCIEngineTooOldError),
),
(
"docker",
'{"Client":{"Version":"20.10.0","ApiVersion":"1.41"},"Server":{"ApiVersion":"1.41"}}',
nullcontext(),
),
(
"docker",
'{"Client":{"Version":"24.0.0","ApiVersion":"1.43"},"Server":{"ApiVersion":"1.43"}}',
nullcontext(),
),
(
"docker",
'{"Client":{"ApiVersion":"1.43"},"Server":{"ApiVersion":"1.30"}}',
pytest.raises(OCIEngineTooOldError),
),
(
"docker",
'{"Client":{"ApiVersion":"1.30"},"Server":{"ApiVersion":"1.43"}}',
pytest.raises(OCIEngineTooOldError),
),
("podman", '{"Client":{"Version":"5.2.0"},"Server":{"Version":"5.1.2"}}', nullcontext()),
("podman", '{"Client":{"Version":"4.9.4-rhel"}}', nullcontext()),
(
"podman",
'{"Client":{"Version":"5.2.0"},"Server":{"Version":"2.1.2"}}',
pytest.raises(OCIEngineTooOldError),
),
(
"podman",
'{"Client":{"Version":"2.2.0"},"Server":{"Version":"5.1.2"}}',
pytest.raises(OCIEngineTooOldError),
),
("podman", '{"Client":{"Version":"3.0~rc1-rhel"}}', nullcontext()),
("podman", '{"Client":{"Version":"2.1.0~rc1"}}', pytest.raises(OCIEngineTooOldError)),
],
)
def test_engine_version(engine_name, version, context, monkeypatch):
def mockcall(*args, **kwargs):
if version is None:
raise subprocess.CalledProcessError(1, " ".join(str(arg) for arg in args))
return version

monkeypatch.setattr(cibuildwheel.oci_container, "call", mockcall)
engine = OCIContainerEngineConfig.from_config_string(engine_name)
with context:
_check_engine_version(engine)

0 comments on commit 3c8daff

Please sign in to comment.