From 79889887bc1a1e3594e0efb4eeb95d1186d13d06 Mon Sep 17 00:00:00 2001 From: Tobias Henkel Date: Fri, 24 Jul 2020 12:22:59 +0200 Subject: [PATCH] Support promote via tenant scoped rest api The tenant scoped rest api already supports enqueue, dequeue and some others. For project admins it's also useful to be able to use promote to push high priority changes to the front of the gate. Change-Id: I27e06ea2fc813c6f084fd01a2e9af284d3b15b89 --- .../discussion/tenant-scoped-rest-api.rst | 6 +- doc/source/reference/client.rst | 2 - ...dmin_web_api_promote-b72d137af109341c.yaml | 5 ++ tests/unit/test_web.py | 86 +++++++++++++++++++ zuul/cmd/client.py | 16 +++- zuul/web/__init__.py | 41 +++++++++ 6 files changed, 148 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/admin_web_api_promote-b72d137af109341c.yaml diff --git a/doc/source/discussion/tenant-scoped-rest-api.rst b/doc/source/discussion/tenant-scoped-rest-api.rst index 601de6e7b4..0bf586ffde 100644 --- a/doc/source/discussion/tenant-scoped-rest-api.rst +++ b/doc/source/discussion/tenant-scoped-rest-api.rst @@ -8,9 +8,9 @@ Tenant Scoped REST API Users can perform some privileged actions at the tenant level through protected endpoints of the REST API, if these endpoints are activated. -The supported actions are **autohold**, **enqueue/enqueue-ref** and -**dequeue/dequeue-ref**. These are similar to the ones available through Zuul's -CLI. +The supported actions are **autohold**, **enqueue/enqueue-ref**, +**dequeue/dequeue-ref** and **promote**. These are similar to the ones available +through Zuul's CLI. The protected endpoints require a bearer token, passed to Zuul Web Server as the **Authorization** header of the request. The token and this workflow follow the diff --git a/doc/source/reference/client.rst b/doc/source/reference/client.rst index 4084c2c1a7..6939648df0 100644 --- a/doc/source/reference/client.rst +++ b/doc/source/reference/client.rst @@ -141,8 +141,6 @@ for these more advanced operations. Promote ^^^^^^^ -.. note:: This command is only available through a Gearman connection. - .. program-output:: zuul promote --help Example:: diff --git a/releasenotes/notes/admin_web_api_promote-b72d137af109341c.yaml b/releasenotes/notes/admin_web_api_promote-b72d137af109341c.yaml new file mode 100644 index 0000000000..bdd2e5c91b --- /dev/null +++ b/releasenotes/notes/admin_web_api_promote-b72d137af109341c.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The action "promote" is now available via the tenant-scoped REST API using + the zuul cli. diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 0cfba4d799..e4d0498d2f 100644 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -2220,3 +2220,89 @@ class TestCLIViaWebApi(BaseTestWeb): self.executor_server.release() self.waitUntilSettled() self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1) + + def test_promote(self): + "Test that the RPC client can promote a change" + self.executor_server.hold_jobs_in_build = True + A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') + B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B') + C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C') + A.addApproval('Code-Review', 2) + B.addApproval('Code-Review', 2) + C.addApproval('Code-Review', 2) + + self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) + self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) + self.fake_gerrit.addEvent(C.addApproval('Approved', 1)) + + self.waitUntilSettled() + + tenant = self.scheds.first.sched.abide.tenants.get('tenant-one') + items = tenant.layout.pipelines['gate'].getAllItems() + enqueue_times = {} + for item in items: + enqueue_times[str(item.change)] = item.enqueue_time + + # Promote B and C using the cli + authz = {'iss': 'zuul_operator', + 'aud': 'zuul.example.com', + 'sub': 'testuser', + 'zuul': { + 'admin': ['tenant-one', ] + }, + 'exp': time.time() + 3600} + token = jwt.encode(authz, key='NoDanaOnlyZuul', + algorithm='HS256').decode('utf-8') + p = subprocess.Popen( + [os.path.join(sys.prefix, 'bin/zuul'), + '--zuul-url', self.base_url, '--auth-token', token, + 'promote', '--tenant', 'tenant-one', + '--pipeline', 'gate', '--changes', '2,1', '3,1'], + stdout=subprocess.PIPE) + output = p.communicate() + self.assertEqual(p.returncode, 0, output[0]) + self.waitUntilSettled() + + # ensure that enqueue times are durable + items = tenant.layout.pipelines['gate'].getAllItems() + for item in items: + self.assertEqual( + enqueue_times[str(item.change)], item.enqueue_time) + + self.waitUntilSettled() + self.executor_server.release('.*-merge') + self.waitUntilSettled() + self.executor_server.release('.*-merge') + self.waitUntilSettled() + self.executor_server.release('.*-merge') + self.waitUntilSettled() + + self.assertEqual(len(self.builds), 6) + self.assertEqual(self.builds[0].name, 'project-test1') + self.assertEqual(self.builds[1].name, 'project-test2') + self.assertEqual(self.builds[2].name, 'project-test1') + self.assertEqual(self.builds[3].name, 'project-test2') + self.assertEqual(self.builds[4].name, 'project-test1') + self.assertEqual(self.builds[5].name, 'project-test2') + + self.assertTrue(self.builds[0].hasChanges(B)) + self.assertFalse(self.builds[0].hasChanges(A)) + self.assertFalse(self.builds[0].hasChanges(C)) + + self.assertTrue(self.builds[2].hasChanges(B)) + self.assertTrue(self.builds[2].hasChanges(C)) + self.assertFalse(self.builds[2].hasChanges(A)) + + self.assertTrue(self.builds[4].hasChanges(B)) + self.assertTrue(self.builds[4].hasChanges(C)) + self.assertTrue(self.builds[4].hasChanges(A)) + + self.executor_server.release() + self.waitUntilSettled() + + self.assertEqual(A.data['status'], 'MERGED') + self.assertEqual(A.reported, 2) + self.assertEqual(B.data['status'], 'MERGED') + self.assertEqual(B.reported, 2) + self.assertEqual(C.data['status'], 'MERGED') + self.assertEqual(C.reported, 2) diff --git a/zuul/cmd/client.py b/zuul/cmd/client.py index db535b888b..f9515b32aa 100755 --- a/zuul/cmd/client.py +++ b/zuul/cmd/client.py @@ -140,9 +140,19 @@ class ZuulRESTClient(object): self._check_status(req) return req.json() - def promote(self, *args, **kwargs): - raise NotImplementedError( - 'This action is unsupported by the REST API') + def promote(self, tenant, pipeline, change_ids): + if not self.auth_token: + raise Exception('Auth Token required') + args = { + "pipeline": pipeline, + "changes": change_ids, + } + url = urllib.parse.urljoin( + self.base_url, + 'tenant/%s/promote' % tenant) + req = self.session.post(url, json=args) + self._check_status(req) + return req.json() def get_running_jobs(self, *args, **kwargs): raise NotImplementedError( diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index 82362b08de..6f41a961b0 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -371,6 +371,42 @@ class ZuulWebAPI(object): resp.headers['Access-Control-Allow-Origin'] = '*' return result + @cherrypy.expose + @cherrypy.tools.json_in() + @cherrypy.tools.json_out(content_type='application/json; charset=utf-8') + @cherrypy.tools.handle_options(allowed_methods=['POST', ]) + def promote(self, tenant): + basic_error = self._basic_auth_header_check() + if basic_error is not None: + return basic_error + if cherrypy.request.method != 'POST': + raise cherrypy.HTTPError(405) + # AuthN/AuthZ + claims, token_error = self._auth_token_check() + if token_error is not None: + return token_error + self.is_authorized(claims, tenant) + + body = cherrypy.request.json + pipeline = body.get('pipeline') + changes = body.get('changes') + + msg = 'User "%s" requesting "%s" on %s/%s' + self.log.info( + msg % (claims['__zuul_uid_claim'], 'promote', + tenant, pipeline)) + + job = self.rpc.submitJob('zuul:promote', + { + 'tenant': tenant, + 'pipeline': pipeline, + 'change_ids': changes, + }) + result = not job.failure + resp = cherrypy.response + resp.headers['Access-Control-Allow-Origin'] = '*' + return result + @cherrypy.expose @cherrypy.tools.json_out(content_type='application/json; charset=utf-8') def autohold_list(self, tenant, *args, **kwargs): @@ -563,6 +599,7 @@ class ZuulWebAPI(object): 'autohold_delete': '/api/tenant/{tenant}/autohold/{request_id}', 'enqueue': '/api/tenant/{tenant}/project/{project:.*}/enqueue', 'dequeue': '/api/tenant/{tenant}/project/{project:.*}/dequeue', + 'promote': '/api/tenant/{tenant}/promote', } @cherrypy.expose @@ -1199,6 +1236,10 @@ class ZuulWeb(object): 'api', '/api/tenant/{tenant}/project/{project:.*}/dequeue', controller=api, action='dequeue') + route_map.connect( + 'api', + '/api/tenant/{tenant}/promote', + controller=api, action='promote') route_map.connect('api', '/api/tenant/{tenant}/autohold/{request_id}', controller=api, action='autohold_by_request_id') route_map.connect('api', '/api/tenant/{tenant}/autohold',