Skip to content

Commit 3e988ea

Browse files
committed
feat(model): Add new model to store settings
1 parent 98a4798 commit 3e988ea

9 files changed

Lines changed: 213 additions & 3 deletions

File tree

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9.8 on 2016-08-12 19:05
3+
from __future__ import unicode_literals
4+
5+
from django.conf import settings
6+
from django.db import migrations, models
7+
import django.db.models.deletion
8+
import uuid
9+
10+
11+
class Migration(migrations.Migration):
12+
13+
dependencies = [
14+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
15+
('api', '0012_auto_20160810_1603'),
16+
]
17+
18+
operations = [
19+
migrations.CreateModel(
20+
name='AppSettings',
21+
fields=[
22+
('uuid', models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID')),
23+
('created', models.DateTimeField(auto_now_add=True)),
24+
('updated', models.DateTimeField(auto_now=True)),
25+
('maintenance', models.BooleanField(default=False)),
26+
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')),
27+
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
28+
],
29+
options={
30+
'get_latest_by': 'created',
31+
'ordering': ['-created'],
32+
},
33+
),
34+
migrations.AlterUniqueTogether(
35+
name='appsettings',
36+
unique_together=set([('app', 'uuid')]),
37+
),
38+
]

rootfs/api/models/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ class Meta:
117117
from .release import Release # noqa
118118
from .config import Config # noqa
119119
from .build import Build # noqa
120+
from .appsettings import AppSettings # noqa
120121

121122
# define update/delete callbacks for synchronizing
122123
# models with the configuration management backend
@@ -142,6 +143,12 @@ def _log_config_updated(**kwargs):
142143
config.app.log("config {} updated".format(config))
143144

144145

146+
def _log_app_settings_updated(**kwargs):
147+
appSettings = kwargs['instance']
148+
# log only to the controller; this event will be logged in the release summary
149+
appSettings.app.log("application settings {} updated".format(appSettings))
150+
151+
145152
def _log_domain_added(**kwargs):
146153
if kwargs.get('created'):
147154
domain = kwargs['instance']
@@ -170,6 +177,7 @@ def _log_cert_removed(**kwargs):
170177
post_save.connect(_log_config_updated, sender=Config, dispatch_uid='api.models.log')
171178
post_save.connect(_log_domain_added, sender=Domain, dispatch_uid='api.models.log')
172179
post_save.connect(_log_cert_added, sender=Certificate, dispatch_uid='api.models.log')
180+
post_save.connect(_log_app_settings_updated, sender=AppSettings, dispatch_uid='api.models.log')
173181
post_delete.connect(_log_domain_removed, sender=Domain, dispatch_uid='api.models.log')
174182
post_delete.connect(_log_cert_removed, sender=Certificate, dispatch_uid='api.models.log')
175183

rootfs/api/models/app.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
from api.models.release import Release
2424
from api.models.config import Config
2525
from api.models.domain import Domain
26+
from api.models.appsettings import AppSettings
2627

2728
from scheduler import KubeHTTPException, KubeException
2829

@@ -178,7 +179,7 @@ def log(self, message, level=logging.INFO):
178179

179180
def create(self, *args, **kwargs): # noqa
180181
"""
181-
Create a application with an initial config, release, domain
182+
Create a application with an initial config, settings, release, domain
182183
and k8s resource if needed
183184
"""
184185
try:
@@ -220,6 +221,10 @@ def create(self, *args, **kwargs): # noqa
220221

221222
raise ServiceUnavailable('Kubernetes resources could not be created') from e
222223

224+
try:
225+
self.appsettings_set.latest()
226+
except AppSettings.DoesNotExist:
227+
AppSettings.objects.create(owner=self.owner, app=self)
223228
# Attach the platform specific application sub domain to the k8s service
224229
# Only attach it on first release in case a customer has remove the app domain
225230
if rel.version == 1 and not Domain.objects.filter(domain=self.id).exists():
@@ -389,6 +394,7 @@ def scale(self, user, structure): # noqa
389394

390395
def _scale_pods(self, scale_types):
391396
release = self.release_set.latest()
397+
app_settings = self.appsettings_set.latest()
392398
version = "v{}".format(release.version)
393399
image = release.image
394400
envs = self._build_env_vars(release.build.type, version, image, release.config.values)
@@ -424,6 +430,7 @@ def _scale_pods(self, scale_types):
424430
'app_type': scale_type,
425431
'build_type': release.build.type,
426432
'healthcheck': healthcheck,
433+
'annotations': {'maintenance': app_settings.maintenance},
427434
'routable': routable,
428435
'deploy_batches': batches,
429436
'deploy_timeout': deploy_timeout,
@@ -461,6 +468,8 @@ def deploy(self, release, force_deploy=False):
461468
if release.build is None:
462469
raise DeisException('No build associated with this release')
463470

471+
app_settings = self.appsettings_set.latest()
472+
464473
# use create to make sure minimum resources are created
465474
self.create()
466475

@@ -509,6 +518,7 @@ def deploy(self, release, force_deploy=False):
509518
'build_type': release.build.type,
510519
'healthcheck': healthcheck,
511520
'routable': routable,
521+
'annotations': {'maintenance': app_settings.maintenance},
512522
'deploy_batches': batches,
513523
'deploy_timeout': deploy_timeout,
514524
'deployment_history_limit': deployment_history,

rootfs/api/models/appsettings.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import logging
2+
from django.conf import settings
3+
from django.db import models
4+
5+
from api.models import UuidAuditedModel
6+
from api.exceptions import DeisException, AlreadyExists
7+
from scheduler import KubeException
8+
9+
10+
class AppSettings(UuidAuditedModel):
11+
"""
12+
Instance of Application settings used by scheduler
13+
"""
14+
15+
owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
16+
app = models.ForeignKey('App', on_delete=models.CASCADE)
17+
maintenance = models.BooleanField(default=False)
18+
19+
class Meta:
20+
get_latest_by = 'created'
21+
unique_together = (('app', 'uuid'))
22+
ordering = ['-created']
23+
24+
def __str__(self):
25+
return "{}-{}".format(self.app.id, str(self.uuid)[:7])
26+
27+
def set_maintenance(self, maintenance):
28+
namespace = self.app.id
29+
service = self._fetch_service_config(namespace)
30+
old_service = service.copy() # in case anything fails for rollback
31+
32+
try:
33+
service['metadata']['annotations']['router.deis.io/maintenance'] = str(maintenance)
34+
self._scheduler.update_service(namespace, namespace, data=service)
35+
except Exception as e:
36+
self._scheduler.update_service(namespace, namespace, data=old_service)
37+
raise KubeException(str(e)) from e
38+
39+
def save(self, *args, **kwargs):
40+
summary = ''
41+
previous_settings = None
42+
try:
43+
previous_settings = self.app.appsettings_set.latest()
44+
except AppSettings.DoesNotExist:
45+
pass
46+
47+
prev_maintenance = getattr(previous_settings, 'maintenance', None)
48+
new_maintenance = getattr(self, 'maintenance')
49+
50+
try:
51+
if new_maintenance is None and prev_maintenance is not None:
52+
setattr(self, 'maintenance', prev_maintenance)
53+
elif prev_maintenance != new_maintenance:
54+
self.set_maintenance(new_maintenance)
55+
summary += "{} changed maintenance mode from {} to {}".format(self.owner, prev_maintenance, new_maintenance) # noqa
56+
except Exception as e:
57+
self.delete()
58+
raise DeisException(str(e)) from e
59+
60+
if not summary and previous_settings:
61+
self.delete()
62+
raise AlreadyExists("{} changed nothing".format(self.owner))
63+
self.app.log('summary of app setting changes: {}'.format(summary), logging.DEBUG)
64+
65+
return super(AppSettings, self).save(**kwargs)

rootfs/api/serializers.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,3 +479,15 @@ class PodSerializer(serializers.BaseSerializer):
479479

480480
def to_representation(self, obj):
481481
return obj
482+
483+
484+
class AppSettingsSerializer(serializers.ModelSerializer):
485+
"""Serialize a :class:`~api.models.AppSettings` model."""
486+
487+
app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all())
488+
owner = serializers.ReadOnlyField(source='owner.username')
489+
490+
class Meta:
491+
"""Metadata options for a :class:`AppSettingsSerializer`."""
492+
model = models.AppSettings
493+
fields = '__all__'
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import requests_mock
2+
3+
from django.core.cache import cache
4+
from django.contrib.auth.models import User
5+
from rest_framework.authtoken.models import Token
6+
7+
from api.models import App
8+
from api.tests import adapter, DeisTransactionTestCase
9+
10+
11+
@requests_mock.Mocker(real_http=True, adapter=adapter)
12+
class TestAppSettings(DeisTransactionTestCase):
13+
"""Tests setting and updating config values"""
14+
15+
fixtures = ['tests.json']
16+
17+
def setUp(self):
18+
self.user = User.objects.get(username='autotest')
19+
self.token = Token.objects.get(user=self.user).key
20+
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)
21+
22+
def tearDown(self):
23+
# make sure every test has a clean slate for k8s mocking
24+
cache.clear()
25+
26+
def test_settings_maintenance(self, mock_requests):
27+
"""
28+
Test that maintenance can be applied
29+
"""
30+
app_id = self.create_app()
31+
app = App.objects.get(id=app_id)
32+
33+
settings = {'maintenance': True}
34+
response = self.client.post(
35+
'/v2/apps/{app_id}/settings'.format(**locals()),
36+
settings)
37+
self.assertEqual(response.status_code, 201, response.data)
38+
self.assertTrue(response.data['maintenance'])
39+
self.assertTrue(app.appsettings_set.latest().maintenance)
40+
41+
settings['maintenance'] = False
42+
response = self.client.post(
43+
'/v2/apps/{app_id}/settings'.format(**locals()),
44+
settings)
45+
self.assertEqual(response.status_code, 201, response.data)
46+
self.assertFalse(response.data['maintenance'])
47+
self.assertFalse(app.appsettings_set.latest().maintenance)
48+
49+
response = self.client.post(
50+
'/v2/apps/{app_id}/settings'.format(**locals()),
51+
settings)
52+
self.assertEqual(response.status_code, 409, response.data)
53+
self.assertFalse(app.appsettings_set.latest().maintenance)
54+
55+
settings = {}
56+
response = self.client.post(
57+
'/v2/apps/{app_id}/settings'.format(**locals()),
58+
settings)
59+
self.assertEqual(response.status_code, 409, response.data)
60+
self.assertFalse(app.appsettings_set.latest().maintenance)
61+
62+
settings['maintenance'] = "test"
63+
response = self.client.post(
64+
'/v2/apps/{app_id}/settings'.format(**locals()),
65+
settings)
66+
self.assertEqual(response.status_code, 400, response.data)

