From 4c46cac159c8faa4899337d9fd9230aaabc20d60 Mon Sep 17 00:00:00 2001 From: LuLo Date: Mon, 7 Sep 2026 13:51:25 +0200 Subject: [PATCH] fix(go:report): handle floating golang builder tags in streams.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit go:report and find-bugs:golang crash when a golang builder stream in streams.yml uses a floating tag (e.g. v1.22-rhel9) instead of a full NVR-style tag. parse_nvr() needs an X.Y.Z version and split_el_suffix_in_release needs .elN — both fail on -rhelN floating tags. Add is_floating_golang_builder_tag() to detect vX.Y-rhelN tags before the existing NVR code paths. For non-exact mode, extract major.minor + RHEL suffix from the tag string directly (no network). For exact mode, call oc image info to resolve the floating tag to an actual build, read the golang package NVR from OCI labels, and delegate to get_golang_container_nvrs as usual. All three existing NVR tag formats are unaffected. find-bugs:golang inherits the fix through the shared golang_report_for_version function. Also adds defensive len==1 assertions before indexing go_builder_nvr_map in both the floating-tag and NVR exact-mode paths. Adds elliott/tests/test_get_golang_report_cli.py covering: (a) floating-tag detection (True/False for all known formats) (b) non-exact version extraction from floating tag (c) exact-mode resolution via mocked oc image info (d) regression tests for all three legacy NVR formats (e) golang_report_for_version(exact=True) with a floating-tag stream Fixes: ART-23298 Part of: ART-23167 (floating tag audit) Blocks: ART-23148 (streams.yml migration) rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED Assisted-by: Claude (Anthropic) --- .../elliottlib/cli/get_golang_report_cli.py | 76 ++++- elliott/tests/test_get_golang_report_cli.py | 281 ++++++++++++++++++ 2 files changed, 355 insertions(+), 2 deletions(-) create mode 100644 elliott/tests/test_get_golang_report_cli.py diff --git a/elliott/elliottlib/cli/get_golang_report_cli.py b/elliott/elliottlib/cli/get_golang_report_cli.py index 7eb64477fc..7546ca5fb4 100644 --- a/elliott/elliottlib/cli/get_golang_report_cli.py +++ b/elliott/elliottlib/cli/get_golang_report_cli.py @@ -6,6 +6,7 @@ from artcommonlib.format_util import green_print from artcommonlib.release_util import split_el_suffix_in_release from artcommonlib.rpm_utils import parse_nvr +from artcommonlib.util import oc_image_info from elliottlib.cli.common import cli from elliottlib.runtime import Runtime @@ -13,6 +14,64 @@ _LOGGER = logutil.get_logger(__name__) +# Matches floating golang-builder tags such as: +# openshift-golang-builder-container-v1.22-rhel9 +# openshift-golang-builder-container-v1.22-rhel8 +# These lack the X.Y.Z patch version present in full NVR tags. +_FLOATING_TAG_RE = re.compile(r'v(\d+\.\d+)-rhel(\d+)$') + + +def is_floating_golang_builder_tag(nvr_like: str) -> bool: + """Return True when *nvr_like* is a floating tag (vX.Y-rhelN) rather than a full NVR string.""" + return bool(_FLOATING_TAG_RE.search(nvr_like)) + + +def go_version_from_floating_tag(nvr_like: str, ignore_rhel: bool) -> str: + """Extract a go-version string from a floating tag like + ``openshift-golang-builder-container-v1.22-rhel9``. + + Returns ``X.Y.elN`` normally, or just ``X.Y`` when *ignore_rhel* is True. + """ + m = _FLOATING_TAG_RE.search(nvr_like) + if not m: + raise ValueError(f"Not a floating golang-builder tag: {nvr_like!r}") + major_minor = m.group(1) + rhel_version = m.group(2) + if ignore_rhel: + return major_minor + return f"{major_minor}.el{rhel_version}" + + +def go_version_from_floating_tag_exact(image_pullspec: str) -> str: + """Resolve a floating-tag pullspec to the exact golang package NVR. + + Calls ``oc image info`` to read the OCI labels from the resolved image, + constructs the builder NVR, then delegates to ``get_golang_container_nvrs`` + (exact mode) to return the golang package NVR string (e.g. + ``golang-1.22.5-1.el9``). + """ + _LOGGER.info(f"Resolving floating tag via oc image info: {image_pullspec}") + image_data = oc_image_info(image_pullspec, '--filter-by-os=amd64') + labels = image_data.get('config', {}).get('config', {}).get('Labels', {}) + component = labels.get('com.redhat.component') + version = labels.get('version') + release = labels.get('release') + if not all([component, version, release]): + raise ValueError( + f"Cannot determine NVR from image labels for {image_pullspec}: " + f"component={component!r} version={version!r} release={release!r}" + ) + _LOGGER.info(f"Resolved floating tag to builder NVR: {component}-{version}-{release}") + go_builder_nvr_map = get_golang_container_nvrs([(component, version, release)], _LOGGER, exact=True) + if not go_builder_nvr_map: + raise ValueError(f"Could not determine golang package NVR for builder {component}-{version}-{release}") + if len(go_builder_nvr_map) != 1: + raise ValueError( + f"Expected exactly one golang version for builder {component}-{version}-{release}, " + f"got {list(go_builder_nvr_map.keys())}" + ) + return list(go_builder_nvr_map.keys())[0] + @cli.command("go:report", short_help="Report about golang streams configured in streams.yml") @click.option('--ocp-versions', help="OCP versions to show report for. e.g. `4.14`. Comma separated") @@ -26,7 +85,7 @@ def get_golang_report_cli(runtime: Runtime, ocp_versions: str, ignore_rhel: bool Usage: - $ elliott go:report --versions 4.11,4.12,4.13,4.14,4.15,4.16 + $ elliott go:report --ocp-versions 4.11,4.12,4.13,4.14,4.15,4.16 """ results = {} @@ -93,11 +152,24 @@ def golang_report_for_version(runtime, ocp_version: str, ignore_rhel: bool = Fal _LOGGER.info(f"Detected stream {stream_name} with builder nvr: {nvr}") - if exact: + if is_floating_golang_builder_tag(nvr): + # Floating tag (e.g. openshift-golang-builder-container-v1.22-rhel9): no full NVR available. + # Non-exact mode: extract major.minor + RHEL suffix from the tag string directly. + # Exact mode: resolve to actual image via oc image info to obtain the real golang package NVR. + _LOGGER.info(f"Stream {stream_name} uses a floating tag; extracting version from tag") + if exact: + version = go_version_from_floating_tag_exact(image_nvr_like) + else: + version = go_version_from_floating_tag(nvr, ignore_rhel) + elif exact: parsed_nvr = parse_nvr(nvr) go_builder_nvr_map = get_golang_container_nvrs( [(parsed_nvr['name'], parsed_nvr['version'], parsed_nvr['release'])], _LOGGER, exact=exact ) + if len(go_builder_nvr_map) != 1: + raise ValueError( + f"Expected exactly one golang version for builder {nvr}, got {list(go_builder_nvr_map.keys())}" + ) exact_pkg = list(go_builder_nvr_map.keys())[0] version = exact_pkg else: diff --git a/elliott/tests/test_get_golang_report_cli.py b/elliott/tests/test_get_golang_report_cli.py new file mode 100644 index 0000000000..2bdb130a71 --- /dev/null +++ b/elliott/tests/test_get_golang_report_cli.py @@ -0,0 +1,281 @@ +"""Unit tests for elliott go:report floating-tag support. + +Scenarios covered: + (a) is_floating_golang_builder_tag: True for floating, False for all three NVR formats + (b) go_version_from_floating_tag: correct major.minor + RHEL extraction + (c) go_version_from_floating_tag_exact: mocked oc image info returns correct NVR + (d) go_version_from_nvr_string: all three legacy NVR tag formats still parse + (e) golang_report_for_version with exact=True and a floating-tag stream (SC-5 / find-bugs path) +""" + +import unittest.mock +from unittest import TestCase +from unittest.mock import MagicMock, patch + +from elliottlib.cli.get_golang_report_cli import ( + go_version_from_floating_tag, + go_version_from_floating_tag_exact, + go_version_from_nvr_string, + golang_report_for_version, + is_floating_golang_builder_tag, +) + +# --------------------------------------------------------------------------- +# (a) floating-tag detection +# --------------------------------------------------------------------------- + + +class TestIsFloatingGolangBuilderTag(TestCase): + def test_floating_rhel9(self): + self.assertTrue(is_floating_golang_builder_tag('openshift-golang-builder-container-v1.22-rhel9')) + + def test_floating_rhel8(self): + self.assertTrue(is_floating_golang_builder_tag('openshift-golang-builder-container-v1.23-rhel8')) + + def test_nvr_format_legacy(self): + # openshift/golang-builder:v1.23.9-202506111225.g6c23478.el9 after prefix replacement + self.assertFalse( + is_floating_golang_builder_tag('openshift-golang-builder-container-v1.23.9-202506111225.g6c23478.el9') + ) + + def test_nvr_format_registry_nvr_tag(self): + # registry.redhat.io/openshift/golang-builder:openshift-golang-builder-container-v1.25.8-... + self.assertFalse( + is_floating_golang_builder_tag( + 'openshift-golang-builder-container-v1.25.8-202608271548.p2.gedd1cdd.assembly.stream.el9' + ) + ) + + def test_nvr_format_quay_konflux(self): + # quay.io/...art-images:golang-builder-v1.23.10-... after tag replace + self.assertFalse( + is_floating_golang_builder_tag( + 'openshift-golang-builder-container-v1.23.10-202608241902.p2.gedd1cdd.assembly.stream.el9' + ) + ) + + +# --------------------------------------------------------------------------- +# (b) non-exact version extraction from floating tag +# --------------------------------------------------------------------------- + + +class TestGoVersionFromFloatingTag(TestCase): + def test_rhel9_with_rhel(self): + version = go_version_from_floating_tag('openshift-golang-builder-container-v1.22-rhel9', ignore_rhel=False) + self.assertEqual(version, '1.22.el9') + + def test_rhel8_with_rhel(self): + version = go_version_from_floating_tag('openshift-golang-builder-container-v1.23-rhel8', ignore_rhel=False) + self.assertEqual(version, '1.23.el8') + + def test_ignore_rhel(self): + version = go_version_from_floating_tag('openshift-golang-builder-container-v1.22-rhel9', ignore_rhel=True) + self.assertEqual(version, '1.22') + + def test_non_floating_raises(self): + with self.assertRaises(ValueError): + go_version_from_floating_tag( + 'openshift-golang-builder-container-v1.22.5-202506111225.el9', ignore_rhel=False + ) + + +# --------------------------------------------------------------------------- +# (c) exact-mode resolution via mocked oc image info +# --------------------------------------------------------------------------- + + +class TestGoVersionFromFloatingTagExact(TestCase): + def test_resolves_via_oc_image_info(self): + fake_image_data = { + 'config': { + 'config': { + 'Labels': { + 'com.redhat.component': 'openshift-golang-builder-container', + 'version': 'v1.22.12', + 'release': '202608131106.p2.g7d3050a.assembly.stream.el9', + } + } + } + } + with ( + patch( + 'elliottlib.cli.get_golang_report_cli.oc_image_info', + return_value=fake_image_data, + ) as mock_oi, + patch( + 'elliottlib.cli.get_golang_report_cli.get_golang_container_nvrs', + return_value={ + 'golang-1.22.12-1.el9': { + ( + 'openshift-golang-builder-container', + 'v1.22.12', + '202608131106.p2.g7d3050a.assembly.stream.el9', + ) + } + }, + ) as mock_nvrs, + ): + result = go_version_from_floating_tag_exact('registry.redhat.io/openshift/golang-builder:v1.22-rhel9') + + self.assertEqual(result, 'golang-1.22.12-1.el9') + mock_oi.assert_called_once_with( + 'registry.redhat.io/openshift/golang-builder:v1.22-rhel9', '--filter-by-os=amd64' + ) + mock_nvrs.assert_called_once_with( + [('openshift-golang-builder-container', 'v1.22.12', '202608131106.p2.g7d3050a.assembly.stream.el9')], + unittest.mock.ANY, + exact=True, + ) + + def test_raises_on_missing_labels(self): + fake_image_data = {'config': {'config': {'Labels': {}}}} + with patch('elliottlib.cli.get_golang_report_cli.oc_image_info', return_value=fake_image_data): + with self.assertRaises(ValueError): + go_version_from_floating_tag_exact('registry.redhat.io/openshift/golang-builder:v1.22-rhel9') + + def test_raises_on_multiple_nvr_map_entries(self): + fake_image_data = { + 'config': { + 'config': { + 'Labels': { + 'com.redhat.component': 'openshift-golang-builder-container', + 'version': 'v1.22.12', + 'release': '202608131106.p2.g7d3050a.assembly.stream.el9', + } + } + } + } + with ( + patch('elliottlib.cli.get_golang_report_cli.oc_image_info', return_value=fake_image_data), + patch( + 'elliottlib.cli.get_golang_report_cli.get_golang_container_nvrs', + return_value={'golang-1.22.12-1.el9': set(), 'golang-1.22.11-1.el9': set()}, + ), + ): + with self.assertRaises(ValueError): + go_version_from_floating_tag_exact('registry.redhat.io/openshift/golang-builder:v1.22-rhel9') + + def test_raises_on_empty_nvr_map(self): + fake_image_data = { + 'config': { + 'config': { + 'Labels': { + 'com.redhat.component': 'openshift-golang-builder-container', + 'version': 'v1.22.12', + 'release': '202608131106.p2.g7d3050a.assembly.stream.el9', + } + } + } + } + with ( + patch('elliottlib.cli.get_golang_report_cli.oc_image_info', return_value=fake_image_data), + patch('elliottlib.cli.get_golang_report_cli.get_golang_container_nvrs', return_value={}), + ): + with self.assertRaises(ValueError): + go_version_from_floating_tag_exact('registry.redhat.io/openshift/golang-builder:v1.22-rhel9') + + +# --------------------------------------------------------------------------- +# (d) legacy NVR formats still parse without error +# --------------------------------------------------------------------------- + + +class TestGoVersionFromNvrString(TestCase): + def test_legacy_openshift_builder_format(self): + # openshift/golang-builder:v1.23.9-202506111225.g6c23478.el9 → prefix replaced + nvr = 'openshift-golang-builder-container-v1.23.9-202506111225.g6c23478.el9' + result = go_version_from_nvr_string(nvr, ignore_rhel=False) + self.assertEqual(result, '1.23.9.el9') + + def test_legacy_format_ignore_rhel(self): + nvr = 'openshift-golang-builder-container-v1.23.9-202506111225.g6c23478.el9' + result = go_version_from_nvr_string(nvr, ignore_rhel=True) + self.assertEqual(result, '1.23.9') + + def test_registry_nvr_tag_format(self): + # registry.redhat.io tag where the tag is already in NVR name format + nvr = 'openshift-golang-builder-container-v1.25.8-202608271548.p2.gedd1cdd.assembly.stream.el9' + result = go_version_from_nvr_string(nvr, ignore_rhel=False) + self.assertEqual(result, '1.25.8.el9') + + def test_quay_konflux_format(self): + # quay.io tag after golang-builder → openshift-golang-builder-container replacement + nvr = 'openshift-golang-builder-container-v1.23.10-202608241902.p2.gedd1cdd.assembly.stream.el9' + result = go_version_from_nvr_string(nvr, ignore_rhel=False) + self.assertEqual(result, '1.23.10.el9') + + +# --------------------------------------------------------------------------- +# (e) golang_report_for_version with exact=True and floating-tag stream +# Mirrors the find-bugs:golang call site (exact=True) — SC-5 guard +# --------------------------------------------------------------------------- + + +class TestGolangReportForVersionFloatingExact(TestCase): + def _make_runtime(self, stream_image: str): + runtime = MagicMock() + # image_metas must be non-empty to pass the initialization guard. + # Give the image a builder reference to 'rhel-9-golang' so the stream count becomes 1 + # and the resolved NVR actually appears in the output (needed to verify SC-5). + mock_image = MagicMock() + mock_image.enabled = True + mock_image.config_filename = 'test-image.yml' + mock_image.config = {'from': {'builder': [{'stream': 'rhel-9-golang'}]}} + runtime.image_metas.return_value = [mock_image] + # rpm_metas must be non-empty; use an rpm not in the golang_rpms set. + mock_rpm = MagicMock() + mock_rpm.config_filename = 'unrelated-rpm.yml' + runtime.rpm_metas.return_value = [mock_rpm] + runtime.get_streams_config.return_value = { + 'rhel-9-golang': { + 'image': stream_image, + 'aliases': [], + } + } + # shared_koji_client_session as context manager returning a session with no results + koji_ctx = MagicMock() + koji_ctx.__enter__ = MagicMock(return_value=MagicMock(getLatestBuilds=MagicMock(return_value=[]))) + koji_ctx.__exit__ = MagicMock(return_value=False) + runtime.shared_koji_client_session.return_value = koji_ctx + return runtime + + def test_exact_floating_tag_stream(self): + """golang_report_for_version(exact=True) works when the stream uses a floating tag.""" + stream_image = 'registry.redhat.io/openshift/golang-builder:v1.22-rhel9' + runtime = self._make_runtime(stream_image) + + fake_image_data = { + 'config': { + 'config': { + 'Labels': { + 'com.redhat.component': 'openshift-golang-builder-container', + 'version': 'v1.22.12', + 'release': '202608131106.p2.g7d3050a.assembly.stream.el9', + } + } + } + } + with ( + patch( + 'elliottlib.cli.get_golang_report_cli.oc_image_info', + return_value=fake_image_data, + ), + patch( + 'elliottlib.cli.get_golang_report_cli.get_golang_container_nvrs', + return_value={ + 'golang-1.22.12-1.el9': { + ( + 'openshift-golang-builder-container', + 'v1.22.12', + '202608131106.p2.g7d3050a.assembly.stream.el9', + ) + } + }, + ), + ): + result = golang_report_for_version(runtime, '4.18', ignore_rhel=False, exact=True) + + # The image references stream 'rhel-9-golang', so building_image_count=1 and the + # resolved NVR must appear in the output — verifying SC-5 (find-bugs:golang inherits fix). + self.assertEqual(result, [{'go_version': 'golang-1.22.12-1.el9', 'building_image_count': 1}])