record_store 6.2.1 → 6.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +28 -0
- 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 +11 -2
- 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 +17 -8
- data/lib/record_store/provider/provider_utils/waiter.rb +3 -3
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +48 -6
- data/lib/record_store/zone/config.rb +5 -3
- data/lib/record_store/zone/config/implicit_record_template.rb +94 -0
- data/lib/record_store/zone/yaml_definitions.rb +4 -1
- 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 +10 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f5d50f38de714c7b8d6d316070fc9d6bca5ac4d40751629c3c565866178d628
|
4
|
+
data.tar.gz: 14e3171ad53e5903404fcda9404bbfa109ec63db0c9253d8f3f7df76ee0e0ff1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ac87302ec7f2d9e1afcbac7af6ef340a7a6fdd7d503a2f941fb7e7f021a46eb3a2b071841a152b264ba1956c50aa45141853a3bfdda4e8101a5e226ddec595ac
|
7
|
+
data.tar.gz: 17fdee4d8ca5b4e9692e6a3718cdbb143e076890b2752a9dd4516c110835edbd73208056c8be8bfc5d18b926d97050d33300a04c29639d77cdc1429b2caf41cc
|
@@ -0,0 +1,28 @@
|
|
1
|
+
name: CI
|
2
|
+
|
3
|
+
on: [push, pull_request]
|
4
|
+
|
5
|
+
env:
|
6
|
+
SRB_SKIP_GEM_RBIS: true
|
7
|
+
|
8
|
+
jobs:
|
9
|
+
build:
|
10
|
+
runs-on: ubuntu-latest
|
11
|
+
strategy:
|
12
|
+
fail-fast: false
|
13
|
+
matrix:
|
14
|
+
ruby: [ 2.7.1 ]
|
15
|
+
name: Test Ruby ${{ matrix.ruby }}
|
16
|
+
steps:
|
17
|
+
- uses: actions/checkout@v2
|
18
|
+
- name: Set up Ruby
|
19
|
+
uses: ruby/setup-ruby@v1
|
20
|
+
with:
|
21
|
+
ruby-version: ${{ matrix.ruby }}
|
22
|
+
bundler-cache: true
|
23
|
+
- name: rubocop
|
24
|
+
run: bin/rubocop --version && bin/rubocop
|
25
|
+
- name: setup
|
26
|
+
run: bin/setup
|
27
|
+
- name: test
|
28
|
+
run: bundle exec rake test
|
@@ -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 =
|
@@ -136,12 +139,13 @@ module RecordStore
|
|
136
139
|
def retry_on_connection_errors(
|
137
140
|
max_timeouts: 5,
|
138
141
|
max_conn_resets: 5,
|
142
|
+
max_retries: 5,
|
139
143
|
delay: 1,
|
140
144
|
backoff_multiplier: 2,
|
141
145
|
max_backoff: 10
|
142
146
|
)
|
143
147
|
waiter = BackoffWaiter.new(
|
144
|
-
|
148
|
+
'Waiting to retry after a connection reset',
|
145
149
|
initial_delay: delay,
|
146
150
|
multiplier: backoff_multiplier,
|
147
151
|
max_delay: max_backoff,
|
@@ -150,11 +154,16 @@ module RecordStore
|
|
150
154
|
loop do
|
151
155
|
begin
|
152
156
|
return yield
|
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')
|
153
162
|
rescue Net::OpenTimeout, Errno::ETIMEDOUT
|
154
163
|
raise if max_timeouts <= 0
|
155
164
|
max_timeouts -= 1
|
156
165
|
|
157
|
-
$stderr.puts(
|
166
|
+
$stderr.puts('Retrying after a connection timeout')
|
158
167
|
rescue Errno::ECONNRESET
|
159
168
|
raise if max_conn_resets <= 0
|
160
169
|
max_conn_resets -= 1
|
@@ -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,6 +1,13 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
require_relative '../provider_utils/waiter'
|
3
3
|
|
4
|
+
class NS1::Response::UnparsableBodyError < NS1::Response::Error
|
5
|
+
def initialize(status)
|
6
|
+
@status = status
|
7
|
+
super({}, status)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
4
11
|
# Patch the method which retrieves headers for API rate limit dynamically
|
5
12
|
module NS1::Transport
|
6
13
|
class NetHttp
|
@@ -18,15 +25,17 @@ module NS1::Transport
|
|
18
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
|
@@ -5,7 +5,7 @@ class Waiter
|
|
5
5
|
|
6
6
|
attr_accessor :message
|
7
7
|
|
8
|
-
def wait(sleep_time)
|
8
|
+
def wait(sleep_time, message: @message)
|
9
9
|
while sleep_time > 0
|
10
10
|
wait_time = [10, sleep_time].min
|
11
11
|
puts "#{message} (#{sleep_time}s left)" if wait_time > 1
|
@@ -34,8 +34,8 @@ class BackoffWaiter < Waiter
|
|
34
34
|
@current_delay = @initial_delay
|
35
35
|
end
|
36
36
|
|
37
|
-
def wait
|
38
|
-
super(@current_delay)
|
37
|
+
def wait(message: @message)
|
38
|
+
super(@current_delay, message: message)
|
39
39
|
@current_delay = [@current_delay * @multiplier, @max_delay].compact.min
|
40
40
|
end
|
41
41
|
end
|
data/lib/record_store/version.rb
CHANGED
data/lib/record_store/zone.rb
CHANGED
@@ -20,6 +20,7 @@ module RecordStore
|
|
20
20
|
validate :validate_provider_can_handle_zone_records
|
21
21
|
validate :validate_no_empty_non_terminal
|
22
22
|
validate :validate_can_handle_alias_records
|
23
|
+
validate :validate_no_duplicate_keys
|
23
24
|
|
24
25
|
class << self
|
25
26
|
def download(name, provider_name, **write_options)
|
@@ -45,13 +46,18 @@ module RecordStore
|
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
48
|
-
|
49
|
+
DEFAULT_MAX_PARALLEL_THREADS = 10
|
50
|
+
|
51
|
+
def max_parallel_threads
|
52
|
+
(ENV['RECORD_STORE_MAX_THREADS'] || DEFAULT_MAX_PARALLEL_THREADS).to_i
|
53
|
+
end
|
54
|
+
|
49
55
|
def modified(verbose: false) # rubocop:disable Lint/UnusedMethodArgument
|
50
56
|
modified_zones = []
|
51
57
|
mutex = Mutex.new
|
52
58
|
zones = all
|
53
59
|
|
54
|
-
(1..
|
60
|
+
(1..max_parallel_threads).map do
|
55
61
|
Thread.new do
|
56
62
|
current_zone = nil
|
57
63
|
while zones.any?
|
@@ -65,10 +71,11 @@ module RecordStore
|
|
65
71
|
end
|
66
72
|
end
|
67
73
|
|
68
|
-
def initialize(name:, records: [], config: {})
|
74
|
+
def initialize(name:, records: [], config: {}, abstract_syntax_trees: {})
|
69
75
|
@name = Record.ensure_ends_with_dot(name)
|
70
76
|
@config = RecordStore::Zone::Config.new(config.deep_symbolize_keys)
|
71
77
|
@records = build_records(records)
|
78
|
+
@abstract_syntax_trees = abstract_syntax_trees
|
72
79
|
end
|
73
80
|
|
74
81
|
def build_changesets(all: false)
|
@@ -184,7 +191,13 @@ module RecordStore
|
|
184
191
|
end
|
185
192
|
|
186
193
|
def build_records(records)
|
187
|
-
records.map { |record| Record.build_from_yaml_definition(record) }
|
194
|
+
all_records = records.map { |record| Record.build_from_yaml_definition(record) }
|
195
|
+
|
196
|
+
config.implicit_records_templates.each do |template|
|
197
|
+
all_records.push(*template.generate_records_to_inject(current_records: all_records))
|
198
|
+
end
|
199
|
+
|
200
|
+
all_records
|
188
201
|
end
|
189
202
|
|
190
203
|
def validate_records
|
@@ -267,11 +280,11 @@ module RecordStore
|
|
267
280
|
suffix = wildcard[1..-1]
|
268
281
|
|
269
282
|
terminal_records = records.map(&:fqdn)
|
270
|
-
.select { |record| record.match?(/^([a-zA-Z0-9
|
283
|
+
.select { |record| record.match?(/^([a-zA-Z0-9\-_]+\.[a-zA-Z0-9\-_])#{Regexp.escape(suffix)}$/) }
|
271
284
|
next unless terminal_records.any?
|
272
285
|
|
273
286
|
intermediate_records = records.map(&:fqdn)
|
274
|
-
.select { |record| record.match?(/^([a-zA-Z0-9
|
287
|
+
.select { |record| record.match?(/^([a-zA-Z0-9\-_]+)#{Regexp.escape(suffix)}$/) }
|
275
288
|
terminal_records.each do |terminal_record|
|
276
289
|
non_terminal = terminal_record.partition('.').last
|
277
290
|
errors.add(:records, "found empty non-terminal #{non_terminal} "\
|
@@ -296,5 +309,34 @@ module RecordStore
|
|
296
309
|
|
297
310
|
errors.add(:records, "ALIAS record should be defined on the root of the zone: #{alias_record}")
|
298
311
|
end
|
312
|
+
|
313
|
+
def validate_no_duplicate_keys
|
314
|
+
@abstract_syntax_trees.each do |filename, ast|
|
315
|
+
validate_no_duplicate_keys_in_node(filename, ast)
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
def validate_no_duplicate_keys_in_node(filename, node)
|
320
|
+
if node.mapping?
|
321
|
+
keys = node
|
322
|
+
.children
|
323
|
+
.each_slice(2)
|
324
|
+
.map(&:first)
|
325
|
+
.map(&:value)
|
326
|
+
.sort
|
327
|
+
dup_keys = keys
|
328
|
+
.find_all { |k| keys.count(k) > 1 }
|
329
|
+
.uniq
|
330
|
+
unless dup_keys.empty?
|
331
|
+
location = "#{File.basename(filename)}:#{node.start_line}"
|
332
|
+
description = "multiple definitions for keys #{dup_keys}"
|
333
|
+
errors.add(:records, "#{location}: #{description}")
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
node.children&.each do |child|
|
338
|
+
validate_no_duplicate_keys_in_node(filename, child)
|
339
|
+
end
|
340
|
+
end
|
299
341
|
end
|
300
342
|
end
|
@@ -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_records_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_records_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_records_templates = implicit_records_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
|
@@ -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
|
@@ -66,7 +66,10 @@ module RecordStore
|
|
66
66
|
Dir["#{dir}/#{name}/*__*.yml"].each do |record_file|
|
67
67
|
definition['records'] += load_yml_record_definitions(name, record_file)
|
68
68
|
end
|
69
|
-
|
69
|
+
|
70
|
+
asts = { filename => Psych.parse_file(filename) }
|
71
|
+
|
72
|
+
Zone.new(name: name, records: definition['records'], config: definition['config'], abstract_syntax_trees: asts)
|
70
73
|
end
|
71
74
|
|
72
75
|
def load_yml_record_definitions(name, record_file)
|
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.5.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:
|
12
|
+
date: 2021-06-04 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
|
@@ -335,6 +335,7 @@ executables:
|
|
335
335
|
extensions: []
|
336
336
|
extra_rdoc_files: []
|
337
337
|
files:
|
338
|
+
- ".github/workflows/ci.yml"
|
338
339
|
- ".gitignore"
|
339
340
|
- ".rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml"
|
340
341
|
- ".rubocop.yml"
|
@@ -380,6 +381,7 @@ files:
|
|
380
381
|
- lib/record_store/zone.rb
|
381
382
|
- lib/record_store/zone/config.rb
|
382
383
|
- lib/record_store/zone/config/ignore_pattern.rb
|
384
|
+
- lib/record_store/zone/config/implicit_record_template.rb
|
383
385
|
- lib/record_store/zone/yaml_definitions.rb
|
384
386
|
- record_store.gemspec
|
385
387
|
- shipit.rubygems.yml
|
@@ -389,6 +391,7 @@ files:
|
|
389
391
|
- template/bin/test
|
390
392
|
- template/config.yml
|
391
393
|
- template/secrets.json
|
394
|
+
- template/templates/implicit_records/implicit_example.yml.erb
|
392
395
|
- template/zones/dnsimple.example.com.yml
|
393
396
|
- template/zones/dnsimple.example.com/A__a-record.yml
|
394
397
|
- template/zones/dnsimple.example.com/TXT__marco.yml
|
@@ -414,7 +417,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
414
417
|
- !ruby/object:Gem::Version
|
415
418
|
version: '0'
|
416
419
|
requirements: []
|
417
|
-
rubygems_version: 3.
|
420
|
+
rubygems_version: 3.2.17
|
418
421
|
signing_key:
|
419
422
|
specification_version: 4
|
420
423
|
summary: Manage DNS using git
|