From: Miod Vallat Date: Fri, 12 Jun 2026 14:53:02 +0000 (+0200) Subject: Add an optional write_unchanged argument to update RRset even if no change. X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=0da35a035525afc8a6d44a87cc585f7d3aefbb5d;p=thirdparty%2Fpdns.git Add an optional write_unchanged argument to update RRset even if no change. This will change the modified_at timestamp. Signed-off-by: Miod Vallat --- diff --git a/docs/http-api/openapi/authoritative-api-openapi.yaml b/docs/http-api/openapi/authoritative-api-openapi.yaml index 66d2e5800f..1aadf99bb5 100644 --- a/docs/http-api/openapi/authoritative-api-openapi.yaml +++ b/docs/http-api/openapi/authoritative-api-openapi.yaml @@ -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 diff --git a/pdns/ws-auth.cc b/pdns/ws-auth.cc index 6e91f63dd9..abcf2f9562 100644 --- a/pdns/ws-auth.cc +++ b/pdns/ws-auth.cc @@ -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; } diff --git a/regression-tests.api/test_Zones.py b/regression-tests.api/test_Zones.py index 03cf612785..d8b3a1e75a 100644 --- a/regression-tests.api/test_Zones.py +++ b/regression-tests.api/test_Zones.py @@ -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")