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.
@@ -1,3 +1,3 @@
1
1
  module RecordStore
2
- VERSION = '2.1.0'
2
+ VERSION = '3.0.0'
3
3
  end
@@ -1,108 +1,10 @@
1
1
  module RecordStore
2
2
  class Zone
3
- module YamlDefinitions
4
- include Enumerable
5
-
6
- def all
7
- defined.values
8
- end
9
-
10
- def defined
11
- @defined ||= yaml_files.inject({}) { |zones, file| zones.merge(load_yml_zone_definition(file)) }
12
- end
13
-
14
- def [](name)
15
- defined.fetch(name)
16
- end
17
-
18
- def each(&block)
19
- defined.each(&block)
20
- end
21
-
22
- def find(name)
23
- return unless File.exists?(zone_path = "#{RecordStore.zones_path}/#{name}.yml")
24
- load_yml_zone_definition(zone_path).first.last
25
- end
26
-
27
- def write(name, config:, records:, format: :file)
28
- raise ArgumentError, "format must be :directory or :file" unless %i(file directory).include?(format)
29
- name = name.chomp('.')
30
- zone_file = "#{RecordStore.zones_path}/#{name}.yml"
31
- zone = { name => { config: config.to_hash } }
32
- records = records.map(&:to_hash).sort_by! {|r| [r.fetch(:fqdn), r.fetch(:type), r[:nsdname] || r[:address]]}
33
-
34
- if format == :file
35
- zone[name][:records] = records
36
- write_yml_file(zone_file, zone.deep_stringify_keys)
37
- remove_record_files(name)
38
- else
39
- write_yml_file(zone_file, zone.deep_stringify_keys)
40
- remove_record_files(name)
41
- write_record_files(name, records)
42
- end
43
- end
44
-
45
- private
46
-
47
- def write_yml_file(filename, data)
48
- lines = data.to_yaml.lines
49
- lines.shift if lines.first == "---\n"
50
- File.write(filename, lines.join)
51
- end
52
-
53
- def load_yml_zone_definition(filename)
54
- result = {}
55
- dir = File.dirname(filename)
56
- YAML.load_file(filename).each do |name, definition|
57
- definition['records'] ||= []
58
- Dir["#{dir}/#{name}/*__*.yml"].each do |record_file|
59
- definition['records'] += load_yml_record_definitions(name, record_file)
60
- end
61
- result[name] = Zone.from_yaml_definition(name, definition)
62
- end
63
- result
64
- end
65
-
66
- def load_yml_record_definitions(name, record_file)
67
- type, domain = File.basename(record_file, '.yml').split('__')
68
- Array.wrap(YAML.load_file(record_file)).map do |record_definition|
69
- record_definition.merge(fqdn: "#{domain}.#{name}", type: type)
70
- end
71
- end
72
-
73
- def remove_record_files(name)
74
- dir = "#{RecordStore.zones_path}/#{name}"
75
- File.unlink(*Dir["#{dir}/*"])
76
- Dir.unlink(dir)
77
- rescue Errno::ENOENT
78
- end
79
-
80
- def write_record_files(name, records)
81
- dir = "#{RecordStore.zones_path}/#{name}"
82
- Dir.mkdir(dir)
83
- records.group_by { |record| [record.fetch(:fqdn), record.fetch(:type)] }.each do |(fqdn, type), grouped_records|
84
- grouped_records.each do |record|
85
- record.delete(:fqdn)
86
- record.delete(:type)
87
- record.deep_stringify_keys!
88
- end
89
- grouped_records = grouped_records.first if grouped_records.size == 1
90
- domain = fqdn.chomp('.').chomp(name).chomp('.')
91
- write_yml_file("#{dir}/#{type}__#{domain}.yml", grouped_records)
92
- end
93
- end
94
-
95
- def yaml_files
96
- Dir["#{RecordStore.zones_path}/*.yml"]
97
- end
98
- end
99
-
100
3
  extend YamlDefinitions
101
4
  include ActiveModel::Validations
102
5
 
103
6
  attr_accessor :name
104
7
  attr_reader :config
