Skip to content

Commit 20ffec5

Browse files
committed
Add an "ALWAYSEXTEND" changetype to force RRset update even if no change.
This will change the modified_at timestamp. Signed-off-by: Miod Vallat <miod.vallat@powerdns.com>
1 parent 6919e7f commit 20ffec5

2 files changed

Lines changed: 70 additions & 11 deletions

File tree

pdns/ws-auth.cc

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2491,7 +2491,8 @@ enum changeType
24912491
DELETE, // delete complete RRset
24922492
REPLACE, // replace complete RRset
24932493
PRUNE, // remove single record from RRset if found
2494-
EXTEND // add single record to RRset if not found
2494+
EXTEND, // add single record to RRset if not found
2495+
ALWAYSEXTEND, // add single record to RRset if not found, and always rewrite the RRset
24952496
};
24962497

24972498
// Validate the "changetype" field of a Json patch record.
@@ -2511,6 +2512,9 @@ static changeType validateChangeType(const std::string& changetype)
25112512
if (changetype == "EXTEND") {
25122513
return EXTEND;
25132514
}
2515+
if (changetype == "ALWAYSEXTEND") {
2516+
return ALWAYSEXTEND;
2517+
}
25142518
throw ApiException("Changetype '" + changetype + "' is not a valid value");
25152519
}
25162520

@@ -2667,19 +2671,19 @@ static applyResult applyReplace(const DomainInfo& domainInfo, const ZoneName& zo
26672671
return SUCCESS;
26682672
}
26692673

