The Gatekeeper, or a project gating system
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

2380 lines
94 KiB

  1. # Copyright 2014 Hewlett-Packard Development Company, L.P.
  2. # Copyright 2014 Rackspace Australia
  3. #
  4. # Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. # not use this file except in compliance with the License. You may obtain
  6. # a copy of the License at
  7. #
  8. # http://www.apache.org/licenses/LICENSE-2.0
  9. #
  10. # Unless required by applicable law or agreed to in writing, software
  11. # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. # License for the specific language governing permissions and limitations
  14. # under the License.
  15. import json
  16. import os
  17. import urllib.parse
  18. import socket
  19. import textwrap
  20. import time
  21. import jwt
  22. import sys
  23. import subprocess
  24. import requests
  25. import zuul.web
  26. import zuul.rpcclient
  27. from tests.base import ZuulTestCase, ZuulDBTestCase, AnsibleZuulTestCase
  28. from tests.base import ZuulWebFixture, FIXTURE_DIR, iterate_timeout
  29. from tests.base import simple_layout
  30. class FakeConfig(object):
  31. def __init__(self, config):
  32. self.config = config or {}
  33. def has_option(self, section, option):
  34. return option in self.config.get(section, {})
  35. def get(self, section, option):
  36. return self.config.get(section, {}).get(option)
  37. class BaseTestWeb(ZuulTestCase):
  38. tenant_config_file = 'config/single-tenant/main.yaml'
  39. config_ini_data = {}
  40. def setUp(self):
  41. super(BaseTestWeb, self).setUp()
  42. self.zuul_ini_config = FakeConfig(self.config_ini_data)
  43. # Start the web server
  44. self.web = self.useFixture(
  45. ZuulWebFixture(self.gearman_server.port, self.changes, self.config,
  46. self.additional_event_queues, self.upstream_root,
  47. self.rpcclient, self.poller_events,
  48. self.git_url_with_auth, self.addCleanup,
  49. self.test_root,
  50. info=zuul.model.WebInfo.fromConfig(
  51. self.zuul_ini_config),
  52. zk_hosts=self.zk_config))
  53. self.executor_server.hold_jobs_in_build = True
  54. self.host = 'localhost'
  55. self.port = self.web.port
  56. # Wait until web server is started
  57. while True:
  58. try:
  59. with socket.create_connection((self.host, self.port)):
  60. break
  61. except ConnectionRefusedError:
  62. pass
  63. self.base_url = "http://{host}:{port}".format(
  64. host=self.host, port=self.port)
  65. def add_base_changes(self):
  66. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  67. A.addApproval('Code-Review', 2)
  68. self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
  69. B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B')
  70. B.addApproval('Code-Review', 2)
  71. self.fake_gerrit.addEvent(B.addApproval('Approved', 1))
  72. self.waitUntilSettled()
  73. def get_url(self, url, *args, **kwargs):
  74. return requests.get(
  75. urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
  76. def post_url(self, url, *args, **kwargs):
  77. return requests.post(
  78. urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
  79. def delete_url(self, url, *args, **kwargs):
  80. return requests.delete(
  81. urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
  82. def options_url(self, url, *args, **kwargs):
  83. return requests.options(
  84. urllib.parse.urljoin(self.base_url, url), *args, **kwargs)
  85. def tearDown(self):
  86. self.executor_server.hold_jobs_in_build = False
  87. self.executor_server.release()
  88. self.waitUntilSettled()
  89. super(BaseTestWeb, self).tearDown()
  90. class TestWeb(BaseTestWeb):
  91. def test_web_index(self):
  92. "Test that we can retrieve the index page"
  93. resp = self.get_url('api')
  94. data = resp.json()
  95. # no point checking the whole thing; just make sure _something_ we
  96. # expect is here
  97. self.assertIn('info', data)
  98. def test_web_status(self):
  99. "Test that we can retrieve JSON status info"
  100. self.add_base_changes()
  101. self.executor_server.hold_jobs_in_build = True
  102. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  103. A.addApproval('Code-Review', 2)
  104. self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
  105. self.waitUntilSettled()
  106. self.executor_server.release('project-merge')
  107. self.waitUntilSettled()
  108. resp = self.get_url("api/tenant/tenant-one/status")
  109. self.assertIn('Content-Length', resp.headers)
  110. self.assertIn('Content-Type', resp.headers)
  111. self.assertEqual(
  112. 'application/json; charset=utf-8', resp.headers['Content-Type'])
  113. self.assertIn('Access-Control-Allow-Origin', resp.headers)
  114. self.assertIn('Cache-Control', resp.headers)
  115. self.assertIn('Last-Modified', resp.headers)
  116. self.assertTrue(resp.headers['Last-Modified'].endswith(' GMT'))
  117. self.executor_server.hold_jobs_in_build = False
  118. self.executor_server.release()
  119. self.waitUntilSettled()
  120. data = resp.json()
  121. status_jobs = []
  122. for p in data['pipelines']:
  123. for q in p['change_queues']:
  124. if p['name'] in ['gate', 'conflict']:
  125. self.assertEqual(q['window'], 20)
  126. else:
  127. self.assertEqual(q['window'], 0)
  128. for head in q['heads']:
  129. for change in head:
  130. self.assertIn(
  131. 'review.example.com/org/project',
  132. change['project_canonical'])
  133. self.assertTrue(change['active'])
  134. self.assertIn(change['id'], ('1,1', '2,1', '3,1'))
  135. for job in change['jobs']:
  136. status_jobs.append(job)
  137. self.assertEqual('project-merge', status_jobs[0]['name'])
  138. # TODO(mordred) pull uuids from self.builds
  139. self.assertEqual(
  140. 'stream/{uuid}?logfile=console.log'.format(
  141. uuid=status_jobs[0]['uuid']),
  142. status_jobs[0]['url'])
  143. self.assertEqual(
  144. 'finger://{hostname}/{uuid}'.format(
  145. hostname=self.executor_server.hostname,
  146. uuid=status_jobs[0]['uuid']),
  147. status_jobs[0]['finger_url'])
  148. # TOOD(mordred) configure a success-url on the base job
  149. self.assertEqual(
  150. 'finger://{hostname}/{uuid}'.format(
  151. hostname=self.executor_server.hostname,
  152. uuid=status_jobs[0]['uuid']),
  153. status_jobs[0]['report_url'])
  154. self.assertEqual('project-test1', status_jobs[1]['name'])
  155. self.assertEqual(
  156. 'stream/{uuid}?logfile=console.log'.format(
  157. uuid=status_jobs[1]['uuid']),
  158. status_jobs[1]['url'])
  159. self.assertEqual(
  160. 'finger://{hostname}/{uuid}'.format(
  161. hostname=self.executor_server.hostname,
  162. uuid=status_jobs[1]['uuid']),
  163. status_jobs[1]['finger_url'])
  164. self.assertEqual(
  165. 'finger://{hostname}/{uuid}'.format(
  166. hostname=self.executor_server.hostname,
  167. uuid=status_jobs[1]['uuid']),
  168. status_jobs[1]['report_url'])
  169. self.assertEqual('project-test2', status_jobs[2]['name'])
  170. self.assertEqual(
  171. 'stream/{uuid}?logfile=console.log'.format(
  172. uuid=status_jobs[2]['uuid']),
  173. status_jobs[2]['url'])
  174. self.assertEqual(
  175. 'finger://{hostname}/{uuid}'.format(
  176. hostname=self.executor_server.hostname,
  177. uuid=status_jobs[2]['uuid']),
  178. status_jobs[2]['finger_url'])
  179. self.assertEqual(
  180. 'finger://{hostname}/{uuid}'.format(
  181. hostname=self.executor_server.hostname,
  182. uuid=status_jobs[2]['uuid']),
  183. status_jobs[2]['report_url'])
  184. # check job dependencies
  185. self.assertIsNotNone(status_jobs[0]['dependencies'])
  186. self.assertIsNotNone(status_jobs[1]['dependencies'])
  187. self.assertIsNotNone(status_jobs[2]['dependencies'])
  188. self.assertEqual(len(status_jobs[0]['dependencies']), 0)
  189. self.assertEqual(len(status_jobs[1]['dependencies']), 1)
  190. self.assertEqual(len(status_jobs[2]['dependencies']), 1)
  191. self.assertIn('project-merge', status_jobs[1]['dependencies'])
  192. self.assertIn('project-merge', status_jobs[2]['dependencies'])
  193. def test_web_tenants(self):
  194. "Test that we can retrieve JSON status info"
  195. self.add_base_changes()
  196. self.executor_server.hold_jobs_in_build = True
  197. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  198. A.addApproval('Code-Review', 2)
  199. self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
  200. self.waitUntilSettled()
  201. self.executor_server.release('project-merge')
  202. self.waitUntilSettled()
  203. resp = self.get_url("api/tenants")
  204. self.assertIn('Content-Length', resp.headers)
  205. self.assertIn('Content-Type', resp.headers)
  206. self.assertEqual(
  207. 'application/json; charset=utf-8', resp.headers['Content-Type'])
  208. # self.assertIn('Access-Control-Allow-Origin', resp.headers)
  209. # self.assertIn('Cache-Control', resp.headers)
  210. # self.assertIn('Last-Modified', resp.headers)
  211. data = resp.json()
  212. self.assertEqual('tenant-one', data[0]['name'])
  213. self.assertEqual(3, data[0]['projects'])
  214. self.assertEqual(3, data[0]['queue'])
  215. # release jobs and check if the queue size is 0
  216. self.executor_server.hold_jobs_in_build = False
  217. self.executor_server.release()
  218. self.waitUntilSettled()
  219. data = self.get_url("api/tenants").json()
  220. self.assertEqual('tenant-one', data[0]['name'])
  221. self.assertEqual(3, data[0]['projects'])
  222. self.assertEqual(0, data[0]['queue'])
  223. # test that non-live items are not counted
  224. self.executor_server.hold_jobs_in_build = True
  225. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  226. B = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  227. B.setDependsOn(A, 1)
  228. self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
  229. self.waitUntilSettled()
  230. req = urllib.request.Request(
  231. "http://127.0.0.1:%s/api/tenants" % self.port)
  232. f = urllib.request.urlopen(req)
  233. data = f.read().decode('utf8')
  234. data = json.loads(data)
  235. self.assertEqual('tenant-one', data[0]['name'])
  236. self.assertEqual(3, data[0]['projects'])
  237. self.assertEqual(1, data[0]['queue'])
  238. def test_web_connections_list(self):
  239. data = self.get_url('api/connections').json()
  240. connection = {
  241. 'driver': 'gerrit',
  242. 'name': 'gerrit',
  243. 'baseurl': 'https://review.example.com',
  244. 'canonical_hostname': 'review.example.com',
  245. 'server': 'review.example.com',
  246. 'port': 29418,
  247. }
  248. self.assertEqual([connection], data)
  249. def test_web_bad_url(self):
  250. # do we redirect to index.html
  251. resp = self.get_url("status/foo")
  252. self.assertEqual(200, resp.status_code)
  253. def test_web_find_change(self):
  254. # can we filter by change id
  255. self.add_base_changes()
  256. data = self.get_url("api/tenant/tenant-one/status/change/1,1").json()
  257. self.assertEqual(1, len(data), data)
  258. self.assertEqual("org/project", data[0]['project'])
  259. data = self.get_url("api/tenant/tenant-one/status/change/2,1").json()
  260. self.assertEqual(1, len(data), data)
  261. self.assertEqual("org/project1", data[0]['project'], data)
  262. def test_web_find_job(self):
  263. # can we fetch the variants for a single job
  264. data = self.get_url('api/tenant/tenant-one/job/project-test1').json()
  265. common_config_role = {
  266. 'implicit': True,
  267. 'project_canonical_name': 'review.example.com/common-config',
  268. 'target_name': 'common-config',
  269. 'type': 'zuul',
  270. }
  271. source_ctx = {
  272. 'branch': 'master',
  273. 'path': 'zuul.yaml',
  274. 'project': 'common-config',
  275. }
  276. run = [{
  277. 'path': 'playbooks/project-test1.yaml',
  278. 'roles': [{
  279. 'implicit': True,
  280. 'project_canonical_name': 'review.example.com/common-config',
  281. 'target_name': 'common-config',
  282. 'type': 'zuul'
  283. }],
  284. 'secrets': [],
  285. 'source_context': source_ctx,
  286. }]
  287. self.assertEqual([
  288. {
  289. 'name': 'project-test1',
  290. 'abstract': False,
  291. 'ansible_version': None,
  292. 'attempts': 4,
  293. 'branches': [],
  294. 'dependencies': [],
  295. 'description': None,
  296. 'files': [],
  297. 'intermediate': False,
  298. 'irrelevant_files': [],
  299. 'match_on_config_updates': True,
  300. 'final': False,
  301. 'nodeset': {
  302. 'groups': [],
  303. 'name': '',
  304. 'nodes': [{'comment': None,
  305. 'hold_job': None,
  306. 'label': 'label1',
  307. 'name': 'controller',
  308. 'aliases': [],
  309. 'state': 'unknown'}],
  310. },
  311. 'override_checkout': None,
  312. 'parent': 'base',
  313. 'post_review': None,
  314. 'protected': None,
  315. 'provides': [],
  316. 'required_projects': [],
  317. 'requires': [],
  318. 'roles': [common_config_role],
  319. 'run': run,
  320. 'pre_run': [],
  321. 'post_run': [],
  322. 'cleanup_run': [],
  323. 'semaphore': None,
  324. 'source_context': source_ctx,
  325. 'tags': [],
  326. 'timeout': None,
  327. 'variables': {},
  328. 'extra_variables': {},
  329. 'group_variables': {},
  330. 'host_variables': {},
  331. 'variant_description': '',
  332. 'voting': True
  333. }, {
  334. 'name': 'project-test1',
  335. 'abstract': False,
  336. 'ansible_version': None,
  337. 'attempts': 3,
  338. 'branches': ['stable'],
  339. 'dependencies': [],
  340. 'description': None,
  341. 'files': [],
  342. 'intermediate': False,
  343. 'irrelevant_files': [],
  344. 'match_on_config_updates': True,
  345. 'final': False,
  346. 'nodeset': {
  347. 'groups': [],
  348. 'name': '',
  349. 'nodes': [{'comment': None,
  350. 'hold_job': None,
  351. 'label': 'label2',
  352. 'name': 'controller',
  353. 'aliases': [],
  354. 'state': 'unknown'}],
  355. },
  356. 'override_checkout': None,
  357. 'parent': 'base',
  358. 'post_review': None,
  359. 'protected': None,
  360. 'provides': [],
  361. 'required_projects': [],
  362. 'requires': [],
  363. 'roles': [common_config_role],
  364. 'run': run,
  365. 'pre_run': [],
  366. 'post_run': [],
  367. 'cleanup_run': [],
  368. 'semaphore': None,
  369. 'source_context': source_ctx,
  370. 'tags': [],
  371. 'timeout': None,
  372. 'variables': {},
  373. 'extra_variables': {},
  374. 'group_variables': {},
  375. 'host_variables': {},
  376. 'variant_description': 'stable',
  377. 'voting': True
  378. }], data)
  379. data = self.get_url('api/tenant/tenant-one/job/test-job').json()
  380. run[0]['path'] = 'playbooks/project-merge.yaml'
  381. self.assertEqual([
  382. {
  383. 'abstract': False,
  384. 'ansible_version': None,
  385. 'attempts': 3,
  386. 'branches': [],
  387. 'dependencies': [],
  388. 'description': None,
  389. 'files': [],
  390. 'final': False,
  391. 'intermediate': False,
  392. 'irrelevant_files': [],
  393. 'match_on_config_updates': True,
  394. 'name': 'test-job',
  395. 'override_checkout': None,
  396. 'parent': 'base',
  397. 'post_review': None,
  398. 'protected': None,
  399. 'provides': [],
  400. 'required_projects': [
  401. {'override_branch': None,
  402. 'override_checkout': None,
  403. 'project_name': 'review.example.com/org/project'}],
  404. 'requires': [],
  405. 'roles': [common_config_role],
  406. 'run': run,
  407. 'pre_run': [],
  408. 'post_run': [],
  409. 'cleanup_run': [],
  410. 'semaphore': None,
  411. 'source_context': source_ctx,
  412. 'tags': [],
  413. 'timeout': None,
  414. 'variables': {},
  415. 'extra_variables': {},
  416. 'group_variables': {},
  417. 'host_variables': {},
  418. 'variant_description': '',
  419. 'voting': True
  420. }], data)
  421. def test_find_job_complete_playbooks(self):
  422. # can we fetch the variants for a single job
  423. data = self.get_url('api/tenant/tenant-one/job/complete-job').json()
  424. def expected_pb(path):
  425. return {
  426. 'path': path,
  427. 'roles': [{
  428. 'implicit': True,
  429. 'project_canonical_name':
  430. 'review.example.com/common-config',
  431. 'target_name': 'common-config',
  432. 'type': 'zuul'
  433. }],
  434. 'secrets': [],
  435. 'source_context': {
  436. 'branch': 'master',
  437. 'path': 'zuul.yaml',
  438. 'project': 'common-config',
  439. }
  440. }
  441. self.assertEqual([
  442. expected_pb("playbooks/run.yaml")
  443. ], data[0]['run'])
  444. self.assertEqual([
  445. expected_pb("playbooks/pre-run.yaml")
  446. ], data[0]['pre_run'])
  447. self.assertEqual([
  448. expected_pb("playbooks/post-run-01.yaml"),
  449. expected_pb("playbooks/post-run-02.yaml")
  450. ], data[0]['post_run'])
  451. self.assertEqual([
  452. expected_pb("playbooks/cleanup-run.yaml")
  453. ], data[0]['cleanup_run'])
  454. def test_web_nodes_list(self):
  455. # can we fetch the nodes list
  456. self.add_base_changes()
  457. data = self.get_url('api/tenant/tenant-one/nodes').json()
  458. self.assertGreater(len(data), 0)
  459. self.assertEqual("test-provider", data[0]["provider"])
  460. self.assertEqual("label1", data[0]["type"])
  461. def test_web_labels_list(self):
  462. # can we fetch the labels list
  463. data = self.get_url('api/tenant/tenant-one/labels').json()
  464. expected_list = [{'name': 'label1'}]
  465. self.assertEqual(expected_list, data)
  466. def test_web_pipeline_list(self):
  467. # can we fetch the list of pipelines
  468. data = self.get_url('api/tenant/tenant-one/pipelines').json()
  469. gerrit_trigger = {'name': 'gerrit', 'driver': 'gerrit'}
  470. timer_trigger = {'name': 'timer', 'driver': 'timer'}
  471. expected_list = [
  472. {'name': 'check', 'triggers': [gerrit_trigger]},
  473. {'name': 'gate', 'triggers': [gerrit_trigger]},
  474. {'name': 'post', 'triggers': [gerrit_trigger]},
  475. {'name': 'periodic', 'triggers': [timer_trigger]},
  476. ]
  477. self.assertEqual(expected_list, data)
  478. def test_web_project_list(self):
  479. # can we fetch the list of projects
  480. data = self.get_url('api/tenant/tenant-one/projects').json()
  481. expected_list = [
  482. {'name': 'common-config', 'type': 'config'},
  483. {'name': 'org/project', 'type': 'untrusted'},
  484. {'name': 'org/project1', 'type': 'untrusted'},
  485. {'name': 'org/project2', 'type': 'untrusted'}
  486. ]
  487. for p in expected_list:
  488. p["canonical_name"] = "review.example.com/%s" % p["name"]
  489. p["connection_name"] = "gerrit"
  490. self.assertEqual(expected_list, data)
  491. def test_web_project_get(self):
  492. # can we fetch project details
  493. data = self.get_url(
  494. 'api/tenant/tenant-one/project/org/project1').json()
  495. jobs = [[{'abstract': False,
  496. 'ansible_version': None,
  497. 'attempts': 3,
  498. 'branches': [],
  499. 'dependencies': [],
  500. 'description': None,
  501. 'files': [],
  502. 'final': False,
  503. 'intermediate': False,
  504. 'irrelevant_files': [],
  505. 'match_on_config_updates': True,
  506. 'name': 'project-merge',
  507. 'override_checkout': None,
  508. 'parent': 'base',
  509. 'post_review': None,
  510. 'protected': None,
  511. 'provides': [],
  512. 'required_projects': [],
  513. 'requires': [],
  514. 'roles': [],
  515. 'run': [],
  516. 'pre_run': [],
  517. 'post_run': [],
  518. 'cleanup_run': [],
  519. 'semaphore': None,
  520. 'source_context': {
  521. 'branch': 'master',
  522. 'path': 'zuul.yaml',
  523. 'project': 'common-config'},
  524. 'tags': [],
  525. 'timeout': None,
  526. 'variables': {},
  527. 'extra_variables': {},
  528. 'group_variables': {},
  529. 'host_variables': {},
  530. 'variant_description': '',
  531. 'voting': True}],
  532. [{'abstract': False,
  533. 'ansible_version': None,
  534. 'attempts': 3,
  535. 'branches': [],
  536. 'dependencies': [{'name': 'project-merge',
  537. 'soft': False}],
  538. 'description': None,
  539. 'files': [],
  540. 'final': False,
  541. 'intermediate': False,
  542. 'irrelevant_files': [],
  543. 'match_on_config_updates': True,
  544. 'name': 'project-test1',
  545. 'override_checkout': None,
  546. 'parent': 'base',
  547. 'post_review': None,
  548. 'protected': None,
  549. 'provides': [],
  550. 'required_projects': [],
  551. 'requires': [],
  552. 'roles': [],
  553. 'run': [],
  554. 'pre_run': [],
  555. 'post_run': [],
  556. 'cleanup_run': [],
  557. 'semaphore': None,
  558. 'source_context': {
  559. 'branch': 'master',
  560. 'path': 'zuul.yaml',
  561. 'project': 'common-config'},
  562. 'tags': [],
  563. 'timeout': None,
  564. 'variables': {},
  565. 'extra_variables': {},
  566. 'group_variables': {},
  567. 'host_variables': {},
  568. 'variant_description': '',
  569. 'voting': True}],
  570. [{'abstract': False,
  571. 'ansible_version': None,
  572. 'attempts': 3,
  573. 'branches': [],
  574. 'dependencies': [{'name': 'project-merge',
  575. 'soft': False}],
  576. 'description': None,
  577. 'files': [],
  578. 'final': False,
  579. 'intermediate': False,
  580. 'irrelevant_files': [],
  581. 'match_on_config_updates': True,
  582. 'name': 'project-test2',
  583. 'override_checkout': None,
  584. 'parent': 'base',
  585. 'post_review': None,
  586. 'protected': None,
  587. 'provides': [],
  588. 'required_projects': [],
  589. 'requires': [],
  590. 'roles': [],
  591. 'run': [],
  592. 'pre_run': [],
  593. 'post_run': [],
  594. 'cleanup_run': [],
  595. 'semaphore': None,
  596. 'source_context': {
  597. 'branch': 'master',
  598. 'path': 'zuul.yaml',
  599. 'project': 'common-config'},
  600. 'tags': [],
  601. 'timeout': None,
  602. 'variables': {},
  603. 'extra_variables': {},
  604. 'group_variables': {},
  605. 'host_variables': {},
  606. 'variant_description': '',
  607. 'voting': True}],
  608. [{'abstract': False,
  609. 'ansible_version': None,
  610. 'attempts': 3,
  611. 'branches': [],
  612. 'dependencies': [{'name': 'project-merge',
  613. 'soft': False}],
  614. 'description': None,
  615. 'files': [],
  616. 'final': False,
  617. 'intermediate': False,
  618. 'irrelevant_files': [],
  619. 'match_on_config_updates': True,
  620. 'name': 'project1-project2-integration',
  621. 'override_checkout': None,
  622. 'parent': 'base',
  623. 'post_review': None,
  624. 'protected': None,
  625. 'provides': [],
  626. 'required_projects': [],
  627. 'requires': [],
  628. 'roles': [],
  629. 'run': [],
  630. 'pre_run': [],
  631. 'post_run': [],
  632. 'cleanup_run': [],
  633. 'semaphore': None,
  634. 'source_context': {
  635. 'branch': 'master',
  636. 'path': 'zuul.yaml',
  637. 'project': 'common-config'},
  638. 'tags': [],
  639. 'timeout': None,
  640. 'variables': {},
  641. 'extra_variables': {},
  642. 'group_variables': {},
  643. 'host_variables': {},
  644. 'variant_description': '',
  645. 'voting': True}]]
  646. self.assertEqual(
  647. {
  648. 'canonical_name': 'review.example.com/org/project1',
  649. 'connection_name': 'gerrit',
  650. 'name': 'org/project1',
  651. 'configs': [{
  652. 'templates': [],
  653. 'default_branch': 'master',
  654. 'merge_mode': 'merge-resolve',
  655. 'pipelines': [{
  656. 'name': 'check',
  657. 'queue_name': None,
  658. 'jobs': jobs,
  659. }, {
  660. 'name': 'gate',
  661. 'queue_name': 'integrated',
  662. 'jobs': jobs,
  663. }, {'name': 'post',
  664. 'queue_name': None,
  665. 'jobs': [[
  666. {'abstract': False,
  667. 'ansible_version': None,
  668. 'attempts': 3,
  669. 'branches': [],
  670. 'dependencies': [],
  671. 'description': None,
  672. 'files': [],
  673. 'final': False,
  674. 'intermediate': False,
  675. 'irrelevant_files': [],
  676. 'match_on_config_updates': True,
  677. 'name': 'project-post',
  678. 'override_checkout': None,
  679. 'parent': 'base',
  680. 'post_review': None,
  681. 'post_run': [],
  682. 'cleanup_run': [],
  683. 'pre_run': [],
  684. 'protected': None,
  685. 'provides': [],
  686. 'required_projects': [],
  687. 'requires': [],
  688. 'roles': [],
  689. 'run': [],
  690. 'semaphore': None,
  691. 'source_context': {'branch': 'master',
  692. 'path': 'zuul.yaml',
  693. 'project': 'common-config'},
  694. 'tags': [],
  695. 'timeout': None,
  696. 'variables': {},
  697. 'extra_variables': {},
  698. 'group_variables': {},
  699. 'host_variables': {},
  700. 'variant_description': '',
  701. 'voting': True}
  702. ]],
  703. }
  704. ]
  705. }]
  706. }, data)
  707. def test_web_keys(self):
  708. with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f:
  709. public_pem = f.read()
  710. resp = self.get_url("api/tenant/tenant-one/key/org/project.pub")
  711. self.assertEqual(resp.content, public_pem)
  712. self.assertIn('text/plain', resp.headers.get('Content-Type'))
  713. resp = self.get_url("api/tenant/non-tenant/key/org/project.pub")
  714. self.assertEqual(404, resp.status_code)
  715. resp = self.get_url("api/tenant/tenant-one/key/org/no-project.pub")
  716. self.assertEqual(404, resp.status_code)
  717. with open(os.path.join(FIXTURE_DIR, 'ssh.pub'), 'rb') as f:
  718. public_ssh = f.read()
  719. resp = self.get_url("api/tenant/tenant-one/project-ssh-key/"
  720. "org/project.pub")
  721. self.assertEqual(resp.content, public_ssh)
  722. self.assertIn('text/plain', resp.headers.get('Content-Type'))
  723. def test_web_404_on_unknown_tenant(self):
  724. resp = self.get_url("api/tenant/non-tenant/status")
  725. self.assertEqual(404, resp.status_code)
  726. def test_autohold_info_404_on_invalid_id(self):
  727. resp = self.get_url("api/tenant/tenant-one/autohold/12345")
  728. self.assertEqual(404, resp.status_code)
  729. def test_autohold_delete_404_on_invalid_id(self):
  730. resp = self.delete_url("api/tenant/tenant-one/autohold/12345")
  731. self.assertEqual(404, resp.status_code)
  732. def test_autohold_info(self):
  733. client = zuul.rpcclient.RPCClient('127.0.0.1',
  734. self.gearman_server.port)
  735. self.addCleanup(client.shutdown)
  736. r = client.autohold('tenant-one', 'org/project', 'project-test2',
  737. "", "", "reason text", 1)
  738. self.assertTrue(r)
  739. # Use autohold-list API to retrieve request ID
  740. resp = self.get_url(
  741. "api/tenant/tenant-one/autohold")
  742. self.assertEqual(200, resp.status_code, resp.text)
  743. autohold_requests = resp.json()
  744. self.assertNotEqual([], autohold_requests)
  745. self.assertEqual(1, len(autohold_requests))
  746. request_id = autohold_requests[0]['id']
  747. # Now try the autohold-info API
  748. resp = self.get_url("api/tenant/tenant-one/autohold/%s" % request_id)
  749. self.assertEqual(200, resp.status_code, resp.text)
  750. request = resp.json()
  751. self.assertEqual(request_id, request['id'])
  752. self.assertEqual('tenant-one', request['tenant'])
  753. self.assertIn('org/project', request['project'])
  754. self.assertEqual('project-test2', request['job'])
  755. self.assertEqual(".*", request['ref_filter'])
  756. self.assertEqual(1, request['max_count'])
  757. self.assertEqual(0, request['current_count'])
  758. self.assertEqual("reason text", request['reason'])
  759. self.assertEqual([], request['nodes'])
  760. def test_autohold_list(self):
  761. """test listing autoholds through zuul-web"""
  762. client = zuul.rpcclient.RPCClient('127.0.0.1',
  763. self.gearman_server.port)
  764. self.addCleanup(client.shutdown)
  765. r = client.autohold('tenant-one', 'org/project', 'project-test2',
  766. "", "", "reason text", 1)
  767. self.assertTrue(r)
  768. resp = self.get_url(
  769. "api/tenant/tenant-one/autohold")
  770. self.assertEqual(200, resp.status_code, resp.text)
  771. autohold_requests = resp.json()
  772. self.assertNotEqual([], autohold_requests)
  773. self.assertEqual(1, len(autohold_requests))
  774. ah_request = autohold_requests[0]
  775. self.assertEqual('tenant-one', ah_request['tenant'])
  776. self.assertIn('org/project', ah_request['project'])
  777. self.assertEqual('project-test2', ah_request['job'])
  778. self.assertEqual(".*", ah_request['ref_filter'])
  779. self.assertEqual(1, ah_request['max_count'])
  780. self.assertEqual(0, ah_request['current_count'])
  781. self.assertEqual("reason text", ah_request['reason'])
  782. self.assertEqual([], ah_request['nodes'])
  783. # filter by project
  784. resp = self.get_url(
  785. "api/tenant/tenant-one/autohold?project=org/project2")
  786. self.assertEqual(200, resp.status_code, resp.text)
  787. autohold_requests = resp.json()
  788. self.assertEqual([], autohold_requests)
  789. resp = self.get_url(
  790. "api/tenant/tenant-one/autohold?project=org/project")
  791. self.assertEqual(200, resp.status_code, resp.text)
  792. autohold_requests = resp.json()
  793. self.assertNotEqual([], autohold_requests)
  794. self.assertEqual(1, len(autohold_requests))
  795. ah_request = autohold_requests[0]
  796. self.assertEqual('tenant-one', ah_request['tenant'])
  797. self.assertIn('org/project', ah_request['project'])
  798. self.assertEqual('project-test2', ah_request['job'])
  799. self.assertEqual(".*", ah_request['ref_filter'])
  800. self.assertEqual(1, ah_request['max_count'])
  801. self.assertEqual(0, ah_request['current_count'])
  802. self.assertEqual("reason text", ah_request['reason'])
  803. self.assertEqual([], ah_request['nodes'])
  804. def test_admin_routes_404_by_default(self):
  805. resp = self.post_url(
  806. "api/tenant/tenant-one/project/org/project/autohold",
  807. json={'job': 'project-test1',
  808. 'count': 1,
  809. 'reason': 'because',
  810. 'node_hold_expiration': 36000})
  811. self.assertEqual(404, resp.status_code)
  812. resp = self.post_url(
  813. "api/tenant/tenant-one/project/org/project/enqueue",
  814. json={'trigger': 'gerrit',
  815. 'change': '2,1',
  816. 'pipeline': 'check'})
  817. self.assertEqual(404, resp.status_code)
  818. resp = self.post_url(
  819. "api/tenant/tenant-one/project/org/project/enqueue",
  820. json={'trigger': 'gerrit',
  821. 'ref': 'abcd',
  822. 'newrev': 'aaaa',
  823. 'oldrev': 'bbbb',
  824. 'pipeline': 'check'})
  825. self.assertEqual(404, resp.status_code)
  826. def test_jobs_list(self):
  827. jobs = self.get_url("api/tenant/tenant-one/jobs").json()
  828. self.assertEqual(len(jobs), 10)
  829. resp = self.get_url("api/tenant/non-tenant/jobs")
  830. self.assertEqual(404, resp.status_code)
  831. def test_jobs_list_variants(self):
  832. resp = self.get_url("api/tenant/tenant-one/jobs").json()
  833. for job in resp:
  834. if job['name'] in ["base", "noop"]:
  835. variants = None
  836. elif job['name'] == 'project-test1':
  837. variants = [
  838. {'parent': 'base'},
  839. {'branches': ['stable'], 'parent': 'base'},
  840. ]
  841. else:
  842. variants = [{'parent': 'base'}]
  843. self.assertEqual(variants, job.get('variants'))
  844. def test_jobs_list_tags(self):
  845. resp = self.get_url("api/tenant/tenant-one/jobs").json()
  846. post_job = None
  847. for job in resp:
  848. if job['name'] == 'project-post':
  849. post_job = job
  850. break
  851. self.assertIsNotNone(post_job)
  852. self.assertEqual(['post'], post_job.get('tags'))
  853. def test_web_job_noop(self):
  854. job = self.get_url("api/tenant/tenant-one/job/noop").json()
  855. self.assertEqual("noop", job[0]["name"])
  856. def test_freeze_jobs(self):
  857. # Test can get a list of the jobs for a given project+pipeline+branch.
  858. resp = self.get_url(
  859. "api/tenant/tenant-one/pipeline/check"
  860. "/project/org/project1/branch/master/freeze-jobs")
  861. freeze_jobs = [{
  862. 'name': 'project-merge',
  863. 'dependencies': [],
  864. }, {
  865. 'name': 'project-test1',
  866. 'dependencies': [{
  867. 'name': 'project-merge',
  868. 'soft': False,
  869. }],
  870. }, {
  871. 'name': 'project-test2',
  872. 'dependencies': [{
  873. 'name': 'project-merge',
  874. 'soft': False,
  875. }],
  876. }, {
  877. 'name': 'project1-project2-integration',
  878. 'dependencies': [{
  879. 'name': 'project-merge',
  880. 'soft': False,
  881. }],
  882. }]
  883. self.assertEqual(freeze_jobs, resp.json())
  884. def test_freeze_jobs_set_includes_all_jobs(self):
  885. # When freezing a job set we want to include all jobs even if they
  886. # have certain matcher requirements (such as required files) since we
  887. # can't otherwise evaluate them.
  888. resp = self.get_url(
  889. "api/tenant/tenant-one/pipeline/gate"
  890. "/project/org/project/branch/master/freeze-jobs")
  891. expected = {
  892. 'name': 'project-testfile',
  893. 'dependencies': [{
  894. 'name': 'project-merge',
  895. 'soft': False,
  896. }],
  897. }
  898. self.assertIn(expected, resp.json())
  899. class TestWebMultiTenant(BaseTestWeb):
  900. tenant_config_file = 'config/multi-tenant/main.yaml'
  901. def test_web_labels_allowed_list(self):
  902. labels = ["tenant-one-label", "fake", "tenant-two-label"]
  903. self.fake_nodepool.registerLauncher(labels, "FakeLauncher2")
  904. # Tenant-one has label restriction in place on tenant-two
  905. res = self.get_url('api/tenant/tenant-one/labels').json()
  906. self.assertEqual([{'name': 'fake'}, {'name': 'tenant-one-label'}], res)
  907. # Tenant-two has label restriction in place on tenant-one
  908. expected = ["label1", "fake", "tenant-two-label"]
  909. res = self.get_url('api/tenant/tenant-two/labels').json()
  910. self.assertEqual(
  911. list(map(lambda x: {'name': x}, sorted(expected))), res)
  912. class TestWebSecrets(BaseTestWeb):
  913. tenant_config_file = 'config/secrets/main.yaml'
  914. def test_web_find_job_secret(self):
  915. data = self.get_url('api/tenant/tenant-one/job/project1-secret').json()
  916. run = data[0]['run']
  917. secret = {'name': 'project1_secret', 'alias': 'secret_name'}
  918. self.assertEqual([secret], run[0]['secrets'])
  919. class TestInfo(ZuulDBTestCase, BaseTestWeb):
  920. config_file = 'zuul-sql-driver.conf'
  921. def setUp(self):
  922. super(TestInfo, self).setUp()
  923. web_config = self.config_ini_data.get('web', {})
  924. self.websocket_url = web_config.get('websocket_url')
  925. self.stats_url = web_config.get('stats_url')
  926. statsd_config = self.config_ini_data.get('statsd', {})
  927. self.stats_prefix = statsd_config.get('prefix')
  928. def _expected_info(self):
  929. return {
  930. "info": {
  931. "capabilities": {
  932. "job_history": True,
  933. "auth": {
  934. "realms": {},
  935. "default_realm": None
  936. }
  937. },
  938. "stats": {
  939. "url": self.stats_url,
  940. "prefix": self.stats_prefix,
  941. "type": "graphite",
  942. },
  943. "websocket_url": self.websocket_url,
  944. }
  945. }
  946. def test_info(self):
  947. info = self.get_url("api/info").json()
  948. self.assertEqual(
  949. info, self._expected_info())
  950. def test_tenant_info(self):
  951. info = self.get_url("api/tenant/tenant-one/info").json()
  952. expected_info = self._expected_info()
  953. expected_info['info']['tenant'] = 'tenant-one'
  954. self.assertEqual(
  955. info, expected_info)
  956. class TestWebCapabilitiesInfo(TestInfo):
  957. config_file = 'zuul-admin-web-oidc.conf'
  958. def _expected_info(self):
  959. info = super(TestWebCapabilitiesInfo, self)._expected_info()
  960. info['info']['capabilities']['auth'] = {
  961. 'realms': {
  962. 'myOIDC1': {
  963. 'authority': 'http://oidc1',
  964. 'client_id': 'zuul',
  965. 'type': 'JWT',
  966. 'scope': 'openid profile',
  967. 'driver': 'OpenIDConnect',
  968. },
  969. 'myOIDC2': {
  970. 'authority': 'http://oidc2',
  971. 'client_id': 'zuul',
  972. 'type': 'JWT',
  973. 'scope': 'openid profile email special-scope',
  974. 'driver': 'OpenIDConnect',
  975. },
  976. 'zuul.example.com': {
  977. 'authority': 'zuul_operator',
  978. 'client_id': 'zuul.example.com',
  979. 'type': 'JWT',
  980. 'driver': 'HS256',
  981. }
  982. },
  983. 'default_realm': 'myOIDC1'
  984. }
  985. return info
  986. class TestTenantInfoConfigBroken(BaseTestWeb):
  987. tenant_config_file = 'config/broken/main.yaml'
  988. def test_tenant_info_broken_config(self):
  989. config_errors = self.get_url(
  990. "api/tenant/tenant-one/config-errors").json()
  991. self.assertEqual(
  992. len(config_errors), 2)
  993. self.assertEqual(
  994. config_errors[0]['source_context']['project'], 'org/project3')
  995. self.assertIn('Zuul encountered an error while accessing the repo '
  996. 'org/project3',
  997. config_errors[0]['error'])
  998. self.assertEqual(
  999. config_errors[1]['source_context']['project'], 'org/project2')
  1000. self.assertEqual(
  1001. config_errors[1]['source_context']['branch'], 'master')
  1002. self.assertEqual(
  1003. config_errors[1]['source_context']['path'], '.zuul.yaml')
  1004. self.assertIn('Zuul encountered a syntax error',
  1005. config_errors[1]['error'])
  1006. resp = self.get_url("api/tenant/non-tenant/config-errors")
  1007. self.assertEqual(404, resp.status_code)
  1008. class TestWebSocketInfo(TestInfo):
  1009. config_ini_data = {
  1010. 'web': {
  1011. 'websocket_url': 'wss://ws.example.com'
  1012. }
  1013. }
  1014. class TestGraphiteUrl(TestInfo):
  1015. config_ini_data = {
  1016. 'statsd': {
  1017. 'prefix': 'example'
  1018. },
  1019. 'web': {
  1020. 'stats_url': 'https://graphite.example.com',
  1021. }
  1022. }
  1023. class TestBuildInfo(ZuulDBTestCase, BaseTestWeb):
  1024. config_file = 'zuul-sql-driver.conf'
  1025. tenant_config_file = 'config/sql-driver/main.yaml'
  1026. def test_web_list_builds(self):
  1027. # Generate some build records in the db.
  1028. self.add_base_changes()
  1029. self.executor_server.hold_jobs_in_build = False
  1030. self.executor_server.release()
  1031. self.waitUntilSettled()
  1032. builds = self.get_url("api/tenant/tenant-one/builds").json()
  1033. self.assertEqual(len(builds), 6)
  1034. uuid = builds[0]['uuid']
  1035. build = self.get_url("api/tenant/tenant-one/build/%s" % uuid).json()
  1036. self.assertEqual(build['job_name'], builds[0]['job_name'])
  1037. resp = self.get_url("api/tenant/tenant-one/build/1234")
  1038. self.assertEqual(404, resp.status_code)
  1039. builds_query = self.get_url("api/tenant/tenant-one/builds?"
  1040. "project=org/project&"
  1041. "project=org/project1").json()
  1042. self.assertEqual(len(builds_query), 6)
  1043. resp = self.get_url("api/tenant/non-tenant/builds")
  1044. self.assertEqual(404, resp.status_code)
  1045. def test_web_badge(self):
  1046. # Generate some build records in the db.
  1047. self.add_base_changes()
  1048. self.executor_server.hold_jobs_in_build = False
  1049. self.executor_server.release()
  1050. self.waitUntilSettled()
  1051. # Now request badge for the buildsets
  1052. result = self.get_url("api/tenant/tenant-one/badge")
  1053. self.log.error(result.content)
  1054. result.raise_for_status()
  1055. self.assertTrue(result.text.startswith('<svg '))
  1056. self.assertIn('passing', result.text)
  1057. # Generate a failing record
  1058. self.executor_server.hold_jobs_in_build = True
  1059. C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
  1060. C.addApproval('Code-Review', 2)
  1061. self.fake_gerrit.addEvent(C.addApproval('Approved', 1))
  1062. self.waitUntilSettled()
  1063. self.executor_server.failJob('project-merge', C)
  1064. self.executor_server.hold_jobs_in_build = False
  1065. self.executor_server.release()
  1066. self.waitUntilSettled()
  1067. # Request again badge for the buildsets
  1068. result = self.get_url("api/tenant/tenant-one/badge")
  1069. self.log.error(result.content)
  1070. result.raise_for_status()
  1071. self.assertTrue(result.text.startswith('<svg '))
  1072. self.assertIn('failing', result.text)
  1073. def test_web_list_buildsets(self):
  1074. # Generate some build records in the db.
  1075. self.add_base_changes()
  1076. self.executor_server.hold_jobs_in_build = False
  1077. self.executor_server.release()
  1078. self.waitUntilSettled()
  1079. buildsets = self.get_url("api/tenant/tenant-one/buildsets").json()
  1080. self.assertEqual(2, len(buildsets))
  1081. project_bs = [x for x in buildsets if x["project"] == "org/project"][0]
  1082. buildset = self.get_url(
  1083. "api/tenant/tenant-one/buildset/%s" % project_bs['uuid']).json()
  1084. self.assertEqual(3, len(buildset["builds"]))
  1085. project_test1_build = [x for x in buildset["builds"]
  1086. if x["job_name"] == "project-test1"][0]
  1087. self.assertEqual('SUCCESS', project_test1_build['result'])
  1088. project_test2_build = [x for x in buildset["builds"]
  1089. if x["job_name"] == "project-test2"][0]
  1090. self.assertEqual('SUCCESS', project_test2_build['result'])
  1091. project_merge_build = [x for x in buildset["builds"]
  1092. if x["job_name"] == "project-merge"][0]
  1093. self.assertEqual('SUCCESS', project_merge_build['result'])
  1094. @simple_layout('layouts/sql-build-error.yaml')
  1095. def test_build_error(self):
  1096. conf = textwrap.dedent(
  1097. """
  1098. - job:
  1099. name: test-job
  1100. run: playbooks/dne.yaml
  1101. - project:
  1102. name: org/project
  1103. check:
  1104. jobs:
  1105. - test-job
  1106. """)
  1107. file_dict = {'.zuul.yaml': conf}
  1108. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A',
  1109. files=file_dict)
  1110. self.fake_gerrit.addEvent(A.getPatchsetCreatedEvent(1))
  1111. self.waitUntilSettled()
  1112. builds = self.get_url("api/tenant/tenant-one/builds").json()
  1113. self.assertIn('Unable to find playbook',
  1114. builds[0]['error_detail'])
  1115. class TestArtifacts(ZuulDBTestCase, BaseTestWeb, AnsibleZuulTestCase):
  1116. config_file = 'zuul-sql-driver.conf'
  1117. tenant_config_file = 'config/sql-driver/main.yaml'
  1118. def test_artifacts(self):
  1119. # Generate some build records in the db.
  1120. self.add_base_changes()
  1121. self.executor_server.hold_jobs_in_build = False
  1122. self.executor_server.release()
  1123. self.waitUntilSettled()
  1124. build_query = self.get_url("api/tenant/tenant-one/builds?"
  1125. "project=org/project&"
  1126. "job_name=project-test1").json()
  1127. self.assertEqual(len(build_query), 1)
  1128. self.assertEqual(len(build_query[0]['artifacts']), 3)
  1129. arts = build_query[0]['artifacts']
  1130. arts.sort(key=lambda x: x['name'])
  1131. self.assertEqual(build_query[0]['artifacts'], [
  1132. {'url': 'http://example.com/docs',
  1133. 'name': 'docs'},
  1134. {'url': 'http://logs.example.com/build/relative/docs',
  1135. 'name': 'relative',
  1136. 'metadata': {'foo': 'bar'}},
  1137. {'url': 'http://example.com/tarball',
  1138. 'name': 'tarball'},
  1139. ])
  1140. def test_buildset_artifacts(self):
  1141. self.add_base_changes()
  1142. self.executor_server.hold_jobs_in_build = False
  1143. self.executor_server.release()
  1144. self.waitUntilSettled()
  1145. buildsets = self.get_url("api/tenant/tenant-one/buildsets").json()
  1146. project_bs = [x for x in buildsets if x["project"] == "org/project"][0]
  1147. buildset = self.get_url(
  1148. "api/tenant/tenant-one/buildset/%s" % project_bs['uuid']).json()
  1149. self.assertEqual(3, len(buildset["builds"]))
  1150. test1_build = [x for x in buildset["builds"]
  1151. if x["job_name"] == "project-test1"][0]
  1152. arts = test1_build['artifacts']
  1153. arts.sort(key=lambda x: x['name'])
  1154. self.assertEqual([
  1155. {'url': 'http://example.com/docs',
  1156. 'name': 'docs'},
  1157. {'url': 'http://logs.example.com/build/relative/docs',
  1158. 'name': 'relative',
  1159. 'metadata': {'foo': 'bar'}},
  1160. {'url': 'http://example.com/tarball',
  1161. 'name': 'tarball'},
  1162. ], test1_build['artifacts'])
  1163. class TestTenantScopedWebApi(BaseTestWeb):
  1164. config_file = 'zuul-admin-web.conf'
  1165. def test_admin_routes_no_token(self):
  1166. resp = self.post_url(
  1167. "api/tenant/tenant-one/project/org/project/autohold",
  1168. json={'job': 'project-test1',
  1169. 'count': 1,
  1170. 'reason': 'because',
  1171. 'node_hold_expiration': 36000})
  1172. self.assertEqual(401, resp.status_code)
  1173. resp = self.post_url(
  1174. "api/tenant/tenant-one/project/org/project/enqueue",
  1175. json={'trigger': 'gerrit',
  1176. 'change': '2,1',
  1177. 'pipeline': 'check'})
  1178. self.assertEqual(401, resp.status_code)
  1179. resp = self.post_url(
  1180. "api/tenant/tenant-one/project/org/project/enqueue",
  1181. json={'trigger': 'gerrit',
  1182. 'ref': 'abcd',
  1183. 'newrev': 'aaaa',
  1184. 'oldrev': 'bbbb',
  1185. 'pipeline': 'check'})
  1186. self.assertEqual(401, resp.status_code)
  1187. def test_bad_key_JWT_token(self):
  1188. authz = {'iss': 'zuul_operator',
  1189. 'aud': 'zuul.example.com',
  1190. 'sub': 'testuser',
  1191. 'zuul': {
  1192. 'admin': ['tenant-one', ],
  1193. },
  1194. 'exp': time.time() + 3600}
  1195. token = jwt.encode(authz, key='OnlyZuulNoDana',
  1196. algorithm='HS256').decode('utf-8')
  1197. resp = self.post_url(
  1198. "api/tenant/tenant-one/project/org/project/autohold",
  1199. headers={'Authorization': 'Bearer %s' % token},
  1200. json={'job': 'project-test1',
  1201. 'count': 1,
  1202. 'reason': 'because',
  1203. 'node_hold_expiration': 36000})
  1204. self.assertEqual(401, resp.status_code)
  1205. resp = self.post_url(
  1206. "api/tenant/tenant-one/project/org/project/enqueue",
  1207. headers={'Authorization': 'Bearer %s' % token},
  1208. json={'trigger': 'gerrit',
  1209. 'change': '2,1',
  1210. 'pipeline': 'check'})
  1211. self.assertEqual(401, resp.status_code)
  1212. resp = self.post_url(
  1213. "api/tenant/tenant-one/project/org/project/enqueue",
  1214. headers={'Authorization': 'Bearer %s' % token},
  1215. json={'trigger': 'gerrit',
  1216. 'ref': 'abcd',
  1217. 'newrev': 'aaaa',
  1218. 'oldrev': 'bbbb',
  1219. 'pipeline': 'check'})
  1220. self.assertEqual(401, resp.status_code)
  1221. def test_expired_JWT_token(self):
  1222. authz = {'iss': 'zuul_operator',
  1223. 'sub': 'testuser',
  1224. 'aud': 'zuul.example.com',
  1225. 'zuul': {
  1226. 'admin': ['tenant-one', ]
  1227. },
  1228. 'exp': time.time() - 3600}
  1229. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1230. algorithm='HS256').decode('utf-8')
  1231. resp = self.post_url(
  1232. "api/tenant/tenant-one/project/org/project/autohold",
  1233. headers={'Authorization': 'Bearer %s' % token},
  1234. json={'job': 'project-test1',
  1235. 'count': 1,
  1236. 'reason': 'because',
  1237. 'node_hold_expiration': 36000})
  1238. self.assertEqual(401, resp.status_code)
  1239. resp = self.post_url(
  1240. "api/tenant/tenant-one/project/org/project/enqueue",
  1241. headers={'Authorization': 'Bearer %s' % token},
  1242. json={'trigger': 'gerrit',
  1243. 'change': '2,1',
  1244. 'pipeline': 'check'})
  1245. self.assertEqual(401, resp.status_code)
  1246. resp = self.post_url(
  1247. "api/tenant/tenant-one/project/org/project/enqueue",
  1248. headers={'Authorization': 'Bearer %s' % token},
  1249. json={'trigger': 'gerrit',
  1250. 'ref': 'abcd',
  1251. 'newrev': 'aaaa',
  1252. 'oldrev': 'bbbb',
  1253. 'pipeline': 'check'})
  1254. self.assertEqual(401, resp.status_code)
  1255. def test_valid_JWT_bad_tenants(self):
  1256. authz = {'iss': 'zuul_operator',
  1257. 'sub': 'testuser',
  1258. 'aud': 'zuul.example.com',
  1259. 'zuul': {
  1260. 'admin': ['tenant-six', 'tenant-ten', ]
  1261. },
  1262. 'exp': time.time() + 3600}
  1263. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1264. algorithm='HS256').decode('utf-8')
  1265. resp = self.post_url(
  1266. "api/tenant/tenant-one/project/org/project/autohold",
  1267. headers={'Authorization': 'Bearer %s' % token},
  1268. json={'job': 'project-test1',
  1269. 'count': 1,
  1270. 'reason': 'because',
  1271. 'node_hold_expiration': 36000})
  1272. self.assertEqual(403, resp.status_code)
  1273. resp = self.post_url(
  1274. "api/tenant/tenant-one/project/org/project/enqueue",
  1275. headers={'Authorization': 'Bearer %s' % token},
  1276. json={'trigger': 'gerrit',
  1277. 'change': '2,1',
  1278. 'pipeline': 'check'})
  1279. self.assertEqual(403, resp.status_code)
  1280. resp = self.post_url(
  1281. "api/tenant/tenant-one/project/org/project/enqueue",
  1282. headers={'Authorization': 'Bearer %s' % token},
  1283. json={'trigger': 'gerrit',
  1284. 'ref': 'abcd',
  1285. 'newrev': 'aaaa',
  1286. 'oldrev': 'bbbb',
  1287. 'pipeline': 'check'})
  1288. self.assertEqual(403, resp.status_code)
  1289. # For autohold-delete, we first must make sure that an autohold
  1290. # exists before the delete attempt.
  1291. good_authz = {'iss': 'zuul_operator',
  1292. 'aud': 'zuul.example.com',
  1293. 'sub': 'testuser',
  1294. 'zuul': {'admin': ['tenant-one', ]},
  1295. 'exp': time.time() + 3600}
  1296. args = {"reason": "some reason",
  1297. "count": 1,
  1298. 'job': 'project-test2',
  1299. 'change': None,
  1300. 'ref': None,
  1301. 'node_hold_expiration': None}
  1302. good_token = jwt.encode(good_authz, key='NoDanaOnlyZuul',
  1303. algorithm='HS256').decode('utf-8')
  1304. req = self.post_url(
  1305. 'api/tenant/tenant-one/project/org/project/autohold',
  1306. headers={'Authorization': 'Bearer %s' % good_token},
  1307. json=args)
  1308. self.assertEqual(200, req.status_code, req.text)
  1309. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1310. self.gearman_server.port)
  1311. self.addCleanup(client.shutdown)
  1312. autohold_requests = client.autohold_list()
  1313. self.assertNotEqual([], autohold_requests)
  1314. self.assertEqual(1, len(autohold_requests))
  1315. request = autohold_requests[0]
  1316. resp = self.delete_url(
  1317. "api/tenant/tenant-one/autohold/%s" % request['id'],
  1318. headers={'Authorization': 'Bearer %s' % token})
  1319. self.assertEqual(403, resp.status_code)
  1320. def test_autohold(self):
  1321. """Test that autohold can be set through the admin web interface"""
  1322. args = {"reason": "some reason",
  1323. "count": 1,
  1324. 'job': 'project-test2',
  1325. 'change': None,
  1326. 'ref': None,
  1327. 'node_hold_expiration': None}
  1328. authz = {'iss': 'zuul_operator',
  1329. 'aud': 'zuul.example.com',
  1330. 'sub': 'testuser',
  1331. 'zuul': {
  1332. 'admin': ['tenant-one', ]
  1333. },
  1334. 'exp': time.time() + 3600}
  1335. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1336. algorithm='HS256').decode('utf-8')
  1337. req = self.post_url(
  1338. 'api/tenant/tenant-one/project/org/project/autohold',
  1339. headers={'Authorization': 'Bearer %s' % token},
  1340. json=args)
  1341. self.assertEqual(200, req.status_code, req.text)
  1342. data = req.json()
  1343. self.assertEqual(True, data)
  1344. # Check result in rpc client
  1345. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1346. self.gearman_server.port)
  1347. self.addCleanup(client.shutdown)
  1348. autohold_requests = client.autohold_list()
  1349. self.assertNotEqual([], autohold_requests)
  1350. self.assertEqual(1, len(autohold_requests))
  1351. request = autohold_requests[0]
  1352. self.assertEqual('tenant-one', request['tenant'])
  1353. self.assertIn('org/project', request['project'])
  1354. self.assertEqual('project-test2', request['job'])
  1355. self.assertEqual(".*", request['ref_filter'])
  1356. self.assertEqual("some reason", request['reason'])
  1357. self.assertEqual(1, request['max_count'])
  1358. def test_autohold_delete(self):
  1359. authz = {'iss': 'zuul_operator',
  1360. 'aud': 'zuul.example.com',
  1361. 'sub': 'testuser',
  1362. 'zuul': {
  1363. 'admin': ['tenant-one', ]
  1364. },
  1365. 'exp': time.time() + 3600}
  1366. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1367. algorithm='HS256').decode('utf-8')
  1368. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1369. self.gearman_server.port)
  1370. self.addCleanup(client.shutdown)
  1371. r = client.autohold('tenant-one', 'org/project', 'project-test2',
  1372. "", "", "reason text", 1)
  1373. self.assertTrue(r)
  1374. # Use autohold-list API to retrieve request ID
  1375. resp = self.get_url(
  1376. "api/tenant/tenant-one/autohold")
  1377. self.assertEqual(200, resp.status_code, resp.text)
  1378. autohold_requests = resp.json()
  1379. self.assertNotEqual([], autohold_requests)
  1380. self.assertEqual(1, len(autohold_requests))
  1381. request_id = autohold_requests[0]['id']
  1382. # now try the autohold-delete API
  1383. resp = self.delete_url(
  1384. "api/tenant/tenant-one/autohold/%s" % request_id,
  1385. headers={'Authorization': 'Bearer %s' % token})
  1386. self.assertEqual(204, resp.status_code, resp.text)
  1387. # autohold-list should be empty now
  1388. resp = self.get_url(
  1389. "api/tenant/tenant-one/autohold")
  1390. self.assertEqual(200, resp.status_code, resp.text)
  1391. autohold_requests = resp.json()
  1392. self.assertEqual([], autohold_requests)
  1393. def _test_enqueue(self, use_trigger=False):
  1394. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1395. A.addApproval('Code-Review', 2)
  1396. A.addApproval('Approved', 1)
  1397. authz = {'iss': 'zuul_operator',
  1398. 'aud': 'zuul.example.com',
  1399. 'sub': 'testuser',
  1400. 'zuul': {
  1401. 'admin': ['tenant-one', ]
  1402. },
  1403. 'exp': time.time() + 3600}
  1404. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1405. algorithm='HS256').decode('utf-8')
  1406. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1407. enqueue_args = {'tenant': 'tenant-one',
  1408. 'project': 'org/project', }
  1409. change = {'change': '1,1',
  1410. 'pipeline': 'gate', }
  1411. if use_trigger:
  1412. change['trigger'] = 'gerrit'
  1413. req = self.post_url(path % enqueue_args,
  1414. headers={'Authorization': 'Bearer %s' % token},
  1415. json=change)
  1416. # The JSON returned is the same as the client's output
  1417. self.assertEqual(200, req.status_code, req.text)
  1418. data = req.json()
  1419. self.assertEqual(True, data)
  1420. self.waitUntilSettled()
  1421. def test_enqueue_with_deprecated_trigger(self):
  1422. """Test that the admin web interface can enqueue a change"""
  1423. # TODO(mhu) remove in a few releases
  1424. self._test_enqueue(use_trigger=True)
  1425. def test_enqueue(self):
  1426. """Test that the admin web interface can enqueue a change"""
  1427. self._test_enqueue()
  1428. def _test_enqueue_ref(self, use_trigger=False):
  1429. """Test that the admin web interface can enqueue a ref"""
  1430. p = "review.example.com/org/project"
  1431. upstream = self.getUpstreamRepos([p])
  1432. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1433. A.setMerged()
  1434. A_commit = str(upstream[p].commit('master'))
  1435. self.log.debug("A commit: %s" % A_commit)
  1436. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1437. enqueue_args = {'tenant': 'tenant-one',
  1438. 'project': 'org/project', }
  1439. ref = {'ref': 'master',
  1440. 'oldrev': '90f173846e3af9154517b88543ffbd1691f31366',
  1441. 'newrev': A_commit,
  1442. 'pipeline': 'post', }
  1443. if use_trigger:
  1444. ref['trigger'] = 'gerrit'
  1445. authz = {'iss': 'zuul_operator',
  1446. 'aud': 'zuul.example.com',
  1447. 'sub': 'testuser',
  1448. 'zuul': {
  1449. 'admin': ['tenant-one', ]
  1450. },
  1451. 'exp': time.time() + 3600}
  1452. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1453. algorithm='HS256').decode('utf-8')
  1454. req = self.post_url(path % enqueue_args,
  1455. headers={'Authorization': 'Bearer %s' % token},
  1456. json=ref)
  1457. self.assertEqual(200, req.status_code, req.text)
  1458. # The JSON returned is the same as the client's output
  1459. data = req.json()
  1460. self.assertEqual(True, data)
  1461. self.waitUntilSettled()
  1462. def test_enqueue_ref_with_deprecated_trigger(self):
  1463. """Test that the admin web interface can enqueue a ref"""
  1464. # TODO(mhu) remove in a few releases
  1465. self._test_enqueue_ref(use_trigger=True)
  1466. def test_enqueue_ref(self):
  1467. """Test that the admin web interface can enqueue a ref"""
  1468. self._test_enqueue_ref()
  1469. def test_dequeue(self):
  1470. """Test that the admin web interface can dequeue a change"""
  1471. start_builds = len(self.builds)
  1472. self.create_branch('org/project', 'stable')
  1473. self.executor_server.hold_jobs_in_build = True
  1474. self.commitConfigUpdate('common-config', 'layouts/timer.yaml')
  1475. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1476. self.waitUntilSettled()
  1477. for _ in iterate_timeout(30, 'Wait for a build on hold'):
  1478. if len(self.builds) > start_builds:
  1479. break
  1480. self.waitUntilSettled()
  1481. authz = {'iss': 'zuul_operator',
  1482. 'aud': 'zuul.example.com',
  1483. 'sub': 'testuser',
  1484. 'zuul': {
  1485. 'admin': ['tenant-one', ]
  1486. },
  1487. 'exp': time.time() + 3600}
  1488. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1489. algorithm='HS256').decode('utf-8')
  1490. path = "api/tenant/%(tenant)s/project/%(project)s/dequeue"
  1491. dequeue_args = {'tenant': 'tenant-one',
  1492. 'project': 'org/project', }
  1493. change = {'ref': 'refs/heads/stable',
  1494. 'pipeline': 'periodic', }
  1495. req = self.post_url(path % dequeue_args,
  1496. headers={'Authorization': 'Bearer %s' % token},
  1497. json=change)
  1498. # The JSON returned is the same as the client's output
  1499. self.assertEqual(200, req.status_code, req.text)
  1500. data = req.json()
  1501. self.assertEqual(True, data)
  1502. self.waitUntilSettled()
  1503. self.commitConfigUpdate('common-config',
  1504. 'layouts/no-timer.yaml')
  1505. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  1506. self.waitUntilSettled()
  1507. self.executor_server.hold_jobs_in_build = False
  1508. self.executor_server.release()
  1509. self.waitUntilSettled()
  1510. self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1)
  1511. def test_OPTIONS(self):
  1512. """Ensure that protected endpoints handle CORS preflight requests
  1513. properly"""
  1514. # Note that %tenant, %project are not relevant here. The client is
  1515. # just checking what the endpoint allows.
  1516. endpoints = [
  1517. {'action': 'enqueue',
  1518. 'path': 'api/tenant/my-tenant/project/my-project/enqueue',
  1519. 'allowed_methods': ['POST', ]},
  1520. {'action': 'enqueue_ref',
  1521. 'path': 'api/tenant/my-tenant/project/my-project/enqueue',
  1522. 'allowed_methods': ['POST', ]},
  1523. {'action': 'autohold',
  1524. 'path': 'api/tenant/my-tenant/project/my-project/autohold',
  1525. 'allowed_methods': ['GET', 'POST', ]},
  1526. {'action': 'autohold_by_request_id',
  1527. 'path': 'api/tenant/my-tenant/autohold/123',
  1528. 'allowed_methods': ['GET', 'DELETE', ]},
  1529. {'action': 'authorizations',
  1530. 'path': 'api/user/authorizations',
  1531. 'allowed_methods': ['GET', ]},
  1532. {'action': 'dequeue',
  1533. 'path': 'api/tenant/my-tenant/project/my-project/enqueue',
  1534. 'allowed_methods': ['POST', ]},
  1535. ]
  1536. for endpoint in endpoints:
  1537. preflight = self.options_url(
  1538. endpoint['path'],
  1539. headers={'Access-Control-Request-Method': 'GET',
  1540. 'Access-Control-Request-Headers': 'Authorization'})
  1541. self.assertEqual(
  1542. 204,
  1543. preflight.status_code,
  1544. "%s failed: %s" % (endpoint['action'], preflight.text))
  1545. self.assertEqual(
  1546. '*',
  1547. preflight.headers.get('Access-Control-Allow-Origin'),
  1548. "%s failed: %s" % (endpoint['action'], preflight.headers))
  1549. self.assertEqual(
  1550. 'Authorization, Content-Type',
  1551. preflight.headers.get('Access-Control-Allow-Headers'),
  1552. "%s failed: %s" % (endpoint['action'], preflight.headers))
  1553. allowed_methods = preflight.headers.get(
  1554. 'Access-Control-Allow-Methods').split(', ')
  1555. self.assertTrue(
  1556. 'OPTIONS' in allowed_methods,
  1557. "%s has allowed methods: %s" % (endpoint['action'],
  1558. allowed_methods))
  1559. for method in endpoint['allowed_methods']:
  1560. self.assertTrue(
  1561. method in allowed_methods,
  1562. "%s has allowed methods: %s,"
  1563. " expected: %s" % (endpoint['action'],
  1564. allowed_methods,
  1565. endpoint['allowed_methods']))
  1566. class TestTenantScopedWebApiWithAuthRules(BaseTestWeb):
  1567. config_file = 'zuul-admin-web-no-override.conf'
  1568. tenant_config_file = 'config/authorization/single-tenant/main.yaml'
  1569. def test_override_not_allowed(self):
  1570. """Test that authz cannot be overriden if config does not allow it"""
  1571. args = {"reason": "some reason",
  1572. "count": 1,
  1573. 'job': 'project-test2',
  1574. 'change': None,
  1575. 'ref': None,
  1576. 'node_hold_expiration': None}
  1577. authz = {'iss': 'zuul_operator',
  1578. 'aud': 'zuul.example.com',
  1579. 'sub': 'testuser',
  1580. 'zuul': {
  1581. 'admin': ['tenant-one', ],
  1582. },
  1583. 'exp': time.time() + 3600}
  1584. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1585. algorithm='HS256').decode('utf-8')
  1586. req = self.post_url(
  1587. 'api/tenant/tenant-one/project/org/project/autohold',
  1588. headers={'Authorization': 'Bearer %s' % token},
  1589. json=args)
  1590. self.assertEqual(401, req.status_code, req.text)
  1591. def test_tenant_level_rule(self):
  1592. """Test that authz rules defined at tenant level are checked"""
  1593. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1594. def _test_project_enqueue_with_authz(i, project, authz, expected):
  1595. f_ch = self.fake_gerrit.addFakeChange(project, 'master',
  1596. '%s %i' % (project, i))
  1597. f_ch.addApproval('Code-Review', 2)
  1598. f_ch.addApproval('Approved', 1)
  1599. change = {'trigger': 'gerrit',
  1600. 'change': '%i,1' % i,
  1601. 'pipeline': 'gate', }
  1602. enqueue_args = {'tenant': 'tenant-one',
  1603. 'project': project, }
  1604. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1605. algorithm='HS256').decode('utf-8')
  1606. req = self.post_url(path % enqueue_args,
  1607. headers={'Authorization': 'Bearer %s' % token},
  1608. json=change)
  1609. self.assertEqual(expected, req.status_code, req.text)
  1610. self.waitUntilSettled()
  1611. i = 0
  1612. for p in ['org/project', 'org/project1', 'org/project2']:
  1613. i += 1
  1614. # Authorized sub
  1615. authz = {'iss': 'zuul_operator',
  1616. 'aud': 'zuul.example.com',
  1617. 'sub': 'venkman',
  1618. 'exp': time.time() + 3600}
  1619. _test_project_enqueue_with_authz(i, p, authz, 200)
  1620. i += 1
  1621. # Unauthorized sub
  1622. authz = {'iss': 'zuul_operator',
  1623. 'aud': 'zuul.example.com',
  1624. 'sub': 'vigo',
  1625. 'exp': time.time() + 3600}
  1626. _test_project_enqueue_with_authz(i, p, authz, 403)
  1627. i += 1
  1628. # unauthorized issuer
  1629. authz = {'iss': 'columbia.edu',
  1630. 'aud': 'zuul.example.com',
  1631. 'sub': 'stantz',
  1632. 'exp': time.time() + 3600}
  1633. _test_project_enqueue_with_authz(i, p, authz, 401)
  1634. self.waitUntilSettled()
  1635. def test_group_rule(self):
  1636. """Test a group rule"""
  1637. A = self.fake_gerrit.addFakeChange('org/project2', 'master', 'A')
  1638. A.addApproval('Code-Review', 2)
  1639. A.addApproval('Approved', 1)
  1640. authz = {'iss': 'zuul_operator',
  1641. 'aud': 'zuul.example.com',
  1642. 'sub': 'melnitz',
  1643. 'groups': ['ghostbusters', 'secretary'],
  1644. 'exp': time.time() + 3600}
  1645. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1646. algorithm='HS256').decode('utf-8')
  1647. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1648. enqueue_args = {'tenant': 'tenant-one',
  1649. 'project': 'org/project2', }
  1650. change = {'trigger': 'gerrit',
  1651. 'change': '1,1',
  1652. 'pipeline': 'gate', }
  1653. req = self.post_url(path % enqueue_args,
  1654. headers={'Authorization': 'Bearer %s' % token},
  1655. json=change)
  1656. self.assertEqual(200, req.status_code, req.text)
  1657. self.waitUntilSettled()
  1658. def test_depth_claim_rule(self):
  1659. """Test a rule based on a complex claim"""
  1660. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1661. A.addApproval('Code-Review', 2)
  1662. A.addApproval('Approved', 1)
  1663. authz = {'iss': 'zuul_operator',
  1664. 'aud': 'zuul.example.com',
  1665. 'sub': 'zeddemore',
  1666. 'vehicle': {
  1667. 'car': 'ecto-1'},
  1668. 'exp': time.time() + 3600}
  1669. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1670. algorithm='HS256').decode('utf-8')
  1671. path = "api/tenant/%(tenant)s/project/%(project)s/enqueue"
  1672. enqueue_args = {'tenant': 'tenant-one',
  1673. 'project': 'org/project', }
  1674. change = {'trigger': 'gerrit',
  1675. 'change': '1,1',
  1676. 'pipeline': 'gate', }
  1677. req = self.post_url(path % enqueue_args,
  1678. headers={'Authorization': 'Bearer %s' % token},
  1679. json=change)
  1680. self.assertEqual(200, req.status_code, req.text)
  1681. self.waitUntilSettled()
  1682. def test_user_actions_action_override(self):
  1683. """Test that user with 'zuul.admin' claim does NOT get it back"""
  1684. admin_tenants = ['tenant-zero', ]
  1685. authz = {'iss': 'zuul_operator',
  1686. 'aud': 'zuul.example.com',
  1687. 'sub': 'testuser',
  1688. 'zuul': {'admin': admin_tenants},
  1689. 'exp': time.time() + 3600}
  1690. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1691. algorithm='HS256').decode('utf-8')
  1692. req = self.get_url('/api/user/authorizations',
  1693. headers={'Authorization': 'Bearer %s' % token})
  1694. self.assertEqual(401, req.status_code, req.text)
  1695. def test_user_actions(self):
  1696. """Test that users get the right 'zuul.actions' trees"""
  1697. users = [
  1698. {'authz': {'iss': 'zuul_operator',
  1699. 'aud': 'zuul.example.com',
  1700. 'sub': 'vigo'},
  1701. 'zuul.admin': []},
  1702. {'authz': {'iss': 'zuul_operator',
  1703. 'aud': 'zuul.example.com',
  1704. 'sub': 'venkman'},
  1705. 'zuul.admin': ['tenant-one', ]},
  1706. {'authz': {'iss': 'zuul_operator',
  1707. 'aud': 'zuul.example.com',
  1708. 'sub': 'stantz'},
  1709. 'zuul.admin': []},
  1710. {'authz': {'iss': 'zuul_operator',
  1711. 'aud': 'zuul.example.com',
  1712. 'sub': 'zeddemore',
  1713. 'vehicle': {
  1714. 'car': 'ecto-1'
  1715. }},
  1716. 'zuul.admin': ['tenant-one', ]},
  1717. {'authz': {'iss': 'zuul_operator',
  1718. 'aud': 'zuul.example.com',
  1719. 'sub': 'melnitz',
  1720. 'groups': ['secretary', 'ghostbusters']},
  1721. 'zuul.admin': ['tenant-one', ]},
  1722. ]
  1723. for test_user in users:
  1724. authz = test_user['authz']
  1725. authz['exp'] = time.time() + 3600
  1726. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1727. algorithm='HS256').decode('utf-8')
  1728. req = self.get_url('/api/user/authorizations',
  1729. headers={'Authorization': 'Bearer %s' % token})
  1730. self.assertEqual(200, req.status_code, req.text)
  1731. data = req.json()
  1732. self.assertTrue('zuul' in data,
  1733. "%s got %s" % (authz['sub'], data))
  1734. self.assertTrue('admin' in data['zuul'],
  1735. "%s got %s" % (authz['sub'], data))
  1736. self.assertEqual(test_user['zuul.admin'],
  1737. data['zuul']['admin'],
  1738. "%s got %s" % (authz['sub'], data))
  1739. def test_authorizations_no_header(self):
  1740. """Test that missing Authorization header results in HTTP 401"""
  1741. req = self.get_url('/api/user/authorizations')
  1742. self.assertEqual(401, req.status_code, req.text)
  1743. class TestTenantScopedWebApiTokenWithExpiry(BaseTestWeb):
  1744. config_file = 'zuul-admin-web-token-expiry.conf'
  1745. def test_iat_claim_mandatory(self):
  1746. """Test that the 'iat' claim is mandatory when
  1747. max_validity_time is set"""
  1748. authz = {'iss': 'zuul_operator',
  1749. 'sub': 'testuser',
  1750. 'aud': 'zuul.example.com',
  1751. 'zuul': {
  1752. 'admin': ['tenant-one', ]
  1753. },
  1754. 'exp': time.time() + 3600}
  1755. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1756. algorithm='HS256').decode('utf-8')
  1757. resp = self.post_url(
  1758. "api/tenant/tenant-one/project/org/project/autohold",
  1759. headers={'Authorization': 'Bearer %s' % token},
  1760. json={'job': 'project-test1',
  1761. 'count': 1,
  1762. 'reason': 'because',
  1763. 'node_hold_expiration': 36000})
  1764. self.assertEqual(401, resp.status_code)
  1765. resp = self.post_url(
  1766. "api/tenant/tenant-one/project/org/project/enqueue",
  1767. headers={'Authorization': 'Bearer %s' % token},
  1768. json={'trigger': 'gerrit',
  1769. 'change': '2,1',
  1770. 'pipeline': 'check'})
  1771. self.assertEqual(401, resp.status_code)
  1772. resp = self.post_url(
  1773. "api/tenant/tenant-one/project/org/project/enqueue",
  1774. headers={'Authorization': 'Bearer %s' % token},
  1775. json={'trigger': 'gerrit',
  1776. 'ref': 'abcd',
  1777. 'newrev': 'aaaa',
  1778. 'oldrev': 'bbbb',
  1779. 'pipeline': 'check'})
  1780. self.assertEqual(401, resp.status_code)
  1781. def test_token_from_the_future(self):
  1782. authz = {'iss': 'zuul_operator',
  1783. 'sub': 'testuser',
  1784. 'aud': 'zuul.example.com',
  1785. 'zuul': {
  1786. 'admin': ['tenant-one', ],
  1787. },
  1788. 'exp': time.time() + 7200,
  1789. 'iat': time.time() + 3600}
  1790. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1791. algorithm='HS256').decode('utf-8')
  1792. resp = self.post_url(
  1793. "api/tenant/tenant-one/project/org/project/autohold",
  1794. headers={'Authorization': 'Bearer %s' % token},
  1795. json={'job': 'project-test1',
  1796. 'count': 1,
  1797. 'reason': 'because',
  1798. 'node_hold_expiration': 36000})
  1799. self.assertEqual(401, resp.status_code)
  1800. resp = self.post_url(
  1801. "api/tenant/tenant-one/project/org/project/enqueue",
  1802. headers={'Authorization': 'Bearer %s' % token},
  1803. json={'trigger': 'gerrit',
  1804. 'change': '2,1',
  1805. 'pipeline': 'check'})
  1806. self.assertEqual(401, resp.status_code)
  1807. resp = self.post_url(
  1808. "api/tenant/tenant-one/project/org/project/enqueue",
  1809. headers={'Authorization': 'Bearer %s' % token},
  1810. json={'trigger': 'gerrit',
  1811. 'ref': 'abcd',
  1812. 'newrev': 'aaaa',
  1813. 'oldrev': 'bbbb',
  1814. 'pipeline': 'check'})
  1815. self.assertEqual(401, resp.status_code)
  1816. def test_token_expired(self):
  1817. authz = {'iss': 'zuul_operator',
  1818. 'sub': 'testuser',
  1819. 'aud': 'zuul.example.com',
  1820. 'zuul': {
  1821. 'admin': ['tenant-one', ],
  1822. },
  1823. 'exp': time.time() + 3600,
  1824. 'iat': time.time()}
  1825. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1826. algorithm='HS256').decode('utf-8')
  1827. time.sleep(10)
  1828. resp = self.post_url(
  1829. "api/tenant/tenant-one/project/org/project/autohold",
  1830. headers={'Authorization': 'Bearer %s' % token},
  1831. json={'job': 'project-test1',
  1832. 'count': 1,
  1833. 'reason': 'because',
  1834. 'node_hold_expiration': 36000})
  1835. self.assertEqual(401, resp.status_code)
  1836. resp = self.post_url(
  1837. "api/tenant/tenant-one/project/org/project/enqueue",
  1838. headers={'Authorization': 'Bearer %s' % token},
  1839. json={'trigger': 'gerrit',
  1840. 'change': '2,1',
  1841. 'pipeline': 'check'})
  1842. self.assertEqual(401, resp.status_code)
  1843. resp = self.post_url(
  1844. "api/tenant/tenant-one/project/org/project/enqueue",
  1845. headers={'Authorization': 'Bearer %s' % token},
  1846. json={'trigger': 'gerrit',
  1847. 'ref': 'abcd',
  1848. 'newrev': 'aaaa',
  1849. 'oldrev': 'bbbb',
  1850. 'pipeline': 'check'})
  1851. self.assertEqual(401, resp.status_code)
  1852. def test_autohold(self):
  1853. """Test that autohold can be set through the admin web interface"""
  1854. args = {"reason": "some reason",
  1855. "count": 1,
  1856. 'job': 'project-test2',
  1857. 'change': None,
  1858. 'ref': None,
  1859. 'node_hold_expiration': None}
  1860. authz = {'iss': 'zuul_operator',
  1861. 'aud': 'zuul.example.com',
  1862. 'sub': 'testuser',
  1863. 'zuul': {
  1864. 'admin': ['tenant-one', ],
  1865. },
  1866. 'exp': time.time() + 3600,
  1867. 'iat': time.time()}
  1868. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1869. algorithm='HS256').decode('utf-8')
  1870. req = self.post_url(
  1871. 'api/tenant/tenant-one/project/org/project/autohold',
  1872. headers={'Authorization': 'Bearer %s' % token},
  1873. json=args)
  1874. self.assertEqual(200, req.status_code, req.text)
  1875. data = req.json()
  1876. self.assertEqual(True, data)
  1877. # Check result in rpc client
  1878. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1879. self.gearman_server.port)
  1880. self.addCleanup(client.shutdown)
  1881. autohold_requests = client.autohold_list()
  1882. self.assertNotEqual([], autohold_requests)
  1883. self.assertEqual(1, len(autohold_requests))
  1884. ah_request = autohold_requests[0]
  1885. self.assertEqual('tenant-one', ah_request['tenant'])
  1886. self.assertIn('org/project', ah_request['project'])
  1887. self.assertEqual('project-test2', ah_request['job'])
  1888. self.assertEqual(".*", ah_request['ref_filter'])
  1889. self.assertEqual("some reason", ah_request['reason'])
  1890. class TestHeldAttributeInBuildInfo(ZuulDBTestCase, BaseTestWeb):
  1891. config_file = 'zuul-sql-driver.conf'
  1892. tenant_config_file = 'config/sql-driver/main.yaml'
  1893. def test_autohold_and_retrieve_held_build_info(self):
  1894. """Ensure the "held" attribute can be used to filter builds"""
  1895. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1896. self.gearman_server.port)
  1897. self.addCleanup(client.shutdown)
  1898. r = client.autohold('tenant-one', 'org/project', 'project-test2',
  1899. "", "", "reason text", 1)
  1900. self.assertTrue(r)
  1901. B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
  1902. self.executor_server.failJob('project-test2', B)
  1903. self.fake_gerrit.addEvent(B.getPatchsetCreatedEvent(1))
  1904. self.waitUntilSettled()
  1905. self.executor_server.hold_jobs_in_build = False
  1906. self.executor_server.release()
  1907. self.waitUntilSettled()
  1908. all_builds_resp = self.get_url("api/tenant/tenant-one/builds?"
  1909. "project=org/project")
  1910. held_builds_resp = self.get_url("api/tenant/tenant-one/builds?"
  1911. "project=org/project&"
  1912. "held=1")
  1913. self.assertEqual(200,
  1914. all_builds_resp.status_code,
  1915. all_builds_resp.text)
  1916. self.assertEqual(200,
  1917. held_builds_resp.status_code,
  1918. held_builds_resp.text)
  1919. all_builds = all_builds_resp.json()
  1920. held_builds = held_builds_resp.json()
  1921. self.assertEqual(len(held_builds), 1, all_builds)
  1922. held_build = held_builds[0]
  1923. self.assertEqual('project-test2', held_build['job_name'], held_build)
  1924. self.assertEqual(True, held_build['held'], held_build)
  1925. class TestWebMulti(BaseTestWeb):
  1926. config_file = 'zuul-gerrit-github.conf'
  1927. def test_web_connections_list_multi(self):
  1928. data = self.get_url('api/connections').json()
  1929. gerrit_connection = {
  1930. 'driver': 'gerrit',
  1931. 'name': 'gerrit',
  1932. 'baseurl': 'https://review.example.com',
  1933. 'canonical_hostname': 'review.example.com',
  1934. 'server': 'review.example.com',
  1935. 'port': 29418,
  1936. }
  1937. github_connection = {
  1938. 'baseurl': 'https://api.github.com',
  1939. 'canonical_hostname': 'github.com',
  1940. 'driver': 'github',
  1941. 'name': 'github',
  1942. 'server': 'github.com',
  1943. }
  1944. self.assertEqual([gerrit_connection, github_connection], data)
  1945. # TODO Remove this class once REST support is removed from Zuul CLI, or
  1946. # switch to the gearman client
  1947. class TestCLIViaWebApi(BaseTestWeb):
  1948. config_file = 'zuul-admin-web.conf'
  1949. def test_autohold(self):
  1950. """Test that autohold can be set with the CLI through REST"""
  1951. authz = {'iss': 'zuul_operator',
  1952. 'aud': 'zuul.example.com',
  1953. 'sub': 'testuser',
  1954. 'zuul': {
  1955. 'admin': ['tenant-one', ]
  1956. },
  1957. 'exp': time.time() + 3600}
  1958. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1959. algorithm='HS256').decode('utf-8')
  1960. p = subprocess.Popen(
  1961. [os.path.join(sys.prefix, 'bin/zuul'),
  1962. '--zuul-url', self.base_url, '--auth-token', token,
  1963. 'autohold', '--reason', 'some reason',
  1964. '--tenant', 'tenant-one', '--project', 'org/project',
  1965. '--job', 'project-test2', '--count', '1'],
  1966. stdout=subprocess.PIPE)
  1967. output = p.communicate()
  1968. self.assertEqual(p.returncode, 0, output[0])
  1969. # Check result in rpc client
  1970. client = zuul.rpcclient.RPCClient('127.0.0.1',
  1971. self.gearman_server.port)
  1972. self.addCleanup(client.shutdown)
  1973. autohold_requests = client.autohold_list()
  1974. self.assertNotEqual([], autohold_requests)
  1975. self.assertEqual(1, len(autohold_requests))
  1976. request = autohold_requests[0]
  1977. self.assertEqual('tenant-one', request['tenant'])
  1978. self.assertIn('org/project', request['project'])
  1979. self.assertEqual('project-test2', request['job'])
  1980. self.assertEqual(".*", request['ref_filter'])
  1981. self.assertEqual("some reason", request['reason'])
  1982. self.assertEqual(1, request['max_count'])
  1983. def test_enqueue(self):
  1984. """Test that the CLI can enqueue a change via REST"""
  1985. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  1986. A.addApproval('Code-Review', 2)
  1987. A.addApproval('Approved', 1)
  1988. authz = {'iss': 'zuul_operator',
  1989. 'aud': 'zuul.example.com',
  1990. 'sub': 'testuser',
  1991. 'zuul': {
  1992. 'admin': ['tenant-one', ]
  1993. },
  1994. 'exp': time.time() + 3600}
  1995. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  1996. algorithm='HS256').decode('utf-8')
  1997. p = subprocess.Popen(
  1998. [os.path.join(sys.prefix, 'bin/zuul'),
  1999. '--zuul-url', self.base_url, '--auth-token', token,
  2000. 'enqueue', '--tenant', 'tenant-one',
  2001. '--project', 'org/project',
  2002. '--pipeline', 'gate', '--change', '1,1'],
  2003. stdout=subprocess.PIPE)
  2004. output = p.communicate()
  2005. self.assertEqual(p.returncode, 0, output[0])
  2006. self.waitUntilSettled()
  2007. def test_enqueue_ref(self):
  2008. """Test that the CLI can enqueue a ref via REST"""
  2009. p = "review.example.com/org/project"
  2010. upstream = self.getUpstreamRepos([p])
  2011. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  2012. A.setMerged()
  2013. A_commit = str(upstream[p].commit('master'))
  2014. self.log.debug("A commit: %s" % A_commit)
  2015. authz = {'iss': 'zuul_operator',
  2016. 'aud': 'zuul.example.com',
  2017. 'sub': 'testuser',
  2018. 'zuul': {
  2019. 'admin': ['tenant-one', ]
  2020. },
  2021. 'exp': time.time() + 3600}
  2022. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  2023. algorithm='HS256').decode('utf-8')
  2024. p = subprocess.Popen(
  2025. [os.path.join(sys.prefix, 'bin/zuul'),
  2026. '--zuul-url', self.base_url, '--auth-token', token,
  2027. 'enqueue-ref', '--tenant', 'tenant-one',
  2028. '--project', 'org/project',
  2029. '--pipeline', 'post', '--ref', 'master',
  2030. '--oldrev', '90f173846e3af9154517b88543ffbd1691f31366',
  2031. '--newrev', A_commit],
  2032. stdout=subprocess.PIPE)
  2033. output = p.communicate()
  2034. self.assertEqual(p.returncode, 0, output[0])
  2035. self.waitUntilSettled()
  2036. def test_dequeue(self):
  2037. """Test that the CLI can dequeue a change via REST"""
  2038. start_builds = len(self.builds)
  2039. self.create_branch('org/project', 'stable')
  2040. self.executor_server.hold_jobs_in_build = True
  2041. self.commitConfigUpdate('common-config', 'layouts/timer.yaml')
  2042. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  2043. self.waitUntilSettled()
  2044. for _ in iterate_timeout(30, 'Wait for a build on hold'):
  2045. if len(self.builds) > start_builds:
  2046. break
  2047. self.waitUntilSettled()
  2048. authz = {'iss': 'zuul_operator',
  2049. 'aud': 'zuul.example.com',
  2050. 'sub': 'testuser',
  2051. 'zuul': {
  2052. 'admin': ['tenant-one', ]
  2053. },
  2054. 'exp': time.time() + 3600}
  2055. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  2056. algorithm='HS256').decode('utf-8')
  2057. p = subprocess.Popen(
  2058. [os.path.join(sys.prefix, 'bin/zuul'),
  2059. '--zuul-url', self.base_url, '--auth-token', token,
  2060. 'dequeue', '--tenant', 'tenant-one', '--project', 'org/project',
  2061. '--pipeline', 'periodic', '--ref', 'refs/heads/stable'],
  2062. stdout=subprocess.PIPE)
  2063. output = p.communicate()
  2064. self.assertEqual(p.returncode, 0, output[0])
  2065. self.waitUntilSettled()
  2066. self.commitConfigUpdate('common-config',
  2067. 'layouts/no-timer.yaml')
  2068. self.scheds.execute(lambda app: app.sched.reconfigure(app.config))
  2069. self.waitUntilSettled()
  2070. self.executor_server.hold_jobs_in_build = False
  2071. self.executor_server.release()
  2072. self.waitUntilSettled()
  2073. self.assertEqual(self.countJobResults(self.history, 'ABORTED'), 1)
  2074. def test_promote(self):
  2075. "Test that the RPC client can promote a change"
  2076. self.executor_server.hold_jobs_in_build = True
  2077. A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A')
  2078. B = self.fake_gerrit.addFakeChange('org/project', 'master', 'B')
  2079. C = self.fake_gerrit.addFakeChange('org/project', 'master', 'C')
  2080. A.addApproval('Code-Review', 2)
  2081. B.addApproval('Code-Review', 2)
  2082. C.addApproval('Code-Review', 2)
  2083. self.fake_gerrit.addEvent(A.addApproval('Approved', 1))
  2084. self.fake_gerrit.addEvent(B.addApproval('Approved', 1))
  2085. self.fake_gerrit.addEvent(C.addApproval('Approved', 1))
  2086. self.waitUntilSettled()
  2087. tenant = self.scheds.first.sched.abide.tenants.get('tenant-one')
  2088. items = tenant.layout.pipelines['gate'].getAllItems()
  2089. enqueue_times = {}
  2090. for item in items:
  2091. enqueue_times[str(item.change)] = item.enqueue_time
  2092. # Promote B and C using the cli
  2093. authz = {'iss': 'zuul_operator',
  2094. 'aud': 'zuul.example.com',
  2095. 'sub': 'testuser',
  2096. 'zuul': {
  2097. 'admin': ['tenant-one', ]
  2098. },
  2099. 'exp': time.time() + 3600}
  2100. token = jwt.encode(authz, key='NoDanaOnlyZuul',
  2101. algorithm='HS256').decode('utf-8')
  2102. p = subprocess.Popen(
  2103. [os.path.join(sys.prefix, 'bin/zuul'),
  2104. '--zuul-url', self.base_url, '--auth-token', token,
  2105. 'promote', '--tenant', 'tenant-one',
  2106. '--pipeline', 'gate', '--changes', '2,1', '3,1'],
  2107. stdout=subprocess.PIPE)
  2108. output = p.communicate()
  2109. self.assertEqual(p.returncode, 0, output[0])
  2110. self.waitUntilSettled()
  2111. # ensure that enqueue times are durable
  2112. items = tenant.layout.pipelines['gate'].getAllItems()
  2113. for item in items:
  2114. self.assertEqual(
  2115. enqueue_times[str(item.change)], item.enqueue_time)
  2116. self.waitUntilSettled()
  2117. self.executor_server.release('.*-merge')
  2118. self.waitUntilSettled()
  2119. self.executor_server.release('.*-merge')
  2120. self.waitUntilSettled()
  2121. self.executor_server.release('.*-merge')
  2122. self.waitUntilSettled()
  2123. self.assertEqual(len(self.builds), 6)
  2124. self.assertEqual(self.builds[0].name, 'project-test1')
  2125. self.assertEqual(self.builds[1].name, 'project-test2')
  2126. self.assertEqual(self.builds[2].name, 'project-test1')
  2127. self.assertEqual(self.builds[3].name, 'project-test2')
  2128. self.assertEqual(self.builds[4].name, 'project-test1')
  2129. self.assertEqual(self.builds[5].name, 'project-test2')
  2130. self.assertTrue(self.builds[0].hasChanges(B))
  2131. self.assertFalse(self.builds[0].hasChanges(A))
  2132. self.assertFalse(self.builds[0].hasChanges(C))
  2133. self.assertTrue(self.builds[2].hasChanges(B))
  2134. self.assertTrue(self.builds[2].hasChanges(C))
  2135. self.assertFalse(self.builds[2].hasChanges(A))
  2136. self.assertTrue(self.builds[4].hasChanges(B))
  2137. self.assertTrue(self.builds[4].hasChanges(C))
  2138. self.assertTrue(self.builds[4].hasChanges(A))
  2139. self.executor_server.release()
  2140. self.waitUntilSettled()
  2141. self.assertEqual(A.data['status'], 'MERGED')
  2142. self.assertEqual(A.reported, 2)
  2143. self.assertEqual(B.data['status'], 'MERGED')
  2144. self.assertEqual(B.reported, 2)
  2145. self.assertEqual(C.data['status'], 'MERGED')
  2146. self.assertEqual(C.reported, 2)