zonesync 0.9.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 +9 -0
  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 +27 -60
  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
@@ -1,34 +1,57 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  require "zonesync/record"
2
5
  require "zonesync/http"
3
6
  require "rexml/document"
4
7
 
5
8
  module Zonesync
6
9
  class Route53 < Provider
10
+ sig { returns(String) }
7
11
  def read
8
- doc = REXML::Document.new(http.get(nil))
9
- records = doc.elements.collect("*/ResourceRecordSets/ResourceRecordSet") do |record_set|
10
- to_records(record_set)
11
- end.flatten.sort
12
- records.map(&:to_s).join("\n") + "\n"
12
+ @read = T.let(@read, T.nilable(String))
13
+ @read ||= begin
14
+ doc = REXML::Document.new(http.get(""))
15
+ records = doc.elements.collect("*/ResourceRecordSets/ResourceRecordSet") do |record_set|
16
+ to_records(record_set)
17
+ end.flatten.sort
18
+ records.map(&:to_s).join("\n") + "\n"
19
+ end
13
20
  end
14
21
 
22
+ sig { params(record: Record).void }
15
23
  def remove(record)
16
24
  change_record("DELETE", record)
17
25
  end
18
26
 
27
+ sig { params(old_record: Record, new_record: Record).void }
19
28
  def change(old_record, new_record)
20
29
  remove(old_record)
21
30
  add(new_record)
22
31
  end
23
32
 
33
+ sig { params(record: Record).void }
24
34
  def add(record)
25
- change_record("CREATE", record)
35
+ add_with_duplicate_handling(record) do
36
+ begin
37
+ change_record("CREATE", record)
38
+ rescue RuntimeError => e
39
+ # Convert Route53-specific duplicate error to standard exception
40
+ if e.message.include?("RRSet already exists")
41
+ raise DuplicateRecordError.new(record, "Route53 duplicate record error")
42
+ else
43
+ # Re-raise other errors
44
+ raise
45
+ end
46
+ end
47
+ end
26
48
  end
27
49
 
28
50
  private
29
51
 
52
+ sig { params(action: String, record: Record).void }
30
53
  def change_record(action, record)
31
- http.post(nil, <<~XML)
54
+ http.post("", <<~XML)
32
55
  <?xml version="1.0" encoding="UTF-8"?>
33
56
  <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
34
57
  <ChangeBatch>
@@ -36,12 +59,12 @@ module Zonesync
36
59
  <Change>
37
60
  <Action>#{action}</Action>
38
61
  <ResourceRecordSet>
39
- <Name>#{record[:name]}</Name>
40
- <Type>#{record[:type]}</Type>
41
- <TTL>#{record[:ttl]}</TTL>
62
+ <Name>#{record.name}</Name>
63
+ <Type>#{record.type}</Type>
64
+ <TTL>#{record.ttl}</TTL>
42
65
  <ResourceRecords>
43
66
  <ResourceRecord>
44
- <Value>#{record[:rdata]}</Value>
67
+ <Value>#{record.rdata}</Value>
45
68
  </ResourceRecord>
46
69
  </ResourceRecords>
47
70
  </ResourceRecordSet>
@@ -52,50 +75,47 @@ module Zonesync
52
75
  XML
53
76
  end
54
77
 
78
+ sig { params(el: REXML::Element).returns(T::Array[Record]) }
55
79
  def to_records(el)
56
80
  el.elements.collect("ResourceRecords/ResourceRecord") do |rr|
57
81
  name = normalize_trailing_period(get_value(el, "Name"))
58
82
  type = get_value(el, "Type")
83
+ ttl = get_value(el, "TTL")
59
84
  rdata = get_value(rr, "Value")
60
85
 
61
86
  record = Record.new(
62
87
  name:,
63
88
  type:,
64
- ttl: get_value(el, "TTL"),
89
+ ttl:,
65
90
  rdata:,
66
91
  comment: nil, # Route 53 does not have a direct comment field
67
92
  )
