leap_cli 1.7.3 → 1.7.4

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 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