Skip to content

Commit df50eee

Browse files
author
Gabriel Monroy
committed
add test coverage for cm synchronization using mock CM module
wip on task reorg
1 parent bc27e99 commit df50eee

9 files changed

Lines changed: 288 additions & 87 deletions

File tree

api/models.py

Lines changed: 66 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from django.conf import settings
1616
from django.contrib.auth.models import User
1717
from django.db import models
18+
from django.db.models.signals import post_save, post_delete
1819
from django.dispatch import receiver
1920
from django.dispatch.dispatcher import Signal
2021
from django.utils.encoding import python_2_unicode_compatible
@@ -435,22 +436,21 @@ def calculate(self):
435436
"""Calculate and update the application databag"""
436437
d = {}
437438
d['id'] = self.id
438-
release = self.release_set.all().order_by('-created')[0]
439439
d['release'] = {}
440-
d['release']['version'] = release.version
441-
d['release']['config'] = release.config.values
442-
d['release']['image'] = release.image
443-
d['release']['build'] = {}
444-
if release.build:
445-
d['release']['build']['url'] = release.build.url
446-
d['release']['build']['procfile'] = release.build.procfile
447-
# add collaborators TODO: add sharing
440+
releases = self.release_set.all().order_by('-created')
441+
if releases:
442+
release = releases[0]
443+
d['release']['version'] = release.version
444+
d['release']['config'] = release.config.values
445+
d['release']['image'] = release.image
446+
d['release']['build'] = {}
447+
if release.build:
448+
d['release']['build']['url'] = release.build.url
449+
d['release']['build']['procfile'] = release.build.procfile
450+
# TODO: add proper sharing and access controls
448451
d['users'] = {}
449452
for u in (self.owner.username,):
450453
d['users'][u] = 'admin'
451-
# # call a celery task to update the data bag
452-
# if settings.CHEF_ENABLED:
453-
# controller.update_application.delay(self.id, d).wait() # @UndefinedVariable
454454
return d
455455

456456

@@ -663,9 +663,8 @@ def push(cls, push):
663663
# recalculate the formation databag including the new
664664
# build and release
665665
databag = formation.calculate()
666-
# if enabled, force-converge all of the chef nodes
667-
if settings.CONVERGE_ON_PUSH is True:
668-
formation.converge(databag)
666+
# force-converge all of the chef nodes
667+
formation.converge(databag)
669668
# return the databag object so the git-receive hook
670669
# can tell the user about proxy URLs, etc.
671670
return databag
@@ -731,5 +730,57 @@ def new_release(sender, **kwargs):
731730
return release
732731

733732

733+
def calculate(self):
734+
"""
735+
Calculate configuration management representation
736+
for this user account
737+
"""
738+
data = {'id': self.username, 'ssh_keys': {}}
739+
for k in self.key_set.all():
740+
data['ssh_keys'][k.id] = k.public
741+
return data
742+
743+
# attach to built-in django user
744+
User.calculate = calculate
745+
746+
# define update/delete callbacks for synchronizing
747+
# models with the configuration management backend
748+
749+
750+
def update_user(sender, **kwargs):
751+
user = kwargs['instance']
752+
tasks.publish_user.delay(user.username, user.calculate()).wait()
753+
754+
755+
def update_key(sender, **kwargs):
756+
user = kwargs['instance'].owner
757+
tasks.publish_user.delay(user.username, user.calculate()).wait()
758+
759+
760+
def update_app(sender, **kwargs):
761+
tasks.publish_app.delay(kwargs['instance'].id).wait()
762+
763+
764+
def delete_app(sender, **kwargs):
765+
tasks.purge_app.delay(kwargs['instance'].id).wait()
766+
767+
768+
def update_formation(sender, **kwargs):
769+
tasks.publish_formation.delay(kwargs['instance'].id).wait()
770+
771+
772+
def delete_formation(sender, **kwargs):
773+
tasks.purge_formation.delay(kwargs['instance'].id).wait()
774+
775+
# use django signals to synchronize database updates with
776+
# the configuration management backend
777+
post_save.connect(update_user, sender=User)
778+
post_save.connect(update_key, sender=Key)
779+
post_delete.connect(update_key, sender=Key)
780+
post_save.connect(update_app, sender=App)
781+
post_delete.connect(delete_app, sender=App)
782+
post_save.connect(update_formation, sender=Formation)
783+
post_delete.connect(delete_formation, sender=Formation)
784+
734785
# import tasks after models are defined
735786
from api import tasks

