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
This commit is contained in:
Tobias Henkel 2020-07-24 12:22:59 +02:00
parent 4502f91807
commit 79889887bc
No known key found for this signature in database
GPG Key ID: 03750DEC158E5FA2
6 changed files with 148 additions and 8 deletions

View File

@ -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

View File

@ -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::

View File

@ -0,0 +1,5 @@
---
features:
- |
The action "promote" is now available via the tenant-scoped REST API using
the zuul cli.

View File

@ -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)

View File

@ -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(

View File

@ -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',