68
93
  end
69
94
  end
70
95
 
96
+ sig { params(el: REXML::Element, field: String).returns(String) }
71
97
  def get_value el, field
72
98
  el.elements[field].text.gsub(/\\(\d{3})/) { $1.to_i(8).chr } # unescape octal
73
99
  end
74
100
 
75
- def from_record(record)
76
- {
77
- Name: normalize_trailing_period(record[:name]),
78
- Type: record[:type],
79
- TTL: record[:ttl],
80
- ResourceRecords: record[:rdata].split(",").map { |value| { Value: value } }
81
- }
82
- end
83
-
101
+ sig { params(value: String).returns(String) }
84
102
  def normalize_trailing_period(value)
85
103
  value =~ /\.$/ ? value : value + "."
86
104
  end
87
105
 
106
+ sig { returns(HTTP) }
88
107
  def http
89
108
  return @http if @http
90
- @http = HTTP.new("https://route53.amazonaws.com/2013-04-01/hostedzone/#{credentials.fetch(:hosted_zone_id)}/rrset")
91
- @http.before_request do |request, uri, body|
109
+ @http = T.let(HTTP.new("https://route53.amazonaws.com/2013-04-01/hostedzone/#{config.fetch(:hosted_zone_id)}/rrset"), T.nilable(Zonesync::HTTP))
110
+ T.must(@http).before_request do |request, uri, body|
92
111
  request["Content-Type"] = "application/xml"
93
112
  request["X-Amz-Date"] = Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
94
113
  request["Authorization"] = sign_request(request.method, uri, body)
95
114
  end
96
- @http
115
+ T.must(@http)
97
116
  end
98
117
 
118
+ sig { params(method: String, uri: URI::HTTPS, body: T.nilable(String)).returns(String) }
99
119
  def sign_request(method, uri, body)
100
120
  service = "route53"
101
121
  date = Time.now.utc.strftime("%Y%m%d")
@@ -115,7 +135,7 @@ module Zonesync
115
135
  ].join("\n")
116
136
 
117
137
  algorithm = "AWS4-HMAC-SHA256"
118
- credential_scope = "#{date}/#{credentials.fetch(:aws_region)}/#{service}/aws4_request"
138
+ credential_scope = "#{date}/#{config.fetch(:aws_region)}/#{service}/aws4_request"
119
139
  string_to_sign = [
120
140
  algorithm,
121
141
  amz_date,
@@ -123,12 +143,13 @@ module Zonesync
123
143
  OpenSSL::Digest::SHA256.hexdigest(canonical_request)
124
144
  ].join("\n")
125
145
 
126
- signing_key = get_signature_key(credentials.fetch(:aws_secret_access_key), date, credentials.fetch(:aws_region), service)
146
+ signing_key = get_signature_key(config.fetch(:aws_secret_access_key), date, config.fetch(:aws_region), service)
127
147
  signature = OpenSSL::HMAC.hexdigest("SHA256", signing_key, string_to_sign)
128
148
 
129
- "#{algorithm} Credential=#{credentials.fetch(:aws_access_key_id)}/#{credential_scope}, SignedHeaders=#{signed_headers}, Signature=#{signature}"
149
+ "#{algorithm} Credential=#{config.fetch(:aws_access_key_id)}/#{credential_scope}, SignedHeaders=#{signed_headers}, Signature=#{signature}"
130
150
  end
131
151
 
152
+ sig { params(key: String, date_stamp: String, region_name: String, service_name: String).returns(String) }
132
153
  def get_signature_key(key, date_stamp, region_name, service_name)
133
154
  k_date = OpenSSL::HMAC.digest("SHA256", "AWS4" + key, date_stamp)
134
155
  k_region = OpenSSL::HMAC.digest("SHA256", k_date, region_name)