rootfs/api/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@
5757
views.AppViewSet.as_view({'get': 'logs'})),
5858
url(r"^apps/(?P<id>{})/run/?".format(settings.APP_URL_REGEX),
5959
views.AppViewSet.as_view({'post': 'run'})),
60+
# application settings
61+
url(r"^apps/(?P<id>{})/settings/?".format(settings.APP_URL_REGEX),
62+
views.AppSettingsViewSet.as_view({'get': 'retrieve', 'post': 'create'})),
6063
# apps sharing
6164
url(r"^apps/(?P<id>{})/perms/(?P<username>[-_\w]+)/?".format(settings.APP_URL_REGEX),
6265
views.AppPermsViewSet.as_view({'delete': 'destroy'})),

rootfs/api/views.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,11 @@ def restart(self, *args, **kwargs):
296296
return Response(pagination, status=status.HTTP_200_OK)
297297

298298

299+
class AppSettingsViewSet(AppResourceViewSet):
300+
model = models.AppSettings
301+
serializer_class = serializers.AppSettingsSerializer
302+
303+
299304
class DomainViewSet(AppResourceViewSet):
300305
"""A viewset for interacting with Domain objects."""
301306
model = models.Domain

rootfs/scheduler/__init__.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ def deploy(self, namespace, name, image, entrypoint, command, **kwargs): # noqa
8989

