zonesync 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CLAUDE.md +90 -0
  3. data/Gemfile +1 -0
  4. data/lib/zonesync/cli.rb +9 -0
  5. data/lib/zonesync/cloudflare.rb +64 -39
  6. data/lib/zonesync/diff.rb +10 -1
  7. data/lib/zonesync/errors.rb +35 -0
  8. data/lib/zonesync/generate.rb +14 -0
  9. data/lib/zonesync/http.rb +24 -8
  10. data/lib/zonesync/logger.rb +20 -15
  11. data/lib/zonesync/manifest.rb +68 -21
  12. data/lib/zonesync/parser.rb +337 -0
  13. data/lib/zonesync/provider.rb +105 -26
  14. data/lib/zonesync/record.rb +18 -23
  15. data/lib/zonesync/record_hash.rb +15 -0
  16. data/lib/zonesync/route53.rb +146 -28
  17. data/lib/zonesync/sync.rb +51 -0
  18. data/lib/zonesync/validator.rb +77 -19
  19. data/lib/zonesync/version.rb +1 -1
  20. data/lib/zonesync/zonefile.rb +22 -311
  21. data/lib/zonesync.rb +28 -60
  22. data/sorbet/config +4 -0
  23. data/sorbet/rbi/annotations/.gitattributes +1 -0
  24. data/sorbet/rbi/annotations/activesupport.rbi +457 -0
  25. data/sorbet/rbi/annotations/minitest.rbi +119 -0
  26. data/sorbet/rbi/annotations/webmock.rbi +9 -0
  27. data/sorbet/rbi/gems/.gitattributes +1 -0
  28. data/sorbet/rbi/gems/activesupport@8.0.1.rbi +18474 -0
  29. data/sorbet/rbi/gems/addressable@2.8.7.rbi +1994 -0
  30. data/sorbet/rbi/gems/base64@0.2.0.rbi +507 -0
  31. data/sorbet/rbi/gems/benchmark@0.4.0.rbi +618 -0
  32. data/sorbet/rbi/gems/bigdecimal@3.1.9.rbi +9 -0
  33. data/sorbet/rbi/gems/concurrent-ruby@1.3.4.rbi +11645 -0
  34. data/sorbet/rbi/gems/connection_pool@2.4.1.rbi +9 -0
  35. data/sorbet/rbi/gems/crack@1.0.0.rbi +145 -0
  36. data/sorbet/rbi/gems/date@3.4.1.rbi +75 -0
  37. data/sorbet/rbi/gems/diff-lcs@1.5.1.rbi +1131 -0
  38. data/sorbet/rbi/gems/drb@2.2.1.rbi +1347 -0
  39. data/sorbet/rbi/gems/erubi@1.13.1.rbi +155 -0
  40. data/sorbet/rbi/gems/hashdiff@1.1.2.rbi +353 -0
  41. data/sorbet/rbi/gems/i18n@1.14.6.rbi +2275 -0
  42. data/sorbet/rbi/gems/io-console@0.8.0.rbi +9 -0
  43. data/sorbet/rbi/gems/logger@1.6.4.rbi +940 -0
  44. data/sorbet/rbi/gems/minitest@5.25.4.rbi +1547 -0
  45. data/sorbet/rbi/gems/netrc@0.11.0.rbi +159 -0
  46. data/sorbet/rbi/gems/parallel@1.26.3.rbi +291 -0
  47. data/sorbet/rbi/gems/polyglot@0.3.5.rbi +42 -0
  48. data/sorbet/rbi/gems/prism@1.3.0.rbi +40040 -0
  49. data/sorbet/rbi/gems/psych@5.2.2.rbi +1785 -0
  50. data/sorbet/rbi/gems/public_suffix@6.0.1.rbi +936 -0
  51. data/sorbet/rbi/gems/rake@13.2.1.rbi +3028 -0
  52. data/sorbet/rbi/gems/rbi@0.2.2.rbi +4527 -0
  53. data/sorbet/rbi/gems/rdoc@6.10.0.rbi +12766 -0
  54. data/sorbet/rbi/gems/reline@0.6.0.rbi +9 -0
  55. data/sorbet/rbi/gems/rexml@3.4.0.rbi +4974 -0
  56. data/sorbet/rbi/gems/rspec-core@3.13.2.rbi +10896 -0
  57. data/sorbet/rbi/gems/rspec-expectations@3.13.3.rbi +8183 -0
  58. data/sorbet/rbi/gems/rspec-mocks@3.13.2.rbi +5341 -0
  59. data/sorbet/rbi/gems/rspec-support@3.13.2.rbi +1630 -0
  60. data/sorbet/rbi/gems/rspec@3.13.0.rbi +83 -0
  61. data/sorbet/rbi/gems/securerandom@0.4.1.rbi +75 -0
  62. data/sorbet/rbi/gems/spoom@1.5.0.rbi +4932 -0
  63. data/sorbet/rbi/gems/stringio@3.1.2.rbi +9 -0
  64. data/sorbet/rbi/gems/tapioca@0.16.6.rbi +3611 -0
  65. data/sorbet/rbi/gems/thor@1.3.2.rbi +4378 -0
  66. data/sorbet/rbi/gems/treetop@1.6.12.rbi +1895 -0
  67. data/sorbet/rbi/gems/tzinfo@2.0.6.rbi +5918 -0
  68. data/sorbet/rbi/gems/uri@1.0.2.rbi +2340 -0
  69. data/sorbet/rbi/gems/webmock@3.24.0.rbi +1780 -0
  70. data/sorbet/rbi/gems/yard-sorbet@0.9.0.rbi +435 -0
  71. data/sorbet/rbi/gems/yard@0.9.37.rbi +18379 -0
  72. data/sorbet/rbi/todo.rbi +7 -0
  73. data/sorbet/tapioca/config.yml +13 -0
  74. data/sorbet/tapioca/require.rb +4 -0
  75. data/zonesync.gemspec +3 -0
  76. metadata +102 -2
