diff --git a/doc/source/reference/tenants.rst b/doc/source/reference/tenants.rst index a585986f58..8511295c8c 100644 --- a/doc/source/reference/tenants.rst +++ b/doc/source/reference/tenants.rst @@ -362,8 +362,8 @@ Access Rule An access rule is a set of conditions the claims of a user's JWT must match in order to be allowed to perform protected actions at a tenant's level. -The protected actions available at tenant level are **autohold**, **enqueue** -or **dequeue**. +The protected actions available at tenant level are **autohold**, **enqueue**, +**dequeue** or **promote**. .. note:: diff --git a/tests/unit/test_web.py b/tests/unit/test_web.py index 6c53ada058..e58d6d743d 100644 --- a/tests/unit/test_web.py +++ b/tests/unit/test_web.py @@ -1726,6 +1726,9 @@ class TestTenantScopedWebApi(BaseTestWeb): # Note that %tenant, %project are not relevant here. The client is # just checking what the endpoint allows. endpoints = [ + {'action': 'promote', + 'path': 'api/tenant/my-tenant/promote', + 'allowed_methods': ['POST', ]}, {'action': 'enqueue', 'path': 'api/tenant/my-tenant/project/my-project/enqueue', 'allowed_methods': ['POST', ]}, @@ -1780,6 +1783,94 @@ class TestTenantScopedWebApi(BaseTestWeb): allowed_methods, endpoint['allowed_methods'])) + def test_promote(self): + """Test that a change can be promoted via the admin web interface""" + # taken from test_client_promote in test_scheduler + 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 + + # REST API + args = {'pipeline': 'gate', + 'changes': ['2,1', '3,1']} + authz = {'iss': 'zuul_operator', + 'aud': 'zuul.example.com', + 'sub': 'testuser', + 'zuul': { + 'admin': ['tenant-one', ], + }, + 'exp': time.time() + 3600, + 'iat': time.time()} + token = jwt.encode(authz, key='NoDanaOnlyZuul', + algorithm='HS256').decode('utf-8') + req = self.post_url( + 'api/tenant/tenant-one/promote', + headers={'Authorization': 'Bearer %s' % token}, + json=args) + self.assertEqual(200, req.status_code, req.text) + data = req.json() + self.assertEqual(True, data) + + # 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) + class TestTenantScopedWebApiWithAuthRules(BaseTestWeb): config_file = 'zuul-admin-web-no-override.conf' diff --git a/zuul/web/__init__.py b/zuul/web/__init__.py index fe3e7dc551..c5aecb440b 100755 --- a/zuul/web/__init__.py +++ b/zuul/web/__init__.py @@ -1271,6 +1271,8 @@ class ZuulWeb(object): route_map.connect('api', '/api/tenant/{tenant}/authorizations', controller=api, action='tenant_authorizations') + route_map.connect('api', '/api/tenant/{tenant}/promote', + controller=api, action='promote') route_map.connect( 'api', '/api/tenant/{tenant}/project/{project:.*}/autohold', @@ -1283,10 +1285,6 @@ 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')