diff --git a/source-container-build/app/source_build.py b/source-container-build/app/source_build.py index 0de6f841..9d5f8413 100644 --- a/source-container-build/app/source_build.py +++ b/source-container-build/app/source_build.py @@ -389,6 +389,12 @@ def push_to_registry(image_build_output_dir: str, dest_images: list[str]) -> str return f.read().strip() +def generate_konflux_source_image(image: str) -> str: + # in format: sha256:1234567 + digest = fetch_image_manifest_digest(image) + return f"{image.rsplit(':', 1)[0]}:{digest.replace(':', '-')}.src" + + def generate_source_images(image: str) -> list[str]: """Generate source container images from the built binary image @@ -397,11 +403,7 @@ def generate_source_images(image: str) -> list[str]: """ # For backward-compatibility. It will be removed in near future. deprecated_image = f"{image}.src" - - # in format: sha256:1234567 - digest = fetch_image_manifest_digest(image) - source_image = f"{image.rsplit(':', 1)[0]}:{digest.replace(':', '-')}.src" - + source_image = generate_konflux_source_image(image) return [deprecated_image, source_image] @@ -420,11 +422,27 @@ def resolve_source_image_by_version_release(binary_image: str) -> str | None: release = config_data["config"]["Labels"].get("release") if not (version and release): log.warning("Image %s is not labelled with version and release.", binary_image) - return + return None # Remove possible tag or digest from binary image source_image = f"{name}:{version}-{release}-source" if registry_has_image(source_image): return source_image + else: + log.info("Source container image %s does not exist.", source_image) + + +def resolve_source_image_by_manifest(image: str) -> str | None: + """Resolve source image by following Konflux source image scheme + + :param image: str, a binary image whose source image is resolved. + :return: the resolved source image URL. If no one is resolved, None is returned. + """ + source_image = generate_konflux_source_image(image) + if registry_has_image(source_image): + return source_image + else: + log = logging.getLogger(f"{logger.name}.resolve_source_image") + log.info("Source container image %s does not exist.", source_image) def parse_image_name(image: str) -> tuple[str, str, str]: @@ -999,11 +1017,11 @@ def build(args) -> BuildResult: allowed = urlparse("docker://" + base_image).netloc in args.registry_allowlist if allowed: - source_image = resolve_source_image_by_version_release(base_image) + source_image = resolve_source_image_by_version_release( + base_image + ) or resolve_source_image_by_manifest(base_image) if source_image: parent_sources_dir = download_parent_image_sources(source_image, work_dir) - else: - logger.warning("Registry does not have source image %s", source_image) else: logger.info( "Image %s does not come from supported allowed registry. " diff --git a/source-container-build/app/test_source_build.py b/source-container-build/app/test_source_build.py index c3edb415..6e67dc9d 100644 --- a/source-container-build/app/test_source_build.py +++ b/source-container-build/app/test_source_build.py @@ -750,6 +750,7 @@ def _test_include_sources( parent_images: str = "", expect_parent_image_sources_included: bool = False, mock_nonexisting_source_image: bool = False, + source_image_is_resolved_by_version_release: bool = True, ): """Test include various sources and app source will always be included""" @@ -785,13 +786,19 @@ def run_side_effect(cmd, **kwargs): self.assertNotIn(":9.3-1", dest_image, "tag is not removed from image pullspec") if cmd[2] == "--config": - return Mock( - stdout=json.dumps( - {"config": {"Labels": {"version": "9.3", "release": "1"}}} - ) - ) + # Get image config + if source_image_is_resolved_by_version_release: + config = {"config": {"Labels": {"version": "9.3", "release": "1"}}} + else: + config = {"config": {"Labels": {}}} + return Mock(stdout=json.dumps(config)) if cmd[2] == "--raw": + if not source_image_is_resolved_by_version_release: + dest_image = cmd[-1] + source_tag = self.BINARY_IMAGE_MANIFEST_DIGEST.replace(":", "-") + ".src" + self.assertTrue(dest_image.endswith(source_tag)) + # Indicate the source image of parent image exists if mock_nonexisting_source_image: return Mock(returncode=1) @@ -799,9 +806,8 @@ def run_side_effect(cmd, **kwargs): return Mock(returncode=0) if cmd[2] == "--format": - mock = Mock() - mock.stdout = self.BINARY_IMAGE_MANIFEST_DIGEST - return mock + # Get image manifest + return Mock(stdout=self.BINARY_IMAGE_MANIFEST_DIGEST) if run_cmd == ["skopeo", "copy"]: args = create_skopeo_cli_parser().parse_args(cmd[1:]) @@ -958,6 +964,13 @@ def test_not_include_parent_image_sources_from_disallowed_registry(self): expect_parent_image_sources_included=False, ) + def test_resolve_source_image_by_image_manifest(self): + self._test_include_sources( + parent_images="registry.access.example.com/ubi9/ubi:9.3-1@sha256:123\n", + expect_parent_image_sources_included=True, + source_image_is_resolved_by_version_release=False, + ) + @patch("source_build.run") def test_create_a_temp_dir_as_workspace(self, run): def run_side_effect(cmd, **kwargs): @@ -1219,3 +1232,29 @@ def test_deduplicate(self): expected = sorted(["requests-1.23-1.src.rpm", "flask-2.0.tar.gz"]) self.assertListEqual(expected, sorted(remains_in_parent)) + + +class TestResolveSourceImageByManifest(unittest.TestCase): + """Test resolve_source_image_by_manifest""" + + @patch("source_build.run") + def test_source_image_is_resolved(self, mock_run: MagicMock): + manifest_digest = "sha256:123456" + skopeo_inspect_digest_rv = Mock(stdout=manifest_digest) + skopeo_inspect_raw_rv = Mock(returncode=0) + mock_run.side_effect = [skopeo_inspect_digest_rv, skopeo_inspect_raw_rv] + + source_image = source_build.resolve_source_image_by_manifest("registry.io:3000/ns/app:1.0") + + self.assertEqual("registry.io:3000/ns/app:sha256-123456.src", source_image) + + @patch("source_build.run") + def test_source_image_does_not_exist(self, mock_run: MagicMock): + manifest_digest = "sha256:123456" + skopeo_inspect_digest_rv = Mock(stdout=manifest_digest) + skopeo_inspect_raw_rv = Mock(returncode=1) + mock_run.side_effect = [skopeo_inspect_digest_rv, skopeo_inspect_raw_rv] + + source_image = source_build.resolve_source_image_by_manifest("registry.io:3000/ns/app:1.0") + + self.assertIsNone(source_image)