Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: more reliably validate Podman API version #2016

Merged
merged 11 commits into from
Oct 1, 2024

Conversation

zachlewis
Copy link
Contributor

@zachlewis zachlewis commented Sep 18, 2024

This PR addresses two separate issues sometimes encountered with Podman:

  1. Sometimes, the podman cli utility does not support the version subcommand, whose output is currently used for ascertaining the "APIVersion".
  2. Podman's reported "Client.APIVersion" sometimes includes characters that the Version class cannot parse (e.g., "4.9.4-rhel")

For clients whose podman version -f "{{json .}}" command does function as expected, the reported "Version" and "APIVersion" values seem to match; so, this PR implements an alternate method of determining Podman's version that seems to behave more reliably across distributions + architectures: we use regex to match a valid version pattern from the single-line output of the podman --version command.

fix #2020

*edit: rewritten for clarity

zachlewis and others added 2 commits September 18, 2024 17:50
It's possible for some container engines to report their versions with a dash (e.g., "4.9.4-rhel"), which breaks packaging.version.Version's ability to parse the string. This commit introduces a version_from_string method which santizies the version string and returns an instance of Version.
@Czaki
Copy link
Contributor

Czaki commented Sep 18, 2024

Aa far as I know, you cannot upload such a wheel on PyPI?

@zachlewis
Copy link
Contributor Author

zachlewis commented Sep 18, 2024

Aa far as I know, you cannot upload such a wheel on PyPI?

Indeed, but this isn't a problem with parsing python package versions; it's that packaging.version.Version is used for validating the container engine version -- i.e., docker or podman, installed via homebrew or some other package management system.

For example, on my machine:

$ podman version -f "{{json .}}"
{"Client":{"APIVersion":"4.9.4-rhel","Version":"4.9.4-rhel","GoVersion":"go1.21.11 (Red Hat 1.21.11-1.module+el8.10.0+1831+fc70fba6)","GitCommit":"","BuiltTime":"Tue Aug 13 05:04:24 2024","Built":1723539864,"OsArch":"linux/arm64","Os":"linux"}}

That Podman's reported version is "4.9.4-rhel" ends up causing the cibuildwheel script to raise a packaging.version.InvalidVersion exception:

$ export CIBW_CONTAINER_ENGINE=podman
$ cibuildwheel 
...
Here we go!

Starting container image quay.io/pypa/manylinux_2_28_aarch64:2024.09.09-0...

info: This container will host the build for cp312-manylinux_aarch64...
+ podman version -f '{{json .}}'
Traceback (most recent call last):
  File "/home/zozobra/.local/share/pipx/venvs/cibuildwheel/lib64/python3.11/site-packages/cibuildwheel/oci_container.py", line 112, in _check_engine_version
    client_api_version = Version(version_info["Client"]["APIVersion"])
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/zozobra/.local/share/pipx/venvs/cibuildwheel/lib64/python3.11/site-packages/packaging/version.py", line 202, in __init__
    raise InvalidVersion(f"Invalid version: '{version}'")
packaging.version.InvalidVersion: Invalid version: '4.9.4-rhel'

(which in turn raises cibuildwheel.errors.OCIEngineTooOldError)

zachlewis and others added 3 commits September 18, 2024 22:17
Also, lift the method up and prefix with a "_" to better match the existing conventions
@zachlewis zachlewis changed the title fix: parse version strings that include dashes fix: parse container engine version strings that include dashes Sep 18, 2024
@zachlewis
Copy link
Contributor Author

I should note here that the new minimum container engine version stuff that was recently committed breaks cibuildwheel on systems whose "docker" or "podman" CLI utilities do not support the docker version -f "{{json .}}" command. For example, the command works fine for me in aarch64 Rocky and Alma containers; but not in x86_64 containers of the same distributions + versions emulated on the same machine, with Podman (4.9.4-rhel) installed the same way (e.g. via sudo dnf docker)

