zonesync 0.8.0 → 0.10.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/Gemfile +1 -0
- data/lib/zonesync/cli.rb +19 -4
- data/lib/zonesync/cloudflare.rb +64 -39
- data/lib/zonesync/diff.rb +10 -1
- data/lib/zonesync/errors.rb +35 -0
- data/lib/zonesync/generate.rb +14 -0
- data/lib/zonesync/http.rb +24 -8
- data/lib/zonesync/logger.rb +20 -15
- data/lib/zonesync/manifest.rb +21 -5
- data/lib/zonesync/parser.rb +337 -0
- data/lib/zonesync/provider.rb +80 -26
- data/lib/zonesync/record.rb +18 -23
- data/lib/zonesync/route53.rb +48 -27
- data/lib/zonesync/sync.rb +40 -0
- data/lib/zonesync/validator.rb +19 -15
- data/lib/zonesync/version.rb +1 -1
- data/lib/zonesync/zonefile.rb +22 -311
- data/lib/zonesync.rb +31 -62
- data/sorbet/config +4 -0
- data/sorbet/rbi/annotations/.gitattributes +1 -0
- data/sorbet/rbi/annotations/activesupport.rbi +457 -0
- data/sorbet/rbi/annotations/minitest.rbi +119 -0
- data/sorbet/rbi/annotations/webmock.rbi +9 -0
- data/sorbet/rbi/gems/.gitattributes +1 -0
- data/sorbet/rbi/gems/activesupport@8.0.1.rbi +18474 -0
- data/sorbet/rbi/gems/addressable@2.8.7.rbi +1994 -0
- data/sorbet/rbi/gems/base64@0.2.0.rbi +507 -0
- data/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
- data/sorbet/rbi/gems/bigdecimal@3.1.9.rbi +9 -0
- data/sorbet/rbi/gems/concurrent-ruby@1.3.4.rbi +11645 -0
- data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +9 -0
- data/sorbet/rbi/gems/crack@1.0.0.rbi +145 -0
- data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
- data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1131 -0
- data/sorbet/rbi/gems/drb@2.2.1.rbi +1347 -0
- data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
- data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +353 -0
- data/sorbet/rbi/gems/i18n@1.14.6.rbi +2275 -0
- data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
- data/sorbet/rbi/gems/logger@1.6.4.rbi +940 -0
- data/sorbet/rbi/gems/minitest@5.25.4.rbi +1547 -0
- data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
- data/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
- data/sorbet/rbi/gems/polyglot@0.3.5.rbi +42 -0
- data/sorbet/rbi/gems/prism@1.3.0.rbi +40040 -0
- data/sorbet/rbi/gems/psych@5.2.2.rbi +1785 -0
- data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +936 -0
- data/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
- data/sorbet/rbi/gems/rbi@0.2.2.rbi +4527 -0
- data/sorbet/rbi/gems/rdoc@6.10.0.rbi +12766 -0
- data/sorbet/rbi/gems/reline@0.6.0.rbi +9 -0
- data/sorbet/rbi/gems/rexml@3.4.0.rbi +4974 -0
- data/sorbet/rbi/gems/rspec-core@3.13.2.rbi +10896 -0
- data/sorbet/rbi/gems/rspec-expectations@3.13.3.rbi +8183 -0
- data/sorbet/rbi/gems/rspec-mocks@3.13.2.rbi +5341 -0
- data/sorbet/rbi/gems/rspec-support@3.13.2.rbi +1630 -0
- data/sorbet/rbi/gems/rspec@3.13.0.rbi +83 -0
- data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
- data/sorbet/rbi/gems/spoom@1.5.0.rbi +4932 -0
- data/sorbet/rbi/gems/stringio@3.1.2.rbi +9 -0
- data/sorbet/rbi/gems/tapioca@0.16.6.rbi +3611 -0
- data/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
- data/sorbet/rbi/gems/treetop@1.6.12.rbi +1895 -0
- data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5918 -0
- data/sorbet/rbi/gems/uri@1.0.2.rbi +2340 -0
- data/sorbet/rbi/gems/webmock@3.24.0.rbi +1780 -0
- data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
- data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
- data/sorbet/rbi/todo.rbi +7 -0
- data/sorbet/tapioca/config.yml +13 -0
- data/sorbet/tapioca/require.rb +4 -0
- data/zonesync.gemspec +3 -0
- metadata +100 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ed23482c3eeed63566e03e79231562447d9f63c09a2d382151d6cd3b123b3502
|
4
|
+
data.tar.gz: 45c154c9bd89d9f1ad97df54ec0c4d30ab49995716fbd3e519b4381f890c2259
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2870e1fd94f37248ddd609da2a90be47e9487f11e371043b48ed00465fbc727f69fd36614d83b8adf4ff7661bc90dfbbeb100c964c572a49b2ad97ab36b4ad97
|
7
|
+
data.tar.gz: 3dcde654cdbdcaa806734b41c99286ba4724b812c58a4b22af7232bde1e5410892edda80b4475918b512738750a44a709c87c0af4b3483d6a9cc669bc12158b5
|
data/Gemfile
CHANGED
data/lib/zonesync/cli.rb
CHANGED
@@ -1,22 +1,37 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "thor"
|
2
5
|
|
3
6
|
module Zonesync
|
4
7
|
class CLI < Thor
|
8
|
+
extend T::Sig
|
9
|
+
|
5
10
|
default_command :sync
|
6
|
-
desc "sync", "syncs the contents of Zonefile to the DNS server configured in Rails.application.credentials.zonesync"
|
11
|
+
desc "sync --source=Zonefile --destination=zonesync", "syncs the contents of the Zonefile to the DNS server configured in Rails.application.credentials.zonesync"
|
12
|
+
option :source, default: "Zonefile", desc: "path to the zonefile"
|
13
|
+
option :destination, default: "zonesync", desc: "key to the DNS server configuration in Rails.application.credentials"
|
7
14
|
method_option :dry_run, type: :boolean, default: false, aliases: :n, desc: "log operations to STDOUT but don't perform the sync"
|
15
|
+
method_option :force, type: :boolean, default: false, desc: "ignore checksum mismatches and force the sync"
|
16
|
+
sig { void }
|
8
17
|
def sync
|
9
|
-
|
18
|
+
kwargs = options.to_hash.transform_keys(&:to_sym)
|
19
|
+
Zonesync.call(**kwargs)
|
10
20
|
rescue ConflictError, MissingManifestError, ChecksumMismatchError => e
|
11
21
|
puts e.message
|
12
22
|
exit 1
|
13
23
|
end
|
14
24
|
|
15
|
-
desc "generate", "generates a Zonefile from the DNS server configured in Rails.application.credentials.zonesync"
|
25
|
+
desc "generate --source=zonesync --destination=Zonefile", "generates a Zonefile from the DNS server configured in Rails.application.credentials.zonesync"
|
26
|
+
option :source, default: "zonesync", desc: "key to the DNS server configuration in Rails.application.credentials"
|
27
|
+
option :destination, default: "Zonefile", desc: "path to the zonefile"
|
28
|
+
sig { void }
|
16
29
|
def generate
|
17
|
-
|
30
|
+
kwargs = options.to_hash.transform_keys(&:to_sym)
|
31
|
+
Zonesync.generate(**kwargs)
|
18
32
|
end
|
19
33
|
|
34
|
+
sig { returns(TrueClass) }
|
20
35
|
def self.exit_on_failure? = true
|
21
36
|
end
|
22
37
|
end
|
data/lib/zonesync/cloudflare.rb
CHANGED
@@ -1,83 +1,107 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "zonesync/record"
|
2
5
|
require "zonesync/http"
|
3
6
|
|
4
7
|
module Zonesync
|
5
8
|
class Cloudflare < Provider
|
9
|
+
sig { returns(String) }
|
6
10
|
def read
|
7
|
-
|
8
|
-
|
9
|
-
end).map(&:to_s).join("\n") + "\n"
|
11
|
+
records = [fake_soa] + all.keys
|
12
|
+
records.map(&:to_s).join("\n") + "\n"
|
10
13
|
end
|
11
14
|
|
15
|
+
sig { params(record: Record).void }
|
12
16
|
def remove record
|
13
|
-
id = all.fetch(record
|
17
|
+
id = all.fetch(record)
|
14
18
|
http.delete("/#{id}")
|
15
19
|
end
|
16
20
|
|
21
|
+
sig { params(old_record: Record, new_record: Record).void }
|
17
22
|
def change old_record, new_record
|
18
|
-
id = all.fetch(old_record
|
19
|
-
http.patch("/#{id}",
|
20
|
-
name: new_record[:name],
|
21
|
-
type: new_record[:type],
|
22
|
-
ttl: new_record[:ttl],
|
23
|
-
content: new_record[:rdata],
|
24
|
-
comment: new_record[:comment],
|
25
|
-
})
|
23
|
+
id = all.fetch(old_record)
|
24
|
+
http.patch("/#{id}", to_hash(new_record))
|
26
25
|
end
|
27
26
|
|
27
|
+
sig { params(record: Record).void }
|
28
28
|
def add record
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
29
|
+
add_with_duplicate_handling(record) do
|
30
|
+
begin
|
31
|
+
http.post("", to_hash(record))
|
32
|
+
rescue RuntimeError => e
|
33
|
+
# Convert CloudFlare-specific duplicate error to standard exception
|
34
|
+
if e.message.include?('"code":81058') && e.message.include?("An identical record already exists")
|
35
|
+
raise DuplicateRecordError.new(record, "CloudFlare error 81058")
|
36
|
+
else
|
37
|
+
# Re-raise other errors
|
38
|
+
raise
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
36
42
|
end
|
37
43
|
|
44
|
+
sig { returns(T::Hash[Record, String]) }
|
38
45
|
def all
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
map.merge to_record(attrs) => attrs["id"]
|
43
|
-
end
|
46
|
+
response = http.get("")
|
47
|
+
response["result"].reduce({}) do |map, attrs|
|
48
|
+
map.merge to_record(attrs) => attrs["id"]
|
44
49
|
end
|
45
50
|
end
|
46
51
|
|
47
52
|
private
|
48
53
|
|
54
|
+
sig { params(record: Record).returns(T::Hash[String, String]) }
|
55
|
+
def to_hash record
|
56
|
+
hash = record.to_h
|
57
|
+
content = hash.delete(:rdata)
|
58
|
+
|
59
|
+
if record.type == "MX"
|
60
|
+
# For MX records, split "priority hostname" into separate fields
|
61
|
+
priority, hostname = T.must(content).split(" ", 2)
|
62
|
+
hash[:priority] = priority.to_i
|
63
|
+
hash[:content] = hostname.sub(/\.$/, "") # remove trailing dot
|
64
|
+
else
|
65
|
+
hash[:content] = content
|
66
|
+
end
|
67
|
+
|
68
|
+
hash[:comment] = hash.delete(:comment) # maintain original order
|
69
|
+
hash
|
70
|
+
end
|
71
|
+
|
72
|
+
sig { params(attrs: T::Hash[String, String]).returns(Record) }
|
49
73
|
def to_record attrs
|
50
74
|
rdata = attrs["content"]
|
51
75
|
if %w[CNAME MX].include?(attrs["type"])
|
52
|
-
rdata = normalize_trailing_period(rdata)
|
76
|
+
rdata = normalize_trailing_period(T.must(rdata))
|
53
77
|
end
|
54
78
|
if attrs["type"] == "MX"
|
55
79
|
rdata = "#{attrs["priority"]} #{rdata}"
|
56
80
|
end
|
57
81
|
if %w[TXT SPF NAPTR].include?(attrs["type"])
|
58
|
-
rdata = normalize_quoting(rdata)
|
59
|
-
end
|
60
|
-
if attrs["type"] == "TXT"
|
61
|
-
rdata = normalize_quoting(rdata)
|
82
|
+
rdata = normalize_quoting(T.must(rdata))
|
62
83
|
end
|
63
84
|
Record.new(
|
64
|
-
name: normalize_trailing_period(attrs["name"]),
|
85
|
+
name: normalize_trailing_period(T.must(attrs["name"])),
|
65
86
|
type: attrs["type"],
|
66
87
|
ttl: attrs["ttl"].to_i,
|
67
88
|
rdata:,
|
68
89
|
comment: attrs["comment"],
|
69
|
-
)
|
90
|
+
)
|
70
91
|
end
|
71
92
|
|
93
|
+
sig { params(value: String).returns(String) }
|
72
94
|
def normalize_trailing_period value
|
73
95
|
value =~ /\.$/ ? value : value + "."
|
74
96
|
end
|
75
97
|
|
98
|
+
sig { params(value: String).returns(String) }
|
76
99
|
def normalize_quoting value
|
77
|
-
value =~ /^".+"$/ ? value : %("#{value}") # handle quote wrapping
|
100
|
+
value = value =~ /^".+"$/ ? value : %("#{value}") # handle quote wrapping
|
78
101
|
value.gsub('" "', "") # handle multiple txt record joining
|
79
102
|
end
|
80
103
|
|
104
|
+
sig { returns(Zonesync::Record) }
|
81
105
|
def fake_soa
|
82
106
|
zone_name = http.get("/..")["result"]["name"]
|
83
107
|
Record.new(
|
@@ -89,19 +113,20 @@ module Zonesync
|
|
89
113
|
)
|
90
114
|
end
|
91
115
|
|
116
|
+
sig { returns(HTTP) }
|
92
117
|
def http
|
93
118
|
return @http if @http
|
94
|
-
@http = HTTP.new("https://api.cloudflare.com/client/v4/zones/#{
|
95
|
-
@http.before_request do |request|
|
119
|
+
@http = T.let(HTTP.new("https://api.cloudflare.com/client/v4/zones/#{config.fetch(:zone_id)}/dns_records"), T.nilable(Zonesync::HTTP))
|
120
|
+
T.must(@http).before_request do |request|
|
96
121
|
request["Content-Type"] = "application/json"
|
97
|
-
if
|
98
|
-
request["Authorization"] = "Bearer #{
|
122
|
+
if config[:token]
|
123
|
+
request["Authorization"] = "Bearer #{config[:token]}"
|
99
124
|
else
|
100
|
-
request["X-Auth-Email"] =
|
101
|
-
request["X-Auth-Key"] =
|
125
|
+
request["X-Auth-Email"] = config.fetch(:email)
|
126
|
+
request["X-Auth-Key"] = config.fetch(:key)
|
102
127
|
end
|
103
128
|
end
|
104
|
-
@http
|
129
|
+
T.must(@http)
|
105
130
|
end
|
106
131
|
end
|
107
132
|
end
|
data/lib/zonesync/diff.rb
CHANGED
@@ -1,11 +1,20 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "diff/lcs"
|
2
5
|
|
3
6
|
module Zonesync
|
4
|
-
|
7
|
+
Operation = T.type_alias { [Symbol, T::Array[Record]] }
|
8
|
+
|
9
|
+
Diff = Struct.new(:from, :to) do
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { params(from: T::Array[Record], to: T::Array[Record]).returns(T.untyped) }
|
5
13
|
def self.call(from:, to:)
|
6
14
|
new(from, to).call
|
7
15
|
end
|
8
16
|
|
17
|
+
sig { returns(T::Array[[Symbol, T::Array[Record]]]) }
|
9
18
|
def call
|
10
19
|
changes = ::Diff::LCS.sdiff(from, to)
|
11
20
|
changes.map do |change|
|
data/lib/zonesync/errors.rb
CHANGED
@@ -1,10 +1,17 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
module Zonesync
|
2
5
|
class ConflictError < StandardError
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { params(existing: T.nilable(Record), new: Record).void }
|
3
9
|
def initialize existing, new
|
4
10
|
@existing = existing
|
5
11
|
@new = new
|
6
12
|
end
|
7
13
|
|
14
|
+
sig { returns(String) }
|
8
15
|
def message
|
9
16
|
<<~MSG
|
10
17
|
The following untracked DNS record already exists and would be overwritten.
|
@@ -15,10 +22,14 @@ module Zonesync
|
|
15
22
|
end
|
16
23
|
|
17
24
|
class MissingManifestError < StandardError
|
25
|
+
extend T::Sig
|
26
|
+
|
27
|
+
sig { params(manifest: Record).void }
|
18
28
|
def initialize manifest
|
19
29
|
@manifest = manifest
|
20
30
|
end
|
21
31
|
|
32
|
+
sig { returns(String) }
|
22
33
|
def message
|
23
34
|
<<~MSG
|
24
35
|
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.
|
@@ -28,11 +39,15 @@ module Zonesync
|
|
28
39
|
end
|
29
40
|
|
30
41
|
class ChecksumMismatchError < StandardError
|
42
|
+
extend T::Sig
|
43
|
+
|
44
|
+
sig { params(existing: T.nilable(Record), new: Record).void }
|
31
45
|
def initialize existing, new
|
32
46
|
@existing = existing
|
33
47
|
@new = new
|
34
48
|
end
|
35
49
|
|
50
|
+
sig { returns(String) }
|
36
51
|
def message
|
37
52
|
<<~MSG
|
38
53
|
The zonesync_checksum TXT record does not match the current state of the DNS records. This probably means that someone else has changed them.
|
@@ -41,5 +56,25 @@ module Zonesync
|
|
41
56
|
MSG
|
42
57
|
end
|
43
58
|
end
|
59
|
+
|
60
|
+
class DuplicateRecordError < StandardError
|
61
|
+
extend T::Sig
|
62
|
+
|
63
|
+
sig { params(record: Record, provider_message: T.nilable(String)).void }
|
64
|
+
def initialize record, provider_message = nil
|
65
|
+
@record = record
|
66
|
+
@provider_message = provider_message
|
67
|
+
end
|
68
|
+
|
69
|
+
sig { returns(String) }
|
70
|
+
def message
|
71
|
+
msg = "Record already exists: #{@record.name} #{@record.type}"
|
72
|
+
msg += " (#{@provider_message})" if @provider_message
|
73
|
+
msg
|
74
|
+
end
|
75
|
+
|
76
|
+
sig { returns(Record) }
|
77
|
+
attr_reader :record
|
78
|
+
end
|
44
79
|
end
|
45
80
|
|
data/lib/zonesync/http.rb
CHANGED
@@ -1,38 +1,51 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "net/http"
|
2
5
|
require "json"
|
3
6
|
|
4
7
|
module Zonesync
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
HTTP = Struct.new(:base) do
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { params(base: String).void }
|
12
|
+
def initialize(base)
|
13
|
+
super
|
14
|
+
@before_request = T.let([], T::Array[T.untyped])
|
15
|
+
@after_response = T.let([], T::Array[T.untyped])
|
10
16
|
end
|
11
17
|
|
18
|
+
sig { params(path: String).returns(T.untyped) }
|
12
19
|
def get path
|
13
20
|
request("get", path)
|
14
21
|
end
|
15
22
|
|
23
|
+
sig { params(path: String, body: T.untyped).returns(T.untyped) }
|
16
24
|
def post path, body
|
17
25
|
request("post", path, body)
|
18
26
|
end
|
19
27
|
|
28
|
+
sig { params(path: String, body: T.untyped).returns(T.untyped) }
|
20
29
|
def patch path, body
|
21
30
|
request("patch", path, body)
|
22
31
|
end
|
23
32
|
|
33
|
+
sig { params(path: String).returns(T.untyped) }
|
24
34
|
def delete path
|
25
35
|
request("delete", path)
|
26
36
|
end
|
27
37
|
|
38
|
+
sig { params(block: T.proc.params(arg0: T.untyped, arg1: T.untyped, arg2: T.untyped).void).void }
|
28
39
|
def before_request &block
|
29
40
|
@before_request << block
|
30
41
|
end
|
31
42
|
|
43
|
+
sig { params(block: T.proc.params(arg0: T.untyped).void).void }
|
32
44
|
def after_response &block
|
33
45
|
@after_response << block
|
34
46
|
end
|
35
47
|
|
48
|
+
sig { params(method: String, path: String, body: T.untyped).returns(T.untyped) }
|
36
49
|
def request method, path, body=nil
|
37
50
|
uri = URI.parse("#{base}#{path}")
|
38
51
|
request = Net::HTTP.const_get(method.to_s.capitalize).new(uri.path)
|
@@ -42,12 +55,15 @@ module Zonesync
|
|
42
55
|
end
|
43
56
|
|
44
57
|
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
|
45
|
-
|
46
|
-
|
58
|
+
if request.fetch("Content-Type", "").include?("application/json")
|
59
|
+
http.request(request, JSON.dump(body))
|
60
|
+
else
|
61
|
+
http.request(request, body)
|
62
|
+
end
|
47
63
|
end
|
48
64
|
|
49
65
|
@after_response.each do |block|
|
50
|
-
call(response)
|
66
|
+
block.call(response)
|
51
67
|
end
|
52
68
|
|
53
69
|
raise response.body unless response.code =~ /^20.$/
|
data/lib/zonesync/logger.rb
CHANGED
@@ -1,24 +1,29 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "logger"
|
2
5
|
require "fileutils"
|
3
6
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
+
module Zonesync
|
8
|
+
class Logger
|
9
|
+
extend T::Sig
|
7
10
|
|
8
|
-
|
9
|
-
|
10
|
-
loggers
|
11
|
-
end
|
11
|
+
sig { params(method: Symbol, records: T::Array[Record], dry_run: T::Boolean).void }
|
12
|
+
def self.log method, records, dry_run: false
|
13
|
+
loggers = [::Logger.new(STDOUT)]
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
15
|
+
if !dry_run
|
16
|
+
FileUtils.mkdir_p("log")
|
17
|
+
loggers << ::Logger.new("log/zonesync.log")
|
18
|
+
end
|
19
|
+
|
20
|
+
loggers.each do |logger|
|
21
|
+
operation =
|
22
|
+
(records.length == 2 ? "\n" : "") +
|
23
|
+
records.map { |r| r.to_h.values.join(" ") }.join("->\n")
|
24
|
+
logger.info "Zonesync: #{method.capitalize} #{operation}"
|
20
25
|
end
|
21
|
-
logger.info "Zonesync: #{method.capitalize} #{operation}"
|
22
26
|
end
|
23
27
|
end
|
24
28
|
end
|
29
|
+
|
data/lib/zonesync/manifest.rb
CHANGED
@@ -1,19 +1,26 @@
|
|
1
|
+
# typed: strict
|
2
|
+
require "sorbet-runtime"
|
3
|
+
|
1
4
|
require "zonesync/record"
|
2
5
|
require "digest"
|
3
6
|
|
4
7
|
module Zonesync
|
5
|
-
|
8
|
+
Manifest = Struct.new(:records, :zone) do
|
9
|
+
extend T::Sig
|
6
10
|
DIFFABLE_RECORD_TYPES =
|
7
|
-
%w[A AAAA CNAME MX TXT SPF NAPTR PTR].sort
|
11
|
+
T.let(%w[A AAAA CNAME MX TXT SPF NAPTR PTR].sort, T::Array[String])
|
8
12
|
|
13
|
+
sig { returns(T.nilable(Zonesync::Record)) }
|
9
14
|
def existing
|
10
15
|
records.find(&:manifest?)
|
11
16
|
end
|
12
17
|
|
18
|
+
sig { returns(T::Boolean) }
|
13
19
|
def existing?
|
14
20
|
!!existing
|
15
21
|
end
|
16
22
|
|
23
|
+
sig { returns(Zonesync::Record) }
|
17
24
|
def generate
|
18
25
|
Record.new(
|
19
26
|
name: "zonesync_manifest.#{zone.origin}",
|
@@ -24,10 +31,12 @@ module Zonesync
|
|
24
31
|
)
|
25
32
|
end
|
26
33
|
|
34
|
+
sig { returns(T.nilable(Zonesync::Record)) }
|
27
35
|
def existing_checksum
|
28
36
|
records.find(&:checksum?)
|
29
37
|
end
|
30
38
|
|
39
|
+
sig { returns(Zonesync::Record) }
|
31
40
|
def generate_checksum
|
32
41
|
input_string = diffable_records.map(&:to_s).join
|
33
42
|
sha256 = Digest::SHA256.hexdigest(input_string)
|
@@ -40,6 +49,7 @@ module Zonesync
|
|
40
49
|
)
|
41
50
|
end
|
42
51
|
|
52
|
+
sig { params(record: Zonesync::Record).returns(T::Boolean) }
|
43
53
|
def diffable? record
|
44
54
|
if existing?
|
45
55
|
matches?(record)
|
@@ -48,9 +58,10 @@ module Zonesync
|
|
48
58
|
end
|
49
59
|
end
|
50
60
|
|
61
|
+
sig { params(record: Zonesync::Record).returns(T::Boolean) }
|
51
62
|
def matches? record
|
52
63
|
return false unless existing?
|
53
|
-
hash = existing
|
64
|
+
hash = T.must(existing)
|
54
65
|
.rdata[1..-2] # remove quotes
|
55
66
|
.split(";")
|
56
67
|
.reduce({}) do |hash, pair|
|
@@ -62,6 +73,7 @@ module Zonesync
|
|
62
73
|
shorthands.include?(shorthand_for(record))
|
63
74
|
end
|
64
75
|
|
76
|
+
sig { params(record: Zonesync::Record, with_type: T::Boolean).returns(String) }
|
65
77
|
def shorthand_for record, with_type: false
|
66
78
|
shorthand = record.short_name(zone.origin)
|
67
79
|
shorthand = "#{record.type}:#{shorthand}" if with_type
|
@@ -73,25 +85,29 @@ module Zonesync
|
|
73
85
|
|
74
86
|
private
|
75
87
|
|
88
|
+
sig { returns(String) }
|
76
89
|
def generate_rdata
|
77
90
|
generate_manifest.map do |type, short_names|
|
78
91
|
"#{type}:#{short_names.join(",")}"
|
79
92
|
end.join(";").inspect
|
80
93
|
end
|
81
94
|
|
95
|
+
sig { returns(T::Array[Zonesync::Record]) }
|
82
96
|
def diffable_records
|
83
97
|
records.select do |record|
|
84
98
|
diffable?(record)
|
85
99
|
end.sort
|
86
100
|
end
|
87
101
|
|
102
|
+
sig { returns(T::Hash[String, T::Array[String]]) }
|
88
103
|
def generate_manifest
|
89
|
-
diffable_records.reduce({}) do |hash, record|
|
104
|
+
hash = diffable_records.reduce({}) do |hash, record|
|
90
105
|
hash[record.type] ||= []
|
91
106
|
hash[record.type] << shorthand_for(record)
|
92
107
|
hash[record.type].sort!
|
93
108
|
hash
|
94
|
-
end
|
109
|
+
end
|
110
|
+
Hash[hash.sort_by(&:first)]
|
95
111
|
end
|
96
112
|
end
|
97
113
|
end
|