record_store 6.1.2 → 6.4.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
  SHA256:
3
- metadata.gz: 4d05ecb06f048975b797baa10df042222f03ee4f0b1b5e268afa87e97b16ede6
4
- data.tar.gz: 4ca96a597c88f5d17df9f85ea50e8743b0cab6fa4d57196447d3808cc22711a6
3
+ metadata.gz: 729d72dfcdc31837d583ed7701c8c9a20a344ff1a15c16e0473b6bec160eaa4a
4
+ data.tar.gz: 77287fe899cd38ac839f62925daae1e416a69ddf3dab895c1f8c5f61eef5777b
5
5
  SHA512:
6
- metadata.gz: 98581b53f61499cb2f83d92ae2de4cf132ce2784cf56f8dc81531228f4090995f8e7a5a2c70f0a8fcac0cc5a3bdea29dd5c1d7b6996eef61e416c0c65c6a252d
7
- data.tar.gz: 8e753593bc358e83f82a0191f07528e14aa793305570afae5fcc865e1bdd97fffb48319fec43642e662c7b62e1070dd3b384464b9ca55a39e2a590fcf0f6738b
6
+ metadata.gz: 2facb09ae40af90a3f7b83700b00251b3ba29415a1c0c91ab09f9be36136591ad338d63685bb60e12b5779a1645242f5aa4a3f458436f91ea604235f11b78a68
7
+ data.tar.gz: 5f77c86a04dcc27385358fb8039b2c2e717b12e086e726c7d08915db284866e95b2038529675a1516ea0114ca2b53785bcd9331b05f75b93e187a7f87d1918a9
@@ -195,6 +195,7 @@ Style/FrozenStringLiteralComment:
195
195
  SupportedStyles:
196
196
  - always
197
197
  - never
198
+ SafeAutoCorrect: true
198
199
 
199
200
  Style/GlobalVars:
200
201
  AllowedVariables: []
@@ -264,7 +265,7 @@ Style/MethodCallWithArgsParentheses:
264
265
  - raise
265
266
  - puts
266
267
  Exclude:
267
- - Gemfile
268
+ - '**/Gemfile'
268
269
 
269
270
  Style/MethodDefParentheses:
270
271
  EnforcedStyle: require_parentheses
@@ -577,6 +578,7 @@ Layout/BlockEndNewline:
577
578
 
578
579
  Style/CaseEquality:
579
580
  Enabled: true
581
+ AllowOnConstant: true
580
582
 
581
583
  Style/CharacterLiteral:
582
584
  Enabled: true
@@ -659,6 +661,9 @@ Style/IfWithSemicolon:
659
661
  Style/IdenticalConditionalBranches:
660
662
  Enabled: true
661
663
 
664
+ Layout/IndentationStyle:
665
+ Enabled: true
666
+
662
667
  Style/InfiniteLoop:
663
668
  Enabled: true
664
669
 
@@ -803,9 +808,6 @@ Layout/SpaceInsideRangeLiteral:
803
808
  Style/SymbolLiteral:
804
809
  Enabled: true
805
810
 
806
- Layout/IndentationStyle:
807
- Enabled: true
808
-
809
811
  Layout/TrailingWhitespace:
810
812
  Enabled: true
811
813
 
@@ -834,7 +836,7 @@ Style/ZeroLengthPredicate:
834
836
  Enabled: true
835
837
 
836
838
  Layout/HeredocIndentation:
837
- EnforcedStyle: squiggly
839
+ Enabled: true
838
840
 
839
841
  Lint/AmbiguousOperator:
840
842
  Enabled: true
@@ -1015,3 +1017,6 @@ Style/ModuleFunction:
1015
1017
 
1016
1018
  Lint/OrderedMagicComments:
1017
1019
  Enabled: true
1020
+
1021
+ Lint/DeprecatedOpenSSLConstant:
1022
+ Enabled: true
@@ -1,7 +1,7 @@
1
1
  cache: bundler
2
2
  language: ruby
3
3
  rvm:
4
- - 2.4.3
4
+ - 2.6.4
5
5
 
6
6
  before_install:
7
7
  - gem update --system
