puppet-runner 0.0.20 → 0.0.21

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: 0ef7b7a722f72764a0d2b013624442fbf1a953a78f9ccd380d718209f53acf61
4
- data.tar.gz: c060b7c2a5d188de60cda6afc41f9beb6e1693aed4966d609396596cbbdf6c57
3
+ metadata.gz: 255f8a295029af51ad19f6b7eaedaed8d84c23443e113a71d377e2ec619e4cef
4
+ data.tar.gz: 21d692bf0b338f1f4e53e74a675131843e1206cdc0dc7c19c08268cc7ac77eeb
5
5
  SHA512:
6
- metadata.gz: 67de10395d2f7c802a5b00e8c0a15b2149a33514b1cbda0a811cdd34d97a108b5b343b142386918e6aa957ead50fcc0ffe38bc503a56d877bfd89d85faa243af
7
- data.tar.gz: 864c05e7c5b9223d01fc09e1b43ae2925904542128b5ecd81310c60167706918a1768a833679f10fb545cb27850335f803c32fa5b49c282fc71588d679153be2
6
+ metadata.gz: 04c85a270ffedc2ff99e77c264dbb807b22cba02d0b87cd42359323d1793b440d9d145722beb26e3129ed1c62bf2154f7a5153d3c46fcd48e8c7327e25144261
7
+ data.tar.gz: ebb4ab20384b64d3420721fe031a0edad4e230eeeafc22719790ab842b496e2abef9ee32218a0a18635942bb201f6638d09efb92ef85bf19c4dcf218a9f1944c
data/README.md CHANGED
@@ -101,6 +101,271 @@ With this setup we define prefix -> substitution_string pair for each prefix in
101
101
 
102
102
  "hostname"_facts.yaml - must contain all prefixed custom facts to customize the setup
103
103
 
104
+ ### Fact Metadata
105
+
106
+ The defaults file contains all the facts (variables) that are needed to run the tempalte, the basic format of this is:
107
+ ```
108
+ fact_name: 'fact_value'
109
+ ```
110
+
111
+ So in this instance fact_name will have a default value of fact_value, which can be overiden by the user in their own facts file.
112
+
113
+ However it is also possible to add metadata to the facts by changing the format, the current metadata is `comment` and `type`, this extened format looks like:
114
+
115
+ ```
116
+ fact_name:
117
+ value: 'fact_value'
118
+ comment: 'this is an important fact'
119
+ type: 'string'
120
+ ```
121
+
122
+ The two metadata values are:
123
+ `comment` - This is a description for the fact to help identify what it is for, it is added above the fact in the reultant facts files written by puppet-runner
124
+ `type` - This identifies the data type of the fact, by default if this meatadata is not set the value defaults to `string`, current valid types are:
125
+ **string**
126
+ **boolean**
127
+ **nilable**
128
+
129
+ ### Fact Types
130
+
131
+ As mentioned above it is possible to assign a "type" metadata element to each fact, the reasons is that facter delivers all its data as a string, this can cause issues so the "type" metadata allows puppet-runner to do something special for other data typs:
132
+
133
+ #### string
134
+
135
+ All data is passed by default as a string, setting the type to string does not do anythign special
136
+
137
+ #### boolean
138
+
139
+ If the type is set to boolean this tells puppet-runner that we want to pass in true boolean values, a conversion from the string value into true boolean is attempted. If the conversions is successfull the fact reference in the final compiled hiera document (/etc/puppet/hiera/<HOSTNAME>.eyaml) would be replaced with the actual boolean value so that puppet does not recive its string representation.
140
+
141
+ Example:
142
+
143
+ Template
144
+ ```
145
+ ---
146
+
147
+ prefixes:
148
+ - yum_
149
+
150
+ classes:
151
+ - yum
152
+
153
+ dependencies:
154
+ - yum
155
+ - puppi
156
+
157
+ yum::defaultrepo: "%{::yum_defaultrepo}"
158
+ ```
159
+
160
+ If the defaults does not use metadata (or uses a type of string) and the facts are set as below
161
+ ```
162
+ ---
163
+
164
+ yum_defaultrepo: "true"
165
+ ```
166
+
167
+ Then the value writen into /etc/puppet/hiera/<HOSTNAME>.eyaml would be
168
+
169
+ ```
170
+ yum::defaultrepo: "%{::yum_defaultrepo}"
171
+ ```
172
+
173
+ Whereas if the default was set to boolean as below:
174
+ ```
175
+ ---
176
+
177
+ yum_defaultrepo:
178
+ value: "true"
179
+ type: "boolean"
180
+ ```
181
+
182
+ Then the value writen into /etc/puppet/hiera/<HOSTNAME>.eyaml would be
183
+
184
+ ```
185
+ yum::defaultrepo: true
186
+ ```
187
+
188
+ #### nilable
189
+
190
+ If the type is set to nilable this tells puppet-runner that we want to pass in a nil/undef (null) value instead of an empty string, if the fact value is blank '' it will be converted into a tilda (~) as this is the nil representation in hiera. If the conversions is successfull the fact reference in the final compiled hiera document (/etc/puppet/hiera/<HOSTNAME>.eyaml) would be replaced with a tilda. Please note this only works for puppet variables that are defaulted in the code to undef, if they have a value passing nil to them will result in the code default still being set
191
+
192
+ Example:
193
+
194
+ Template
195
+ ```
196
+ ---
197
+ classes:
198
+ - artifactory
199
+
200
+
201
+ artifactory::conf:
202
+ tarball_location_file: "%{::artifactory_file_location}"
203
+ tarball_location_url: "%{::artifactory_url_location}"
204
+ .........
205
+ ```
206
+
207
+ If the defaults does not use metadata (or uses a type of string) and the facts are set as below
208
+ ```
209
+ ---
210
+
211
+ artifactory_file_location: '/tmp/file.zip'
212
+ artifactory_url_location:
213
+ ```
214
+
215
+ They are therefore both mandatory fileds and puppet-runner will ask for you to give a value for both, however in reality these are mutually exclusive, one will take precident over the other so passing them both could have unforseen consequences.
216
+
217
+ The other otpion is to set the facts to be empty string, however this still passed an empty string into puppet which, unless the code has been written to discount that, could still cause issues:
218
+ ```
219
+ artifactory_file_location: '/tmp/file.zip'
220
+ artifactory_url_location: ''
221
+ ```
222
+
223
+ Then the value writen into /etc/puppet/hiera/<HOSTNAME>.eyaml would be
224
+
225
+ ```
226
+ artifactory::conf:
227
+ tarball_location_file: "%{::artifactory_file_location}"
228
+ tarball_location_url: "%{::artifactory_url_location}"
229
+ ```
230
+
231
+ Whereas if the metadata type was set to nilable and a value of '' (empty string is supplied) in the defaults
232
+ ```
233
+ ---
234
+
235
+ artifactory_file_location:
236
+ value: ''
237
+ comment: 'blah'
238
+ type: 'nilable'
239
+ artifactory_url_location:
240
+ value: ''
241
+ comment: 'other blah'
242
+ type: 'nilable'
243
+ ```
244
+
245
+ And the facts were set with a value for one and empty string for the other as below:
246
+ ```
247
+ artifactory_file_location: '/tmp/file.zip'
248
+ artifactory_url_location: ''
249
+ ```
250
+
251
+ Then the value writen into /etc/puppet/hiera/<HOSTNAME>.eyaml would be
252
+
253
+ ```
254
+ artifactory::conf:
255
+ tarball_location_file: "%{::artifactory_file_location}"
256
+ tarball_location_url: ~
257
+ ```
258
+
259
+ ### Inter fact references
260
+
261
+ There are a number of occasions where you may want one fact to point to the value of another, either because you want it to be exactly the same or you want your fact to be a superset of the other, puppet-runner will evaluate facts that reference another fact and present the last fact reference to puppet, the resolution will recursivly resolve facts down to their base fact to a max depth of 5, after which it will stop in order to prevent infinite loops, this will cause a failure of the fact lookup.
262
+
263
+ #### Example 1: Fact directly referneces another fact
264
+
265
+ In this example we want our fact to reference another, in this example we will point a custom fact at a system fact (although you can point it at any fact, system or custom)
266
+
267
+ **template:**
268
+ ````
269
+ ---
270
+
271
+ prefixes:
272
+ - vpn_snat_
273
+
274
+ classes:
275
+ - fw
276
+
277
+ dependencies:
278
+ - fw
279
+ - firewall
280
+ - stdlib
281
+
282
+ fw::rules: &fw_rules
283
+ "%{::vpn_snat_description}":
284
+ chain: "%{::vpn_snat_chain}"
285
+ tosource: "%{::vpn_snat_tosource}"
286
+ jump: "%{::vpn_snat_jump}"
287
+ source: "%{::vpn_snat_source}"
288
+ table: "%{::vpn_snat_table}"
289
+ proto: "%{::vpn_snat_proto}"
290
+ ````
291
+
292
+ We want the `tosource` value to be set with the IP address for the servers eth0 adapter, there is already a system fact for this `ipaddress_eth0`
293
+
294
+ We set the facts as below:
295
+
296
+ ````
297
+ vpn_snat_description: '000 VPN SNAT Configuration'
298
+ vpn_snat_chain: 'POSTROUTING'
299
+ vpn_snat_tosource: "%{::ipaddress_eth0}"
300
+ vpn_snat_jump: 'SNAT'
301
+ vpn_snat_source: '172.28.254.0/23'
302
+ vpn_snat_table: 'nat'
303
+ vpn_snat_proto: 'all'
304
+ ````
305
+
306
+ Pupet-runner will resolve the `vpn_snat_tosource` faqt down to the first fact it references, which is `ipaddress_eth0`, as a result the value writen into /etc/puppet/hiera/<HOSTNAME>.eyaml would be
307
+
308
+ ````
309
+ fw::rules: &fw_rules
310
+ "%{::vpn_snat_description}":
311
+ chain: "%{::vpn_snat_chain}"
312
+ tosource: "%{::ipaddress_eth0}"
313
+ jump: "%{::vpn_snat_jump}"
314
+ source: "%{::vpn_snat_source}"
315
+ table: "%{::vpn_snat_table}"
316
+ proto: "%{::vpn_snat_proto}"
317
+ ````
318
+
319
+ #### Example 2: Fact includes another fact as part of its value
320
+
321
+ In this example we want our fact to be a superset of another fact.
322
+
323
+ **template:**
324
+ ````
325
+ ---
326
+
327
+ prefixes:
328
+ - tripwire_
329
+
330
+ classes:
331
+ - tripwire
332
+
333
+ dependencies:
334
+ - tripwire
335
+ - stdlib
336
+ - concat
337
+
338
+ tripwire::local_passphrase: '%{::tripwire_local_passphrase}'
339
+ tripwire::site_passphrase: '%{::tripwire_site_passphrase}'
340
+ tripwire::tripwire_email: '%{::tripwire_tripwire_email}'
341
+ tripwire::tripwire_policy_file: '%{::tripwire_tripwire_policy_file}'
342
+ ````
343
+
344
+ We want the `site_passphrase` value to be the same as `local_passphrase` but with _LOCAL at the end
345
+
346
+ We set the facts as below:
347
+
348
+ ````
349
+ tripwire_global_passphrase: 'super_secret'
350
+ tripwire_local_passphrase: "%{::tripwire_site_passphrase}_LOCAL"
351
+ tripwire_site_passphrase: "%{::tripwire_global_passphrase}
352
+ tripwire_tripwire_email: 'blackhole'
353
+ tripwire_tripwire_policy_file: 'false'
354
+ ````
355
+
356
+ Note here we have added a custom fact that is not references in any template or default, this will still be avaliable via facter in the normal way.
357
+
358
+ The fact `tripwire_site_passphrase` will resolve down to `tripwire_global_passphrase` as in the previous example, however the fact `tripwire_local_passphrase` will be resolved twice (once to `tripwire_site_passphrase` and then again down to `tripwire_global_passphrase`)
359
+
360
+ As a result the value writen into /etc/puppet/hiera/<HOSTNAME>.eyaml would be
361
+
362
+ ````
363
+ tripwire::local_passphrase: '%{::tripwire_global_passphrase}_LOCAL'
364
+ tripwire::site_passphrase: '%{::tripwire_global_passphrase}'
365
+ tripwire::tripwire_email: '%{::tripwire_tripwire_email}'
366
+ tripwire::tripwire_policy_file: '%{::tripwire_tripwire_policy_file}'
367
+ ````
368
+
104
369
  #### TEMPLATES
105
370
  Must contain 2 subdirectories.
106
371
  - templates - template yaml files
@@ -155,6 +420,7 @@ Path to output Puppetfile.
155
420
  * -o PUPPETFILE_OUTPUT_PATH --puppetfile_output_path PUPPETFILE_OUTPUT_PATH Result Puppetfile path
156
421
  * -e EYAML_KEY_PATH --eyaml_key_pair EYAML_KEY_PATH Path to eyaml encryption key pair
157
422
  * -p PUPPET_APPLY --puppet_apply PUPPET_APPLY Custom puppet apply command to run
423
+ * -k --keep-facts Flag to keep the encrypted facts file in /tmp for analysis
158
424
 
159
425
  Commands:
160
426
 
data/bin/puppet-runner CHANGED
@@ -107,12 +107,35 @@ def extract_type_from_hash(input)
107
107
  type = val["type"]
108
108
  end
109
109
  end
110
- {key => type }
110
+ {type => key }
111
111
  }
112
112
  end
113
113
  res
114
114
  end
115
115
 
116
+ # function to resolve inter fact references, it should return the last fact name (not value)
117
+ def resolve_fact(fact_name, max_depth=5, current_depth=0)
118
+ final_val = nil
119
+ fact_name = fact_name.sub(/%{::/,'').sub(/}/,'')
120
+ if max_depth == current_depth
121
+ warning "Fact resolution has reached a depth of 5 facts, aborting lookup"
122
+ else
123
+ # attempt to get value
124
+ val = $all_facts[fact_name] || Facter.value(fact_name) || nil
125
+
126
+ if val == "%{::#{fact_name}}"
127
+ warning "Fact resolves to itself, skipping"
128
+ elsif val.instance_of?(String) and val.match(/%{::.*.}/)
129
+ val = resolve_fact(val,max_depth, current_depth + 1)
130
+ else
131
+ val = "%{::#{fact_name}}"
132
+ end
133
+ final_val = val
134
+ end
135
+ final_val
136
+ end
137
+
138
+
116
139
  begin
117
140
  options = Docopt::docopt(doc)
118
141
  rescue Docopt::Exit => e
@@ -170,7 +193,8 @@ if options['all'] || options['prepare']
170
193
  prefixed_facts_comments = {}
171
194
  puppetfile_config = YAML.load_file(puppetfile_config_path) || {}
172
195
  puppetfile_dependencies = []
173
- global_transform_data = []
196
+ global_data_types = []
197
+ nil_transform_present = false
174
198
  # functionalities:
175
199
  # # In honor of Henry...
176
200
  # 1_app:
@@ -211,17 +235,7 @@ if options['all'] || options['prepare']
211
235
  fact_comments_as_string = extract_comment_from_hash(default_facts).to_s
212
236
 
213
237
  # get a list of each fields type if set (if not they will report as strings)
214
- data_type = extract_type_from_hash(default_facts)
215
-
216
- # Check to see if any of the fields are booleans, if so add an entry to the transform_data array
217
- transform_data = []
218
- data_type.each do | field|
219
- field.each do | field_name, field_type |
220
- if field_type == 'boolean'
221
- transform_data << field_name
222
- end
223
- end
224
- end
238
+ data_types = extract_type_from_hash(default_facts)
225
239
 
226
240
  if to_add.is_a?(Hash)
227
241
  # if prefixes are not defined skip replacing
@@ -239,7 +253,13 @@ if options['all'] || options['prepare']
239
253
  facts_as_string = facts_as_string.gsub(/#{prefix}/, "#{replace_prefixes_with}")
240
254
  fact_comments_as_string = fact_comments_as_string.gsub(/#{prefix}/, "#{replace_prefixes_with}")
241
255
  prefixed_required_facts = prefixed_required_facts.merge(required_facts.map! { |item| item.gsub(/#{prefix}/, "#{replace_prefixes_with}") })
242
- transform_data = transform_data.map {|s| s.gsub(/#{prefix}/, "#{replace_prefixes_with}")} if !transform_data.empty?
256
+ if !data_types.empty?
257
+ data_types.each do | transform |
258
+ transform.each do|key, value|
259
+ transform[key] = value.gsub(/#{prefix}/, "#{replace_prefixes_with}")
260
+ end
261
+ end
262
+ end
243
263
  end
244
264
  end
245
265
  end
@@ -252,7 +272,13 @@ if options['all'] || options['prepare']
252
272
  facts_as_string = facts_as_string.gsub(/#{prefix}/, "#{replace_prefixes_with}")
253
273
  fact_comments_as_string = fact_comments_as_string.gsub(/#{prefix}/, "#{replace_prefixes_with}")
254
274
  prefixed_required_facts = prefixed_required_facts.merge(required_facts.map! { |item| item.gsub(/#{prefix}/, "#{replace_prefixes_with}") })
255
- transform_data = transform_data.map {|s| s.gsub(/#{prefix}/, "#{replace_prefixes_with}")} if !transform_data.empty?
275
+ if !data_types.empty?
276
+ data_types.each do | transform |
277
+ transform.each do|key, value|
278
+ transform[key] = value.gsub(/#{prefix}/, "#{replace_prefixes_with}")
279
+ end
280
+ end
281
+ end
256
282
  end
257
283
  end
258
284
  end
@@ -267,8 +293,8 @@ if options['all'] || options['prepare']
267
293
  default_fact_comments = extract_comment_from_hash(plain_facts)
268
294
  prefixed_required_facts = prefixed_required_facts.merge(required_facts)
269
295
  end
270
- # add the "local" transformation list to the global one
271
- global_transform_data.push(*transform_data)
296
+ # add the "local" data type list to the global one
297
+ global_data_types.push(*data_types)
272
298
 
273
299
  result_template.deep_merge!(template)
274
300
  # default_facts_prefixed is Array of hashes as the result of map, this will create hash from it
@@ -287,37 +313,89 @@ if options['all'] || options['prepare']
287
313
 
288
314
  File.open(output_encrypted_facts_file_path, 'w+') do |output_file|
289
315
  output_result_default_facts = result_default_facts.deep_merge!(custom_facts, {:merge_hash_arrays => true}).to_yaml
316
+
317
+ # convert final facts to hash for inter fact resolution and also for potential transformations later
318
+ $all_facts = YAML.load(output_result_default_facts)
319
+
320
+ #convert result templat to string
321
+ result_template = result_template.to_s
322
+
323
+ # loop through facts looking for any that reference other facts
324
+ $all_facts.each do | fact_key, fact_val |
325
+ if fact_val.instance_of?(String) and fact_val.match(/%{::.*.}/)
326
+ debug "Fact #{fact_key} references another fact or facts"
327
+ # find each instance of a fact within the value (it may contain multiple facts
328
+ # i.e "TEST%{::fact1}TEST%{::fact2}"
329
+ fact_val.gsub(/}/,"}\n").scan(/%{::.*.}/).each do | fact |
330
+ # resolve the fact down to its last reference to another fact (not the end value)
331
+ resolved_fact = resolve_fact(fact)
332
+ # if the resolved fact name is not the same as the original fact we found referenced
333
+ # then replace the value in fact_val
334
+ if !resolved_fact.nil? and resolved_fact != fact
335
+ fact_val = fact_val.gsub(/#{fact}/,"#{resolved_fact}")
336
+ end
337
+ end
338
+ debug "Attempting to replace fact '#{fact_key}' with value '#{fact_val}' in compiled template"
339
+ # replace the original fact reference in the template with the resovled value
340
+ # this is done before global teansformation as they may change the final value again
341
+ result_template = result_template.gsub(/\"\%{::#{fact_key}}\"/, "\"#{fact_val}\"")
342
+ end
343
+ end
344
+
345
+ # add comments above any fact lines
290
346
  prefixed_facts_comments.each do |pattern, replacement|
291
347
  if replacement != nil
292
348
  output_result_default_facts.gsub!(/^#{pattern}/, "\##{replacement}\n#{pattern}")
293
349
  end
294
350
  end
295
351
 
352
+ # write the temp encrypted facts file
296
353
  output_file.write(output_result_default_facts)
297
354
 
298
355
  # now that the merged final facts are present look for any global transformations to apply
299
- # global transformations are currently just booleans that need to be expressed directly in output file
300
- if !global_transform_data.empty?
301
- result_template = result_template.to_s
302
- all_facts = YAML.load(output_result_default_facts)
303
- global_transform_data.each do | transform_value |
304
- debug "Attempting to replace boolean value for fact #{transform_value}"
305
- begin
306
- # convert the final template to a string, replace any facts that are booleans with their actual value and then convert the result bacl to YAML
307
- # convert the fact to boolean and then back to string during the replace, this allows validation that the fact is actually a boolean
308
- result_template = result_template.gsub(/\"\%{::#{transform_value}}\"/, all_facts[transform_value].to_bool.to_s)
309
- rescue
310
- warning "Unable to convert fact #{transform_value} with value #{all_facts[transform_value]} into boolean, conversion will be skipped"
356
+ # global transformations are currently either booleans that need to be expressed directly in
357
+ # the output file or nilable values that need to be expressed as unquoted tildas (~)
358
+ if !global_data_types.empty?
359
+ global_data_types.each do | transform |
360
+ transform.each do|transform_type, transform_value|
361
+ begin
362
+ if transform_type == 'boolean'
363
+ debug "Attempting to replace boolean value for fact #{transform_value}"
364
+ # convert the fact to boolean and then back to string during the replace, this allows validation that the fact is actually a boolean
365
+ result_template = result_template.gsub(/\"\%{::#{transform_value}}\"/, $all_facts[transform_value].to_bool.to_s)
366
+ elsif transform_type == 'nilable'
367
+ # replace fact reference with tilda if the value is nil, empty or already a tilda
368
+ # due to issues with ruby hash values being unquoted we will quote it now and remove the quotes later
369
+ if $all_facts[transform_value].nil? or $all_facts[transform_value].empty? or $all_facts[transform_value] == "~"
370
+ debug "Attempting to replace nilable value for fact #{transform_value}"
371
+ # replace value with tilda
372
+ result_template = result_template.gsub(/\"\%{::#{transform_value}}\"/, '"~"')
373
+ # identify that we have made at least one nil transformation
374
+ nil_transform_present = true
375
+ end
376
+ end
377
+ rescue
378
+ warning "Unable to convert fact #{transform_value} with value #{all_facts[transform_value]} into #{transform_type}, conversion will be skipped"
379
+ end
311
380
  end
312
381
  end
313
- result_template = eval(result_template)
314
382
  end
315
383
  end
316
384
 
385
+ # convert result_template back to hash
386
+ result_template = eval(result_template)
387
+
317
388
  # Write results
318
389
  File.open(output_file_path, 'w+') do |output_file|
319
390
  YAML.dump(result_template, output_file)
320
391
  end
392
+
393
+ # hack to get around the fact we have to pass tilda as quoted earlier
394
+ if nil_transform_present
395
+ compiled_hiera = File.read(output_file_path)
396
+ replaced_hiera = compiled_hiera.gsub('"~"', '~')
397
+ File.open(output_file_path, "w") {|new_hiera| new_hiera.puts replaced_hiera}
398
+ end
321
399
 
322
400
  # decrypt facts file because Puppet doesn't appear to be able to read encrypted facts
323
401
  require 'hiera/backend/eyaml/plugins'
@@ -4,7 +4,7 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
 
5
5
  Gem::Specification.new do |spec|
6
6
  spec.name = "puppet-runner"
7
- spec.version = "0.0.20"
7
+ spec.version = "0.0.21"
8
8
  spec.authors = ["Martin Brehovsky", "Matthew Hope"]
9
9
  spec.email = ["mbrehovsky@adaptavist.com"]
10
10
  spec.summary = %q{Preprocessor for hiera config}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: puppet-runner
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.20
4
+ version: 0.0.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Martin Brehovsky
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-04-05 00:00:00.000000000 Z
12
+ date: 2018-04-20 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler