Reduce config error context

We include the yaml text of the entire configuration object in
many of our configuration error reports.  This can be quite large
which is not always helpful to the user, and can cause operational
problems with large numbers of errors of large objects.

To address that, let's only include a few lines.  But let's also
try to make those lines useful by centering the actual attribute
that caused the error in our snippet.

To do this, we need to keep the line number of all of our configuration
attribute keys.  This is accomplished by subclassing str and adding
a "line" attribute so that it behaves exactly like a string most
of the time, but when we go to assemble a configuration error, we can
check to see if we have a line number, and if so, zoom in on that line.

Example:

Zuul encountered a deprecated syntax while parsing its configuration
in the repo org/common-config on branch master.  The problem was:

  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.

  The RE2 syntax error is: invalid perl operator: (?!

The problem appears in the "branches" attribute
of the "org/project" project stanza:

  ...
      name: org/project
      check:
        jobs:
          - check-job:
              branches: ^(?!invalid).*$
      gate:
        jobs:
          - check-job
      post:
  ...

  in "org/common-config/zuul.yaml@master", line 84

This reduction is currently only performed for deprecation warnings but
can be extended to the rest of the configuration errors in subsequent
changes.

Change-Id: I9d9cb286dacee86d54ea854df48d7a4dd37f5f12
This commit is contained in:
James E. Blair 2023-10-25 17:24:19 -07:00
parent d781291dbf
commit a29b7e75af
5 changed files with 106 additions and 18 deletions

View File

@ -319,14 +319,33 @@ class ConfigurationSyntaxWarning(object, metaclass=abc.ABCMeta):
zuul_error_message = 'Unknown Configuration Warning' zuul_error_message = 'Unknown Configuration Warning'
@abc.abstractmethod @abc.abstractmethod
def makeConfigurationError(self, stanza, conf): def makeConfigurationError(self, stanza, conf, attr=None):
"Return a ConfigurationError object" "Return a ConfigurationError object"
class DeprecationWarning(ConfigurationSyntaxWarning): class DeprecationWarning(ConfigurationSyntaxWarning):
def makeConfigurationError(self, stanza, conf): def makeConfigurationError(self, stanza, conf, attr=None):
context = conf.get('_source_context') context = conf.get('_source_context')
start_mark = conf.get('_start_mark') start_mark = conf.get('_start_mark')
snippet = None
name = conf.get('name')
line = start_mark.line
if name:
name = f'"{name}"'
else:
name = 'following'
if attr is not None:
pointer = (
f'The problem appears in the "{attr}" attribute\n'
f'of the {name} {stanza} stanza:'
)
line = getattr(attr, 'line', line)
else:
pointer = (
f'The problem appears in the the {name} {stanza} stanza:'
)
snippet = start_mark.getLineSnippet(line).rstrip()
location = start_mark.getLineLocation(line)
intro = textwrap.fill(textwrap.dedent("""\ intro = textwrap.fill(textwrap.dedent("""\
Zuul encountered a deprecated syntax while parsing its Zuul encountered a deprecated syntax while parsing its
configuration in the repo {repo} on branch {branch}. The configuration in the repo {repo} on branch {branch}. The
@ -340,17 +359,17 @@ class DeprecationWarning(ConfigurationSyntaxWarning):
{error} {error}
The problem appears in the following {stanza} stanza: {pointer}
{content} {content}
{start_mark}""") {location}""")
m = m.format(intro=intro, m = m.format(intro=intro,
error=indent(self.zuul_error_message), error=indent(self.zuul_error_message),
stanza=stanza, pointer=pointer,
content=indent(start_mark.snippet.rstrip()), content=indent(snippet),
start_mark=str(start_mark)) location=location)
return model.ConfigurationError( return model.ConfigurationError(
context, start_mark, m, context, start_mark, m,
@ -517,10 +536,16 @@ class LocalAccumulator:
"""An error accumulator that wraps another accumulator (like """An error accumulator that wraps another accumulator (like
LoadingErrors) while holding local context information. LoadingErrors) while holding local context information.
""" """
def __init__(self, accumulator, stanza, conf): def __init__(self, accumulator, stanza, conf, attr=None):
self.accumulator = accumulator self.accumulator = accumulator
self.stanza = stanza self.stanza = stanza
self.conf = conf self.conf = conf
self.attr = attr
def extend(self, attr=None):
"""Return a new accumulator that extends this one with additional
info"""
return LocalAccumulator(self.accumulator, self.stanza, self.conf, attr)
def addError(self, error): def addError(self, error):
"""Add an error to the accumulator with the local context info. """Add an error to the accumulator with the local context info.
@ -532,10 +557,20 @@ class LocalAccumulator:
exception handlers, so this method doesn't expect them. exception handlers, so this method doesn't expect them.
""" """
e = error.makeConfigurationError(self.stanza, self.conf) e = error.makeConfigurationError(self.stanza, self.conf, self.attr)
self.accumulator.addError(e) self.accumulator.addError(e)
def get_conf_attr(conf, attr, accumulator=None):
found = None
for k in conf.keys():
if k == attr:
found = k
break
acc = accumulator.extend(found) if accumulator else None
return acc, conf.get(found)
class ZuulSafeLoader(yaml.EncryptedLoader): class ZuulSafeLoader(yaml.EncryptedLoader):
zuul_node_types = frozenset(('job', 'nodeset', 'secret', 'pipeline', zuul_node_types = frozenset(('job', 'nodeset', 'secret', 'pipeline',
'project', 'project-template', 'project', 'project-template',
@ -569,6 +604,8 @@ class ZuulSafeLoader(yaml.EncryptedLoader):
raise YAMLDuplicateKeyError(k.value, node, self.zuul_context, raise YAMLDuplicateKeyError(k.value, node, self.zuul_context,
mark) mark)
keys.add(k.value) keys.add(k.value)
if k.tag == 'tag:yaml.org,2002:str':
k.value = yaml.ZuulConfigKey(k.value, node.start_mark.line)
r = super(ZuulSafeLoader, self).construct_mapping(node, deep) r = super(ZuulSafeLoader, self).construct_mapping(node, deep)
keys = frozenset(r.keys()) keys = frozenset(r.keys())
if len(keys) == 1 and keys.intersection(self.zuul_node_types): if len(keys) == 1 and keys.intersection(self.zuul_node_types):
@ -659,7 +696,8 @@ class PragmaParser(object):
if bm is not None: if bm is not None:
source_context.implied_branch_matchers = bm source_context.implied_branch_matchers = bm
branches = conf.get('implied-branches') acc, branches = get_conf_attr(conf, 'implied-branches',
error_accumulator)
if branches is not None: if branches is not None:
# This is a BranchMatcher (not an ImpliedBranchMatcher) # This is a BranchMatcher (not an ImpliedBranchMatcher)
# because as user input, we allow/expect this to be # because as user input, we allow/expect this to be
@ -667,7 +705,7 @@ class PragmaParser(object):
# (automatically generated from source file branches) are # (automatically generated from source file branches) are
# ImpliedBranchMatchers. # ImpliedBranchMatchers.
source_context.implied_branches = [ source_context.implied_branches = [
change_matcher.BranchMatcher(make_regex(x, error_accumulator)) change_matcher.BranchMatcher(make_regex(x, acc))
for x in as_list(branches)] for x in as_list(branches)]
@ -1219,9 +1257,11 @@ class JobParser(object):
branches = None branches = None
if 'branches' in conf: if 'branches' in conf:
acc, conf_branches = get_conf_attr(conf, 'branches',
error_accumulator)
branches = [ branches = [
change_matcher.BranchMatcher( change_matcher.BranchMatcher(
make_regex(x, error_accumulator)) make_regex(x, acc))
for x in as_list(conf['branches']) for x in as_list(conf['branches'])
] ]
elif not project_pipeline: elif not project_pipeline:

View File

@ -18,7 +18,13 @@ import voluptuous as v
from zuul.trigger import BaseTrigger from zuul.trigger import BaseTrigger
from zuul.driver.gerrit.gerritmodel import GerritEventFilter from zuul.driver.gerrit.gerritmodel import GerritEventFilter
from zuul.driver.gerrit import gerritsource from zuul.driver.gerrit import gerritsource
from zuul.driver.util import scalar_or_list, to_list, make_regex, ZUUL_REGEX from zuul.driver.util import (
scalar_or_list,
to_list,
make_regex,
ZUUL_REGEX,
get_conf_attr,
)
from zuul.configloader import DeprecationWarning from zuul.configloader import DeprecationWarning
@ -58,11 +64,13 @@ class GerritTrigger(BaseTrigger):
usernames = to_list(trigger.get('username_filter')) usernames = to_list(trigger.get('username_filter'))
ignore_deletes = trigger.get('ignore-deletes', True) ignore_deletes = trigger.get('ignore-deletes', True)
if 'require-approval' in trigger: if 'require-approval' in trigger:
error_accumulator.addError( acc, _ = get_conf_attr(trigger, 'require-approval',
GerritRequireApprovalDeprecation()) error_accumulator)
acc.addError(GerritRequireApprovalDeprecation())
if 'reject-approval' in trigger: if 'reject-approval' in trigger:
error_accumulator.addError( acc, _ = get_conf_attr(trigger, 'reject-approval',
GerritRejectApprovalDeprecation()) error_accumulator)
acc.addError(GerritRejectApprovalDeprecation())
types = [make_regex(x, error_accumulator) types = [make_regex(x, error_accumulator)
for x in to_list(trigger['event'])] for x in to_list(trigger['event'])]

View File

@ -17,7 +17,7 @@
import voluptuous as vs import voluptuous as vs
from zuul.configloader import ZUUL_REGEX, make_regex # noqa from zuul.configloader import ZUUL_REGEX, make_regex, get_conf_attr # noqa
def time_to_seconds(s): def time_to_seconds(s):

View File

@ -77,6 +77,24 @@ class EncryptedPKCS1_OAEP:
private_key).decode('utf8') private_key).decode('utf8')
class ZuulConfigKey(str):
def __new__(cls, s, line):
self = super().__new__(cls, s)
self.line = line
return self
def __copy__(self):
return ZuulConfigKey(self, self.line)
def __deepcopy__(self, memo):
return self.__copy__()
@classmethod
def to_yaml(cls, dumper, data):
return yaml.representer.SafeRepresenter.represent_str(
dumper, str(data))
def safe_load(stream, *args, **kwargs): def safe_load(stream, *args, **kwargs):
return yaml.load(stream, *args, Loader=SafeLoader, **kwargs) return yaml.load(stream, *args, Loader=SafeLoader, **kwargs)
@ -102,6 +120,9 @@ EncryptedLoader.add_constructor(EncryptedPKCS1_OAEP.yaml_tag,
EncryptedDumper.add_representer( EncryptedDumper.add_representer(
types.MappingProxyType, types.MappingProxyType,
yaml.representer.SafeRepresenter.represent_dict) yaml.representer.SafeRepresenter.represent_dict)
EncryptedDumper.add_representer(
ZuulConfigKey,
ZuulConfigKey.to_yaml)
def encrypted_dump(data, *args, **kwargs): def encrypted_dump(data, *args, **kwargs):

View File

@ -174,6 +174,25 @@ class ZuulMark:
return (self.line == other.line and return (self.line == other.line and
self.snippet == other.snippet) self.snippet == other.snippet)
line_snippet_context = 4
def getLineSnippet(self, line):
start = max(line - self.line - self.line_snippet_context, 0)
end = start + (self.line_snippet_context * 2) + 1
all_lines = self.snippet.splitlines()
lines = all_lines[start:end]
if start > 0:
lines.insert(0, '...')
if end < len(all_lines):
lines.append('...')
return '\n'.join(lines)
def getLineLocation(self, line):
return ' in "{name}", line {line}'.format(
name=self.name,
line=line + 1,
)
def serialize(self): def serialize(self):
return { return {
"name": self.name, "name": self.name,