record_store 2.1.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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