I've only experienced this issue with Podman -- I have no idea if this is a problem with Docker. Likewise, I dunno if Docker ever uses dashes for their server or client Version or API Version; whereas Podman uses the same value for Version and API Version.

That said, I propose using a slightly different mechanism for validating Docker and Podman versions:

  • For Docker, continue doing exactly the same thing as before
  • For Podman, use a simple regex match to parse the version from the single string line output of "podman --version"

zachlewis and others added 2 commits September 19, 2024 11:59
Use the "podman --version" command instead of "podman version -f {{json .}}" for better reliability across distributions.
@henryiii
Copy link
Contributor

henryiii commented Sep 19, 2024

docker cp, as used by cibuildwheel, has been fixed in v24 => API 1.43

This comment is not true anymore. We don't use docker cp again. Do we still need the min version check, @mayeut?

@zachlewis
Copy link
Contributor Author

docker cp, as used by cibuildwheel, has been fixed in v24 => API 1.43

This comment is not true anymore. We don't use docker cp again. Do we still need the min version check, @mayeut?

It seems like the cp command is used here:

call(self.engine.name, "cp", f"{self.name}:{from_path}/.", to_path)

The minimum version check was just added in #1961, merged last week -- that's what prompted this PR, cuz fresh installs of cibuildwheel suddenly stopped working for me.

I'm renaming this PR title to better reflect what it addresses.

@zachlewis zachlewis changed the title fix: parse container engine version strings that include dashes fix: more reliably validate Podman API version Sep 20, 2024
@henryiii
Copy link
Contributor

I don't see a docker cp in https://github.com/pypa/cibuildwheel/blob/main/cibuildwheel/oci_container.py. The line you are referencing in 2.21.0 was removed in #2007 in 2.21.1. So do we need the min version check?

@mayeut
Copy link
Member

mayeut commented Sep 21, 2024

Do we still need the min version check ?

We still need the version check for --platform support though it could be lowered to 1.41 now that docker cp isn't used anymore.

@mayeut
Copy link
Member

mayeut commented Sep 21, 2024

For example, the command works fine for me in aarch64 Rocky and Alma containers; but not in x86_64 containers of the same distributions + versions emulated on the same machine, with Podman (4.9.4-rhel) installed the same way (e.g. via sudo dnf docker)

The docker version -f "{{json .}}" seems well supported. The "unknown flag" error seems to be a symptom of a non working podman environment altogether (I got this error with podman run --rm ... on the --rm flag).
Running the container as privileged (docker in my case as podman runs fine within podman) seems to do the trick to get a somewhat working environment and in this case, there's no issue getting the version:

sandbox % podman version --format '{{json .}}'                                                                                                           
{"Client":{"APIVersion":"5.2.0","Version":"5.2.0","GoVersion":"go1.22.5","GitCommit":"b22d5c61eef93475413724f49fd6a32980d2c746","BuiltTime":"Fri Aug  2 14:05:53 2024","Built":1722600353,"OsArch":"darwin/arm64","Os":"darwin"},"Server":{"APIVersion":"5.1.2","Version":"5.1.2","GoVersion":"go1.22.5","GitCommit":"","BuiltTime":"Wed Jul 10 02:00:00 2024","Built":1720569600,"OsArch":"linux/arm64","Os":"linux"}}

