zuul/zuul/exceptions.py
James E. Blair 4eb3b655c1
Batch fake build database additions
When skipping a job, we create a fake build entry in ZK and also
a database record.  When skipping a large number of jobs, we
create many such builds and after each one, we rewrite the entire
QueueItem object in ZooKeeper.  To avoid excessive unecessary
database queries, create all the fake build objects and then add
them to the database in one transaction, with only one buildset
query.

Also, remove the "missing_buildset_okay" parameter and replace
it with an exception handler in order to localize the logic
around when it's okay for a buildset to be missing.

Change-Id: Ibfb553a7d4d408c2c870fb9e9a653ef35db21fd8
2024-11-22 07:25:01 +01:00

383 lines
12 KiB
Python

# Copyright 2015 Rackspace Australia
# Copyright 2023-2024 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
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# 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):
self.number = number
self.ps = ps
self.change = "%s,%s" % (str(number), str(ps))
message = "Change %s not found" % self.change
super(ChangeNotFound, self).__init__(message)
class RevNotFound(Exception):
def __init__(self, project, rev):
self.project = project
self.revision = rev
message = ("Failed to checkout project '%s' at revision '%s'"
% (self.project, self.revision))
super(RevNotFound, self).__init__(message)
class MergeFailure(Exception):
pass
class MissingBuildsetError(Exception):
pass
class ConfigurationError(Exception):
pass
class StreamingError(Exception):
pass
class DependencyLimitExceededError(Exception):
pass
class VariableNameError(Exception):
pass
# Provider exceptions
class LaunchStatusException(Exception):
statsd_key = 'error.status'
class LaunchNetworkException(Exception):
statsd_key = 'error.network'
class LaunchKeyscanException(Exception):
statsd_key = 'error.keyscan'
class CapacityException(Exception):
statsd_key = 'error.capacity'
class RuntimeConfigurationException(Exception):
pass
# Authentication Exceptions
class AuthTokenException(Exception):
defaultMsg = 'Unknown Error'
HTTPError = 400
def __init__(self, realm=None, msg=None):
super(AuthTokenException, self).__init__(msg or self.defaultMsg)
self.realm = realm
self.error = self.__class__.__name__
self.error_description = msg or self.defaultMsg
def getAdditionalHeaders(self):
return {}
class JWKSException(AuthTokenException):
defaultMsg = 'Unknown error involving JSON Web Key Set'
class AuthTokenForbiddenException(AuthTokenException):
defaultMsg = 'Insufficient privileges'
HTTPError = 403
class AuthTokenUnauthorizedException(AuthTokenException):
defaultMsg = 'This action requires authentication'
HTTPError = 401
def getAdditionalHeaders(self):
error_header = '''Bearer realm="%s"
error="%s"
error_description="%s"'''
return {"WWW-Authenticate": error_header % (self.realm,
self.error,
self.error_description)}
class AuthTokenUndecodedException(AuthTokenUnauthorizedException):
defaultMsg = 'Auth Token could not be decoded'
class AuthTokenInvalidSignatureException(AuthTokenUnauthorizedException):
defaultMsg = 'Invalid signature'
class BearerTokenRequiredError(AuthTokenUnauthorizedException):
defaultMsg = 'Authorization with bearer token required'
class IssuerUnknownError(AuthTokenUnauthorizedException):
defaultMsg = 'Issuer unknown'
class MissingClaimError(AuthTokenUnauthorizedException):
defaultMsg = 'Token is missing claims'
class IncorrectAudienceError(AuthTokenUnauthorizedException):
defaultMsg = 'Incorrect audience'
class TokenExpiredError(AuthTokenUnauthorizedException):
defaultMsg = 'Token has expired'
class MissingUIDClaimError(MissingClaimError):
defaultMsg = 'Token is missing id claim'
class IncorrectZuulAdminClaimError(AuthTokenUnauthorizedException):
defaultMsg = (
'The "zuul.admin" claim is expected to be a list of tenants')
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):
projects = None
if isinstance(project, (list, tuple)):
if len(project) > 1:
projects = ', '.join(f'"{p}"' for p in project)
else:
project = project[0]
if projects:
message = textwrap.dedent(f"""\
The projects {projects} were not found. All projects
referenced within a Zuul configuration must first be
added to the main configuration file by the Zuul
administrator.""")
else:
message = textwrap.dedent(f"""\
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)
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)
class CleanupRunDeprecation(DeprecationWarning):
zuul_error_name = 'Cleanup Run Deprecation'
zuul_error_message = """\
The cleanup-run job attribute is deprecated. Replace it with
post-run playbooks with the `cleanup` attribute set."""