zonesync 0.10.0 → 0.11.0
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 +4 -4
- data/CLAUDE.md +90 -0
- data/lib/zonesync/manifest.rb +48 -17
- data/lib/zonesync/provider.rb +25 -0
- data/lib/zonesync/record_hash.rb +15 -0
- data/lib/zonesync/route53.rb +99 -2
- data/lib/zonesync/sync.rb +18 -7
- data/lib/zonesync/validator.rb +62 -8
- data/lib/zonesync/version.rb +1 -1
- data/lib/zonesync.rb +1 -0
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e2ac350c2fe58aaed3387c001755daba2e460747b09e12ca187e3643895cfec
|
4
|
+
data.tar.gz: b9c28c96bfcb6e4964060aa6fac6b05af5533d7903dd74b76850cc1d58904158
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8861b5bbcb843c30e3017eb2989fc3391913452246d5a6d798b8ccac677430ae845f5189271a6a07d8f4b7f72f07db34153f480281a3e6d398e135c3b0b28cf1
|
7
|
+
data.tar.gz: 6a471b920e9cd971a023eca9992894c65f43ef9e6fffe9d873abb32706f265db98043fdeb7e749cd7a87feeda0faefac8bd4b1477f27059e28b8e793f3f091fe
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Project Overview
|
6
|
+
|
7
|
+
Zonesync is a Ruby gem that synchronizes DNS zone files with DNS providers (Cloudflare, Route53). It treats DNS configuration as code, enabling version control and CI/CD workflows for DNS records.
|
8
|
+
|
9
|
+
## Commands
|
10
|
+
|
11
|
+
### Development Commands
|
12
|
+
- `bundle install` - Install dependencies
|
13
|
+
- `bundle exec rake` - Run all tests (default task)
|
14
|
+
- `bundle exec rspec` - Run RSpec tests
|
15
|
+
- `bundle exec rspec spec/path/to/specific_spec.rb` - Run a single test file
|
16
|
+
- `bundle exec rspec spec/path/to/specific_spec.rb:123` - Run a specific test at line 123
|
17
|
+
|
18
|
+
### Application Commands
|
19
|
+
- `bundle exec zonesync` - Sync Zonefile to configured DNS provider
|
20
|
+
- `bundle exec zonesync --dry-run` - Preview changes without applying them
|
21
|
+
- `bundle exec zonesync --force` - Force sync ignoring checksum mismatches
|
22
|
+
- `bundle exec zonesync generate` - Generate Zonefile from DNS provider
|
23
|
+
|
24
|
+
### Type Checking (Sorbet)
|
25
|
+
- `bundle exec srb tc` - Run Sorbet type checker
|
26
|
+
- `bundle exec tapioca gem` - Generate RBI files for gems
|
27
|
+
|
28
|
+
## Architecture
|
29
|
+
|
30
|
+
### Core Components
|
31
|
+
|
32
|
+
**Provider Pattern**: Abstract base class `Provider` with concrete implementations:
|
33
|
+
- `Cloudflare` - Cloudflare DNS API integration
|
34
|
+
- `Route53` (AWS) - Route53 DNS API integration
|
35
|
+
- `Filesystem` - Local zone file operations
|
36
|
+
- `Memory` - In-memory provider for testing
|
37
|
+
|
38
|
+
**Key Classes**:
|
39
|
+
- `Sync` - Orchestrates synchronization between source and destination providers
|
40
|
+
- `Generate` - Generates zone files from DNS providers
|
41
|
+
- `Record` - Represents DNS records with type, name, content, TTL
|
42
|
+
- `Zonefile` - Parses and generates RFC-compliant zone files
|
43
|
+
- `Diff` - Calculates differences between record sets
|
44
|
+
- `Manifest` - Tracks which records are managed by zonesync
|
45
|
+
- `Validator` - Validates operations and handles conflicts
|
46
|
+
|
47
|
+
### Data Flow
|
48
|
+
|
49
|
+
1. **Sync Process**: `Zonefile` → `Provider.diff!()` → `operations[]` → `destination.apply()`
|
50
|
+
2. **Generate Process**: DNS Provider → `Zonefile.generate()` → local file
|
51
|
+
3. **Validation**: Checksum verification prevents conflicting external changes
|
52
|
+
4. **Manifest**: TXT records track zonesync-managed records vs external ones
|
53
|
+
|
54
|
+
### Safety Mechanisms
|
55
|
+
|
56
|
+
- **Checksum verification**: Detects external changes to managed records
|
57
|
+
- **Manifest tracking**: Distinguishes zonesync-managed vs external records
|
58
|
+
- **Force mode**: Bypass safety checks when needed
|
59
|
+
- **Dry-run mode**: Preview changes without applying them
|
60
|
+
|
61
|
+
### Configuration
|
62
|
+
|
63
|
+
Credentials stored in Rails-style encrypted configuration:
|
64
|
+
- `config/credentials.yml.enc` - Encrypted credentials file
|
65
|
+
- `config/master.key` - Encryption key
|
66
|
+
- `RAILS_MASTER_KEY` env var - Alternative key source
|
67
|
+
|
68
|
+
## Testing
|
69
|
+
|
70
|
+
- **RSpec** for testing framework
|
71
|
+
- **WebMock** for HTTP request stubbing
|
72
|
+
- **Feature specs** in `spec/features/` test end-to-end workflows
|
73
|
+
- **Unit specs** test individual classes and methods
|
74
|
+
- All tests should pass before committing changes
|
75
|
+
|
76
|
+
## Type Safety
|
77
|
+
|
78
|
+
Uses **Sorbet** for gradual typing:
|
79
|
+
- All files have `# typed: strict` or similar headers
|
80
|
+
- Method signatures use `sig { ... }` blocks
|
81
|
+
- `extend T::Sig` enables signature checking
|
82
|
+
- RBI files in `sorbet/rbi/` define external gem types
|
83
|
+
|
84
|
+
## Error Handling
|
85
|
+
|
86
|
+
Custom exceptions in `lib/zonesync/errors.rb`:
|
87
|
+
- `ConflictError` - Record conflicts
|
88
|
+
- `ChecksumMismatchError` - External changes detected
|
89
|
+
- `MissingManifestError` - Missing manifest record
|
90
|
+
- `DuplicateRecordError` - Duplicate record handling
|
data/lib/zonesync/manifest.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
require "sorbet-runtime"
|
3
3
|
|
4
4
|
require "zonesync/record"
|
5
|
+
require "zonesync/record_hash"
|
5
6
|
require "digest"
|
6
7
|
|
7
8
|
module Zonesync
|
@@ -22,13 +23,7 @@ module Zonesync
|
|
22
23
|
|
23
24
|
sig { returns(Zonesync::Record) }
|
24
25
|
def generate
|
25
|
-
|
26
|
-
name: "zonesync_manifest.#{zone.origin}",
|
27
|
-
type: "TXT",
|
28
|
-
ttl: 3600,
|
29
|
-
rdata: generate_rdata,
|
30
|
-
comment: nil,
|
31
|
-
)
|
26
|
+
generate_v2
|
32
27
|
end
|
33
28
|
|
34
29
|
sig { returns(T.nilable(Zonesync::Record)) }
|
@@ -49,6 +44,18 @@ module Zonesync
|
|
49
44
|
)
|
50
45
|
end
|
51
46
|
|
47
|
+
sig { returns(Zonesync::Record) }
|
48
|
+
def generate_v2
|
49
|
+
hashes = diffable_records.map { |record| RecordHash.generate(record) }
|
50
|
+
Record.new(
|
51
|
+
name: "zonesync_manifest.#{zone.origin}",
|
52
|
+
type: "TXT",
|
53
|
+
ttl: 3600,
|
54
|
+
rdata: hashes.join(',').inspect,
|
55
|
+
comment: nil,
|
56
|
+
)
|
57
|
+
end
|
58
|
+
|
52
59
|
sig { params(record: Zonesync::Record).returns(T::Boolean) }
|
53
60
|
def diffable? record
|
54
61
|
if existing?
|
@@ -58,19 +65,43 @@ module Zonesync
|
|
58
65
|
end
|
59
66
|
end
|
60
67
|
|
68
|
+
sig { returns(T::Boolean) }
|
69
|
+
def v1_format?
|
70
|
+
return false unless existing?
|
71
|
+
manifest_data = T.must(existing).rdata[1..-2]
|
72
|
+
# V1 format uses "TYPE:" syntax, v2 uses comma-separated hashes
|
73
|
+
manifest_data.include?(":") || manifest_data.include?(";")
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { returns(T::Boolean) }
|
77
|
+
def v2_format?
|
78
|
+
return false unless existing?
|
79
|
+
!v1_format?
|
80
|
+
end
|
81
|
+
|
61
82
|
sig { params(record: Zonesync::Record).returns(T::Boolean) }
|
62
83
|
def matches? record
|
63
84
|
return false unless existing?
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
85
|
+
manifest_data = T.must(existing).rdata[1..-2] # remove quotes
|
86
|
+
|
87
|
+
# Check if this is v2 format (comma-separated hashes) or v1 format (type:names)
|
88
|
+
if manifest_data.include?(";")
|
89
|
+
# V1 format: "A:@,mail;CNAME:www;MX:@ 10,@ 20"
|
90
|
+
hash = manifest_data
|
91
|
+
.split(";")
|
92
|
+
.reduce({}) do |hash, pair|
|
93
|
+
type, short_names = pair.split(":")
|
94
|
+
hash[type] = short_names.split(",")
|
95
|
+
hash
|
96
|
+
end
|
97
|
+
shorthands = hash.fetch(record.type, [])
|
98
|
+
shorthands.include?(shorthand_for(record))
|
99
|
+
else
|
100
|
+
# V2 format: "1r81el0,60oib3,ky0g92,9pp0kg"
|
101
|
+
expected_hashes = manifest_data.split(",")
|
102
|
+
record_hash = RecordHash.generate(record)
|
103
|
+
expected_hashes.include?(record_hash)
|
104
|
+
end
|
74
105
|
end
|
75
106
|
|
76
107
|
sig { params(record: Zonesync::Record, with_type: T::Boolean).returns(String) }
|
data/lib/zonesync/provider.rb
CHANGED
@@ -98,6 +98,31 @@ module Zonesync
|
|
98
98
|
return
|
99
99
|
end
|
100
100
|
end
|
101
|
+
|
102
|
+
private
|
103
|
+
|
104
|
+
sig { params(remote_records: T::Array[Record], expected_hashes: T::Array[String]).returns(T::Array[Record]) }
|
105
|
+
def hash_based_diffable_records(remote_records, expected_hashes)
|
106
|
+
require 'set'
|
107
|
+
expected_set = Set.new(expected_hashes)
|
108
|
+
found_set = Set.new
|
109
|
+
diffable = []
|
110
|
+
|
111
|
+
remote_records.each do |record|
|
112
|
+
hash = RecordHash.generate(record)
|
113
|
+
if expected_set.include?(hash)
|
114
|
+
found_set.add(hash)
|
115
|
+
diffable << record
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
missing = expected_set - found_set
|
120
|
+
if missing.any?
|
121
|
+
raise ConflictError.new(nil, diffable.first || remote_records.first)
|
122
|
+
end
|
123
|
+
|
124
|
+
diffable.sort
|
125
|
+
end
|
101
126
|
end
|
102
127
|
|
103
128
|
require "zonesync/cloudflare"
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
require "zlib"
|
4
|
+
|
5
|
+
module Zonesync
|
6
|
+
module RecordHash
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
sig { params(record: Record).returns(String) }
|
10
|
+
def self.generate(record)
|
11
|
+
identity = "#{record.name}:#{record.type}:#{record.ttl}:#{record.rdata}"
|
12
|
+
Zlib.crc32(identity).to_s(36)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
data/lib/zonesync/route53.rb
CHANGED
@@ -4,6 +4,7 @@ require "sorbet-runtime"
|
|
4
4
|
require "zonesync/record"
|
5
5
|
require "zonesync/http"
|
6
6
|
require "rexml/document"
|
7
|
+
require "erb"
|
7
8
|
|
8
9
|
module Zonesync
|
9
10
|
class Route53 < Provider
|
@@ -21,7 +22,58 @@ module Zonesync
|
|
21
22
|
|
22
23
|
sig { params(record: Record).void }
|
23
24
|
def remove(record)
|
24
|
-
|
25
|
+
if record.type == "TXT"
|
26
|
+
# Route53 requires all TXT records with the same name to be managed together
|
27
|
+
existing_txt_records = records.select do |r|
|
28
|
+
r.name == record.name && r.type == "TXT"
|
29
|
+
end
|
30
|
+
|
31
|
+
if existing_txt_records.length == 1
|
32
|
+
# Only one TXT record, delete it normally
|
33
|
+
change_record("DELETE", record)
|
34
|
+
else
|
35
|
+
# Multiple TXT records - delete all, then recreate without the removed one
|
36
|
+
remaining_txt_records = existing_txt_records.reject { |r| r == record }
|
37
|
+
|
38
|
+
# Use change_records to handle both DELETE and CREATE in one request
|
39
|
+
grouped = [
|
40
|
+
[existing_txt_records, "DELETE"],
|
41
|
+
[remaining_txt_records, "CREATE"]
|
42
|
+
]
|
43
|
+
|
44
|
+
http.post("", ERB.new(<<~XML, trim_mode: "-").result(binding))
|
45
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
46
|
+
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
47
|
+
<ChangeBatch>
|
48
|
+
<Changes>
|
49
|
+
<%- grouped.each do |records_list, action| -%>
|
50
|
+
<%- records_grouped = records_list.group_by { |r| [r.name, r.type, r.ttl] } -%>
|
51
|
+
<%- records_grouped.each do |(name, type, ttl), group_records| -%>
|
52
|
+
<Change>
|
53
|
+
<Action><%= action %></Action>
|
54
|
+
<ResourceRecordSet>
|
55
|
+
<Name><%= name %></Name>
|
56
|
+
<Type><%= type %></Type>
|
57
|
+
<TTL><%= ttl %></TTL>
|
58
|
+
<ResourceRecords>
|
59
|
+
<%- group_records.each do |group_record| -%>
|
60
|
+
<ResourceRecord>
|
61
|
+
<Value><%= group_record.rdata %></Value>
|
62
|
+
</ResourceRecord>
|
63
|
+
<%- end -%>
|
64
|
+
</ResourceRecords>
|
65
|
+
</ResourceRecordSet>
|
66
|
+
</Change>
|
67
|
+
<%- end -%>
|
68
|
+
<%- end -%>
|
69
|
+
</Changes>
|
70
|
+
</ChangeBatch>
|
71
|
+
</ChangeResourceRecordSetsRequest>
|
72
|
+
XML
|
73
|
+
end
|
74
|
+
else
|
75
|
+
change_record("DELETE", record)
|
76
|
+
end
|
25
77
|
end
|
26
78
|
|
27
79
|
sig { params(old_record: Record, new_record: Record).void }
|
@@ -34,7 +86,19 @@ module Zonesync
|
|
34
86
|
def add(record)
|
35
87
|
add_with_duplicate_handling(record) do
|
36
88
|
begin
|
37
|
-
|
89
|
+
if record.type == "TXT"
|
90
|
+
# Route53 requires all TXT records with the same name to be combined into a single record set
|
91
|
+
existing_txt_records = records.select do |r|
|
92
|
+
r.name == record.name && r.type == "TXT"
|
93
|
+
end
|
94
|
+
all_txt_records = existing_txt_records + [record]
|
95
|
+
|
96
|
+
# Use UPSERT if records already exist, CREATE if they don't
|
97
|
+
action = existing_txt_records.empty? ? "CREATE" : "UPSERT"
|
98
|
+
change_records(action, all_txt_records)
|
99
|
+
else
|
100
|
+
change_record("CREATE", record)
|
101
|
+
end
|
38
102
|
rescue RuntimeError => e
|
39
103
|
# Convert Route53-specific duplicate error to standard exception
|
40
104
|
if e.message.include?("RRSet already exists")
|
@@ -75,6 +139,39 @@ module Zonesync
|
|
75
139
|
XML
|
76
140
|
end
|
77
141
|
|
142
|
+
sig { params(action: String, records_list: T::Array[Record]).void }
|
143
|
+
def change_records(action, records_list)
|
144
|
+
# Group records by name and type to handle multiple values
|
145
|
+
grouped = records_list.group_by { |r| [r.name, r.type, r.ttl] }
|
146
|
+
|
147
|
+
http.post("", ERB.new(<<~XML, trim_mode: "-").result(binding))
|
148
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
149
|
+
<ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
150
|
+
<ChangeBatch>
|
151
|
+
<Changes>
|
152
|
+
<%- grouped.each do |(name, type, ttl), records| -%>
|
153
|
+
<Change>
|
154
|
+
<Action><%= action %></Action>
|
155
|
+
<ResourceRecordSet>
|
156
|
+
<Name><%= name %></Name>
|
157
|
+
<Type><%= type %></Type>
|
158
|
+
<TTL><%= ttl %></TTL>
|
159
|
+
<ResourceRecords>
|
160
|
+
<%- records.each do |record| -%>
|
161
|
+
<ResourceRecord>
|
162
|
+
<Value><%= record.rdata %></Value>
|
163
|
+
</ResourceRecord>
|
164
|
+
<%- end -%>
|
165
|
+
</ResourceRecords>
|
166
|
+
</ResourceRecordSet>
|
167
|
+
</Change>
|
168
|
+
<%- end -%>
|
169
|
+
</Changes>
|
170
|
+
</ChangeBatch>
|
171
|
+
</ChangeResourceRecordSetsRequest>
|
172
|
+
XML
|
173
|
+
end
|
174
|
+
|
78
175
|
sig { params(el: REXML::Element).returns(T::Array[Record]) }
|
79
176
|
def to_records(el)
|
80
177
|
el.elements.collect("ResourceRecords/ResourceRecord") do |rr|
|
data/lib/zonesync/sync.rb
CHANGED
@@ -21,13 +21,24 @@ module Zonesync
|
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
24
|
+
# Only sync checksums for v1 manifests (v2 manifests provide integrity via hashes)
|
25
|
+
source_will_generate_v2 = !source.manifest.existing? # No existing manifest = will generate v2
|
26
|
+
dest_has_checksum = destination.manifest.existing_checksum
|
27
|
+
dest_has_v1_manifest = destination.manifest.existing? && destination.manifest.v1_format?
|
28
|
+
|
29
|
+
if source_will_generate_v2 && dest_has_checksum
|
30
|
+
# Transitioning to v2: remove old checksum
|
31
|
+
operations << [:remove, [dest_has_checksum]]
|
32
|
+
elsif !source_will_generate_v2 && (source.manifest.v1_format? || dest_has_v1_manifest)
|
33
|
+
# Both source and dest use v1 format: sync checksum
|
34
|
+
schecksum = source.manifest.generate_checksum
|
35
|
+
dchecksum = destination.manifest.existing_checksum
|
36
|
+
if schecksum != dchecksum
|
37
|
+
if dchecksum
|
38
|
+
operations << [:change, [dchecksum, schecksum]]
|
39
|
+
else
|
40
|
+
operations << [:add, [schecksum]]
|
41
|
+
end
|
31
42
|
end
|
32
43
|
end
|
33
44
|
|
data/lib/zonesync/validator.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
# typed: strict
|
2
2
|
require "sorbet-runtime"
|
3
|
+
require "zonesync/record_hash"
|
3
4
|
|
4
5
|
module Zonesync
|
5
6
|
Validator = Struct.new(:operations, :destination) do
|
@@ -12,15 +13,16 @@ module Zonesync
|
|
12
13
|
|
13
14
|
sig { params(force: T::Boolean).void }
|
14
15
|
def call(force: false)
|
15
|
-
if operations.any? && !manifest.existing?
|
16
|
+
if !force && operations.any? && !manifest.existing?
|
16
17
|
raise MissingManifestError.new(manifest.generate)
|
17
18
|
end
|
18
|
-
|
19
|
+
# Only validate checksums for v1 manifests (v2 manifests provide integrity via hashes)
|
20
|
+
if !force && manifest.v1_format? && manifest.existing_checksum && manifest.existing_checksum != manifest.generate_checksum
|
19
21
|
raise ChecksumMismatchError.new(manifest.existing_checksum, manifest.generate_checksum)
|
20
22
|
end
|
21
23
|
operations.each do |method, args|
|
22
24
|
if method == :add
|
23
|
-
validate_addition args.first
|
25
|
+
validate_addition args.first, force: force
|
24
26
|
end
|
25
27
|
end
|
26
28
|
nil
|
@@ -33,13 +35,65 @@ module Zonesync
|
|
33
35
|
destination.manifest
|
34
36
|
end
|
35
37
|
|
36
|
-
sig { params(record: Record).void }
|
37
|
-
def validate_addition record
|
38
|
+
sig { params(record: Record, force: T::Boolean).void }
|
39
|
+
def validate_addition record, force: false
|
38
40
|
return if manifest.matches?(record)
|
39
|
-
|
40
|
-
|
41
|
-
|
41
|
+
return if force
|
42
|
+
|
43
|
+
# Use hash-based conflict detection when we have a v2 manifest
|
44
|
+
if manifest.existing? && !manifest.existing.rdata[1..-2].include?(";")
|
45
|
+
# V2 hash-based manifest: check for untracked records that conflict
|
46
|
+
record_hash = RecordHash.generate(record)
|
47
|
+
expected_hashes = manifest.existing.rdata[1..-2].split(",")
|
48
|
+
|
49
|
+
# Find conflicting records that would be overwritten by this addition
|
50
|
+
conflicting_record = destination.records.find do |r|
|
51
|
+
next if r.manifest? || r.checksum?
|
52
|
+
|
53
|
+
# Skip if this record is tracked in the manifest
|
54
|
+
r_hash = RecordHash.generate(r)
|
55
|
+
next if expected_hashes.include?(r_hash)
|
56
|
+
|
57
|
+
# Skip if it's exactly the same record (we're just starting to track it)
|
58
|
+
next if r.name == record.name && r.type == record.type && r.ttl == record.ttl && r.rdata == record.rdata
|
59
|
+
|
60
|
+
# Check for conflicts based on record type
|
61
|
+
case record.type
|
62
|
+
when "CNAME", "SOA"
|
63
|
+
# These types only allow one record per name
|
64
|
+
r.name == record.name && r.type == record.type
|
65
|
+
when "MX"
|
66
|
+
# MX records conflict if same name and same priority (first part of rdata)
|
67
|
+
if r.name == record.name && r.type == record.type
|
68
|
+
existing_priority = r.rdata.split(' ').first
|
69
|
+
new_priority = record.rdata.split(' ').first
|
70
|
+
existing_priority == new_priority
|
71
|
+
end
|
72
|
+
else
|
73
|
+
# For other types (A, AAAA, TXT, etc.), multiple records are allowed
|
74
|
+
# Only conflict if trying to add identical record (but we already checked that above)
|
75
|
+
false
|
76
|
+
end
|
77
|
+
end
|
78
|
+
else
|
79
|
+
# V1 name-based manifest or no manifest
|
80
|
+
if manifest.existing?
|
81
|
+
# V1 name-based manifest: use old shorthand logic
|
82
|
+
shorthand = manifest.shorthand_for(record, with_type: true)
|
83
|
+
conflicting_record = destination.records.find do |r|
|
84
|
+
manifest.shorthand_for(r, with_type: true) == shorthand
|
85
|
+
end
|
86
|
+
else
|
87
|
+
# No manifest: only conflict if exact same record exists
|
88
|
+
conflicting_record = destination.records.find do |r|
|
89
|
+
r.name == record.name &&
|
90
|
+
r.type == record.type &&
|
91
|
+
r.ttl == record.ttl &&
|
92
|
+
r.rdata == record.rdata
|
93
|
+
end
|
94
|
+
end
|
42
95
|
end
|
96
|
+
|
43
97
|
return if !conflicting_record
|
44
98
|
return if conflicting_record == record
|
45
99
|
raise Zonesync::ConflictError.new(conflicting_record, record)
|
data/lib/zonesync/version.rb
CHANGED
data/lib/zonesync.rb
CHANGED
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.
|
4
|
+
version: 0.11.0
|
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: 2025-
|
12
|
+
date: 2025-09-18 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: diff-lcs
|
@@ -148,6 +148,7 @@ extra_rdoc_files: []
|
|
148
148
|
files:
|
149
149
|
- ".envrc"
|
150
150
|
- ".rspec"
|
151
|
+
- CLAUDE.md
|
151
152
|
- Gemfile
|
152
153
|
- LICENSE.txt
|
153
154
|
- README.md
|
@@ -166,6 +167,7 @@ files:
|
|
166
167
|
- lib/zonesync/provider.rb
|
167
168
|
- lib/zonesync/rake.rb
|
168
169
|
- lib/zonesync/record.rb
|
170
|
+
- lib/zonesync/record_hash.rb
|
169
171
|
- lib/zonesync/route53.rb
|
170
172
|
- lib/zonesync/sync.rb
|
171
173
|
- lib/zonesync/validator.rb
|