105
- attr_writer :records
106
8
 
107
9
  validates :name, presence: true, format: { with: Record::FQDN_REGEX, message: 'is not a fully qualified domain name' }
108
10
  validate :validate_records
@@ -115,50 +17,80 @@ module RecordStore
115
17
  validate :validate_can_handle_alias_records
116
18
  validate :validate_alias_points_to_root
117
19
 
118
- def self.from_yaml_definition(name, definition)
119
- new(name, definition.deep_symbolize_keys)
120
- end
20
+ class << self
21
+ def download(name, provider_name, **write_options)
22
+ zone = Zone.new(name: name, config: {providers: [provider_name]})
23
+ raise ArgumentError, zone.errors.full_messages.join("\n") unless zone.valid?
121
24
 
122
- def self.download(name, provider_name, **write_options)
123
- dns = new(name, config: {provider: provider_name}).provider
124
- current_records = dns.retrieve_current_records
125
- write(name, records: current_records, config: {
126
- provider: provider_name,
127
- ignore_patterns: [{type: "NS", fqdn: "#{name}."}],
128
- supports_alias: current_records.map(&:type).include?('ALIAS') || nil
129
- }, **write_options)
130
- end
25
+ zone.records = zone.providers.first.retrieve_current_records(zone: name)
26
+
27
+ zone.config = Zone::Config.new(
28
+ providers: [provider_name],
29
+ ignore_patterns: [{type: "NS", fqdn: "#{name}."}],
30
+ supports_alias: (zone.records.map(&:type).include?('ALIAS') || nil)
31
+ )
32
+
33
+ zone.write(**write_options)
34
+ end
131
35
 
132
- def initialize(name, records: [], config: {})
133
- @name = Record.ensure_ends_with_dot(name)
134
- @config = RecordStore::Zone::Config.new(config)
135
- @records = build_records(records)
36
+ def filter_records(current_records, ignore_patterns)
37
+ ignore_patterns.inject(current_records) do |remaining_records, pattern|
38
+ remaining_records.reject do |record|
39
+ pattern.all? { |(key, value)| record.respond_to?(key) && value === record.send(key) }
40
+ end
41
+ end
42
+ end
43
+
44
+ def modified
45
+ modified_zones, mutex = [], Mutex.new
46
+ self.all.map do |zone|
47
+ thread = Thread.new do
48
+ mutex.synchronize {modified_zones << zone} unless zone.unchanged?
49
+ end
50
+ end.each(&:join)
51
+
52
+ modified_zones
53
+ end
136
54
  end
137
55
 
138
- def changeset
139
- current_records = Zone.filter_records(provider.retrieve_current_records, config.ignore_patterns)
56
+ def initialize(name:, records: [], config: {})
57
+ @name = Record.ensure_ends_with_dot(name)
58
+ @config = RecordStore::Zone::Config.new(config.deep_symbolize_keys)
59
+ @records = build_records(records)
60
+ end
140
61
 
141
- Changeset.new(
142
- current_records: current_records,
143
- desired_records: records
144
- )
62
+ def build_changesets
63
+ @changesets ||= begin
64
+ providers.map do |provider|
65
+ Changeset.build_from(provider: provider, zone: self)
66
+ end
67
+ end
145
68
  end
146
69
 
147
70
  def unchanged?
148
- changeset.empty?
71
+ build_changesets.all?(&:empty?)
72
+ end
73
+
74
+ def unrooted_name
75
+ @name.chomp('.')
149
76
  end
150
77
 
151
78
  def records
152
79
  @records_cache ||= Zone.filter_records(@records, config.ignore_patterns)
153
80
  end
154
81
 
82
+ def records=(records)
83
+ @records_cache = nil
84
+ @records = records
85
+ end
86
+
155
87
  def config=(config)
156
88
  @records_cache = nil
157
89
  @config = config
158
90
  end
159
91
 
160
- def provider
161
- Provider.const_get(config.provider).new(zone: name.chomp('.'))
92
+ def providers
93
+ @providers ||= config.providers.map { |provider| Provider.const_get(provider) }
162
94
  end
163
95
 
