diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 19fcbe9f05..88fc5276b6 100755 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -372,6 +372,144 @@ class TestWeb(BaseTestWeb): 'voting': True }], data) + def test_web_project_list(self): + # can we fetch the list of projects + data = self.get_url('api/tenant/tenant-one/projects').json() + + expected_list = [ + {'name': 'common-config', 'type': 'config'}, + {'name': 'org/project', 'type': 'untrusted'}, + {'name': 'org/project1', 'type': 'untrusted'}, + {'name': 'org/project2', 'type': 'untrusted'} + ] + for p in expected_list: + p["canonical_name"] = "review.example.com/%s" % p["name"] + p["connection_name"] = "gerrit" + self.assertEqual(expected_list, data) + + def test_web_project_get(self): + # can we fetch project details + data = self.get_url( + 'api/tenant/tenant-one/project/org/project1').json() + + jobs = [[{'abstract': False, + 'attempts': 3, + 'branches': [], + 'dependencies': [], + 'description': None, + 'files': [], + 'final': False, + 'implied_branch': None, + 'irrelevant_files': [], + 'name': 'project-merge', + 'parent': 'base', + 'post_review': None, + 'protected': None, + 'required_projects': [], + 'roles': [], + 'semaphore': None, + 'source_context': { + 'branch': 'master', + 'path': 'zuul.yaml', + 'project': 'common-config'}, + 'timeout': None, + 'variables': {}, + 'variant_description': '', + 'voting': True}], + [{'abstract': False, + 'attempts': 3, + 'branches': [], + 'dependencies': ['project-merge'], + 'description': None, + 'files': [], + 'final': False, + 'implied_branch': None, + 'irrelevant_files': [], + 'name': 'project-test1', + 'parent': 'base', + 'post_review': None, + 'protected': None, + 'required_projects': [], + 'roles': [], + 'semaphore': None, + 'source_context': { + 'branch': 'master', + 'path': 'zuul.yaml', + 'project': 'common-config'}, + 'timeout': None, + 'variables': {}, + 'variant_description': '', + 'voting': True}], + [{'abstract': False, + 'attempts': 3, + 'branches': [], + 'dependencies': ['project-merge'], + 'description': None, + 'files': [], + 'final': False, + 'implied_branch': None, + 'irrelevant_files': [], + 'name': 'project-test2', + 'parent': 'base', + 'post_review': None, + 'protected': None, + 'required_projects': [], + 'roles': [], + 'semaphore': None, + 'source_context': { + 'branch': 'master', + 'path': 'zuul.yaml', + 'project': 'common-config'}, + 'timeout': None, + 'variables': {}, + 'variant_description': '', + 'voting': True}], + [{'abstract': False, + 'attempts': 3, + 'branches': [], + 'dependencies': ['project-merge'], + 'description': None, + 'files': [], + 'final': False, + 'implied_branch': None, + 'irrelevant_files': [], + 'name': 'project1-project2-integration', + 'parent': 'base', + 'post_review': None, + 'protected': None, + 'required_projects': [], + 'roles': [], + 'semaphore': None, + 'source_context': { + 'branch': 'master', + 'path': 'zuul.yaml', + 'project': 'common-config'}, + 'timeout': None, + 'variables': {}, + 'variant_description': '', + 'voting': True}]] + + self.assertEqual( + { + 'canonical_name': 'review.example.com/org/project1', + 'connection_name': 'gerrit', + 'name': 'org/project1', + 'configs': [{ + 'templates': [], + 'default_branch': 'master', + 'merge_mode': 'merge-resolve', + 'pipelines': [{ + 'name': 'check', + 'queue_name': None, + 'jobs': jobs, + }, { + 'name': 'gate', + 'queue_name': 'integrated', + 'jobs': jobs, + }] + }] + }, data) + def test_web_keys(self): with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f: public_pem = f.read() diff --git a/zuul/model.py b/zuul/model.py index 495090b035..eddf5af707 100644 --- a/zuul/model.py +++ b/zuul/model.py @@ -2815,6 +2815,11 @@ class ProjectPipelineConfig(ConfigObject): # only come from templates. self.variables = Job._deepUpdate(self.variables, other) + def toDict(self): + d = {} + d['queue_name'] = self.queue_name + return d + class ProjectConfig(ConfigObject): # Represents a project configuration @@ -2865,6 +2870,17 @@ class ProjectConfig(ConfigObject): return False return True + def toDict(self): + d = {} + d['default_branch'] = self.default_branch + if self.merge_mode: + d['merge_mode'] = list(filter(lambda x: x[1] == self.merge_mode, + MERGER_MAP.items()))[0][0] + else: + d['merge_mode'] = None + d['templates'] = self.templates + return d + class ProjectMetadata(object): """Information about a Project diff --git a/zuul/rpclistener.py b/zuul/rpclistener.py index 9c0a4e977f..382317b5c6 100644 --- a/zuul/rpclistener.py +++ b/zuul/rpclistener.py @@ -71,6 +71,8 @@ class RPCListener(object): self.worker.registerFunction("zuul:status_get") self.worker.registerFunction("zuul:job_get") self.worker.registerFunction("zuul:job_list") + self.worker.registerFunction("zuul:project_get") + self.worker.registerFunction("zuul:project_list") self.worker.registerFunction("zuul:key_get") self.worker.registerFunction("zuul:config_errors_list") @@ -390,6 +392,55 @@ class RPCListener(object): "description": desc}) job.sendWorkComplete(json.dumps(output)) + def handle_project_get(self, gear_job): + args = json.loads(gear_job.arguments) + tenant = self.sched.abide.tenants.get(args["tenant"]) + if not tenant: + gear_job.sendWorkComplete(json.dumps(None)) + return + trusted, project = tenant.getProject(args["project"]) + if not project: + gear_job.sendWorkComplete(json.dumps({})) + return + result = project.toDict() + result['configs'] = [] + configs = tenant.layout.getAllProjectConfigs(project.canonical_name) + for config_obj in configs: + config = config_obj.toDict() + config['pipelines'] = [] + for pipeline_name, pipeline_config in sorted( + config_obj.pipelines.items()): + pipeline = pipeline_config.toDict() + pipeline['name'] = pipeline_name + pipeline['jobs'] = [] + for jobs in pipeline_config.job_list.jobs.values(): + job_list = [] + for job in jobs: + job_list.append(job.toDict(tenant)) + pipeline['jobs'].append(job_list) + config['pipelines'].append(pipeline) + result['configs'].append(config) + + gear_job.sendWorkComplete(json.dumps(result, cls=MappingProxyEncoder)) + + def handle_project_list(self, job): + args = json.loads(job.arguments) + tenant = self.sched.abide.tenants.get(args.get("tenant")) + if not tenant: + job.sendWorkComplete(json.dumps(None)) + return + output = [] + for project in tenant.config_projects: + pobj = project.toDict() + pobj['type'] = "config" + output.append(pobj) + for project in tenant.untrusted_projects: + pobj = project.toDict() + pobj['type'] = "untrusted" + output.append(pobj) + job.sendWorkComplete(json.dumps( + sorted(output, key=lambda project: project["name"]))) + def handle_key_get(self, job): args = json.loads(job.arguments) tenant = self.sched.abide.tenants.get(args.get("tenant")) diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index c90f9cb8e3..4d583a8e02 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -304,6 +304,34 @@ 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 projects(self, tenant): + job = self.rpc.submitJob('zuul:project_list', {'tenant': tenant}) + ret = json.loads(job.data[0]) + if ret is None: + raise cherrypy.HTTPError(404, 'Tenant %s does not exist.' % tenant) + resp = cherrypy.response + 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 project(self, tenant, project): + job = self.rpc.submitJob( + 'zuul:project_get', {'tenant': tenant, 'project': project}) + ret = json.loads(job.data[0]) + if ret is None: + raise cherrypy.HTTPError(404, 'Tenant %s does not exist.' % tenant) + if not ret: + raise cherrypy.HTTPError( + 404, 'Project %s does not exist.' % project) + resp = cherrypy.response + resp.headers['Access-Control-Allow-Origin'] = '*' + return ret + @cherrypy.expose @cherrypy.tools.save_params() def key(self, tenant, project): @@ -530,6 +558,10 @@ class ZuulWeb(object): 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}/projects', + controller=api, action='projects') + route_map.connect('api', '/api/tenant/{tenant}/project/{project:.*}', + controller=api, action='project') route_map.connect('api', '/api/tenant/{tenant}/key/{project:.*}.pub', controller=api, action='key') route_map.connect('api', '/api/tenant/{tenant}/'