record_store 2.1.0 → 3.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b939adb80cff762bc3463596e80a684a8a4accc6
4
- data.tar.gz: a97fdd41e9ef24859af390d2004ff3468911b637
3
+ metadata.gz: 6533d74657e0ba545a411f88adeab33272cb728b
4
+ data.tar.gz: 79cdc4fd12512478cad79039f0c69eb38b7cd504
5
5
  SHA512:
6
- metadata.gz: 187c99f674558e6032f722d9bc352acca2385380c981dacae81d1ae1e24b7f4893d6bc4e61e91d1eb1e4ffe028d42fe128c4ec8b7cd15cd2f9c56160085c6c02
7
- data.tar.gz: 33e19e66a3d68da1f1af1eb0147dca02e430e769bee89617f199c3684509a2137f9eca5ff52fbf20e007a27c2399524e92ec9b3abf70af3ac05df22998afb34c
6
+ metadata.gz: 14019ec878ecc6593d7034c17c0cbd83eefeefc9b52b30ec8bb03c3178861ccd1c89c53c294654defec078bef29a3ca1eedd0ea72c838f0bd83cc25c70f9a7e2
7
+ data.tar.gz: 418e35bc70829c278102d6f4442ef4c0a93612055ccf48eed85812b7bda57eac81475f1d1df643349d15ea4ed5342a5c93e9f2edd504083285c7d81409707850
data/bin/console CHANGED
@@ -5,6 +5,7 @@ require 'bundler/setup'
5
5
  require 'record_store'
6
6
 
7
7
  RecordStore.zones_path = File.expand_path('../../dev/zones', __FILE__)
8
+ RecordStore.config_path = File.expand_path('../../dev/config.yml', __FILE__)
8
9
 
9
- require 'irb'
10
- IRB.start
10
+ require 'pry'
11
+ binding.pry(RecordStore)
data/lib/record_store.rb CHANGED
@@ -2,6 +2,7 @@ require 'json'
2
2
  require 'yaml'
3
3
  require 'active_support'
4
4
  require 'active_support/core_ext/array/wrap'
5
+ require 'active_support/core_ext/enumerable'
5
6
  require 'active_support/core_ext/hash/keys'
6
7
  require 'active_support/core_ext/string'
7
8
  require 'active_model'
@@ -20,6 +21,7 @@ require 'record_store/record/ns'
20
21
  require 'record_store/record/txt'
21
22
  require 'record_store/record/spf'
22
23
  require 'record_store/record/srv'
24
+ require 'record_store/zone/yaml_definitions'
23
25
  require 'record_store/zone'
24
26
  require 'record_store/zone/config'
25
27
  require 'record_store/changeset'
@@ -33,15 +33,34 @@ module RecordStore
33
33
  end
34
34
  end
35
35
 
36
- attr_reader :current_records, :desired_records, :removals, :additions, :updates
36
+ attr_reader :current_records, :desired_records, :removals, :additions, :updates, :provider, :zone
37
+
38
+ def self.build_from(provider:, zone:)
39
+ current_zone = provider.build_zone(zone_name: zone.unrooted_name, config: zone.config)
40
+
41
+ self.new(
42
+ current_records: current_zone.records,
43
+ desired_records: zone.records,
44
+ provider: provider,
45
+ zone: zone.unrooted_name
46
+ )
47
+ end
48
+
49
+ def initialize(current_records: [], desired_records: [], provider:, zone:)
50
+ @current_records = Set.new(current_records)
51
+ @desired_records = Set.new(desired_records)
52
+ @provider = provider
53
+ @zone = zone
37
54
 
38
- def initialize(current_records: [], desired_records: [])
39
- @current_records, @desired_records = Set.new(current_records), Set.new(desired_records)
40
55
  @additions, @removals, @updates = [], [], []
41
56
 
42
57
  build_changeset
43
58
  end
44
59
 
60
+ def apply
61
+ provider.apply_changeset(self)
62
+ end
63
+
45
64
  def unchanged
