From 5e4189dfda69797126d082974ca1b4e19d58081d Mon Sep 17 00:00:00 2001 From: Ashwin Das Date: Tue, 9 Jun 2026 13:33:52 -0400 Subject: [PATCH 1/6] Update golang builder CVE component name to openshift4/openshift-golang-builder The JIRA pscomponent for golang builder security trackers is changing from openshift-golang-builder-container to openshift4/openshift-golang-builder. Introduce GOLANG_BUILDER_BREW_COMPONENT to preserve the Brew/Konflux package name (openshift-golang-builder-container) which remains unchanged, and update GOLANG_BUILDER_CVE_COMPONENT to the new JIRA pscomponent value. All JIRA bug matching uses the CVE constant; all Brew/Konflux NVR and package lookups use the Brew constant. Filtering logic in attach_cve_flaws_cli and find_bugs_sweep_cli is adjusted to handle the new openshift4/ prefix correctly. Co-authored-by: Cursor rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- elliott/elliottlib/cli/attach_cve_flaws_cli.py | 9 ++++++--- elliott/elliottlib/cli/find_bugs_golang_cli.py | 8 ++++---- elliott/elliottlib/cli/find_bugs_sweep_cli.py | 1 + elliott/elliottlib/constants.py | 3 ++- elliott/elliottlib/errata_async.py | 4 ++-- elliott/elliottlib/util.py | 6 +++--- elliott/tests/test_bzutil.py | 12 ++++++------ elliott/tests/test_find_bugs_golang_cli.py | 2 +- pyartcd/pyartcd/pipelines/update_golang.py | 10 +++++----- pyartcd/tests/pipelines/test_update_golang.py | 4 ++-- 10 files changed, 32 insertions(+), 27 deletions(-) diff --git a/elliott/elliottlib/cli/attach_cve_flaws_cli.py b/elliott/elliottlib/cli/attach_cve_flaws_cli.py index f5d244899d..6a61959e73 100644 --- a/elliott/elliottlib/cli/attach_cve_flaws_cli.py +++ b/elliott/elliottlib/cli/attach_cve_flaws_cli.py @@ -537,7 +537,8 @@ def get_cve_component_mapping( raise ValueError(f"Bug {tracker.id} doesn't have a valid whiteboard component.") whiteboard_component = tracker.whiteboard_component - if is_ocp_delivery_repo(whiteboard_component): + is_golang_builder = whiteboard_component == constants.GOLANG_BUILDER_CVE_COMPONENT + if is_ocp_delivery_repo(whiteboard_component) and not is_golang_builder: # this means the component here is the delivery repo name # we need to translate it to build component name new_component = get_component_by_delivery_repo(runtime, whiteboard_component) @@ -548,10 +549,12 @@ def get_cve_component_mapping( component_names = None # Initialize to ensure it's always defined if konflux: - new_component = get_konflux_component_by_component(runtime, whiteboard_component) + new_component = ( + get_konflux_component_by_component(runtime, whiteboard_component) if not is_golang_builder else None + ) if not new_component: # Special case for builder containers: they should map to all components that use this builder - if whiteboard_component == "openshift-golang-builder-container": + if is_golang_builder or whiteboard_component == constants.GOLANG_BUILDER_BREW_COMPONENT: # Check which components actually use the golang builder logger.info(f"Processing builder container CVE for '{whiteboard_component}' (golang builder)") diff --git a/elliott/elliottlib/cli/find_bugs_golang_cli.py b/elliott/elliottlib/cli/find_bugs_golang_cli.py index ac857069cd..fcae5a62a2 100644 --- a/elliott/elliottlib/cli/find_bugs_golang_cli.py +++ b/elliott/elliottlib/cli/find_bugs_golang_cli.py @@ -162,7 +162,7 @@ def _is_fixed(self, bug: JIRABug, tracker_fixed_in: Set[Version], go_nvr_map) -> for go_build in go_nvr_map.keys(): # if this is a builder image then fetch the golang rpm parent_go_build = go_build - if constants.GOLANG_BUILDER_CVE_COMPONENT in go_build: + if constants.GOLANG_BUILDER_BREW_COMPONENT in go_build: parsed_nvr = parse_nvr(go_build) go_builder_nvr_map = get_golang_container_nvrs( [(parsed_nvr['name'], parsed_nvr['version'], parsed_nvr['release'])], self._logger, exact=True @@ -748,7 +748,7 @@ def compare(b1, b2): "--component", "components", multiple=True, - help="Only operate on trackers for these JIRA Bug components e.g. openshift-golang-builder-container", + help="Only operate on trackers for these JIRA Bug components e.g. openshift4/openshift-golang-builder", ) @click.option('--art-jira', help='Related ART Jira ticket for reference e.g. ART-1234') @click.option( @@ -815,7 +815,7 @@ async def find_bugs_golang_cli( Bugs are compared with latest builds in `stream` assembly by default. Pass --assembly to specify. - For openshift-golang-builder-container build, use --pullspec to determine if fixed for builds in + For openshift-golang-builder build, use --pullspec to determine if fixed for builds in given pullspec Note: rpm trackers cannot be processed if --pullspec is used, for that rely on --assembly. @@ -828,7 +828,7 @@ async def find_bugs_golang_cli( bugs like openshift-golang-builder where we want to move the bug to VERIFIED after or close to when mass rebuild is triggered. - --component: Only operate on trackers for these JIRA Bug components e.g. openshift-golang-builder-container. + --component: Only operate on trackers for these JIRA Bug components e.g. openshift4/openshift-golang-builder. --rpms-only: Ignore builder container bugs and only analyze RPM trackers. diff --git a/elliott/elliottlib/cli/find_bugs_sweep_cli.py b/elliott/elliottlib/cli/find_bugs_sweep_cli.py index f94b79205d..b721736b1a 100644 --- a/elliott/elliottlib/cli/find_bugs_sweep_cli.py +++ b/elliott/elliottlib/cli/find_bugs_sweep_cli.py @@ -615,6 +615,7 @@ def categorize_bugs_by_type( # golang builder is a special tracker component # which applies to all our golang images exception_packages.append(constants.GOLANG_BUILDER_CVE_COMPONENT) + exception_packages.append(constants.GOLANG_BUILDER_BREW_COMPONENT) for bug in tracker_bugs: package_name = bug.whiteboard_component diff --git a/elliott/elliottlib/constants.py b/elliott/elliottlib/constants.py index 3f8984da9a..54877082cb 100644 --- a/elliott/elliottlib/constants.py +++ b/elliott/elliottlib/constants.py @@ -22,7 +22,8 @@ } # Golang builder needs special treatment when associating security tracking bugs with builds: -GOLANG_BUILDER_CVE_COMPONENT = 'openshift-golang-builder-container' +GOLANG_BUILDER_CVE_COMPONENT = 'openshift4/openshift-golang-builder' +GOLANG_BUILDER_BREW_COMPONENT = 'openshift-golang-builder-container' BUG_LOOKUP_CHUNK_SIZE = 100 BUG_ATTACH_CHUNK_SIZE = 100 diff --git a/elliott/elliottlib/errata_async.py b/elliott/elliottlib/errata_async.py index d8ab04883c..836f55b9ef 100644 --- a/elliott/elliottlib/errata_async.py +++ b/elliott/elliottlib/errata_async.py @@ -417,10 +417,10 @@ def populate_golang_cve_components(cls, golang_cve_names, expected_cve_component builder_nvr = parse_nvr(builder_nvr_string) # Make sure they are go builder nvrs (this should never happen) - if builder_nvr['name'] != constants.GOLANG_BUILDER_CVE_COMPONENT: + if builder_nvr['name'] != constants.GOLANG_BUILDER_BREW_COMPONENT: raise ValueError( f"Unexpected `name` value for nvr {builder_nvr}. Expected " - f"{constants.GOLANG_BUILDER_CVE_COMPONENT}. Please investigate." + f"{constants.GOLANG_BUILDER_BREW_COMPONENT}. Please investigate." ) if 'etcd' in list(go_nvr_map[builder_nvr_string])[0]: diff --git a/elliott/elliottlib/util.py b/elliott/elliottlib/util.py index 8ec03e04b8..f09b33a9a6 100644 --- a/elliott/elliottlib/util.py +++ b/elliott/elliottlib/util.py @@ -488,7 +488,7 @@ def get_golang_container_nvrs_brew(nvrs: List[Tuple[str, str, str]], logger, exa logger.error(f'Error parsing {build}') raise name = nvr[0] - if name == 'openshift-golang-builder-container' or 'go-toolset' in name: + if name == constants.GOLANG_BUILDER_BREW_COMPONENT or 'go-toolset' in name: go_version = get_parent_golang_from_brew(nvr) if not go_version: raise ValueError(f'Could not find go version for golang builder {nvr}') @@ -591,7 +591,7 @@ def get_golang_container_nvrs_for_konflux_record( # NVRs never contain '/', so we can distinguish them from pullspecs. if '/' not in p: # This is an NVR - look for golang builder (e.g. openshift-golang-builder-container-v1.24.4-...) - if p.startswith(f'{constants.GOLANG_BUILDER_CVE_COMPONENT}-'): + if p.startswith(f'{constants.GOLANG_BUILDER_BREW_COMPONENT}-'): go_version = p break else: @@ -602,7 +602,7 @@ def get_golang_container_nvrs_for_konflux_record( break elif spec.startswith('art-images:golang-builder-'): go_version = spec.replace( - 'art-images:golang-builder-', f'{constants.GOLANG_BUILDER_CVE_COMPONENT}-' + 'art-images:golang-builder-', f'{constants.GOLANG_BUILDER_BREW_COMPONENT}-' ) break diff --git a/elliott/tests/test_bzutil.py b/elliott/tests/test_bzutil.py index af6413825d..522d347c95 100644 --- a/elliott/tests/test_bzutil.py +++ b/elliott/tests/test_bzutil.py @@ -740,36 +740,36 @@ def test_whiteboard_component_normalizes_duplicated_component(self): flexmock( key=1, fields=flexmock( - labels=["pscomponent:openshift-golang-builder-container/openshift-golang-builder-container"], + labels=["pscomponent:some-component/some-component"], issuetype=flexmock(name="Bug"), ), ) ) - self.assertEqual(bug.whiteboard_component, "openshift-golang-builder-container") + self.assertEqual(bug.whiteboard_component, "some-component") # Non-duplicated component with slash should be left alone bug = JIRABug( flexmock( key=2, fields=flexmock( - labels=["pscomponent:openshift4/ose-cli"], + labels=["pscomponent:openshift4/openshift-golang-builder"], issuetype=flexmock(name="Bug"), ), ) ) - self.assertEqual(bug.whiteboard_component, "openshift4/ose-cli") + self.assertEqual(bug.whiteboard_component, "openshift4/openshift-golang-builder") # Component without slash should be unaffected bug = JIRABug( flexmock( key=3, fields=flexmock( - labels=["pscomponent:openshift-golang-builder-container"], + labels=["pscomponent:openshift-clients"], issuetype=flexmock(name="Bug"), ), ) ) - self.assertEqual(bug.whiteboard_component, "openshift-golang-builder-container") + self.assertEqual(bug.whiteboard_component, "openshift-clients") class TestBugzillaBug(unittest.TestCase): diff --git a/elliott/tests/test_find_bugs_golang_cli.py b/elliott/tests/test_find_bugs_golang_cli.py index 53f47ac0ba..a806865c38 100644 --- a/elliott/tests/test_find_bugs_golang_cli.py +++ b/elliott/tests/test_find_bugs_golang_cli.py @@ -28,7 +28,7 @@ def _make_cli(**overrides): return FindBugsGolangCli(**defaults) -def _make_bug(bug_id="OCPBUGS-99999", component="openshift-golang-builder-container"): +def _make_bug(bug_id="OCPBUGS-99999", component=constants.GOLANG_BUILDER_CVE_COMPONENT): bug = MagicMock() bug.id = bug_id bug.whiteboard_component = component diff --git a/pyartcd/pyartcd/pipelines/update_golang.py b/pyartcd/pyartcd/pipelines/update_golang.py index bd1021217f..7479109517 100644 --- a/pyartcd/pyartcd/pipelines/update_golang.py +++ b/pyartcd/pyartcd/pipelines/update_golang.py @@ -24,7 +24,7 @@ from artcommonlib.util import new_roundtrip_yaml_handler from doozerlib.constants import ART_IMAGES_BASE_APPLICATION from elliottlib import util as elliottutil -from elliottlib.constants import GOLANG_BUILDER_CVE_COMPONENT +from elliottlib.constants import GOLANG_BUILDER_BREW_COMPONENT, GOLANG_BUILDER_CVE_COMPONENT from pyartcd import constants, jenkins from pyartcd.cli import cli, click_coroutine, pass_runtime @@ -532,7 +532,7 @@ async def tag_build(self, el_v, nvr): await self._slack_client.say_in_thread(f"Tagged {build} with {build_tag} tag") def get_existing_builders_brew(self, el_nvr_map, go_version): - component = GOLANG_BUILDER_CVE_COMPONENT + component = GOLANG_BUILDER_BREW_COMPONENT _LOGGER.info(f"Checking if {component} builds exist in Brew for given golang builds") package_info = self.koji_session.getPackage(component) if not package_info: @@ -574,7 +574,7 @@ async def get_existing_builders_konflux( _LOGGER.info(f"Checking if {GOLANG_BUILDER_IMAGE_NAME} builds exist in Konflux for given golang builds") builder_records: dict[int, KonfluxBuildRecord] = {} - extra_patterns = {'nvr': f"{GOLANG_BUILDER_CVE_COMPONENT}-v{go_version}"} + extra_patterns = {'nvr': f"{GOLANG_BUILDER_BREW_COMPONENT}-v{go_version}"} build_records = await asyncio.gather( *( anext( @@ -624,8 +624,8 @@ def _get_builder_pullspec(self, builder_nvr: str): parsed_nvr = parse_nvr(builder_nvr) component_name = parsed_nvr["name"] if component_name == GOLANG_BUILDER_IMAGE_NAME: - component_name = GOLANG_BUILDER_CVE_COMPONENT - elif component_name != GOLANG_BUILDER_CVE_COMPONENT: + component_name = GOLANG_BUILDER_BREW_COMPONENT + elif component_name != GOLANG_BUILDER_BREW_COMPONENT: raise ValueError(f"Expected a golang builder image NVR, got: {builder_nvr}") published_nvr = f'{component_name}-{parsed_nvr["version"]}-{parsed_nvr["release"]}' return f'{PUBLISHED_GOLANG_BUILDER_REPO}:{published_nvr}' diff --git a/pyartcd/tests/pipelines/test_update_golang.py b/pyartcd/tests/pipelines/test_update_golang.py index 034f388915..99acd3ccca 100644 --- a/pyartcd/tests/pipelines/test_update_golang.py +++ b/pyartcd/tests/pipelines/test_update_golang.py @@ -225,7 +225,7 @@ async def test_move_golang_bugs_with_cves(self, mock_cmd_assert): ocp_version="4.16", cves=["CVE-2024-1234", "CVE-2024-5678"], nvrs=["golang-1.20.12-2.el8"], - components=["openshift-golang-builder-container"], + components=["openshift4/openshift-golang-builder"], force_update_tracker=False, dry_run=False, ) @@ -246,7 +246,7 @@ async def test_move_golang_bugs_with_cves(self, mock_cmd_assert): "--fixed-in-nvr", "golang-1.20.12-2.el8", "--component", - "openshift-golang-builder-container", + "openshift4/openshift-golang-builder", ] mock_cmd_assert.assert_called_once_with(expected_cmd, log_stdout=True) From bf89fe6c179b109dd3337ea75f76dba7fbd65ec4 Mon Sep 17 00:00:00 2001 From: Ashwin Das Date: Tue, 9 Jun 2026 14:00:09 -0400 Subject: [PATCH 2/6] Fix migration gap and is_first_fix mismatch for golang builder rename Add is_golang_builder_component() helper to accept both old (openshift-golang-builder-container) and new (openshift4/openshift-golang-builder) pscomponent names during migration. Replace 8 equality checks across find_bugs_golang, attach_cve_flaws, and find_bugs_sweep CLIs with the helper. Fix is_first_fix_for_tracker/is_first_fix_any to normalize the tracker whiteboard_component via normalize_component_by_ocp_delivery_repo before comparing against unfixed_components from Hydra. Add fallback in get_cve_unfixed_components to preserve the raw delivery-repo name when translation fails, so trackers using that pscomponent still match. Co-authored-by: Cursor rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- elliott/elliottlib/bzutil.py | 8 +- .../elliottlib/cli/attach_cve_flaws_cli.py | 4 +- .../elliottlib/cli/find_bugs_golang_cli.py | 12 +-- elliott/elliottlib/cli/find_bugs_sweep_cli.py | 2 +- elliott/elliottlib/constants.py | 5 ++ elliott/tests/test_bzutil.py | 85 +++++++++++++++++++ 6 files changed, 105 insertions(+), 11 deletions(-) diff --git a/elliott/elliottlib/bzutil.py b/elliott/elliottlib/bzutil.py index 5be280342f..7b8bbd1f8c 100644 --- a/elliott/elliottlib/bzutil.py +++ b/elliott/elliottlib/bzutil.py @@ -35,6 +35,7 @@ chunk, get_component_by_delivery_repo, isolate_timestamp_in_release, + normalize_component_by_ocp_delivery_repo, ) logger = logutil.get_logger(__name__) @@ -1730,6 +1731,7 @@ def get_cve_unfixed_components(runtime, cve_alias: str) -> Dict: logger.warning( f"Could not find component name for {pkg_name}! is it an art component? is the delivery repo defined?" ) + unfixed_components.append(pkg_name) else: unfixed_components.append(comp_name) else: @@ -1751,7 +1753,8 @@ def is_first_fix_for_tracker(runtime, flaw_bug: BugzillaBug, tracker_bug: JIRABu ) alias = flaw_bug.alias[0] unfixed_components = get_cve_unfixed_components(runtime, alias) - first_fix = tracker_bug.whiteboard_component in unfixed_components + tracker_component = normalize_component_by_ocp_delivery_repo(runtime, tracker_bug.whiteboard_component) + first_fix = tracker_component in unfixed_components logger.info( f"Flaw bug {flaw_bug.id} is {'first-fix' if first_fix else 'second-fix'} for tracker bug {tracker_bug.id}" ) @@ -1781,7 +1784,8 @@ def is_first_fix_any(runtime, flaw_bug: BugzillaBug, tracker_bugs: Iterable[JIRA # and if not then second-fix close operation should be performed on them # but for now we will just return True if any of the trackers is a first fix for tracker_bug in tracker_bugs: - if tracker_bug.whiteboard_component in unfixed_components: + tracker_component = normalize_component_by_ocp_delivery_repo(runtime, tracker_bug.whiteboard_component) + if tracker_component in unfixed_components: logger.info( f"Flaw bug {flaw_bug.id} is a first-fix for tracker bug {tracker_bug.id}. Other associated trackers of the flaw bug will not be checked." ) diff --git a/elliott/elliottlib/cli/attach_cve_flaws_cli.py b/elliott/elliottlib/cli/attach_cve_flaws_cli.py index 6a61959e73..98bf1426fd 100644 --- a/elliott/elliottlib/cli/attach_cve_flaws_cli.py +++ b/elliott/elliottlib/cli/attach_cve_flaws_cli.py @@ -537,7 +537,7 @@ def get_cve_component_mapping( raise ValueError(f"Bug {tracker.id} doesn't have a valid whiteboard component.") whiteboard_component = tracker.whiteboard_component - is_golang_builder = whiteboard_component == constants.GOLANG_BUILDER_CVE_COMPONENT + is_golang_builder = constants.is_golang_builder_component(whiteboard_component) if is_ocp_delivery_repo(whiteboard_component) and not is_golang_builder: # this means the component here is the delivery repo name # we need to translate it to build component name @@ -554,7 +554,7 @@ def get_cve_component_mapping( ) if not new_component: # Special case for builder containers: they should map to all components that use this builder - if is_golang_builder or whiteboard_component == constants.GOLANG_BUILDER_BREW_COMPONENT: + if is_golang_builder: # Check which components actually use the golang builder logger.info(f"Processing builder container CVE for '{whiteboard_component}' (golang builder)") diff --git a/elliott/elliottlib/cli/find_bugs_golang_cli.py b/elliott/elliottlib/cli/find_bugs_golang_cli.py index fcae5a62a2..46b4b2d00c 100644 --- a/elliott/elliottlib/cli/find_bugs_golang_cli.py +++ b/elliott/elliottlib/cli/find_bugs_golang_cli.py @@ -210,13 +210,13 @@ def _is_fixed(self, bug: JIRABug, tracker_fixed_in: Set[Version], go_nvr_map) -> # In case this is for builder image # and if vulnerable builds make up for less than 10% of total builds, consider it fixed # this is due to etcd and a few payload images lagging behind due to special reasons - if bug.whiteboard_component == constants.GOLANG_BUILDER_CVE_COMPONENT and vuln_builds / total_builds < 0.1: + if constants.is_golang_builder_component(bug.whiteboard_component) and vuln_builds / total_builds < 0.1: self._logger.info("Vulnerable builds make up for less than 10% of total builds, considering it fixed") fixed = True else: fixed = True - if bug.whiteboard_component == constants.GOLANG_BUILDER_CVE_COMPONENT: + if constants.is_golang_builder_component(bug.whiteboard_component): build_artifacts = f"Images in {self.pullspec}" else: nvrs = [] @@ -486,7 +486,7 @@ def is_valid(b: JIRABug): if comp in not_art: logger.info(f"{b.id} is for a component that is not built by ART: {comp}. Skipping") return False - if comp.endswith("-container") and comp != constants.GOLANG_BUILDER_CVE_COMPONENT: + if comp.endswith("-container") and not constants.is_golang_builder_component(comp): logger.info(f"{b.id} is for a non-builder image: {comp}. Skipping") return False return True @@ -510,7 +510,7 @@ def is_valid(b: JIRABug): filtered_bugs_rpms_only = [] for b in bugs: # Skip builder container bugs - if b.whiteboard_component == constants.GOLANG_BUILDER_CVE_COMPONENT: + if constants.is_golang_builder_component(b.whiteboard_component): continue # Skip image components (those that start with "openshift/") if re.match(r'^openshift\d+/', b.whiteboard_component): @@ -648,7 +648,7 @@ def compare(b1, b2): fixed_bugs, unfixed_bugs, updated_bugs = [], [], [] for bug in bugs: component = bug.whiteboard_component - art_managed = (component == constants.GOLANG_BUILDER_CVE_COMPONENT) or (component in self._runtime.rpm_map) + art_managed = constants.is_golang_builder_component(component) or (component in self._runtime.rpm_map) logger.info(f"{bug.id} has security component: {component}") fixed, comment, component_builds, parent_golang_builds = False, '', [], [] @@ -658,7 +658,7 @@ def compare(b1, b2): continue logger.info(f"{bug.id} is fixed in: {[str(v) for v in tracker_fixed_in]}") - if component == constants.GOLANG_BUILDER_CVE_COMPONENT: + if constants.is_golang_builder_component(component): fixed, comment, component_builds, parent_golang_builds = await self.is_fixed_golang_builder( bug, tracker_fixed_in=tracker_fixed_in ) diff --git a/elliott/elliottlib/cli/find_bugs_sweep_cli.py b/elliott/elliottlib/cli/find_bugs_sweep_cli.py index b721736b1a..9525d3b055 100644 --- a/elliott/elliottlib/cli/find_bugs_sweep_cli.py +++ b/elliott/elliottlib/cli/find_bugs_sweep_cli.py @@ -96,7 +96,7 @@ def filter_art_managed_jira_trackers( continue if ( - normalized_component == constants.GOLANG_BUILDER_CVE_COMPONENT + constants.is_golang_builder_component(normalized_component) or normalized_component in art_managed_image_components ): art_trackers.append(bug) diff --git a/elliott/elliottlib/constants.py b/elliott/elliottlib/constants.py index 54877082cb..5ef32a2582 100644 --- a/elliott/elliottlib/constants.py +++ b/elliott/elliottlib/constants.py @@ -25,6 +25,11 @@ GOLANG_BUILDER_CVE_COMPONENT = 'openshift4/openshift-golang-builder' GOLANG_BUILDER_BREW_COMPONENT = 'openshift-golang-builder-container' + +def is_golang_builder_component(component: str) -> bool: + """Accept both old (Brew) and new (JIRA) pscomponent names during migration.""" + return component in (GOLANG_BUILDER_CVE_COMPONENT, GOLANG_BUILDER_BREW_COMPONENT) + BUG_LOOKUP_CHUNK_SIZE = 100 BUG_ATTACH_CHUNK_SIZE = 100 diff --git a/elliott/tests/test_bzutil.py b/elliott/tests/test_bzutil.py index 522d347c95..c2c7a3ab40 100644 --- a/elliott/tests/test_bzutil.py +++ b/elliott/tests/test_bzutil.py @@ -1344,6 +1344,91 @@ def test_is_first_fix_any_false(self): actual = bzutil.is_first_fix_any(mock_runtime, flaw_bug, tracker_bugs) self.assertEqual(expected, actual) + def test_is_first_fix_normalizes_delivery_repo_tracker(self): + """When tracker has delivery-repo pscomponent (openshift4/foo) and Hydra + returns the same name resolved to its Brew component, normalization + should make them match.""" + mock_runtime = flexmock() + mock_runtime.should_receive("get_major_minor").and_return((4, 17)) + + mock_meta = flexmock() + mock_config = flexmock() + mock_delivery = flexmock() + mock_delivery.delivery_repo_names = ["openshift4/ose-cli"] + mock_config.delivery = mock_delivery + mock_meta.config = mock_config + mock_meta.should_receive("get_component_name").and_return("ose-cli-container") + + mock_runtime.should_receive("image_metas").and_return([mock_meta]) + + hydra_data = { + "package_state": [ + { + "product_name": "Red Hat OpenShift Container Platform 4", + "fix_state": "Affected", + "package_name": "openshift4/ose-cli", + }, + ], + } + flexmock(requests).should_receive("get").and_return( + flexmock(json=lambda: hydra_data, raise_for_status=lambda: None) + ) + + flaw_bug = BugzillaBug(flexmock(id=1, alias=["CVE-2024-999"])) + tracker_bugs = [flexmock(id=2, whiteboard_component="openshift4/ose-cli")] + actual = bzutil.is_first_fix_any(mock_runtime, flaw_bug, tracker_bugs) + self.assertTrue(actual) + + def test_is_first_fix_fallback_when_delivery_repo_unresolved(self): + """When Hydra returns a delivery-repo name that can't be translated, + the raw name should be kept in unfixed_components so trackers using + that same delivery-repo pscomponent still match.""" + mock_runtime = flexmock() + mock_runtime.should_receive("get_major_minor").and_return((4, 17)) + + # Provide a dummy image meta that does NOT match the golang builder, + # so get_component_by_delivery_repo returns None instead of raising. + dummy_meta = flexmock() + dummy_config = flexmock() + dummy_delivery = flexmock() + dummy_delivery.delivery_repo_names = ["openshift4/unrelated-image"] + dummy_config.delivery = dummy_delivery + dummy_meta.config = dummy_config + dummy_meta.should_receive("get_component_name").and_return("unrelated-container") + mock_runtime.should_receive("image_metas").and_return([dummy_meta]) + + hydra_data = { + "package_state": [ + { + "product_name": "Red Hat OpenShift Container Platform 4", + "fix_state": "Affected", + "package_name": "openshift4/openshift-golang-builder", + }, + ], + } + flexmock(requests).should_receive("get").and_return( + flexmock(json=lambda: hydra_data, raise_for_status=lambda: None) + ) + + flaw_bug = BugzillaBug(flexmock(id=1, alias=["CVE-2024-888"])) + tracker_bugs = [flexmock(id=2, whiteboard_component="openshift4/openshift-golang-builder")] + actual = bzutil.is_first_fix_any(mock_runtime, flaw_bug, tracker_bugs) + self.assertTrue(actual) + + +class TestIsGolangBuilderComponent(unittest.TestCase): + def test_matches_cve_component(self): + self.assertTrue(constants.is_golang_builder_component(constants.GOLANG_BUILDER_CVE_COMPONENT)) + + def test_matches_brew_component(self): + self.assertTrue(constants.is_golang_builder_component(constants.GOLANG_BUILDER_BREW_COMPONENT)) + + def test_rejects_unrelated(self): + self.assertFalse(constants.is_golang_builder_component("some-other-container")) + + def test_rejects_empty(self): + self.assertFalse(constants.is_golang_builder_component("")) + class TestSearchFilter(unittest.TestCase): def test_search_filter(self): From 639d44286c83b02ff6c33e76a754271a3d960dda Mon Sep 17 00:00:00 2001 From: Ashwin Das Date: Tue, 9 Jun 2026 14:37:02 -0400 Subject: [PATCH 3/6] Fix ruff formatting in constants.py Co-authored-by: Cursor rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- elliott/elliottlib/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/elliott/elliottlib/constants.py b/elliott/elliottlib/constants.py index 5ef32a2582..bd92843016 100644 --- a/elliott/elliottlib/constants.py +++ b/elliott/elliottlib/constants.py @@ -30,6 +30,7 @@ def is_golang_builder_component(component: str) -> bool: """Accept both old (Brew) and new (JIRA) pscomponent names during migration.""" return component in (GOLANG_BUILDER_CVE_COMPONENT, GOLANG_BUILDER_BREW_COMPONENT) + BUG_LOOKUP_CHUNK_SIZE = 100 BUG_ATTACH_CHUNK_SIZE = 100 From ed7c7fc7aa5e858f377b5f3fcbe4d35760982753 Mon Sep 17 00:00:00 2001 From: Ashwin Das Date: Tue, 9 Jun 2026 15:19:37 -0400 Subject: [PATCH 4/6] Filter delivery-repo-format image trackers in is_valid The is_valid filter only skipped image trackers ending with -container, but with the transition to delivery-repo pscomponents, non-builder image trackers like openshift4/ would slip through. Add is_ocp_delivery_repo check to also skip those, preserving the golang builder exemption. Co-authored-by: Cursor rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- elliott/elliottlib/cli/find_bugs_golang_cli.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/elliott/elliottlib/cli/find_bugs_golang_cli.py b/elliott/elliottlib/cli/find_bugs_golang_cli.py index 46b4b2d00c..8a60db2901 100644 --- a/elliott/elliottlib/cli/find_bugs_golang_cli.py +++ b/elliott/elliottlib/cli/find_bugs_golang_cli.py @@ -9,6 +9,7 @@ from artcommonlib.release_util import get_patch_from_release, isolate_el_version_in_release from artcommonlib.rhcos import get_container_configs from artcommonlib.rpm_utils import parse_nvr +from artcommonlib.util import is_ocp_delivery_repo from doozerlib.cli.get_nightlies import find_rc_nightlies from prettytable import PrettyTable from pyartcd.util import get_release_name_for_assembly @@ -486,7 +487,9 @@ def is_valid(b: JIRABug): if comp in not_art: logger.info(f"{b.id} is for a component that is not built by ART: {comp}. Skipping") return False - if comp.endswith("-container") and not constants.is_golang_builder_component(comp): + if ( + comp.endswith("-container") or is_ocp_delivery_repo(comp) + ) and not constants.is_golang_builder_component(comp): logger.info(f"{b.id} is for a non-builder image: {comp}. Skipping") return False return True From 80b30a91e05cadfaebb78baf857da2adfff2241d Mon Sep 17 00:00:00 2001 From: Ashwin Das Date: Tue, 9 Jun 2026 21:22:30 -0400 Subject: [PATCH 5/6] Rename golang builder constants per review feedback Rename GOLANG_BUILDER_BREW_COMPONENT to GOLANG_BUILDER_COMPONENT since Konflux computes component names the same way as Brew. Rename GOLANG_BUILDER_CVE_COMPONENT to GOLANG_BUILDER_OCP4_DELIVERY_REPO to clarify it is the OCP4 delivery repo name. Add is_golang_builder_delivery_repo() for OCP4/OCP5 forward-compatibility. Co-authored-by: Cursor rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- elliott/elliottlib/cli/find_bugs_golang_cli.py | 2 +- elliott/elliottlib/cli/find_bugs_sweep_cli.py | 4 ++-- elliott/elliottlib/constants.py | 12 ++++++++---- elliott/elliottlib/errata_async.py | 8 ++++---- elliott/elliottlib/util.py | 8 +++----- elliott/tests/test_bzutil.py | 4 ++-- elliott/tests/test_errata_async.py | 4 ++-- elliott/tests/test_find_bugs_golang_cli.py | 4 ++-- elliott/tests/test_find_bugs_sweep_cli.py | 2 +- pyartcd/pyartcd/pipelines/update_golang.py | 12 ++++++------ 10 files changed, 31 insertions(+), 29 deletions(-) diff --git a/elliott/elliottlib/cli/find_bugs_golang_cli.py b/elliott/elliottlib/cli/find_bugs_golang_cli.py index 8a60db2901..828ec0c8f0 100644 --- a/elliott/elliottlib/cli/find_bugs_golang_cli.py +++ b/elliott/elliottlib/cli/find_bugs_golang_cli.py @@ -163,7 +163,7 @@ def _is_fixed(self, bug: JIRABug, tracker_fixed_in: Set[Version], go_nvr_map) -> for go_build in go_nvr_map.keys(): # if this is a builder image then fetch the golang rpm parent_go_build = go_build - if constants.GOLANG_BUILDER_BREW_COMPONENT in go_build: + if constants.GOLANG_BUILDER_COMPONENT in go_build: parsed_nvr = parse_nvr(go_build) go_builder_nvr_map = get_golang_container_nvrs( [(parsed_nvr['name'], parsed_nvr['version'], parsed_nvr['release'])], self._logger, exact=True diff --git a/elliott/elliottlib/cli/find_bugs_sweep_cli.py b/elliott/elliottlib/cli/find_bugs_sweep_cli.py index 9525d3b055..8f81717ce2 100644 --- a/elliott/elliottlib/cli/find_bugs_sweep_cli.py +++ b/elliott/elliottlib/cli/find_bugs_sweep_cli.py @@ -614,8 +614,8 @@ def categorize_bugs_by_type( if kind == 'image': # golang builder is a special tracker component # which applies to all our golang images - exception_packages.append(constants.GOLANG_BUILDER_CVE_COMPONENT) - exception_packages.append(constants.GOLANG_BUILDER_BREW_COMPONENT) + exception_packages.append(constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO) + exception_packages.append(constants.GOLANG_BUILDER_COMPONENT) for bug in tracker_bugs: package_name = bug.whiteboard_component diff --git a/elliott/elliottlib/constants.py b/elliott/elliottlib/constants.py index bd92843016..d5b198b308 100644 --- a/elliott/elliottlib/constants.py +++ b/elliott/elliottlib/constants.py @@ -22,13 +22,17 @@ } # Golang builder needs special treatment when associating security tracking bugs with builds: -GOLANG_BUILDER_CVE_COMPONENT = 'openshift4/openshift-golang-builder' -GOLANG_BUILDER_BREW_COMPONENT = 'openshift-golang-builder-container' +GOLANG_BUILDER_COMPONENT = 'openshift-golang-builder-container' +GOLANG_BUILDER_OCP4_DELIVERY_REPO = 'openshift4/openshift-golang-builder' + + +def is_golang_builder_delivery_repo(name: str) -> bool: + return name in (GOLANG_BUILDER_OCP4_DELIVERY_REPO,) def is_golang_builder_component(component: str) -> bool: - """Accept both old (Brew) and new (JIRA) pscomponent names during migration.""" - return component in (GOLANG_BUILDER_CVE_COMPONENT, GOLANG_BUILDER_BREW_COMPONENT) + """Accept both component name and delivery repo names during migration.""" + return component == GOLANG_BUILDER_COMPONENT or is_golang_builder_delivery_repo(component) BUG_LOOKUP_CHUNK_SIZE = 100 diff --git a/elliott/elliottlib/errata_async.py b/elliott/elliottlib/errata_async.py index 836f55b9ef..d584234fb0 100644 --- a/elliott/elliottlib/errata_async.py +++ b/elliott/elliottlib/errata_async.py @@ -380,13 +380,13 @@ def compute_cve_exclusions(cls, attached_builds: Iterable[str], expected_cve_com # separate out golang CVEs and non-golang CVEs # expected_brew_components contain non-golang CVEs components for regular analysis - # All golang CVEs will have component as constants.GOLANG_BUILDER_CVE_COMPONENT + # All golang CVEs will have component as constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO # which we do not attach to our advisories since it's a builder image # It requires special treatment expected_brew_components = set() golang_cve_names = set() for cve_name, components in expected_cve_components.items(): - if constants.GOLANG_BUILDER_CVE_COMPONENT not in components: + if constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO not in components: expected_brew_components.update(components) else: golang_cve_names.add(cve_name) @@ -417,10 +417,10 @@ def populate_golang_cve_components(cls, golang_cve_names, expected_cve_component builder_nvr = parse_nvr(builder_nvr_string) # Make sure they are go builder nvrs (this should never happen) - if builder_nvr['name'] != constants.GOLANG_BUILDER_BREW_COMPONENT: + if builder_nvr['name'] != constants.GOLANG_BUILDER_COMPONENT: raise ValueError( f"Unexpected `name` value for nvr {builder_nvr}. Expected " - f"{constants.GOLANG_BUILDER_BREW_COMPONENT}. Please investigate." + f"{constants.GOLANG_BUILDER_COMPONENT}. Please investigate." ) if 'etcd' in list(go_nvr_map[builder_nvr_string])[0]: diff --git a/elliott/elliottlib/util.py b/elliott/elliottlib/util.py index f09b33a9a6..879d5bb89a 100644 --- a/elliott/elliottlib/util.py +++ b/elliott/elliottlib/util.py @@ -488,7 +488,7 @@ def get_golang_container_nvrs_brew(nvrs: List[Tuple[str, str, str]], logger, exa logger.error(f'Error parsing {build}') raise name = nvr[0] - if name == constants.GOLANG_BUILDER_BREW_COMPONENT or 'go-toolset' in name: + if name == constants.GOLANG_BUILDER_COMPONENT or 'go-toolset' in name: go_version = get_parent_golang_from_brew(nvr) if not go_version: raise ValueError(f'Could not find go version for golang builder {nvr}') @@ -591,7 +591,7 @@ def get_golang_container_nvrs_for_konflux_record( # NVRs never contain '/', so we can distinguish them from pullspecs. if '/' not in p: # This is an NVR - look for golang builder (e.g. openshift-golang-builder-container-v1.24.4-...) - if p.startswith(f'{constants.GOLANG_BUILDER_BREW_COMPONENT}-'): + if p.startswith(f'{constants.GOLANG_BUILDER_COMPONENT}-'): go_version = p break else: @@ -601,9 +601,7 @@ def get_golang_container_nvrs_for_konflux_record( go_version = spec.replace(':', '-container-') break elif spec.startswith('art-images:golang-builder-'): - go_version = spec.replace( - 'art-images:golang-builder-', f'{constants.GOLANG_BUILDER_BREW_COMPONENT}-' - ) + go_version = spec.replace('art-images:golang-builder-', f'{constants.GOLANG_BUILDER_COMPONENT}-') break if not go_version: diff --git a/elliott/tests/test_bzutil.py b/elliott/tests/test_bzutil.py index c2c7a3ab40..9d031952e3 100644 --- a/elliott/tests/test_bzutil.py +++ b/elliott/tests/test_bzutil.py @@ -1418,10 +1418,10 @@ def test_is_first_fix_fallback_when_delivery_repo_unresolved(self): class TestIsGolangBuilderComponent(unittest.TestCase): def test_matches_cve_component(self): - self.assertTrue(constants.is_golang_builder_component(constants.GOLANG_BUILDER_CVE_COMPONENT)) + self.assertTrue(constants.is_golang_builder_component(constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO)) def test_matches_brew_component(self): - self.assertTrue(constants.is_golang_builder_component(constants.GOLANG_BUILDER_BREW_COMPONENT)) + self.assertTrue(constants.is_golang_builder_component(constants.GOLANG_BUILDER_COMPONENT)) def test_rejects_unrelated(self): self.assertFalse(constants.is_golang_builder_component("some-other-container")) diff --git a/elliott/tests/test_errata_async.py b/elliott/tests/test_errata_async.py index 6e1ffcb845..2d0421081a 100644 --- a/elliott/tests/test_errata_async.py +++ b/elliott/tests/test_errata_async.py @@ -162,7 +162,7 @@ def test_populate_golang_cve_components(self, get_go_container_nvrs: Mock): expected_cve_components = { "CVE-2099-1": {"a", "b"}, "CVE-2099-2": {"c"}, - "CVE-2099-3": {constants.GOLANG_BUILDER_CVE_COMPONENT}, + "CVE-2099-3": {constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO}, } attached_builds = ["a-1.0.0-1.el8", "a-1.0.0-1.el9", "b-1.0.0-1.el9", "c-1.0.0-1.el8", "d-1.0.0-1.el8"] builder_el8 = 'openshift-golang-builder-container-v1.18.0-202204191948.sha1patch.el8.g4d4caca' @@ -190,7 +190,7 @@ def test_compute_cve_exclusions(self, populate_golang_cve: Mock): cve_components = { "CVE-2099-1": {"a", "b"}, "CVE-2099-2": {"c"}, - "CVE-2099-3": {constants.GOLANG_BUILDER_CVE_COMPONENT}, + "CVE-2099-3": {constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO}, } attached_builds = ["a-1.0.0-1.el8", "a-1.0.0-1.el7", "b-1.0.0-1.el8", "c-1.0.0-1.el8", "d-1.0.0-1.el8"] populate_golang_cve.return_value = { diff --git a/elliott/tests/test_find_bugs_golang_cli.py b/elliott/tests/test_find_bugs_golang_cli.py index a806865c38..ade1f98398 100644 --- a/elliott/tests/test_find_bugs_golang_cli.py +++ b/elliott/tests/test_find_bugs_golang_cli.py @@ -28,7 +28,7 @@ def _make_cli(**overrides): return FindBugsGolangCli(**defaults) -def _make_bug(bug_id="OCPBUGS-99999", component=constants.GOLANG_BUILDER_CVE_COMPONENT): +def _make_bug(bug_id="OCPBUGS-99999", component=constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO): bug = MagicMock() bug.id = bug_id bug.whiteboard_component = component @@ -103,7 +103,7 @@ def test_different_major_minor_not_compared(self, mock_errata): def test_builder_container_10pct_threshold(self, mock_get_golang, mock_errata): mock_get_golang.return_value = {"golang-1.22.12-11.el9": []} cli = _make_cli() - bug = _make_bug(component=constants.GOLANG_BUILDER_CVE_COMPONENT) + bug = _make_bug(component=constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO) tracker_fixed_in = {Version.parse("1.22.12-11"), Version.parse("1.22.12-13")} go_nvr_map = { "golang-1.22.12-11.el9": [("img", "1.0", "1.el9")] * 173, diff --git a/elliott/tests/test_find_bugs_sweep_cli.py b/elliott/tests/test_find_bugs_sweep_cli.py index 189637839e..88953e17eb 100644 --- a/elliott/tests/test_find_bugs_sweep_cli.py +++ b/elliott/tests/test_find_bugs_sweep_cli.py @@ -64,7 +64,7 @@ def test_filter_art_managed_jira_trackers(self): builder_tracker = flexmock( id="OCPBUGS-5", is_tracker_bug=lambda: True, - whiteboard_component=constants.GOLANG_BUILDER_CVE_COMPONENT, + whiteboard_component=constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO, ) delivery_repo_tracker = flexmock( id="OCPBUGS-6", is_tracker_bug=lambda: True, whiteboard_component="openshift5/payload-image-rhel9" diff --git a/pyartcd/pyartcd/pipelines/update_golang.py b/pyartcd/pyartcd/pipelines/update_golang.py index 7479109517..10b01d35d9 100644 --- a/pyartcd/pyartcd/pipelines/update_golang.py +++ b/pyartcd/pyartcd/pipelines/update_golang.py @@ -24,7 +24,7 @@ from artcommonlib.util import new_roundtrip_yaml_handler from doozerlib.constants import ART_IMAGES_BASE_APPLICATION from elliottlib import util as elliottutil -from elliottlib.constants import GOLANG_BUILDER_BREW_COMPONENT, GOLANG_BUILDER_CVE_COMPONENT +from elliottlib.constants import GOLANG_BUILDER_COMPONENT, GOLANG_BUILDER_OCP4_DELIVERY_REPO from pyartcd import constants, jenkins from pyartcd.cli import cli, click_coroutine, pass_runtime @@ -470,7 +470,7 @@ async def run(self): ocp_version=self.ocp_version, cves=self.cves, nvrs=self.go_nvrs if self.cves else None, - components=[GOLANG_BUILDER_CVE_COMPONENT], + components=[GOLANG_BUILDER_OCP4_DELIVERY_REPO], force_update_tracker=self.force_update_tracker, dry_run=self.dry_run, ) @@ -532,7 +532,7 @@ async def tag_build(self, el_v, nvr): await self._slack_client.say_in_thread(f"Tagged {build} with {build_tag} tag") def get_existing_builders_brew(self, el_nvr_map, go_version): - component = GOLANG_BUILDER_BREW_COMPONENT + component = GOLANG_BUILDER_COMPONENT _LOGGER.info(f"Checking if {component} builds exist in Brew for given golang builds") package_info = self.koji_session.getPackage(component) if not package_info: @@ -574,7 +574,7 @@ async def get_existing_builders_konflux( _LOGGER.info(f"Checking if {GOLANG_BUILDER_IMAGE_NAME} builds exist in Konflux for given golang builds") builder_records: dict[int, KonfluxBuildRecord] = {} - extra_patterns = {'nvr': f"{GOLANG_BUILDER_BREW_COMPONENT}-v{go_version}"} + extra_patterns = {'nvr': f"{GOLANG_BUILDER_COMPONENT}-v{go_version}"} build_records = await asyncio.gather( *( anext( @@ -624,8 +624,8 @@ def _get_builder_pullspec(self, builder_nvr: str): parsed_nvr = parse_nvr(builder_nvr) component_name = parsed_nvr["name"] if component_name == GOLANG_BUILDER_IMAGE_NAME: - component_name = GOLANG_BUILDER_BREW_COMPONENT - elif component_name != GOLANG_BUILDER_BREW_COMPONENT: + component_name = GOLANG_BUILDER_COMPONENT + elif component_name != GOLANG_BUILDER_COMPONENT: raise ValueError(f"Expected a golang builder image NVR, got: {builder_nvr}") published_nvr = f'{component_name}-{parsed_nvr["version"]}-{parsed_nvr["release"]}' return f'{PUBLISHED_GOLANG_BUILDER_REPO}:{published_nvr}' From 12bbef1f9dd6731994ac9c28eef3774323e58418 Mon Sep 17 00:00:00 2001 From: Ashwin Das Date: Tue, 9 Jun 2026 21:37:10 -0400 Subject: [PATCH 6/6] Use is_golang_builder_component helper in compute_cve_exclusions The golang CVE detection was checking for a single constant, missing legacy builder names during migration. Use the helper to match any form of the golang builder component. Co-authored-by: Cursor rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED --- elliott/elliottlib/errata_async.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/elliott/elliottlib/errata_async.py b/elliott/elliottlib/errata_async.py index d584234fb0..d5911bea87 100644 --- a/elliott/elliottlib/errata_async.py +++ b/elliott/elliottlib/errata_async.py @@ -380,16 +380,16 @@ def compute_cve_exclusions(cls, attached_builds: Iterable[str], expected_cve_com # separate out golang CVEs and non-golang CVEs # expected_brew_components contain non-golang CVEs components for regular analysis - # All golang CVEs will have component as constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO + # Golang CVEs have a builder component (delivery repo or legacy name) # which we do not attach to our advisories since it's a builder image # It requires special treatment expected_brew_components = set() golang_cve_names = set() for cve_name, components in expected_cve_components.items(): - if constants.GOLANG_BUILDER_OCP4_DELIVERY_REPO not in components: - expected_brew_components.update(components) - else: + if any(constants.is_golang_builder_component(c) for c in components): golang_cve_names.add(cve_name) + else: + expected_brew_components.update(components) missing_brew_components = expected_brew_components - attached_brew_components if missing_brew_components: