github.com/dustinrc/deis@v1.10.1-0.20150917223407-0894a5fb979e/controller/api/serializers.py (about) 1 """ 2 Classes to serialize the RESTful representation of Deis API models. 3 """ 4 5 from __future__ import unicode_literals 6 7 import json 8 import re 9 10 from django.conf import settings 11 from django.contrib.auth.models import User 12 from django.utils import timezone 13 from rest_framework import serializers 14 from rest_framework.validators import UniqueTogetherValidator 15 16 from api import models 17 18 19 PROCTYPE_MATCH = re.compile(r'^(?P<type>[a-z]+)') 20 MEMLIMIT_MATCH = re.compile(r'^(?P<mem>[0-9]+(MB|KB|GB|[BKMG]))$', re.IGNORECASE) 21 CPUSHARE_MATCH = re.compile(r'^(?P<cpu>[0-9]+)$') 22 TAGKEY_MATCH = re.compile(r'^[a-z]+$') 23 TAGVAL_MATCH = re.compile(r'^\w+$') 24 25 26 class JSONFieldSerializer(serializers.Field): 27 """ 28 A Django REST framework serializer for JSON data. 29 """ 30 31 def to_representation(self, obj): 32 """Serialize the field's JSON data, for read operations.""" 33 return obj 34 35 def to_internal_value(self, data): 36 """Deserialize the field's JSON data, for write operations.""" 37 try: 38 val = json.loads(data) 39 except TypeError: 40 val = data 41 return val 42 43 44 class JSONIntFieldSerializer(JSONFieldSerializer): 45 """ 46 A JSON serializer that coerces its data to integers. 47 """ 48 49 def to_internal_value(self, data): 50 """Deserialize the field's JSON integer data.""" 51 field = super(JSONIntFieldSerializer, self).to_internal_value(data) 52 53 for k, v in field.viewitems(): 54 if v is not None: # NoneType is used to unset a value 55 try: 56 field[k] = int(v) 57 except ValueError: 58 field[k] = v 59 # Do nothing, the validator will catch this later 60 return field 61 62 63 class JSONStringFieldSerializer(JSONFieldSerializer): 64 """ 65 A JSON serializer that coerces its data to strings. 66 """ 67 68 def to_internal_value(self, data): 69 """Deserialize the field's JSON string data.""" 70 field = super(JSONStringFieldSerializer, self).to_internal_value(data) 71 72 for k, v in field.viewitems(): 73 if v is not None: # NoneType is used to unset a value 74 field[k] = unicode(v) 75 76 return field 77 78 79 class ModelSerializer(serializers.ModelSerializer): 80 81 uuid = serializers.ReadOnlyField() 82 83 def get_validators(self): 84 """ 85 Hack to remove DRF's UniqueTogetherValidator when it concerns the UUID. 86 87 See https://github.com/deis/deis/pull/2898#discussion_r23105147 88 """ 89 validators = super(ModelSerializer, self).get_validators() 90 for v in validators: 91 if isinstance(v, UniqueTogetherValidator) and 'uuid' in v.fields: 92 validators.remove(v) 93 return validators 94 95 96 class UserSerializer(serializers.ModelSerializer): 97 class Meta: 98 model = User 99 fields = ['email', 'username', 'password', 'first_name', 'last_name', 'is_superuser', 100 'is_staff', 'groups', 'user_permissions', 'last_login', 'date_joined', 101 'is_active'] 102 read_only_fields = ['is_superuser', 'is_staff', 'groups', 103 'user_permissions', 'last_login', 'date_joined', 'is_active'] 104 extra_kwargs = {'password': {'write_only': True}} 105 106 def create(self, validated_data): 107 now = timezone.now() 108 user = User( 109 email=validated_data.get('email'), 110 username=validated_data.get('username'), 111 last_login=now, 112 date_joined=now, 113 is_active=True 114 ) 115 if validated_data.get('first_name'): 116 user.first_name = validated_data['first_name'] 117 if validated_data.get('last_name'): 118 user.last_name = validated_data['last_name'] 119 user.set_password(validated_data['password']) 120 # Make the first signup an admin / superuser 121 if not User.objects.filter(is_superuser=True).exists(): 122 user.is_superuser = user.is_staff = True 123 user.save() 124 return user 125 126 127 class AdminUserSerializer(serializers.ModelSerializer): 128 """Serialize admin status for a User model.""" 129 130 class Meta: 131 model = User 132 fields = ['username', 'is_superuser'] 133 read_only_fields = ['username'] 134 135 136 class AppSerializer(ModelSerializer): 137 """Serialize a :class:`~api.models.App` model.""" 138 139 owner = serializers.ReadOnlyField(source='owner.username') 140 structure = JSONFieldSerializer(required=False) 141 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 142 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 143 144 class Meta: 145 """Metadata options for a :class:`AppSerializer`.""" 146 model = models.App 147 fields = ['uuid', 'id', 'owner', 'url', 'structure', 'created', 'updated'] 148 read_only_fields = ['uuid'] 149 150 151 class BuildSerializer(ModelSerializer): 152 """Serialize a :class:`~api.models.Build` model.""" 153 154 app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all()) 155 owner = serializers.ReadOnlyField(source='owner.username') 156 procfile = JSONFieldSerializer(required=False) 157 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 158 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 159 160 class Meta: 161 """Metadata options for a :class:`BuildSerializer`.""" 162 model = models.Build 163 fields = ['owner', 'app', 'image', 'sha', 'procfile', 'dockerfile', 'created', 164 'updated', 'uuid'] 165 read_only_fields = ['uuid'] 166 167 168 class ConfigSerializer(ModelSerializer): 169 """Serialize a :class:`~api.models.Config` model.""" 170 171 app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all()) 172 owner = serializers.ReadOnlyField(source='owner.username') 173 values = JSONStringFieldSerializer(required=False) 174 memory = JSONStringFieldSerializer(required=False) 175 cpu = JSONIntFieldSerializer(required=False) 176 tags = JSONStringFieldSerializer(required=False) 177 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 178 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 179 180 class Meta: 181 """Metadata options for a :class:`ConfigSerializer`.""" 182 model = models.Config 183 184 def validate_memory(self, value): 185 for k, v in value.viewitems(): 186 if v is None: # use NoneType to unset a value 187 continue 188 if not re.match(PROCTYPE_MATCH, k): 189 raise serializers.ValidationError("Process types can only contain [a-z]") 190 if not re.match(MEMLIMIT_MATCH, str(v)): 191 raise serializers.ValidationError( 192 "Limit format: <number><unit>, where unit = B, K, M or G") 193 return value 194 195 def validate_cpu(self, value): 196 for k, v in value.viewitems(): 197 if v is None: # use NoneType to unset a value 198 continue 199 if not re.match(PROCTYPE_MATCH, k): 200 raise serializers.ValidationError("Process types can only contain [a-z]") 201 shares = re.match(CPUSHARE_MATCH, str(v)) 202 if not shares: 203 raise serializers.ValidationError("CPU shares must be an integer") 204 for v in shares.groupdict().viewvalues(): 205 try: 206 i = int(v) 207 except ValueError: 208 raise serializers.ValidationError("CPU shares must be an integer") 209 if i > 1024 or i < 0: 210 raise serializers.ValidationError("CPU shares must be between 0 and 1024") 211 return value 212 213 def validate_tags(self, value): 214 for k, v in value.viewitems(): 215 if v is None: # use NoneType to unset a value 216 continue 217 if not re.match(TAGKEY_MATCH, k): 218 raise serializers.ValidationError("Tag keys can only contain [a-z]") 219 if not re.match(TAGVAL_MATCH, str(v)): 220 raise serializers.ValidationError("Invalid tag value") 221 return value 222 223 224 class ReleaseSerializer(ModelSerializer): 225 """Serialize a :class:`~api.models.Release` model.""" 226 227 app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all()) 228 owner = serializers.ReadOnlyField(source='owner.username') 229 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 230 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 231 232 class Meta: 233 """Metadata options for a :class:`ReleaseSerializer`.""" 234 model = models.Release 235 236 237 class ContainerSerializer(ModelSerializer): 238 """Serialize a :class:`~api.models.Container` model.""" 239 240 app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all()) 241 owner = serializers.ReadOnlyField(source='owner.username') 242 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 243 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 244 release = serializers.SerializerMethodField() 245 246 class Meta: 247 """Metadata options for a :class:`ContainerSerializer`.""" 248 model = models.Container 249 fields = ['owner', 'app', 'release', 'type', 'num', 'state', 'created', 'updated', 'uuid'] 250 251 def get_release(self, obj): 252 return "v{}".format(obj.release.version) 253 254 255 class KeySerializer(ModelSerializer): 256 """Serialize a :class:`~api.models.Key` model.""" 257 258 owner = serializers.ReadOnlyField(source='owner.username') 259 fingerprint = serializers.CharField(read_only=True) 260 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 261 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 262 263 class Meta: 264 """Metadata options for a KeySerializer.""" 265 model = models.Key 266 267 268 class DomainSerializer(ModelSerializer): 269 """Serialize a :class:`~api.models.Domain` model.""" 270 271 app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all()) 272 owner = serializers.ReadOnlyField(source='owner.username') 273 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 274 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 275 276 class Meta: 277 """Metadata options for a :class:`DomainSerializer`.""" 278 model = models.Domain 279 fields = ['uuid', 'owner', 'created', 'updated', 'app', 'domain'] 280 281 def validate_domain(self, value): 282 """ 283 Check that the hostname is valid 284 """ 285 if len(value) > 255: 286 raise serializers.ValidationError('Hostname must be 255 characters or less.') 287 if value[-1:] == ".": 288 value = value[:-1] # strip exactly one dot from the right, if present 289 labels = value.split('.') 290 if 'xip.io' in value: 291 return value 292 if labels[0] == '*': 293 raise serializers.ValidationError( 294 'Adding a wildcard subdomain is currently not supported.') 295 allowed = re.compile("^(?!-)[a-z0-9-]{1,63}(?<!-)$", re.IGNORECASE) 296 for label in labels: 297 match = allowed.match(label) 298 if not match or '--' in label or label.isdigit() or \ 299 len(labels) == 1 and any(char.isdigit() for char in label): 300 raise serializers.ValidationError('Hostname does not look valid.') 301 if models.Domain.objects.filter(domain=value).exists(): 302 raise serializers.ValidationError( 303 "The domain {} is already in use by another app".format(value)) 304 return value 305 306 307 class CertificateSerializer(ModelSerializer): 308 """Serialize a :class:`~api.models.Cert` model.""" 309 310 owner = serializers.ReadOnlyField(source='owner.username') 311 expires = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 312 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 313 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 314 315 class Meta: 316 """Metadata options for a DomainCertSerializer.""" 317 model = models.Certificate 318 extra_kwargs = {'certificate': {'write_only': True}, 319 'key': {'write_only': True}, 320 'common_name': {'required': False}} 321 read_only_fields = ['expires', 'created', 'updated'] 322 323 324 class PushSerializer(ModelSerializer): 325 """Serialize a :class:`~api.models.Push` model.""" 326 327 app = serializers.SlugRelatedField(slug_field='id', queryset=models.App.objects.all()) 328 owner = serializers.ReadOnlyField(source='owner.username') 329 created = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 330 updated = serializers.DateTimeField(format=settings.DEIS_DATETIME_FORMAT, read_only=True) 331 332 class Meta: 333 """Metadata options for a :class:`PushSerializer`.""" 334 model = models.Push 335 fields = ['uuid', 'owner', 'app', 'sha', 'fingerprint', 'receive_user', 'receive_repo', 336 'ssh_connection', 'ssh_original_command', 'created', 'updated']