api/tasks.py

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,13 @@ def converge_node(node_id):
8686
@task
8787
def run_node(app_id, command):
8888
app = App.objects.get(id=app_id)
89-
node = app.node_set.order_by('?')[0]
90-
release = app.release_set.order_by('-created')[0]
89+
nodes = app.formation.node_set.order_by('?')
90+
releases = app.release_set.order_by('-created')
91+
if not nodes:
92+
raise EnvironmentError('No nodes available to run command')
93+
if not releases:
94+
raise EnvironmentError('No release to run command against')
95+
node, release = nodes[0], releases[0]
9196
# prepare ssh command
9297
version = release.version
9398
docker_args = ' '.join(
@@ -125,3 +130,40 @@ def destroy_formation(formation_id):
125130
group([destroy_layer.s(l.id) for l in formation.layer_set.all()]).apply_async().join()
126131
formation.delete()
127132
return formation_id
133+
134+
135+
@task
136+
def converge_controller():
137+
CM.converge_controller()
138+
139+
140+
@task
141+
def publish_user(username, data):
142+
CM.publish_user(username, data)
143+
converge_controller.delay().wait()
144+
return username
145+
146+
147+
@task
148+
def publish_app(app_id):
149+
app = App.objects.get(id=app_id)
150+
data = app.calculate()
151+
CM.publish_app(app_id, data)
152+
converge_controller.delay().wait()
153+
return app_id
154+
155+
156+
@task
157+
def purge_app(app_id):
158+
CM.purge_app(app_id)
159+
converge_controller.delay().wait()
160+
return app_id
161+
162+
163+
@task
164+
def publish_formation(formation_id, delete=False):
165+
if delete is True:
166+
return CM.purge_formation(formation_id)
167+
formation = Formation.objects.get(id=formation_id)
168+
data = formation.calculate()
169+
return CM.publish_formation(formation_id, data)

api/tests/app.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,12 @@ def setUp(self):
3838
self.assertEqual(response.status_code, 201)
3939
# create & scale a basic formation
4040
url = '/api/formations/{formation_id}/layers'.format(**locals())
41-
body = {'id': 'proxy', 'flavor': 'autotest', 'type': 'proxy',
41+
body = {'id': 'proxy', 'flavor': 'autotest', 'proxy': True,
4242
'run_list': 'recipe[deis::proxy]'}
4343
response = self.client.post(url, json.dumps(body), content_type='application/json')
4444
self.assertEqual(response.status_code, 201)
4545
url = '/api/formations/{formation_id}/layers'.format(**locals())
46-
body = {'id': 'runtime', 'flavor': 'autotest', 'type': 'runtime',
46+
body = {'id': 'runtime', 'flavor': 'autotest', 'runtime': True,
4747
'run_list': 'recipe[deis::proxy]'}
4848
response = self.client.post(url, json.dumps(body), content_type='application/json')
4949
self.assertEqual(response.status_code, 201)
@@ -75,6 +75,25 @@ def test_app(self):
7575
response = self.client.delete(url)
7676
self.assertEqual(response.status_code, 204)
7777

78+
def test_app_cm(self):
79+
"""
80+
Test that configuration management is updated on app changes
81+
"""
82+
url = '/api/apps'
83+
body = {'formation': 'autotest'}
84+
response = self.client.post(url, json.dumps(body), content_type='application/json')
85+
self.assertEqual(response.status_code, 201)
86+
app_id = response.data['id'] # noqa
87+
path = os.path.join(settings.TEMPDIR, 'app-{}'.format(app_id))
88+
with open(path) as f:
89+
data = json.loads(f.read())
90+
self.assertIn('id', data)
91+
self.assertEquals(data['id'], app_id)
92+
url = '/api/apps/{app_id}'.format(**locals())
93+
response = self.client.delete(url)
94+
self.assertEqual(response.status_code, 204)
95+
self.assertFalse(os.path.exists(path))
96+
7897
def test_app_override_id(self):
7998
body = {'formation': 'autotest', 'id': 'myid'}
8099
response = self.client.post('/api/apps', json.dumps(body),

api/tests/formation.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import os.path
1011

1112
from django.test import TestCase
1213

14+
from deis import settings
15+
1316

1417
class FormationTest(TestCase):
1518

@@ -53,6 +56,25 @@ def test_formation(self):
5356
response = self.client.delete(url)
5457
self.assertEqual(response.status_code, 204)
5558

59+
def test_formation_cm(self):
60+
"""
61+
Test that configuration management is updated on formation changes
62+
"""
63+
url = '/api/formations'
64+
body = {'id': 'autotest'}
65+
response = self.client.post(url, json.dumps(body), content_type='application/json')
66+
self.assertEqual(response.status_code, 201)
67+
formation_id = response.data['id'] # noqa
68+
path = os.path.join(settings.TEMPDIR, 'formation-{}'.format(formation_id))
69+
with open(path) as f:
70+
data = json.loads(f.read())
71+
self.assertIn('id', data)
72+
self.assertEquals(data['id'], formation_id)
73+
url = '/api/formations/{formation_id}'.format(**locals())
74+
response = self.client.delete(url)
75+
self.assertEqual(response.status_code, 204)
76+
self.assertFalse(os.path.exists(path))
77+
5678
def test_formation_id(self):
5779
body = {'id': 'autotest'}
5880
response = self.client.post('/api/formations', json.dumps(body),

api/tests/key.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,12 @@
77
from __future__ import unicode_literals
88

99
import json
10+
import os.path
1011

1112
from django.test import TestCase
1213

14+
from deis import settings
15+
1316

1417
class KeyTest(TestCase):
1518

@@ -41,6 +44,29 @@ def test_key(self):
4144
response = self.client.delete(url)
4245
self.assertEqual(response.status_code, 204)
4346

47+
def test_key_cm(self):
48+
"""
49+
Test that creating and deleting a key updates configuration management
50+
"""
51+
url = '/api/keys'
52+
body = {'id': 'mykey@box.local', 'public': 'ssh-rsa XXX'}
53+
response = self.client.post(url, json.dumps(body), content_type='application/json')
54+
self.assertEqual(response.status_code, 201)
55+
key_id = response.data['id']
56+
path = os.path.join(settings.TEMPDIR, 'user-autotest')
57+
with open(path) as f:
58+
data = json.loads(f.read())
59+
self.assertIn('id', data)
60+
self.assertEquals(data['id'], 'autotest')
61+
self.assertIn(body['id'], data['ssh_keys'])
62+
self.assertEqual(body['public'], data['ssh_keys'][body['id']])
63+
url = '/api/keys/{key_id}'.format(**locals())
64+
response = self.client.delete(url)
65+
self.assertEqual(response.status_code, 204)
66+
with open(path) as f:
67+
data = json.loads(f.read())
68+
self.assertNotIn(body['id'], data['ssh_keys'])
69+
4470
def test_key_duplicate(self):
4571
"""
4672
Test that a user cannot add a duplicate key

0 commit comments

Comments
 (0)