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.
Files changed (74) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -0
  3. data/lib/zonesync/cli.rb +19 -4
  4. data/lib/zonesync/cloudflare.rb +64 -39
  5. data/lib/zonesync/diff.rb +10 -1
  6. data/lib/zonesync/errors.rb +35 -0
  7. data/lib/zonesync/generate.rb +14 -0
  8. data/lib/zonesync/http.rb +24 -8
  9. data/lib/zonesync/logger.rb +20 -15
  10. data/lib/zonesync/manifest.rb +21 -5
  11. data/lib/zonesync/parser.rb +337 -0
  12. data/lib/zonesync/provider.rb +80 -26
  13. data/lib/zonesync/record.rb +18 -23
  14. data/lib/zonesync/route53.rb +48 -27
  15. data/lib/zonesync/sync.rb +40 -0
  16. data/lib/zonesync/validator.rb +19 -15
  17. data/lib/zonesync/version.rb +1 -1
  18. data/lib/zonesync/zonefile.rb +22 -311
  19. data/lib/zonesync.rb +31 -62
  20. data/sorbet/config +4 -0
  21. data/sorbet/rbi/annotations/.gitattributes +1 -0
  22. data/sorbet/rbi/annotations/activesupport.rbi +457 -0
  23. data/sorbet/rbi/annotations/minitest.rbi +119 -0
  24. data/sorbet/rbi/annotations/webmock.rbi +9 -0
  25. data/sorbet/rbi/gems/.gitattributes +1 -0
  26. data/sorbet/rbi/gems/activesupport@8.0.1.rbi +18474 -0
  27. data/sorbet/rbi/gems/addressable@2.8.7.rbi +1994 -0
  28. data/sorbet/rbi/gems/base64@0.2.0.rbi +507 -0
  29. data/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
  30. data/sorbet/rbi/gems/bigdecimal@3.1.9.rbi +9 -0
  31. data/sorbet/rbi/gems/concurrent-ruby@1.3.4.rbi +11645 -0
  32. data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +9 -0
  33. data/sorbet/rbi/gems/crack@1.0.0.rbi +145 -0
  34. data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
  35. data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1131 -0
  36. data/sorbet/rbi/gems/drb@2.2.1.rbi +1347 -0
  37. data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
  38. data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +353 -0
  39. data/sorbet/rbi/gems/i18n@1.14.6.rbi +2275 -0
  40. data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
  41. data/sorbet/rbi/gems/logger@1.6.4.rbi +940 -0
  42. data/sorbet/rbi/gems/minitest@5.25.4.rbi +1547 -0
  43. data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
  44. data/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
  45. data/sorbet/rbi/gems/polyglot@0.3.5.rbi +42 -0
  46. data/sorbet/rbi/gems/prism@1.3.0.rbi +40040 -0
  47. data/sorbet/rbi/gems/psych@5.2.2.rbi +1785 -0
  48. data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +936 -0
  49. data/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
  50. data/sorbet/rbi/gems/rbi@0.2.2.rbi +4527 -0
  51. data/sorbet/rbi/gems/rdoc@6.10.0.rbi +12766 -0
  52. data/sorbet/rbi/gems/reline@0.6.0.rbi +9 -0
  53. data/sorbet/rbi/gems/rexml@3.4.0.rbi +4974 -0
  54. data/sorbet/rbi/gems/rspec-core@3.13.2.rbi +10896 -0
  55. data/sorbet/rbi/gems/rspec-expectations@3.13.3.rbi +8183 -0
  56. data/sorbet/rbi/gems/rspec-mocks@3.13.2.rbi +5341 -0
  57. data/sorbet/rbi/gems/rspec-support@3.13.2.rbi +1630 -0
  58. data/sorbet/rbi/gems/rspec@3.13.0.rbi +83 -0
  59. data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
  60. data/sorbet/rbi/gems/spoom@1.5.0.rbi +4932 -0
  61. data/sorbet/rbi/gems/stringio@3.1.2.rbi +9 -0
  62. data/sorbet/rbi/gems/tapioca@0.16.6.rbi +3611 -0
  63. data/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
  64. data/sorbet/rbi/gems/treetop@1.6.12.rbi +1895 -0
  65. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5918 -0
  66. data/sorbet/rbi/gems/uri@1.0.2.rbi +2340 -0
  67. data/sorbet/rbi/gems/webmock@3.24.0.rbi +1780 -0
  68. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
  69. data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
  70. data/sorbet/rbi/todo.rbi +7 -0
  71. data/sorbet/tapioca/config.yml +13 -0
  72. data/sorbet/tapioca/require.rb +4 -0
  73. data/zonesync.gemspec +3 -0
  74. metadata +100 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a674bd1519c4b8bd6a30814b43e70365191c18a59b24615e0ed93bbf8f79c286