164
96
  def write(**write_options)
@@ -232,28 +164,23 @@ module RecordStore
232
164
  end
233
165
  end
234
166
 
235
- def self.filter_records(current_records, ignore_patterns)
236
- ignore_patterns.inject(current_records) do |remaining_records, pattern|
237
- remaining_records.reject do |record|
238
- pattern.all? { |(key, value)| record.respond_to?(key) && value === record.send(key) }
239
- end
240
- end
241
- end
242
-
243
167
  def validate_provider_can_handle_zone_records
244
168
  record_types = records.map(&:type).to_set
245
169
  return unless config.valid?
246
- provider_supported_record_types = Provider.const_get(config.provider).record_types
247
170
 
248
- (record_types - provider_supported_record_types).each do |record_type|
249
- errors.add(:records, "#{record_type} is a not a supported record type in #{config.provider}")
171
+ providers.each do |provider|
172
+ (record_types - provider.record_types).each do |record_type|
173
+ errors.add(:records, "#{record_type} is a not a supported record type in #{provider.to_s}")
174
+ end
250
175
  end
251
176
  end
252
177
 
253
178
  def validate_can_handle_alias_records
254
179
  return unless records.any? { |record| record.is_a?(Record::ALIAS) }
255
180
  return if config.supports_alias?
256
- errors.add(:records, "#{config.provider} does not support ALIAS records for #{name.chomp('.')} zone")
181
+
182
+ # TODO(es): refactor to specify which provider
183
+ errors.add(:records, "one of the providers for #{unrooted_name} does not support ALIAS records")
257
184
  end
258
185
 
259
186
  def validate_alias_points_to_root
@@ -3,35 +3,42 @@ module RecordStore
3
3
  class Config
4
4
  include ActiveModel::Validations
5
5
 
6
- attr_reader :ignore_patterns, :provider, :supports_alias
6
+ attr_reader :ignore_patterns, :providers, :supports_alias
7
7
 
8
8
  validate :validate_zone_config
9
9
 
10
- def initialize(ignore_patterns: [], provider: nil, supports_alias: nil)
10
+ def initialize(ignore_patterns: [], providers: nil, supports_alias: nil)
11
11
  @ignore_patterns = ignore_patterns
12
- @provider = provider
12
+ @providers = providers
13
13
  @supports_alias = supports_alias
14
14
  end
15
15
 
16
16
  def supports_alias?
17
- @supports_alias.nil? && valid_provider? ? Provider.const_get(provider).supports_alias? : @supports_alias
17
+ if @supports_alias.nil?
18
+ valid_providers? && providers.all? { |provider| Provider.const_get(provider).supports_alias? }
19
+ else
20
+ @supports_alias
21
+ end
18
22
  end
19
23
 
20
24
  def to_hash