@@ -1,5 +1,28 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 6.4.0
4
+
5
+ Add support for injecting implicit records into a zone based on a pre-configured template. Brief overview of template keys:
6
+
7
+ - `each_record`: the template will generate all `injected_records` in the template for each Record in the Zone that matches criteria listed here.
8
+ - `conflict_with`: if any Record in the Zone matches the criteria listed here, the template will avoid injecting any of its generated implicit records into the zone
9
+ - `injected_records`: the implicit records that will be generated by the template for every Zone Record that matches the `each_record` criteria
10
+
11
+ ## 6.3.1
12
+ - Improve resiliency in the face of temporary provider outages [BUGFIX]
13
+
14
+ ## 6.3.0
15
+ - Support for configurable number of threads via environment variable [FEATURE]
16
+
17
+ ## 6.2.1
18
+ - Improved error reporting after timeouts [FEATURE]
19
+
20
+ ## 6.2.0
21
+ - Add validation for non-terminal conflict with wildcard [FEATURE]
22
+
23
+ ## 6.1.2
24
+ - Retry on connection errors [FEATURE]
25
+
3
26
  ## 6.1.1
4
27
  - Emit messages when waiting for rate-limit to elapse for DNSimple and NS1 providers, so deployment does not timeout [BUGFIX]
5
28
 
data/README.md CHANGED
@@ -112,6 +112,17 @@ Changesets are how Record Store knows what updates to make. A `Changeset` is gen
112
112
 
113
113
  When running `bin/record-store apply`, a `Changeset` is generated by comparing the current records in a zone's YAML file with the records the provider defines. A zone's YAML file is always considered the primary source of truth.
114
114
 
115
+ ### Parallelism
116
+
117
+ Record store attempts to parallelize some of the bulk zone fetching operations. It does so by spawning multiple threads (default: 10). This value can be configured by setting the RECORD_STORE_MAX_THREADS environment variable to a positive integer value.
118
+
119
+
120
+ ### Templates
121
+
122
+ Record Store supports injecting implicit records into a zone based on templates. Each `Zone` can have a list of `Zone::Config::ImplicitRecordTemplate` in its `Zone::Config` that can define their own criteria for which records they use for generating implicit template records, which records conflict with the template-generated records, and what the template-generated records look like. These records are injected at the time of `Zone` initialization within `Zone#build_records`.
123
+
124
+ Templates can help reduce zone file bloat where instead of defining many generic literals within the zone file for a given criteria zone record, these generic records can be implicitly injected into the `Zone` at the time of initialization based on information provided within the template.
125
+
115
126
  ----
116
127
 
117
128
  # Development
@@ -6,6 +6,7 @@ require 'record_store'
6
6
 
7
7
  RecordStore.zones_path = File.expand_path('../../dev/zones', __FILE__)
8
8
  RecordStore.config_path = File.expand_path('../../dev/config.yml', __FILE__)
9
+ RecordStore.implicit_records_templates_path = File.expand_path('../../dev/templates/implicit_records', __FILE__)
9
10
 
10
11
  require 'pry'
11
12
  binding.pry(RecordStore) # rubocop:disable Lint/Debugger
data/dev.yml CHANGED
@@ -2,7 +2,7 @@
2
2
  name: recordstore
3
3
 
4
4
  up:
5
- - ruby: 2.5.0
5
+ - ruby: 2.6.4
6
6
  - bundler
7
7
 
8
8
  commands:
@@ -28,6 +28,7 @@ require 'record_store/zone/yaml_definitions'
28
28
  require 'record_store/zone'
29
29
  require 'record_store/zone/config'
30
30
  require 'record_store/zone/config/ignore_pattern'
31
+ require 'record_store/zone/config/implicit_record_template'
31
32
  require 'record_store/changeset'
32
33
  require 'record_store/provider'
33
34
  require 'record_store/provider/dynect'
@@ -59,6 +60,13 @@ module RecordStore
59
60
  @config_path ||= File.expand_path('config.yml', Dir.pwd)
60
61
  end
61
62
 
63
+ def implicit_records_templates_path
64
+ @implicit_records_templates_path ||= Pathname.new(
65
+ File.expand_path(config.fetch('implicit_records_templates_path'),
66
+ File.dirname(config_path)),
67
+ ).realpath.to_s
68
+ end
69
+
62
70
  def config_path=(config_path)
63
71
  @config = @zones_path = @secrets_path = nil
64
72
  @config_path = config_path
@@ -2,6 +2,9 @@ require 'resolv'
2
2
 
3
3
  module RecordStore
4
4
  class Provider
5
+ class Error < StandardError; end
6
+ class UnparseableBodyError < Error; end
7
+
5
8
  class << self