46
65
  current_records & desired_records
47
66
  end
@@ -11,14 +11,18 @@ module RecordStore
11
11
  desc 'thaw', 'Thaws all zones under management to allow manual edits'
12
12
  def thaw
13
13
  Zone.each do |_, zone|
14
- zone.provider.thaw if zone.provider.thawable?
14
+ zone.providers.each do |provider|
15
+ provider.thaw_zone(zone.unrooted_name) if provider.thawable?
16
+ end
15
17
  end
16
18
  end
17
19
 
18
20
  desc 'freeze', 'Freezes all zones under management to prevent manual edits'
19
21
  def freeze
20
22
  Zone.each do |_, zone|
21
- zone.provider.freeze_zone if zone.provider.freezable?
23
+ zone.providers.each do |provider|
24
+ provider.freeze_zone(zone.unrooted_name) if provider.freezable?
25
+ end
22
26
  end
23
27
  end
24
28
 
@@ -35,47 +39,55 @@ module RecordStore
35
39
  option :verbose, desc: 'Print records that haven\'t diverged', aliases: '-v', type: :boolean, default: false
36
40
  desc 'diff', 'Displays the DNS differences between the zone files in this repo and production'
37
41
  def diff
42
+ puts "Diffing #{Zone.defined.count} zones"
43
+
38
44
  Zone.each do |name, zone|
45
+ changesets = zone.build_changesets
46
+
47
+ next if !options.fetch('verbose') && changesets.empty?
39
48
  puts "Zone: #{name}"
40
49
 
41
- diff = zone.changeset
50
+ changesets.each do |changeset|
51
+ next if !options.fetch('verbose') && changeset.changes.empty?
42
52
 
43
- if !diff.additions.empty? || options.fetch('verbose')
44
- puts "Add:"
45
- diff.additions.map(&:record).each do |record|
46
- puts " - #{record.to_s}"
53
+ puts '-' * 20
54
+ puts "Provider: #{changeset.provider.to_s}"
55
+
56
+ if !changeset.additions.empty? || options.fetch('verbose')
57
+ puts "Add:"
58
+ changeset.additions.map(&:record).each do |record|
59
+ puts " - #{record.to_s}"
60
+ end
47
61
  end
48
- end
49
62
 
50
- if !diff.removals.empty? || options.fetch('verbose')
51
- puts "Remove:"
52
- diff.removals.map(&:record).each do |record|
53
- puts " - #{record.to_s}"
63
+ if !changeset.removals.empty? || options.fetch('verbose')
64
+ puts "Remove:"
65
+ changeset.removals.map(&:record).each do |record|
66
+ puts " - #{record.to_s}"
67
+ end
54
68
  end
55
- end
56
69
 
57
- if !diff.updates.empty? || options.fetch('verbose')
58
- puts "Update:"
59
- diff.updates.map(&:record).each do |record|
60
- puts " - #{record.to_s}"
70
+ if !changeset.updates.empty? || options.fetch('verbose')
71
+ puts "Update:"
72
+ changeset.updates.map(&:record).each do |record|
73
+ puts " - #{record.to_s}"
74
+ end
61
75
  end
62
- end
63
76
 
64
- if options.fetch('verbose')
65
- puts "Unchanged:"
66
- diff.unchanged.each do |record|
67
- puts " - #{record.to_s}"
77
+ if options.fetch('verbose')
78
+ puts "Unchanged:"
79
+ changeset.unchanged.each do |record|
80
+ puts " - #{record.to_s}"
81
+ end
68
82
  end
69
83
  end
70
-
71
- puts "Empty diff" if diff.changes.empty?
72
84
  puts '=' * 20
73
85
  end
74
86
  end
75
87
 
76
88
  desc 'apply', 'Applies the DNS changes'
77
89
  def apply
78
- zones = zones_modified
90
+ zones = Zone.modified
79
91
 
80
92
  if zones.empty?
