diff --git a/atomic_reactor/config.py b/atomic_reactor/config.py index 58960aba5..b1aa4767f 100644 --- a/atomic_reactor/config.py +++ b/atomic_reactor/config.py @@ -188,6 +188,7 @@ class ReactorConfigKeys(object): IMAGE_SIZE_LIMIT_KEY = 'image_size_limit' BUILDER_CA_BUNDLE_KEY = 'builder_ca_bundle' REMOTE_SOURCES_DEFAULT_VERSION = 'remote_sources_default_version' + ALLOWED_BUILD_TARGETS_KEY = 'allowed_build_targets' class ODCSConfig(object): @@ -516,3 +517,7 @@ def builder_ca_bundle(self): @property def remote_sources_default_version(self): return self._get_value(ReactorConfigKeys.REMOTE_SOURCES_DEFAULT_VERSION, fallback=1) + + @property + def allowed_build_targets(self): + return self._get_value(ReactorConfigKeys.ALLOWED_BUILD_TARGETS_KEY, fallback=[]) diff --git a/atomic_reactor/plugins/check_user_settings.py b/atomic_reactor/plugins/check_user_settings.py index 6be85a88d..6b4484d55 100644 --- a/atomic_reactor/plugins/check_user_settings.py +++ b/atomic_reactor/plugins/check_user_settings.py @@ -6,6 +6,8 @@ of the BSD license. See the LICENSE file for details. """ +from fnmatch import fnmatch + from atomic_reactor.constants import ( PLUGIN_CHECK_USER_SETTINGS, REMOTE_SOURCE_VERSION_SKIP, @@ -135,10 +137,41 @@ def resolve_remote_sources_version(self): self.log.warning("remote_sources_version_result path is not specified, " "result won't be written") + def check_build_target_allowed(self): + """Check if build target is in the allowed list""" + allowed_targets = self.workflow.conf.allowed_build_targets + + # If not configured or empty, all build targets are allowed + if not allowed_targets: + self.log.debug("No build target restrictions configured, all targets allowed") + return + + # Get build target from user params + build_target = self.workflow.user_params.get('koji_target') + if not build_target: + self.log.debug("No koji_target in user_params, skipping build target check") + return + + self.log.info("Build target: %s", build_target) + + # Check if build target matches any allowed pattern + for pattern in allowed_targets: + if fnmatch(build_target, pattern): + self.log.info("Build target '%s' matches allowed pattern '%s'", + build_target, pattern) + return + + # Build target is not allowed + raise RuntimeError( + f"Your build target '{build_target}' is not allow-listed for using OSBS, " + "please contact your OSBS maintainers." + ) + def run(self): """ run the plugin """ + self.check_build_target_allowed() self.dockerfile_checks() self.validate_user_config_files() self.resolve_remote_sources_version() diff --git a/atomic_reactor/schemas/config.json b/atomic_reactor/schemas/config.json index 99fd051da..5bf42c88d 100644 --- a/atomic_reactor/schemas/config.json +++ b/atomic_reactor/schemas/config.json @@ -714,6 +714,13 @@ "type": "number", "minimum": 1, "maximum": 2 + }, + "allowed_build_targets": { + "description": "List of allowed build targets (supports glob pattern matching). If empty or not defined, all build targets are allowed", + "type": "array", + "items": { + "type": "string" + } } }, "definitions": { diff --git a/tests/plugins/test_check_user_settings.py b/tests/plugins/test_check_user_settings.py index 733698985..2e9755495 100644 --- a/tests/plugins/test_check_user_settings.py +++ b/tests/plugins/test_check_user_settings.py @@ -385,3 +385,115 @@ def test_return_skip_when_default_version_configured(self, workflow, source_dir, with open(result_file, "r") as f: assert f.read() == str(REMOTE_SOURCE_VERSION_SKIP) + + +class TestCheckBuildTargetAllowed(object): + """Test the check_build_target_allowed method""" + + def test_no_restrictions_configured(self, workflow, source_dir, caplog): + """All build targets allowed when no restrictions configured""" + runner = mock_env(workflow, source_dir) + workflow.user_params['koji_target'] = 'f40-container-candidate' + # Don't configure allowed_build_targets in reactor config + runner.run() + + assert 'No build target restrictions configured' in caplog.text + + def test_empty_restrictions_list(self, workflow, source_dir, caplog): + """All build targets allowed when restrictions list is empty""" + runner = mock_env(workflow, source_dir) + workflow.user_params['koji_target'] = 'f40-container-candidate' + + mock_reactor_config( + workflow, + dedent(""" + --- + allowed_build_targets: [] + """)) + runner.run() + + assert 'No build target restrictions configured' in caplog.text + + def test_no_koji_target_in_user_params(self, workflow, source_dir, caplog): + """Skip check when no koji_target in user_params""" + runner = mock_env(workflow, source_dir) + + mock_reactor_config( + workflow, + dedent(""" + --- + allowed_build_targets: + - "f40-*" + """)) + runner.run() + + assert 'No koji_target in user_params' in caplog.text + + def test_build_target_matches_exact(self, workflow, source_dir, caplog): + """Build target matches exact allowed pattern""" + runner = mock_env(workflow, source_dir) + workflow.user_params['koji_target'] = 'f40-container-candidate' + + mock_reactor_config( + workflow, + dedent(""" + --- + allowed_build_targets: + - "f40-container-candidate" + - "f39-container-candidate" + """)) + runner.run() + + assert "matches allowed pattern 'f40-container-candidate'" in caplog.text + + def test_build_target_matches_glob_pattern(self, workflow, source_dir, caplog): + """Build target matches glob pattern""" + runner = mock_env(workflow, source_dir) + workflow.user_params['koji_target'] = 'f40-container-candidate' + + mock_reactor_config( + workflow, + dedent(""" + --- + allowed_build_targets: + - "f*-container-candidate" + - "rhel-*-candidate" + """)) + runner.run() + + assert "matches allowed pattern 'f*-container-candidate'" in caplog.text + + def test_build_target_not_allowed(self, workflow, source_dir): + """Build target does not match any allowed pattern""" + runner = mock_env(workflow, source_dir) + workflow.user_params['koji_target'] = 'unknown-target' + + mock_reactor_config( + workflow, + dedent(""" + --- + allowed_build_targets: + - "f*-container-candidate" + - "rhel-*-candidate" + """)) + + with pytest.raises(PluginFailedException) as e: + runner.run() + assert "Your build target 'unknown-target' is not allow-listed" in str(e.value) + assert "please contact your OSBS maintainers" in str(e.value) + + def test_build_target_matches_wildcard(self, workflow, source_dir, caplog): + """Build target matches wildcard pattern""" + runner = mock_env(workflow, source_dir) + workflow.user_params['koji_target'] = 'my-special-target-v2' + + mock_reactor_config( + workflow, + dedent(""" + --- + allowed_build_targets: + - "*" + """)) + runner.run() + + assert "matches allowed pattern '*'" in caplog.text