@@ -0,0 +1,40 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
4
+ require "zonesync/logger"
5
+
6
+ module Zonesync
7
+ Sync = Struct.new(:source, :destination) do
8
+ extend T::Sig
9
+
10
+ sig { params(dry_run: T::Boolean, force: T::Boolean).void }
11
+ def call dry_run: false, force: false
12
+ operations = destination.diff!(source, force: force)
13
+
14
+ smanifest = source.manifest.generate
15
+ dmanifest = destination.manifest.existing
16
+ if smanifest != dmanifest
17
+ if dmanifest
18
+ operations << [:change, [dmanifest, smanifest]]
19
+ else
20
+ operations << [:add, [smanifest]]
21
+ end
22
+ end
23
+
24
+ schecksum = source.manifest.generate_checksum
25
+ dchecksum = destination.manifest.existing_checksum
26
+ if schecksum != dchecksum
27
+ if dchecksum
28
+ operations << [:change, [dchecksum, schecksum]]
29
+ else
30
+ operations << [:add, [schecksum]]
31
+ end
32
+ end
33
+
34
+ operations.each do |method, records|
35
+ Logger.log(method, records, dry_run: dry_run)
36
+ destination.send(method, *records) unless dry_run
37
+ end
38
+ end
39
+ end
40
+ end
@@ -1,28 +1,40 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
1
4
  module Zonesync
2
- class Validator < Struct.new(:operations, :destination)
3
- def self.call(...)
4
- new(...).call
5
+ Validator = Struct.new(:operations, :destination) do
6
+ extend T::Sig
7
+
8
+ sig { params(operations: T::Array[Operation], destination: Provider, force: T::Boolean).void }
9
+ def self.call(operations, destination, force: false)
10
+ new(operations, destination).call(force: force)
5
11
  end
6
12
 
7
- def call
13
+ sig { params(force: T::Boolean).void }
14
+ def call(force: false)
8
15
  if operations.any? && !manifest.existing?
9
16
  raise MissingManifestError.new(manifest.generate)
10
17
  end
11
- if manifest.existing_checksum && manifest.existing_checksum != manifest.generate_checksum
18
+ if !force && manifest.existing_checksum && manifest.existing_checksum != manifest.generate_checksum
12
19
  raise ChecksumMismatchError.new(manifest.existing_checksum, manifest.generate_checksum)
13
20
  end
14
21
  operations.each do |method, args|
15
- send(method, *args)
22
+ if method == :add
23
+ validate_addition args.first
24
+ end
16
25
  end
26
+ nil
17
27
  end
18
28
 
19
29
  private
20
30
 
31
+ sig { returns(Manifest) }
21
32
  def manifest
22
33
  destination.manifest
23
34
  end
24
35
 
25
- def add record
36
+ sig { params(record: Record).void }
37
+ def validate_addition record
26
38
  return if manifest.matches?(record)
27
39
  shorthand = manifest.shorthand_for(record, with_type: true)
28
40
  conflicting_record = destination.records.find do |r|
@@ -32,14 +44,6 @@ module Zonesync
32
44
  return if conflicting_record == record
33
45
  raise Zonesync::ConflictError.new(conflicting_record, record)
34
46
  end
35
-
36
- def change *records
37
- # FIXME? is it possible to break something with a tracked changed record
38
- end
39
-
40
- def remove record
41
- # FIXME? is it possible to break something with a tracked removed record
42
- end
43
47
  end
44
48
  end
45
49
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Zonesync
4
- VERSION = "0.9.0"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -1,323 +1,34 @@
1
- require "treetop"
2
- Treetop.load File.join(__dir__, "zonefile")
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
3
 