81
93
  puts "No changes to sync"
@@ -84,8 +96,9 @@ module RecordStore
84
96
 
85
97
  zones.each do |zone|
86
98
  abort "Attempted to apply invalid zone: #{zone.name}" unless zone.valid?
87
- provider = zone.provider
88
- provider.apply_changeset(zone.changeset)
99
+
100
+ changesets = zone.build_changesets
101
+ changesets.each(&:apply)
89
102
  end
90
103
 
91
104
  puts "All zone changes deployed"
@@ -93,7 +106,6 @@ module RecordStore
93
106
 
94
107
  option :name, desc: 'Zone to download', aliases: '-n', type: :string, required: true
95
108
  option :provider, desc: 'Provider in which this zone exists', aliases: '-p', type: :string
96
- option :format, desc: 'Format', aliases: '-f', type: :string, default: 'file', enum: FORMATS
97
109
  desc 'download', 'Downloads all records from zone and creates YAML zone definition in zones/ e.g. record-store download --name=shopify.io'
98
110
  def download
99
111
  name = options.fetch('name')
@@ -109,23 +121,19 @@ module RecordStore
109
121
  end
110
122
 
111
123
  puts "Downloading records for #{name}"
112
- Zone.download(name, provider, format: options.fetch('format').to_sym)
124
+ Zone.download(name, provider)
113
125
  puts "Records have been downloaded & can be found in zones/#{name}.yml"
114
126
  end
115
127
 
116
128
  option :name, desc: 'Zone to reformat', aliases: '-n', type: :string, required: false
117
- option :format, desc: 'Format', aliases: '-f', type: :string, default: 'file', enum: FORMATS
118
129
  desc 'reformat', 'Sorts and re-outputs the zone (or all zones) as specified format (file)'
119
130
  def reformat
120
131
  name = options['name']
121
- zones = if name
122
- [Zone.find(name)]
123
- else
124
- Zone.all
125
- end
132
+ zones = name ? [Zone.find(name)] : Zone.all
133
+
126
134
  zones.each do |zone|
127
135
  puts "Writing #{zone.name}"
128
- zone.write(format: options.fetch('format').to_sym)
136
+ zone.write
129
137
  end
130
138
  end
131
139
 
@@ -163,7 +171,7 @@ module RecordStore
163
171
 
164
172
  desc 'assert_empty_diff', 'Asserts there is no divergence between DynECT & the zone files'
165
173
  def assert_empty_diff
166
- zones = zones_modified.map(&:name)
174
+ zones = Zone.modified.map(&:name)
167
175
 
168
176
  unless zones.empty?
169
177
  abort "The following zones have diverged: #{zones.join(', ')}"
@@ -173,23 +181,21 @@ module RecordStore
173
181
  desc 'validate_records', 'Validates that all DNS records have valid definitions'
174
182
  def validate_records
175
183
  invalid_zones = []
176
- Zone.each do |name, zone|
177
- if !zone.valid?
178
- invalid_zones << name
184
+ Zone.all.reject(&:valid?).each do |zone|
185
+ invalid_zones << zone.unrooted_name
179
186
 
180
- puts "#{name} definition is not valid:"
181
- zone.errors.each do |field, msg|
182
- puts " - #{field}: #{msg}"
183
- end
187
+ puts "#{zone.unrooted_name} definition is not valid:"
188
+ zone.errors.each do |field, msg|
189
+ puts " - #{field}: #{msg}"
190
+ end
184
191
 
185
- invalid_records = zone.records.reject(&:valid?)
186
- puts ' Invalid records' if invalid_records.size > 0
192
+ invalid_records = zone.records.reject(&:valid?)
193
+ puts ' Invalid records' if invalid_records.size > 0
187
194
 
188
- invalid_records.each do |record|
189
- puts " #{record.to_s}"
190
- record.errors.each do |field, msg|
191
- puts " - #{field}: #{msg}"
192
- end
195
+ invalid_records.each do |record|
196
+ puts " #{record.to_s}"
197
+ record.errors.each do |field, msg|
198
+ puts " - #{field}: #{msg}"
193
199
  end
