Repository: lilydjwg/nvchecker (language: python)

Issue:
test_container fails
@bianjp

<details>
<summary>Test failure log</summary>

```
$ pytest tests/test_container.py::test_container_with_tag
=========================================================================== test session starts ============================================================================
platform linux -- Python 3.11.6, pytest-7.4.3, pluggy-1.3.0
rootdir: /home/yen/var/local/Computer/archlinux/nvchecker
configfile: pyproject.toml
plugins: anyio-3.7.1, httpbin-2.0.0, flaky-3.7.0, asyncio-0.23.0
asyncio: mode=Mode.STRICT
collected 1 item

tests/test_container.py F                                                                                                                                            [100%]

================================================================================= FAILURES =================================================================================
_________________________________________________________________________ test_container_with_tag __________________________________________________________________________

get_version = <function get_version.<locals>.__call__ at 0x7f5c42e05da0>

    async def test_container_with_tag(get_version):
>     update_time = await get_version("hello-world:linux", {
        "source": "container",
        "container": "library/hello-world:linux",
      })

tests/test_container.py:16:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests/conftest.py:57: in __call__
    newvers = await run(entries)
tests/conftest.py:50: in run
    vers, _has_failures = await main.run(result_coro, runner_coro)
nvchecker/__main__.py:102: in run
    return await result_fu
nvchecker/core.py:397: in process_result
    logger.exception('error processing result', result=r)
/usr/lib/python3.11/site-packages/structlog/_log_levels.py:88: in exception
    return self.error(event, *args, **kw)
/usr/lib/python3.11/site-packages/structlog/_log_levels.py:168: in meth
    return self._proxy_to_logger(name, event, **kw)
/usr/lib/python3.11/site-packages/structlog/_base.py:222: in _proxy_to_logger
    args, kw = self._process_event(method_name, event, event_kw)
/usr/lib/python3.11/site-packages/structlog/_base.py:171: in _process_event
    event_dict = proc(self._logger, method_name, event_dict)
nvchecker/core.py:395: in process_result
    r1 = _process_result(r)
nvchecker/core.py:342: in _process_result
    logger.error('unexpected error happened',
/usr/lib/python3.11/site-packages/structlog/_log_levels.py:168: in meth
    return self._proxy_to_logger(name, event, **kw)
/usr/lib/python3.11/site-packages/structlog/_base.py:222: in _proxy_to_logger
    args, kw = self._process_event(method_name, event, event_kw)
/usr/lib/python3.11/site-packages/structlog/_base.py:171: in _process_event
    event_dict = proc(self._logger, method_name, event_dict)
tests/conftest.py:87: in proc
    raise exc
nvchecker/util.py:298: in run_one
    version = await self.func(
nvchecker_source/container.py:146: in get_version
    return await cache.get(key, get_container_tag_update_time)
nvchecker/util.py:237: in get
    r = await fu
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

info = ('library/hello-world', 'linux', 'registry-1.docker.io', AuthInfo(service='registry.docker.io', realm='https://auth.docker.io/token'))

    async def get_container_tag_update_time(info: Tuple[str, str, str, AuthInfo]):
      '''
      Find the update time of a container tag.

      In fact, it's the creation time of the image ID referred by the tag. Tag itself does not have any update time.
      '''
      image_path, image_tag, registry_host, auth_info = info
      token = await get_auth_token(auth_info, image_path)

      # HTTP headers
      headers = {
        'Authorization': f'Bearer {token}',
        # Prefer Image Manifest Version 2, Schema 2: https://distribution.github.io/distribution/spec/manifest-v2-2/
        'Accept': 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.container.image.v1+json, application/json',
      }

      # Get tag manifest
      url = f'https://{registry_host}/v2/{image_path}/manifests/{image_tag}'
      res = await session.get(url, headers=headers)
      data = res.json()
      # Schema 1 returns the creation time in the response
      if data['schemaVersion'] == 1:
        return json.loads(data['history'][0]['v1Compatibility'])['created']

      # For schema 2, we have to fetch the config's blob
>     digest = data['config']['digest']
E     KeyError: 'config'

nvchecker_source/container.py:124: KeyError
========================================================================= short test summary info ==========================================================================
FAILED tests/test_container.py::test_container_with_tag - KeyError: 'config'
============================================================================ 1 failed in 1.93s =============================================================================
```

