Skip to content
This repository has been archived by the owner on May 6, 2020. It is now read-only.

Commit

Permalink
feat(model): Add new model to store settings
Browse files Browse the repository at this point in the history
  • Loading branch information
kmala committed Aug 16, 2016
1 parent 98a4798 commit 3e988ea
Show file tree
Hide file tree
Showing 9 changed files with 213 additions and 3 deletions.
38 changes: 38 additions & 0 deletions rootfs/api/migrations/0013_auto_20160812_1905.py
@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.9.8 on 2016-08-12 19:05
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('api', '0012_auto_20160810_1603'),
]

operations = [
migrations.CreateModel(
name='AppSettings',
fields=[
('uuid', models.UUIDField(auto_created=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True, verbose_name='UUID')),
('created', models.DateTimeField(auto_now_add=True)),
('updated', models.DateTimeField(auto_now=True)),
('maintenance', models.BooleanField(default=False)),
('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.App')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
options={
'get_latest_by': 'created',
'ordering': ['-created'],
},
),
migrations.AlterUniqueTogether(
name='appsettings',
unique_together=set([('app', 'uuid')]),
),
]
8 changes: 8 additions & 0 deletions rootfs/api/models/__init__.py
Expand Up @@ -117,6 +117,7 @@ class Meta:
from .release import Release # noqa
from .config import Config # noqa
from .build import Build # noqa
from .appsettings import AppSettings # noqa

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


def _log_app_settings_updated(**kwargs):
appSettings = kwargs['instance']
# log only to the controller; this event will be logged in the release summary
appSettings.app.log("application settings {} updated".format(appSettings))


def _log_domain_added(**kwargs):
if kwargs.get('created'):
domain = kwargs['instance']
Expand Down Expand Up @@ -170,6 +177,7 @@ def _log_cert_removed(**kwargs):
post_save.connect(_log_config_updated, sender=Config, dispatch_uid='api.models.log')
post_save.connect(_log_domain_added, sender=Domain, dispatch_uid='api.models.log')
post_save.connect(_log_cert_added, sender=Certificate, dispatch_uid='api.models.log')
post_save.connect(_log_app_settings_updated, sender=AppSettings, dispatch_uid='api.models.log')
post_delete.connect(_log_domain_removed, sender=Domain, dispatch_uid='api.models.log')
post_delete.connect(_log_cert_removed, sender=Certificate, dispatch_uid='api.models.log')

Expand Down
12 changes: 11 additions & 1 deletion rootfs/api/models/app.py
Expand Up @@ -23,6 +23,7 @@
from api.models.release import Release
from api.models.config import Config
from api.models.domain import Domain
from api.models.appsettings import AppSettings

from scheduler import KubeHTTPException, KubeException

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

def create(self, *args, **kwargs): # noqa
"""
Create a application with an initial config, release, domain
Create a application with an initial config, settings, release, domain
and k8s resource if needed
"""
try:
Expand Down Expand Up @@ -220,6 +221,10 @@ def create(self, *args, **kwargs): # noqa

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

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

def _scale_pods(self, scale_types):
release = self.release_set.latest()
app_settings = self.appsettings_set.latest()
version = "v{}".format(release.version)
image = release.image
envs = self._build_env_vars(release.build.type, version, image, release.config.values)
Expand Down Expand Up @@ -424,6 +430,7 @@ def _scale_pods(self, scale_types):
'app_type': scale_type,
'build_type': release.build.type,
'healthcheck': healthcheck,
'annotations': {'maintenance': app_settings.maintenance},
'routable': routable,
'deploy_batches': batches,
'deploy_timeout': deploy_timeout,
Expand Down Expand Up @@ -461,6 +468,8 @@ def deploy(self, release, force_deploy=False):
if release.build is None:
raise DeisException('No build associated with this release')

app_settings = self.appsettings_set.latest()

# use create to make sure minimum resources are created
self.create()

Expand Down Expand Up @@ -509,6 +518,7 @@ def deploy(self, release, force_deploy=False):
'build_type': release.build.type,
'healthcheck': healthcheck,
'routable': routable,
'annotations': {'maintenance': app_settings.maintenance},
'deploy_batches': batches,
'deploy_timeout': deploy_timeout,
'deployment_history_limit': deployment_history,
Expand Down
65 changes: 65 additions & 0 deletions rootfs/api/models/appsettings.py
@@ -0,0 +1,65 @@
import logging
from django.conf import settings
from django.db import models

from api.models import UuidAuditedModel
from api.exceptions import DeisException, AlreadyExists
from scheduler import KubeException


class AppSettings(UuidAuditedModel):
"""
Instance of Application settings used by scheduler
"""

owner = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT)
app = models.ForeignKey('App', on_delete=models.CASCADE)
maintenance = models.BooleanField(default=False)

class Meta:
get_latest_by = 'created'
unique_together = (('app', 'uuid'))
ordering = ['-created']

def __str__(self):
return "{}-{}".format(self.app.id, str(self.uuid)[:7])

def set_maintenance(self, maintenance):
namespace = self.app.id
service = self._fetch_service_config(namespace)
old_service = service.copy() # in case anything fails for rollback

try:
service['metadata']['annotations']['router.deis.io/maintenance'] = str(maintenance)
self._scheduler.update_service(namespace, namespace, data=service)
except Exception as e:
self._scheduler.update_service(namespace, namespace, data=old_service)
raise KubeException(str(e)) from e

def save(self, *args, **kwargs):
summary = ''
previous_settings = None
try:
previous_settings = self.app.appsettings_set.latest()
except AppSettings.DoesNotExist:
pass

