Skip to content

Commit 23df4ab

Browse files
nathansamsoncarmstrong
authored andcommitted
feat(controller): add Domain model
This is added in the API with 2 new URLS GET /apps/{app}/domains to retrieve the list of the domains POST /apps/{app}/domains/{domain} to create a new domain. The CLI is adapted to make use of this new API.
1 parent 2aa99a6 commit 23df4ab

9 files changed

Lines changed: 433 additions & 0 deletions

File tree

client/deis.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
clusters manage clusters used to host applications
1717
ps manage processes inside an app container
1818
config manage environment variables that define app config
19+
domains manage and assign domain names to your applications
1920
builds manage builds created using `git push`
2021
releases manage releases of an application
2122
@@ -519,6 +520,7 @@ def apps_info(self, args):
519520
print(json.dumps(response.json(), indent=2))
520521
print()
521522
self.ps_list(args)
523+
self.domains_list(args)
522524
print()
523525
else:
524526
raise ResponseError(response)
@@ -1044,6 +1046,76 @@ def config_unset(self, args):
10441046
else:
10451047
raise ResponseError(response)
10461048

1049+
def domains(self, args):
1050+
"""
1051+
Valid commands for domains:
1052+
1053+
domains:add bind a domain to an application
1054+
domains:list list domains bound to an application
1055+
domains:remove unbind a domain from an application
1056+
1057+
Use `deis help [command]` to learn more
1058+
"""
1059+
return self.domains_list(args)
1060+
1061+
def domains_add(self, args):
1062+
"""
1063+
Bind a domain to an application
1064+
1065+
Usage: deis domains:add <domain> [--app=<app>]
1066+
"""
1067+
app = args.get('--app')
1068+
if not app:
1069+
app = self._session.app
1070+
domain = args.get('<domain>')
1071+
if domain is None:
1072+
print("Faulty input")
1073+
sys.exit(1)
1074+
body = {'domain': domain}
1075+
response = self._dispatch(
1076+
'post', "/api/apps/{app}/domains".format(app=app),
1077+
json.dumps(body))
1078+
if response.status_code == requests.codes.ok: # @UndefinedVariable
1079+
print("Domain created")
1080+
else:
1081+
raise ResponseError(response)
1082+
1083+
def domains_remove(self, args):
1084+
"""
1085+
Unbind a domain for an application
1086+
1087+
Usage: deis domains:rm <domain> [--app=<app>]
1088+
"""
1089+
app = args.get('--app')
1090+
if not app:
1091+
app = self._session.app
1092+
domain = args.get('<domain>')
1093+
if domain is None:
1094+
print("Faulty input")
1095+
return
1096+
response = self._dispatch(
1097+
'delete', "/api/domains/{domain}".format(app=app, domain=domain))
1098+
if response.status_code == requests.codes.ok: # @UndefinedVariable
1099+
print("Domain removed")
1100+
else:
1101+
raise ResponseError(response)
1102+
1103+
def domains_list(self, args):
1104+
"""
1105+
List domains bound to an application
1106+
1107+
Usage: deis domains:list [--app=<app>]
1108+
"""
1109+
app = args.get('--app')
1110+
if not app:
1111+
app = self._session.app
1112+
response = self._dispatch(
1113+
'get', "/api/apps/{app}/domains".format(app=app))
1114+
if response.status_code == requests.codes.ok: # @UndefinedVariable
1115+
print(json.dumps(response.json(), indent=2))
1116+
else:
1117+
raise ResponseError(response)
1118+
10471119
def ps(self, args):
10481120
"""
10491121
Valid commands for processes:

controller/api/admin.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .models import Cluster
1515
from .models import Config
1616
from .models import Container
17+
from .models import Domain
1718
from .models import Key
1819
from .models import Release
1920

@@ -68,6 +69,16 @@ class ContainerAdmin(admin.ModelAdmin):
6869
admin.site.register(Container, ContainerAdmin)
6970

7071

72+
class DomainAdmin(admin.ModelAdmin):
73+
"""Set presentation options for :class:`~api.models.Domain` models
74+
in the Django admin.
75+
"""
76+
date_hierarchy = 'created'
77+
list_display = ('owner', 'app', 'domain')
78+
list_filter = ('owner', 'app')
79+
admin.site.register(Domain, DomainAdmin)
80+
81+
7182
class KeyAdmin(admin.ModelAdmin):
7283
"""Set presentation options for :class:`~api.models.Key` models
7384
in the Django admin.
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 model 'Domain'
12+
db.create_table(u'api_domain', (
13+
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
14+
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
15+
('updated', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
16+
('owner', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
17+
('app', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['api.App'])),
18+
('domain', self.gf('django.db.models.fields.TextField')(unique=True)),
19+
))
20+
db.send_create_signal(u'api', ['Domain'])
21+
22+
23+
def backwards(self, orm):
24+
# Deleting model 'Domain'
25+
db.delete_table(u'api_domain')
26+
27+
28+
models = {
29+
u'api.app': {
30+
'Meta': {'object_name': 'App'},
31+
'cluster': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.Cluster']"}),
32+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
33+
'id': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '64'}),
34+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
35+
'structure': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'}),
36+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
37+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
38+
},
39+
u'api.build': {
40+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Build'},
41+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
42+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
43+
'image': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
44+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
45+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
46+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
47+
},
48+
u'api.cluster': {
49+
'Meta': {'object_name': 'Cluster'},
50+
'auth': ('django.db.models.fields.TextField', [], {}),
51+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
52+
'domain': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
53+
'hosts': ('django.db.models.fields.CharField', [], {'max_length': '256'}),
54+
'id': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '128'}),
55+
'options': ('json_field.fields.JSONField', [], {'default': "u'{}'", 'blank': 'True'}),
56+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
57+
'type': ('django.db.models.fields.CharField', [], {'default': "u'coreos'", 'max_length': '16'}),
58+
'updated': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
59+
'uuid': ('api.fields.UuidField', [], {'unique': 'True', 'max_length': '32', 'primary_key': 'True'})
60+
},
61+
u'api.config': {
62+
'Meta': {'ordering': "[u'-created']", 'unique_together': "((u'app', u'uuid'),)", 'object_name': 'Config'},
63+
'app': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['api.App']"}),
64+
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
65+
'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"}),
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': "u'{}'", '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', 'blank': 'True'}),
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', [], {'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']

controller/api/models.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,16 @@ def save(self, *args, **kwargs):
533533
super(Release, self).save(*args, **kwargs)
534534

535535

536+
@python_2_unicode_compatible
537+
class Domain(AuditedModel):
538+
owner = models.ForeignKey(settings.AUTH_USER_MODEL)
539+
app = models.ForeignKey('App')
540+
domain = models.TextField(blank=False, null=False, unique=True)
541+
542+
def __str__(self):
543+
return "{0} -> {1}".format(self.domain, self.app.id)
544+
545+
536546
@python_2_unicode_compatible
537547
class Key(UuidAuditedModel):
538548
"""An SSH public key."""

controller/api/serializers.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,3 +163,38 @@ class Meta:
163163
"""Metadata options for a KeySerializer."""
164164
model = models.Key
165165
read_only_fields = ('created', 'updated')
166+
167+
168+
class DomainSerializer(serializers.ModelSerializer):
169+
"""Serialize a :class:`~api.models.Domain` model."""
170+
171+
owner = serializers.Field(source='owner.username')
172+
app = serializers.SlugRelatedField(slug_field='id')
173+
174+
class Meta:
175+
"""Metadata options for a :class:`DomainSerializer`."""
176+
model = models.Domain
177+
fields = ('domain', 'owner', 'created', 'updated', 'app')
178+
read_only_fields = ('created', 'updated')
179+
180+
def validate_domain(self, attrs, source):
181+
"""
182+
Check that the hostname is valid
183+
"""
184+
value = attrs[source]
185+
match = re.match(r'^(\*\.)?([a-z0-9-]+\.)*([a-z0-9-]+)\.([a-z0-9]{2,})$', value)
186+
if not match:
187+
raise serializers.ValidationError(
188+
"Hostname does not look like a valid hostname. "
189+
"Only lowercase characters are allowed.")
190+
191+
if models.Domain.objects.filter(domain=value).exists():
192+
raise serializers.ValidationError(
193+
"The domain {} is already in use by another app".format(value))
194+
195+
domain_parts = value.split('.')
196+
if domain_parts[0] == '*':
197+
raise serializers.ValidationError(
198+
"Adding a wildcard subdomain is currently not supported".format(value))
199+
200+
return attrs

controller/api/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ def run_tests(self, test_labels, extra_tests=None, **kwargs):
4646
from .test_build import * # noqa
4747
from .test_cluster import * # noqa
4848
from .test_config import * # noqa
49+
from .test_domain import * # noqa
4950
from .test_container import * # noqa
5051
from .test_hooks import * # noqa
5152
from .test_key import * # noqa

0 commit comments

Comments
 (0)