From 6b6c1901ae0f94e8cc60aa6db0acb45e00ed6960 Mon Sep 17 00:00:00 2001 From: ethanbalcik Date: Wed, 19 Nov 2025 14:33:07 -0500 Subject: [PATCH 1/2] fix(lint): extend manifest list linter to manifests Signed-off-by: ethanbalcik (cherry picked from commit b9d9b9c7fec803eca779251cb3e351cad730e420) --- image/linter.py | 127 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 91 insertions(+), 36 deletions(-) diff --git a/image/linter.py b/image/linter.py index def1b4f..f65abc8 100644 --- a/image/linter.py +++ b/image/linter.py @@ -1,8 +1,10 @@ from datetime import datetime, timezone from image.auth import AUTH from image.byteunit import ByteUnit +from image.client import ContainerImageRegistryClient from image.config import ContainerImageConfig from image.containerimage import ContainerImage +from image.errors import ContainerImageError from image.manifest import ContainerImageManifest from image.manifestlist import ContainerImageManifestList from image.mediatypes import * @@ -11,6 +13,7 @@ from lint.result import LintResult from lint.rule import LintRule, DEFAULT_LINT_RULE_CONFIG from lint.status import LintStatus +from typing import Union DEFAULT_CONTAINER_IMAGE_LINTER_CONFIG = LinterConfig({ "ManifestListSupportsRequiredPlatforms": { @@ -100,36 +103,51 @@ class ContainerImageManifestLinter( pass class ManifestListSupportsRequiredPlatforms( - LintRule[ContainerImageManifestList] + LintRule[Union[ContainerImageManifestList, ContainerImageManifest]] ): """ A lint rule ensuring a manifest list supports the required platforms """ def lint( self, - artifact: ContainerImageManifestList, - config: LintRuleConfig=DEFAULT_LINT_RULE_CONFIG + artifact: Union[ContainerImageManifestList, ContainerImageManifest], + config: LintRuleConfig=DEFAULT_LINT_RULE_CONFIG, + **kwargs ) -> LintResult: """ Implementation of the ManifestListSupportsRequiredPlatforms lint rule """ try: required = config.config.get("platforms", [ "linux/amd64" ]) - platforms = set( - str(entry.get_platform()) for entry in artifact.get_entries() - ) + + # The image is actually a manifest list + if isinstance(artifact, ContainerImageManifestList): + image_type = "manifest list" + platforms = set( + str(entry.get_platform()) for entry in artifact.get_entries() + ) + else: + # The image was built as a manifest + image_type = "manifest" + manifest_config = kwargs.get("manifest_config") + if not isinstance(manifest_config, ContainerImageConfig): + raise ContainerImageError( + "manifest list lint rule attempted to lint a manifest " + \ + f"and no manifest config was given, got {type(manifest_config).__name__}" + ) + platforms = set([str(manifest_config.get_platform())]) missing = list(set(required).difference(platforms)) if len(missing) > 0: return LintResult( status=LintStatus.ERROR, - message=f"({self.name()}) manifest list does not support " + \ - "the following required platforms: " + \ + message=f"({self.name()}) {image_type} does not " + \ + "support the following required platforms: " + \ str([ str(platform) for platform in missing ]) ) return LintResult( status=LintStatus.INFO, message=f"({self.name()}) " + \ - "manifest list supports all required platforms" + f"{image_type} supports all required platforms" ) except Exception as e: return LintResult( @@ -138,7 +156,7 @@ def lint( ) class ManifestListSupportsRequiredMediaTypes( - LintRule[ContainerImageManifestList] + LintRule[Union[ContainerImageManifestList, ContainerImageManifest]] ): """ A lint rule ensuring a manifest list and its manifests support the required @@ -146,7 +164,7 @@ class ManifestListSupportsRequiredMediaTypes( """ def lint( self, - artifact: ContainerImageManifestList, + artifact: Union[ContainerImageManifestList, ContainerImageManifest], config: LintRuleConfig=DEFAULT_LINT_RULE_CONFIG, **kwargs ) -> LintResult: @@ -154,41 +172,69 @@ def lint( Implementation of the ManifestListSupportsRequiredMediaTypes lint rule """ try: - list_media_type = artifact.get_media_type() - expected_list_media_types = config.config.get( - "manifest-list-media-types", + allow_single_arch = config.config.get( + "allow-single-arch", + True + ) + expected_manifest_media_types = config.config.get( + "manifest-media-types", [ - DOCKER_V2S2_LIST_MEDIA_TYPE, - OCI_INDEX_MEDIA_TYPE + DOCKER_V2S2_MEDIA_TYPE, + OCI_MANIFEST_MEDIA_TYPE ] ) - if not list_media_type in expected_list_media_types: - return LintResult( - status=LintStatus.ERROR, - message=f"({self.name()}) " + \ - f"manifest list has mediaType {list_media_type}, " + \ - f"expected one of {str(expected_list_media_types)}" - ) - for entry in artifact.get_entries(): - manifest_media_type = entry.get_media_type() - expected_media_types = config.config.get( - "manifest-media-types", + + # The image is actually a manifest list + if isinstance(artifact, ContainerImageManifestList): + list_media_type = artifact.get_media_type() + expected_list_media_types = config.config.get( + "manifest-list-media-types", [ - DOCKER_V2S2_MEDIA_TYPE, - OCI_MANIFEST_MEDIA_TYPE + DOCKER_V2S2_LIST_MEDIA_TYPE, + OCI_INDEX_MEDIA_TYPE ] ) - if not manifest_media_type in expected_media_types: + if not list_media_type in expected_list_media_types: return LintResult( status=LintStatus.ERROR, message=f"({self.name()}) " + \ - f"manifest {entry.get_platform()} has mediaType " + \ - f"{manifest_media_type}, expected one of " + \ - str(expected_media_types) + f"manifest list has mediaType {list_media_type}, " + \ + f"expected one of {str(expected_list_media_types)}" ) + for entry in artifact.get_entries(): + manifest_media_type = entry.get_media_type() + if not manifest_media_type in expected_manifest_media_types: + return LintResult( + status=LintStatus.ERROR, + message=f"({self.name()}) " + \ + f"manifest {entry.get_platform()} has mediaType " + \ + f"{manifest_media_type}, expected one of " + \ + str(expected_manifest_media_types) + ) + return LintResult( + message=f"({self.name()}) " + \ + "manifest list and manifests support expected mediaTypes" + ) + + # The image was built as a manifest + if not allow_single_arch: + return LintResult( + status=LintStatus.ERROR, + message=f"({self.name()}) " + \ + f"got manifest, but expected manifest list" + ) + manifest_media_type = artifact.get_media_type() + if not manifest_media_type in expected_manifest_media_types: + return LintResult( + status=LintStatus.ERROR, + message=f"({self.name()}) " + \ + f"manifest has mediaType {manifest_media_type} " + \ + f", expected one of {str(expected_manifest_media_types)}" + ) return LintResult( + status=LintStatus.INFO, message=f"({self.name()}) " + \ - "manifest list and manifests support expected maediaTypes" + "manifest supports expected mediaTypes" ) except Exception as e: return LintResult( @@ -197,10 +243,12 @@ def lint( ) class ContainerImageManifestListLinter( - Linter[ContainerImageManifestList] + Linter[Union[ContainerImageManifestList, ContainerImageManifest]] ): """ - A linter for container image manifest lists + A linter for container image manifest lists. Can apply the same checks to + manifests in case a manifest is being built when a manifest list should be + built. """ pass @@ -412,6 +460,13 @@ def lint( manifest=manifest, auth=auth ) + results.extend( + self.manifest_list_linter.lint( + manifest, + config, + manifest_config=img_config + ) + ) results.extend(self.config_linter.lint(img_config, config)) # Even though it should always exist, this is protection against OOB From 542dcac04b4c76dffb36e2f5ce5e6694e9f44c74 Mon Sep 17 00:00:00 2001 From: ethanbalcik Date: Wed, 19 Nov 2025 14:39:41 -0500 Subject: [PATCH 2/2] chore(version): increment version for release Signed-off-by: ethanbalcik --- README.md | 4 ++-- doc/source/conf.py | 2 +- doc/source/index.rst | 4 ++-- pyproject.toml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 44fdb15..c5745f7 100644 --- a/README.md +++ b/README.md @@ -56,10 +56,10 @@ pip install containerimage-py 1. Clone this repository 2. [Build the project from source](#build) 3. Locate the `.whl` (wheel) file in the `dist` folder - - It should be named something like so: `containerimage_py-1.1.3-py3-none-any.whl` + - It should be named something like so: `containerimage_py-1.1.4-py3-none-any.whl` 4. Run the following command from the root of the repository, replacing the name of the `.whl` file if necessary ``` - pip install dist/containerimage_py-1.1.3-py3-none-any.whl + pip install dist/containerimage_py-1.1.4-py3-none-any.whl ``` ## Build diff --git a/doc/source/conf.py b/doc/source/conf.py index 4d3cfcb..a90b8d2 100644 --- a/doc/source/conf.py +++ b/doc/source/conf.py @@ -20,7 +20,7 @@ project = 'containerimage-py' copyright = '2025, IBM Corporation' author = 'Ethan Balcik' -release = '1.1.3' +release = '1.1.4' # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration diff --git a/doc/source/index.rst b/doc/source/index.rst index ecc8c8c..18fad79 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -58,12 +58,12 @@ Run the following command to install the latest version of this package using pi 1. Clone `the source repository `_ 2. Build the project from source following `the build instructions `_ -3. Locate the ``.whl`` (wheel) file in the ``dist`` folder. It should be named something like so: ``containerimage_py-1.1.3-py3-none-any.whl`` +3. Locate the ``.whl`` (wheel) file in the ``dist`` folder. It should be named something like so: ``containerimage_py-1.1.4-py3-none-any.whl`` 4. Run the following command from the root of the repository, replacing the name of the ``.whl`` file if necessary .. code-block:: shell - pip install dist/containerimage_py-1.1.3-py3-none-any.whl + pip install dist/containerimage_py-1.1.4-py3-none-any.whl Build diff --git a/pyproject.toml b/pyproject.toml index 3a4b8a2..38dbe43 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "containerimage-py" -version = "1.1.3" +version = "1.1.4" authors = [ {name = "Ethan Balcik", email="ethanbalcik@ibm.com" } ]