194
200
  end
195
201
  end
@@ -197,13 +203,13 @@ module RecordStore
197
203
  if invalid_zones.size > 0
198
204
  abort "The following zones were invalid: #{invalid_zones.join(', ')}"
199
205
  else
200
- puts "All records have valid definitions."
206
+ puts "All zones have valid definitions."
201
207
  end
202
208
  end
203
209
 
204
210
  desc 'validate_change_size', "Validates no more then particular limit of DNS records are removed per zone at a time"
205
211
  def validate_change_size
206
- zones = zones_modified
212
+ zones = Zone.modified
207
213
 
208
214
  unless zones.empty?
209
215
  removals = zones.select do |zone|
@@ -256,18 +262,5 @@ module RecordStore
256
262
  end
257
263
  end
258
264
  end
259
-
260
- private
261
-
262
- def zones_modified
263
- modified_zones, mutex = [], Mutex.new
264
- Zone.all.map do |zone|
265
- thread = Thread.new do
266
- mutex.synchronize {modified_zones << zone} unless zone.unchanged?
267
- end
268
- end.each(&:join)
269
-
270
- modified_zones
271
- end
272
265
  end
273
266
  end
@@ -1,107 +1,117 @@
1
1
  module RecordStore
2
2
  class Provider
3
- def self.provider_for(zone_name)
4
- dns = Resolv::DNS.new(nameserver: ['8.8.8.8', '8.8.4.4'])
3
+ class << self
4
+ def provider_for(zone_name)
5
+ dns = Resolv::DNS.new(nameserver: ['8.8.8.8', '8.8.4.4'])
6
+
7
+ begin
8
+ ns_server = dns.getresource(zone_name, Resolv::DNS::Resource::IN::SOA).mname.to_s
9
+ rescue Resolv::ResolvError => e
10
+ abort "Domain doesn't exist"
11
+ end
5
12
 
6
- begin
7
- ns_server = dns.getresource(zone_name, Resolv::DNS::Resource::IN::SOA).mname.to_s
8
- rescue Resolv::ResolvError => e
9
- abort "Domain doesn't exist"
13
+ case ns_server
14
+ when /dnsimple\.com\z/
15
+ 'DNSimple'
16
+ when /dynect\.net\z/
17
+ 'DynECT'
18
+ else
19
+ nil
20
+ end
10
21
  end
11
22
 
12
- case ns_server
13
- when /dnsimple\.com\z/
14
- 'DNSimple'
15
- when /dynect\.net\z/
16
- 'DynECT'
17
- else
18
- nil
23
+ def record_types
24
+ Set.new([
25
+ 'A',
26
+ 'AAAA',
27
+ 'ALIAS',
28
+ 'CNAME',
29
+ 'MX',
30
+ 'NS',
31
+ 'SPF',
32
+ 'SRV',
33
+ 'TXT',
34
+ ])
19
35
  end
20
- end
21
36
 
22
- def self.record_types
23
- Set.new([
24
- 'A',
25
- 'AAAA',
26
- 'ALIAS',
27
- 'CNAME',
28
- 'MX',
29
- 'NS',
30
- 'SPF',
31
- 'SRV',
32
- 'TXT',
33
- ])
34
- end
37
+ def supports_alias?
38
+ false
39
+ end
35
40
 
36
- def self.supports_alias?
37
- false
38
- end
41
+ def build_zone(zone_name:, config:)
42
+ zone = Zone.new(name: zone_name)
43
+ zone.records = retrieve_current_records(zone: zone_name)
44
+ zone.config = config
39
45
 
40
- def initialize(zone:)
41
- @zone_name = zone
42
- end
46
+ zone
47
+ end
43
48
 