2670-
// Apply a PRUNE or EXTEND changetype.
2674+
// Apply a PRUNE, EXTEND or ALWAYSEXTEND changetype.
26712675
static applyResult applyPruneOrExtend(const DomainInfo& domainInfo, const ZoneName& zonename, const Json& container, DNSName& qname, QType& qtype, bool allowUnderscores, soaEditSettings& soa, HttpResponse* resp, changeType operationType, std::vector<DNSResourceRecord>& rrset)
26722676
{
26732677
if (!container["records"].is_array()) {
2674-
throw ApiException("No record provided for PRUNE or EXTEND operation");
2678+
throw ApiException("No record provided for PRUNE, EXTEND or ALWAYSEXTEND operation");
26752679
}
26762680

26772681
try {
26782682
vector<DNSResourceRecord> new_records;
26792683
uint32_t ttl = uintFromJson(container, "ttl");
26802684
gatherRecords(container, qname, qtype, ttl, new_records);
26812685
if (new_records.size() != 1) {
2682-
throw ApiException("Exactly one record should be provided for PRUNE or EXTEND operation");
2686+
throw ApiException("Exactly one record should be provided for PRUNE, EXTEND or ALWAYSEXTEND operation");
26832687
}
26842688

26852689
auto& new_record = new_records.front();
@@ -2703,10 +2707,15 @@ static applyResult applyPruneOrExtend(const DomainInfo& domainInfo, const ZoneNa
27032707
}
27042708
}
27052709
// Add new record to RRset if not found.
2706-
if (operationType == EXTEND && !seenRecord) {
2710+
if (operationType != PRUNE && !seenRecord) {
27072711
rrset.emplace_back(new_record);
27082712
}
2709-
bool submitChanges = (operationType == EXTEND && !seenRecord) || (operationType == PRUNE && seenRecord);
2713+
// clang-format off
2714+
bool submitChanges =
2715+
operationType == ALWAYSEXTEND ||
2716+
(operationType == EXTEND && !seenRecord) ||
2717+
(operationType == PRUNE && seenRecord);
2718+
// clang-format on
27102719
if (!submitChanges) {
27112720
return NOP;
27122721
}
@@ -2741,7 +2750,7 @@ static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInf
27412750
domainInfo.backend->getDomainMetadataOne(zonename, "SOA-EDIT", soa.edit_kind);
27422751
bool allowUnderscores = areUnderscoresAllowed(zonename, *domainInfo.backend);
27432752

2744-
// For PRUNE and EXTEND operations, we are not being passed the complete
2753+
// For PRUNE and *EXTEND operations, we are not being passed the complete
27452754
// RRset, and will need to fetch it from the backend. But we may have
27462755
// processed a DELETE or REPLACE operation for the same RRset first, in
27472756
// which case we can't assume querying the backend will be consistent with
@@ -2750,7 +2759,7 @@ static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInf
27502759
// To be sure to work on consistent contents, without having to rely upon
27512760
// specific backend behaviour, we will need to cache the RRset values
27522761
// in this routine, but we only need to do that for RRset which are
2753-
// subject to both PRUNE/EXTEND and DELETE/REPLACE operation.
2762+
// subject to both PRUNE/*EXTEND and DELETE/REPLACE operation.
27542763
// That first pass over the change requests computes this (and also
27552764
// performs basic validation).
27562765
using key = std::pair<DNSName, QType>;
@@ -2782,7 +2791,7 @@ static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInf
27822791
if (auto iter = changes.find(currentKey); iter != changes.end()) {
27832792
auto operations = iter->second;
27842793
// Only allow one DELETE or REPLACE operation per RRset. On the other
2785-
// hand, it makes sense to allow multiple PRUNE or EXTEND, since the
2794+
// hand, it makes sense to allow multiple PRUNE or *EXTEND, since the
27862795
// individual records they'll concern might differ.
27872796
if (operationType == DELETE || operationType == REPLACE) {
27882797
if ((operations & newOperation) != 0) {
@@ -2797,7 +2806,7 @@ static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInf
27972806
}
27982807

27992808
// In this second pass, we will process the changes and maintain a cache
2800-
// of the RRset subject to PRUNE/EXTEND operations.
2809+
// of the RRset subject to PRUNE/*EXTEND operations.
28012810
std::map<key, std::vector<DNSResourceRecord>> cache;
28022811
for (const auto& container : rrsets) {
28032812
string changetype = toUpper(stringFromJson(container, "changetype"));
@@ -2810,7 +2819,7 @@ static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInf
28102819
bool cacheNeeded{false};
28112820
if (auto iter = changes.find(currentKey); iter != changes.end()) {
28122821
auto operations = iter->second;
2813-
cacheNeeded = (operations & ((1U << PRUNE) | (1U << EXTEND))) != 0;
2822+
cacheNeeded = (operations & ((1U << PRUNE) | (1U << EXTEND) | (1U << ALWAYSEXTEND))) != 0;
28142823
}
28152824

28162825
applyResult result{ABORT};
@@ -2824,6 +2833,7 @@ static void patchZone(UeberBackend& backend, const ZoneName& zonename, DomainInf
28242833
break;
28252834
case PRUNE:
28262835
case EXTEND:
2836+
case ALWAYSEXTEND:
28272837
// First, obtain the current RRset, either from the backend or from
28282838
// our local cache if we already did some operations.
28292839
if (const auto iter = cache.find(currentKey); iter != cache.end()) {

regression-tests.api/test_Zones.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1949,6 +1949,55 @@ def test_zone_rr_update_with_everything(self):
19491949
self.assertIn(a2, records)
19501950
self.assertIn(a4, records)
19511951

1952+
@unittest.skipIf(not is_auth_lmdb(), "No rrset timestamps except with LMDB")
1953+
def test_zone_rr_update_with_alwaysextend(self):
1954+
name, payload, zone = self.create_zone()
1955+
# add a record
1956+
rec = {"content": "1.2.3.4", "disabled": False}
1957+
rrset = {"changetype": "alwaysextend", "name": "a." + name, "type": "A", "ttl": 3600, "records": [rec]}
1958+
payload = {"rrsets": [rrset]}
1959+
r = self.session.patch(
1960+
self.url("/api/v1/servers/localhost/zones/" + name),
1961+
data=json.dumps(payload),
1962+
headers={"content-type": "application/json"},
1963+
)
1964+
self.assert_success(r)
1965+
data = self.get_zone(name)
1966+
self.assertEqual(get_rrset(data, "a." + name, "A")["records"], rrset["records"])
1967+
# reload the zone because get_rrset above has removed the timestamps
1968+
data = self.get_zone(name)
1969+
# force update of the record with a different timestamp
1970+
time.sleep(1)
1971+
rrset = {"changetype": "alwaysextend", "name": "a." + name, "type": "A", "ttl": 3600, "records": [rec]}
1972+
payload = {"rrsets": [rrset]}
1973+
r = self.session.patch(
1974+
self.url("/api/v1/servers/localhost/zones/" + name),
1975+
data=json.dumps(payload),
1976+
headers={"content-type": "application/json"},
1977+
)
1978+
self.assert_success(r)
1979+
# verify the zone contents
1980+
data1 = self.get_zone(name)
1981+
# not using get_rrset() here so as NOT to remove timestamps
1982+
before = None
1983+
for rrset in data["rrsets"]:
1984+
if rrset["name"] == "a." + name:
1985+
before = rrset["records"]
1986+
break
1987+
after = None
1988+
for rrset in data1["rrsets"]:
1989+
if rrset["name"] == "a." + name:
1990+
after = rrset["records"]
1991+
break
1992+
self.assertEqual(len(before), 1)
1993+
self.assertEqual(len(after), 1)
1994+
before = before[0]
1995+
after = after[0]
1996+
self.assertNotEqual(before["modified_at"], after["modified_at"])
1997+
del before["modified_at"]
1998+
del after["modified_at"]
1999+
self.assertEqual(before, after)
2000+
19522001
def test_zone_disable_reenable(self):
19532002
# This also tests that SOA-EDIT-API works.
19542003
name, payload, zone = self.create_zone(soa_edit_api="EPOCH")

0 commit comments

Comments
 (0)