zonesync 0.14.2 → 0.14.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 27283e7f0b1a63729264d144c0a5c4773005e5f97a142da59ca43bac1327122f
4
- data.tar.gz: 4179d9f774c3950247525071bd0a207af9ddd8b9a1d5049181d33b08e1edb927
3
+ metadata.gz: 413d039b491283ed46fa0b03a08a5bbd8c02489798850119b27372f8017ea30e
4
+ data.tar.gz: 2ffb50fce0dc89cbf955d5528e280a167b5170bc994d17ed7d02495a9b53e337
5
5
  SHA512:
6
- metadata.gz: 3a462d3158d821afe0c8a629b344139e9166c7d6f21844574ce6a6bdb52af66b81f5492eb97408e58c34ffd32ec583c2ba1215eced6d0fa981dde63034c8f48c
7
- data.tar.gz: cbae6f2fcdc44409118d4ccd457f7022c29037c66222fa30d6e828f3336853b6c0160d1ee8a4bd0704d3ee47dfd81d9cf5318b68d25eaf0705a638aa52a90dcb
6
+ metadata.gz: '09fe6fd394d6ae175c9c0a5325206b3fc0c5fef6523fb8802a14d6e4b90d95d5282fb58de35bc6fceac4642df87021ac352b50368a2308fc0db8c7b0e190f350'
7
+ data.tar.gz: 1cb14bf843af73c34dd1c49205438e2e62e4d8e5f810932ff410dce2e5d7ab36768ef2c429852ce7f1b858958ab958839db0a0cdabc4bb707d54ceae1f3ff02b
data/README.md CHANGED
@@ -110,6 +110,22 @@ Zonesync.call(source: "Zonefile", destination: "zonesync", dry_run: true)
110
110
  Zonesync.generate(source: "zonesync", destination: "Zonefile")