6
9
  def provider_for(object)
7
10
  ns_server =
@@ -52,6 +55,10 @@ module RecordStore
52
55
  false
53
56
  end
54
57
 
58
+ def empty_non_terminal_over_wildcard?
59
+ true
60
+ end
61
+
55
62
  def build_zone(zone_name:, config:)
56
63
  zone = Zone.new(name: zone_name)
57
64
  zone.records = retrieve_current_records(zone: zone_name)
@@ -132,32 +139,39 @@ module RecordStore
132
139
  def retry_on_connection_errors(
133
140
  max_timeouts: 5,
134
141
  max_conn_resets: 5,
142
+ max_retries: 5,
135
143
  delay: 1,
136
144
  backoff_multiplier: 2,
137
145
  max_backoff: 10
138
146
  )
147
+ waiter = BackoffWaiter.new(
148
+ 'Waiting to retry after a connection reset',
149
+ initial_delay: delay,
150
+ multiplier: backoff_multiplier,
151
+ max_delay: max_backoff,
152
+ )
153
+
139
154
  loop do
140
155
  begin
141
156
  return yield
142
- rescue Net::OpenTimeout
157
+ rescue UnparseableBodyError
158
+ raise if max_retries <= 0
159
+ max_retries -= 1
160
+
161
+ waiter.wait(message: 'Waiting to retry after receiving an unparseable response')
162
+ rescue Net::OpenTimeout, Errno::ETIMEDOUT
143
163
  raise if max_timeouts <= 0
144
164
  max_timeouts -= 1
145
165
 
146
- $stderr.puts("Retrying after a connection timeout")
166
+ $stderr.puts('Retrying after a connection timeout')
147
167
  rescue Errno::ECONNRESET
148
168
  raise if max_conn_resets <= 0
149
169
  max_conn_resets -= 1
150
170
 
151
- $stderr.puts("Retrying in #{delay}s after a connection reset")
152
- backoff_sleep(delay)
153
- delay = [delay * backoff_multiplier, max_backoff].min
171
+ waiter.wait
154
172
  end
155
173
  end
156
174
  end
157
-
158
- def backoff_sleep(delay)
159
- sleep(delay)
160
- end
161
175
  end
162
176
  end
163
177
  end
@@ -12,6 +12,10 @@ module RecordStore
12
12
  true
13
13
  end
14
14
 
15
+ def empty_non_terminal_over_wildcard?
16
+ false
17
+ end
18
+
15
19
  # returns an array of Record objects that match the records which exist in the provider
16
20
  def retrieve_current_records(zone:, stdout: $stdout)
17
21
  retry_on_connection_errors do
@@ -1,4 +1,4 @@
1
- require_relative '../provider_utils/rate_limit'
1
+ require_relative '../provider_utils/waiter'
2
2
 
3
3
  # Patch Dnsimple client method which retrieves headers for API rate limit dynamically
4
4
  module Dnsimple
@@ -26,8 +26,8 @@ module Dnsimple
26
26
  rate_limit_periods = rate_limit_remaining + 1
27
27
  sleep_time = rate_limit_reset_in / rate_limit_periods.to_f
28
28
 
29
- rate_limit = RateLimit.new('DNSimple')
30
- rate_limit.sleep_for(sleep_time)
29
+ rate_limit = RateLimitWaiter.new('DNSimple')
30
+ rate_limit.wait(sleep_time)
31
31
  end
32
32
  end
33
33
  end
@@ -3,8 +3,6 @@ require_relative 'ns1/patch_api_header'
3
3
 
4
4
  module RecordStore
5
5
  class Provider::NS1 < Provider
6
- class Error < StandardError; end
7
-
8
6
  class ApiAnswer
9
7
  class << self
10
8
  def from_full_api_answer(type:, record_id:, answer:)
@@ -180,7 +178,7 @@ module RecordStore
180
178
  unless updated
181
179
  error = +'while trying to update a record, could not find answer with fqdn: '
182
180
  error << "#{record.fqdn}, type; #{record.type}, id: #{id}"
183
- raise Error, error
181
+ raise RecordStore::Provider::Error, error
184
182
  end
185
183
 
