Przeglądaj źródła

feat(api): add Domain.published

This commit adds a `published` field to the Domain model which is null
by default.  We now wrap calls to pdns.set_rrsets() in Domain.publish()
which updates the timestamp in the database.

pdns is only contacted if there are any rrsets to publish, but the
timestamp is always updated, unless the user is locked.

This commit also exposes Domain.created as a read-only field.
Peter Thomassen 6 lat temu
rodzic
commit
8ddf8f0d59

+ 38 - 0
api/desecapi/migrations/0022_domain_published.py

@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.15 on 2018-09-17 15:00
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+from django.db.models import Max
+
+
+def forward_convert(apps, schema_editor):
+    # This probably could be done in a single query using
+    # https://stackoverflow.com/questions/48119049/django-using-an-annotated-aggregate-in-queryset-update/48212331#48212331
+    Domain = apps.get_model('desecapi', 'Domain')
+    RRset = apps.get_model('desecapi', 'RRset')
+    for domain in Domain.objects.all().iterator():
+        if domain.owner.locked:
+            continue
+
+        rrsets = RRset.objects.filter(domain=domain)
+        created = rrsets.aggregate(Max('created'))['created__max']
+        published = rrsets.aggregate(Max('published'))['published__max'] or created
+        # .update() operates on a queryset (not on a Model instance)
+        Domain.objects.filter(pk=domain.pk).update(published=max(created, published))
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('desecapi', '0021_tokens'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='domain',
+            name='published',
+            field=models.DateTimeField(null=True),
+        ),
+        migrations.RunPython(forward_convert, reverse_code=migrations.RunPython.noop),
+    ]

+ 18 - 11
api/desecapi/models.py

@@ -130,6 +130,7 @@ class Domain(models.Model, mixins.SetterMixin):
     created = models.DateTimeField(auto_now_add=True)
     name = models.CharField(max_length=191, unique=True)
     owner = models.ForeignKey(settings.AUTH_USER_MODEL, related_name='domains')
+    published = models.DateTimeField(null=True)
     _dirtyName = False
 
     def setter_name(self, val):
@@ -185,10 +186,8 @@ class Domain(models.Model, mixins.SetterMixin):
             # response.
             pdns.create_zone(self, settings.DEFAULT_NS)
 
-            # Import RRsets that may have been created (e.g. during lock).
-            rrsets = self.rrset_set.all()
-            if rrsets:
-                pdns.set_rrsets(self, rrsets)
+            # Send RRsets to pdns that may have been created (e.g. during lock).
+            self._publish()
 
             # Make our RRsets consistent with pdns (specifically, NS may exist)
             self.sync_from_pdns()
@@ -215,9 +214,7 @@ class Domain(models.Model, mixins.SetterMixin):
             # do this through the pdns API (not to mention doing it atomically
             # with setting the new RRsets). So for now, we have disabled RRset
             # deletion for locked accounts.
-            rrsets = self.rrset_set.all()
-            if rrsets:
-                pdns.set_rrsets(self, rrsets)
+            self._publish()
 
     @transaction.atomic
     def sync_from_pdns(self):
@@ -341,12 +338,23 @@ class Domain(models.Model, mixins.SetterMixin):
                                 for rr in rrs])
 
         # Send RRsets to pdns
-        if rrsets_for_pdns and not self.owner.locked:
-            pdns.set_rrsets(self, rrsets_for_pdns)
+        if not self.owner.locked:
+            self._publish(rrsets_for_pdns)
 
         # Return RRsets
         return list(rrsets_to_return.values())
 
+    @transaction.atomic
+    def _publish(self, rrsets = None):
+        if rrsets is None:
+            rrsets = self.rrset_set.all()
+
+        self.published = timezone.now()
+        self.save()
+
+        if rrsets:
+            pdns.set_rrsets(self, rrsets)
+
     @transaction.atomic
     def delete(self, *args, **kwargs):
         # Delete delegation for dynDNS domains (direct child of dedyn.io)
@@ -493,8 +501,7 @@ class RRset(models.Model, mixins.SetterMixin):
         # For locked users, we can't easily sync deleted RRsets to pdns later,
         # so let's forbid it for now.
         assert not self.domain.owner.locked
-        super().delete(*args, **kwargs)
-        pdns.set_rrset(self)
+        self.domain.write_rrsets({self: []})
         self._dirties = {}
 
     def save(self, *args, **kwargs):

+ 0 - 4
api/desecapi/pdns.py

@@ -128,10 +128,6 @@ def get_rrset_datas(domain):
             for rrset in get_zone(domain)['rrsets']]
 
 
-def set_rrset(rrset, notify=True):
-    return set_rrsets(rrset.domain, [rrset], notify=notify)
-
-
 def set_rrsets(domain, rrsets, notify=True):
     data = {'rrsets':
         [{'name': rrset.name, 'type': rrset.type, 'ttl': rrset.ttl,

+ 1 - 1
api/desecapi/serializers.py

@@ -206,7 +206,7 @@ class DomainSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = Domain
-        fields = ('name', 'keys')
+        fields = ('created', 'published', 'name', 'keys')
 
 
 class DonationSerializer(serializers.ModelSerializer):

+ 21 - 1
docs/domains.rst

@@ -15,6 +15,7 @@ Domain Field Reference
 A JSON object representing a domain has the following structure::
 
     {
+        "created": "2018-09-18T16:36:16.510368Z",
         "name": "example.com",
         "keys": [
             {
@@ -29,11 +30,18 @@ A JSON object representing a domain has the following structure::
                 "keytype": "csk"
             },
             ...
-        ]
+        ],
+        "published": "2018-09-18T17:21:38.348112Z"
     }
 
 Field details:
 
+``created``
+    :Access mode: read-only
+
+    Timestamp of domain creation, in ISO 8601 format (e.g.
+    ``2013-01-29T12:34:56.000000Z``).
+
 ``keys``
     :Access mode: read-only
 
@@ -62,6 +70,18 @@ Field details:
     characters as well as hyphens ``-`` and underscores ``_`` (except at the
     beginning of the name).  The maximum length is 191.
 
+``published``
+    :Access mode: read-only
+
+    Timestamp of when the domain's DNS records have last been published,
+    in ISO 8601 format (e.g. ``2013-01-29T12:34:56.000000Z``).
+
+    As we publish record modifications immediately, this indicates the
+    point in time of the last successful write request to a domain's
+    ``rrsets/`` endpoint.  Exception: If the user account is locked, record
+    changes are queued and not published immediately. In this case, the
+    ``published`` field is not updated.
+
 
 Creating a Domain
 ~~~~~~~~~~~~~~~~~

+ 3 - 1
test/e2e/schemas.js

@@ -26,8 +26,10 @@ exports.domain = {
             minItems: 1
         },
         name: { type: "string" },
+        created: { type: "string" },
+        published: { type: "string" },
     },
-    required: ["name", "keys"]
+    required: ["name", "keys", "created", "published"]
 };
 
 exports.rrset = {