James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | |
| 3 | # Copyright 2014 Hewlett-Packard Development Company, L.P. |
| 4 | # Copyright 2014 Rackspace Australia |
| 5 | # |
| 6 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 7 | # not use this file except in compliance with the License. You may obtain |
| 8 | # a copy of the License at |
| 9 | # |
| 10 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 11 | # |
| 12 | # Unless required by applicable law or agreed to in writing, software |
| 13 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 14 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 15 | # License for the specific language governing permissions and limitations |
| 16 | # under the License. |
| 17 | |
| 18 | import asyncio |
| 19 | import threading |
| 20 | import os |
| 21 | import json |
| 22 | import urllib |
| 23 | import time |
| 24 | import socket |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 25 | |
| 26 | import zuul.web |
| 27 | |
| 28 | from tests.base import ZuulTestCase, FIXTURE_DIR |
| 29 | |
| 30 | |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 31 | class FakeConfig(object): |
| 32 | |
| 33 | def __init__(self, config): |
| 34 | self.config = config or {} |
| 35 | |
| 36 | def has_option(self, section, option): |
| 37 | return option in self.config.get(section, {}) |
| 38 | |
| 39 | def get(self, section, option): |
| 40 | return self.config.get(section, {}).get(option) |
| 41 | |
| 42 | |
| 43 | class BaseTestWeb(ZuulTestCase): |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 44 | tenant_config_file = 'config/single-tenant/main.yaml' |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 45 | config_ini_data = {} |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 46 | |
| 47 | def setUp(self): |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 48 | super(BaseTestWeb, self).setUp() |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 49 | self.executor_server.hold_jobs_in_build = True |
| 50 | A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') |
| 51 | A.addApproval('Code-Review', 2) |
| 52 | self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) |
| 53 | B = self.fake_gerrit.addFakeChange('org/project1', 'master', 'B') |
| 54 | B.addApproval('Code-Review', 2) |
| 55 | self.fake_gerrit.addEvent(B.addApproval('Approved', 1)) |
| 56 | self.waitUntilSettled() |
| 57 | |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 58 | self.zuul_ini_config = FakeConfig(self.config_ini_data) |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 59 | # Start the web server |
| 60 | self.web = zuul.web.ZuulWeb( |
| 61 | listen_address='127.0.0.1', listen_port=0, |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 62 | gear_server='127.0.0.1', gear_port=self.gearman_server.port, |
| 63 | info=zuul.model.WebInfo.fromConfig(self.zuul_ini_config) |
| 64 | ) |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 65 | loop = asyncio.new_event_loop() |
| 66 | loop.set_debug(True) |
| 67 | ws_thread = threading.Thread(target=self.web.run, args=(loop,)) |
| 68 | ws_thread.start() |
| 69 | self.addCleanup(loop.close) |
| 70 | self.addCleanup(ws_thread.join) |
| 71 | self.addCleanup(self.web.stop) |
| 72 | |
| 73 | self.host = 'localhost' |
| 74 | # Wait until web server is started |
| 75 | while True: |
| 76 | time.sleep(0.1) |
| 77 | if self.web.server is None: |
| 78 | continue |
| 79 | self.port = self.web.server.sockets[0].getsockname()[1] |
| 80 | print(self.host, self.port) |
| 81 | try: |
| 82 | with socket.create_connection((self.host, self.port)): |
| 83 | break |
| 84 | except ConnectionRefusedError: |
| 85 | pass |
| 86 | |
| 87 | def tearDown(self): |
| 88 | self.executor_server.hold_jobs_in_build = False |
| 89 | self.executor_server.release() |
| 90 | self.waitUntilSettled() |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 91 | super(BaseTestWeb, self).tearDown() |
| 92 | |
| 93 | |
| 94 | class TestWeb(BaseTestWeb): |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 95 | |
| 96 | def test_web_status(self): |
Tobias Henkel | e0bad8d | 2018-01-23 12:34:15 +0100 | [diff] [blame] | 97 | "Test that we can retrieve JSON status info" |
| 98 | self.executor_server.hold_jobs_in_build = True |
| 99 | A = self.fake_gerrit.addFakeChange('org/project', 'master', 'A') |
| 100 | A.addApproval('Code-Review', 2) |
| 101 | self.fake_gerrit.addEvent(A.addApproval('Approved', 1)) |
| 102 | self.waitUntilSettled() |
| 103 | |
| 104 | self.executor_server.release('project-merge') |
| 105 | self.waitUntilSettled() |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 106 | |
| 107 | req = urllib.request.Request( |
Monty Taylor | 9010dc5 | 2018-02-17 13:29:28 -0600 | [diff] [blame] | 108 | "http://localhost:%s/tenant-one/status" % self.port) |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 109 | f = urllib.request.urlopen(req) |
Tobias Henkel | e0bad8d | 2018-01-23 12:34:15 +0100 | [diff] [blame] | 110 | headers = f.info() |
| 111 | self.assertIn('Content-Length', headers) |
| 112 | self.assertIn('Content-Type', headers) |
| 113 | self.assertEqual( |
| 114 | 'application/json; charset=utf-8', headers['Content-Type']) |
| 115 | self.assertIn('Access-Control-Allow-Origin', headers) |
| 116 | self.assertIn('Cache-Control', headers) |
| 117 | self.assertIn('Last-Modified', headers) |
| 118 | data = f.read().decode('utf8') |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 119 | |
Tobias Henkel | e0bad8d | 2018-01-23 12:34:15 +0100 | [diff] [blame] | 120 | self.executor_server.hold_jobs_in_build = False |
| 121 | self.executor_server.release() |
| 122 | self.waitUntilSettled() |
| 123 | |
| 124 | data = json.loads(data) |
| 125 | status_jobs = [] |
| 126 | for p in data['pipelines']: |
| 127 | for q in p['change_queues']: |
| 128 | if p['name'] in ['gate', 'conflict']: |
| 129 | self.assertEqual(q['window'], 20) |
| 130 | else: |
| 131 | self.assertEqual(q['window'], 0) |
| 132 | for head in q['heads']: |
| 133 | for change in head: |
| 134 | self.assertTrue(change['active']) |
| 135 | self.assertIn(change['id'], ('1,1', '2,1', '3,1')) |
| 136 | for job in change['jobs']: |
| 137 | status_jobs.append(job) |
| 138 | self.assertEqual('project-merge', status_jobs[0]['name']) |
| 139 | # TODO(mordred) pull uuids from self.builds |
| 140 | self.assertEqual( |
| 141 | 'stream.html?uuid={uuid}&logfile=console.log'.format( |
| 142 | uuid=status_jobs[0]['uuid']), |
| 143 | status_jobs[0]['url']) |
| 144 | self.assertEqual( |
| 145 | 'finger://{hostname}/{uuid}'.format( |
| 146 | hostname=self.executor_server.hostname, |
| 147 | uuid=status_jobs[0]['uuid']), |
| 148 | status_jobs[0]['finger_url']) |
| 149 | # TOOD(mordred) configure a success-url on the base job |
| 150 | self.assertEqual( |
| 151 | 'finger://{hostname}/{uuid}'.format( |
| 152 | hostname=self.executor_server.hostname, |
| 153 | uuid=status_jobs[0]['uuid']), |
| 154 | status_jobs[0]['report_url']) |
| 155 | self.assertEqual('project-test1', status_jobs[1]['name']) |
| 156 | self.assertEqual( |
| 157 | 'stream.html?uuid={uuid}&logfile=console.log'.format( |
| 158 | uuid=status_jobs[1]['uuid']), |
| 159 | status_jobs[1]['url']) |
| 160 | self.assertEqual( |
| 161 | 'finger://{hostname}/{uuid}'.format( |
| 162 | hostname=self.executor_server.hostname, |
| 163 | uuid=status_jobs[1]['uuid']), |
| 164 | status_jobs[1]['finger_url']) |
| 165 | self.assertEqual( |
| 166 | 'finger://{hostname}/{uuid}'.format( |
| 167 | hostname=self.executor_server.hostname, |
| 168 | uuid=status_jobs[1]['uuid']), |
| 169 | status_jobs[1]['report_url']) |
| 170 | |
| 171 | self.assertEqual('project-test2', status_jobs[2]['name']) |
| 172 | self.assertEqual( |
| 173 | 'stream.html?uuid={uuid}&logfile=console.log'.format( |
| 174 | uuid=status_jobs[2]['uuid']), |
| 175 | status_jobs[2]['url']) |
| 176 | self.assertEqual( |
| 177 | 'finger://{hostname}/{uuid}'.format( |
| 178 | hostname=self.executor_server.hostname, |
| 179 | uuid=status_jobs[2]['uuid']), |
| 180 | status_jobs[2]['finger_url']) |
| 181 | self.assertEqual( |
| 182 | 'finger://{hostname}/{uuid}'.format( |
| 183 | hostname=self.executor_server.hostname, |
| 184 | uuid=status_jobs[2]['uuid']), |
| 185 | status_jobs[2]['report_url']) |
| 186 | |
| 187 | # check job dependencies |
| 188 | self.assertIsNotNone(status_jobs[0]['dependencies']) |
| 189 | self.assertIsNotNone(status_jobs[1]['dependencies']) |
| 190 | self.assertIsNotNone(status_jobs[2]['dependencies']) |
| 191 | self.assertEqual(len(status_jobs[0]['dependencies']), 0) |
| 192 | self.assertEqual(len(status_jobs[1]['dependencies']), 1) |
| 193 | self.assertEqual(len(status_jobs[2]['dependencies']), 1) |
| 194 | self.assertIn('project-merge', status_jobs[1]['dependencies']) |
| 195 | self.assertIn('project-merge', status_jobs[2]['dependencies']) |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 196 | |
| 197 | def test_web_bad_url(self): |
| 198 | # do we 404 correctly |
| 199 | req = urllib.request.Request( |
| 200 | "http://localhost:%s/status/foo" % self.port) |
| 201 | self.assertRaises(urllib.error.HTTPError, urllib.request.urlopen, req) |
| 202 | |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 203 | def test_web_find_change(self): |
| 204 | # can we filter by change id |
| 205 | req = urllib.request.Request( |
| 206 | "http://localhost:%s/tenant-one/status/change/1,1" % self.port) |
| 207 | f = urllib.request.urlopen(req) |
| 208 | data = json.loads(f.read().decode('utf8')) |
| 209 | |
| 210 | self.assertEqual(1, len(data), data) |
| 211 | self.assertEqual("org/project", data[0]['project']) |
| 212 | |
| 213 | req = urllib.request.Request( |
| 214 | "http://localhost:%s/tenant-one/status/change/2,1" % self.port) |
| 215 | f = urllib.request.urlopen(req) |
| 216 | data = json.loads(f.read().decode('utf8')) |
| 217 | |
| 218 | self.assertEqual(1, len(data), data) |
| 219 | self.assertEqual("org/project1", data[0]['project'], data) |
| 220 | |
| 221 | def test_web_keys(self): |
| 222 | with open(os.path.join(FIXTURE_DIR, 'public.pem'), 'rb') as f: |
| 223 | public_pem = f.read() |
| 224 | |
| 225 | req = urllib.request.Request( |
| 226 | "http://localhost:%s/tenant-one/org/project.pub" % |
| 227 | self.port) |
| 228 | f = urllib.request.urlopen(req) |
| 229 | self.assertEqual(f.read(), public_pem) |
| 230 | |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 231 | def test_web_404_on_unknown_tenant(self): |
| 232 | req = urllib.request.Request( |
Monty Taylor | 9010dc5 | 2018-02-17 13:29:28 -0600 | [diff] [blame] | 233 | "http://localhost:{}/non-tenant/status".format(self.port)) |
James E. Blair | 4f56826 | 2017-12-21 09:18:21 -0800 | [diff] [blame] | 234 | e = self.assertRaises( |
| 235 | urllib.error.HTTPError, urllib.request.urlopen, req) |
| 236 | self.assertEqual(404, e.code) |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 237 | |
| 238 | |
| 239 | class TestInfo(BaseTestWeb): |
| 240 | |
| 241 | def setUp(self): |
| 242 | super(TestInfo, self).setUp() |
| 243 | web_config = self.config_ini_data.get('web', {}) |
| 244 | self.websocket_url = web_config.get('websocket_url') |
| 245 | self.stats_url = web_config.get('stats_url') |
| 246 | statsd_config = self.config_ini_data.get('statsd', {}) |
| 247 | self.stats_prefix = statsd_config.get('prefix') |
| 248 | |
| 249 | def test_info(self): |
| 250 | req = urllib.request.Request( |
| 251 | "http://localhost:%s/info" % self.port) |
| 252 | f = urllib.request.urlopen(req) |
| 253 | info = json.loads(f.read().decode('utf8')) |
| 254 | self.assertEqual( |
| 255 | info, { |
| 256 | "info": { |
Monty Taylor | f93c2fb | 2018-02-20 14:38:47 -0600 | [diff] [blame^] | 257 | "rest_api_url": None, |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 258 | "capabilities": { |
| 259 | "job_history": False |
| 260 | }, |
| 261 | "stats": { |
| 262 | "url": self.stats_url, |
| 263 | "prefix": self.stats_prefix, |
| 264 | "type": "graphite", |
| 265 | }, |
| 266 | "websocket_url": self.websocket_url, |
| 267 | } |
| 268 | }) |
| 269 | |
| 270 | def test_tenant_info(self): |
| 271 | req = urllib.request.Request( |
| 272 | "http://localhost:%s/tenant-one/info" % self.port) |
| 273 | f = urllib.request.urlopen(req) |
| 274 | info = json.loads(f.read().decode('utf8')) |
| 275 | self.assertEqual( |
| 276 | info, { |
| 277 | "info": { |
Monty Taylor | f93c2fb | 2018-02-20 14:38:47 -0600 | [diff] [blame^] | 278 | "rest_api_url": None, |
Monty Taylor | 518dcf8 | 2018-01-23 12:51:26 -0600 | [diff] [blame] | 279 | "tenant": "tenant-one", |
| 280 | "capabilities": { |
| 281 | "job_history": False |
| 282 | }, |
| 283 | "stats": { |
| 284 | "url": self.stats_url, |
| 285 | "prefix": self.stats_prefix, |
| 286 | "type": "graphite", |
| 287 | }, |
| 288 | "websocket_url": self.websocket_url, |
| 289 | } |
| 290 | }) |
| 291 | |
| 292 | |
| 293 | class TestWebSocketInfo(TestInfo): |
| 294 | |
| 295 | config_ini_data = { |
| 296 | 'web': { |
| 297 | 'websocket_url': 'wss://ws.example.com' |
| 298 | } |
| 299 | } |
| 300 | |
| 301 | |
| 302 | class TestGraphiteUrl(TestInfo): |
| 303 | |
| 304 | config_ini_data = { |
| 305 | 'statsd': { |
| 306 | 'prefix': 'example' |
| 307 | }, |
| 308 | 'web': { |
| 309 | 'stats_url': 'https://graphite.example.com', |
| 310 | } |
| 311 | } |