</details>

Here is a dump of results from the relevant API (prettified JSON):
<details>
<summary>data.json</summary>

```JSON
{
    "manifests": [
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:amd64/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "amd64",
                "os": "linux"
            },
            "size": 861
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:e2fc4e5012d16e7fe466f5291c476431beaa1f9b90a5c2125b493ed28e2aba57",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:579b3724a7b189f6dca599a46f16d801a43d5def185de0b7bcd5fb9d1e312c27",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 837
        },
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm32v5/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:c2d891e5c2fb4c723efb72b064be3351189f62222bd3681ce7e57f2a1527362c",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "arm",
                "os": "linux",
                "variant": "v5"
            },
            "size": 863
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:c2d891e5c2fb4c723efb72b064be3351189f62222bd3681ce7e57f2a1527362c",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:6901d6a88eee6e90f0baa62b020bb61c4f13194cbcd9bf568ab66e8cc3f940dd",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 566
        },
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm32v7/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:20aea1c63c90d5e117db787c9fe1a8cd0ad98bedb5fd711273ffe05c084ff18a",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "arm",
                "os": "linux",
                "variant": "v7"
            },
            "size": 863
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:20aea1c63c90d5e117db787c9fe1a8cd0ad98bedb5fd711273ffe05c084ff18a",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:70304c314d8a61ba1b36518624bb00bfff8d4b6016153792042de43f0453ca61",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 837
        },
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:arm64v8/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:2d4e459f4ecb5329407ae3e47cbc107a2fbace221354ca75960af4c047b3cb13",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "arm64",
                "os": "linux",
                "variant": "v8"
            },
            "size": 863
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:2d4e459f4ecb5329407ae3e47cbc107a2fbace221354ca75960af4c047b3cb13",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:1f11fbd1720fcae3e402fc3eecb7d57c67023d2d1e11becc99ad9c7fe97d65ca",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 837
        },
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:i386/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:dbbd3cf666311ad526fad9d1746177469268f32fd91b371df2ebd1c84eb22f23",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "386",
                "os": "linux"
            },
            "size": 860
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:dbbd3cf666311ad526fad9d1746177469268f32fd91b371df2ebd1c84eb22f23",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:18b1c92de36d42c75440c6fd6b25605cc91709d176faaccca8afe58b317bc33a",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 566
        },
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:mips64le/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:c19784034d46da48550487c5c44639f5f92d48be7b9baf4d67b5377a454d92af",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "mips64le",
                "os": "linux"
            },
            "size": 864
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:c19784034d46da48550487c5c44639f5f92d48be7b9baf4d67b5377a454d92af",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:951bcd144ddccd1ee902dc180b435faabaaa6a8747e70cbc893f2dca16badb94",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 566
        },
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:ppc64le/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:f0c95f1ebb50c9b0b3e3416fb9dd4d1d197386a076c464cceea3d1f94c321b8f",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "ppc64le",
                "os": "linux"
            },
            "size": 863
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:f0c95f1ebb50c9b0b3e3416fb9dd4d1d197386a076c464cceea3d1f94c321b8f",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:838d191bca398e46cddebc48e816da83b0389d4ed2d64f408d618521b8fd1a57",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 837
        },
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:riscv64/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:8d064a6fc27fd5e97fa8225994a1addd872396236367745bea30c92d6c032fa3",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "riscv64",
                "os": "linux"
            },
            "size": 863
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:8d064a6fc27fd5e97fa8225994a1addd872396236367745bea30c92d6c032fa3",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:48147407c4594e45b7c3f0be1019bb0f44d78d7f037ce63e0e3da75b256f849e",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 837
        },
        {
            "annotations": {
                "org.opencontainers.image.revision": "3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee",
                "org.opencontainers.image.source": "https://github.com/docker-library/hello-world.git#3fb6ebca4163bf5b9cc496ac3e8f11cb1e754aee:s390x/hello-world",
                "org.opencontainers.image.url": "https://hub.docker.com/_/hello-world",
                "org.opencontainers.image.version": "linux"
            },
            "digest": "sha256:65f4b0d1802589b418bb6774d85de3d1a11d5bd971ee73cb8569504d928bb5d9",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "s390x",
                "os": "linux"
            },
            "size": 861
        },
        {
            "annotations": {
                "vnd.docker.reference.digest": "sha256:65f4b0d1802589b418bb6774d85de3d1a11d5bd971ee73cb8569504d928bb5d9",
                "vnd.docker.reference.type": "attestation-manifest"
            },
            "digest": "sha256:50f420e8710676da03668e446f1f51097b745e3e2c9807b018e569d26d4f65f7",
            "mediatype": "application/vnd.oci.image.manifest.v1+json",
            "platform": {
                "architecture": "unknown",
                "os": "unknown"
            },
            "size": 837
        }
    ],
    "mediatype": "application/vnd.oci.image.index.v1+json",
    "schemaversion": 2
}
```