prev_maintenance = getattr(previous_settings, 'maintenance', None)
new_maintenance = getattr(self, 'maintenance')

try:
if new_maintenance is None and prev_maintenance is not None:
setattr(self, 'maintenance', prev_maintenance)
elif prev_maintenance != new_maintenance:
self.set_maintenance(new_maintenance)
summary += "{} changed maintenance mode from {} to {}".format(self.owner, prev_maintenance, new_maintenance) # noqa
except Exception as e:
self.delete()
raise DeisException(str(e)) from e

if not summary and previous_settings:
self.delete()
raise AlreadyExists("{} changed nothing".format(self.owner))
self.app.log('summary of app setting changes: {}'.format(summary), logging.DEBUG)

return super(AppSettings, self).save(**kwargs)
12 changes: 12 additions & 0 deletions rootfs/api/serializers.py
Expand Up @@ -479,3 +479,15 @@ class PodSerializer(serializers.BaseSerializer):

def to_representation(self, obj):
return obj


class AppSettingsSerializer(serializers.ModelSerializer):
"""Serialize a :class:`~api.models.AppSettings` model."""

app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all())
owner = serializers.ReadOnlyField(source='owner.username')

class Meta:
"""Metadata options for a :class:`AppSettingsSerializer`."""
model = models.AppSettings
fields = '__all__'
66 changes: 66 additions & 0 deletions rootfs/api/tests/test_app_settings.py
@@ -0,0 +1,66 @@
import requests_mock

from django.core.cache import cache
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token

from api.models import App
from api.tests import adapter, DeisTransactionTestCase


@requests_mock.Mocker(real_http=True, adapter=adapter)
class TestAppSettings(DeisTransactionTestCase):
"""Tests setting and updating config values"""

fixtures = ['tests.json']

def setUp(self):
self.user = User.objects.get(username='autotest')
self.token = Token.objects.get(user=self.user).key
self.client.credentials(HTTP_AUTHORIZATION='Token ' + self.token)

def tearDown(self):
# make sure every test has a clean slate for k8s mocking
cache.clear()

def test_settings_maintenance(self, mock_requests):
"""
Test that maintenance can be applied
"""
app_id = self.create_app()
app = App.objects.get(id=app_id)

settings = {'maintenance': True}
response = self.client.post(
'/v2/apps/{app_id}/settings'.format(**locals()),
settings)
self.assertEqual(response.status_code, 201, response.data)
self.assertTrue(response.data['maintenance'])
self.assertTrue(app.appsettings_set.latest().maintenance)

settings['maintenance'] = False
response = self.client.post(
'/v2/apps/{app_id}/settings'.format(**locals()),
settings)
self.assertEqual(response.status_code, 201, response.data)
self.assertFalse(response.data['maintenance'])
self.assertFalse(app.appsettings_set.latest().maintenance)

response = self.client.post(
'/v2/apps/{app_id}/settings'.format(**locals()),
settings)
self.assertEqual(response.status_code, 409, response.data)
self.assertFalse(app.appsettings_set.latest().maintenance)

settings = {}
response = self.client.post(
'/v2/apps/{app_id}/settings'.format(**locals()),
settings)
self.assertEqual(response.status_code, 409, response.data)
self.assertFalse(app.appsettings_set.latest().maintenance)

settings['maintenance'] = "test"
response = self.client.post(
'/v2/apps/{app_id}/settings'.format(**locals()),
settings)
self.assertEqual(response.status_code, 400, response.data)
3 changes: 3 additions & 0 deletions rootfs/api/urls.py
Expand Up @@ -57,6 +57,9 @@
views.AppViewSet.as_view({'get': 'logs'})),
url(r"^apps/(?P<id>{})/run/?".format(settings.APP_URL_REGEX),
views.AppViewSet.as_view({'post': 'run'})),
# application settings
url(r"^apps/(?P<id>{})/settings/?".format(settings.APP_URL_REGEX),
views.AppSettingsViewSet.as_view({'get': 'retrieve', 'post': 'create'})),
# apps sharing
url(r"^apps/(?P<id>{})/perms/(?P<username>[-_\w]+)/?".format(settings.APP_URL_REGEX),
views.AppPermsViewSet.as_view({'delete': 'destroy'})),
Expand Down
5 changes: 5 additions & 0 deletions rootfs/api/views.py
Expand Up @@ -296,6 +296,11 @@ def restart(self, *args, **kwargs):
return Response(pagination, status=status.HTTP_200_OK)


class AppSettingsViewSet(AppResourceViewSet):
model = models.AppSettings
serializer_class = serializers.AppSettingsSerializer


class DomainViewSet(AppResourceViewSet):
"""A viewset for interacting with Domain objects."""
model = models.Domain
Expand Down
7 changes: 5 additions & 2 deletions rootfs/scheduler/__init__.py
Expand Up @@ -89,6 +89,7 @@ def deploy(self, namespace, name, image, entrypoint, command, **kwargs): # noqa

app_type = kwargs.get('app_type')
routable = kwargs.get('routable', False)
annotations = kwargs.get('annotations', {})
envs = kwargs.get('envs', {})
port = envs.get('PORT', None)

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

def cleanup_release(self, namespace, controller, timeout):
"""
Expand Down Expand Up @@ -176,13 +177,15 @@ def _get_deploy_batches(self, steps, desired):

return batches

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

try:
# Update service information
for key, value in annotations.items():
service['metadata']['annotations']['router.deis.io/%s' % key] = str(value)
if routable:
service['metadata']['labels']['router.deis.io/routable'] = 'true'
else:
Expand Down

0 comments on commit 3e988ea

Please sign in to comment.