Merge "web: add /{tenant}/job/{job_name} route"

This commit is contained in:
Zuul 2018-08-06 19:27:04 +00:00 committed by Gerrit Code Review
commit 91c775a1aa
5 changed files with 216 additions and 11 deletions

View File

@ -260,6 +260,88 @@ class TestWeb(BaseTestWeb):
self.assertEqual(1, len(data), data)
self.assertEqual("org/project1", data[0]['project'], data)
def test_web_find_job(self):
# can we fetch the variants for a single job
data = self.get_url('api/tenant/tenant-one/job/project-test1').json()
common_config_role = {
'implicit': True,
'project_canonical_name': 'review.example.com/common-config',
'target_name': 'common-config',
'type': 'zuul',
}
source_ctx = {
'branch': 'master',
'path': 'zuul.yaml',
'project': 'common-config',
}
self.assertEqual([
{
'name': 'project-test1',
'abstract': False,
'attempts': 4,
'branches': [],
'dependencies': [],
'description': None,
'files': [],
'irrelevant_files': [],
'final': False,
'implied_branch': None,
'nodeset': {
'groups': [],
'name': '',
'nodes': [{'comment': None,
'hold_job': None,
'label': 'label1',
'name': 'controller',
'aliases': [],
'state': 'unknown'}],
},
'parent': 'base',
'post_review': None,
'protected': None,
'required_projects': [],
'roles': [common_config_role],
'semaphore': None,
'source_context': source_ctx,
'timeout': None,
'variables': {},
'variant_description': '',
'voting': True
}, {
'name': 'project-test1',
'abstract': False,
'attempts': 3,
'branches': ['stable'],
'dependencies': [],
'description': None,
'files': [],
'irrelevant_files': [],
'final': False,
'implied_branch': None,
'nodeset': {
'groups': [],
'name': '',
'nodes': [{'comment': None,
'hold_job': None,
'label': 'label2',
'name': 'controller',
'aliases': [],
'state': 'unknown'}],
},
'parent': 'base',
'post_review': None,
'protected': None,
'required_projects': [],
'roles': [common_config_role],
'semaphore': None,
'source_context': source_ctx,
'timeout': None,
'variables': {},
'variant_description': 'stable',
'voting': True
}], data)
def test_web_keys(self):
with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f:
public_pem = f.read()

View File

@ -29,7 +29,6 @@ from zuul.lib import yamlutil as yaml
import zuul.manager.dependent
import zuul.manager.independent
import zuul.manager.supercedent
from zuul import change_matcher
from zuul.lib import encryption
@ -771,16 +770,9 @@ class JobParser(object):
if branches:
job.setBranchMatcher(branches)
if 'files' in conf:
matchers = []
for fn in as_list(conf['files']):
matchers.append(change_matcher.FileMatcher(fn))
job.file_matcher = change_matcher.MatchAny(matchers)
job.setFileMatcher(as_list(conf['files']))
if 'irrelevant-files' in conf:
matchers = []
for fn in as_list(conf['irrelevant-files']):
matchers.append(change_matcher.FileMatcher(fn))
job.irrelevant_file_matcher = change_matcher.MatchAllFiles(
matchers)
job.setIrrelevantFileMatcher(as_list(conf['irrelevant-files']))
job.freeze()
return job

View File