sandbox % docker version --format '{{json .}}'
{"Client":{"Version":"27.2.0","ApiVersion":"1.47","DefaultAPIVersion":"1.47","GitCommit":"3ab4256","GoVersion":"go1.21.13","Os":"darwin","Arch":"arm64","BuildTime":"Tue Aug 27 14:14:45 2024","Context":"desktop-linux"},"Server":{"Platform":{"Name":"Docker Desktop 4.34.2 (167172)"},"Components":[{"Name":"Engine","Version":"27.2.0","Details":{"ApiVersion":"1.47","Arch":"arm64","BuildTime":"Tue Aug 27 14:15:41 2024","Experimental":"false","GitCommit":"3ab5c7d","GoVersion":"go1.21.13","KernelVersion":"6.10.4-linuxkit","MinAPIVersion":"1.24","Os":"linux"}},{"Name":"containerd","Version":"1.7.20","Details":{"GitCommit":"8fc6bcff51318944179630522a095cc9dbf9f353"}},{"Name":"runc","Version":"1.1.13","Details":{"GitCommit":"v1.1.13-0-g58aa920"}},{"Name":"docker-init","Version":"0.19.0","Details":{"GitCommit":"de40ad0"}}],"Version":"27.2.0","ApiVersion":"1.47","MinAPIVersion":"1.24","GitCommit":"3ab5c7d","GoVersion":"go1.21.13","Os":"linux","Arch":"arm64","KernelVersion":"6.10.4-linuxkit","BuildTime":"2024-08-27T14:15:41.000000000+00:00"}}

sandbox %  podman run --privileged --rm --platform linux/amd64 almalinux:8 /bin/bash -xec 'dnf install -qy podman && podman version --format "{{json .}}"'
+ dnf install -qy podman
Installed:
  conmon-3:2.1.10-1.module_el8.10.0+3876+e55593a8.x86_64                        
  containernetworking-plugins-1:1.4.0-5.module_el8.10.0+3876+e55593a8.x86_64    
  containers-common-2:1-82.module_el8.10.0+3876+e55593a8.x86_64                 
  criu-3.18-5.module_el8.10.0+3876+e55593a8.x86_64                              
  dnsmasq-2.79-33.el8_10.x86_64                                                 
  fuse-common-3.3.0-19.el8.x86_64                                               
  fuse-overlayfs-1.13-1.module_el8.10.0+3876+e55593a8.x86_64                    
  fuse3-3.3.0-19.el8.x86_64                                                     
  fuse3-libs-3.3.0-19.el8.x86_64                                                
  iptables-1.8.5-11.el8_9.x86_64                                                
  iptables-libs-1.8.5-11.el8_9.x86_64                                           
  jansson-2.14-1.el8.x86_64                                                     
  kmod-25-20.el8.x86_64                                                         
  libibverbs-48.0-1.el8.x86_64                                                  
  libmnl-1.0.4-6.el8.x86_64                                                     
  libnet-1.1.6-15.el8.x86_64                                                    
  libnetfilter_conntrack-1.0.6-5.el8.x86_64                                     
  libnfnetlink-1.0.1-13.el8.x86_64                                              
  libnftnl-1.2.2-3.el8.x86_64                                                   
  libnl3-3.7.0-1.el8.x86_64                                                     
  libpcap-14:1.9.1-5.el8.x86_64                                                 
  libslirp-4.4.0-2.module_el8.10.0+3876+e55593a8.x86_64                         
  nftables-1:1.0.4-4.el8_9.x86_64                                               
  podman-4:4.9.4-12.module_el8.10.0+3876+e55593a8.x86_64                        
  podman-catatonit-4:4.9.4-12.module_el8.10.0+3876+e55593a8.x86_64              
  podman-gvproxy-4:4.9.4-12.module_el8.10.0+3876+e55593a8.x86_64                
  podman-plugins-4:4.9.4-12.module_el8.10.0+3876+e55593a8.x86_64                
  protobuf-c-1.3.0-8.el8.x86_64                                                 
  runc-1:1.1.12-4.module_el8.10.0+3876+e55593a8.x86_64                          
  shadow-utils-subid-2:4.6-22.el8.x86_64                                        
  slirp4netns-1.2.3-1.module_el8.10.0+3876+e55593a8.x86_64                      
+ podman version --format '{{json .}}'
{"Client":{"APIVersion":"4.9.4-rhel","Version":"4.9.4-rhel","GoVersion":"go1.21.11 (Red Hat 1.21.11-1.module_el8.10.0+3863+bb82df69)","GitCommit":"","BuiltTime":"Tue Aug 13 16:37:27 2024","Built":1723567047,"OsArch":"linux/amd64","Os":"linux"}}

