leap_cli 1.7.3 → 1.7.4

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
  SHA1:
3
- metadata.gz: 36be7f0c765dbd1c64ca36079780ad6f44797fe0
4
- data.tar.gz: 379a244c172b9a07d0f9b1a1635d47aa16bd68bc
3
+ metadata.gz: fde4e852d415bcc30ed9d48a65e5b264ac999d4f
4
+ data.tar.gz: a3658cdc96431cf991509bbd48e0d3aa4afdadc3
5
5
  SHA512:
6
- metadata.gz: 079ed74cf97faa015438ecf0dbc4ac2e4bc50a0360e48b602597ef9e8a7e7959943872a498554ef38d455ba461ff65e481d444c25bd55d14e92b6f7abd842fed
7
- data.tar.gz: 1ec35e8f043b0b0b4516a6fa447420f0d1eb0e42c062b40afb9aae9ab939c0ce50b97dcf5989a5aac7fd70d26aa88333a8e77c655dfa76d97d7573a616f77f20
6
+ metadata.gz: 5ab70f0f7a3a39e09ccac39dd3fb69d7dd6a5b47366f1251b24dbfc283ada437ceed72ce1a9e5a27b96d620e7206da597e1c482c0722aca3d2116db115b5f9e5
7
+ data.tar.gz: 9838e7df5040c85caae31cb394edad4da7f4f809c15c3e5046707e8d4ea97195c75f49d0947df289c91bfaad3fe1549249bf1409b2bce07d49ecae0bfdb9ddd7
data/bin/leap CHANGED
@@ -18,20 +18,11 @@ LEAP_CLI_BASE_DIR = File.expand_path('..', File.dirname(File.symlink?(__FILE__)
18
18
  ORIGINAL_ARGV = ARGV.dup
19
19
 
20
20
  begin
21
+ # First, try to load the leap_cli code that is local to this `leap` command.
22
+ # If that fails, then we try to load leap_cli as a gem.
23
+ require File.join(LEAP_CLI_BASE_DIR, 'lib','leap_cli','load_paths')
21
24
  require 'leap_cli'
22
25
  rescue LoadError
23
- #
24
- # When developing a gem with a command, you normally use `bundle exec bin/command-name`
25
- # to run your app. At install-time, RubyGems will make sure lib, etc. are in the load path,
26
- # so that you can run the command directly.
27
- #
28
- # However, I don't like using 'bundle exec'. It is slow, and limits which directory you can
29
- # run in. So, instead, we fall back to some path manipulation hackery.
30
- #
31
- # This allows you to run the command directly while developing the gem, and also lets you
32
- # run from anywhere (I like to link 'bin/leap' to /usr/local/bin/leap).
33
- #
34
- require File.join(LEAP_CLI_BASE_DIR, 'lib','leap_cli','load_paths')
35
26
  require 'leap_cli'
36
27
  end
37
28
 
@@ -1,3 +1,4 @@
1
+ require 'socket'
1
2
 
2
3
  module LeapCli
3
4
  module Commands
@@ -14,12 +15,16 @@ module LeapCli
14
15
  end
15
16
  if environment
16
17
  if manager.environment_names.include?(environment)
17
- compile_hiera_files(manager.filter([environment]))
18
+ compile_hiera_files(manager.filter([environment]), false)
18
19
  else
19
20
  bail! "There is no environment named `#{environment}`."
20
21
  end
21
22
  else
22
- compile_hiera_files(manager.filter)
23
+ clean_export = LeapCli.leapfile.environment.nil?
24
+ compile_hiera_files(manager.filter, clean_export)
25
+ end
26
+ if file_exists?(:static_web_readme)
27
+ compile_provider_json(environment)
23
28
  end
24
29
  end
25
30
  end
@@ -31,20 +36,26 @@ module LeapCli
31
36
  end
32
37
  end
33
38
 
39
+ c.desc "Compile provider.json bootstrap files for your provider."
40
+ c.command 'provider.json' do |provider|
41
+ provider.action do |global_options, options, args|
42
+ compile_provider_json
43
+ end
44
+ end
45
+
34
46
  c.default_command :all
35
47
  end
36
48
 
37
49
  protected
38
50
 
39
- def compile_hiera_files(nodes=nil)
40
- # these must come first
41
- update_compiled_ssh_configs
42
-
43
- # export generated files
51
+ #
52
+ # a "clean" export of secrets will also remove keys that are no longer used,
53
+ # but this should not be done if we are not examining all possible nodes.
54
+ #
55
+ def compile_hiera_files(nodes, clean_export)
56
+ update_compiled_ssh_configs # must come first
57
+ sanity_check(nodes)
44
58
  manager.export_nodes(nodes)
45
- # a "clean" export of secrets will also remove keys that are no longer used,
46
- # but this should not be done if we are not examining all possible nodes.
47
- clean_export = nodes.nil?
48
59
  manager.export_secrets(clean_export)
49
60
  end
50
61
 
@@ -54,6 +65,34 @@ module LeapCli
54
65
  update_known_hosts
55
66
  end
56
67
 
68
+ def sanity_check(nodes)
69
+ # confirm that every node has a unique ip address
70
+ ips = {}
71
+ nodes.pick_fields('ip_address').each do |name, ip_address|
72
+ if ips.key?(ip_address)
73
+ bail! {
74
+ log(:fatal_error, "Every node must have its own IP address.") {
75
+ log "Nodes `#{name}` and `#{ips[ip_address]}` are both configured with `#{ip_address}`."
76
+ }
77
+ }
78
+ else
79
+ ips[ip_address] = name
80
+ end
81
+ end
82
+ # confirm that the IP address of this machine is not also used for a node.
83
+ Socket.ip_address_list.each do |addrinfo|
84
+ if !addrinfo.ipv4_private? && ips.key?(addrinfo.ip_address)
85
+ ip = addrinfo.ip_address
86
+ name = ips[ip]
87
+ bail! {
88
+ log(:fatal_error, "Something is very wrong. The `leap` command must only be run on your sysadmin machine, not on a provider node.") {
89
+ log "This machine has the same IP address (#{ip}) as node `#{name}`."
90
+ }
91
+ }
92
+ end
93
+ end
94
+ end
95
+
57
96
  ##
58
97
  ## SSH
59
98
  ##
@@ -132,6 +171,78 @@ module LeapCli
132
171
  write_file!(:known_hosts, buffer.string)
133
172
  end
134
173
 
174
+ ##
175
+ ## provider.json
176
+ ##
177
+
178
+ #
179
+ # generates static provider.json files that can put into place
180
+ # (e.g. https://domain/provider.json) for the cases where the
181
+ # webapp domain does not match the provider's domain.
182
+ #
183
+ def compile_provider_json(environments=nil)
184
+ webapp_nodes = manager.nodes[:services => 'webapp']
185
+ write_file!(:static_web_readme, STATIC_WEB_README)
186
+ environments ||= manager.environment_names
187
+ environments.each do |env|
188
+ node = webapp_nodes[:environment => env].values.first
189
+ if node
190
+ env ||= 'default'
191
+ write_file!(
192
+ [:static_web_provider_json, env],
193
+ node['definition_files']['provider']
194
+ )
195
+ write_file!(
196
+ [:static_web_htaccess, env],
197
+ HTACCESS_FILE % {:min_version => manager.env(env).provider.client_version['min']}
198
+ )
199
+ end
200
+ end
201
+ end
202
+
203
+ HTACCESS_FILE = %[
204
+ <Location /provider.json>
205
+ Header set X-Minimum-Client-Version %{min_version}
206
+ </Location>
207
+ ]
208
+
209
+ STATIC_WEB_README = %[
210
+ This directory contains statically rendered copies of the `provider.json` file
211
+ used by the client to "bootstrap" configure itself for use with your service
212
+ provider.
213
+
214
+ There is a separate provider.json file for each environment, although you
215
+ should only need 'production/provider.json' or, if you have no environments
216
+ configured, 'default/provider.json'.
217
+
218
+ To clarify, this is the public `provider.json` file used by the client, not the
219
+ `provider.json` file that is used to configure the provider.
220
+
221
+ The provider.json file must be available at `https://domain/provider.json`
222
+ (unless this provider is included in the list of providers which are pre-
223
+ seeded in client).
224
+
225
+ This provider.json file can be served correctly in one of three ways:
226
+
227
+ (1) If the property webapp.domain is not configured, then the web app will be
228
+ installed at https://domain/ and it will handle serving the provider.json file.
229
+
230
+ (2) If one or more nodes have the 'static' service configured for the provider's
231
+ domain, then these 'static' nodes will correctly serve provider.json.
232
+
233
+ (3) Otherwise, you must copy the provider.json file to your web
234
+ server and make it available at '/provider.json'. The example htaccess
235
+ file shows what header options should be sent by the web server
236
+ with the response.
237
+
238
+ This directory is needed for method (3), but not for methods (1) or (2).
239
+
240
+ This directory has been created by the command `leap compile provider.json`.
241
+ Once created, it will be kept up to date everytime you compile. You may safely
242
+ remove this directory if you don't use it.
243
+ ]
244
+
245
+ ##
135
246
  ##
136
247
  ## ZONE FILE
137
248
  ##
@@ -183,7 +294,7 @@ module LeapCli
183
294
  if node.dns['aliases']
184
295
  node.dns.aliases.each do |host_alias|
185
296
  if host_alias != node.domain.full && host_alias != provider.domain
186
- put_line.call relative_hostname(host_alias), "IN CNAME #{relative_hostname(node.domain.full)}"
297
+ put_line.call relative_hostname(host_alias), "IN A #{node.ip_address}"
187
298
  end
188
299
  end
189
300
  end
@@ -34,7 +34,7 @@ module LeapCli
34
34
  init_submodules
35
35
  end
36
36
 
37
- nodes = manager.filter!(args)
37
+ nodes = manager.filter!(args, :disabled => false)
38
38
  if nodes.size > 1
39
39
  say "Deploying to these nodes: #{nodes.keys.join(', ')}"
40
40
  if !global[:yes] && !agree("Continue? ")
@@ -51,7 +51,7 @@ module LeapCli
51
51
  end
52
52
  # compile hiera files for all the nodes in every environment that is
53
53
  # being deployed and only those environments.
54
- compile_hiera_files(manager.filter(environments))
54
+ compile_hiera_files(manager.filter(environments), false)
55
55
  # update server certificates if needed
56
56
  update_certificates(nodes)
57
57
 
@@ -49,7 +49,7 @@ module LeapCli; module Commands
49
49
  if overwrite || content.nil? || content.empty?
50
50
  old_facts = {}
51
51
  else
52
- old_facts = JSON.parse(content)
52
+ old_facts = manager.facts
53
53
  end
54
54
  facts = old_facts.merge(new_facts)
55
55
  facts.each do |name, value|
@@ -57,7 +57,7 @@ module LeapCli; module Commands
57
57
  if value == ""
58
58
  value = nil
59
59
  else
60
- value = JSON.parse(value)
60
+ value = JSON.parse(value) rescue JSON::ParserError
61
61
  end
62
62
  end
63
63
  if value.is_a? Hash
@@ -69,7 +69,7 @@ module LeapCli; module Commands
69
69
  value.nil? || value.empty?
70
70
  end
71
71
  if facts.empty?
72
- nil
72
+ "{}\n"
73
73
  else
74
74
  JSON.sorted_generate(facts) + "\n"
75
75
  end
@@ -79,7 +79,7 @@ module LeapCli; module Commands
79
79
  private
80
80
 
81
81
  def update_facts(global_options, options, args)
82
- nodes = manager.filter(args, :local => false)
82
+ nodes = manager.filter(args, :local => false, :disabled => false)
83
83
  new_facts = {}
84
84
  ssh_connect(nodes) do |ssh|
85
85
  ssh.leap.run_with_progress(facter_cmd) do |response|
@@ -46,12 +46,15 @@ module LeapCli; module Commands
46
46
  max_width = nodes.keys.inject(0) {|max,i| [i.size,max].max}
47
47
  nodes.each_node do |node|
48
48
  value = properties.collect{|prop|
49
- if node[prop].nil?
49
+ prop_value = node[prop]
50
+ if prop_value.nil?
50
51
  "null"
51
- elsif node[prop] == ""
52
+ elsif prop_value == ""
52
53
  "empty"
54
+ elsif prop_value.is_a? LeapCli::Config::Object
55
+ node[prop].dump_json(:compact) # TODO: add option of getting pre-evaluation values.
53
56
  else
54
- node[prop]
57
+ prop_value.to_s
55
58
  end
56
59
  }.join(', ')
57
60
  printf("%#{max_width}s %s\n", node.name, value)
@@ -47,6 +47,10 @@ module LeapCli; module Commands
47
47
  # :color -- true or false, to log in color or not.
48
48
  #
49
49
  def initialize_leap_cli(require_provider, options={})
50
+ if Process::Sys.getuid == 0
51
+ bail! "`leap` should not be run as root."
52
+ end
53
+
50
54
  # set verbosity
51
55
  options[:verbose] ||= 1
52
56
  LeapCli.set_log_level(options[:verbose].to_i)
@@ -9,8 +9,15 @@ module LeapCli; module Commands
9
9
  local.desc 'Starts up the virtual machine(s)'
10
10
  local.arg_name 'FILTER', :optional => true #, :multiple => false
11
11
  local.command :start do |start|
12
+ start.flag(:basebox,
13
+ :desc => "The basebox to use. This value is passed to vagrant as the "+
14
+ "`config.vm.box` option. The value here should be the name of an installed box or a "+
15
+ "shorthand name of a box in HashiCorp's Atlas.",
16
+ :arg_name => 'BASEBOX',
17
+ :default_value => 'LEAP/wheezy'
18
+ )
12
19
  start.action do |global_options,options,args|
13
- vagrant_command(["up", "sandbox on"], args)
20
+ vagrant_command(["up", "sandbox on"], args, options)
14
21
  end
15
22
  end
16
23
 
@@ -84,8 +91,8 @@ module LeapCli; module Commands
84
91
 
85
92
  protected
86
93
 
87
- def vagrant_command(cmds, args)
88
- vagrant_setup
94
+ def vagrant_command(cmds, args, options={})
95
+ vagrant_setup(options)
89
96
  cmds = cmds.to_a
90
97
  if args.empty?
91
98
  nodes = [""]
@@ -108,7 +115,7 @@ module LeapCli; module Commands
108
115
 
109
116
  private
110
117
 
111
- def vagrant_setup
118
+ def vagrant_setup(options)
112
119
  assert_bin! 'vagrant', 'Vagrant is required for running local virtual machines. Run "sudo apt-get install vagrant".'
113
120
 
114
121
  if vagrant_version <= Gem::Version.new('1.0.0')
@@ -123,7 +130,7 @@ module LeapCli; module Commands
123
130
  assert_run! 'vagrant plugin install sahara'
124
131
  end
125
132
  end
126
- create_vagrant_file
133
+ create_vagrant_file(options)
127
134
  end
128
135
 
129
136
  def vagrant_version
@@ -135,16 +142,18 @@ module LeapCli; module Commands
135
142
  exec cmd
136
143
  end
137
144
 
138
- def create_vagrant_file
145
+ def create_vagrant_file(options)
139
146
  lines = []
140
147
  netmask = IPAddr.new('255.255.255.255').mask(LeapCli.leapfile.vagrant_network.split('/').last).to_s
141
148
 
149
+ basebox = options[:basebox] || 'LEAP/wheezy'
150
+
142
151
  if vagrant_version <= Gem::Version.new('1.1.0')
143
152
  lines << %[Vagrant::Config.run do |config|]
144
153
  manager.each_node do |node|
145
154
  if node.vagrant?
146
155
  lines << %[ config.vm.define :#{node.name} do |config|]
147
- lines << %[ config.vm.box = "LEAP/wheezy"]
156
+ lines << %[ config.vm.box = "#{basebox}"]
148
157
  lines << %[ config.vm.network :hostonly, "#{node.ip_address}", :netmask => "#{netmask}"]
149
158
  lines << %[ config.vm.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]]
150
159
  lines << %[ config.vm.customize ["modifyvm", :id, "--name", "#{node.name}"]]
@@ -157,7 +166,7 @@ module LeapCli; module Commands
157
166
  manager.each_node do |node|
158
167
  if node.vagrant?
159
168
  lines << %[ config.vm.define :#{node.name} do |config|]
160
- lines << %[ config.vm.box = "LEAP/wheezy"]
169
+ lines << %[ config.vm.box = "#{basebox}"]
161
170
  lines << %[ config.vm.network :private_network, ip: "#{node.ip_address}"]
162
171
  lines << %[ config.vm.provider "virtualbox" do |v|]
163
172
  lines << %[ v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]]
@@ -97,6 +97,9 @@ module LeapCli
97
97
  if @options[:local] === false
98
98
  node_list = node_list[:environment => '!local']
99
99
  end
100
+ if @options[:disabled] === false
101
+ node_list = node_list[:environment => '!disabled']
102
+ end
100
103
  node_list
101
104
  end
102
105
 
@@ -43,7 +43,15 @@ module LeapCli
43
43
  # returns the Hash of the contents of facts.json
44
44
  #
45
45
  def facts
46
- @facts ||= JSON.parse(Util.read_file(:facts) || "{}")
46
+ @facts ||= begin
47
+ content = Util.read_file(:facts)
48
+ if !content || content.empty?
49
+ content = "{}"
50
+ end
51
+ JSON.parse(content)
52
+ rescue SyntaxError, JSON::ParserError => exc
53
+ Util::bail! "Could not parse facts.json -- #{exc}"
54
+ end
47
55
  end
48
56
 
49
57
  #
@@ -247,8 +255,8 @@ module LeapCli
247
255
  #
248
256
  # same as filter(), but exits if there is no matching nodes
249
257
  #
250
- def filter!(filters)
251
- node_list = filter(filters)
258
+ def filter!(filters, options={})
259
+ node_list = filter(filters, options)
252
260
  Util::assert! node_list.any?, "Could not match any nodes from '#{filters.join ' '}'"
253
261
  return node_list
254
262
  end
@@ -85,9 +85,13 @@ module LeapCli
85
85
  #
86
86
  # export JSON
87
87
  #
88
- def dump_json
88
+ def dump_json(*options)
89
89
  evaluate(@node)
90
- JSON.sorted_generate(self)
90
+ if options.include? :compact
91
+ self.to_json
92
+ else
93
+ JSON.sorted_generate(self)
94
+ end
91
95
  end
92
96
 
93
97
  def evaluate(context=@node)
@@ -143,7 +147,7 @@ module LeapCli
143
147
  elsif key =~ /\./
144
148
  # for keys with with '.' in them, we start from the root object (@node).
145
149
  keys = key.split('.')
146
- value = @node.get!(keys.first)
150
+ value = self.get!(keys.first)
147
151
  if value.is_a? Config::Object
148
152
  value.get!(keys[1..-1].join('.'))
149
153
  else
@@ -15,6 +15,7 @@ module LeapCli; module Config
15
15
 
16
16
  # we can't use fetch() or get(), since those already have special meanings
17
17
  def retrieve(key, environment)
18
+ environment ||= 'default'
18
19
  self.fetch(environment, {})[key.to_s]
19
20
  end
20
21
 
@@ -31,6 +32,7 @@ module LeapCli; module Config
31
32
  end
32
33
 
33
34
  def set_with_block(key, environment, &block)
35
+ environment ||= 'default'
34
36
  key = key.to_s
35
37
  @discovered_keys[environment] ||= {}
36
38
  @discovered_keys[environment][key] = true
data/lib/leap_cli/log.rb CHANGED
@@ -80,7 +80,7 @@ module LeapCli
80
80
  if title
81
81
  prefix_options = case title
82
82
  when :error then ['error', :red, :bold]
83
- when :fatal_error then ['fatal error', :red, :bold]
83
+ when :fatal_error then ['fatal error:', :red, :bold]
84
84
  when :warning then ['warning:', :yellow, :bold]
85
85
  when :info then ['info', :cyan, :bold]
86
86
  when :updated then ['updated', :cyan, :bold]
@@ -36,7 +36,8 @@ module LeapCli; module Remote; module LeapPlugin
36
36
  rescue Capistrano::CommandError => exc
37
37
  LeapCli::Util.bail! do
38
38
  exc.hosts.each do |host|
39
- LeapCli::Util.log :error, "running deploy: node not initialized. Run 'leap node init #{host}'", :host => host
39
+ node = host.to_s.split('.').first
40
+ LeapCli::Util.log :error, "running deploy: node not initialized. Run 'leap node init #{node}'", :host => host
40
41
  end
41
42
  end
42
43
  end
@@ -184,7 +185,8 @@ module LeapCli; module Remote; module LeapPlugin
184
185
  private
185
186
 
186
187
  def progress(str='.')
187
- $stdout.print str; $stdout.flush;
188
+ print str
189
+ STDOUT.flush
188
190
  end
189
191
 
190
192
  #def mkdir(dir)
@@ -1,7 +1,7 @@
1
1
  module LeapCli
2
2
  unless defined?(LeapCli::VERSION)
3
- VERSION = '1.7.3'
4
- COMPATIBLE_PLATFORM_VERSION = '0.7'..'0.99'
3
+ VERSION = '1.7.4'
4
+ COMPATIBLE_PLATFORM_VERSION = '0.7.1'..'0.99'
5
5
  SUMMARY = 'Command line interface to the LEAP platform'
6
6
  DESCRIPTION = 'The command "leap" can be used to manage a bevy of servers running the LEAP platform from the comfort of your own home.'
7
7
  LOAD_PATHS = ['lib', 'vendor/certificate_authority/lib', 'vendor/rsync_command/lib']
data/lib/leap_cli.rb CHANGED
@@ -11,7 +11,7 @@ $:.unshift(File.expand_path('../leap_cli/override',__FILE__))
11
11
  # for a few gems, things will break if using earlier versions.
12
12
  # enforce the compatible versions here:
13
13
  require 'rubygems'
14
- gem 'net-ssh', '~> 2.7.0'
14
+ gem 'net-ssh', '~> 2.7'
15
15
  gem 'gli', '~> 2.12', '>= 2.12.0'
16
16
 
17
17
  require 'leap/platform'
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: leap_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.3
4
+ version: 1.7.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - LEAP Encryption Access Project
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-06-05 00:00:00.000000000 Z
11
+ date: 2015-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -92,28 +92,28 @@ dependencies:
92
92
  requirements:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
- version: 2.7.0
95
+ version: '2.7'
96
96
  type: :runtime
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - "~>"
101
101
  - !ruby/object:Gem::Version
102
- version: 2.7.0
102
+ version: '2.7'
103
103
  - !ruby/object:Gem::Dependency
104
104
  name: capistrano
105
105
  requirement: !ruby/object:Gem::Requirement
106
106
  requirements:
107
107
  - - "~>"
108
108
  - !ruby/object:Gem::Version
109
- version: 2.15.5
109
+ version: '2.15'
110
110
  type: :runtime
111
111
  prerelease: false
112
112
  version_requirements: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - "~>"
115
115
  - !ruby/object:Gem::Version
116
- version: 2.15.5
116
+ version: '2.15'
117
117
  - !ruby/object:Gem::Dependency
118
118
  name: ya2yaml
119
119
  requirement: !ruby/object:Gem::Requirement
@@ -160,6 +160,9 @@ dependencies:
160
160
  name: activemodel
161
161
  requirement: !ruby/object:Gem::Requirement
162
162
  requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '3.0'
163
166
  - - ">="
164
167
  - !ruby/object:Gem::Version
165
168
  version: 3.0.6
@@ -167,6 +170,9 @@ dependencies:
167
170
  prerelease: false
168
171
  version_requirements: !ruby/object:Gem::Requirement
169
172
  requirements:
173
+ - - "~>"
174
+ - !ruby/object:Gem::Version
175
+ version: '3.0'
170
176
  - - ">="
171
177
  - !ruby/object:Gem::Version
172
178
  version: 3.0.6