Skip to content

Commit 517869d

Browse files
author
Gabriel Monroy
committed
feat(controller): support for application key/value tags
1 parent e4a4281 commit 517869d

6 files changed

Lines changed: 279 additions & 4 deletions

File tree

controller/api/models.py

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,8 @@ def _command_announceable(self):
337337
def create(self):
338338
image = self.release.image
339339
kwargs = {'memory': self.release.config.memory,
340-
'cpu': self.release.config.cpu}
340+
'cpu': self.release.config.cpu,
341+
'tags': self.release.config.tags}
341342
self._scheduler.create(name=self._job_id,
342343
image=image,
343344
command=self._command,
@@ -366,7 +367,8 @@ def deploy(self, release):
366367
image = self.release.image
367368
c_type = self.type
368369
kwargs = {'memory': self.release.config.memory,
369-
'cpu': self.release.config.cpu}
370+
'cpu': self.release.config.cpu,
371+
'tags': self.release.config.tags}
370372
self._scheduler.create(name=new_job_id,
371373
image=image,
372374
command=self._command.format(**locals()),
@@ -459,6 +461,7 @@ class Config(UuidAuditedModel):
459461
values = JSONField(default={}, blank=True)
460462
memory = JSONField(default={}, blank=True)
461463
cpu = JSONField(default={}, blank=True)
464+
tags = JSONField(default={}, blank=True)
462465

463466
class Meta:
464467
get_latest_by = 'created'
@@ -595,6 +598,22 @@ def save(self, *args, **kwargs): # noqa
595598
if changes:
596599
changes = 'changed limits for '+', '.join(changes)
597600
self.summary += "{} {}".format(self.config.owner, changes)
601+
# if the tags changed, log the dict diff
602+
changes = []
603+
old_tags = old_config.tags if old_config else {}
604+
diff = dict_diff(self.config.tags, old_tags)
605+
# try to be as succinct as possible
606+
added = ', '.join(k for k in diff.get('added', {}))
607+
added = 'added tag ' + added if added else ''
608+
changed = ', '.join(k for k in diff.get('changed', {}))
609+
changed = 'changed tag ' + changed if changed else ''
610+
deleted = ', '.join(k for k in diff.get('deleted', {}))
611+
deleted = 'deleted tag ' + deleted if deleted else ''
612+
changes = ', '.join(i for i in (added, changed, deleted) if i)
613+
if changes:
614+
if self.summary:
615+
self.summary += ' and '
616+
self.summary += "{} {}".format(self.config.owner, changes)
598617
if not self.summary:
599618
if self.version == 1:
600619
self.summary = "{} created the initial release".format(self.owner)

controller/api/serializers.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
PROCTYPE_MATCH = re.compile(r'^(?P<type>[a-z]+)')
1818
MEMLIMIT_MATCH = re.compile(r'^(?P<mem>[0-9]+[BbKkMmGg])$')
1919
CPUSHARE_MATCH = re.compile(r'^(?P<cpu>[0-9]+)$')
20+
TAGKEY_MATCH = re.compile(r'^[a-z]+$')
21+
TAGVAL_MATCH = re.compile(r'^\w+$')
2022

2123

2224
class OwnerSlugRelatedField(serializers.SlugRelatedField):
@@ -114,6 +116,8 @@ class ConfigSerializer(serializers.ModelSerializer):
114116
model_field=models.Config()._meta.get_field('memory'), required=False)
115117
cpu = serializers.ModelField(
116118
model_field=models.Config()._meta.get_field('cpu'), required=False)
119+
tags = serializers.ModelField(
120+
model_field=models.Config()._meta.get_field('tags'), required=False)
117121

118122
class Meta:
119123
"""Metadata options for a :class:`ConfigSerializer`."""
@@ -149,6 +153,16 @@ def validate_cpu(self, attrs, source):
149153
raise serializers.ValidationError("CPU shares must be between 0 and 1024")
150154
return attrs
151155

156+
def validate_tags(self, attrs, source):
157+
for k, v in attrs.get(source, {}).items():
158+
if v is None: # use NoneType to unset a value
159+
continue
160+
if not re.match(TAGKEY_MATCH, k):
161+
raise serializers.ValidationError("Tag keys can only contain [a-z]")
162+
if not re.match(TAGVAL_MATCH, str(v)):
163+
raise serializers.ValidationError("Invalid tag value")
164+
return attrs
165+
152166

