Add autohold delete/info commands to web API

The autohold-delete and autohold-info CLI commands were added
in earlier changes. This adds support for them to the web API.

Change-Id: I2bfb3dbeb34221c978964afd63be536df9e11229
This commit is contained in:
David Shrewsbury 2019-08-28 09:31:34 -04:00
parent 9f5743366d
commit 4931eed2b8
2 changed files with 180 additions and 3 deletions

View File

@ -89,6 +89,10 @@ class BaseTestWeb(ZuulTestCase):
return requests.post(
urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
def delete_url(self, url, *args, **kwargs):
return requests.delete(
urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
def tearDown(self):
self.executor_server.hold_jobs_in_build = False
self.executor_server.release()
@ -746,6 +750,44 @@ class TestWeb(BaseTestWeb):
resp = self.get_url("api/tenant/non-tenant/status")
self.assertEqual(404, resp.status_code)
def test_autohold_info_404_on_invalid_id(self):
resp = self.get_url("api/tenant/tenant-one/autohold/12345")
self.assertEqual(404, resp.status_code)
def test_autohold_delete_404_on_invalid_id(self):
resp = self.delete_url("api/tenant/tenant-one/autohold/12345")
self.assertEqual(404, resp.status_code)
def test_autohold_info(self):
client = zuul.rpcclient.RPCClient('127.0.0.1',
self.gearman_server.port)
self.addCleanup(client.shutdown)
r = client.autohold('tenant-one', 'org/project', 'project-test2',
"", "", "reason text", 1)
self.assertTrue(r)
# Use autohold-list API to retrieve request ID
resp = self.get_url(
"api/tenant/tenant-one/autohold")
self.assertEqual(200, resp.status_code, resp.text)
autohold_requests = resp.json()
self.assertNotEqual([], autohold_requests)
self.assertEqual(1, len(autohold_requests))
request_id = autohold_requests[0]['id']
# Now try the autohold-info API
resp = self.get_url("api/tenant/tenant-one/autohold/%s" % request_id)
self.assertEqual(200, resp.status_code, resp.text)
request = resp.json()
self.assertEqual(request_id, request['id'])
self.assertEqual('tenant-one', request['tenant'])
self.assertIn('org/project', request['project'])
self.assertEqual('project-test2', request['job'])
self.assertEqual(".*", request['ref_filter'])
self.assertEqual(1, request['count'])
self.assertEqual("reason text", request['reason'])
def test_autohold_list(self):
"""test listing autoholds through zuul-web"""
client = zuul.rpcclient.RPCClient('127.0.0.1',
@ -761,7 +803,6 @@ class TestWeb(BaseTestWeb):
self.assertNotEqual([], autohold_requests)
self.assertEqual(1, len(autohold_requests))
# The single dict key should be a CSV string value
ah_request = autohold_requests[0]
self.assertEqual('tenant-one', ah_request['tenant'])
@ -784,7 +825,6 @@ class TestWeb(BaseTestWeb):
self.assertNotEqual([], autohold_requests)
self.assertEqual(1, len(autohold_requests))
# The single dict key should be a CSV string value
ah_request = autohold_requests[0]
self.assertEqual('tenant-one', ah_request['tenant'])
@ -1265,6 +1305,38 @@ class TestTenantScopedWebApi(BaseTestWeb):
'pipeline': 'check'})
self.assertEqual(403, resp.status_code)
# For autohold-delete, we first must make sure that an autohold
# exists before the delete attempt.
good_authz = {'iss': 'zuul_operator',
'aud': 'zuul.example.com',
'sub': 'testuser',
'zuul': {'admin': ['tenant-one', ]},
'exp': time.time() + 3600}
args = {"reason": "some reason",
"count": 1,
'job': 'project-test2',
'change': None,
'ref': None,
'node_hold_expiration': None}
good_token = jwt.encode(good_authz, key='NoDanaOnlyZuul',
algorithm='HS256').decode('utf-8')
req = self.post_url(
'api/tenant/tenant-one/project/org/project/autohold',
headers={'Authorization': 'Bearer %s' % good_token},
json=args)
self.assertEqual(200, req.status_code, req.text)
client = zuul.rpcclient.RPCClient('127.0.0.1',
self.gearman_server.port)
self.addCleanup(client.shutdown)
autohold_requests = client.autohold_list()
self.assertNotEqual([], autohold_requests)
self.assertEqual(1, len(autohold_requests))
request = autohold_requests[0]
resp = self.delete_url(
"api/tenant/tenant-one/autohold/%s" % request['id'],
headers={'Authorization': 'Bearer %s' % token})
self.assertEqual(403, resp.status_code)
def test_autohold(self):
"""Test that autohold can be set through the admin web interface"""
args = {"reason": "some reason",
@ -1305,6 +1377,46 @@ class TestTenantScopedWebApi(BaseTestWeb):
self.assertEqual("some reason", request['reason'])
self.assertEqual(1, request['max_count'])
def test_autohold_delete(self):
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')
client = zuul.rpcclient.RPCClient('127.0.0.1',
self.gearman_server.port)
self.addCleanup(client.shutdown)
r = client.autohold('tenant-one', 'org/project', 'project-test2',
"", "", "reason text", 1)
self.assertTrue(r)
# Use autohold-list API to retrieve request ID
resp = self.get_url(
"api/tenant/tenant-one/autohold")
self.assertEqual(200, resp.status_code, resp.text)
autohold_requests = resp.json()
self.assertNotEqual([], autohold_requests)
self.assertEqual(1, len(autohold_requests))
request_id = autohold_requests[0]['id']
# now try the autohold-delete API
resp = self.delete_url(
"api/tenant/tenant-one/autohold/%s" % request_id,
headers={'Authorization': 'Bearer %s' % token})
self.assertEqual(204, resp.status_code, resp.text)
# autohold-list should be empty now
resp = self.get_url(
"api/tenant/tenant-one/autohold")
self.assertEqual(200, resp.status_code, resp.text)
autohold_requests = resp.json()
self.assertEqual([], autohold_requests)
def test_enqueue(self):
"""Test that the admin web interface can enqueue a change"""
A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')

View File

@ -422,7 +422,8 @@ class ZuulWebAPI(object):
if (project is None or
request['project'].endswith(project)):
result.append(
{'tenant': request['tenant'],
{'id': request['id'],
'tenant': request['tenant'],
'project': request['project'],
'job': request['job'],
'ref_filter': request['ref_filter'],
@ -432,6 +433,68 @@ class ZuulWebAPI(object):
})
return result
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
def autohold_by_request_id(self, tenant, request_id):
if cherrypy.request.method == 'GET':
return self._autohold_info(request_id)
elif cherrypy.request.method == 'DELETE':
return self._autohold_delete(request_id)
else:
raise cherrypy.HTTPError(405)
def _autohold_info(self, request_id):
job = self.rpc.submitJob('zuul:autohold_info',
{'request_id': request_id})
if job.failure:
raise cherrypy.HTTPError(500, 'autohold-info failed')
else:
request = json.loads(job.data[0])
if not request:
raise cherrypy.HTTPError(
404, 'Hold request %s does not exist.' % request_id)
return {
'id': request['id'],
'tenant': request['tenant'],
'project': request['project'],
'job': request['job'],
'ref_filter': request['ref_filter'],
'count': request['max_count'],
'reason': request['reason'],
'node_hold_expiration': request['node_expiration']
}
def _autohold_delete(self, request_id):
# We need tenant info from the request for authz
request = self._autohold_info(request_id)
basic_error = self._basic_auth_header_check()
if basic_error is not None:
return basic_error
# AuthN/AuthZ
rawToken = cherrypy.request.headers['Authorization'][len('Bearer '):]
try:
claims = self.zuulweb.authenticators.authenticate(rawToken)
except exceptions.AuthTokenException as e:
for header, contents in e.getAdditionalHeaders().items():
cherrypy.response.headers[header] = contents
cherrypy.response.status = e.HTTPError
return {'description': e.error_description,
'error': e.error,
'realm': e.realm}
self.is_authorized(claims, request['tenant'])
msg = 'User "%s" requesting "%s" on %s/%s'
self.log.info(
msg % (claims['__zuul_uid_claim'], 'autohold-delete',
request['tenant'], request['project']))
job = self.rpc.submitJob('zuul:autohold_delete',
{'request_id': request_id})
if job.failure:
raise cherrypy.HTTPError(500, 'autohold-delete failed')
cherrypy.response.status = 204
@cherrypy.expose
@cherrypy.tools.json_out(content_type='application/json; charset=utf-8')
def index(self):
@ -1037,6 +1100,8 @@ class ZuulWeb(object):
'api',
'/api/tenant/{tenant}/project/{project:.*}/dequeue',
controller=api, action='dequeue')
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',
controller=api, action='autohold_list')
route_map.connect('api', '/api/tenant/{tenant}/projects',