github.com/chasestarr/deis@v1.13.5-0.20170519182049-1d9e59fbdbfc/controller/api/tests/test_config.py (about) 1 # -*- coding: utf-8 -*- 2 """ 3 Unit tests for the Deis api app. 4 5 Run the tests with "./manage.py test api" 6 """ 7 8 from __future__ import unicode_literals 9 10 import json 11 import logging 12 import os.path 13 import requests 14 import tempfile 15 16 from django.contrib.auth.models import User 17 from django.test import TransactionTestCase 18 import etcd 19 import mock 20 from rest_framework.authtoken.models import Token 21 from simpleflock import SimpleFlock 22 23 import api.exceptions 24 from api.models import App, Config 25 from . import mock_status_ok 26 27 28 def mock_status_not_found(*args, **kwargs): 29 resp = requests.Response() 30 resp.status_code = 404 31 resp._content_consumed = True 32 return resp 33 34 35 def mock_request_connection_error(*args, **kwargs): 36 raise requests.exceptions.ConnectionError("connection error") 37 38 39 class MockEtcdClient: 40 41 def __init__(self, app): 42 self.app = app 43 44 def get(self, key, *args, **kwargs): 45 node = { 46 'key': '/deis/services/{}/{}_v2.web.1'.format(self.app, self.app), 47 'value': '127.0.0.1:1234' 48 } 49 return etcd.EtcdResult(None, node) 50 51 52 @mock.patch('api.models.publish_release', lambda *args: None) 53 class ConfigTest(TransactionTestCase): 54 55 """Tests setting and updating config values""" 56 57 fixtures = ['tests.json'] 58 59 def setUp(self): 60 self.user = User.objects.get(username='autotest') 61 self.token = Token.objects.get(user=self.user).key 62 url = '/v1/apps' 63 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 64 self.assertEqual(response.status_code, 201) 65 self.app = App.objects.all()[0] 66 67 @mock.patch('requests.post', mock_status_ok) 68 def test_config(self): 69 """ 70 Test that config is auto-created for a new app and that 71 config can be updated using a PATCH 72 """ 73 url = '/v1/apps' 74 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 75 self.assertEqual(response.status_code, 201) 76 app_id = response.data['id'] 77 # check to see that an initial/empty config was created 78 url = "/v1/apps/{app_id}/config".format(**locals()) 79 response = self.client.get(url, 80 HTTP_AUTHORIZATION='token {}'.format(self.token)) 81 self.assertEqual(response.status_code, 200) 82 self.assertIn('values', response.data) 83 self.assertEqual(response.data['values'], {}) 84 config1 = response.data 85 # set an initial config value 86 body = {'values': json.dumps({'NEW_URL1': 'http://localhost:8080/'})} 87 response = self.client.post(url, json.dumps(body), content_type='application/json', 88 HTTP_AUTHORIZATION='token {}'.format(self.token)) 89 self.assertEqual(response.status_code, 201) 90 config2 = response.data 91 self.assertNotEqual(config1['uuid'], config2['uuid']) 92 self.assertIn('NEW_URL1', response.data['values']) 93 # read the config 94 response = self.client.get(url, 95 HTTP_AUTHORIZATION='token {}'.format(self.token)) 96 self.assertEqual(response.status_code, 200) 97 config3 = response.data 98 self.assertEqual(config2, config3) 99 self.assertIn('NEW_URL1', response.data['values']) 100 # set an additional config value 101 body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})} 102 response = self.client.post(url, json.dumps(body), content_type='application/json', 103 HTTP_AUTHORIZATION='token {}'.format(self.token)) 104 self.assertEqual(response.status_code, 201) 105 config3 = response.data 106 self.assertNotEqual(config2['uuid'], config3['uuid']) 107 self.assertIn('NEW_URL1', response.data['values']) 108 self.assertIn('NEW_URL2', response.data['values']) 109 # read the config again 110 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 111 self.assertEqual(response.status_code, 200) 112 config4 = response.data 113 self.assertEqual(config3, config4) 114 self.assertIn('NEW_URL1', response.data['values']) 115 self.assertIn('NEW_URL2', response.data['values']) 116 # unset a config value 117 body = {'values': json.dumps({'NEW_URL2': None})} 118 response = self.client.post(url, json.dumps(body), content_type='application/json', 119 HTTP_AUTHORIZATION='token {}'.format(self.token)) 120 self.assertEqual(response.status_code, 201) 121 config5 = response.data 122 self.assertNotEqual(config4['uuid'], config5['uuid']) 123 self.assertNotIn('NEW_URL2', json.dumps(response.data['values'])) 124 # unset all config values 125 body = {'values': json.dumps({'NEW_URL1': None})} 126 response = self.client.post(url, json.dumps(body), content_type='application/json', 127 HTTP_AUTHORIZATION='token {}'.format(self.token)) 128 self.assertEqual(response.status_code, 201) 129 self.assertNotIn('NEW_URL1', json.dumps(response.data['values'])) 130 # disallow put/patch/delete 131 response = self.client.put(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 132 self.assertEqual(response.status_code, 405) 133 response = self.client.patch(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 134 self.assertEqual(response.status_code, 405) 135 response = self.client.delete(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 136 self.assertEqual(response.status_code, 405) 137 return config5 138 139 @mock.patch('requests.post', mock_status_ok) 140 def test_overlapping_config(self): 141 """ 142 Test that config won't be created if a similar operation 143 is in progress for that app. 144 """ 145 url = '/v1/apps' 146 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 147 self.assertEqual(response.status_code, 201) 148 app_id = response.data['id'] 149 # check to see that an initial/empty config was created 150 url = "/v1/apps/{app_id}/config".format(**locals()) 151 response = self.client.get(url, 152 HTTP_AUTHORIZATION='token {}'.format(self.token)) 153 self.assertEqual(response.status_code, 200) 154 self.assertIn('values', response.data) 155 self.assertEqual(response.data['values'], {}) 156 config1 = response.data 157 # create the lockfile as though a "deis config:set" were in progress 158 lockfile = os.path.join(tempfile.gettempdir(), app_id + "-config") 159 with SimpleFlock(lockfile): 160 # set an initial config value 161 body = {'values': json.dumps({'NEW_URL1': 'http://localhost:8080/'})} 162 response = self.client.post(url, json.dumps(body), content_type='application/json', 163 HTTP_AUTHORIZATION='token {}'.format(self.token)) 164 self.assertEqual(response.status_code, 409) 165 self.assertNotIn('values', response.data) 166 self.assertNotIn('uuid', response.data) 167 # read the config 168 response = self.client.get(url, 169 HTTP_AUTHORIZATION='token {}'.format(self.token)) 170 self.assertEqual(response.status_code, 200) 171 config2 = response.data 172 self.assertEqual(config1, config2) 173 self.assertNotIn('NEW_URL1', response.data['values']) 174 175 @mock.patch('requests.post', mock_status_ok) 176 def test_response_data(self): 177 """Test that the serialized response contains only relevant data.""" 178 body = {'id': 'test'} 179 response = self.client.post('/v1/apps', json.dumps(body), 180 content_type='application/json', 181 HTTP_AUTHORIZATION='token {}'.format(self.token)) 182 url = "/v1/apps/test/config" 183 # set an initial config value 184 body = {'values': json.dumps({'PORT': '5000'})} 185 response = self.client.post(url, json.dumps(body), content_type='application/json', 186 HTTP_AUTHORIZATION='token {}'.format(self.token)) 187 for key in response.data: 188 self.assertIn(key, ['uuid', 'owner', 'created', 'updated', 'app', 'values', 'memory', 189 'cpu', 'tags']) 190 expected = { 191 'owner': self.user.username, 192 'app': 'test', 193 'values': {'PORT': '5000'}, 194 'memory': {}, 195 'cpu': {}, 196 'tags': {} 197 } 198 self.assertDictContainsSubset(expected, response.data) 199 200 @mock.patch('requests.post', mock_status_ok) 201 def test_response_data_types_converted(self): 202 """Test that config data is converted into the correct type.""" 203 body = {'id': 'test'} 204 response = self.client.post('/v1/apps', json.dumps(body), 205 content_type='application/json', 206 HTTP_AUTHORIZATION='token {}'.format(self.token)) 207 url = "/v1/apps/test/config" 208 209 body = {'values': json.dumps({'PORT': 5000}), 'cpu': json.dumps({'web': '1024'})} 210 response = self.client.post(url, json.dumps(body), content_type='application/json', 211 HTTP_AUTHORIZATION='token {}'.format(self.token)) 212 self.assertEqual(response.status_code, 201) 213 for key in response.data: 214 self.assertIn(key, ['uuid', 'owner', 'created', 'updated', 'app', 'values', 'memory', 215 'cpu', 'tags']) 216 expected = { 217 'owner': self.user.username, 218 'app': 'test', 219 'values': {'PORT': '5000'}, 220 'memory': {}, 221 'cpu': {'web': 1024}, 222 'tags': {} 223 } 224 self.assertDictContainsSubset(expected, response.data) 225 226 body = {'cpu': json.dumps({'web': 'this will fail'})} 227 response = self.client.post(url, json.dumps(body), content_type='application/json', 228 HTTP_AUTHORIZATION='token {}'.format(self.token)) 229 self.assertEqual(response.status_code, 400) 230 self.assertIn('CPU shares must be an integer', response.data['cpu']) 231 232 @mock.patch('requests.post', mock_status_ok) 233 def test_config_set_same_key(self): 234 """ 235 Test that config sets on the same key function properly 236 """ 237 url = '/v1/apps' 238 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 239 self.assertEqual(response.status_code, 201) 240 app_id = response.data['id'] 241 url = "/v1/apps/{app_id}/config".format(**locals()) 242 # set an initial config value 243 body = {'values': json.dumps({'PORT': '5000'})} 244 response = self.client.post(url, json.dumps(body), content_type='application/json', 245 HTTP_AUTHORIZATION='token {}'.format(self.token)) 246 self.assertEqual(response.status_code, 201) 247 self.assertIn('PORT', response.data['values']) 248 # reset same config value 249 body = {'values': json.dumps({'PORT': '5001'})} 250 response = self.client.post(url, json.dumps(body), content_type='application/json', 251 HTTP_AUTHORIZATION='token {}'.format(self.token)) 252 self.assertEqual(response.status_code, 201) 253 self.assertIn('PORT', response.data['values']) 254 self.assertEqual(response.data['values']['PORT'], '5001') 255 256 @mock.patch('requests.post', mock_status_ok) 257 def test_config_set_unicode(self): 258 """ 259 Test that config sets with unicode values are accepted. 260 """ 261 url = '/v1/apps' 262 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 263 self.assertEqual(response.status_code, 201) 264 app_id = response.data['id'] 265 url = "/v1/apps/{app_id}/config".format(**locals()) 266 # set an initial config value 267 body = {'values': json.dumps({'POWERED_BY': 'Деис'})} 268 response = self.client.post(url, json.dumps(body), content_type='application/json', 269 HTTP_AUTHORIZATION='token {}'.format(self.token)) 270 self.assertEqual(response.status_code, 201) 271 self.assertIn('POWERED_BY', response.data['values']) 272 # reset same config value 273 body = {'values': json.dumps({'POWERED_BY': 'Кроликов'})} 274 response = self.client.post(url, json.dumps(body), content_type='application/json', 275 HTTP_AUTHORIZATION='token {}'.format(self.token)) 276 self.assertEqual(response.status_code, 201) 277 self.assertIn('POWERED_BY', response.data['values']) 278 self.assertEqual(response.data['values']['POWERED_BY'], 'Кроликов') 279 # set an integer to test unicode regression 280 body = {'values': json.dumps({'INTEGER': 1})} 281 response = self.client.post(url, json.dumps(body), content_type='application/json', 282 HTTP_AUTHORIZATION='token {}'.format(self.token)) 283 self.assertEqual(response.status_code, 201) 284 self.assertIn('INTEGER', response.data['values']) 285 self.assertEqual(response.data['values']['INTEGER'], '1') 286 287 @mock.patch('requests.post', mock_status_ok) 288 def test_config_str(self): 289 """Test the text representation of a node.""" 290 config5 = self.test_config() 291 config = Config.objects.get(uuid=config5['uuid']) 292 self.assertEqual(str(config), "{}-{}".format(config5['app'], config5['uuid'][:7])) 293 294 @mock.patch('requests.post', mock_status_ok) 295 def test_valid_config_keys(self): 296 """Test that valid config keys are accepted. 297 """ 298 keys = ("FOO", "_foo", "f001", "FOO_BAR_BAZ_") 299 url = '/v1/apps' 300 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 301 self.assertEqual(response.status_code, 201) 302 app_id = response.data['id'] 303 url = '/v1/apps/{app_id}/config'.format(**locals()) 304 for k in keys: 305 body = {'values': json.dumps({k: "testvalue"})} 306 resp = self.client.post( 307 url, json.dumps(body), content_type='application/json', 308 HTTP_AUTHORIZATION='token {}'.format(self.token)) 309 self.assertEqual(resp.status_code, 201) 310 self.assertIn(k, resp.data['values']) 311 312 @mock.patch('requests.post', mock_status_ok) 313 def test_invalid_config_keys(self): 314 """Test that invalid config keys are rejected. 315 """ 316 keys = ("123", "../../foo", "FOO/", "FOO-BAR") 317 url = '/v1/apps' 318 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 319 self.assertEqual(response.status_code, 201) 320 app_id = response.data['id'] 321 url = '/v1/apps/{app_id}/config'.format(**locals()) 322 for k in keys: 323 body = {'values': json.dumps({k: "testvalue"})} 324 resp = self.client.post( 325 url, json.dumps(body), content_type='application/json', 326 HTTP_AUTHORIZATION='token {}'.format(self.token)) 327 self.assertEqual(resp.status_code, 400) 328 329 @mock.patch('requests.post', mock_status_ok) 330 def test_admin_can_create_config_on_other_apps(self): 331 """If a non-admin creates an app, an administrator should be able to set config 332 values for that app. 333 """ 334 user = User.objects.get(username='autotest2') 335 token = Token.objects.get(user=user).key 336 url = '/v1/apps' 337 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(token)) 338 self.assertEqual(response.status_code, 201) 339 app_id = response.data['id'] 340 url = "/v1/apps/{app_id}/config".format(**locals()) 341 # set an initial config value 342 body = {'values': json.dumps({'PORT': '5000'})} 343 response = self.client.post(url, json.dumps(body), content_type='application/json', 344 HTTP_AUTHORIZATION='token {}'.format(self.token)) 345 self.assertEqual(response.status_code, 201) 346 self.assertIn('PORT', response.data['values']) 347 return response 348 349 @mock.patch('requests.post', mock_status_ok) 350 def test_limit_memory(self): 351 """ 352 Test that limit is auto-created for a new app and that 353 limits can be updated using a PATCH 354 """ 355 url = '/v1/apps' 356 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 357 self.assertEqual(response.status_code, 201) 358 app_id = response.data['id'] 359 url = '/v1/apps/{app_id}/config'.format(**locals()) 360 # check default limit 361 response = self.client.get(url, content_type='application/json', 362 HTTP_AUTHORIZATION='token {}'.format(self.token)) 363 self.assertEqual(response.status_code, 200) 364 self.assertIn('memory', response.data) 365 self.assertEqual(response.data['memory'], {}) 366 # regression test for https://github.com/deis/deis/issues/1563 367 self.assertNotIn('"', response.data['memory']) 368 # set an initial limit 369 mem = {'web': '1G'} 370 body = {'memory': json.dumps(mem)} 371 response = self.client.post(url, json.dumps(body), content_type='application/json', 372 HTTP_AUTHORIZATION='token {}'.format(self.token)) 373 self.assertEqual(response.status_code, 201) 374 limit1 = response.data 375 # check memory limits 376 response = self.client.get(url, content_type='application/json', 377 HTTP_AUTHORIZATION='token {}'.format(self.token)) 378 self.assertEqual(response.status_code, 200) 379 self.assertIn('memory', response.data) 380 memory = response.data['memory'] 381 self.assertIn('web', memory) 382 self.assertEqual(memory['web'], '1G') 383 # set an additional value 384 body = {'memory': json.dumps({'worker': '512M'})} 385 response = self.client.post(url, json.dumps(body), content_type='application/json', 386 HTTP_AUTHORIZATION='token {}'.format(self.token)) 387 self.assertEqual(response.status_code, 201) 388 limit2 = response.data 389 self.assertNotEqual(limit1['uuid'], limit2['uuid']) 390 memory = response.data['memory'] 391 self.assertIn('worker', memory) 392 self.assertEqual(memory['worker'], '512M') 393 self.assertIn('web', memory) 394 self.assertEqual(memory['web'], '1G') 395 # read the limit again 396 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 397 self.assertEqual(response.status_code, 200) 398 limit3 = response.data 399 self.assertEqual(limit2, limit3) 400 memory = response.data['memory'] 401 self.assertIn('worker', memory) 402 self.assertEqual(memory['worker'], '512M') 403 self.assertIn('web', memory) 404 self.assertEqual(memory['web'], '1G') 405 # regression test for https://github.com/deis/deis/issues/1613 406 # ensure that config:set doesn't wipe out previous limits 407 body = {'values': json.dumps({'NEW_URL2': 'http://localhost:8080/'})} 408 response = self.client.post(url, json.dumps(body), content_type='application/json', 409 HTTP_AUTHORIZATION='token {}'.format(self.token)) 410 self.assertEqual(response.status_code, 201) 411 self.assertIn('NEW_URL2', response.data['values']) 412 # read the limit again 413 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 414 self.assertEqual(response.status_code, 200) 415 memory = response.data['memory'] 416 self.assertIn('worker', memory) 417 self.assertEqual(memory['worker'], '512M') 418 self.assertIn('web', memory) 419 self.assertEqual(memory['web'], '1G') 420 # unset a value 421 body = {'memory': json.dumps({'worker': None})} 422 response = self.client.post(url, json.dumps(body), content_type='application/json', 423 HTTP_AUTHORIZATION='token {}'.format(self.token)) 424 self.assertEqual(response.status_code, 201) 425 limit4 = response.data 426 self.assertNotEqual(limit3['uuid'], limit4['uuid']) 427 self.assertNotIn('worker', json.dumps(response.data['memory'])) 428 # disallow put/patch/delete 429 response = self.client.put(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 430 self.assertEqual(response.status_code, 405) 431 response = self.client.patch(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 432 self.assertEqual(response.status_code, 405) 433 response = self.client.delete(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 434 self.assertEqual(response.status_code, 405) 435 return limit4 436 437 @mock.patch('requests.post', mock_status_ok) 438 def test_limit_cpu(self): 439 """ 440 Test that CPU limits can be set 441 """ 442 url = '/v1/apps' 443 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 444 self.assertEqual(response.status_code, 201) 445 app_id = response.data['id'] 446 url = '/v1/apps/{app_id}/config'.format(**locals()) 447 # check default limit 448 response = self.client.get(url, content_type='application/json', 449 HTTP_AUTHORIZATION='token {}'.format(self.token)) 450 self.assertEqual(response.status_code, 200) 451 self.assertIn('cpu', response.data) 452 self.assertEqual(response.data['cpu'], {}) 453 # regression test for https://github.com/deis/deis/issues/1563 454 self.assertNotIn('"', response.data['cpu']) 455 # set an initial limit 456 body = {'cpu': json.dumps({'web': '1024'})} 457 response = self.client.post(url, json.dumps(body), content_type='application/json', 458 HTTP_AUTHORIZATION='token {}'.format(self.token)) 459 self.assertEqual(response.status_code, 201) 460 limit1 = response.data 461 # check memory limits 462 response = self.client.get(url, content_type='application/json', 463 HTTP_AUTHORIZATION='token {}'.format(self.token)) 464 self.assertEqual(response.status_code, 200) 465 self.assertIn('cpu', response.data) 466 cpu = response.data['cpu'] 467 self.assertIn('web', cpu) 468 self.assertEqual(cpu['web'], 1024) 469 # set an additional value 470 body = {'cpu': json.dumps({'worker': '512'})} 471 response = self.client.post(url, json.dumps(body), content_type='application/json', 472 HTTP_AUTHORIZATION='token {}'.format(self.token)) 473 self.assertEqual(response.status_code, 201) 474 limit2 = response.data 475 self.assertNotEqual(limit1['uuid'], limit2['uuid']) 476 cpu = response.data['cpu'] 477 self.assertIn('worker', cpu) 478 self.assertEqual(cpu['worker'], 512) 479 self.assertIn('web', cpu) 480 self.assertEqual(cpu['web'], 1024) 481 # read the limit again 482 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 483 self.assertEqual(response.status_code, 200) 484 limit3 = response.data 485 self.assertEqual(limit2, limit3) 486 cpu = response.data['cpu'] 487 self.assertIn('worker', cpu) 488 self.assertEqual(cpu['worker'], 512) 489 self.assertIn('web', cpu) 490 self.assertEqual(cpu['web'], 1024) 491 # unset a value 492 body = {'memory': json.dumps({'worker': None})} 493 response = self.client.post(url, json.dumps(body), content_type='application/json', 494 HTTP_AUTHORIZATION='token {}'.format(self.token)) 495 self.assertEqual(response.status_code, 201) 496 limit4 = response.data 497 self.assertNotEqual(limit3['uuid'], limit4['uuid']) 498 self.assertNotIn('worker', json.dumps(response.data['memory'])) 499 # disallow put/patch/delete 500 response = self.client.put(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 501 self.assertEqual(response.status_code, 405) 502 response = self.client.patch(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 503 self.assertEqual(response.status_code, 405) 504 response = self.client.delete(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 505 self.assertEqual(response.status_code, 405) 506 return limit4 507 508 @mock.patch('requests.post', mock_status_ok) 509 def test_tags(self): 510 """ 511 Test that tags can be set on an application 512 """ 513 url = '/v1/apps' 514 response = self.client.post(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 515 self.assertEqual(response.status_code, 201) 516 app_id = response.data['id'] 517 url = '/v1/apps/{app_id}/config'.format(**locals()) 518 # check default 519 response = self.client.get(url, content_type='application/json', 520 HTTP_AUTHORIZATION='token {}'.format(self.token)) 521 self.assertEqual(response.status_code, 200) 522 self.assertIn('tags', response.data) 523 self.assertEqual(response.data['tags'], {}) 524 # set some tags 525 body = {'tags': json.dumps({'environ': 'dev'})} 526 response = self.client.post(url, json.dumps(body), content_type='application/json', 527 HTTP_AUTHORIZATION='token {}'.format(self.token)) 528 self.assertEqual(response.status_code, 201) 529 tags1 = response.data 530 # check tags again 531 response = self.client.get(url, content_type='application/json', 532 HTTP_AUTHORIZATION='token {}'.format(self.token)) 533 self.assertEqual(response.status_code, 200) 534 self.assertIn('tags', response.data) 535 tags = response.data['tags'] 536 self.assertIn('environ', tags) 537 self.assertEqual(tags['environ'], 'dev') 538 # set an additional value 539 body = {'tags': json.dumps({'rack': '1'})} 540 response = self.client.post(url, json.dumps(body), content_type='application/json', 541 HTTP_AUTHORIZATION='token {}'.format(self.token)) 542 self.assertEqual(response.status_code, 201) 543 tags2 = response.data 544 self.assertNotEqual(tags1['uuid'], tags2['uuid']) 545 tags = response.data['tags'] 546 self.assertIn('rack', tags) 547 self.assertEqual(tags['rack'], '1') 548 self.assertIn('environ', tags) 549 self.assertEqual(tags['environ'], 'dev') 550 # read the limit again 551 response = self.client.get(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 552 self.assertEqual(response.status_code, 200) 553 tags3 = response.data 554 self.assertEqual(tags2, tags3) 555 tags = response.data['tags'] 556 self.assertIn('rack', tags) 557 self.assertEqual(tags['rack'], '1') 558 self.assertIn('environ', tags) 559 self.assertEqual(tags['environ'], 'dev') 560 # unset a value 561 body = {'tags': json.dumps({'rack': None})} 562 response = self.client.post(url, json.dumps(body), content_type='application/json', 563 HTTP_AUTHORIZATION='token {}'.format(self.token)) 564 self.assertEqual(response.status_code, 201) 565 tags4 = response.data 566 self.assertNotEqual(tags3['uuid'], tags4['uuid']) 567 self.assertNotIn('rack', json.dumps(response.data['tags'])) 568 # set invalid values 569 body = {'tags': json.dumps({'valid': 'in\nvalid'})} 570 response = self.client.post(url, json.dumps(body), content_type='application/json', 571 HTTP_AUTHORIZATION='token {}'.format(self.token)) 572 self.assertEqual(response.status_code, 400) 573 body = {'tags': json.dumps({'in.valid': 'valid'})} 574 response = self.client.post(url, json.dumps(body), content_type='application/json', 575 HTTP_AUTHORIZATION='token {}'.format(self.token)) 576 self.assertEqual(response.status_code, 400) 577 # disallow put/patch/delete 578 response = self.client.put(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 579 self.assertEqual(response.status_code, 405) 580 response = self.client.patch(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 581 self.assertEqual(response.status_code, 405) 582 response = self.client.delete(url, HTTP_AUTHORIZATION='token {}'.format(self.token)) 583 self.assertEqual(response.status_code, 405) 584 585 def test_config_owner_is_requesting_user(self): 586 """ 587 Ensure that setting the config value is owned by the requesting user 588 See https://github.com/deis/deis/issues/2650 589 """ 590 response = self.test_admin_can_create_config_on_other_apps() 591 self.assertEqual(response.data['owner'], self.user.username) 592 593 def test_unauthorized_user_cannot_modify_config(self): 594 """ 595 An unauthorized user should not be able to modify other config. 596 597 Since an unauthorized user can't access the application, these 598 requests should return a 403. 599 """ 600 app_id = 'autotest' 601 base_url = '/v1/apps' 602 body = {'id': app_id} 603 response = self.client.post(base_url, json.dumps(body), content_type='application/json', 604 HTTP_AUTHORIZATION='token {}'.format(self.token)) 605 unauthorized_user = User.objects.get(username='autotest2') 606 unauthorized_token = Token.objects.get(user=unauthorized_user).key 607 url = '{}/{}/config'.format(base_url, app_id) 608 body = {'values': {'FOO': 'bar'}} 609 response = self.client.post(url, json.dumps(body), content_type='application/json', 610 HTTP_AUTHORIZATION='token {}'.format(unauthorized_token)) 611 self.assertEqual(response.status_code, 403) 612 613 def _test_app_healthcheck(self): 614 # post a new build, expecting it to pass as usual 615 url = "/v1/apps/{self.app}/builds".format(**locals()) 616 body = {'image': 'autotest/example'} 617 response = self.client.post(url, json.dumps(body), content_type='application/json', 618 HTTP_AUTHORIZATION='token {}'.format(self.token)) 619 self.assertEqual(response.status_code, 201) 620 # mock out the etcd client 621 api.models._etcd_client = MockEtcdClient(self.app) 622 # set an initial healthcheck url. 623 url = "/v1/apps/{self.app}/config".format(**locals()) 624 body = {'values': json.dumps({'HEALTHCHECK_URL': '/'})} 625 return self.client.post(url, json.dumps(body), content_type='application/json', 626 HTTP_AUTHORIZATION='token {}'.format(self.token)) 627 628 @mock.patch('requests.get', mock_status_ok) 629 @mock.patch('time.sleep', lambda func: func) 630 def test_app_healthcheck_good(self): 631 """ 632 If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller 633 should check that the application responds with a 200 OK. 634 """ 635 response = self._test_app_healthcheck() 636 self.assertEqual(response.status_code, 201) 637 self.assertEqual(self.app.release_set.latest().version, 3) 638 639 @mock.patch('requests.get', mock_status_not_found) 640 @mock.patch('api.models.get_etcd_client', lambda func: func) 641 @mock.patch('time.sleep', lambda func: func) 642 @mock.patch('api.models.logger') 643 def test_app_healthcheck_bad(self, mock_logger): 644 """ 645 If a user deploys an app with a config value set for HEALTHCHECK_URL, the controller 646 should check that the application responds with a 200 OK. If it's down, the app should be 647 rolled back. 648 """ 649 response = self._test_app_healthcheck() 650 self.assertEqual(response.status_code, 503) 651 self.assertEqual( 652 response.data, 653 {'detail': 'aborting, app containers failed to respond to health check'}) 654 # check that only the build and initial release exist 655 self.assertEqual(self.app.release_set.latest().version, 2) 656 # assert that the reason why the containers failed was because 657 # they failed the health check 4 times; we do this by looking 658 # at logs-- there may be a better way 659 exp_msg = "{}: app failed health check (got '404', expected: '200'); trying again in 0.0 \ 660 seconds".format(self.app.id) 661 exp_log_call = mock.call(logging.WARNING, exp_msg) 662 log_calls = mock_logger.log.mock_calls 663 self.assertEqual(log_calls.count(exp_log_call), 3) 664 exp_msg = "{}: app failed health check (got '404', expected: '200')".format(self.app.id) 665 exp_log_call = mock.call(logging.WARNING, exp_msg) 666 self.assertEqual(log_calls.count(exp_log_call), 1) 667 668 @mock.patch('requests.get', mock_status_not_found) 669 @mock.patch('api.models.get_etcd_client', lambda func: func) 670 @mock.patch('time.sleep') 671 def test_app_backoff_interval(self, mock_time): 672 """ 673 Ensure that when a healthcheck fails, a backoff strategy is used before trying again. 674 """ 675 # post a new build, expecting it to pass as usual 676 url = "/v1/apps/{self.app}/builds".format(**locals()) 677 body = {'image': 'autotest/example'} 678 response = self.client.post(url, json.dumps(body), content_type='application/json', 679 HTTP_AUTHORIZATION='token {}'.format(self.token)) 680 self.assertEqual(response.status_code, 201) 681 # mock out the etcd client 682 api.models._etcd_client = MockEtcdClient(self.app) 683 # set an initial healthcheck url. 684 url = "/v1/apps/{self.app}/config".format(**locals()) 685 body = {'values': json.dumps({'HEALTHCHECK_URL': '/'})} 686 return self.client.post(url, json.dumps(body), content_type='application/json', 687 HTTP_AUTHORIZATION='token {}'.format(self.token)) 688 self.assertEqual(mock_time.call_count, 5) 689 690 @mock.patch('requests.get', mock_status_ok) 691 @mock.patch('time.sleep') 692 def test_app_healthcheck_initial_delay(self, mock_time): 693 """ 694 Ensure that when an initial delay is set, the request will sleep for x seconds, where 695 x is the number of seconds in the initial timeout. 696 """ 697 # post a new build, expecting it to pass as usual 698 url = "/v1/apps/{self.app}/builds".format(**locals()) 699 body = {'image': 'autotest/example'} 700 response = self.client.post(url, json.dumps(body), content_type='application/json', 701 HTTP_AUTHORIZATION='token {}'.format(self.token)) 702 self.assertEqual(response.status_code, 201) 703 # mock out the etcd client 704 api.models._etcd_client = MockEtcdClient(self.app) 705 # set an initial healthcheck url. 706 url = "/v1/apps/{self.app}/config".format(**locals()) 707 body = {'values': json.dumps({'HEALTHCHECK_URL': '/'})} 708 return self.client.post(url, json.dumps(body), content_type='application/json', 709 HTTP_AUTHORIZATION='token {}'.format(self.token)) 710 # mock_time increments by one each time its called, so we should expect 2 calls to 711 # mock_time; one for the call in the code, and one for this invocation. 712 mock_time.assert_called_with(0) 713 app = App.objects.all()[0] 714 url = "/v1/apps/{app}/config".format(**locals()) 715 body = {'values': json.dumps({'HEALTHCHECK_INITIAL_DELAY': 10})} 716 self.client.post(url, json.dumps(body), content_type='application/json', 717 HTTP_AUTHORIZATION='token {}'.format(self.token)) 718 mock_time.assert_called_with(10) 719 720 @mock.patch('requests.get') 721 @mock.patch('time.sleep', lambda func: func) 722 def test_app_healthcheck_timeout(self, mock_request): 723 """ 724 Ensure when a timeout value is set, the controller respects that value 725 when making a request. 726 """ 727 self._test_app_healthcheck() 728 app = App.objects.all()[0] 729 url = "/v1/apps/{app}/config".format(**locals()) 730 body = {'values': json.dumps({'HEALTHCHECK_TIMEOUT': 10})} 731 self.client.post(url, json.dumps(body), content_type='application/json', 732 HTTP_AUTHORIZATION='token {}'.format(self.token)) 733 mock_request.assert_called_with('http://127.0.0.1:1234/', timeout=10) 734 735 @mock.patch('requests.get', mock_request_connection_error) 736 @mock.patch('time.sleep', lambda func: func) 737 def test_app_healthcheck_connection_error(self): 738 """ 739 If a user deploys an app with a config value set for HEALTHCHECK_URL but the app 740 returns a connection error, the controller should continue checking until either the app 741 responds or the app fails to respond within the timeout. 742 743 NOTE (bacongobbler): the Docker userland proxy listens for connections and returns a 744 ConnectionError, hence the unit test. 745 """ 746 response = self._test_app_healthcheck() 747 self.assertEqual(response.status_code, 503) 748 self.assertEqual( 749 response.data, 750 {'detail': 'aborting, app containers failed to respond to health check'})