4
- module Zonesync
5
- module Zonefile
6
- class << self
7
- def parse(zone_string)
8
- parser = ZonefileParser.new
9
- result = parser.parse(zone_string)
10
- return result if result
11
- puts zone_string
12
- raise ParsingError, parser.failure_reason
13
- end
14
-
15
- def load(zone_string)
16
- parsed = parse(zone_string)
17
- Zone.new(parsed.entries,
18
- origin: parsed.variables["ORIGIN"],
19
- )
20
- end
21
- end
22
-
23
- class ParsingError < RuntimeError; end
24
- class UnknownRecordType < RuntimeError; end
25
- class Zone
26
- attr_reader :origin
27
- attr_reader :records
28
-
29
- def initialize(entries, origin: nil, alternate_origin: ".")
30
- @origin = origin
31
- @records = []
32
- @vars = {"origin" => alternate_origin, :last_host => "."}
33
- entries.each do |e|
34
- case e.parse_type
35
- when :variable
36
- key = e.name.text_value.downcase
37
- @vars[key] = case key
38
- when "ttl"
39
- e.value.text_value.to_i
40
- else
41
- e.value.text_value
42
- end
43
- when :soa
44
- @records << SOA.new(@vars, e)
45
- when :record
46
- case e.record_type
47
- when "A" then @records << A.new(@vars, e)
48
- when "AAAA" then @records << AAAA.new(@vars, e)
49
- when "CAA" then @records << CAA.new(@vars, e)
50
- when "CNAME" then @records << CNAME.new(@vars, e)
51
- when "MX" then @records << MX.new(@vars, e)
52
- when "NAPTR" then @records << NAPTR.new(@vars, e)
53
- when "NS" then @records << NS.new(@vars, e)
54
- when "PTR" then @records << PTR.new(@vars, e)
55
- when "SRV" then @records << SRV.new(@vars, e)
56
- when "SPF" then @records << SPF.new(@vars, e)
57
- when "SSHFP" then @records << SSHFP.new(@vars, e)
58
- when "TXT" then @records << TXT.new(@vars, e)
59
- when "SOA" then
60
- # No-op
61
- else
62
- raise UnknownRecordType, "Unknown record type: #{e.record_type}"
63
- end
64
- end
65
- end
66
- @origin ||= soa.origin
67
- end
68
-
69
- def soa
70
- records_of(SOA).first
71
- end
72
-
73
- def records_of(kl)
74
- @records.select { |r| r.instance_of? kl }
75
- end
76
- end
77
-
78
- class Record
79
- # assign, with handling for global TTL
80
- def self.writer_for_ttl(*attribs)
81
- attribs.each do |attrib|
82
- define_method "#{attrib}=" do |val|
83
- instance_variable_set("@#{attrib}", val || @vars["ttl"])
84
- end
85
- end
86
- end
87
-
88
- attr_reader :ttl
89
- attr_writer :klass
90
- writer_for_ttl :ttl
91
-
92
- def klass
93
- @klass = nil if @klass == ""
94
- @klass ||= "IN"
95
- end
96
-
97
- attr_accessor :comment
98
-
99
- private
100
-
101
- def qualify_host(host)
102
- origin = vars["origin"]
103
- host = vars[:last_host] if /^\s*$/.match?(host)
104
- host = host.gsub(/@/, origin)
105
- if /\.$/.match?(host)
106
- host
107
- elsif /^\./.match?(origin)
108
- host + origin
109
- else
110
- host + "." + origin
111
- end
112
- end
113
- attr_accessor :vars
114
- end
115
-
116
- class SOA < Record
117
- attr_accessor :origin, :nameserver, :responsible_party, :serial, :refresh_time, :retry_time, :expiry_time, :nxttl
118
-
119
- def initialize(vars, zonefile_soa = nil)
120
- @vars = vars
121
- if zonefile_soa
122
- self.origin = qualify_host(zonefile_soa.origin.to_s)
123
- @vars[:last_host] = origin
124
- self.ttl = zonefile_soa.ttl.to_i
125
- self.klass = zonefile_soa.klass.to_s
126
- self.nameserver = qualify_host(zonefile_soa.ns.to_s)
127
- self.responsible_party = qualify_host(zonefile_soa.rp.to_s)
128
- self.serial = zonefile_soa.serial.to_i
129
- self.refresh_time = zonefile_soa.refresh.to_i
130
- self.retry_time = zonefile_soa.reretry.to_i
131
- self.expiry_time = zonefile_soa.expiry.to_i
132
- self.nxttl = zonefile_soa.nxttl.to_i
133
- end
134
- end
135
- end
136
-
137
- class A < Record
138
- attr_accessor :host, :address
139
-
140
- def initialize(vars, zonefile_record)
141
- @vars = vars
142
- if zonefile_record
143
- self.host = qualify_host(zonefile_record.host.to_s)
144
- @vars[:last_host] = host
145
- self.ttl = zonefile_record.ttl.to_i
146
- self.klass = zonefile_record.klass.to_s
147
- self.address = zonefile_record.ip_address.to_s
148
- self.comment = zonefile_record.comment&.to_s
149
- end
150
- end
151
- end
152
-
153
- class AAAA < A
154
- end
155
-
156
- class CAA < Record
157
- attr_accessor :host, :flags, :tag, :value
4
+ require "zonesync/parser"
158
5
 
