Skip to content

Commit f8d28a2

Browse files
authored
Merge pull request #966 from kmala/maint
feat(model): Add new model to store settings
2 parents 98a4798 + 67811f5 commit f8d28a2

15 files changed

Lines changed: 289 additions & 64 deletions

rootfs/api/migrations/0012_auto_20160810_1603.py renamed to rootfs/api/migrations/0011_auto_20160810_1603.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
class Migration(migrations.Migration):
1010

1111
dependencies = [
12-
('api', '0011_config_routable'),
12+
('api', '0010_config_healthcheck'),
1313
]
1414

1515
operations = [

rootfs/api/migrations/0011_config_routable.py

Lines changed: 0 additions & 21 deletions
This file was deleted.
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# -*- coding: utf-8 -*-
2+
# Generated by Django 1.9.8 on 2016-08-16 19:34
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', '0011_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.NullBooleanField(default=None)),
26+
('routable', models.NullBooleanField(default=None)),
27+
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')),
28+
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
29+
],
30+
options={
31+
'get_latest_by': 'created',
32+
'ordering': ['-created'],
33+
},
34+
),
35+
migrations.AlterUniqueTogether(
36+
name='appsettings',
37+
unique_together=set([('app', 'uuid')]),
38+
),
39+
]

rootfs/api/models/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def _fetch_service_config(self, app):
6161
default = {'metadata': {'annotations': {}}}
6262
svc = dict_merge(svc, default)
6363

64+
if 'labels' not in svc['metadata']:
65+
default = {'metadata': {'labels': {}}}
66+
svc = dict_merge(svc, default)
67+
6468
return svc
6569

6670
def _load_service_config(self, app, component):
@@ -117,6 +121,7 @@ class Meta:
117121
from .release import Release # noqa
118122
from .config import Config # noqa
119123
from .build import Build # noqa
124+
from .appsettings import AppSettings # noqa
120125

121126
# define update/delete callbacks for synchronizing
122127
# models with the configuration management backend
@@ -142,6 +147,12 @@ def _log_config_updated(**kwargs):
142147
config.app.log("config {} updated".format(config))
143148

144149

150+
def _log_app_settings_updated(**kwargs):
151+
appSettings = kwargs['instance']
152+
# log only to the controller; this event will be logged in the release summary
153+
appSettings.app.log("application settings {} updated".format(appSettings))
154+
155+
145156
def _log_domain_added(**kwargs):
146157
if kwargs.get('created'):
147158
domain = kwargs['instance']
@@ -170,6 +181,7 @@ def _log_cert_removed(**kwargs):
170181
post_save.connect(_log_config_updated, sender=Config, dispatch_uid='api.models.log')
171182
post_save.connect(_log_domain_added, sender=Domain, dispatch_uid='api.models.log')
172183
post_save.connect(_log_cert_added, sender=Certificate, dispatch_uid='api.models.log')
184+
post_save.connect(_log_app_settings_updated, sender=AppSettings, dispatch_uid='api.models.log')
173185
post_delete.connect(_log_domain_removed, sender=Domain, dispatch_uid='api.models.log')
174186
post_delete.connect(_log_cert_removed, sender=Certificate, dispatch_uid='api.models.log')
175187

rootfs/api/models/app.py

Lines changed: 15 additions & 4 deletions
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)
@@ -403,7 +409,7 @@ def _scale_pods(self, scale_types):
403409
for scale_type, replicas in scale_types.items():
404410
# only web / cmd are routable
405411
# http://docs.deis.io/en/latest/using_deis/process-types/#web-vs-cmd-process-types
406-
routable = True if scale_type in ['web', 'cmd'] and release.config.routable else False
412+
routable = True if scale_type in ['web', 'cmd'] and app_settings.routable else False
407413
# fetch application port and inject into ENV Vars as needed
408414
port = release.get_port()
409415
if port:
@@ -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+
'service_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

