zonesync 0.12.1 → 0.13.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 +0 -12
- data/lib/zonesync/cli.rb +1 -7
- data/lib/zonesync/cloudflare/proxied_support.rb +61 -0
- data/lib/zonesync/cloudflare.rb +48 -35
- data/lib/zonesync/diff.rb +1 -9
- data/lib/zonesync/errors.rb +5 -39
- data/lib/zonesync/generate.rb +1 -5
- data/lib/zonesync/http.rb +10 -21
- data/lib/zonesync/logger.rb +2 -7
- data/lib/zonesync/manifest.rb +7 -24
- data/lib/zonesync/parser.rb +3 -5
- data/lib/zonesync/provider.rb +19 -51
- data/lib/zonesync/record.rb +6 -18
- data/lib/zonesync/record_hash.rb +3 -6
- data/lib/zonesync/route53.rb +9 -24
- data/lib/zonesync/sync.rb +2 -6
- data/lib/zonesync/validator.rb +4 -17
- data/lib/zonesync/version.rb +1 -1
- data/lib/zonesync/zonefile.rb +2 -10
- data/lib/zonesync.rb +7 -17
- data/zonesync.gemspec +0 -3
- metadata +6 -100
- data/sorbet/config +0 -4
- data/sorbet/rbi/annotations/.gitattributes +0 -1
- data/sorbet/rbi/annotations/activesupport.rbi +0 -457
- data/sorbet/rbi/annotations/minitest.rbi +0 -119
- data/sorbet/rbi/annotations/webmock.rbi +0 -9
- data/sorbet/rbi/gems/.gitattributes +0 -1
- data/sorbet/rbi/gems/activesupport@8.0.1.rbi +0 -18474
- data/sorbet/rbi/gems/addressable@2.8.7.rbi +0 -1994
- data/sorbet/rbi/gems/base64@0.2.0.rbi +0 -507
- data/sorbet/rbi/gems/benchmark@0.4.0.rbi +0 -618
- data/sorbet/rbi/gems/bigdecimal@3.1.9.rbi +0 -9
- data/sorbet/rbi/gems/concurrent-ruby@1.3.4.rbi +0 -11645
- data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +0 -9
- data/sorbet/rbi/gems/crack@1.0.0.rbi +0 -145
- data/sorbet/rbi/gems/date@3.4.1.rbi +0 -75
- data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +0 -1131
- data/sorbet/rbi/gems/drb@2.2.1.rbi +0 -1347
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +0 -155
- data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +0 -353
- data/sorbet/rbi/gems/i18n@1.14.6.rbi +0 -2275
- data/sorbet/rbi/gems/io-console@0.8.0.rbi +0 -9
- data/sorbet/rbi/gems/logger@1.6.4.rbi +0 -940
- data/sorbet/rbi/gems/minitest@5.25.4.rbi +0 -1547
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +0 -159
- data/sorbet/rbi/gems/parallel@1.26.3.rbi +0 -291
- data/sorbet/rbi/gems/polyglot@0.3.5.rbi +0 -42
- data/sorbet/rbi/gems/prism@1.3.0.rbi +0 -40040
- data/sorbet/rbi/gems/psych@5.2.2.rbi +0 -1785
- data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +0 -936
- data/sorbet/rbi/gems/rake@13.2.1.rbi +0 -3028
- data/sorbet/rbi/gems/rbi@0.2.2.rbi +0 -4527
- data/sorbet/rbi/gems/rdoc@6.10.0.rbi +0 -12766
- data/sorbet/rbi/gems/reline@0.6.0.rbi +0 -9
- data/sorbet/rbi/gems/rexml@3.4.0.rbi +0 -4974
- data/sorbet/rbi/gems/rspec-core@3.13.2.rbi +0 -10896
- data/sorbet/rbi/gems/rspec-expectations@3.13.3.rbi +0 -8183
- data/sorbet/rbi/gems/rspec-mocks@3.13.2.rbi +0 -5341
- data/sorbet/rbi/gems/rspec-support@3.13.2.rbi +0 -1630
- data/sorbet/rbi/gems/rspec@3.13.0.rbi +0 -83
- data/sorbet/rbi/gems/securerandom@0.4.1.rbi +0 -75
- data/sorbet/rbi/gems/spoom@1.5.0.rbi +0 -4932
- data/sorbet/rbi/gems/stringio@3.1.2.rbi +0 -9
- data/sorbet/rbi/gems/tapioca@0.16.6.rbi +0 -3611
- data/sorbet/rbi/gems/thor@1.3.2.rbi +0 -4378
- data/sorbet/rbi/gems/treetop@1.6.12.rbi +0 -1895
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +0 -5918
- data/sorbet/rbi/gems/uri@1.0.2.rbi +0 -2340
- data/sorbet/rbi/gems/webmock@3.24.0.rbi +0 -1780
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +0 -435
- data/sorbet/rbi/gems/yard@0.9.37.rbi +0 -18379
- data/sorbet/rbi/todo.rbi +0 -7
- data/sorbet/tapioca/config.yml +0 -13
- data/sorbet/tapioca/require.rb +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d52bed9a567bd04886887c0846ca81a74086ca75e56636ccfd90ec16ca0a113f
|
|
4
|
+
data.tar.gz: 7561f624f4360a4977a59941483f917832ab126f48843915216af9a51d0904c6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b187df8cf8cce75a84187704c069e12b9d10d3e830f42065f26ab0d223d85ca79889c9b9f6b6d0edebd486fba38184ce5ad7047aa26759d39b6715ca9f1f785
|
|
7
|
+
data.tar.gz: 021437d1e18832be6dbc131a60a47c7672d5018d01588a3f6de00cff2571948ca265a3f40b8a4185f6362c054d5f35471b876fabee3bb7da9dc2623c351db7b5
|
data/CLAUDE.md
CHANGED
|
@@ -21,10 +21,6 @@ Zonesync is a Ruby gem that synchronizes DNS zone files with DNS providers (Clou
|
|
|
21
21
|
- `bundle exec zonesync --force` - Force sync ignoring checksum mismatches
|
|
22
22
|
- `bundle exec zonesync generate` - Generate Zonefile from DNS provider
|
|
23
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
24
|
## Architecture
|
|
29
25
|
|
|
30
26
|
### Core Components
|
|
@@ -73,14 +69,6 @@ Credentials stored in Rails-style encrypted configuration:
|
|
|
73
69
|
- **Unit specs** test individual classes and methods
|
|
74
70
|
- All tests should pass before committing changes
|
|
75
71
|
|
|
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
72
|
## Error Handling
|
|
85
73
|
|
|
86
74
|
Custom exceptions in `lib/zonesync/errors.rb`:
|
data/lib/zonesync/cli.rb
CHANGED
|
@@ -1,19 +1,15 @@
|
|
|
1
|
-
#
|
|
2
|
-
require "sorbet-runtime"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "thor"
|
|
5
4
|
|
|
6
5
|
module Zonesync
|
|
7
6
|
class CLI < Thor
|
|
8
|
-
extend T::Sig
|
|
9
|
-
|
|
10
7
|
default_command :sync
|
|
11
8
|
desc "sync --source=Zonefile --destination=zonesync", "syncs the contents of the Zonefile to the DNS server configured in Rails.application.credentials.zonesync"
|
|
12
9
|
option :source, default: "Zonefile", desc: "path to the zonefile"
|
|
13
10
|
option :destination, default: "zonesync", desc: "key to the DNS server configuration in Rails.application.credentials"
|
|
14
11
|
method_option :dry_run, type: :boolean, default: false, aliases: :n, desc: "log operations to STDOUT but don't perform the sync"
|
|
15
12
|
method_option :force, type: :boolean, default: false, desc: "ignore checksum mismatches and force the sync"
|
|
16
|
-
sig { void }
|
|
17
13
|
def sync
|
|
18
14
|
kwargs = options.to_hash.transform_keys(&:to_sym)
|
|
19
15
|
Zonesync.call(**kwargs)
|
|
@@ -25,13 +21,11 @@ module Zonesync
|
|
|
25
21
|
desc "generate --source=zonesync --destination=Zonefile", "generates a Zonefile from the DNS server configured in Rails.application.credentials.zonesync"
|
|
26
22
|
option :source, default: "zonesync", desc: "key to the DNS server configuration in Rails.application.credentials"
|
|
27
23
|
option :destination, default: "Zonefile", desc: "path to the zonefile"
|
|
28
|
-
sig { void }
|
|
29
24
|
def generate
|
|
30
25
|
kwargs = options.to_hash.transform_keys(&:to_sym)
|
|
31
26
|
Zonesync.generate(**kwargs)
|
|
32
27
|
end
|
|
33
28
|
|
|
34
|
-
sig { returns(TrueClass) }
|
|
35
29
|
def self.exit_on_failure? = true
|
|
36
30
|
end
|
|
37
31
|
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Zonesync
|
|
4
|
+
class Cloudflare < Provider
|
|
5
|
+
# Module that adds proxied support to individual Record instances.
|
|
6
|
+
# When extended onto a record, it parses cf_tags=cf-proxied:true/false
|
|
7
|
+
# from the comment and provides a proxied accessor.
|
|
8
|
+
#
|
|
9
|
+
# Semantics:
|
|
10
|
+
# - cf_tags=cf-proxied:true → explicitly enable Cloudflare proxy
|
|
11
|
+
# - cf_tags=cf-proxied:false → explicitly disable Cloudflare proxy
|
|
12
|
+
# - No cf_tags → don't touch proxied (use Cloudflare default)
|
|
13
|
+
module ProxiedSupport
|
|
14
|
+
CF_TAGS_PATTERN = /\bcf_tags=cf-proxied:(true|false)\b/
|
|
15
|
+
|
|
16
|
+
def self.extended(record)
|
|
17
|
+
record.instance_variable_set :@original_comment, record[:comment]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def proxied
|
|
21
|
+
@original_comment&.match(CF_TAGS_PATTERN) { |m| m[1] == "true" }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def comment
|
|
25
|
+
return @original_comment unless @original_comment&.match?(CF_TAGS_PATTERN)
|
|
26
|
+
cleaned = @original_comment.sub(/\s*#{CF_TAGS_PATTERN}\s*/, " ").strip
|
|
27
|
+
cleaned.empty? ? nil : cleaned
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_h
|
|
31
|
+
super.merge(comment: comment, proxied: proxied)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def ==(other)
|
|
35
|
+
other_proxied = other.respond_to?(:proxied) ? other.proxied : nil
|
|
36
|
+
name == other.name &&
|
|
37
|
+
type == other.type &&
|
|
38
|
+
ttl == other.ttl &&
|
|
39
|
+
rdata == other.rdata &&
|
|
40
|
+
comment == other.comment &&
|
|
41
|
+
proxied == other_proxied
|
|
42
|
+
end
|
|
43
|
+
alias eql? ==
|
|
44
|
+
|
|
45
|
+
def hash
|
|
46
|
+
[name, type, ttl, rdata, comment, proxied].hash
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_s
|
|
50
|
+
string = [name, ttl, type, rdata].join(" ")
|
|
51
|
+
|
|
52
|
+
comment_parts = []
|
|
53
|
+
comment_parts << "cf_tags=cf-proxied:true" if proxied == true
|
|
54
|
+
comment_parts << comment if comment
|
|
55
|
+
|
|
56
|
+
string << " ; #{comment_parts.join(' ')}" if comment_parts.any?
|
|
57
|
+
string
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
data/lib/zonesync/cloudflare.rb
CHANGED
|
@@ -1,31 +1,42 @@
|
|
|
1
|
-
#
|
|
2
|
-
require "sorbet-runtime"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "zonesync/record"
|
|
5
4
|
require "zonesync/http"
|
|
5
|
+
require "zonesync/cloudflare/proxied_support"
|
|
6
6
|
|
|
7
7
|
module Zonesync
|
|
8
8
|
class Cloudflare < Provider
|
|
9
|
-
sig { returns(String) }
|
|
10
9
|
def read
|
|
11
|
-
records = [fake_soa] + all.keys
|
|
10
|
+
records = [fake_soa.extend(ProxiedSupport)] + all.keys
|
|
12
11
|
records.map(&:to_s).join("\n") + "\n"
|
|
13
12
|
end
|
|
14
13
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
def diff(other)
|
|
15
|
+
source_records = other.diffable_records.map { |r| r.extend(ProxiedSupport) }
|
|
16
|
+
Diff.new(
|
|
17
|
+
from: diffable_records,
|
|
18
|
+
to: source_records,
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def remove(record)
|
|
23
|
+
id = find_record_id(record)
|
|
18
24
|
http.delete("/#{id}")
|
|
19
25
|
end
|
|
20
26
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
id = all.fetch(old_record)
|
|
27
|
+
def change(old_record, new_record)
|
|
28
|
+
id = find_record_id(old_record)
|
|
24
29
|
http.patch("/#{id}", to_hash(new_record))
|
|
25
30
|
end
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
def find_record_id(record)
|
|
33
|
+
all.each do |existing, id|
|
|
34
|
+
return id if existing.identical_to?(record)
|
|
35
|
+
end
|
|
36
|
+
raise KeyError, "record not found: #{record.inspect}"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def add(record)
|
|
29
40
|
add_with_duplicate_handling(record) do
|
|
30
41
|
begin
|
|
31
42
|
http.post("", to_hash(record))
|
|
@@ -41,67 +52,71 @@ module Zonesync
|
|
|
41
52
|
end
|
|
42
53
|
end
|
|
43
54
|
|
|
44
|
-
sig { returns(T::Hash[Record, String]) }
|
|
45
55
|
def all
|
|
46
56
|
response = http.get("")
|
|
47
57
|
response["result"].reduce({}) do |map, attrs|
|
|
48
|
-
map.merge
|
|
58
|
+
map.merge(to_record(attrs) => attrs["id"])
|
|
49
59
|
end
|
|
50
60
|
end
|
|
51
61
|
|
|
52
62
|
private
|
|
53
63
|
|
|
54
|
-
|
|
55
|
-
def to_hash record
|
|
64
|
+
def to_hash(record)
|
|
56
65
|
hash = record.to_h
|
|
57
66
|
content = hash.delete(:rdata)
|
|
67
|
+
proxied = hash.delete(:proxied)
|
|
58
68
|
|
|
59
69
|
if record.type == "MX"
|
|
60
70
|
# For MX records, split "priority hostname" into separate fields
|
|
61
|
-
priority, hostname =
|
|
71
|
+
priority, hostname = content.split(" ", 2)
|
|
62
72
|
hash[:priority] = priority.to_i
|
|
63
73
|
hash[:content] = hostname.sub(/\.$/, "") # remove trailing dot
|
|
64
74
|
else
|
|
65
75
|
hash[:content] = content
|
|
66
76
|
end
|
|
67
77
|
|
|
78
|
+
hash[:proxied] = proxied if proxied != nil
|
|
68
79
|
hash[:comment] = hash.delete(:comment) # maintain original order
|
|
69
80
|
hash
|
|
70
81
|
end
|
|
71
82
|
|
|
72
|
-
|
|
73
|
-
def to_record attrs
|
|
83
|
+
def to_record(attrs)
|
|
74
84
|
rdata = attrs["content"]
|
|
75
85
|
if %w[CNAME MX].include?(attrs["type"])
|
|
76
|
-
rdata = normalize_trailing_period(
|
|
86
|
+
rdata = normalize_trailing_period(rdata)
|
|
77
87
|
end
|
|
78
88
|
if attrs["type"] == "MX"
|
|
79
89
|
rdata = "#{attrs["priority"]} #{rdata}"
|
|
80
90
|
end
|
|
81
91
|
if %w[TXT SPF NAPTR].include?(attrs["type"])
|
|
82
|
-
rdata = normalize_quoting(
|
|
92
|
+
rdata = normalize_quoting(rdata)
|
|
83
93
|
end
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
|
|
95
|
+
record = Record.new(
|
|
96
|
+
name: normalize_trailing_period(attrs["name"]),
|
|
86
97
|
type: attrs["type"],
|
|
87
98
|
ttl: attrs["ttl"].to_i,
|
|
88
|
-
rdata
|
|
89
|
-
comment: attrs["comment"],
|
|
99
|
+
rdata: rdata,
|
|
100
|
+
comment: comment_with_proxied(attrs["comment"], attrs["proxied"]),
|
|
90
101
|
)
|
|
102
|
+
record.extend(ProxiedSupport)
|
|
91
103
|
end
|
|
92
104
|
|
|
93
|
-
|
|
94
|
-
|
|
105
|
+
def comment_with_proxied(comment, proxied)
|
|
106
|
+
return comment if proxied.nil?
|
|
107
|
+
cf_tag = "cf_tags=cf-proxied:#{proxied}"
|
|
108
|
+
[comment, cf_tag].compact.join(" ")
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def normalize_trailing_period(value)
|
|
95
112
|
value =~ /\.$/ ? value : value + "."
|
|
96
113
|
end
|
|
97
114
|
|
|
98
|
-
|
|
99
|
-
def normalize_quoting value
|
|
115
|
+
def normalize_quoting(value)
|
|
100
116
|
value = value =~ /^".+"$/ ? value : %("#{value}") # handle quote wrapping
|
|
101
117
|
value.gsub('" "', "") # handle multiple txt record joining
|
|
102
118
|
end
|
|
103
119
|
|
|
104
|
-
sig { returns(Zonesync::Record) }
|
|
105
120
|
def fake_soa
|
|
106
121
|
zone_name = http.get("/..")["result"]["name"]
|
|
107
122
|
Record.new(
|
|
@@ -113,11 +128,10 @@ module Zonesync
|
|
|
113
128
|
)
|
|
114
129
|
end
|
|
115
130
|
|
|
116
|
-
sig { returns(HTTP) }
|
|
117
131
|
def http
|
|
118
132
|
return @http if @http
|
|
119
|
-
@http =
|
|
120
|
-
|
|
133
|
+
@http = HTTP.new("https://api.cloudflare.com/client/v4/zones/#{config.fetch(:zone_id)}/dns_records")
|
|
134
|
+
@http.before_request do |request|
|
|
121
135
|
request["Content-Type"] = "application/json"
|
|
122
136
|
if config[:token]
|
|
123
137
|
request["Authorization"] = "Bearer #{config[:token]}"
|
|
@@ -126,8 +140,7 @@ module Zonesync
|
|
|
126
140
|
request["X-Auth-Key"] = config.fetch(:key)
|
|
127
141
|
end
|
|
128
142
|
end
|
|
129
|
-
|
|
143
|
+
@http
|
|
130
144
|
end
|
|
131
145
|
end
|
|
132
146
|
end
|
|
133
|
-
|
data/lib/zonesync/diff.rb
CHANGED
|
@@ -1,18 +1,11 @@
|
|
|
1
|
-
#
|
|
2
|
-
require "sorbet-runtime"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module Zonesync
|
|
5
|
-
Operation = T.type_alias { [Symbol, T::Array[Record]] }
|
|
6
|
-
|
|
7
4
|
Diff = Struct.new(:from, :to) do
|
|
8
|
-
extend T::Sig
|
|
9
|
-
|
|
10
|
-
sig { params(from: T::Array[Record], to: T::Array[Record]).returns(T.untyped) }
|
|
11
5
|
def self.call(from:, to:)
|
|
12
6
|
new(from, to).call
|
|
13
7
|
end
|
|
14
8
|
|
|
15
|
-
sig { returns(T::Array[[Symbol, T::Array[Record]]]) }
|
|
16
9
|
def call
|
|
17
10
|
# Group records by their primary key (name + type)
|
|
18
11
|
from_by_key = from.group_by { |r| [r.name, r.type] }
|
|
@@ -58,4 +51,3 @@ module Zonesync
|
|
|
58
51
|
end
|
|
59
52
|
end
|
|
60
53
|
end
|
|
61
|
-
|
data/lib/zonesync/errors.rb
CHANGED
|
@@ -1,44 +1,33 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
3
|
require "zonesync/record_hash"
|
|
4
4
|
|
|
5
5
|
module Zonesync
|
|
6
6
|
class ValidationError < StandardError
|
|
7
|
-
extend T::Sig
|
|
8
|
-
|
|
9
|
-
sig { void }
|
|
10
7
|
def initialize
|
|
11
|
-
@errors =
|
|
8
|
+
@errors = []
|
|
12
9
|
end
|
|
13
10
|
|
|
14
|
-
sig { params(error: StandardError).void }
|
|
15
11
|
def add(error)
|
|
16
12
|
@errors << error
|
|
17
13
|
end
|
|
18
14
|
|
|
19
|
-
sig { returns(T::Boolean) }
|
|
20
15
|
def any?
|
|
21
16
|
@errors.any?
|
|
22
17
|
end
|
|
23
18
|
|
|
24
|
-
sig { returns(String) }
|
|
25
19
|
def message
|
|
26
20
|
@errors.map(&:message).join("\n\n#{'-' * 60}\n\n")
|
|
27
21
|
end
|
|
28
22
|
|
|
29
|
-
sig { returns(T::Array[StandardError]) }
|
|
30
23
|
attr_reader :errors
|
|
31
24
|
end
|
|
32
25
|
|
|
33
26
|
class ConflictError < StandardError
|
|
34
|
-
extend T::Sig
|
|
35
|
-
|
|
36
|
-
sig { params(conflicts: T::Array[[T.nilable(Record), Record]]).void }
|
|
37
27
|
def initialize(conflicts)
|
|
38
28
|
@conflicts = conflicts
|
|
39
29
|
end
|
|
40
30
|
|
|
41
|
-
sig { returns(String) }
|
|
42
31
|
def message
|
|
43
32
|
conflicts_text = @conflicts.sort_by { |_existing, new_rec| new_rec.name }.map do |existing_rec, new_rec|
|
|
44
33
|
" existing: #{existing_rec}\n new: #{new_rec}"
|
|
@@ -56,14 +45,10 @@ module Zonesync
|
|
|
56
45
|
end
|
|
57
46
|
|
|
58
47
|
class MissingManifestError < StandardError
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
sig { params(manifest: Record).void }
|
|
62
|
-
def initialize manifest
|
|
48
|
+
def initialize(manifest)
|
|
63
49
|
@manifest = manifest
|
|
64
50
|
end
|
|
65
51
|
|
|
66
|
-
sig { returns(String) }
|
|
67
52
|
def message
|
|
68
53
|
<<~MSG
|
|
69
54
|
The zonesync_manifest TXT record is missing. If this is the very first sync, make sure the Zonefile matches what's on the DNS server exactly. Otherwise, someone else may have removed it.
|
|
@@ -73,17 +58,6 @@ module Zonesync
|
|
|
73
58
|
end
|
|
74
59
|
|
|
75
60
|
class ChecksumMismatchError < StandardError
|
|
76
|
-
extend T::Sig
|
|
77
|
-
|
|
78
|
-
sig {
|
|
79
|
-
params(
|
|
80
|
-
existing: T.nilable(Record),
|
|
81
|
-
new: T.nilable(Record),
|
|
82
|
-
expected_record: T.nilable(Record),
|
|
83
|
-
actual_record: T.nilable(Record),
|
|
84
|
-
missing_hash: T.nilable(String)
|
|
85
|
-
).void
|
|
86
|
-
}
|
|
87
61
|
def initialize(existing = nil, new = nil, expected_record: nil, actual_record: nil, missing_hash: nil)
|
|
88
62
|
@existing = existing
|
|
89
63
|
@new = new
|
|
@@ -92,7 +66,6 @@ module Zonesync
|
|
|
92
66
|
@missing_hash = missing_hash
|
|
93
67
|
end
|
|
94
68
|
|
|
95
|
-
sig { returns(String) }
|
|
96
69
|
def message
|
|
97
70
|
# V2 manifest integrity violation
|
|
98
71
|
if @missing_hash
|
|
@@ -109,7 +82,6 @@ module Zonesync
|
|
|
109
82
|
|
|
110
83
|
private
|
|
111
84
|
|
|
112
|
-
sig { returns(String) }
|
|
113
85
|
def generate_v2_message
|
|
114
86
|
if @expected_record && @actual_record
|
|
115
87
|
# Record was modified
|
|
@@ -143,23 +115,17 @@ module Zonesync
|
|
|
143
115
|
end
|
|
144
116
|
|
|
145
117
|
class DuplicateRecordError < StandardError
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
sig { params(record: Record, provider_message: T.nilable(String)).void }
|
|
149
|
-
def initialize record, provider_message = nil
|
|
118
|
+
def initialize(record, provider_message = nil)
|
|
150
119
|
@record = record
|
|
151
120
|
@provider_message = provider_message
|
|
152
121
|
end
|
|
153
122
|
|
|
154
|
-
sig { returns(String) }
|
|
155
123
|
def message
|
|
156
124
|
msg = "Record already exists: #{@record.name} #{@record.type}"
|
|
157
125
|
msg += " (#{@provider_message})" if @provider_message
|
|
158
126
|
msg
|
|
159
127
|
end
|
|
160
128
|
|
|
161
|
-
sig { returns(Record) }
|
|
162
129
|
attr_reader :record
|
|
163
130
|
end
|
|
164
131
|
end
|
|
165
|
-
|
data/lib/zonesync/generate.rb
CHANGED
data/lib/zonesync/http.rb
CHANGED
|
@@ -1,52 +1,41 @@
|
|
|
1
|
-
#
|
|
2
|
-
require "sorbet-runtime"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "net/http"
|
|
5
4
|
require "json"
|
|
6
5
|
|
|
7
6
|
module Zonesync
|
|
8
7
|
HTTP = Struct.new(:base) do
|
|
9
|
-
extend T::Sig
|
|
10
|
-
|
|
11
|
-
sig { params(base: String).void }
|
|
12
8
|
def initialize(base)
|
|
13
9
|
super
|
|
14
|
-
@before_request =
|
|
15
|
-
@after_response =
|
|
10
|
+
@before_request = []
|
|
11
|
+
@after_response = []
|
|
16
12
|
end
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
def get path
|
|
14
|
+
def get(path)
|
|
20
15
|
request("get", path)
|
|
21
16
|
end
|
|
22
17
|
|
|
23
|
-
|
|
24
|
-
def post path, body
|
|
18
|
+
def post(path, body)
|
|
25
19
|
request("post", path, body)
|
|
26
20
|
end
|
|
27
21
|
|
|
28
|
-
|
|
29
|
-
def patch path, body
|
|
22
|
+
def patch(path, body)
|
|
30
23
|
request("patch", path, body)
|
|
31
24
|
end
|
|
32
25
|
|
|
33
|
-
|
|
34
|
-
def delete path
|
|
26
|
+
def delete(path)
|
|
35
27
|
request("delete", path)
|
|
36
28
|
end
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
def before_request &block
|
|
30
|
+
def before_request(&block)
|
|
40
31
|
@before_request << block
|
|
41
32
|
end
|
|
42
33
|
|
|
43
|
-
|
|
44
|
-
def after_response &block
|
|
34
|
+
def after_response(&block)
|
|
45
35
|
@after_response << block
|
|
46
36
|
end
|
|
47
37
|
|
|
48
|
-
|
|
49
|
-
def request method, path, body=nil
|
|
38
|
+
def request(method, path, body = nil)
|
|
50
39
|
uri = URI.parse("#{base}#{path}")
|
|
51
40
|
request = Net::HTTP.const_get(method.to_s.capitalize).new(uri.path)
|
|
52
41
|
|
data/lib/zonesync/logger.rb
CHANGED
|
@@ -1,15 +1,11 @@
|
|
|
1
|
-
#
|
|
2
|
-
require "sorbet-runtime"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "logger"
|
|
5
4
|
require "fileutils"
|
|
6
5
|
|
|
7
6
|
module Zonesync
|
|
8
7
|
class Logger
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
sig { params(method: Symbol, records: T::Array[Record], dry_run: T::Boolean).void }
|
|
12
|
-
def self.log method, records, dry_run: false
|
|
8
|
+
def self.log(method, records, dry_run: false)
|
|
13
9
|
loggers = [::Logger.new(STDOUT)]
|
|
14
10
|
|
|
15
11
|
if !dry_run
|
|
@@ -26,4 +22,3 @@ module Zonesync
|
|
|
26
22
|
end
|
|
27
23
|
end
|
|
28
24
|
end
|
|
29
|
-
|
data/lib/zonesync/manifest.rb
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
#
|
|
2
|
-
require "sorbet-runtime"
|
|
1
|
+
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
require "zonesync/record"
|
|
5
4
|
require "zonesync/record_hash"
|
|
@@ -7,31 +6,24 @@ require "digest"
|
|
|
7
6
|
|
|
8
7
|
module Zonesync
|
|
9
8
|
Manifest = Struct.new(:records, :zone) do
|
|
10
|
-
|
|
11
|
-
DIFFABLE_RECORD_TYPES =
|
|
12
|
-
T.let(%w[A AAAA CNAME MX TXT SPF NAPTR PTR].sort, T::Array[String])
|
|
9
|
+
DIFFABLE_RECORD_TYPES = %w[A AAAA CNAME MX TXT SPF NAPTR PTR].sort
|
|
13
10
|
|
|
14
|
-
sig { returns(T.nilable(Zonesync::Record)) }
|
|
15
11
|
def existing
|
|
16
12
|
records.find(&:manifest?)
|
|
17
13
|
end
|
|
18
14
|
|
|
19
|
-
sig { returns(T::Boolean) }
|
|
20
15
|
def existing?
|
|
21
16
|
!!existing
|
|
22
17
|
end
|
|
23
18
|
|
|
24
|
-
sig { returns(Zonesync::Record) }
|
|
25
19
|
def generate
|
|
26
20
|
generate_v2
|
|
27
21
|
end
|
|
28
22
|
|
|
29
|
-
sig { returns(T.nilable(Zonesync::Record)) }
|
|
30
23
|
def existing_checksum
|
|
31
24
|
records.find(&:checksum?)
|
|
32
25
|
end
|
|
33
26
|
|
|
34
|
-
sig { returns(Zonesync::Record) }
|
|
35
27
|
def generate_checksum
|
|
36
28
|
input_string = diffable_records.map(&:to_s).join
|
|
37
29
|
sha256 = Digest::SHA256.hexdigest(input_string)
|
|
@@ -44,7 +36,6 @@ module Zonesync
|
|
|
44
36
|
)
|
|
45
37
|
end
|
|
46
38
|
|
|
47
|
-
sig { returns(Zonesync::Record) }
|
|
48
39
|
def generate_v2
|
|
49
40
|
hashes = diffable_records.map { |record| RecordHash.generate(record) }
|
|
50
41
|
Record.new(
|
|
@@ -56,8 +47,7 @@ module Zonesync
|
|
|
56
47
|
)
|
|
57
48
|
end
|
|
58
49
|
|
|
59
|
-
|
|
60
|
-
def diffable? record
|
|
50
|
+
def diffable?(record)
|
|
61
51
|
if existing?
|
|
62
52
|
matches?(record)
|
|
63
53
|
else
|
|
@@ -65,24 +55,21 @@ module Zonesync
|
|
|
65
55
|
end
|
|
66
56
|
end
|
|
67
57
|
|
|
68
|
-
sig { returns(T::Boolean) }
|
|
69
58
|
def v1_format?
|
|
70
59
|
return false unless existing?
|
|
71
|
-
manifest_data =
|
|
60
|
+
manifest_data = existing.rdata[1..-2]
|
|
72
61
|
# V1 format uses "TYPE:" syntax, v2 uses comma-separated hashes
|
|
73
62
|
manifest_data.include?(":") || manifest_data.include?(";")
|
|
74
63
|
end
|
|
75
64
|
|
|
76
|
-
sig { returns(T::Boolean) }
|
|
77
65
|
def v2_format?
|
|
78
66
|
return false unless existing?
|
|
79
67
|
!v1_format?
|
|
80
68
|
end
|
|
81
69
|
|
|
82
|
-
|
|
83
|
-
def matches? record
|
|
70
|
+
def matches?(record)
|
|
84
71
|
return false unless existing?
|
|
85
|
-
manifest_data =
|
|
72
|
+
manifest_data = existing.rdata[1..-2] # remove quotes
|
|
86
73
|
|
|
87
74
|
# Check if this is v2 format (comma-separated hashes) or v1 format (type:names)
|
|
88
75
|
if manifest_data.include?(";")
|
|
@@ -104,8 +91,7 @@ module Zonesync
|
|
|
104
91
|
end
|
|
105
92
|
end
|
|
106
93
|
|
|
107
|
-
|
|
108
|
-
def shorthand_for record, with_type: false
|
|
94
|
+
def shorthand_for(record, with_type: false)
|
|
109
95
|
shorthand = record.short_name(zone.origin)
|
|
110
96
|
shorthand = "#{record.type}:#{shorthand}" if with_type
|
|
111
97
|
if record.type == "MX"
|
|
@@ -116,21 +102,18 @@ module Zonesync
|
|
|
116
102
|
|
|
117
103
|
private
|
|
118
104
|
|
|
119
|
-
sig { returns(String) }
|
|
120
105
|
def generate_rdata
|
|
121
106
|
generate_manifest.map do |type, short_names|
|
|
122
107
|
"#{type}:#{short_names.join(",")}"
|
|
123
108
|
end.join(";").inspect
|
|
124
109
|
end
|
|
125
110
|
|
|
126
|
-
sig { returns(T::Array[Zonesync::Record]) }
|
|
127
111
|
def diffable_records
|
|
128
112
|
records.select do |record|
|
|
129
113
|
diffable?(record)
|
|
130
114
|
end.sort
|
|
131
115
|
end
|
|
132
116
|
|
|
133
|
-
sig { returns(T::Hash[String, T::Array[String]]) }
|
|
134
117
|
def generate_manifest
|
|
135
118
|
hash = diffable_records.reduce({}) do |hash, record|
|
|
136
119
|
hash[record.type] ||= []
|