Repository: deis/deis

Issue:
`deis clusters:create` lets you specify a non-domain name as the domain name
Found by making a typo:

``` console
$ deis clusters:create dev http://dev.23.236.48.218.xip.io --hosts=23.236.48.218 --auth=~/.ssh/deis
```

This should error otherwise apps will return a URL in the form of http://appname.http:///dev...



Discussion:
Related: #1552. We need to validate `clusters:create` in the client and server.

See also #1552. I have a fix for that in a branch, maybe I will add this validation too.


Resolving patch:
diff --git a/controller/api/models.py b/controller/api/models.py
--- a/controller/api/models.py
+++ b/controller/api/models.py
@@ -52,6 +52,16 @@ def _inner(*args, **kwargs):
     return _inner
 
 
+def validate_app_structure(value):
+    """Error if the dict values aren't ints >= 0."""
+    try:
+        for k, v in value.iteritems():
+            if int(v) < 0:
+                raise ValueError("Must be greater than or equal to zero")
+    except ValueError, err:
+        raise ValidationError(err)
+
+
 def validate_comma_separated(value):
     """Error if the value doesn't look like a list of hostnames or IP addresses
     separated by commas.
@@ -61,14 +71,10 @@ def validate_comma_separated(value):
             "{} should be a comma-separated list".format(value))
 
 
-def validate_app_structure(value):
-    """Error if the dict values aren't ints >= 0."""
-    try:
-        for k, v in value.iteritems():
-            if int(v) < 0:
-                raise ValueError("Must be greater than or equal to zero")
-    except ValueError, err:
-        raise ValidationError(err)
+def validate_domain(value):
+    """Error if the domain contains unexpected characters."""
+    if not re.search(r'^[a-zA-Z0-9-\.]+$', value):
+        raise ValidationError('"{}" contains unexpected characters'.format(value))
 
 
 class AuditedModel(models.Model):
@@ -106,7 +112,7 @@ class Cluster(UuidAuditedModel):
     id = models.CharField(max_length=128, unique=True)
     type = models.CharField(max_length=16, choices=CLUSTER_TYPES, default='coreos')
 
-    domain = models.CharField(max_length=128)
+    domain = models.CharField(max_length=128, validators=[validate_domain])
     hosts = models.CharField(max_length=256, validators=[validate_comma_separated])
     auth = models.TextField()
     options = JSONField(default={}, blank=True)
diff --git a/controller/api/serializers.py b/controller/api/serializers.py
--- a/controller/api/serializers.py
+++ b/controller/api/serializers.py
@@ -68,6 +68,11 @@ class Meta:
         model = models.Cluster
         read_only_fields = ('created', 'updated')
 
+    def validate_domain(self, attrs, source):
+        value = attrs[source]
+        models.validate_domain(value)
+        return attrs
+
     def validate_hosts(self, attrs, source):
         value = attrs[source]
         models.validate_comma_separated(value)


Test patch:
diff --git a/controller/api/tests/test_cluster.py b/controller/api/tests/test_cluster.py
--- a/controller/api/tests/test_cluster.py
+++ b/controller/api/tests/test_cluster.py
@@ -85,3 +85,22 @@ def test_cluster_perms_denied(self):
                 'hosts': 'host1,host2', 'auth': 'base64string', 'options': options}
         response = self.client.post(url, json.dumps(body), content_type='application/json')
         self.assertEqual(response.status_code, 403)
+
+    def test_cluster_errors(self):
+        """
+        Tests bad inputs to clusters:create and clusters:update.
+        """
+        url = '/api/clusters'
+        options = {'key': 'val'}
+        body = {'id': 'autotest', 'domain': 'http://bad.domain', 'type': 'mock',
+                'hosts': 'host1,host2', 'auth': 'base64string', 'options': options}
+        response = self.client.post(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 400)
+        body['domain'] = 'autotest.local'
+        response = self.client.post(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 201)
+        cluster_id = response.data['id']  # noqa
+        url = '/api/clusters/{cluster_id}'.format(**locals())
+        body = {'domain': 'http://bad.domain'}
+        response = self.client.patch(url, json.dumps(body), content_type='application/json')
+        self.assertEqual(response.status_code, 400)