186
184
  client.modify_record(
@@ -1,46 +1,59 @@
1
+ require 'net/http'
1
2
  require 'ns1'
2
3
 
3
4
  module RecordStore
4
5
  class Provider::NS1 < Provider
5
- class Error < StandardError; end
6
-
7
6
  class Client < ::NS1::Client
8
7
  def initialize(api_key:)
9
8
  super(api_key)
10
9
  end
11
10
 
12
11
  def zones
13
- super
12
+ zones = super
13
+ raise_if_error!(zones)
14
+ zones
14
15
  end
15
16
 
16
17
  def zone(name)
17
- super(name)
18
+ zone = super(name)
19
+ raise_if_error!(zone)
20
+ zone
18
21
  end
19
22
 
20
23
  def record(zone:, fqdn:, type:, must_exist: false)
21
24
  result = super(zone, fqdn, type)
22
- raise(Error, result.to_s) if must_exist && result.is_a?(NS1::Response::Error)
25
+ raise_if_error!(result) if must_exist
23
26
  return nil if result.is_a?(NS1::Response::Error)
24
27
  result
25
28
  end
26
29
 
27
30
  def create_record(zone:, fqdn:, type:, params:)
28
31
  result = super(zone, fqdn, type, params)
29
- raise(Error, result.to_s) if result.is_a?(NS1::Response::Error)
32
+ raise_if_error!(result)
30
33
  nil
31
34
  end
32
35
 
33
36
  def modify_record(zone:, fqdn:, type:, params:)
34
37
  result = super(zone, fqdn, type, params)
35
- raise(Error, result.to_s) if result.is_a?(NS1::Response::Error)
38
+ raise_if_error!(result)
36
39
  nil
37
40
  end
38
41
 
39
42
  def delete_record(zone:, fqdn:, type:)
40
43
  result = super(zone, fqdn, type)
41
- raise(Error, result.to_s) if result.is_a?(NS1::Response::Error)
44
+ raise_if_error!(result)
42
45
  nil
43
46
  end
47
+
48
+ private
49
+
50
+ def raise_if_error!(result)
51
+ return unless result.is_a?(NS1::Response::Error)
52
+ if result.is_a?(NS1::Response::UnparsableBodyError)
53
+ raise RecordStore::Provider::UnparseableBodyError, result.to_s
54
+ end
55
+ raise RecordStore::Provider::Error, result.to_s
56
+ end
44
57
  end
45
58
  end
46
59
  end
@@ -1,5 +1,12 @@
1
1
  require 'net/http'
2
- require_relative '../provider_utils/rate_limit'
2
+ require_relative '../provider_utils/waiter'
3
+
4
+ class NS1::Response::UnparsableBodyError < NS1::Response::Error
5
+ def initialize(status)
6
+ @status = status
7
+ super({}, status)
8
+ end
9
+ end
3
10
 
4
11
  # Patch the method which retrieves headers for API rate limit dynamically
5
12
  module NS1::Transport
@@ -14,19 +21,21 @@ module NS1::Transport
14
21
  sleep_time = response_hash[X_RATELIMIT_PERIOD].first.to_i /
15
22
  [1, response_hash[X_RATELIMIT_REMAINING].first.to_i].max.to_f
16
23
 
17
- rate_limit = RateLimit.new('NS1')
18
- rate_limit.sleep_for(sleep_time)
24
+ rate_limit = RateLimitWaiter.new('NS1')
25
+ rate_limit.wait(sleep_time)
19
26
  end
20
27
 
21
- body = JSON.parse(response.body)
22
- case response
23
- when Net::HTTPOK
24
- NS1::Response::Success.new(body, response.code.to_i)
25
- else
26
- NS1::Response::Error.new(body, response.code.to_i)
28
+ begin
29
+ body = JSON.parse(response.body)
30
+ case response
31
+ when Net::HTTPOK
32
+ NS1::Response::Success.new(body, response.code.to_i)
33
+ else
34
+ NS1::Response::Error.new(body, response.code.to_i)
35
+ end
36
+ rescue JSON::ParserError
37
+ NS1::Response::UnparsableBodyError.new(response.code.to_i)
27
38
  end
28
- rescue JSON::ParserError
29
- raise NS1::Transport::ResponseParseError
30
39
  end
31
40
  end
32
41
  end
@@ -0,0 +1,41 @@
1
+ class Waiter
2
+ def initialize(message = nil)
3
+ @message = message || 'Waiting'
4
+ end
5
+
6
+ attr_accessor :message
7
+
8
+ def wait(sleep_time, message: @message)
9
+ while sleep_time > 0
10
+ wait_time = [10, sleep_time].min
11
+ puts "#{message} (#{sleep_time}s left)" if wait_time > 1
12
+ sleep(wait_time)
13
+ sleep_time -= wait_time
14
+ end
15
+ end
16
+ end
17
+
18
+ class RateLimitWaiter < Waiter
19
+ def initialize(provider)
20
+ super("Waiting on #{provider} rate-limit")
21
+ end
22
+ end
23
+
24
+ class BackoffWaiter < Waiter
25
+ def initialize(message, initial_delay:, multiplier:, max_delay: nil)
26
+ super(message)
27
+
28
+ @initial_delay = @current_delay = initial_delay
29
+ @multiplier = multiplier
30
+ @max_delay = max_delay
31
+ end
32
+
33
+ def reset
34
+ @current_delay = @initial_delay
35
+ end
36
+
37
+ def wait(message: @message)
38
+ super(@current_delay, message: message)
39
+ @current_delay = [@current_delay * @multiplier, @max_delay].compact.min
40
+ end
41
+ end
@@ -112,6 +112,10 @@ module RecordStore
112
112
  "[#{type}Record] #{fqdn} #{ttl} IN #{type} #{rdata_txt}"
113
113
  end
114
114
 
115
+ def wildcard?
116
+ fqdn.match?(/^\*\./)
117
+ end
118
+
115
119
  protected
116
120
 
117
121
  def validate_label_length
@@ -1,3 +1,3 @@
1
1
  module RecordStore
2
- VERSION = '6.1.2'.freeze
2
+ VERSION = '6.4.0'.freeze
3
3
  end
@@ -18,6 +18,7 @@ module RecordStore
18
18
  validate :validate_cname_records_dont_point_to_root
19
19
  validate :validate_same_ttl_for_records_sharing_fqdn_and_type
20
20
  validate :validate_provider_can_handle_zone_records
21
+ validate :validate_no_empty_non_terminal
21
22
  validate :validate_can_handle_alias_records
22
23
 
23
24
  class << self
@@ -44,13 +45,18 @@ module RecordStore
44
45
  end
45
46
  end
46
47
 
47
- MAX_PARALLEL_THREADS = 10
48
+ DEFAULT_MAX_PARALLEL_THREADS = 10
49
+
50
+ def max_parallel_threads
51
+ (ENV['RECORD_STORE_MAX_THREADS'] || DEFAULT_MAX_PARALLEL_THREADS).to_i
52
+ end
53
+
48
54
  def modified(verbose: false) # rubocop:disable Lint/UnusedMethodArgument
49
55
  modified_zones = []
50
56
  mutex = Mutex.new
51
57
  zones = all
52
58
 
53
- (1..MAX_PARALLEL_THREADS).map do
59
+ (1..max_parallel_threads).map do
54
60
  Thread.new do
55
61
  current_zone = nil
56
62
  while zones.any?
@@ -183,7 +189,13 @@ module RecordStore
183
189
  end
184
190
 
185
191
  def build_records(records)
186
- records.map { |record| Record.build_from_yaml_definition(record) }
192
+ all_records = records.map { |record| Record.build_from_yaml_definition(record) }
193
+
194
+ config.implicit_record_templates.each do |template|
195
+ all_records.push(*template.generate_records_to_inject(current_records: all_records))
196
+ end
197
+
198
+ all_records
187
199
  end
188
200
 
189
201
  def validate_records
@@ -258,6 +270,28 @@ module RecordStore
258
270
  end
259
271
  end
260
272
 
273
+ def validate_no_empty_non_terminal
274
+ return unless config.empty_non_terminal_over_wildcard?
275
+
276
+ wildcards = records.select(&:wildcard?).map(&:fqdn).uniq
277
+ wildcards.each do |wildcard|
278
+ suffix = wildcard[1..-1]
279
+
280
+ terminal_records = records.map(&:fqdn)
281
+ .select { |record| record.match?(/^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_])#{Regexp.escape(suffix)}$/) }
282
+ next unless terminal_records.any?
283
+
284
+ intermediate_records = records.map(&:fqdn)
285
+ .select { |record| record.match?(/^([a-zA-Z0-9\-_]+)#{Regexp.escape(suffix)}$/) }
286
+ terminal_records.each do |terminal_record|
287
+ non_terminal = terminal_record.partition('.').last
288
+ errors.add(:records, "found empty non-terminal #{non_terminal} "\
289
+ "(caused by existing records #{wildcard} and #{terminal_record})")\
290
+ unless intermediate_records.include?(non_terminal)
291
+ end
292
+ end
293
+ end
294
+
261
295
  def validate_can_handle_alias_records
262
296
  return unless records.any? { |record| record.is_a?(Record::ALIAS) }
263
297
  return if config.supports_alias?
@@ -3,15 +3,17 @@ module RecordStore
3
3
  class Config
4
4
  include ActiveModel::Validations
5
5
 
6
- attr_reader :ignore_patterns, :providers, :supports_alias
6
+ attr_reader :ignore_patterns, :providers, :supports_alias, :implicit_record_templates
7
7
 
8
8
  validate :validate_zone_config
9
9
 
10
- def initialize(ignore_patterns: [], providers: nil, supports_alias: nil)
10
+ def initialize(ignore_patterns: [], providers: nil, supports_alias: nil, implicit_record_templates: [])
11
11
  @ignore_patterns = ignore_patterns.map do |ignore_pattern|
12
12
  Zone::Config::IgnorePattern.new(ignore_pattern)
13
13
  end
14
-
14
+ @implicit_record_templates = implicit_record_templates.map do |filename|
15
+ Zone::Config::ImplicitRecordTemplate.from_file(filename: filename)
16
+ end
15
17
  @providers = providers
16
18
  @supports_alias = supports_alias
17
19
  end
@@ -24,6 +26,10 @@ module RecordStore
24
26
  end
25
27
  end
26
28
 
29
+ def empty_non_terminal_over_wildcard?
30
+ valid_providers? && providers.any? { |provider| Provider.const_get(provider).empty_non_terminal_over_wildcard? }
31
+ end
32
+
27
33
  def to_hash
28
34
  config_hash = {
29
35
  providers: providers,
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+ module RecordStore
3
+ class Zone
4
+ class Config
5
+ class TemplateContext
6
+ def self.build(record:, current_records:)
7
+ new(record: record, current_records: current_records)
8
+ end
9
+
10
+ def initialize(record:, current_records:)
11
+ @record = record
12
+ @current_records = current_records
13
+ end
14
+
15
+ def fetch_binding
16
+ binding
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :record, :current_records
22
+ end
23
+
24
+ class ImplicitRecordTemplate
25
+ class << self
26
+ def from_file(filename:)
27
+ filepath = template_filepath_for(filename: filename)
28
+ template_file = File.read(filepath)
29
+ filters_for_records_to_template = YAML.load(template_file).deep_symbolize_keys[:each_record]
30
+
31
+ new(template: ERB.new(template_file), filters_for_records_to_template: filters_for_records_to_template)
32
+ end
33
+
34
+ private
35
+
36
+ def template_filepath_for(filename:)
37
+ "#{RecordStore.implicit_records_templates_path}/#{filename}"
38
+ end
39
+ end
40
+
41
+ def initialize(template:, filters_for_records_to_template:)
42
+ @template = template
43
+ @filters_for_records_to_template = filters_for_records_to_template
44
+ end
45
+
46
+ def generate_records_to_inject(current_records:)
47
+ current_records
48
+ .select { |record| should_template?(record: record) }
49
+ .map { |record| template_record_for(record: record, current_records: current_records) }
50
+ .each_with_object([]) do |template_records, records_to_inject|
51
+ next unless should_inject?(
52
+ template_records: template_records,
53
+ current_records: current_records + records_to_inject
54
+ )
55
+
56
+ records_to_inject.push(
57
+ *template_records.fetch(:injected_records, []).map { |r_yml| Record.build_from_yaml_definition(r_yml) }
58
+ )
59
+ end
60
+ end
61
+
62
+ private
63
+
64
+ attr_reader :template, :filters_for_records_to_template
65
+
66
+ def should_inject?(template_records:, current_records:)
67
+ current_records.none? do |record|
68
+ template_records[:conflict_with].any? do |filter|
69
+ record_match?(record: record, filter: filter)
70
+ end
71
+ end
72
+ end
73
+
74
+ def should_template?(record:)
75
+ filters_for_records_to_template.any? { |filter| record_match?(record: record, filter: filter) }
76
+ end
77
+
78
+ def record_match?(record:, filter:)
79
+ filter.all? do |key, value|
80
+ record.public_send(key) == value
81
+ end
82
+ end
83
+
84
+ def template_record_for(record:, current_records:)
85
+ context = TemplateContext.build(record: record, current_records: current_records)
86
+
87
+ YAML.load(
88
+ template.result(context.fetch_binding)
89
+ ).deep_symbolize_keys
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -48,6 +48,6 @@ Gem::Specification.new do |spec|
48
48
  spec.add_development_dependency 'vcr'
49
49
  spec.add_development_dependency 'pry'
50
50
  spec.add_development_dependency 'webmock'
51
- spec.add_development_dependency 'rubocop'
51
+ spec.add_development_dependency 'rubocop', '~> 1.0.0'
52
52
  spec.add_development_dependency 'minitest-focus'
53
53
  end
@@ -1,2 +1,3 @@
1
1
  zones_path: zones/
2
2
  secrets_path: secrets.json
3
+ implicit_records_templates_path: templates/implicit_records/
@@ -0,0 +1,13 @@
1
+ each_record:
2
+ - type: A
3
+ fqdn: abc.123.com.
4
+ - type: TXT
5
+ conflict_with:
6
+ - type: TXT
7
+ fqdn: <%= record.fqdn %>.more.domain.com.
8
+ injected_records:
9
+ - type: CNAME
10
+ ttl: 3600
11
+ fqdn: <%= record.fqdn %>.added.information.com
12
+ cname: <%= record.fqdn %>.more.added.information.com.
13
+
@@ -5,6 +5,8 @@ dynect.example.com:
5
5
  ignore_patterns:
6
6
  - type: NS
7
7
  fqdn: dynect.example.com.
8
+ implicit_record_templates:
9
+ - implicit_example.yml.erb
8
10
  records:
9
11
  - type: A
10
12
  fqdn: a-record.dynect.example.com.
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: 6.1.2
4
+ version: 6.4.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: 2020-08-18 00:00:00.000000000 Z
12
+ date: 2020-12-02 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: thor
@@ -301,16 +301,16 @@ dependencies:
301
301
  name: rubocop
302
302
  requirement: !ruby/object:Gem::Requirement
303
303
  requirements:
304
- - - ">="
304
+ - - "~>"
305
305
  - !ruby/object:Gem::Version
306
- version: '0'
306
+ version: 1.0.0
307
307
  type: :development
308
308
  prerelease: false
309
309
  version_requirements: !ruby/object:Gem::Requirement
310
310
  requirements:
311
- - - ">="
311
+ - - "~>"
312
312
  - !ruby/object:Gem::Version
313
- version: '0'
313
+ version: 1.0.0
314
314
  - !ruby/object:Gem::Dependency
315
315
  name: minitest-focus
316
316
  requirement: !ruby/object:Gem::Requirement
@@ -362,7 +362,7 @@ files:
362
362
  - lib/record_store/provider/ns1/client.rb
363
363
  - lib/record_store/provider/ns1/patch_api_header.rb
364
364
  - lib/record_store/provider/oracle_cloud_dns.rb
365
- - lib/record_store/provider/provider_utils/rate_limit.rb
365
+ - lib/record_store/provider/provider_utils/waiter.rb
366
366
  - lib/record_store/record.rb
367
367
  - lib/record_store/record/a.rb
368
368
  - lib/record_store/record/aaaa.rb
@@ -380,6 +380,7 @@ files:
380
380
  - lib/record_store/zone.rb
381
381
  - lib/record_store/zone/config.rb
382
382
  - lib/record_store/zone/config/ignore_pattern.rb
383
+ - lib/record_store/zone/config/implicit_record_template.rb
383
384
  - lib/record_store/zone/yaml_definitions.rb
384
385
  - record_store.gemspec
385
386
  - shipit.rubygems.yml
@@ -389,6 +390,7 @@ files:
389
390
  - template/bin/test
390
391
  - template/config.yml
391
392
  - template/secrets.json
393
+ - template/templates/implicit_records/implicit_example.yml.erb
392
394
  - template/zones/dnsimple.example.com.yml
393
395
  - template/zones/dnsimple.example.com/A__a-record.yml
394
396
  - template/zones/dnsimple.example.com/TXT__marco.yml
@@ -1,14 +0,0 @@
1
- class RateLimit
2
- def initialize(provider)
3
- @provider = provider
4
- end
5
-
6
- def sleep_for(sleep_time)
7
- while sleep_time > 0
8
- wait = [10, sleep_time].min
9
- puts "Waiting on #{@provider} rate-limit" if wait > 1
10
- sleep(wait)
11
- sleep_time -= wait
12
- end
13
- end
14
- end