Merge "Provide file locations of config syntax errors" into feature/zuulv3

This commit is contained in:
Jenkins 2017-03-10 15:54:12 +00:00 committed by Gerrit Code Review
commit 0713abff3e
3 changed files with 99 additions and 51 deletions

View File

@ -18,6 +18,7 @@ import random
import fixtures
import testtools
import yaml
from zuul import model
from zuul import configloader
@ -32,15 +33,15 @@ class TestJob(BaseTestCase):
self.project = model.Project('project', None)
self.context = model.SourceContext(self.project, 'master',
'test', True)
self.start_mark = yaml.Mark('name', 0, 0, 0, '', 0)
@property
def job(self):
tenant = model.Tenant('tenant')
layout = model.Layout()
project = model.Project('project', None)
context = model.SourceContext(project, 'master', 'test', True)
job = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'job',
'irrelevant-files': [
'^docs/.*$'
@ -143,10 +144,10 @@ class TestJob(BaseTestCase):
layout.addPipeline(pipeline)
queue = model.ChangeQueue(pipeline)
project = model.Project('project', None)
context = model.SourceContext(project, 'master', 'test', True)
base = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'base',
'timeout': 30,
'pre-run': 'base-pre',
@ -158,7 +159,8 @@ class TestJob(BaseTestCase):
})
layout.addJob(base)
python27 = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'python27',
'parent': 'base',
'pre-run': 'py27-pre',
@ -171,7 +173,8 @@ class TestJob(BaseTestCase):
})
layout.addJob(python27)
python27diablo = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'python27',
'branches': [
'stable/diablo'
@ -188,7 +191,8 @@ class TestJob(BaseTestCase):
layout.addJob(python27diablo)
python27essex = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'python27',
'branches': [
'stable/essex'
@ -199,7 +203,8 @@ class TestJob(BaseTestCase):
layout.addJob(python27essex)
project_config = configloader.ProjectParser.fromYaml(tenant, layout, [{
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'project',
'gate': {
'jobs': [
@ -296,18 +301,18 @@ class TestJob(BaseTestCase):
def test_job_auth_inheritance(self):
tenant = model.Tenant('tenant')
layout = model.Layout()
project = model.Project('project', None)
context = model.SourceContext(project, 'master', 'test', True)
base = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'base',
'timeout': 30,
})
layout.addJob(base)
pypi_upload_without_inherit = configloader.JobParser.fromYaml(
tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'pypi-upload-without-inherit',
'parent': 'base',
'timeout': 40,
@ -320,7 +325,8 @@ class TestJob(BaseTestCase):
layout.addJob(pypi_upload_without_inherit)
pypi_upload_with_inherit = configloader.JobParser.fromYaml(
tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'pypi-upload-with-inherit',
'parent': 'base',
'timeout': 40,
@ -334,7 +340,8 @@ class TestJob(BaseTestCase):
layout.addJob(pypi_upload_with_inherit)
pypi_upload_with_inherit_false = configloader.JobParser.fromYaml(
tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'pypi-upload-with-inherit-false',
'parent': 'base',
'timeout': 40,
@ -348,21 +355,24 @@ class TestJob(BaseTestCase):
layout.addJob(pypi_upload_with_inherit_false)
in_repo_job_without_inherit = configloader.JobParser.fromYaml(
tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'in-repo-job-without-inherit',
'parent': 'pypi-upload-without-inherit',
})
layout.addJob(in_repo_job_without_inherit)
in_repo_job_with_inherit = configloader.JobParser.fromYaml(
tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'in-repo-job-with-inherit',
'parent': 'pypi-upload-with-inherit',
})
layout.addJob(in_repo_job_with_inherit)
in_repo_job_with_inherit_false = configloader.JobParser.fromYaml(
tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'in-repo-job-with-inherit-false',
'parent': 'pypi-upload-with-inherit-false',
})
@ -381,24 +391,25 @@ class TestJob(BaseTestCase):
pipeline = model.Pipeline('gate', layout)
layout.addPipeline(pipeline)
queue = model.ChangeQueue(pipeline)
project = model.Project('project', None)
context = model.SourceContext(project, 'master', 'test', True)
base = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'base',
'timeout': 30,
})
layout.addJob(base)
python27 = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'python27',
'parent': 'base',
'timeout': 40,
})
layout.addJob(python27)
python27diablo = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'python27',
'branches': [
'stable/diablo'
@ -408,7 +419,8 @@ class TestJob(BaseTestCase):
layout.addJob(python27diablo)
project_config = configloader.ProjectParser.fromYaml(tenant, layout, [{
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'project',
'gate': {
'jobs': [
@ -418,7 +430,7 @@ class TestJob(BaseTestCase):
}])
layout.addProjectConfig(project_config)
change = model.Change(project)
change = model.Change(self.project)
change.branch = 'master'
item = queue.enqueueChange(change)
item.current_build_set.layout = layout
@ -455,16 +467,17 @@ class TestJob(BaseTestCase):
layout.addPipeline(pipeline)
queue = model.ChangeQueue(pipeline)
project = model.Project('project', None)
context = model.SourceContext(project, 'master', 'test', True)
base = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'base',
'timeout': 30,
})
layout.addJob(base)
python27 = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'python27',
'parent': 'base',
'timeout': 40,
@ -473,7 +486,8 @@ class TestJob(BaseTestCase):
layout.addJob(python27)
project_config = configloader.ProjectParser.fromYaml(tenant, layout, [{
'_source_context': context,
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'project',
'gate': {
'jobs': [
@ -504,6 +518,7 @@ class TestJob(BaseTestCase):
base = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': base_context,
'_start_mark': self.start_mark,
'name': 'base',
})
layout.addJob(base)
@ -513,6 +528,7 @@ class TestJob(BaseTestCase):
'test', True)
base2 = configloader.JobParser.fromYaml(tenant, layout, {
'_source_context': other_context,
'_start_mark': self.start_mark,
'name': 'base',
})
with testtools.ExpectedException(

View File

@ -17,6 +17,7 @@ import logging
import six
import yaml
import pprint
import textwrap
import voluptuous as vs
@ -44,6 +45,10 @@ class ConfigurationSyntaxError(Exception):
pass
def indent(s):
return '\n'.join([' ' + x for x in s.split('\n')])
@contextmanager
def configuration_exceptions(stanza, conf):
try:
@ -51,28 +56,51 @@ def configuration_exceptions(stanza, conf):
except vs.Invalid as e:
conf = copy.deepcopy(conf)
context = conf.pop('_source_context')
m = """
Zuul encountered a syntax error while parsing its configuration in the
repo {repo} on branch {branch}. The error was:
start_mark = conf.pop('_start_mark')
intro = textwrap.fill(textwrap.dedent("""\
Zuul encountered a syntax error while parsing its configuration in the
repo {repo} on branch {branch}. The error was:""".format(
repo=context.project.name,
branch=context.branch,
)))
{error}
m = textwrap.dedent("""\
{intro}
The offending content was a {stanza} stanza with the content:
{error}
{content}
"""
m = m.format(repo=context.project.name,
branch=context.branch,
error=str(e),
The error appears in a {stanza} stanza with the content:
{content}
{start_mark}""")
m = m.format(intro=intro,
error=indent(str(e)),
stanza=stanza,
content=pprint.pformat(conf))
content=indent(pprint.pformat(conf)),
start_mark=str(start_mark))
raise ConfigurationSyntaxError(m)
class ZuulSafeLoader(yaml.SafeLoader):
zuul_node_types = frozenset(('job', 'nodeset', 'pipeline',
'project', 'project-template'))
def __init__(self, stream, context):
super(ZuulSafeLoader, self).__init__(stream)
self.name = str(context)
self.zuul_context = context
def construct_mapping(self, node, deep=False):
r = super(ZuulSafeLoader, self).construct_mapping(node, deep)
keys = frozenset(r.keys())
if len(keys) == 1 and keys.intersection(self.zuul_node_types):
d = r.values()[0]
if isinstance(d, dict):
d['_start_mark'] = node.start_mark
d['_source_context'] = self.zuul_context
return r
def safe_load_yaml(stream, context):
@ -104,6 +132,7 @@ class NodeSetParser(object):
nodeset = {vs.Required('name'): str,
vs.Required('nodes'): [node],
'_source_context': model.SourceContext,
'_start_mark': yaml.Mark,
}
return vs.Schema(nodeset)
@ -160,6 +189,7 @@ class JobParser(object):
'post-run': to_list(str),
'run': str,
'_source_context': model.SourceContext,
'_start_mark': yaml.Mark,
'roles': to_list(role),
'repos': to_list(str),
'vars': dict,
@ -310,6 +340,7 @@ class ProjectTemplateParser(object):
'merge', 'merge-resolve',
'cherry-pick'),
'_source_context': model.SourceContext,
'_start_mark': yaml.Mark,
}
for p in layout.pipelines.values():
@ -325,6 +356,7 @@ class ProjectTemplateParser(object):
conf = copy.deepcopy(conf)
project_template = model.ProjectConfig(conf['name'])
source_context = conf['_source_context']
start_mark = conf['_start_mark']
for pipeline in layout.pipelines.values():
conf_pipeline = conf.get(pipeline.name)
if not conf_pipeline:
@ -334,11 +366,12 @@ class ProjectTemplateParser(object):
project_pipeline.queue_name = conf_pipeline.get('queue')
project_pipeline.job_tree = ProjectTemplateParser._parseJobTree(
tenant, layout, conf_pipeline.get('jobs', []),
source_context)
source_context, start_mark)
return project_template
@staticmethod
def _parseJobTree(tenant, layout, conf, source_context, tree=None):
def _parseJobTree(tenant, layout, conf, source_context,
start_mark, tree=None):
if not tree:
tree = model.JobTree(None)
for conf_job in conf:
@ -354,6 +387,7 @@ class ProjectTemplateParser(object):
# We are overriding params, so make a new job def
attrs['name'] = jobname
attrs['_source_context'] = source_context
attrs['_start_mark'] = start_mark
subtree = tree.addJob(JobParser.fromYaml(
tenant, layout, attrs))
else:
@ -364,7 +398,8 @@ class ProjectTemplateParser(object):
if jobs:
# This is the root of a sub tree
ProjectTemplateParser._parseJobTree(
tenant, layout, jobs, source_context, subtree)
tenant, layout, jobs, source_context,
start_mark, subtree)
else:
raise Exception("Job must be a string or dictionary")
return tree
@ -381,6 +416,7 @@ class ProjectParser(object):
'merge-mode': vs.Any('merge', 'merge-resolve',
'cherry-pick'),
'_source_context': model.SourceContext,
'_start_mark': yaml.Mark,
}
for p in layout.pipelines.values():
@ -518,6 +554,7 @@ class PipelineParser(object):
'window-decrease-type': window_type,
'window-decrease-factor': window_factor,
'_source_context': model.SourceContext,
'_start_mark': yaml.Mark,
}
pipeline['trigger'] = vs.Required(
PipelineParser.getDriverSchema('trigger', connections))
@ -783,7 +820,7 @@ class TenantParser(object):
def _parseConfigRepoLayout(data, source_context):
# This is the top-level configuration for a tenant.
config = model.UnparsedTenantConfig()
config.extend(safe_load_yaml(data, source_context), source_context)
config.extend(safe_load_yaml(data, source_context))
return config
@staticmethod
@ -791,7 +828,7 @@ class TenantParser(object):
# TODOv3(jeblair): this should implement some rules to protect
# aspects of the config that should not be changed in-repo
config = model.UnparsedTenantConfig()
config.extend(safe_load_yaml(data, source_context), source_context)
config.extend(safe_load_yaml(data, source_context))
return config
@staticmethod

View File

@ -2051,7 +2051,7 @@ class UnparsedTenantConfig(object):
r.nodesets = copy.deepcopy(self.nodesets)
return r
def extend(self, conf, source_context=None):
def extend(self, conf):
if isinstance(conf, UnparsedTenantConfig):
self.pipelines.extend(conf.pipelines)
self.jobs.extend(conf.jobs)
@ -2066,10 +2066,6 @@ class UnparsedTenantConfig(object):
"a list of dictionaries (when parsing %s)" %
(conf,))
if source_context is None:
raise Exception("A source context must be provided "
"(when parsing %s)" % (conf,))
for item in conf:
if not isinstance(item, dict):
raise Exception("Configuration items must be in the form of "
@ -2080,7 +2076,6 @@ class UnparsedTenantConfig(object):
"a single key (when parsing %s)" %
(conf,))
key, value = item.items()[0]
value['_source_context'] = source_context
if key == 'project':
name = value['name']
self.projects.setdefault(name, []).append(value)