record_store 6.1.2 → 6.4.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/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +10 -5
- data/.travis.yml +1 -1
- data/CHANGELOG.md +23 -0
- data/README.md +11 -0
- data/bin/console +1 -0
- data/dev.yml +1 -1
- data/lib/record_store.rb +8 -0
- data/lib/record_store/provider.rb +23 -9
- data/lib/record_store/provider/dnsimple.rb +4 -0
- data/lib/record_store/provider/dnsimple/patch_api_header.rb +3 -3
- data/lib/record_store/provider/ns1.rb +1 -3
- data/lib/record_store/provider/ns1/client.rb +21 -8
- data/lib/record_store/provider/ns1/patch_api_header.rb +20 -11
- data/lib/record_store/provider/provider_utils/waiter.rb +41 -0
- data/lib/record_store/record.rb +4 -0
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +37 -3
- data/lib/record_store/zone/config.rb +9 -3
- data/lib/record_store/zone/config/implicit_record_template.rb +94 -0
- data/record_store.gemspec +1 -1
- data/template/config.yml +1 -0
- data/template/templates/implicit_records/implicit_example.yml.erb +13 -0
- data/template/zones/dynect.example.com.yml +2 -0
- metadata +9 -7
- data/lib/record_store/provider/provider_utils/rate_limit.rb +0 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 729d72dfcdc31837d583ed7701c8c9a20a344ff1a15c16e0473b6bec160eaa4a
|
4
|
+
data.tar.gz: 77287fe899cd38ac839f62925daae1e416a69ddf3dab895c1f8c5f61eef5777b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -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
|
data/bin/console
CHANGED
@@ -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
data/lib/record_store.rb
CHANGED
@@ -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
|
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(
|
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
|
-
|
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/
|
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 =
|
30
|
-
rate_limit.
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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/
|
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 =
|
18
|
-
rate_limit.
|
24
|
+
rate_limit = RateLimitWaiter.new('NS1')
|
25
|
+
rate_limit.wait(sleep_time)
|
19
26
|
end
|
20
27
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
data/lib/record_store/record.rb
CHANGED
data/lib/record_store/version.rb
CHANGED
data/lib/record_store/zone.rb
CHANGED
@@ -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
|
-
|
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..
|
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
|
data/record_store.gemspec
CHANGED
@@ -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
|
data/template/config.yml
CHANGED
@@ -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
|
+
|
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.
|
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-
|
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:
|
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:
|
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/
|
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
|