44
- def add(record)
45
- raise NotImplementedError
46
- end
49
+ # returns an array of Record objects that match the records which exist in the provider
50
+ def retrieve_current_records(zone:, stdout: $stdout)
51
+ raise NotImplementedError
52
+ end
47
53
 
48
- def remove(record)
49
- raise NotImplementedError
50
- end
54
+ def add(record)
55
+ raise NotImplementedError
56
+ end
51
57
 
52
- def update(id, record)
53
- raise NotImplementedError
54
- end
58
+ def remove(record)
59
+ raise NotImplementedError
60
+ end
55
61
 
56
- # Applies changeset to provider
57
- def apply_changeset(changeset, stdout = $stdout)
58
- begin
59
- stdout.puts "Applying #{changeset.additions.size} additions, #{changeset.removals.size} removals, & #{changeset.updates.size} updates..."
60
-
61
- changeset.changes.each do |change|
62
- case change.type
63
- when :removal;
64
- stdout.puts "Removing #{change.record}..."
65
- remove(change.record)
66
- when :addition;
67
- stdout.puts "Creating #{change.record}..."
68
- add(change.record)
69
- when :update;
70
- stdout.puts "Updating record with ID #{change.id} to #{change.record}..."
71
- update(change.id, change.record)
72
- else
73
- raise ArgumentError, "Unknown change type #{change.type.inspect}"
62
+ def update(id, record)
63
+ raise NotImplementedError
64
+ end
65
+
66
+ # Applies changeset to provider
67
+ def apply_changeset(changeset, stdout = $stdout)
68
+ begin
69
+ stdout.puts "Applying #{changeset.additions.size} additions, #{changeset.removals.size} removals, & #{changeset.updates.size} updates..."
70
+
71
+ changeset.changes.each do |change|
72
+ case change.type
73
+ when :removal;
74
+ stdout.puts "Removing #{change.record}..."
75
+ remove(change.record, changeset.zone)
76
+ when :addition;
77
+ stdout.puts "Creating #{change.record}..."
78
+ add(change.record, changeset.zone)
79
+ when :update;
80
+ stdout.puts "Updating record with ID #{change.id} to #{change.record}..."
81
+ update(change.id, change.record, changeset.zone)
82
+ else
83
+ raise ArgumentError, "Unknown change type #{change.type.inspect}"
84
+ end
74
85
  end
75
- end
76
86
 
77
- puts "\nPublished #{@zone_name} changes"
87
+ puts "\nPublished #{changeset.zone} changes to #{changeset.provider.to_s}\n"
88
+ end
78
89
  end
79
- end
80
90
 
81
- # returns an array of Record objects that match the records which exist in the provider
82
- def retrieve_current_records(stdout = $stdout)
83
- raise NotImplementedError
84
- end
91
+ # Returns an array of the zones managed by provider as strings
92
+ def zones
93
+ raise NotImplementedError
94
+ end
85
95
 
86
- # Returns an array of the zones managed by provider as strings
87
- def zones
88
- raise NotImplementedError
89
- end
96
+ def secrets
97
+ @secrets ||= if File.exists?(RecordStore.secrets_path)
98
+ JSON.parse(File.read(RecordStore.secrets_path))
99
+ else
100
+ raise "You don't have a secrets.json file set up!"
101
+ end
102
+ end
90
103
 
91
- def secrets
92
- @secrets ||= if File.exists?(RecordStore.secrets_path)
93
- JSON.parse(File.read(RecordStore.secrets_path))
94
- else
95
- raise "You don't have a secrets.json file set up!"
104
+ def thawable?
105
+ self.respond_to?(:thaw_zone)
96
106
  end
97
- end
98
107
 
99
- def thawable?
100
- self.class.method_defined?(:thaw)
101
- end
108
+ def freezable?
109
+ self.respond_to?(:freeze_zone)
110
+ end
102
111
 
103
- def freezable?
104
- self.class.method_defined?(:freeze_zone)
112
+ def to_s
113
+ self.name.demodulize
114
+ end
105
115
  end
106
116
  end
107
117
  end