159
- def initialize(vars, zonefile_record)
160
- @vars = vars
161
- if zonefile_record
162
- self.host = qualify_host(zonefile_record.host.to_s)
163
- @vars[:last_host] = host
164
- self.ttl = zonefile_record.ttl.to_i
165
- self.klass = zonefile_record.klass.to_s
166
- self.flags = zonefile_record.flags.to_i
167
- self.tag = zonefile_record.tag.to_s
168
- self.value = zonefile_record.value.to_s
169
- self.comment = zonefile_record.comment&.to_s
170
- end
171
- end
172
- end
173
-
174
- class CNAME < Record
175
- attr_accessor :host, :domainname
176
-
177
- def initialize(vars, zonefile_record)
178
- @vars = vars
179
- if zonefile_record
180
- self.host = qualify_host(zonefile_record.host.to_s)
181
- @vars[:last_host] = host
182
- self.ttl = zonefile_record.ttl.to_i
183
- self.klass = zonefile_record.klass.to_s
184
- self.domainname = qualify_host(zonefile_record.target.to_s)
185
- self.comment = zonefile_record.comment&.to_s
186
- end
187
- end
188
-
189
- alias target domainname
190
- alias alias host
191
- end
192
-
193
- class MX < Record
194
- attr_accessor :host, :priority, :domainname
195
-
196
- def initialize(vars, zonefile_record)
197
- @vars = vars
198
- if zonefile_record
199
- self.host = qualify_host(zonefile_record.host.to_s)
200
- @vars[:last_host] = host
201
- self.ttl = zonefile_record.ttl.to_i
202
- self.klass = zonefile_record.klass.to_s
203
- self.priority = zonefile_record.priority.to_i
204
- self.domainname = qualify_host(zonefile_record.exchanger.to_s)
205
- self.comment = zonefile_record.comment&.to_s
206
- end
207
- end
208
-
209
- alias exchange domainname
210
- alias exchanger domainname
211
- end
212
-
213
- class NAPTR < Record
214
- attr_accessor :host, :data
215
-
216
- def initialize(vars, zonefile_record)
217
- @vars = vars
218
- if zonefile_record
219
- self.host = qualify_host(zonefile_record.host.to_s)
220
- @vars[:last_host] = host
221
- self.ttl = zonefile_record.ttl.to_i
222
- self.klass = zonefile_record.klass.to_s
223
- self.data = zonefile_record.data.to_s
224
- self.comment = zonefile_record.comment&.to_s
225
- end
226
- end
227
- end
228
-
229
- class NS < Record
230
- attr_accessor :host, :domainname
6
+ module Zonesync
7
+ class Zonefile
8
+ extend T::Sig
231
9
 