9090
app_type = kwargs.get('app_type')
9191
routable = kwargs.get('routable', False)
92+
annotations = kwargs.get('annotations', {})
9293
envs = kwargs.get('envs', {})
9394
port = envs.get('PORT', None)
9495

@@ -134,7 +135,7 @@ def deploy(self, namespace, name, image, entrypoint, command, **kwargs): # noqa
134135
# Make sure the application is routable and uses the correct port
135136
# Done after the fact to let initial deploy settle before routing
136137
# traffic to the application
137-
self._update_application_service(namespace, name, app_type, port, routable)
138+
self._update_application_service(namespace, name, app_type, port, routable, annotations)
138139

139140
def cleanup_release(self, namespace, controller, timeout):
140141
"""
@@ -176,13 +177,15 @@ def _get_deploy_batches(self, steps, desired):
176177

177178
return batches
178179

179-
def _update_application_service(self, namespace, name, app_type, port, routable=False):
180+
def _update_application_service(self, namespace, name, app_type, port, routable=False, annotations={}): # noqa
180181
"""Update application service with all the various required information"""
181182
service = self.get_service(namespace, namespace).json()
182183
old_service = service.copy() # in case anything fails for rollback
183184

184185
try:
185186
# Update service information
187+
for key, value in annotations.items():
188+
service['metadata']['annotations']['router.deis.io/%s' % key] = str(value)
186189
if routable:
187190
service['metadata']['labels']['router.deis.io/routable'] = 'true'
188191
else:

0 commit comments

Comments
 (0)