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 +4 -4
- data/bin/console +3 -2
- data/lib/record_store.rb +2 -0
- data/lib/record_store/changeset.rb +22 -3
- data/lib/record_store/cli.rb +59 -66
- data/lib/record_store/provider.rb +92 -82
- data/lib/record_store/provider/dnsimple.rb +123 -133
- data/lib/record_store/provider/dynect.rb +73 -71
- data/lib/record_store/record.rb +1 -0
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +62 -135
- data/lib/record_store/zone/config.rb +16 -9
- data/lib/record_store/zone/yaml_definitions.rb +105 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6533d74657e0ba545a411f88adeab33272cb728b
|
4
|
+
data.tar.gz: 79cdc4fd12512478cad79039f0c69eb38b7cd504
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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 '
|
10
|
-
|
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
|
data/lib/record_store/cli.rb
CHANGED
@@ -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.
|
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.
|
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
|
-
|
50
|
+
changesets.each do |changeset|
|
51
|
+
next if !options.fetch('verbose') && changeset.changes.empty?
|
42
52
|
|
43
|
-
|
44
|
-
puts "
|
45
|
-
|
46
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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 =
|
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
|
-
|
88
|
-
|
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
|
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 =
|
122
|
-
|
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
|
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 =
|
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 |
|
177
|
-
|
178
|
-
invalid_zones << name
|
184
|
+
Zone.all.reject(&:valid?).each do |zone|
|
185
|
+
invalid_zones << zone.unrooted_name
|
179
186
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
186
|
-
|
192
|
+
invalid_records = zone.records.reject(&:valid?)
|
193
|
+
puts ' Invalid records' if invalid_records.size > 0
|
187
194
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
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
|
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 =
|
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
|
-
|
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
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
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
|
-
|
41
|
-
|
42
|
-
end
|
46
|
+
zone
|
47
|
+
end
|
43
48
|
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
49
|
-
|
50
|
-
|
54
|
+
def add(record)
|
55
|
+
raise NotImplementedError
|
56
|
+
end
|
51
57
|
|
52
|
-
|
53
|
-
|
54
|
-
|
58
|
+
def remove(record)
|
59
|
+
raise NotImplementedError
|
60
|
+
end
|
55
61
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
87
|
+
puts "\nPublished #{changeset.zone} changes to #{changeset.provider.to_s}\n"
|
88
|
+
end
|
78
89
|
end
|
79
|
-
end
|
80
90
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
91
|
+
# Returns an array of the zones managed by provider as strings
|
92
|
+
def zones
|
93
|
+
raise NotImplementedError
|
94
|
+
end
|
85
95
|
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
|
92
|
-
|
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
|
-
|
100
|
-
|
101
|
-
|
108
|
+
def freezable?
|
109
|
+
self.respond_to?(:freeze_zone)
|
110
|
+
end
|
102
111
|
|
103
|
-
|
104
|
-
|
112
|
+
def to_s
|
113
|
+
self.name.demodulize
|
114
|
+
end
|
105
115
|
end
|
106
116
|
end
|
107
117
|
end
|