Merge "Support promote via tenant scoped rest api"

This commit is contained in:
Zuul 2020-08-01 07:05:00 +00:00 committed by Gerrit Code Review
commit c9a2f1086c
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',