@ -493,6 +493,13 @@ class Project(object):
def getSafeAttributes(self):
return Attributes(name=self.name)
def toDict(self):
d = {}
d['name'] = self.name
d['connection_name'] = self.connection_name
d['canonical_name'] = self.canonical_name
return d
class Node(ConfigObject):
"""A single node for use by a job.
@ -549,13 +556,18 @@ class Node(ConfigObject):
self.label == other.label and
self.id == other.id)
def toDict(self):
def toDict(self, internal_attributes=False):
d = {}
d['state'] = self.state
d['hold_job'] = self.hold_job
d['comment'] = self.comment
for k in self._keys:
d[k] = getattr(self, k)
if internal_attributes:
# These attributes are only useful for the rpc serialization
d['name'] = self.name[0]
d['aliases'] = self.name[1:]
d['label'] = self.label
return d
def updateFromDict(self, data):
@ -626,6 +638,17 @@ class NodeSet(ConfigObject):
return (self.name == other.name and
self.nodes == other.nodes)
def toDict(self):
d = {}
d['name'] = self.name
d['nodes'] = []
for node in self.nodes.values():
d['nodes'].append(node.toDict(internal_attributes=True))
d['groups'] = []
for group in self.groups.values():
d['groups'].append(group.toDict())
return d
def copy(self):
n = NodeSet(self.name)
for name, node in self.nodes.items():
@ -1058,6 +1081,10 @@ class Job(ConfigObject):
description=None,
variant_description=None,
protected_origin=None,
_branches=(),
_implied_branch=None,
_files=(),
_irrelevant_files=(),
)
self.inheritable_attributes = {}
@ -1069,6 +1096,49 @@ class Job(ConfigObject):
self.name = name
def toDict(self, tenant):
'''
Convert a Job object's attributes to a dictionary.
'''
d = {}
d['name'] = self.name
d['branches'] = self._branches
d['files'] = self._files
d['irrelevant_files'] = self._irrelevant_files
d['variant_description'] = self.variant_description
d['implied_branch'] = self._implied_branch
d['source_context'] = self.source_context.toDict()
d['description'] = self.description
d['required_projects'] = []
for project in self.required_projects:
d['required_projects'].append(project.toDict())
d['semaphore'] = self.semaphore
d['variables'] = self.variables
d['final'] = self.final
d['abstract'] = self.abstract
d['protected'] = self.protected
d['voting'] = self.voting
d['timeout'] = self.timeout
d['attempts'] = self.attempts
d['roles'] = list(map(lambda x: x.toDict(), self.roles))
d['post_review'] = self.post_review
if self.isBase():
d['parent'] = None
elif self.parent:
d['parent'] = self.parent
else:
d['parent'] = tenant.default_base_job
d['dependencies'] = []
for dependency in self.dependencies:
d['dependencies'].append(dependency)
if isinstance(self.nodeset, str):
ns = tenant.layout.nodesets.get(self.nodeset)
else:
ns = self.nodeset
if ns:
d['nodeset'] = ns.toDict()
return d
def __ne__(self, other):
return not self.__eq__(other)
@ -1182,11 +1252,28 @@ class Job(ConfigObject):
def setBranchMatcher(self, branches):
# Set the branch matcher to match any of the supplied branches
self._branches = branches
matchers = []
for branch in branches:
matchers.append(change_matcher.BranchMatcher(branch))
self.branch_matcher = change_matcher.MatchAny(matchers)
def setFileMatcher(self, files):
# Set the file matcher to match any of the change files
self._files = files
matchers = []
for fn in files:
matchers.append(change_matcher.FileMatcher(fn))
self.file_matcher = change_matcher.MatchAny(matchers)
def setIrrelevantFileMatcher(self, irrelevant_files):
# Set the irrelevant file matcher to match any of the change files
self._irrelevant_files = irrelevant_files
matchers = []
for fn in irrelevant_files:
matchers.append(change_matcher.FileMatcher(fn))
self.irrelevant_file_matcher = change_matcher.MatchAllFiles(matchers)
def getSimpleBranchMatcher(self):
# If the job has a simple branch matcher, return it; otherwise None.
if not self.branch_matcher:
@ -1204,6 +1291,7 @@ class Job(ConfigObject):
def addImpliedBranchMatcher(self, branch):
# Add a branch matcher that combines as a boolean *and* with
# existing branch matchers, if any.
self._implied_branch = branch
matchers = [change_matcher.ImpliedBranchMatcher(branch)]
if self.branch_matcher:
matchers.append(self.branch_matcher)
@ -1415,6 +1503,13 @@ class JobProject(ConfigObject):
self.override_branch = override_branch
self.override_checkout = override_checkout
def toDict(self):
d = dict()
d['project_name'] = self.project_name
d['override_branch'] = self.override_branch
d['override_checkout'] = self.override_checkout
return d
class JobList(ConfigObject):
""" A list of jobs in a project's pipeline. """

View File

@ -17,6 +17,7 @@ import json
import logging
import threading
import traceback
import types
import gear
@ -25,6 +26,13 @@ from zuul.lib import encryption
from zuul.lib.config import get_default
class MappingProxyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, types.MappingProxyType):
return dict(obj)
return json.JSONEncoder.default(self, obj)
class RPCListener(object):
log = logging.getLogger("zuul.RPCListener")
@ -61,6 +69,7 @@ class RPCListener(object):
self.worker.registerFunction("zuul:tenant_list")
self.worker.registerFunction("zuul:tenant_sql_connection")
self.worker.registerFunction("zuul:status_get")
self.worker.registerFunction("zuul:job_get")
self.worker.registerFunction("zuul:job_list")
self.worker.registerFunction("zuul:key_get")
self.worker.registerFunction("zuul:config_errors_list")
@ -353,6 +362,18 @@ class RPCListener(object):
output = self.sched.formatStatusJSON(args.get("tenant"))
job.sendWorkComplete(output)
def handle_job_get(self, gear_job):
args = json.loads(gear_job.arguments)
tenant = self.sched.abide.tenants.get(args.get("tenant"))
if not tenant:
gear_job.sendWorkComplete(json.dumps(None))
return
jobs = tenant.layout.jobs.get(args.get("job"), [])
output = []
for job in jobs:
output.append(job.toDict(tenant))
gear_job.sendWorkComplete(json.dumps(output, cls=MappingProxyEncoder))
def handle_job_list(self, job):
args = json.loads(job.arguments)
tenant = self.sched.abide.tenants.get(args.get("tenant"))

View File

@ -288,6 +288,19 @@ class ZuulWebAPI(object):
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
def job(self, tenant, job_name):
job = self.rpc.submitJob(
'zuul:job_get', {'tenant': tenant, 'job': job_name})
ret = json.loads(job.data[0])
if not ret:
raise cherrypy.HTTPError(404, 'Job %s does not exist.' % job_name)
resp = cherrypy.response
resp.headers['Access-Control-Allow-Origin'] = '*'
return ret
@cherrypy.expose
@cherrypy.tools.save_params()
def key(self, tenant, project):
@ -471,6 +484,8 @@ class ZuulWeb(object):
controller=api, action='status_change')
route_map.connect('api', '/api/tenant/{tenant}/jobs',
controller=api, action='jobs')
route_map.connect('api', '/api/tenant/{tenant}/job/{job_name}',
controller=api, action='job')
route_map.connect('api', '/api/tenant/{tenant}/key/{project:.*}.pub',
controller=api, action='key')
route_map.connect('api', '/api/tenant/{tenant}/console-stream',