sandbox %  podman run --rm --platform linux/amd64 almalinux:8 /bin/bash -xec 'dnf install -qy podman && podman version --format "{{json .}}"' 
+ dnf install -qy podman
...
+ podman version --format '{{json .}}'
{"Client":{"APIVersion":"4.9.4-rhel","Version":"4.9.4-rhel","GoVersion":"go1.21.11 (Red Hat 1.21.11-1.module_el8.10.0+3863+bb82df69)","GitCommit":"","BuiltTime":"Tue Aug 13 16:37:27 2024","Built":1723567047,"OsArch":"linux/amd64","Os":"linux"}}

sandbox % docker run --rm --platform linux/amd64 almalinux:8 /bin/bash -xec 'dnf install -qy podman && podman version --format "{{json .}}"'
+ dnf install -qy podman
...
+ podman version --format '{{json .}}'
time="2024-09-21T10:01:53Z" level=warning msg="\"/\" is not a shared mount, this could cause issues or missing mounts with rootless containers"
Error: unknown flag: --format
See 'podman --help'

sandbox % docker run --rm --privileged --platform linux/amd64 almalinux:8 /bin/bash -xec 'dnf install -qy podman && podman version --format "{{json .}}"'
+ dnf install -qy podman
...
+ podman version --format '{{json .}}'
{"Client":{"APIVersion":"4.9.4-rhel","Version":"4.9.4-rhel","GoVersion":"go1.21.11 (Red Hat 1.21.11-1.module_el8.10.0+3863+bb82df69)","GitCommit":"","BuiltTime":"Tue Aug 13 16:37:27 2024","Built":1723567047,"OsArch":"linux/amd64","Os":"linux"}}

