web: add /{tenant}/job/{job_name} route
This change adds a job_get function to the scheduler gearman worker so that zuul-web can request job's details. Co-Authored-By: Tristan Cacqueray <tdecacqu@redhat.com> Change-Id: I759aac32b868c53fe0cb87209bba11acde4e27f9
This commit is contained in:
parent
c2c5ce26bf
commit
bd28fa4d09
|
@ -257,6 +257,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()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
@ -768,16 +767,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
|
||||
|
||||
|
|
|
@ -492,6 +492,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.
|
||||
|
@ -548,13 +555,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):
|
||||
|
@ -625,6 +637,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():
|
||||
|
@ -1057,6 +1080,10 @@ class Job(ConfigObject):
|
|||
description=None,
|
||||
variant_description=None,
|
||||
protected_origin=None,
|
||||
_branches=(),
|
||||
_implied_branch=None,
|
||||
_files=(),
|
||||
_irrelevant_files=(),
|
||||
)
|
||||
|
||||
self.inheritable_attributes = {}
|
||||
|
@ -1068,6 +1095,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)
|
||||
|
||||
|
@ -1181,11 +1251,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:
|
||||
|
@ -1203,6 +1290,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)
|
||||
|
@ -1414,6 +1502,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. """
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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',
|
||||
|
|
Loading…
Reference in New Issue