21
- {
22
- provider: provider,
25
+ config_hash = {
26
+ providers: providers,
23
27
  ignore_patterns: ignore_patterns,
24
28
  }
29
+ config_hash.merge!(supports_alias: supports_alias) if supports_alias
30
+
31
+ config_hash
25
32
  end
26
33
 
27
34
  private
28
35
 
29
36
  def validate_zone_config
30
- errors.add(:provider, 'provider specified does not exist') unless valid_provider?
37
+ errors.add(:providers, 'provider specified does not exist') unless valid_providers?
31
38
  end
32
39
 
33
- def valid_provider?
34
- Provider.constants.include?(provider.to_s.to_sym)
40
+ def valid_providers?
41
+ providers.present? && providers.all? { |provider| Provider.constants.include?(provider.to_s.to_sym) }
35
42
  end
36
43
  end
37
44
  end
@@ -0,0 +1,105 @@
1
+ module RecordStore
2
+ class Zone
3
+ module YamlDefinitions
4
+ include Enumerable
5
+
6
+ def all
7
+ defined.values
8
+ end
9
+
10
+ def defined
11
+ @defined ||= yaml_files
12
+ .map { |file| load_yml_zone_definition(file) }
13
+ .index_by { |zone| zone.unrooted_name }
14
+ end
15
+
16
+ def [](name)
17
+ defined.fetch(name)
18
+ end
19
+
20
+ def each(&block)
21
+ defined.each(&block)
22
+ end
23
+
24
+ def find(name)
25
+ defined[name]
26
+ end
27
+
28
+ def reset
29
+ @defined = nil
30
+ end
31
+
32
+ def write(name, config:, records:, format: :file)
33
+ raise ArgumentError, "format must be :directory or :file" unless %i(file directory).include?(format)
34
+ name = name.chomp('.')
35
+ zone_file = "#{RecordStore.zones_path}/#{name}.yml"
36
+ zone = { name => { config: config.to_hash } }
37
+ records = records.map(&:to_hash).sort_by! {|r| [r.fetch(:fqdn), r.fetch(:type), r[:nsdname] || r[:address]]}
38
+
39
+ if format == :file
40
+ zone[name][:records] = records
41
+ write_yml_file(zone_file, zone.deep_stringify_keys)
42
+ remove_record_files(name)
43
+ else
44
+ write_yml_file(zone_file, zone.deep_stringify_keys)
45
+ remove_record_files(name)
46
+ write_record_files(name, records)
47
+ end
48
+ end
49
+
50
+ private
51
+
52
+ def write_yml_file(filename, data)
53
+ lines = data.to_yaml.lines
54
+ lines.shift if lines.first == "---\n"
55
+ File.write(filename, lines.join)
56
+ end
57
+
58
+ def load_yml_zone_definition(filename)
59
+ dir = File.dirname(filename)
60
+ data = YAML.load_file(filename)
61
+ raise 'more than one zone in file' if data.size > 1
62
+ name, definition = data.first
63
+ definition['records'] ||= []
64
+ definition['records'] = definition['records'].map(&:deep_symbolize_keys)
65
+ Dir["#{dir}/#{name}/*__*.yml"].each do |record_file|
66
+ definition['records'] += load_yml_record_definitions(name, record_file)
67
+ end
68
+ Zone.new(name: name, records: definition['records'], config: definition['config'])
69
+ end
70
+
71
+ def load_yml_record_definitions(name, record_file)
72
+ type, domain = File.basename(record_file, '.yml').split('__')
73
+ Array.wrap(YAML.load_file(record_file)).map do |record_definition|
74
+ record_definition.merge('fqdn' => "#{domain}.#{name}", 'type' => type)
75
+ end.map(&:deep_symbolize_keys)
76
+ end
77
+
78
+ def remove_record_files(name)
79
+ dir = "#{RecordStore.zones_path}/#{name}"
80
+ File.unlink(*Dir["#{dir}/*"])
81
+ Dir.unlink(dir)
82
+ rescue Errno::ENOENT
83
+ end
84
+
85
+ def write_record_files(name, records)
86
+ dir = "#{RecordStore.zones_path}/#{name}"
87
+ Dir.mkdir(dir)
88
+ records.group_by { |record| [record.fetch(:fqdn), record.fetch(:type)] }.each do |(fqdn, type), grouped_records|
89
+ grouped_records.each do |record|
90
+ record.delete(:fqdn)
91
+ record.delete(:type)
92
+ record.deep_stringify_keys!
93
+ end
94
+ grouped_records = grouped_records.first if grouped_records.size == 1
95
+ domain = fqdn.chomp('.').chomp(name).chomp('.')
96
+ write_yml_file("#{dir}/#{type}__#{domain}.yml", grouped_records)
97
+ end
98
+ end
99
+
100
+ def yaml_files
101
+ Dir["#{RecordStore.zones_path}/*.yml"]
102
+ end
103
+ end
104
+ end
105
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: record_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Willem van Bergen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2016-09-22 00:00:00.000000000 Z
12
+ date: 2016-10-27 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -234,6 +234,7 @@ files:
234
234
  - lib/record_store/version.rb
235
235
  - lib/record_store/zone.rb
236
236
  - lib/record_store/zone/config.rb
237
+ - lib/record_store/zone/yaml_definitions.rb
237
238
  - record_store.gemspec
238
239
  - template/Gemfile
239
240
  - template/bin/record-store