Skip to content

Commit 1714dd2

Browse files
author
Matthew Fisher
committed
Merge pull request #956 from deis/482-custom-domains
feat(controller): add custom domains to apps
2 parents 3378532 + bdf08ba commit 1714dd2

18 files changed

Lines changed: 579 additions & 61 deletions

File tree

client/deis.py

Lines changed: 85 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,89 @@ 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+
body = {'domain': domain}
1072+
sys.stdout.write("Adding {domain} to {app}... ".format(**locals()))
1073+
sys.stdout.flush()
1074+
try:
1075+
progress = TextProgress()
1076+
progress.start()
1077+
response = self._dispatch('post', "/api/apps/{app}/domains".format(app=app), json.dumps(body))
1078+
finally:
1079+
progress.cancel()
1080+
progress.join()
1081+
if response.status_code == requests.codes.created: # @UndefinedVariable
1082+
print("done")
1083+
else:
1084+
raise ResponseError(response)
1085+
1086+
def domains_remove(self, args):
1087+
"""
1088+
Unbind a domain for an application
1089+
1090+
Usage: deis domains:remove <domain> [--app=<app>]
1091+
"""
1092+
app = args.get('--app')
1093+
if not app:
1094+
app = self._session.app
1095+
domain = args.get('<domain>')
1096+
sys.stdout.write("Removing {domain} from {app}... ".format(**locals()))
1097+
sys.stdout.flush()
1098+
try:
1099+
progress = TextProgress()
1100+
progress.start()
1101+
response = self._dispatch('delete', "/api/apps/{app}/domains/{domain}".format(**locals()))
1102+
finally:
1103+
progress.cancel()
1104+
progress.join()
1105+
if response.status_code == requests.codes.no_content: # @UndefinedVariable
1106+
print("done")
1107+
else:
1108+
raise ResponseError(response)
1109+
1110+
def domains_list(self, args):
1111+
"""
1112+
List domains bound to an application
1113+
1114+
Usage: deis domains:list [--app=<app>]
1115+
"""
1116+
app = args.get('--app')
1117+
if not app:
1118+
app = self._session.app
1119+
response = self._dispatch(
1120+
'get', "/api/apps/{app}/domains".format(app=app))
1121+
if response.status_code == requests.codes.ok: # @UndefinedVariable
1122+
domains = response.json()['results']
1123+
print("=== {} Domains".format(app))
1124+
if len(domains) == 0:
1125+
print('No domains')
1126+
return
1127+
for domain in domains:
1128+
print(domain['domain'])
1129+
else:
1130+
raise ResponseError(response)
1131+
10471132
def ps(self, args):
10481133
"""
10491134
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: 37 additions & 4 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 self.domain
544+
545+
536546
@python_2_unicode_compatible
537547
class Key(UuidAuditedModel):
538548
"""An SSH public key."""
@@ -552,7 +562,6 @@ def __str__(self):
552562
# define update/delete callbacks for synchronizing
553563
# models with the configuration management backend
554564

555-
556565
def _log_build_created(**kwargs):
557566
if kwargs.get('created'):
558567
build = kwargs['instance']
@@ -570,6 +579,16 @@ def _log_config_updated(**kwargs):
570579
log_event(config.app, "Config {} updated".format(config))
571580

572581

582+
def _log_domain_added(**kwargs):
583+
domain = kwargs['instance']
584+
log_event(domain.app, "Domain {} added".format(domain))
585+
586+
587+
def _log_domain_removed(**kwargs):
588+
domain = kwargs['instance']
589+
log_event(domain.app, "Domain {} removed".format(domain))
590+
591+
573592
def _etcd_publish_key(**kwargs):
574593
key = kwargs['instance']
575594
_etcd_client.write('/deis/builder/users/{}/{}'.format(
@@ -587,10 +606,22 @@ def _etcd_purge_user(**kwargs):
587606
_etcd_client.delete('/deis/builder/users/{}'.format(username), dir=True, recursive=True)
588607

589608

609+
def _etcd_publish_domains(**kwargs):
610+
app = kwargs['instance'].app
611+
app_domains = app.domain_set.all()
612+
if app_domains:
613+
_etcd_client.write('/deis/domains/{}'.format(app),
614+
' '.join(str(d.domain) for d in app_domains))
615+
else:
616+
_etcd_client.delete('/deis/domains/{}'.format(app))
617+
618+
590619
# Log significant app-related events
591-
post_save.connect(_log_build_created, sender=Build, dispatch_uid='api.models')
592-
post_save.connect(_log_release_created, sender=Release, dispatch_uid='api.models')
593-
post_save.connect(_log_config_updated, sender=Config, dispatch_uid='api.models')
620+
post_save.connect(_log_build_created, sender=Build, dispatch_uid='api.models.log')
621+
post_save.connect(_log_release_created, sender=Release, dispatch_uid='api.models.log')
622+
post_save.connect(_log_config_updated, sender=Config, dispatch_uid='api.models.log')
623+
post_save.connect(_log_domain_added, sender=Domain, dispatch_uid='api.models.log')
624+
post_delete.connect(_log_domain_removed, sender=Domain, dispatch_uid='api.models.log')
594625

595626

596627
# save FSM transitions as they happen
@@ -611,3 +642,5 @@ def _save_transition(**kwargs):
611642
post_save.connect(_etcd_publish_key, sender=Key, dispatch_uid='api.models')
612643
post_delete.connect(_etcd_purge_key, sender=Key, dispatch_uid='api.models')
613644
post_delete.connect(_etcd_purge_user, sender=User, dispatch_uid='api.models')
645+
post_save.connect(_etcd_publish_domains, sender=Domain, dispatch_uid='api.models')
646+
post_delete.connect(_etcd_publish_domains, sender=Domain, dispatch_uid='api.models')

0 commit comments

Comments
 (0)