]> git.ipfire.org Git - thirdparty/pdns.git/commitdiff
Add an optional write_unchanged argument to update RRset even if no change. 17570/head
authorMiod Vallat <miod.vallat@powerdns.com>
Fri, 12 Jun 2026 14:53:02 +0000 (16:53 +0200)
committerMiod Vallat <miod.vallat@powerdns.com>
Mon, 22 Jun 2026 13:26:42 +0000 (15:26 +0200)
This will change the modified_at timestamp.

Signed-off-by: Miod Vallat <miod.vallat@powerdns.com>
docs/http-api/openapi/authoritative-api-openapi.yaml
pdns/ws-auth.cc
regression-tests.api/test_Zones.py

index 66d2e5800f17e1e4ff893a594bc7f0d3ca612a2b..1aadf99bb53492974f7dc2f29300a1ed94d4f6ce 100644 (file)
@@ -1157,6 +1157,9 @@ components:
           description: "List of Comment. Must be empty when changetype is set to DELETE, EXTEND or PRUNE. An empty list results in deletion of all comments. modified_at is optional and defaults to the current server time."
           items:
             $ref: "#/components/schemas/Comment"
+        write_unchanged:
+          type: boolean
+          description: "In the case of a PRUNE or EXTEND operation, specifies whether a database update should be performed even if the operation does not change the RRSet, causing the modification timestamp to be updated if the backend supports it."
 
     Record:
       title: Record
index 6e91f63dd976d3fafced7ea12cbc48cfab64fccc..abcf2f9562cb2f90452a37ba66ceabb7b6f0e1c2 100644 (file)
@@ -2706,7 +2706,12 @@ static applyResult applyPruneOrExtend(const DomainInfo& domainInfo, const ZoneNa
     if (operationType == EXTEND && !seenRecord) {
       rrset.emplace_back(new_record);
     }
-    bool submitChanges = (operationType == EXTEND && !seenRecord) || (operationType == PRUNE && seenRecord);
+    // clang-format off
+    bool submitChanges =
+      boolFromJson(container, "write_unchanged", false) ||
+      (operationType == EXTEND && !seenRecord) ||
+      (operationType == PRUNE && seenRecord);
+    // clang-format on
     if (!submitChanges) {
       return NOP;
     }
index 03cf6127853db221c8c9a220cd40638e5e96ab7f..d8b3a1e75acd7862d9026dcd97f12f2a643e91cc 100644 (file)
@@ -1949,6 +1949,62 @@ $NAME$  1D  IN  SOA ns1.example.org. hostmaster.example.org. (
         self.assertIn(a2, records)
         self.assertIn(a4, records)
 
+    @unittest.skipIf(not is_auth_lmdb(), "No rrset timestamps except with LMDB")
+    def test_zone_rr_update_with_forced_extend(self):
+        name, payload, zone = self.create_zone()
+        # add a record
+        rec = {"content": "1.2.3.4", "disabled": False}
+        rrset = {"changetype": "extend", "name": "a." + name, "type": "A", "ttl": 3600, "records": [rec]}
+        payload = {"rrsets": [rrset]}
+        r = self.session.patch(
+            self.url("/api/v1/servers/localhost/zones/" + name),
+            data=json.dumps(payload),
+            headers={"content-type": "application/json"},
+        )
+        self.assert_success(r)
+        data = self.get_zone(name)
+        self.assertEqual(get_rrset(data, "a." + name, "A")["records"], rrset["records"])
+        # reload the zone because get_rrset above has removed the timestamps
+        data = self.get_zone(name)
+        # force update of the record with a different timestamp
+        time.sleep(1)
+        rrset = {
+            "changetype": "extend",
+            "name": "a." + name,
+            "type": "A",
+            "ttl": 3600,
+            "records": [rec],
+            "write_unchanged": True,
+        }
+        payload = {"rrsets": [rrset]}
+        r = self.session.patch(
+            self.url("/api/v1/servers/localhost/zones/" + name),
+            data=json.dumps(payload),
+            headers={"content-type": "application/json"},
+        )
+        self.assert_success(r)
+        # verify the zone contents
+        data1 = self.get_zone(name)
+        # not using get_rrset() here so as NOT to remove timestamps
+        before = None
+        for rrset in data["rrsets"]:
+            if rrset["name"] == "a." + name:
+                before = rrset["records"]
+                break
+        after = None
+        for rrset in data1["rrsets"]:
+            if rrset["name"] == "a." + name:
+                after = rrset["records"]
+                break
+        self.assertEqual(len(before), 1)
+        self.assertEqual(len(after), 1)
+        before = before[0]
+        after = after[0]
+        self.assertNotEqual(before["modified_at"], after["modified_at"])
+        del before["modified_at"]
+        del after["modified_at"]
+        self.assertEqual(before, after)
+
     def test_zone_disable_reenable(self):
         # This also tests that SOA-EDIT-API works.
         name, payload, zone = self.create_zone(soa_edit_api="EPOCH")