111
111
  ```
112
112
 
113
+ ### Cloudflare proxy status
114
+
115
+ When using the Cloudflare provider, you can control whether a record is proxied through Cloudflare's CDN by adding a `cf_tags` comment to the record in your zone file:
116
+
117
+ ```
118
+ www IN A 192.0.2.1 ; cf_tags=cf-proxied:true
119
+ mail IN A 192.0.2.2 ; cf_tags=cf-proxied:false
120
+ api IN A 192.0.2.3
121
+ ```
122
+
123
+ - `cf_tags=cf-proxied:true` — enable Cloudflare proxy (orange cloud)
124
+ - `cf_tags=cf-proxied:false` — explicitly disable proxy (grey cloud)
125
+ - No `cf_tags` — don't touch the proxied setting (use Cloudflare's default)
126
+
127
+ When generating a zone file with `zonesync generate`, proxied records will automatically include the `cf_tags=cf-proxied:true` tag in their comments.
128
+
113
129
  ### Managing or avoiding conflicts with other people making edits to the DNS records
114
130
 
115
131
  Zonesync writes two additional TXT records: `zonesync_manifest` and `zonesync_checksum`. These two records together try to handle the situation where someone else makes edits directly to the DNS records managed by zonesync.
@@ -23,6 +23,13 @@ module Zonesync
23
23
  operations
24
24
  end
25
25
 
26
+ def apply(operations)
27
+ operations.each do |method, records|
28
+ yield method, records
29
+ send(method, *records)
30
+ end
31
+ end
32
+
26
33
  def diff(other)
27
34
  Diff.new(
28
35
  from: diffable_records,
@@ -53,17 +53,12 @@ module Zonesync
53
53
 
54
54
  def conflicts_with?(other)
55
55
  return false unless name == other.name && type == other.type
56
+ Record.single_record_per_name?(type)
57
+ end
56
58
 
57
- case type
58
- when "CNAME", "SOA"
59
- true
60
- when "MX"
61
- existing_priority = rdata.split(' ').first
62
- new_priority = other.rdata.split(' ').first
63
- existing_priority == new_priority
64
- else
65
- false
66
- end
59
+ # A CNAME cannot coexist with any other data at its owner name.
60
+ def excludes?(other)
61
+ name == other.name && type != other.type && [type, other.type].include?("CNAME")
67
62
  end
68
63
 
69
64
  def self.single_record_per_name?(type)
@@ -33,46 +33,35 @@ module Zonesync
33
33
  *(remaining_records.any? ? [[remaining_records, "CREATE"]] : [])
34
34
  ]
35
35
 
36
- http.post("", ERB.new(<<~XML, trim_mode: "-").result(binding))
37
- <?xml version="1.0" encoding="UTF-8"?>
38
- <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
39
- <ChangeBatch>
40
- <Changes>
41
- <%- grouped.each do |records_list, action| -%>
42
- <%- records_grouped = records_list.group_by { |r| [r.name, r.type, r.ttl] } -%>
43
- <%- records_grouped.each do |(name, type, ttl), group_records| -%>
44
- <Change>
45
- <Action><%= action %></Action>
46
- <ResourceRecordSet>
47
- <Name><%= name %></Name>
48
- <Type><%= type %></Type>
49
- <TTL><%= ttl %></TTL>
50
- <ResourceRecords>
51
- <%- group_records.each do |group_record| -%>
52
- <ResourceRecord>
53
- <Value><%= rdata_for_api(group_record) %></Value>
54
- </ResourceRecord>
55
- <%- end -%>
56
- </ResourceRecords>
57
- </ResourceRecordSet>
58
- </Change>
59
- <%- end -%>
60
- <%- end -%>
61
- </Changes>
62
- </ChangeBatch>
63
- </ChangeResourceRecordSetsRequest>
64
- XML
36
+ post_changes(grouped)
65
37
 
66
38
  invalidate_cache!
67
39
  end
68
40
  end
69
41
 
42
+ def apply(operations, &block)
43
+ names = type_change_names(operations)
44
+ batched, rest = operations.partition do |method, (record)|
45
+ method != :change && names.include?(record.name)
46
+ end
47
+ batched.group_by { |_, (record)| record.name }.each do |name, ops|
48
+ ops.each(&block)
49
+ replace_at_name(name, ops)
50
+ end
51
+ super(rest, &block)
52
+ end
53
+
70
54
  def change(old_record, new_record)
71
- existing_records = records.select do |r|
72
- r.name == old_record.name && r.type == old_record.type
55
+ new_set = if old_record.manifest? || old_record.checksum?
56
+ # our metadata records are singletons, always replace
57
+ [new_record]
58
+ else
59
+ existing_records = records.select do |r|
60
+ r.name == old_record.name && r.type == old_record.type
61
+ end
62
+ (existing_records.reject { |r| r.identical_to?(old_record) } + [new_record]).uniq
73
63
  end
74
64
 
75
- new_set = (existing_records.reject { |r| r == old_record } + [new_record]).uniq
76
65
  change_records("UPSERT", new_set)
77
66
  invalidate_cache!
78
67
  end
@@ -106,6 +95,61 @@ module Zonesync
106
95
  @zonefile = nil
107
96
  end
108
97
 
98
+ def type_change_names(operations)
99
+ removed = operations.filter_map { |method, (record)| record if method == :remove }
100
+ added = operations.filter_map { |method, (record)| record if method == :add }
101
+ added.select { |a| removed.any? { |r| r.excludes?(a) } }.map(&:name).uniq
102
+ end
103
+
104
+ def replace_at_name(name, ops)
105
+ removed = ops.filter_map { |method, (record)| record if method == :remove }
106
+ added = ops.filter_map { |method, (record)| record if method == :add }
107
+
108
+ deletes, creates = [], []
109
+ (removed + added).map(&:type).uniq.each do |type|
110
+ before = records.select { |r| r.name == name && r.type == type }
111
+ after = before.reject { |r| removed.any? { |x| x.identical_to?(r) } }
112
+ after += added.select { |r| r.type == type && after.none? { |x| x.identical_to?(r) } }
113
+ deletes.concat(before)
114
+ creates.concat(after)
115
+ end
116
+
117
+ post_changes([[deletes, "DELETE"], [creates, "CREATE"]])
118
+ invalidate_cache!
119
+ end
120
+
121
+ def post_changes(grouped)
122
+ http.post("", ERB.new(<<~XML, trim_mode: "-").result(binding))
123
+ <?xml version="1.0" encoding="UTF-8"?>
124
+ <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
125
+ <ChangeBatch>
126
+ <Changes>
127
+ <%- grouped.each do |records_list, action| -%>
128
+ <%- records_grouped = records_list.group_by { |r| [r.name, r.type, r.ttl] } -%>
129
+ <%- records_grouped.each do |(name, type, ttl), group_records| -%>
130
+ <Change>
131
+ <Action><%= action %></Action>
132
+ <ResourceRecordSet>
133
+ <Name><%= name %></Name>
134
+ <Type><%= type %></Type>
135
+ <TTL><%= ttl %></TTL>
136
+ <ResourceRecords>
137
+ <%- group_records.each do |group_record| -%>
138
+ <ResourceRecord>
139
+ <Value><%= rdata_for_api(group_record) %></Value>
140
+ </ResourceRecord>
141
+ <%- end -%>
142
+ </ResourceRecords>
143
+ </ResourceRecordSet>
144
+ </Change>
145
+ <%- end -%>
146
+ <%- end -%>
147
+ </Changes>
148
+ </ChangeBatch>
149
+ </ChangeResourceRecordSetsRequest>
150
+ XML
151
+ end
152
+
109
153
  def change_record(action, record)
110
154
  http.post("", <<~XML)
111
155
  <?xml version="1.0" encoding="UTF-8"?>
data/lib/zonesync/sync.rb CHANGED
@@ -38,9 +38,10 @@ module Zonesync
38
38
  end
39
39
  end
40
40
 
41
- operations.each do |method, records|
42
- Logger.log(method, records, dry_run: dry_run)
43
- destination.send(method, *records) unless dry_run
41
+ if dry_run
42
+ operations.each { |method, records| Logger.log(method, records, dry_run: true) }
43
+ else
44
+ destination.apply(operations) { |method, records| Logger.log(method, records) }
44
45
  end
45
46
  end
46
47
  end
@@ -89,6 +89,8 @@ module Zonesync
89
89
  end
90
90
 
91
91
  def validate_addition(record, force: false)
92
+ excluded_record = find_excluded_record(record)
93
+ return [excluded_record, record] if excluded_record
92
94
  return nil if manifest.matches?(record)
93
95
  return nil if force
94
96
 
@@ -106,6 +108,13 @@ module Zonesync
106
108
  [conflicting_record, record]
107
109
  end
108
110
 
111
+ def find_excluded_record(record)
112
+ removed = operations.filter_map { |method, (r)| r if method == :remove }
113
+ destination.records.find do |r|
114
+ r.excludes?(record) && removed.none? { |x| x.identical_to?(r) }
115
+ end
116
+ end
117
+
109
118
  def find_v2_conflict(record, expected_hashes)
110
119
  destination.records.find do |r|
111
120
  next if r.manifest? || r.checksum?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zonesync
4
- VERSION = "0.14.2"
4
+ VERSION = "0.14.4"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: zonesync
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.2
4
+ version: 0.14.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Micah Geisel
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2026-02-25 00:00:00.000000000 Z
12
+ date: 2026-09-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor