record_store 6.3.1 → 6.5.1
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/.github/workflows/ci.yml +28 -0
- data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +10 -5
- data/CHANGELOG.md +22 -0
- data/README.md +7 -0
- data/bin/console +1 -0
- data/lib/record_store.rb +8 -0
- data/lib/record_store/version.rb +1 -1
- data/lib/record_store/zone.rb +39 -2
- data/lib/record_store/zone/config.rb +5 -3
- data/lib/record_store/zone/config/implicit_record_template.rb +105 -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 +15 -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: ad3694c8bdf89c030abd7d0b6e4dda77f6b0627783c11d5f96194b18c0be047d
|
4
|
+
data.tar.gz: 65a8d64e1a5feff467d78297d9ed299d409d2bad617829d99291f9ddd0557558
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 257591b26a8b5b58d3752742d43be0acd79268ba0d3f69f2bf09b8469beefc70171900ba04f2b651ffd97d4bed82a49a42c65d002b3cb666e20178ba8d15864c
|
7
|
+
data.tar.gz: 8a605452495449a6a18e55ea57facb5d582977c21df5b74863fdda8ed185a9eafce3d788f6cab8b680bad019edf152c52548aa1dae7356700cf8026c1d5c5e1e
|
@@ -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/CHANGELOG.md
CHANGED
@@ -1,5 +1,27 @@
|
|
1
1
|
# CHANGELOG
|
2
2
|
|
3
|
+
## 6.5.1
|
4
|
+
|
5
|
+
Add support for a new parameter in the implicit records templates:
|
6
|
+
|
7
|
+
- `except_record`: the template will *NOT* generate the `injected_records` for records matching `except_record`, even if they matched `each_record`.
|
8
|
+
|
9
|
+
Also added support for regular expressions in the matching, using the already available `!ruby/regexp` text in a YAML value. The object loaded from the YAML
|
10
|
+
will be of `Regexp` type, and will thus be used to _match_ the value, instead of being identical only. This is supported in both `except_record` and
|
11
|
+
`each_record` fields.
|
12
|
+
|
13
|
+
## 6.5.0
|
14
|
+
|
15
|
+
...
|
16
|
+
|
17
|
+
## 6.4.0
|
18
|
+
|
19
|
+
Add support for injecting implicit records into a zone based on a pre-configured template. Brief overview of template keys:
|
20
|
+
|
21
|
+
- `each_record`: the template will generate all `injected_records` in the template for each Record in the Zone that matches criteria listed here.
|
22
|
+
- `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
|
23
|
+
- `injected_records`: the implicit records that will be generated by the template for every Zone Record that matches the `each_record` criteria
|
24
|
+
|
3
25
|
## 6.3.1
|
4
26
|
- Improve resiliency in the face of temporary provider outages [BUGFIX]
|
5
27
|
|
data/README.md
CHANGED
@@ -116,6 +116,13 @@ When running `bin/record-store apply`, a `Changeset` is generated by comparing t
|
|
116
116
|
|
117
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
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
|
+
|
119
126
|
----
|
120
127
|
|
121
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/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
|
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)
|
@@ -70,10 +71,11 @@ module RecordStore
|
|
70
71
|
end
|
71
72
|
end
|
72
73
|
|
73
|
-
def initialize(name:, records: [], config: {})
|
74
|
+
def initialize(name:, records: [], config: {}, abstract_syntax_trees: {})
|
74
75
|
@name = Record.ensure_ends_with_dot(name)
|
75
76
|
@config = RecordStore::Zone::Config.new(config.deep_symbolize_keys)
|
76
77
|
@records = build_records(records)
|
78
|
+
@abstract_syntax_trees = abstract_syntax_trees
|
77
79
|
end
|
78
80
|
|
79
81
|
def build_changesets(all: false)
|
@@ -189,7 +191,13 @@ module RecordStore
|
|
189
191
|
end
|
190
192
|
|
191
193
|
def build_records(records)
|
192
|
-
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
|
193
201
|
end
|
194
202
|
|
195
203
|
def validate_records
|
@@ -301,5 +309,34 @@ module RecordStore
|
|
301
309
|
|
302
310
|
errors.add(:records, "ALIAS record should be defined on the root of the zone: #{alias_record}")
|
303
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
|
304
341
|
end
|
305
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,105 @@
|
|
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
|
+
|
30
|
+
template_file_yaml = YAML.load(template_file).deep_symbolize_keys
|
31
|
+
filters_for_records_to_template = template_file_yaml[:each_record]
|
32
|
+
filters_for_records_to_exclude = template_file_yaml[:except_record]
|
33
|
+
|
34
|
+
new(template: ERB.new(template_file),
|
35
|
+
filters_for_records_to_template: filters_for_records_to_template,
|
36
|
+
filters_for_records_to_exclude: filters_for_records_to_exclude)
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def template_filepath_for(filename:)
|
42
|
+
"#{RecordStore.implicit_records_templates_path}/#{filename}"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def initialize(template:, filters_for_records_to_template:, filters_for_records_to_exclude:)
|
47
|
+
@template = template
|
48
|
+
@filters_for_records_to_template = filters_for_records_to_template
|
49
|
+
@filters_for_records_to_exclude = filters_for_records_to_exclude
|
50
|
+
end
|
51
|
+
|
52
|
+
def generate_records_to_inject(current_records:)
|
53
|
+
current_records
|
54
|
+
.select { |record| should_template?(record: record) }
|
55
|
+
.map { |record| template_record_for(record: record, current_records: current_records) }
|
56
|
+
.each_with_object([]) do |template_records, records_to_inject|
|
57
|
+
next unless should_inject?(
|
58
|
+
template_records: template_records,
|
59
|
+
current_records: current_records + records_to_inject
|
60
|
+
)
|
61
|
+
|
62
|
+
records_to_inject.push(
|
63
|
+
*template_records.fetch(:injected_records, []).map { |r_yml| Record.build_from_yaml_definition(r_yml) }
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
attr_reader :template, :filters_for_records_to_template, :filters_for_records_to_exclude
|
71
|
+
|
72
|
+
def should_inject?(template_records:, current_records:)
|
73
|
+
current_records.none? do |record|
|
74
|
+
template_records[:conflict_with].any? do |filter|
|
75
|
+
record_match?(record: record, filter: filter)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def should_template?(record:)
|
81
|
+
filters_for_records_to_template.any? { |filter| record_match?(record: record, filter: filter) } && \
|
82
|
+
filters_for_records_to_exclude.none? { |filter| record_match?(record: record, filter: filter) }
|
83
|
+
end
|
84
|
+
|
85
|
+
def record_match?(record:, filter:)
|
86
|
+
filter.all? do |key, value|
|
87
|
+
if value.is_a?(Regexp)
|
88
|
+
value.match(record.public_send(key))
|
89
|
+
else
|
90
|
+
record.public_send(key) == value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def template_record_for(record:, current_records:)
|
96
|
+
context = TemplateContext.build(record: record, current_records: current_records)
|
97
|
+
|
98
|
+
YAML.load(
|
99
|
+
template.result(context.fetch_binding)
|
100
|
+
).deep_symbolize_keys
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
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', '0.
|
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,15 @@
|
|
1
|
+
each_record:
|
2
|
+
- type: A
|
3
|
+
fqdn: abc.123.com.
|
4
|
+
- type: TXT
|
5
|
+
except_record:
|
6
|
+
- fqdn: !ruby/regexp /^_/
|
7
|
+
conflict_with:
|
8
|
+
- type: TXT
|
9
|
+
fqdn: <%= record.fqdn %>.more.domain.com.
|
10
|
+
injected_records:
|
11
|
+
- type: CNAME
|
12
|
+
ttl: 3600
|
13
|
+
fqdn: <%= record.fqdn %>.added.information.com
|
14
|
+
cname: <%= record.fqdn %>.more.added.information.com.
|
15
|
+
|
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.1
|
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-08-09 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
|
@@ -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.20
|
418
421
|
signing_key:
|
419
422
|
specification_version: 4
|
420
423
|
summary: Manage DNS using git
|