Add support for job allowed-projects

In order to allow users to create jobs which are only available to
run on certain projects, add an 'allowed-projects' attribute to
jobs.  This is especially useful for jobs with secrets.  For example,
consider a job which performs external API testing with credentials.
The user defining that job may well want to restrict its use to a
single project.

Change-Id: I4457842ea293baf20b83f7b8b86fba2b7d26d2be
This commit is contained in:
James E. Blair
2017-03-17 12:57:39 -07:00
parent 717e8e928d
commit b3f5db1de3
3 changed files with 70 additions and 1 deletions

View File

@@ -27,11 +27,24 @@ from zuul.lib import encryption
from tests.base import BaseTestCase, FIXTURE_DIR
class FakeSource(object):
def __init__(self, name):
self.name = name
class TestJob(BaseTestCase):
def setUp(self):
super(TestJob, self).setUp()
self.project = model.Project('project', None)
self.tenant = model.Tenant('tenant')
self.layout = model.Layout()
self.project = model.Project('project', 'connection')
self.source = FakeSource('connection')
self.tenant.addProjectRepo(self.source, self.project)
self.pipeline = model.Pipeline('gate', self.layout)
self.layout.addPipeline(self.pipeline)
self.queue = model.ChangeQueue(self.pipeline)
private_key_file = os.path.join(FIXTURE_DIR, 'private.pem')
with open(private_key_file, "rb") as f:
self.project.private_key, self.project.public_key = \
@@ -566,6 +579,43 @@ class TestJob(BaseTestCase):
"to shadow job base in base_project"):
layout.addJob(base2)
def test_job_allowed_projects(self):
job = configloader.JobParser.fromYaml(self.tenant, self.layout, {
'_source_context': self.context,
'_start_mark': self.start_mark,
'name': 'job',
'allowed-projects': ['project'],
})
self.layout.addJob(job)
project2 = model.Project('project2', None)
context2 = model.SourceContext(project2, 'master',
'test', True)
project2_config = configloader.ProjectParser.fromYaml(
self.tenant, self.layout, [{
'_source_context': context2,
'_start_mark': self.start_mark,
'name': 'project2',
'gate': {
'jobs': [
'job'
]
}
}]
)
self.layout.addProjectConfig(project2_config)
change = model.Change(project2)
# Test master
change.branch = 'master'
item = self.queue.enqueueChange(change)
item.current_build_set.layout = self.layout
with testtools.ExpectedException(
Exception,
"Project project2 is not allowed to run job job"):
item.freezeJobGraph()
class TestJobTimeData(BaseTestCase):
def setUp(self):

View File

@@ -240,6 +240,7 @@ class JobParser(object):
'repos': to_list(str),
'vars': dict,
'dependencies': to_list(str),
'allowed-projects': to_list(str),
}
return vs.Schema(job)
@@ -349,6 +350,19 @@ class JobParser(object):
if variables:
job.updateVariables(variables)
allowed_projects = conf.get('allowed-projects', None)
if allowed_projects:
allowed = []
for p in as_list(allowed_projects):
# TODOv3(jeblair): this limits allowed_projects to the same
# source; we should remove that limitation.
source = job.source_context.project.connection_name
(trusted, project) = tenant.getRepo(source, p)
if project is None:
raise Exception("Unknown project %s" % (p,))
allowed.append(project.name)
job.allowed_projects = frozenset(allowed)
# If the definition for this job came from a project repo,
# implicitly apply a branch matcher for the branch it was on.
if (not job.source_context.trusted):

View File

@@ -764,6 +764,7 @@ class Job(object):
final=False,
roles=frozenset(),
repos=frozenset(),
allowed_projects=None,
)
# These are generally internal attributes which are not
@@ -2355,6 +2356,10 @@ class Layout(object):
# A change must match at least one project pipeline
# job variant.
continue
if (frozen_job.allowed_projects and
change.project.name not in frozen_job.allowed_projects):
raise Exception("Project %s is not allowed to run job %s" %
(change.project.name, frozen_job.name))
job_graph.addJob(frozen_job)
def createJobGraph(self, item):