We can see that I'm running different versions of podman for the client and server side (most likely a thing on macOS/Windows ?)

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.
),
(
"docker",
'{"Client":{"Version":"19.03.15","ApiVersion": "1.40"},"Server":{"ApiVersion": "1.40"}}',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've kept the "Version" info here to get a real sense of what docker version the ApiVersion relates to. It's unused & just informative.

@mayeut
Copy link
Member

mayeut commented Sep 21, 2024

Do we still need the min version check ?

On second thoughts, now that docker cp has been removed, maybe we could rely on just the error message from docker/podman command ?

With check:

pybase64 % PATH=~/Downloads/docker:$PATH cibuildwheel --only cp313t-manylinux_aarch64
...
Starting container image quay.io/pypa/manylinux_2_28_aarch64:2024.09.16-1...

info: This container will host the build for cp313t-manylinux_aarch64...
+ docker version -f '{{json .}}'

                                                              ✕ 0.06s
Error: 
Build failed because docker is too old.

cibuildwheel requires docker>=20.10.0 running API version 1.41.
The API version found by cibuildwheel is 1.40.

Without check:

pybase64 % PATH=~/Downloads/docker:$PATH cibuildwheel --only cp313t-manylinux_aarch64
...
Starting container image quay.io/pypa/manylinux_2_28_aarch64:2024.09.16-1...

info: This container will host the build for cp313t-manylinux_aarch64...
+ docker image inspect quay.io/pypa/manylinux_2_28_aarch64:2024.09.16-1 --format '{{.Os}}/{{.Architecture}}'
Error: No such image: quay.io/pypa/manylinux_2_28_aarch64:2024.09.16-1
"--platform" is only supported on a Docker daemon with experimental features enabled

                                                              ✕ 6.12s
Error: Command ['docker', 'create', '--env=CIBUILDWHEEL', '--env=SOURCE_DATE_EPOCH', '--name=cibuildwheel-1ba0ac9b-7c12-4a08-a8f1-090a73abba17', '--interactive', '--volume=/:/host', '--platform=linux/arm64', '--pull=always', 'quay.io/pypa/manylinux_2_28_aarch64:2024.09.16-1', '/bin/bash'] failed with code 1. 

@zachlewis
Copy link
Contributor Author

The docker version -f "{{json .}}" seems well supported. The "unknown flag" error seems to be a symptom of a non working podman environment altogether (I got this error with podman run --rm ... on the --rm flag).
Running the container as privileged (docker in my case as podman runs fine within podman) seems to do the trick to get a somewhat working environment and in this case, there's no issue getting the version:

Huh... very interesting...! I appreciate you looking into this.

I don't have the cycles to more thoroughly test this at the moment, but it sounds like this definitely is indicative problematic podman environments, like you've suggested. And, honestly, if I'm the only one reporting this trouble, I'm even more inclined to chalk it up to user-error. Feel free to ignore!

The changes you've made to parsing the version string look terrific. Thanks again.

now that docker cp has been removed, maybe we could rely on just the error message from docker/podman command

This also seems reasonable to me. It's a little less clear what the problem is, but the produced error message does imply that there may be a workaround that doesn't require upgrading Docker, which is pretty convenient.

Copy link
Contributor

@joerick joerick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for putting this together. Would you mind to change the method of version comparison? I think using packaging.version.Version here is wrong because it's specifically designed for python packages.

Here's a class I wrote for comparing flexible versions (it can live in util.py):

@total_ordering
class FlexibleVersion:
    version_str: str
    version_parts: tuple[int, ...]
    suffix: str | None

    def __init__(self, version_str: str) -> None:
        self.version_str = version_str

        # Split into numeric parts and the optional suffix
        match = re.match(r"^(\d+(\.\d+)*)(.*)$", version_str)
        if not match:
            msg = f"Invalid version string: {version_str}"
            raise ValueError(msg)

        version_part, _, suffix = match.groups()

        # Convert numeric version part into a tuple of integers
        self.version_parts = tuple(map(int, version_part.split(".")))
        self.suffix = suffix.strip() if suffix else ""

        # Normalize by removing trailing zeros
        self.version_parts = self._remove_trailing_zeros(self.version_parts)

    def _remove_trailing_zeros(self, parts: tuple[int, ...]) -> tuple[int, ...]:
        # Remove trailing zeros for accurate comparisons
        # without this, "3.0" would be considered greater than "3"
        while parts and parts[-1] == 0:
            parts = parts[:-1]
        return parts

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, FlexibleVersion):
            return NotImplemented
        return (self.version_parts, self.suffix) == (other.version_parts, other.suffix)

    def __lt__(self, other: object) -> bool:
        if not isinstance(other, FlexibleVersion):
            return NotImplemented
        return (self.version_parts, self.suffix) < (other.version_parts, other.suffix)

    def __repr__(self) -> str:
        return f"FlexibleVersion('{self.version_str}')"

    def __str__(self) -> str:
        return self.version_str

Here's the test case (could you also add this to utils_test.py?)

def test_flexible_version_comparisons():
    assert FlexibleVersion("2.0") < FlexibleVersion("2.1")
    assert FlexibleVersion("2.1") > FlexibleVersion("2")
    assert FlexibleVersion("1.9.9") < FlexibleVersion("2.0")
    assert FlexibleVersion("1.10") > FlexibleVersion("1.9.9")
    assert FlexibleVersion("3.0.1") > FlexibleVersion("3.0")
    assert FlexibleVersion("3.0") < FlexibleVersion("3.0.1")
    # Suffix should not affect comparisons
    assert FlexibleVersion("1.0.1-rhel") > FlexibleVersion("1.0")
    assert FlexibleVersion("1.0.1-rhel") < FlexibleVersion("1.1")

That's a bit more code, but it'll work for both Docker and Podman, and remove the podman hacks.

per review comment
@mayeut mayeut requested a review from joerick September 29, 2024 11:04
@henryiii henryiii merged commit dfd01af into pypa:main Oct 1, 2024
23 checks passed
@zachlewis zachlewis deleted the fix_container_engine_version_parsing branch October 10, 2024 21:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

cibuildwheel cannot parse podman version on almalinux 8
5 participants