232
- def initialize(vars, zonefile_record)
233
- @vars = vars
234
- if zonefile_record
235
- self.host = qualify_host(zonefile_record.host.to_s)
236
- @vars[:last_host] = host
237
- self.ttl = zonefile_record.ttl.to_i
238
- self.klass = zonefile_record.klass.to_s
239
- self.domainname = qualify_host(zonefile_record.nameserver.to_s)
240
- self.comment = zonefile_record.comment&.to_s
241
- end
10
+ sig { params(zone_string: String).returns(Zonefile) }
11
+ def self.load(zone_string)
12
+ if zone_string !~ /\sSOA\s/ # insert dummy SOA to trick parser if needed
13
+ zone_string.sub!(/\n([^$])/, "\n@ 1 SOA example.com example.com ( 2000010101 1 1 1 1 )\n\\1")
242
14
  end
243
-
244
- alias nameserver domainname
245
- end
246
-
247
- class PTR < Record
248
- attr_accessor :host, :domainname
249
-
250
- def initialize(vars, zonefile_record)
251
- @vars = vars
252
- if zonefile_record
253
- self.host = qualify_host(zonefile_record.host.to_s)
254
- @vars[:last_host] = host
255
- self.ttl = zonefile_record.ttl.to_i
256
- self.klass = zonefile_record.klass.to_s
257
- self.domainname = qualify_host(zonefile_record.target.to_s)
258
- self.comment = zonefile_record.comment&.to_s
259
- end
15
+ zone = Parser.parse(zone_string)
16
+ records = zone.records.map do |dns_zonefile_record|
17
+ Zonesync::Record.from_dns_zonefile_record(dns_zonefile_record)
260
18
  end
261
-
262
- alias target domainname
19
+ new(records, origin: zone.origin)
263
20
  end
264
21
 
265
- class SRV < Record
266
- attr_accessor :host, :priority, :weight, :port, :domainname
267
-
268
- def initialize(vars, zonefile_record)
269
- @vars = vars
270
- if zonefile_record
271
- self.host = qualify_host(zonefile_record.host.to_s)
272
- @vars[:last_host] = host
273
- self.ttl = zonefile_record.ttl.to_i
274
- self.klass = zonefile_record.klass.to_s
275
- self.priority = zonefile_record.priority.to_i
276
- self.weight = zonefile_record.weight.to_i
277
- self.port = zonefile_record.port.to_i
278
- self.domainname = qualify_host(zonefile_record.target.to_s)
279
- self.comment = zonefile_record.comment&.to_s
280
- end
281
- end
282
-
283
- alias target domainname
22
+ sig { params(records: T::Array[Zonesync::Record], origin: String).void }
23
+ def initialize records, origin:
24
+ @records = records
25
+ @origin = origin
284
26
  end
285
27
 
286
- class SSHFP < Record
287
- attr_accessor :host, :alg, :fptype, :fp
28
+ sig { returns(T::Array[Zonesync::Record]) }
29
+ attr_reader :records
288
30
 
289
- def initialize(vars, zonefile_record)
290
- @vars = vars
291
- if zonefile_record
292
- self.host = qualify_host(zonefile_record.host.to_s)
293
- @vars[:last_host] = host
294
- self.ttl = zonefile_record.ttl.to_i
295
- self.klass = zonefile_record.klass.to_s
296
- self.alg = zonefile_record.alg.to_i
297
- self.fptype = zonefile_record.fptype.to_i
298
- self.fp = zonefile_record.fp.to_s
299
- self.comment = zonefile_record.comment&.to_s
300
- end
301
- end
302
- end
303
-
304
- class TXT < Record
305
- attr_accessor :host, :data
306
-
307
- def initialize(vars, zonefile_record)
308
- @vars = vars
309
- if zonefile_record
310
- self.host = qualify_host(zonefile_record.host.to_s)
311
- @vars[:last_host] = host
312
- self.ttl = zonefile_record.ttl.to_i
313
- self.klass = zonefile_record.klass.to_s
314
- self.data = zonefile_record.data.to_s
315
- self.comment = zonefile_record.comment&.to_s
316
- end
317
- end
318
- end
319
-
320
- class SPF < TXT
321
- end
31
+ sig { returns(String) }
32
+ attr_reader :origin
322
33
  end
323
34
  end