فهرست منبع

feat(api): add Domain.touched

Shorthand for max({RRset.touched}, self.published) of the domain's
RRsets and the domain's publication timestamp.

Domain.touched increases each time the domain's RRsets are touched,
including when deleting RRsets and when doing no-op updates.
Peter Thomassen 5 سال پیش
والد
کامیت
f16b784eaf
5فایلهای تغییر یافته به همراه32 افزوده شده و 3 حذف شده
  1. 10 0
      api/desecapi/models.py
  2. 1 1
      api/desecapi/serializers.py
  3. 2 0
      api/desecapi/tests/test_domains.py
  4. 6 1
      api/desecapi/tests/test_rrsets.py
  5. 13 1
      docs/dns/domains.rst

+ 10 - 0
api/desecapi/models.py

@@ -281,6 +281,16 @@ class Domain(ExportModelOperationsMixin('Domain'), models.Model):
             self._keys = pdns.get_keys(self)
         return self._keys
 
+    @property
+    def touched(self):
+        try:
+            rrset_touched = max(updated for updated in self.rrset_set.values_list('touched', flat=True))
+            # If the domain has not been published yet, self.published is None and max() would fail
+            return rrset_touched if not self.published else max(rrset_touched, self.published)
+        except ValueError:
+            # This can be none if the domain was never published and has no records (but there should be at least NS)
+            return self.published
+
     @property
     def is_locally_registrable(self):
         return self.parent_domain_name in settings.LOCAL_PUBLIC_SUFFIXES

+ 1 - 1
api/desecapi/serializers.py

@@ -514,7 +514,7 @@ class DomainSerializer(serializers.ModelSerializer):
 
     class Meta:
         model = models.Domain
-        fields = ('created', 'published', 'name', 'keys', 'minimum_ttl',)
+        fields = ('created', 'published', 'name', 'keys', 'minimum_ttl', 'touched',)
         read_only_fields = ('published', 'minimum_ttl',)
         extra_kwargs = {
             'name': {'trim_whitespace': False},

+ 2 - 0
api/desecapi/tests/test_domains.py

@@ -216,6 +216,8 @@ class DomainOwnerTestCase1(DomainOwnerTestCase):
             with self.assertPdnsRequests(self.requests_desec_domain_creation(name)):
                 response = self.client.post(self.reverse('v1:domain-list'), {'name': name})
                 self.assertStatus(response, status.HTTP_201_CREATED)
+                self.assertTrue(all(field in response.data for field in
+                                    ['created', 'published', 'name', 'keys', 'minimum_ttl', 'touched']))
                 self.assertEqual(len(mail.outbox), 0)
                 self.assertTrue(isinstance(response.data['keys'], list))
 

+ 6 - 1
api/desecapi/tests/test_rrsets.py

@@ -6,7 +6,7 @@ from django.core.exceptions import ValidationError
 from django.core.management import call_command
 from rest_framework import status
 
-from desecapi.models import RRset
+from desecapi.models import Domain, RRset
 from desecapi.tests.base import DesecTestCase, AuthenticatedRRSetBaseTestCase
 
 
@@ -143,6 +143,8 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
                     response = self.client.post_rr_set(domain_name=self.my_empty_domain.name, **data)
                     self.assertTrue(all(field in response.data for field in
                                         ['created', 'domain', 'subname', 'name', 'records', 'ttl', 'type', 'touched']))
+                    self.assertEqual(self.my_empty_domain.touched,
+                                     max(rrset.touched for rrset in self.my_empty_domain.rrset_set.all()))
                     self.assertStatus(response, status.HTTP_201_CREATED)
 
                 # Check for uniqueness on second attempt
@@ -336,6 +338,7 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
 
             touched_new = RRset.objects.get(domain=self.my_rr_set_domain, type='A', subname=subname).touched
             self.assertGreater(touched_new, touched_old)
+            self.assertEqual(Domain.objects.get(name=self.my_rr_set_domain.name).touched, touched_new)
 
     def test_partially_update_other_rr_sets(self):
         data = {'records': ['3.2.3.4'], 'ttl': 334}
@@ -431,6 +434,8 @@ class AuthenticatedRRSetTestCase(AuthenticatedRRSetBaseTestCase):
             with self.assertPdnsRequests(self.requests_desec_rr_sets_update(name=self.my_rr_set_domain.name)):
                 response = self.client.delete_rr_set(self.my_rr_set_domain.name, subname=subname, type_='A')
                 self.assertStatus(response, status.HTTP_204_NO_CONTENT)
+                domain = Domain.objects.get(name=self.my_rr_set_domain.name)
+                self.assertEqual(domain.touched, domain.published)
 
             response = self.client.delete_rr_set(self.my_rr_set_domain.name, subname=subname, type_='A')
             self.assertStatus(response, status.HTTP_204_NO_CONTENT)

+ 13 - 1
docs/dns/domains.rst

@@ -34,7 +34,8 @@ A JSON object representing a domain has the following structure::
         ],
         "minimum_ttl": 3600,
         "name": "example.com",
-        "published": "2018-09-18T17:21:38.348112Z"
+        "published": "2018-09-18T17:21:38.348112Z",
+        "touched": "2018-09-18T17:21:38.348112Z"
     }
 
 Field details:
@@ -94,6 +95,17 @@ Field details:
     point in time of the last successful write request to a domain's
     ``rrsets/`` endpoint.
 
+``touched``
+    :Access mode: read-only
+
+    Timestamp of when the domain's DNS records have last been touched. Equal to
+    the maximum of the domain's ``published`` field and all :ref:`RRset <RRset
+    object>` ``touched`` values.
+
+    This usually is the same as ``published``, unless there have been RRset
+    write operations that did not trigger publication, such as rewriting an
+    RRset with identical values.
+
 
 Creating a Domain
 ~~~~~~~~~~~~~~~~~