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']