@@ -486,7 +495,7 @@ def deploy(self, release, force_deploy=False):
486495
for scale_type, replicas in self.structure.items():
487496
# only web / cmd are routable
488497
# http://docs.deis.io/en/latest/using_deis/process-types/#web-vs-cmd-process-types
489-
routable = True if scale_type in ['web', 'cmd'] and release.config.routable else False
498+
routable = True if scale_type in ['web', 'cmd'] and app_settings.routable else False
490499
# fetch application port and inject into ENV vars as needed
491500
port = release.get_port()
492501
if port:
@@ -509,6 +518,7 @@ def deploy(self, release, force_deploy=False):
509518
'build_type': release.build.type,
510519
'healthcheck': healthcheck,
511520
'routable': routable,
521+
'service_annotations': {'maintenance': app_settings.maintenance},
512522
'deploy_batches': batches,
513523
'deploy_timeout': deploy_timeout,
514524
'deployment_history_limit': deployment_history,
@@ -597,7 +607,8 @@ def verify_application_health(self, **kwargs):
597607
"""
598608
# Bail out early if the application is not routable
599609
release = self.release_set.latest()
600-
if not kwargs.get('routable', False) and release.config.routable:
610+
app_settings = self.appsettings_set.latest()
611+
if not kwargs.get('routable', False) and app_settings.routable:
601612
return
602613

603614
app_type = kwargs.get('app_type')

rootfs/api/models/appsettings.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
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.NullBooleanField(default=None)
18+
routable = models.NullBooleanField(default=None)
19+
20+
class Meta:
21+
get_latest_by = 'created'
22+
unique_together = (('app', 'uuid'))
23+
ordering = ['-created']
24+
25+
def __str__(self):
26+
return "{}-{}".format(self.app.id, str(self.uuid)[:7])
27+
28+
def set_maintenance(self, maintenance):
29+
namespace = self.app.id
30+
service = self._fetch_service_config(namespace)
31+
old_service = service.copy() # in case anything fails for rollback
32+
33+
try:
34+
service['metadata']['annotations']['router.deis.io/maintenance'] = str(maintenance)
35+
self._scheduler.update_service(namespace, namespace, data=service)
36+
except Exception as e:
37+
self._scheduler.update_service(namespace, namespace, data=old_service)
38+
raise KubeException(str(e)) from e
39+
40+
def set_routable(self, routable):
41+
namespace = self.app.id
42+
service = self._fetch_service_config(namespace)
43+
old_service = service.copy() # in case anything fails for rollback
44+
45+
try:
46+
service['metadata']['labels']['router.deis.io/routable'] = str(routable).lower()
47+
self._scheduler.update_service(namespace, namespace, data=service)
48+
except Exception as e:
49+
self._scheduler.update_service(namespace, namespace, data=old_service)
50+
raise KubeException(str(e)) from e
51+
52+
def update_maintenance(self, previous_settings):
53+
prev_maintenance = getattr(previous_settings, 'maintenance', None)
54+
new_maintenance = getattr(self, 'maintenance', None)
55+
# If no previous settings, assume this is first timeout
56+
# and set the default maintenance as false
57+
if not previous_settings:
58+
setattr(self, 'maintenance', False)
59+
self.set_maintenance(False)
60+
# if nothing changed copy the settings from previous
61+
elif new_maintenance is None and prev_maintenance is not None:
62+
setattr(self, 'maintenance', prev_maintenance)
63+
elif prev_maintenance != new_maintenance:
64+
self.set_maintenance(new_maintenance)
65+
self.summary += "{} changed maintenance mode from {} to {}".format(self.owner, prev_maintenance, new_maintenance) # noqa
66+
67+
def update_routable(self, previous_settings):
68+
old_routable = getattr(previous_settings, 'routable', None)
69+
new_routable = getattr(self, 'routable', None)
70+
# If no previous settings, assume this is first timeout
71+
# and set the default maintenance as true
72+
if not previous_settings:
73+
setattr(self, 'routable', True)
74+
self.set_routable(True)
75+
# if nothing changed copy the settings from previous
76+
elif new_routable is None and old_routable is not None:
77+
setattr(self, 'routable', old_routable)
78+
elif old_routable != new_routable:
79+
self.set_routable(new_routable)
80+
self.summary += "{} changed routablity from {} to {}".format(self.owner, old_routable, new_routable) # noqa
81+
82+
def save(self, *args, **kwargs):
83+
self.summary = ''
84+
previous_settings = None
85+
try:
86+
previous_settings = self.app.appsettings_set.latest()
87+
except AppSettings.DoesNotExist:
88+
pass
89+
90+
try:
91+
self.update_maintenance(previous_settings)
92+
self.update_routable(previous_settings)
93+
except Exception as e:
94+
self.delete()
95+
raise DeisException(str(e)) from e
96+
97+
if not self.summary and previous_settings:
98+
self.delete()
99+
raise AlreadyExists("{} changed nothing".format(self.owner))
100+
self.app.log('summary of app setting changes: {}'.format(self.summary), logging.DEBUG)
101+
102+
return super(AppSettings, self).save(**kwargs)

rootfs/api/models/config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ class Config(UuidAuditedModel):
2121
tags = JSONField(default={}, blank=True)
2222
registry = JSONField(default={}, blank=True)
2323
healthcheck = JSONField(default={}, blank=True)
24-
routable = models.BooleanField(default=True)
2524

2625
class Meta:
2726
get_latest_by = 'created'

rootfs/api/models/release.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -472,17 +472,6 @@ def save(self, *args, **kwargs): # noqa
472472
self.summary += ' and '
473473
self.summary += "{} {}".format(self.config.owner, changes)
474474

475-
# if the routable flag changed, log that too
476-
changes = []
477-
old_routable = old_config.routable if old_config else True
478-
enabled = "enabled routing" if self.config.routable and not old_routable else ''
479-
disabled = "disabled routing" if not self.config.routable and old_routable else ''
480-
changes = ', '.join(i for i in (enabled, disabled) if i)
481-
if changes:
482-
if self.summary:
483-
self.summary += ' and '
484-
self.summary += "{} {}".format(self.config.owner, changes)
485-
486475
if not self.summary:
487476
if self.version == 1:
488477
self.summary = "{} created the initial release".format(self.owner)

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__'

0 commit comments

Comments
 (0)