153167
class ReleaseSerializer(serializers.ModelSerializer):
154168
"""Serialize a :class:`~api.models.Release` model."""
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# -*- coding: utf-8 -*-
2+
from south.utils import datetime_utils as datetime
3+
from south.db import db
4+
from south.v2 import SchemaMigration
5+
from django.db import models
6+
7+
8+
class Migration(SchemaMigration):
9+
10+
def forwards(self, orm):
11+
# Adding field 'Config.tags'
12+
db.add_column(u'api_config', 'tags',
13+
self.gf('json_field.fields.JSONField')(default=u'{}', blank=True),
14+
keep_default=False)
15+
16+
17+
def backwards(self, orm):
18+
# Deleting field 'Config.tags'
19+
db.delete_column(u'api_config', 'tags')
20+
21+
22+
models = {
23+
u'api.app': {
24+
'Meta': {'object_name': 'App'},
25+
'cluster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Cluster']"}),
26+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
27+
'id': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '64'}),
28+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
29+
'structure': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
30+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
31+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
32+
},
33+
u'api.build': {
34+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Build'},
35+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
36+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
37+
'dockerfile': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
38+
'image': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
39+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
40+
'procfile': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
41+
'sha': ('django.db.models.fields.CharField', [], {'max_length': '40', 'blank': 'True'}),
42+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
43+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
44+
},
45+
u'api.cluster': {
46+
'Meta': {'object_name': 'Cluster'},
47+
'auth': ('django.db.models.fields.TextField', [], {}),
48+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
49+
'domain': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
50+
'hosts': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
51+
'id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
52+
'options': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
53+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
54+
'type': ('django.db.models.fields.CharField', [], {'default': "u'coreos'", 'max_length': '16'}),
55+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
56+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
57+
},
58+
u'api.config': {
59+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Config'},
60+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
61+
'cpu': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
62+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
63+
'memory': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
64+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
65+
'tags': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'}),
66+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
67+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'}),
68+
'values': ('json_field.fields.JSONField', [], {'default': '{}', 'blank': 'True'})
69+
},
70+
u'api.container': {
71+
'Meta': {'ordering': "[u'created']", 'object_name': 'Container'},
72+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
73+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
74+
'num': ('django.db.models.fields.PositiveIntegerField', [], {}),
75+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
76+
'release': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Release']"}),
77+
'state': ('django_fsm.FSMField', [], {'default': "u'initialized'", 'max_length': '50'}),
78+
'type': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
79+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
80+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
81+
},
82+
u'api.domain': {
83+
'Meta': {'object_name': 'Domain'},
84+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
85+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
86+
'domain': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
87+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
88+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
89+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
90+
},
91+
u'api.key': {
92+
'Meta': {'unique_together': "((u'owner', u'id'),)", 'object_name': 'Key'},
93+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
94+
'id': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
95+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
96+
'public': ('django.db.models.fields.TextField', [], {'unique': 'True'}),
97+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
98+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
99+
},
100+
u'api.push': {
101+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Push'},
102+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
103+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
104+
'fingerprint': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
105+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
106+
'receive_repo': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
107+
'receive_user': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
108+
'sha': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
109+
'ssh_connection': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
110+
'ssh_original_command': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
111+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
112+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
113+
},
114+
u'api.release': {
115+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'version'),)", 'object_name': 'Release'},
116+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
117+
'build': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Build']"}),
118+
'config': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Config']"}),
119+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
120+
'image': ('django.db.models.fields.CharField', [], {'default': "u'deis/helloworld'", 'max_length': '256'}),
121+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
122+
'summary': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
123+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
124+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'}),
125+
'version': ('django.db.models.fields.PositiveIntegerField', [], {})
126+
},
127+
u'auth.group': {
128+
'Meta': {'object_name': 'Group'},
129+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
130+
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
131+
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
132+
},
133+
u'auth.permission': {
134+
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
135+
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
136+
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
137+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
138+
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
139+
},
140+
u'auth.user': {
141+
'Meta': {'object_name': 'User'},
142+
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
143+
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
144+
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
145+
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
146+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
147+
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
148+
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
149+
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
150+
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
151+
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
152+
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
153+
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
154+
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
155+
},
156+
u'contenttypes.contenttype': {
157+
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
158+
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
159+
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
160+
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
161+
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
162+
}
163+
}
164+
165+
complete_apps = ['api']

0 commit comments

Comments
 (0)