@@ -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
data/lib/zonesync.rb CHANGED
@@ -1,82 +1,50 @@
1
+ # typed: strict
2
+ require "sorbet-runtime"
3
+
4
+ require "zonesync/sync"
5
+ require "zonesync/generate"
1
6
  require "zonesync/provider"
2
- require "zonesync/diff"
3
- require "zonesync/validator"
4
- require "zonesync/logger"
5
7
  require "zonesync/cli"
6
8
  require "zonesync/rake"
7
9
  require "zonesync/errors"
10
+ require "zonesync/record_hash"
11
+
12
+ begin # optional active_support dependency
13
+ require "active_support"
14
+ require "active_support/encrypted_configuration"
15
+ require "active_support/core_ext/hash/keys"
16
+ rescue LoadError; end
8
17
 
9
18
  module Zonesync
10
- def self.call source: "Zonefile", destination: "zonesync", dry_run: false
19
+ extend T::Sig
20
+
21
+ sig { params(source: T.nilable(String), destination: T.nilable(String), dry_run: T::Boolean, force: T::Boolean).void }
22
+ def self.call source: "Zonefile", destination: "zonesync", dry_run: false, force: false
23
+ source = T.must(source)
24
+ destination = T.must(destination).to_sym
11
25
  Sync.new(
12
- { provider: "Filesystem", path: source },
13
- credentials[destination]
14
- ).call(dry_run: dry_run)
26
+ Provider.from({ provider: "Filesystem", path: source }),
27
+ Provider.from(credentials(destination)),
28
+ ).call(dry_run: dry_run, force: force)
15
29
  end
16
30
 
31
+ sig { params(source: T.nilable(String), destination: T.nilable(String)).void }
17
32
  def self.generate source: "zonesync", destination: "Zonefile"
33
+ source = T.must(source).to_sym
18
34
  Generate.new(
19
- credentials[source],
20
- { provider: "Filesystem", path: destination }
35
+ Provider.from(credentials(source)),
36
+ Provider.from({ provider: "Filesystem", path: T.must(destination) }),
21
37
  ).call
22
38
  end
23
39
 
24
- def self.credentials
25
- require "active_support"
26
- require "active_support/encrypted_configuration"
27
- require "active_support/core_ext/hash/keys"
40
+ sig { params(key: Symbol).returns(T::Hash[Symbol, String]) }
41
+ def self.credentials key
28
42
  ActiveSupport::EncryptedConfiguration.new(
29
43
  config_path: "config/credentials.yml.enc",
30
44
  key_path: "config/master.key",
31
45
  env_key: "RAILS_MASTER_KEY",
32
46
  raise_if_missing_key: true,
33
- )
34
- end
35
-
36
- class Sync < Struct.new(:source, :destination)
37
- def call dry_run: false
38
- source = Provider.from(self.source)
39
- destination = Provider.from(self.destination)
40
- operations = Diff.call(
41
- from: destination.diffable_records,
42
- to: source.diffable_records,
43
- )
44
-
45
- Validator.call(operations, destination)
46
-
47
- smanifest = source.manifest.generate
48
- dmanifest = destination.manifest.existing
49
- if smanifest != dmanifest
50
- if dmanifest
51
- operations << [:change, [dmanifest, smanifest]]
52
- else
53
- operations << [:add, [smanifest]]
54
- end
55
- end
56
-
57
- schecksum = source.manifest.generate_checksum
58
- dchecksum = destination.manifest.existing_checksum
59
- if schecksum != dchecksum
60
- if dchecksum
61
- operations << [:change, [dchecksum, schecksum]]
62
- else
63
- operations << [:add, [schecksum]]
64
- end
65
- end
66
-
67
- operations.each do |method, args|
68
- Logger.log(method, args, dry_run: dry_run)
69
- destination.send(method, *args) unless dry_run
70
- end
71
- end
72
- end
73
-
74
- class Generate < Struct.new(:source, :destination)
75
- def call
76
- source = Provider.from(self.source)
77
- destination = Provider.from(self.destination)
78
- destination.write(source.read)
79
- end
47
+ ).config[key]
80
48
  end
81
49
  end
82
50
 
data/sorbet/config ADDED
@@ -0,0 +1,4 @@
1
+ --dir
2
+ .
3
+ --ignore=tmp/
4
+ --ignore=vendor/
@@ -0,0 +1 @@
1
+ **/*.rbi linguist-vendored=true