diff --git a/tests/unit/test_v3.py b/tests/unit/test_v3.py index 02cea64faa..ebe96a8cf8 100644 --- a/tests/unit/test_v3.py +++ b/tests/unit/test_v3.py @@ -5199,7 +5199,7 @@ class TestValidateAllBroken(ZuulTestCase): scheduler_count = 1 def setUp(self): - self.assertRaises(zuul.configloader.ConfigurationSyntaxError, + self.assertRaises(zuul.exceptions.ConfigurationSyntaxError, super().setUp) def test_validate_all_tenants_broken(self): @@ -5223,7 +5223,7 @@ class TestValidateBroken(ZuulTestCase): scheduler_count = 1 def setUp(self): - self.assertRaises(zuul.configloader.ConfigurationSyntaxError, + self.assertRaises(zuul.exceptions.ConfigurationSyntaxError, super().setUp) def test_validate_tenant_broken(self): diff --git a/zuul/configloader.py b/zuul/configloader.py index 5e2d56101c..b7c9bab6f5 100644 --- a/zuul/configloader.py +++ b/zuul/configloader.py @@ -43,6 +43,23 @@ from zuul.lib.re2util import filter_allowed_disallowed, ZuulRegex from zuul.lib.varnames import check_varnames from zuul.zk.components import COMPONENT_REGISTRY from zuul.zk.semaphore import SemaphoreHandler +from zuul.exceptions import ( + SEVERITY_ERROR, + DuplicateGroupError, + DuplicateNodeError, + GlobalSemaphoreNotFoundError, + LabelForbiddenError, + MaxTimeoutError, + MultipleProjectConfigurations, + NodeFromGroupNotFoundError, + PipelineNotPermittedError, + ProjectNotFoundError, + ProjectNotPermittedError, + RegexDeprecation, + TemplateNotFoundError, + UnknownConnection, + YAMLDuplicateKeyError, +) ZUUL_CONF_ROOT = ('zuul.yaml', 'zuul.d', '.zuul.yaml', '.zuul.d') @@ -104,207 +121,6 @@ def indent(s): return '\n'.join([' ' + x for x in s.split('\n')]) -class ConfigurationSyntaxError(Exception): - zuul_error_name = 'Unknown Configuration Error' - zuul_error_severity = model.SEVERITY_ERROR - - -class NodeFromGroupNotFoundError(ConfigurationSyntaxError): - zuul_error_name = 'Node From Group Not Found' - - def __init__(self, nodeset, node, group): - message = textwrap.dedent("""\ - In {nodeset} the group "{group}" contains a - node named "{node}" which is not defined in the nodeset.""") - message = textwrap.fill(message.format(nodeset=nodeset, - node=node, group=group)) - super(NodeFromGroupNotFoundError, self).__init__(message) - - -class DuplicateNodeError(ConfigurationSyntaxError): - zuul_error_name = 'Duplicate Node' - - def __init__(self, nodeset, node): - message = textwrap.dedent("""\ - In nodeset "{nodeset}" the node "{node}" appears multiple times. - Node names must be unique within a nodeset.""") - message = textwrap.fill(message.format(nodeset=nodeset, - node=node)) - super(DuplicateNodeError, self).__init__(message) - - -class UnknownConnection(ConfigurationSyntaxError): - zuul_error_name = 'Unknown Connection' - - def __init__(self, connection_name): - message = textwrap.dedent("""\ - Unknown connection named "{connection}".""") - message = textwrap.fill(message.format(connection=connection_name)) - super(UnknownConnection, self).__init__(message) - - -class LabelForbiddenError(ConfigurationSyntaxError): - zuul_error_name = 'Label Forbidden' - - def __init__(self, label, allowed_labels, disallowed_labels): - message = textwrap.dedent("""\ - Label named "{label}" is not part of the allowed - labels ({allowed_labels}) for this tenant.""") - # Make a string that looks like "a, b and not c, d" if we have - # both allowed and disallowed labels. - labels = ", ".join(allowed_labels or []) - if allowed_labels and disallowed_labels: - labels += ' and ' - if disallowed_labels: - labels += 'not ' - labels += ", ".join(disallowed_labels) - message = textwrap.fill(message.format( - label=label, - allowed_labels=labels)) - super(LabelForbiddenError, self).__init__(message) - - -class MaxTimeoutError(ConfigurationSyntaxError): - zuul_error_name = 'Max Timeout Exceeded' - - def __init__(self, job, tenant): - message = textwrap.dedent("""\ - The job "{job}" exceeds tenant max-job-timeout {maxtimeout}.""") - message = textwrap.fill(message.format( - job=job.name, maxtimeout=tenant.max_job_timeout)) - super(MaxTimeoutError, self).__init__(message) - - -class DuplicateGroupError(ConfigurationSyntaxError): - zuul_error_name = 'Duplicate Nodeset Group' - - def __init__(self, nodeset, group): - message = textwrap.dedent("""\ - In {nodeset} the group "{group}" appears multiple times. - Group names must be unique within a nodeset.""") - message = textwrap.fill(message.format(nodeset=nodeset, - group=group)) - super(DuplicateGroupError, self).__init__(message) - - -class ProjectNotFoundError(ConfigurationSyntaxError): - zuul_error_name = 'Project Not Found' - - def __init__(self, project): - message = textwrap.dedent("""\ - The project "{project}" was not found. All projects - referenced within a Zuul configuration must first be - added to the main configuration file by the Zuul - administrator.""") - message = textwrap.fill(message.format(project=project)) - super(ProjectNotFoundError, self).__init__(message) - - -class TemplateNotFoundError(ConfigurationSyntaxError): - zuul_error_name = 'Template Not Found' - - def __init__(self, template): - message = textwrap.dedent("""\ - The project template "{template}" was not found. - """) - message = textwrap.fill(message.format(template=template)) - super(TemplateNotFoundError, self).__init__(message) - - -class NodesetNotFoundError(ConfigurationSyntaxError): - zuul_error_name = 'Nodeset Not Found' - - def __init__(self, nodeset): - message = textwrap.dedent("""\ - The nodeset "{nodeset}" was not found. - """) - message = textwrap.fill(message.format(nodeset=nodeset)) - super(NodesetNotFoundError, self).__init__(message) - - -class PipelineNotPermittedError(ConfigurationSyntaxError): - zuul_error_name = 'Pipeline Forbidden' - - def __init__(self): - message = textwrap.dedent("""\ - Pipelines may not be defined in untrusted repos, - they may only be defined in config repos.""") - message = textwrap.fill(message) - super(PipelineNotPermittedError, self).__init__(message) - - -class ProjectNotPermittedError(ConfigurationSyntaxError): - zuul_error_name = 'Project Forbidden' - - def __init__(self): - message = textwrap.dedent("""\ - Within an untrusted project, the only project definition - permitted is that of the project itself.""") - message = textwrap.fill(message) - super(ProjectNotPermittedError, self).__init__(message) - - -class GlobalSemaphoreNotFoundError(ConfigurationSyntaxError): - zuul_error_name = 'Global Semaphore Not Found' - - def __init__(self, semaphore): - message = textwrap.dedent("""\ - The global semaphore "{semaphore}" was not found. All - global semaphores must be added to the main configuration - file by the Zuul administrator.""") - message = textwrap.fill(message.format(semaphore=semaphore)) - super(GlobalSemaphoreNotFoundError, self).__init__(message) - - -class YAMLDuplicateKeyError(ConfigurationSyntaxError): - def __init__(self, key, source_context, start_mark): - self.source_context = source_context - self.start_mark = start_mark - message = (f'The key "{key}" appears more than once; ' - 'duplicate keys are not permitted.') - super(YAMLDuplicateKeyError, self).__init__(message) - - -class ConfigurationSyntaxWarning: - zuul_error_name = 'Unknown Configuration Warning' - zuul_error_severity = model.SEVERITY_WARNING - zuul_error_message = 'Unknown Configuration Warning' - - def __init__(self, message=None): - if message: - self.zuul_error_message = message - - -class MultipleProjectConfigurations(ConfigurationSyntaxWarning): - zuul_error_name = 'Multiple Project Configurations' - zuul_error_problem = 'configuration error' - - def __init__(self, source_context): - message = textwrap.dedent(f"""\ - Configuration in {source_context.path} ignored because project-branch - is already configured.""") - message = textwrap.fill(message) - super().__init__(message) - - -class DeprecationWarning(ConfigurationSyntaxWarning): - zuul_error_problem = 'deprecated syntax' - - -class RegexDeprecation(DeprecationWarning): - zuul_error_name = 'Regex Deprecation' - zuul_error_message = """\ -All regular expressions must conform to RE2 syntax, but an -expression using the deprecated Perl-style syntax has been detected. -Adjust the configuration to conform to RE2 syntax.""" - - def __init__(self, message=None): - if message: - message = (self.zuul_error_message + - f"\n\nThe RE2 syntax error is: {message}") - super().__init__(message) - - class LocalAccumulator: """An error accumulator that wraps another accumulator (like LoadingErrors) while holding local context information. @@ -430,7 +246,7 @@ class LocalAccumulator: error_message = '\n\n'.join(msg) error_severity = getattr(error, 'zuul_error_severity', - model.SEVERITY_ERROR) + SEVERITY_ERROR) error_name = getattr(error, 'zuul_error_name', 'Unknown') config_error = model.ConfigurationError( diff --git a/zuul/driver/gerrit/gerrittrigger.py b/zuul/driver/gerrit/gerrittrigger.py index 81f280542d..8e83a72b61 100644 --- a/zuul/driver/gerrit/gerrittrigger.py +++ b/zuul/driver/gerrit/gerrittrigger.py @@ -24,7 +24,7 @@ from zuul.driver.util import ( make_regex, ZUUL_REGEX, ) -from zuul.configloader import DeprecationWarning +from zuul.exceptions import DeprecationWarning class GerritRequireApprovalDeprecation(DeprecationWarning): diff --git a/zuul/driver/github/githubtrigger.py b/zuul/driver/github/githubtrigger.py index e115691d3c..45ef00474e 100644 --- a/zuul/driver/github/githubtrigger.py +++ b/zuul/driver/github/githubtrigger.py @@ -19,7 +19,7 @@ from zuul.trigger import BaseTrigger from zuul.driver.github.githubmodel import GithubEventFilter from zuul.driver.github import githubsource from zuul.driver.util import scalar_or_list, to_list, make_regex, ZUUL_REGEX -from zuul.configloader import DeprecationWarning +from zuul.exceptions import DeprecationWarning class GithubUnlabelDeprecation(DeprecationWarning): diff --git a/zuul/exceptions.py b/zuul/exceptions.py index a332ba1afa..2ab47ba717 100644 --- a/zuul/exceptions.py +++ b/zuul/exceptions.py @@ -1,4 +1,5 @@ # Copyright 2015 Rackspace Australia +# Copyright 2023 Acme Gating, LLC # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain @@ -12,6 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. +import textwrap + +# Error severity +SEVERITY_ERROR = 'error' +SEVERITY_WARNING = 'warning' + class ChangeNotFound(Exception): def __init__(self, number, ps): @@ -120,3 +127,204 @@ class IncorrectZuulAdminClaimError(AuthTokenUnauthorizedException): class UnauthorizedZuulAdminClaimError(AuthTokenUnauthorizedException): defaultMsg = 'Issuer is not allowed to set "zuul.admin" claim' + + +class ConfigurationSyntaxError(Exception): + zuul_error_name = 'Unknown Configuration Error' + zuul_error_severity = SEVERITY_ERROR + + +class NodeFromGroupNotFoundError(ConfigurationSyntaxError): + zuul_error_name = 'Node From Group Not Found' + + def __init__(self, nodeset, node, group): + message = textwrap.dedent("""\ + In {nodeset} the group "{group}" contains a + node named "{node}" which is not defined in the nodeset.""") + message = textwrap.fill(message.format(nodeset=nodeset, + node=node, group=group)) + super(NodeFromGroupNotFoundError, self).__init__(message) + + +class DuplicateNodeError(ConfigurationSyntaxError): + zuul_error_name = 'Duplicate Node' + + def __init__(self, nodeset, node): + message = textwrap.dedent("""\ + In nodeset "{nodeset}" the node "{node}" appears multiple times. + Node names must be unique within a nodeset.""") + message = textwrap.fill(message.format(nodeset=nodeset, + node=node)) + super(DuplicateNodeError, self).__init__(message) + + +class UnknownConnection(ConfigurationSyntaxError): + zuul_error_name = 'Unknown Connection' + + def __init__(self, connection_name): + message = textwrap.dedent("""\ + Unknown connection named "{connection}".""") + message = textwrap.fill(message.format(connection=connection_name)) + super(UnknownConnection, self).__init__(message) + + +class LabelForbiddenError(ConfigurationSyntaxError): + zuul_error_name = 'Label Forbidden' + + def __init__(self, label, allowed_labels, disallowed_labels): + message = textwrap.dedent("""\ + Label named "{label}" is not part of the allowed + labels ({allowed_labels}) for this tenant.""") + # Make a string that looks like "a, b and not c, d" if we have + # both allowed and disallowed labels. + labels = ", ".join(allowed_labels or []) + if allowed_labels and disallowed_labels: + labels += ' and ' + if disallowed_labels: + labels += 'not ' + labels += ", ".join(disallowed_labels) + message = textwrap.fill(message.format( + label=label, + allowed_labels=labels)) + super(LabelForbiddenError, self).__init__(message) + + +class MaxTimeoutError(ConfigurationSyntaxError): + zuul_error_name = 'Max Timeout Exceeded' + + def __init__(self, job, tenant): + message = textwrap.dedent("""\ + The job "{job}" exceeds tenant max-job-timeout {maxtimeout}.""") + message = textwrap.fill(message.format( + job=job.name, maxtimeout=tenant.max_job_timeout)) + super(MaxTimeoutError, self).__init__(message) + + +class DuplicateGroupError(ConfigurationSyntaxError): + zuul_error_name = 'Duplicate Nodeset Group' + + def __init__(self, nodeset, group): + message = textwrap.dedent("""\ + In {nodeset} the group "{group}" appears multiple times. + Group names must be unique within a nodeset.""") + message = textwrap.fill(message.format(nodeset=nodeset, + group=group)) + super(DuplicateGroupError, self).__init__(message) + + +class ProjectNotFoundError(ConfigurationSyntaxError): + zuul_error_name = 'Project Not Found' + + def __init__(self, project): + message = textwrap.dedent("""\ + The project "{project}" was not found. All projects + referenced within a Zuul configuration must first be + added to the main configuration file by the Zuul + administrator.""") + message = textwrap.fill(message.format(project=project)) + super(ProjectNotFoundError, self).__init__(message) + + +class TemplateNotFoundError(ConfigurationSyntaxError): + zuul_error_name = 'Template Not Found' + + def __init__(self, template): + message = textwrap.dedent("""\ + The project template "{template}" was not found. + """) + message = textwrap.fill(message.format(template=template)) + super(TemplateNotFoundError, self).__init__(message) + + +class NodesetNotFoundError(ConfigurationSyntaxError): + zuul_error_name = 'Nodeset Not Found' + + def __init__(self, nodeset): + message = textwrap.dedent("""\ + The nodeset "{nodeset}" was not found. + """) + message = textwrap.fill(message.format(nodeset=nodeset)) + super(NodesetNotFoundError, self).__init__(message) + + +class PipelineNotPermittedError(ConfigurationSyntaxError): + zuul_error_name = 'Pipeline Forbidden' + + def __init__(self): + message = textwrap.dedent("""\ + Pipelines may not be defined in untrusted repos, + they may only be defined in config repos.""") + message = textwrap.fill(message) + super(PipelineNotPermittedError, self).__init__(message) + + +class ProjectNotPermittedError(ConfigurationSyntaxError): + zuul_error_name = 'Project Forbidden' + + def __init__(self): + message = textwrap.dedent("""\ + Within an untrusted project, the only project definition + permitted is that of the project itself.""") + message = textwrap.fill(message) + super(ProjectNotPermittedError, self).__init__(message) + + +class GlobalSemaphoreNotFoundError(ConfigurationSyntaxError): + zuul_error_name = 'Global Semaphore Not Found' + + def __init__(self, semaphore): + message = textwrap.dedent("""\ + The global semaphore "{semaphore}" was not found. All + global semaphores must be added to the main configuration + file by the Zuul administrator.""") + message = textwrap.fill(message.format(semaphore=semaphore)) + super(GlobalSemaphoreNotFoundError, self).__init__(message) + + +class YAMLDuplicateKeyError(ConfigurationSyntaxError): + def __init__(self, key, source_context, start_mark): + self.source_context = source_context + self.start_mark = start_mark + message = (f'The key "{key}" appears more than once; ' + 'duplicate keys are not permitted.') + super(YAMLDuplicateKeyError, self).__init__(message) + + +class ConfigurationSyntaxWarning: + zuul_error_name = 'Unknown Configuration Warning' + zuul_error_severity = SEVERITY_WARNING + zuul_error_message = 'Unknown Configuration Warning' + + def __init__(self, message=None): + if message: + self.zuul_error_message = message + + +class MultipleProjectConfigurations(ConfigurationSyntaxWarning): + zuul_error_name = 'Multiple Project Configurations' + zuul_error_problem = 'configuration error' + + def __init__(self, source_context): + message = textwrap.dedent(f"""\ + Configuration in {source_context.path} ignored because project-branch + is already configured.""") + message = textwrap.fill(message) + super().__init__(message) + + +class DeprecationWarning(ConfigurationSyntaxWarning): + zuul_error_problem = 'deprecated syntax' + + +class RegexDeprecation(DeprecationWarning): + zuul_error_name = 'Regex Deprecation' + zuul_error_message = """\ +All regular expressions must conform to RE2 syntax, but an +expression using the deprecated Perl-style syntax has been detected. +Adjust the configuration to conform to RE2 syntax.""" + + def __init__(self, message=None): + if message: + message = (self.zuul_error_message + + f"\n\nThe RE2 syntax error is: {message}") + super().__init__(message) diff --git a/zuul/model.py b/zuul/model.py index 11e61edff9..b6cb6f5e8d 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -52,6 +52,11 @@ from zuul.lib import tracing from zuul.zk import zkobject from zuul.zk.blob_store import BlobStore from zuul.zk.change_cache import ChangeKey +from zuul.exceptions import ( + SEVERITY_ERROR, + SEVERITY_WARNING, + NodesetNotFoundError, +) MERGER_MERGE = 1 # "git merge" MERGER_MERGE_RESOLVE = 2 # "git merge -s resolve" @@ -118,10 +123,6 @@ SCHEME_GOLANG = 'golang' SCHEME_FLAT = 'flat' SCHEME_UNIQUE = 'unique' -# Error severity -SEVERITY_ERROR = 'error' -SEVERITY_WARNING = 'warning' - def add_debug_line(debug_messages, msg, indent=0): if debug_messages is None: @@ -1620,7 +1621,7 @@ class NodeSet(ConfigObject): # This references an existing named nodeset in the layout. ns = layout.nodesets.get(nodeset) if ns is None: - raise Exception(f'The nodeset "{nodeset}" was not found.') + raise NodesetNotFoundError(nodeset) else: ns = nodeset if ns in history: @@ -2902,7 +2903,7 @@ class Job(ConfigObject): # This references an existing named nodeset in the layout. ns = layout.nodesets.get(nodeset) if ns is None: - raise Exception(f'The nodeset "{nodeset}" was not found.') + raise NodesetNotFoundError(nodeset) else: ns = nodeset return ns.flattenAlternatives(layout) @@ -3044,9 +3045,7 @@ class Job(ConfigObject): # This references an existing named nodeset in the layout. ns = layout.nodesets.get(self.nodeset) if ns is None: - raise Exception( - 'The nodeset "{nodeset}" was not found.'.format( - nodeset=self.nodeset)) + raise NodesetNotFoundError(self.nodeset) return ns return self.nodeset diff --git a/zuul/scheduler.py b/zuul/scheduler.py index 5d8ed27a68..8546564946 100644 --- a/zuul/scheduler.py +++ b/zuul/scheduler.py @@ -1438,7 +1438,7 @@ class Scheduler(threading.Thread): loading_errors.append(repr(error)) if loading_errors: summary = '\n\n\n'.join(loading_errors) - raise configloader.ConfigurationSyntaxError( + raise exceptions.ConfigurationSyntaxError( f"Configuration errors: {summary}") duration = round(time.monotonic() - start, 3)