4
- data.tar.gz: c929821a57474a2d4be05f39f2e24e0cef38ab1484d5dbe6d8fbefba05ec60fc
3
+ metadata.gz: ed23482c3eeed63566e03e79231562447d9f63c09a2d382151d6cd3b123b3502
4
+ data.tar.gz: 45c154c9bd89d9f1ad97df54ec0c4d30ab49995716fbd3e519b4381f890c2259
5
5
  SHA512:
6
- metadata.gz: e7678dc403b138a88de0d30a19a980c6c9d23ec3dcb6aa89157530cfc2e260b9e2dd610a1461b3d09174d3c70246c35d34d418a51802cc3c52cd651e5a42d1b2
7
- data.tar.gz: 42bf9f0ba6346af233735e236e0cc39e69adc3caf9e41901f8aeddc6936cfdf602086b9a0b3b2999bc8852c493f72a453d66106c8543bc9c95b218caaae35a53
6
+ metadata.gz: 2870e1fd94f37248ddd609da2a90be47e9487f11e371043b48ed00465fbc727f69fd36614d83b8adf4ff7661bc90dfbbeb100c964c572a49b2ad97ab36b4ad97
7
+ data.tar.gz: 3dcde654cdbdcaa806734b41c99286ba4724b812c58a4b22af7232bde1e5410892edda80b4475918b512738750a44a709c87c0af4b3483d6a9cc669bc12158b5
data/Gemfile CHANGED
@@ -4,3 +4,4 @@ source 'https://rubygems.org'
4
4
  gemspec
5
5
 
6
6
  gem "debug"
7
+ gem "activesupport"
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
- Zonesync.call dry_run: options[:dry_run]
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
- Zonesync.generate
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
@@ -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
- ([fake_soa] + all.keys.map do |hash|
8
- Record.new(hash)
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.to_h)
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.to_h)
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
- http.post(nil, {
30
- name: record[:name],
31
- type: record[:type],
32
- ttl: record[:ttl],
33
- content: record[:rdata],
34
- comment: record[:comment],
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
- @all ||= begin
40
- response = http.get(nil)
41
- response["result"].reduce({}) do |map, attrs|
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
- ).to_h
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/#{credentials[:zone_id]}/dns_records")
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 credentials[:token]
98
- request["Authorization"] = "Bearer #{credentials[:token]}"
122
+ if config[:token]
123
+ request["Authorization"] = "Bearer #{config[:token]}"
99
124
  else
100
- request["X-Auth-Email"] = credentials[:email]
101
- request["X-Auth-Key"] = credentials[: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
- class Diff < Struct.new(:from, :to)
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|
@@ -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
 
@@ -0,0 +1,14 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
4
+ module Zonesync
5
+ Generate = Struct.new(:source, :destination) do
6
+ extend T::Sig
7
+
8
+ sig { void }
9
+ def call
10
+ destination.write(source.read)
11
+ nil
12
+ end
13
+ end
14
+ end
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
- class HTTP < Struct.new(:base)
6
- def initialize(...)
7
- super
8
- @before_request = []
9
- @after_response = []
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
- body = JSON.dump(body) if request.fetch("Content-Type", "").include?("application/json")
46
- http.request(request, body)
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.$/
@@ -1,24 +1,29 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  require "logger"
2
5
  require "fileutils"
3
6
 
4
- class Logger
5
- def self.log method, args, dry_run: false
6
- loggers = [::Logger.new(STDOUT)]
7
+ module Zonesync
8
+ class Logger
9
+ extend T::Sig
7
10
 
8
- if !dry_run
9
- FileUtils.mkdir_p("log")
10
- loggers << ::Logger.new("log/zonesync.log")
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
- loggers.each do |logger|
14
- operation = case args
15
- when Array
16
- (args.length == 2 ? "\n" : "") +
17
- args.map { |r| r.to_h.values.join(" ") }.join("->\n")
18
- else
19
- args.to_h.values.join(" ")
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
+
@@ -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
- class Manifest < Struct.new(:records, :zone)
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.sort_by(&:first)
109
+ end
110
+ Hash[hash.sort_by(&:first)]
95
111
  end
96
112
  end
97
113
  end