</details>

It seems there are different formats for manifests with `"schemaversion": 2` [1]? Maybe nvchecker needs to handle different cases.

[1] https://distribution.github.io/distribution/spec/manifest-v2-2/


Discussion / hints:
For multi-arch images, registry will return Manifest List instead of Image Manifest: https://distribution.github.io/distribution/spec/manifest-v2-2/#manifest-list.

This case was not handled previously. I'll make a new PR to fix it.

Fix patch:
diff --git a/nvchecker_source/container.py b/nvchecker_source/container.py
index 6fec2089..fc065058 100644
--- a/nvchecker_source/container.py
+++ b/nvchecker_source/container.py
@@ -109,7 +109,13 @@ async def get_container_tag_update_time(info: Tuple[str, str, str, AuthInfo]):
   headers = {
     'Authorization': f'Bearer {token}',
     # Prefer Image Manifest Version 2, Schema 2: https://distribution.github.io/distribution/spec/manifest-v2-2/
-    'Accept': 'application/vnd.docker.distribution.manifest.v2+json, application/vnd.docker.container.image.v1+json, application/json',
+    'Accept': ', '.join([
+      'application/vnd.oci.image.manifest.v1+json',
+      'application/vnd.oci.image.index.v1+json',
+      'application/vnd.docker.distribution.manifest.v2+json',
+      'application/vnd.docker.distribution.manifest.list.v2+json',
+      'application/json',
+    ]),
   }
 
   # Get tag manifest
@@ -121,6 +127,16 @@ async def get_container_tag_update_time(info: Tuple[str, str, str, AuthInfo]):
     return json.loads(data['history'][0]['v1Compatibility'])['created']
 
   # For schema 2, we have to fetch the config's blob
+  # For multi-arch images, multiple manifests are bounded with the same tag. We should choose one and then request
+  # the manifest's detail
+  if data.get('manifests'):
+    # It's quite hard to find the manifest matching with current CPU architecture and system.
+    # For now we just choose the first and it should probably work for most cases
+    image_digest = data['manifests'][0]['digest']
+    url = f'https://{registry_host}/v2/{image_path}/manifests/{image_digest}'
+    res = await session.get(url, headers=headers)
+    data = res.json()
+
   digest = data['config']['digest']
   url = f'https://{registry_host}/v2/{image_path}/blobs/{digest}'
   res = await session.get(url, headers=headers)


Test patch:
diff --git a/tests/test_container.py b/tests/test_container.py
index f0e52770..cdf6e470 100644
--- a/tests/test_container.py
+++ b/tests/test_container.py
@@ -13,6 +13,14 @@ async def test_container(get_version):
   }) == "linux"
 
 async def test_container_with_tag(get_version):
+  update_time = await get_version("bitnami/mongodb:5.0", {
+    "source": "container",
+    "container": "bitnami/mongodb:5.0",
+  })
+  # the update time is changing occasionally, so we can not compare the exact time, otherwise the test will be failed in the future
+  assert datetime.date.fromisoformat(update_time.split('T')[0]) > datetime.date(2023, 12, 1)
+
+async def test_container_with_tag_and_multi_arch(get_version):
   update_time = await get_version("hello-world:linux", {
     "source": "container",
     "container": "library/hello-world:linux",