Add error information to config-errors API endpoint
This is the first in a series of changes to improve the usability of the web view of config errors. The end goal is to be able to display them in a more structured manner. A secondary goal is to eventually add warnings (eg, deprecation warnings) which is really only feasible if we have structured presentation of errors. This change does the following: * Adds severity and error names to existing configuration errors * And makes them available via the config-errors API endpoint * Reduces the call sites for the error accumulator (LoadingErrors.addError) * Unifies the calling convention for the accumulator (we stop passing in Exception objects) Change-Id: Ia17dd3e7ad8cdfa8a07bb03b871078415d0c145e
This commit is contained in:
parent
3489d22290
commit
84e0e76e2f
|
@ -79,6 +79,9 @@ class ConfigurationSyntaxError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class NodeFromGroupNotFoundError(Exception):
|
class NodeFromGroupNotFoundError(Exception):
|
||||||
|
zuul_error_name = 'Node From Group Not Found'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, nodeset, node, group):
|
def __init__(self, nodeset, node, group):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
In {nodeset} the group "{group}" contains a
|
In {nodeset} the group "{group}" contains a
|
||||||
|
@ -89,6 +92,9 @@ class NodeFromGroupNotFoundError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class DuplicateNodeError(Exception):
|
class DuplicateNodeError(Exception):
|
||||||
|
zuul_error_name = 'Duplicate Node'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, nodeset, node):
|
def __init__(self, nodeset, node):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
In nodeset "{nodeset}" the node "{node}" appears multiple times.
|
In nodeset "{nodeset}" the node "{node}" appears multiple times.
|
||||||
|
@ -99,6 +105,9 @@ class DuplicateNodeError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class UnknownConnection(Exception):
|
class UnknownConnection(Exception):
|
||||||
|
zuul_error_name = 'Unknown Connection'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, connection_name):
|
def __init__(self, connection_name):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
Unknown connection named "{connection}".""")
|
Unknown connection named "{connection}".""")
|
||||||
|
@ -107,6 +116,9 @@ class UnknownConnection(Exception):
|
||||||
|
|
||||||
|
|
||||||
class LabelForbiddenError(Exception):
|
class LabelForbiddenError(Exception):
|
||||||
|
zuul_error_name = 'Label Forbidden'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, label, allowed_labels, disallowed_labels):
|
def __init__(self, label, allowed_labels, disallowed_labels):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
Label named "{label}" is not part of the allowed
|
Label named "{label}" is not part of the allowed
|
||||||
|
@ -126,6 +138,9 @@ class LabelForbiddenError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class MaxTimeoutError(Exception):
|
class MaxTimeoutError(Exception):
|
||||||
|
zuul_error_name = 'Max Timeout Exceeded'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, job, tenant):
|
def __init__(self, job, tenant):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
The job "{job}" exceeds tenant max-job-timeout {maxtimeout}.""")
|
The job "{job}" exceeds tenant max-job-timeout {maxtimeout}.""")
|
||||||
|
@ -135,6 +150,9 @@ class MaxTimeoutError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class DuplicateGroupError(Exception):
|
class DuplicateGroupError(Exception):
|
||||||
|
zuul_error_name = 'Duplicate Nodeset Group'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, nodeset, group):
|
def __init__(self, nodeset, group):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
In {nodeset} the group "{group}" appears multiple times.
|
In {nodeset} the group "{group}" appears multiple times.
|
||||||
|
@ -145,6 +163,9 @@ class DuplicateGroupError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class ProjectNotFoundError(Exception):
|
class ProjectNotFoundError(Exception):
|
||||||
|
zuul_error_name = 'Project Not Found'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, project):
|
def __init__(self, project):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
The project "{project}" was not found. All projects
|
The project "{project}" was not found. All projects
|
||||||
|
@ -156,6 +177,9 @@ class ProjectNotFoundError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class TemplateNotFoundError(Exception):
|
class TemplateNotFoundError(Exception):
|
||||||
|
zuul_error_name = 'Template Not Found'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, template):
|
def __init__(self, template):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
The project template "{template}" was not found.
|
The project template "{template}" was not found.
|
||||||
|
@ -165,6 +189,9 @@ class TemplateNotFoundError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class NodesetNotFoundError(Exception):
|
class NodesetNotFoundError(Exception):
|
||||||
|
zuul_error_name = 'Nodeset Not Found'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, nodeset):
|
def __init__(self, nodeset):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
The nodeset "{nodeset}" was not found.
|
The nodeset "{nodeset}" was not found.
|
||||||
|
@ -174,6 +201,9 @@ class NodesetNotFoundError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class PipelineNotPermittedError(Exception):
|
class PipelineNotPermittedError(Exception):
|
||||||
|
zuul_error_name = 'Pipeline Forbidden'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
Pipelines may not be defined in untrusted repos,
|
Pipelines may not be defined in untrusted repos,
|
||||||
|
@ -183,6 +213,9 @@ class PipelineNotPermittedError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class ProjectNotPermittedError(Exception):
|
class ProjectNotPermittedError(Exception):
|
||||||
|
zuul_error_name = 'Project Forbidden'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
Within an untrusted project, the only project definition
|
Within an untrusted project, the only project definition
|
||||||
|
@ -192,6 +225,9 @@ class ProjectNotPermittedError(Exception):
|
||||||
|
|
||||||
|
|
||||||
class GlobalSemaphoreNotFoundError(Exception):
|
class GlobalSemaphoreNotFoundError(Exception):
|
||||||
|
zuul_error_name = 'Global Semaphore Not Found'
|
||||||
|
zuul_error_severity = model.SEVERITY_ERROR
|
||||||
|
|
||||||
def __init__(self, semaphore):
|
def __init__(self, semaphore):
|
||||||
message = textwrap.dedent("""\
|
message = textwrap.dedent("""\
|
||||||
The global semaphore "{semaphore}" was not found. All
|
The global semaphore "{semaphore}" was not found. All
|
||||||
|
@ -261,15 +297,18 @@ def project_configuration_exceptions(context, accumulator):
|
||||||
|
|
||||||
m = m.format(intro=intro,
|
m = m.format(intro=intro,
|
||||||
error=indent(str(e)))
|
error=indent(str(e)))
|
||||||
accumulator.addError(context, None, m)
|
accumulator.addError(
|
||||||
|
context, None, m,
|
||||||
|
short_error=str(e),
|
||||||
|
severity=getattr(e, 'zuul_error_severity', model.SEVERITY_ERROR),
|
||||||
|
name=getattr(e, 'zuul_error_name', 'Unknown'))
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
def early_configuration_exceptions(context):
|
def early_configuration_exceptions(context, accumulator):
|
||||||
try:
|
try:
|
||||||
yield
|
yield
|
||||||
except ConfigurationSyntaxError:
|
# Note: we catch ConfigurationSyntaxErrors here.
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
intro = textwrap.fill(textwrap.dedent("""\
|
intro = textwrap.fill(textwrap.dedent("""\
|
||||||
Zuul encountered a syntax error while parsing its configuration in the
|
Zuul encountered a syntax error while parsing its configuration in the
|
||||||
|
@ -285,7 +324,11 @@ def early_configuration_exceptions(context):
|
||||||
|
|
||||||
m = m.format(intro=intro,
|
m = m.format(intro=intro,
|
||||||
error=indent(str(e)))
|
error=indent(str(e)))
|
||||||
raise ConfigurationSyntaxError(m)
|
accumulator.addError(
|
||||||
|
context, None, m,
|
||||||
|
short_error=str(e),
|
||||||
|
severity=getattr(e, 'zuul_error_severity', model.SEVERITY_ERROR),
|
||||||
|
name=getattr(e, 'zuul_error_name', 'Unknown'))
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
@ -322,7 +365,11 @@ def configuration_exceptions(stanza, conf, accumulator):
|
||||||
content=indent(start_mark.snippet.rstrip()),
|
content=indent(start_mark.snippet.rstrip()),
|
||||||
start_mark=str(start_mark))
|
start_mark=str(start_mark))
|
||||||
|
|
||||||
accumulator.addError(context, start_mark, m, str(e))
|
accumulator.addError(
|
||||||
|
context, start_mark, m,
|
||||||
|
short_error=str(e),
|
||||||
|
severity=getattr(e, 'zuul_error_severity', model.SEVERITY_ERROR),
|
||||||
|
name=getattr(e, 'zuul_error_name', 'Unknown'))
|
||||||
|
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
@ -358,7 +405,11 @@ def reference_exceptions(stanza, obj, accumulator):
|
||||||
content=indent(start_mark.snippet.rstrip()),
|
content=indent(start_mark.snippet.rstrip()),
|
||||||
start_mark=str(start_mark))
|
start_mark=str(start_mark))
|
||||||
|
|
||||||
accumulator.addError(context, start_mark, m, str(e))
|
accumulator.addError(
|
||||||
|
context, start_mark, m,
|
||||||
|
short_error=str(e),
|
||||||
|
severity=getattr(e, 'zuul_error_severity', model.SEVERITY_ERROR),
|
||||||
|
name=getattr(e, 'zuul_error_name', 'Unknown'))
|
||||||
|
|
||||||
|
|
||||||
class ZuulSafeLoader(yaml.EncryptedLoader):
|
class ZuulSafeLoader(yaml.EncryptedLoader):
|
||||||
|
@ -2359,12 +2410,9 @@ class TenantParser(object):
|
||||||
|
|
||||||
def loadProjectYAML(self, data, source_context, loading_errors):
|
def loadProjectYAML(self, data, source_context, loading_errors):
|
||||||
config = model.UnparsedConfig()
|
config = model.UnparsedConfig()
|
||||||
try:
|
with early_configuration_exceptions(source_context, loading_errors):
|
||||||
with early_configuration_exceptions(source_context):
|
r = safe_load_yaml(data, source_context)
|
||||||
r = safe_load_yaml(data, source_context)
|
config.extend(r)
|
||||||
config.extend(r)
|
|
||||||
except ConfigurationSyntaxError as e:
|
|
||||||
loading_errors.addError(source_context, None, e)
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def filterConfigProjectYAML(self, data):
|
def filterConfigProjectYAML(self, data):
|
||||||
|
@ -2389,12 +2437,9 @@ class TenantParser(object):
|
||||||
# Handle pragma items first since they modify the source context
|
# Handle pragma items first since they modify the source context
|
||||||
# used by other classes.
|
# used by other classes.
|
||||||
for config_pragma in unparsed_config.pragmas:
|
for config_pragma in unparsed_config.pragmas:
|
||||||
try:
|
with configuration_exceptions('pragma',
|
||||||
|
config_pragma, loading_errors):
|
||||||
pcontext.pragma_parser.fromYaml(config_pragma)
|
pcontext.pragma_parser.fromYaml(config_pragma)
|
||||||
except ConfigurationSyntaxError as e:
|
|
||||||
loading_errors.addError(
|
|
||||||
config_pragma['_source_context'],
|
|
||||||
config_pragma['_start_mark'], e)
|
|
||||||
|
|
||||||
for config_pipeline in unparsed_config.pipelines:
|
for config_pipeline in unparsed_config.pipelines:
|
||||||
classes = self._getLoadClasses(tenant, config_pipeline)
|
classes = self._getLoadClasses(tenant, config_pipeline)
|
||||||
|
|
|
@ -112,6 +112,10 @@ SCHEME_GOLANG = 'golang'
|
||||||
SCHEME_FLAT = 'flat'
|
SCHEME_FLAT = 'flat'
|
||||||
SCHEME_UNIQUE = 'unique'
|
SCHEME_UNIQUE = 'unique'
|
||||||
|
|
||||||
|
# Error severity
|
||||||
|
SEVERITY_ERROR = 'error'
|
||||||
|
SEVERITY_WARNING = 'warning'
|
||||||
|
|
||||||
|
|
||||||
def add_debug_line(debug_messages, msg, indent=0):
|
def add_debug_line(debug_messages, msg, indent=0):
|
||||||
if debug_messages is None:
|
if debug_messages is None:
|
||||||
|
@ -180,6 +184,9 @@ class ConfigurationErrorKey(object):
|
||||||
sufficient to determine whether we should show an error to a user.
|
sufficient to determine whether we should show an error to a user.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
# Note: this class is serialized to ZK via ConfigurationErrorList,
|
||||||
|
# ensure that it serializes and deserializes appropriately.
|
||||||
|
|
||||||
def __init__(self, context, mark, error_text):
|
def __init__(self, context, mark, error_text):
|
||||||
self.context = context
|
self.context = context
|
||||||
self.mark = mark
|
self.mark = mark
|
||||||
|
@ -240,23 +247,34 @@ class ConfigurationErrorKey(object):
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationError(object):
|
class ConfigurationError(object):
|
||||||
|
|
||||||
"""A configuration error"""
|
"""A configuration error"""
|
||||||
def __init__(self, context, mark, error, short_error=None):
|
|
||||||
self.error = str(error)
|
# Note: this class is serialized to ZK via ConfigurationErrorList,
|
||||||
|
# ensure that it serializes and deserializes appropriately.
|
||||||
|
|
||||||
|
def __init__(self, context, mark, error, short_error=None,
|
||||||
|
severity=None, name=None):
|
||||||
|
self.error = error
|
||||||
self.short_error = short_error
|
self.short_error = short_error
|
||||||
|
self.severity = severity or SEVERITY_ERROR
|
||||||
|
self.name = name or 'Unknown'
|
||||||
self.key = ConfigurationErrorKey(context, mark, self.error)
|
self.key = ConfigurationErrorKey(context, mark, self.error)
|
||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
return {
|
return {
|
||||||
"error": self.error,
|
"error": self.error,
|
||||||
"short_error": self.short_error,
|
"short_error": self.short_error,
|
||||||
"key": self.key.serialize()
|
"key": self.key.serialize(),
|
||||||
|
"severity": self.severity,
|
||||||
|
"name": self.name,
|
||||||
}
|
}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def deserialize(cls, data):
|
def deserialize(cls, data):
|
||||||
data["key"] = ConfigurationErrorKey.deserialize(data["key"])
|
data["key"] = ConfigurationErrorKey.deserialize(data["key"])
|
||||||
|
# These attributes were added in MODEL_API 13
|
||||||
|
data['severity'] = data.get('severity', SEVERITY_ERROR)
|
||||||
|
data['name'] = data.get('name', 'Unknown')
|
||||||
o = cls.__new__(cls)
|
o = cls.__new__(cls)
|
||||||
o.__dict__.update(data)
|
o.__dict__.update(data)
|
||||||
return o
|
return o
|
||||||
|
@ -269,7 +287,9 @@ class ConfigurationError(object):
|
||||||
return False
|
return False
|
||||||
return (self.error == other.error and
|
return (self.error == other.error and
|
||||||
self.short_error == other.short_error and
|
self.short_error == other.short_error and
|
||||||
self.key == other.key)
|
self.key == other.key and
|
||||||
|
self.severity == other.severity and
|
||||||
|
self.name == other.name)
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationErrorList(zkobject.ShardedZKObject):
|
class ConfigurationErrorList(zkobject.ShardedZKObject):
|
||||||
|
@ -306,8 +326,12 @@ class LoadingErrors(object):
|
||||||
self.errors = []
|
self.errors = []
|
||||||
self.error_keys = set()
|
self.error_keys = set()
|
||||||
|
|
||||||
def addError(self, context, mark, error, short_error=None):
|
def addError(self, context, mark, error, short_error=None,
|
||||||
e = ConfigurationError(context, mark, error, short_error)
|
severity=None, name=None):
|
||||||
|
e = ConfigurationError(context, mark, error,
|
||||||
|
short_error=short_error,
|
||||||
|
severity=severity,
|
||||||
|
name=name)
|
||||||
self.errors.append(e)
|
self.errors.append(e)
|
||||||
self.error_keys.add(e.key)
|
self.error_keys.add(e.key)
|
||||||
|
|
||||||
|
|
|
@ -1201,8 +1201,13 @@ class ZuulWebAPI(object):
|
||||||
@cherrypy.tools.check_tenant_auth()
|
@cherrypy.tools.check_tenant_auth()
|
||||||
def config_errors(self, tenant_name, tenant, auth):
|
def config_errors(self, tenant_name, tenant, auth):
|
||||||
ret = [
|
ret = [
|
||||||
{'source_context': e.key.context.toDict(),
|
{
|
||||||
'error': e.error}
|
'source_context': e.key.context.toDict(),
|
||||||
|
'error': e.error,
|
||||||
|
'short_error': e.short_error,
|
||||||
|
'severity': e.severity,
|
||||||
|
'name': e.name,
|
||||||
|
}
|
||||||
for e in tenant.layout.loading_errors.errors
|
for e in tenant.layout.loading_errors.errors
|
||||||
]
|
]
|
||||||
return ret
|
return ret
|
||||||
|
|
Loading…
Reference in New Issue