chef-zero 4.3.2 → 4.4.0

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: ac78b39c39c21f9ec236e084f3cced3e9fede287
4
- data.tar.gz: 1c0e8b64a786743b5be5911d96fe02bfdb1efb59
3
+ metadata.gz: ef05ae4a7aef9de3c3a3da1ec5fbd5143a1a0457
4
+ data.tar.gz: 01e350ee9b6ff42d42550e8bafa07e1de7d1d629
5
5
  SHA512:
6
- metadata.gz: e8957ed6a69e8e3f1f28a5fcbafc114f1013d4e2e35f83ba71cc4050780da5f829a69de6dac9c93a77b4594e3c48019f86e524b0de5555b0fbe1f7700748279f
7
- data.tar.gz: 295f5763b15e6167a106beffbf175436d2d89975a3a90b60e7a714ff3176b1d3a8c2d65bed01f758b2412e574e547fb4678d76988abf34cfd51a1c15572149e3
6
+ metadata.gz: ac2bff1a5f4518d05262d3660ad96709ec60f495706bd0700d4084cdc1a6e14cca17a458a69067bf5da9a14024c7222a17e0bb8de2a582205d9ebe0acbd560e6
7
+ data.tar.gz: 727e1693591ff8b7e7c03c3fd9b0e57868429fe9c1286b8b59cb65850d54b8d90cf54e6bc2a9c4353c1f1cff5081e7853400be21ef50ac1cd98b0bc2ff63a1cb
data/Gemfile ADDED
@@ -0,0 +1,14 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'rest-client', :github => 'chef/rest-client'
5
+
6
+ gem 'oc-chef-pedant', :github => 'chef/chef-server'
7
+
8
+ # gem 'oc-chef-pedant', :path => "../chef-server"
9
+
10
+ # bundler resolve failure on "rspec_junit_formatter"
11
+ # gem 'chef-pedant', :github => 'opscode/chef-pedant', :ref => "server-cli-option"
12
+
13
+ gem 'chef', :github => 'chef/chef'
14
+ # gem 'chef', :path => "../chef"
data/Rakefile CHANGED
@@ -15,6 +15,18 @@ task :pedant do
15
15
  require File.expand_path('spec/run_oc_pedant')
16
16
  end
17
17
 
18
+ desc "run pedant with CHEF_FS set"
19
+ task :cheffs do
20
+ ENV['CHEF_FS'] = "yes"
21
+ require File.expand_path('spec/run_oc_pedant')
22
+ end
23
+
24
+ desc "run pedant with FILE_STORE set"
25
+ task :filestore do
26
+ ENV['FILE_STORE'] = "yes"
27
+ require File.expand_path('spec/run_oc_pedant')
28
+ end
29
+
18
30
  desc "run oc pedant"
19
31
  task :oc_pedant do
20
32
  require File.expand_path('spec/run_oc_pedant')
data/bin/chef-zero CHANGED
@@ -55,6 +55,11 @@ OptionParser.new do |opts|
55
55
  options[:log_file] = value
56
56
  end
57
57
 
58
+ opts.on("--enterprise", "Whether to run in enterprise mode") do |value|
59
+ options[:single_org] = nil
60
+ options[:osc_compat] = false
61
+ end
62
+
58
63
  opts.on("--multi-org", "Whether to run in multi-org mode") do |value|
59
64
  options[:single_org] = nil
60
65
  end
data/chef-zero.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ $:.unshift(File.dirname(__FILE__) + '/lib')
2
+ require 'chef_zero/version'
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = 'chef-zero'
6
+ s.version = ChefZero::VERSION
7
+ s.platform = Gem::Platform::RUBY
8
+ s.summary = 'Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes'
9
+ s.description = s.summary
10
+ s.author = 'John Keiser'
11
+ s.email = 'jkeiser@opscode.com'
12
+ s.homepage = 'http://www.opscode.com'
13
+ s.license = 'Apache 2.0'
14
+
15
+ s.add_dependency 'mixlib-log', '~> 1.3'
16
+ s.add_dependency 'hashie', '>= 2.0', '< 4.0'
17
+ s.add_dependency 'uuidtools', '~> 2.1'
18
+ s.add_dependency 'ffi-yajl', '~> 2.2'
19
+ s.add_dependency 'rack'
20
+
21
+ s.add_development_dependency 'pry'
22
+ s.add_development_dependency 'pry-byebug'
23
+ s.add_development_dependency 'pry-stack_explorer'
24
+ s.add_development_dependency 'rake'
25
+ s.add_development_dependency 'rspec'
26
+ s.add_development_dependency 'github_changelog_generator'
27
+
28
+ s.bindir = 'bin'
29
+ s.executables = ['chef-zero']
30
+ s.require_path = 'lib'
31
+ s.files = %w(LICENSE README.md Gemfile Rakefile) + Dir.glob('*.gemspec') +
32
+ Dir.glob('{lib,spec}/**/*', File::FNM_DOTMATCH).reject {|f| File.directory?(f) }
33
+ end
@@ -79,7 +79,8 @@ module ChefZero
79
79
  data_bag_item
80
80
  end
81
81
 
82
- def self.normalize_cookbook(endpoint, org_prefix, cookbook, name, version, base_uri, method)
82
+ def self.normalize_cookbook(endpoint, org_prefix, cookbook, name, version, base_uri, method,
83
+ is_cookbook_artifact=false)
83
84
  # TODO I feel dirty
84
85
  if method != 'PUT'
85
86
  cookbook.each_pair do |key, value|
@@ -92,24 +93,28 @@ module ChefZero
92
93
  end
93
94
  end
94
95
  cookbook['name'] ||= "#{name}-#{version}"
95
- # TODO this feels wrong, but the real chef server doesn't expand this default
96
- # cookbook['version'] ||= version
97
- cookbook['cookbook_name'] ||= name
96
+ # TODO it feels wrong, but the real chef server doesn't expand 'version', so we don't either.
97
+
98
98
  cookbook['frozen?'] ||= false
99
99
  cookbook['metadata'] ||= {}
100
100
  cookbook['metadata']['version'] ||= version
101
- # Sad to not be expanding defaults just because Chef doesn't :(
102
- # cookbook['metadata']['name'] ||= name
103
- # cookbook['metadata']['description'] ||= "A fabulous new cookbook"
101
+
102
+ # defaults set by the client and not the Server:
103
+ # metadata[name, description, maintainer, maintainer_email, license]
104
+
104
105
  cookbook['metadata']['long_description'] ||= ""
105
- # cookbook['metadata']['maintainer'] ||= "YOUR_COMPANY_NAME"
106
- # cookbook['metadata']['maintainer_email'] ||= "YOUR_EMAIL"
107
- # cookbook['metadata']['license'] ||= "none"
108
106
  cookbook['metadata']['dependencies'] ||= {}
109
107
  cookbook['metadata']['attributes'] ||= {}
110
108
  cookbook['metadata']['recipes'] ||= {}
111
109
  end
112
- cookbook['json_class'] ||= 'Chef::CookbookVersion'
110
+
111
+ if is_cookbook_artifact
112
+ cookbook.delete('json_class')
113
+ else
114
+ cookbook['cookbook_name'] ||= name
115
+ cookbook['json_class'] ||= 'Chef::CookbookVersion'
116
+ end
117
+
113
118
  cookbook['chef_type'] ||= 'cookbook_version'
114
119
  if method == 'MIN'
115
120
  cookbook['metadata'].delete('attributes')
@@ -166,6 +171,20 @@ module ChefZero
166
171
  node
167
172
  end
168
173
 
174
+ def self.normalize_policy(policy, name, revision)
175
+ policy['name'] ||= name
176
+ policy['revision_id'] ||= revision
177
+ policy['run_list'] ||= []
178
+ policy['cookbook_locks'] ||= {}
179
+ policy
180
+ end
181
+
182
+ def self.normalize_policy_group(policy_group, name)
183
+ policy_group[name] ||= 'name'
184
+ policy_group['policies'] ||= {}
185
+ policy_group
186
+ end
187
+
169
188
  def self.normalize_organization(org, name)
170
189
  org['name'] ||= name
171
190
  org['full_name'] ||= name
@@ -155,6 +155,8 @@ module ChefZero
155
155
  'checksums' => {}
156
156
  },
157
157
  'nodes' => {},
158
+ 'policies' => {},
159
+ 'policy_groups' => {},
158
160
  'roles' => {},
159
161
  'sandboxes' => {},
160
162
  'users' => {},
@@ -378,11 +380,12 @@ module ChefZero
378
380
  # Non-default containers do not get superusers added to them,
379
381
  # because reasons.
380
382
  unless path.size == 4 && path[0] == 'organizations' && path[2] == 'containers' && !exists?(path)
381
- owners |= superusers
383
+ owners += superusers
382
384
  end
383
385
  end
384
386
 
385
- owners.uniq
387
+ # we don't de-dup this list, because pedant expects to see ["pivotal", "pivotal"] in some cases.
388
+ owners
386
389
  end
387
390
 
388
391
  def default_acl(acl_path, acl={})
@@ -0,0 +1,24 @@
1
+ require 'chef_zero/chef_data/data_normalizer'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ class CookbookArtifactsCookbookEndpoint < RestBase
6
+ # GET /organizations/ORG/cookbook_artifacts/COOKBOOK
7
+ def get(request)
8
+ cookbook_name = request.rest_path.last
9
+ cookbook_url = build_uri(request.base_uri, request.rest_path)
10
+ response_data = {}
11
+ versions = []
12
+
13
+ list_data(request).each do |identifier|
14
+ artifact_url = build_uri(request.base_uri, request.rest_path + [cookbook_name, identifier])
15
+ versions << { url: artifact_url, identifier: identifier }
16
+ end
17
+
18
+ response_data[cookbook_name] = { url: cookbook_url, versions: versions }
19
+
20
+ return json_response(200, response_data)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,67 @@
1
+ require 'chef_zero/chef_data/data_normalizer'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ class CookbookArtifactsCookbookIdentifierEndpoint < ChefZero::Endpoints::CookbookVersionEndpoint
6
+ # these endpoints are almost, but not quite, not entirely unlike the corresponding /cookbooks endpoints.
7
+ # it could all be refactored for maximum reuse, but they're short REST methods with well-defined
8
+ # behavioral specs (pedant), so there's not a huge benefit.
9
+
10
+ # GET /organizations/ORG/cookbook_artifacts/NAME/IDENTIFIER
11
+ def get(request)
12
+ cookbook_data = normalize(request, parse_json(get_data(request)))
13
+ return json_response(200, cookbook_data)
14
+ end
15
+
16
+ # PUT /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER
17
+ def put(request)
18
+ if exists_data?(request)
19
+ return error(409, "Cookbooks cannot be modified, and a cookbook with this identifier already exists.")
20
+ end
21
+
22
+ set_data(request, nil, request.body, :create_dir)
23
+
24
+ return already_json_response(201, request.body)
25
+ end
26
+
27
+ # DELETE /organizations/ORG/cookbook_artifacts/COOKBOOK/IDENTIFIER
28
+ def delete(request)
29
+ begin
30
+ doomed_cookbook_json = get_data(request)
31
+ identified_cookbook_data = normalize(request, parse_json(doomed_cookbook_json))
32
+ delete_data(request)
33
+
34
+ # go through the recipes and delete stuff in the file store.
35
+ hoover_unused_checksums(get_checksums(doomed_cookbook_json), request, 'cookbook_artifacts')
36
+
37
+ # if this was the last revision, delete the directory so future requests will 404, instead of
38
+ # returning 200 with an empty list.
39
+ artifact_path = request.rest_path[0..-2]
40
+ if list_data(request, artifact_path).size == 0
41
+ delete_data_dir(request, artifact_path)
42
+ end
43
+
44
+ json_response(200, identified_cookbook_data)
45
+ rescue RestErrorResponse => ex
46
+ if ex.response_code == 404
47
+ error(404, "not_found")
48
+ else
49
+ raise
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def make_file_store_path(rest_path, recipe)
57
+ rest_path.first(2) + ["file_store", "checksums", recipe["checksum"]]
58
+ end
59
+
60
+ def normalize(request, cookbook_artifact_data)
61
+ ChefData::DataNormalizer.normalize_cookbook(self, request.rest_path[0..1],
62
+ cookbook_artifact_data, request.rest_path[3], request.rest_path[4],
63
+ request.base_uri, request.method, true)
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,34 @@
1
+ require 'chef_zero/chef_data/data_normalizer'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ class CookbookArtifactsEndpoint < RestBase
6
+ # GET /organizations/ORG/cookbook_artifacts
7
+ def get(request)
8
+ data = {}
9
+
10
+ artifacts = begin
11
+ list_data(request)
12
+ rescue Exception => e
13
+ if e.response_code == 404
14
+ return already_json_response(200, "{}")
15
+ end
16
+ end
17
+
18
+ artifacts.each do |cookbook_artifact|
19
+ cookbook_url = build_uri(request.base_uri, request.rest_path + [cookbook_artifact])
20
+
21
+ versions = []
22
+ list_data(request, request.rest_path + [cookbook_artifact]).each do |identifier|
23
+ artifact_url = build_uri(request.base_uri, request.rest_path + [cookbook_artifact, identifier])
24
+ versions << { url: artifact_url, identifier: identifier }
25
+ end
26
+
27
+ data[cookbook_artifact] = { url: cookbook_url, versions: versions }
28
+ end
29
+
30
+ return json_response(200, data)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -85,10 +85,10 @@ module ChefZero
85
85
 
86
86
  private
87
87
 
88
- def hoover_unused_checksums(deleted_checksums, request)
89
- data_store.list(request.rest_path[0..1] + ['cookbooks']).each do |cookbook_name|
90
- data_store.list(request.rest_path[0..1] + ['cookbooks', cookbook_name]).each do |version|
91
- cookbook = data_store.get(request.rest_path[0..1] + ['cookbooks', cookbook_name, version], request)
88
+ def hoover_unused_checksums(deleted_checksums, request, data_type='cookbooks')
89
+ data_store.list(request.rest_path[0..1] + [data_type]).each do |cookbook_name|
90
+ data_store.list(request.rest_path[0..1] + [data_type, cookbook_name]).each do |version|
91
+ cookbook = data_store.get(request.rest_path[0..1] + [data_type, cookbook_name, version], request)
92
92
  deleted_checksums = deleted_checksums - get_checksums(cookbook)
93
93
  end
94
94
  end
@@ -0,0 +1,31 @@
1
+
2
+ # pedant makes a couple of Solr-related calls from its search_utils.rb file that we can't work around (e.g.
3
+ # with monkeypatching). the necessary Pedant::Config values are set in run_oc_pedant.rb. --cdoherty
4
+ module ChefZero
5
+ module Endpoints
6
+ class DummyEndpoint < RestBase
7
+ # called by #direct_solr_query, once each for roles, nodes, and data bag items. each RSpec example makes
8
+ # 3 calls, with the expected sequence of return values [0, 1, 0].
9
+ def get(request)
10
+
11
+ # this could be made less brittle, but if things change to have more than 3 cycles, we should really
12
+ # be notified by a spec failure.
13
+ @mock_values ||= ([0, 1, 0] * 3).map { |val| make_response(val) }
14
+
15
+ retval = @mock_values.shift
16
+ json_response(200, retval)
17
+ end
18
+
19
+ # called by #force_solr_commit in pedant's , which doesn't check the return value.
20
+ def post(request)
21
+ # sure thing!
22
+ json_response(200, { message: "This dummy POST endpoint didn't do anything." })
23
+ end
24
+
25
+ def make_response(value)
26
+ { "response" => { "numFound" => value } }
27
+ end
28
+ end
29
+ end
30
+ end
31
+
@@ -6,6 +6,20 @@ module ChefZero
6
6
  module Endpoints
7
7
  # /nodes/ID
8
8
  class NodeEndpoint < RestObjectEndpoint
9
+ def put(request)
10
+ data = parse_json(request.body)
11
+
12
+ if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"])
13
+ return error(400, "Field 'policy_name' invalid", :pretty => false)
14
+ end
15
+
16
+ if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"])
17
+ return error(400, "Field 'policy_group' invalid", :pretty => false)
18
+ end
19
+
20
+ super(request)
21
+ end
22
+
9
23
  def populate_defaults(request, response_json)
10
24
  node = FFI_Yajl::Parser.parse(response_json, :create_additions => false)
11
25
  node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3])
@@ -0,0 +1,35 @@
1
+ require 'ffi_yajl'
2
+ require 'chef_zero/endpoints/rest_object_endpoint'
3
+ require 'chef_zero/chef_data/data_normalizer'
4
+
5
+ module ChefZero
6
+ module Endpoints
7
+ # /nodes
8
+ class NodesEndpoint < RestListEndpoint
9
+
10
+ def post(request)
11
+ # /nodes validation
12
+ if request.rest_path.last == "nodes"
13
+ data = parse_json(request.body)
14
+
15
+ if data.has_key?("policy_name") && policy_name_invalid?(data["policy_name"])
16
+ return error(400, "Field 'policy_name' invalid", :pretty => false)
17
+ end
18
+
19
+ if data.has_key?("policy_group") && policy_name_invalid?(data["policy_group"])
20
+ return error(400, "Field 'policy_group' invalid", :pretty => false)
21
+ end
22
+ end
23
+
24
+ super(request)
25
+ end
26
+
27
+ def populate_defaults(request, response_json)
28
+ node = FFI_Yajl::Parser.parse(response_json, :create_additions => false)
29
+ node = ChefData::DataNormalizer.normalize_node(node, request.rest_path[3])
30
+ FFI_Yajl::Encoder.encode(node, :pretty => true)
31
+ end
32
+ end
33
+ end
34
+ end
35
+
@@ -1,151 +1,26 @@
1
- require 'ffi_yajl'
2
-
3
- require 'chef_zero/endpoints/rest_object_endpoint'
4
1
  require 'chef_zero/chef_data/data_normalizer'
5
2
 
6
3
  module ChefZero
7
4
  module Endpoints
8
- # /policies/:group/:name
9
- class PoliciesEndpoint < RestObjectEndpoint
10
- def initialize(server)
11
- super(server, 'id')
12
- end
13
-
5
+ # /organizations/ORG/policies
6
+ class PoliciesEndpoint < RestBase
7
+ # GET /organizations/ORG/policies
14
8
  def get(request)
15
- already_json_response(200, get_data(request))
16
- end
17
-
18
- # Right now we're allowing PUT to create.
19
- def put(request)
20
- error = validate(request)
21
- return error if error
22
-
23
- code =
24
- if data_store.exists?(request.rest_path)
25
- set_data(request, request.rest_path, request.body, :data_store_exceptions)
26
- 200
27
- else
28
- name = request.rest_path[4]
29
- data_store.create(request.rest_path[0..3], name, request.body, :create_dir)
30
- 201
31
- end
32
- already_json_response(code, request.body)
33
- end
34
-
35
- def delete(request)
36
- result = get_data(request, request.rest_path)
37
- delete_data(request, request.rest_path, :data_store_exceptions)
38
- already_json_response(200, result)
39
- end
40
-
41
- private
42
-
43
- def validate(request)
44
- req_object = validate_json(request.body)
45
- validate_revision_id(request, req_object) ||
46
- validate_name(request, req_object) ||
47
- validate_run_list(req_object) ||
48
- validate_each_run_list_item(req_object) ||
49
- validate_cookbook_locks_collection(req_object) ||
50
- validate_each_cookbook_locks_item(req_object)
51
- end
52
-
53
- def validate_json(request_body)
54
- FFI_Yajl::Parser.parse(request_body)
55
- # TODO: rescue parse error, return 400
56
- # error(400, "Must specify #{identity_keys.map { |k| k.inspect }.join(' or ')} in JSON")
57
- end
58
-
59
- def validate_revision_id(request, req_object)
60
- if !req_object.key?("revision_id")
61
- error(400, "Field 'revision_id' missing")
62
- elsif req_object["revision_id"].empty?
63
- error(400, "Field 'revision_id' invalid")
64
- elsif req_object["revision_id"].size > 255
65
- error(400, "Field 'revision_id' invalid")
66
- elsif req_object["revision_id"] !~ /^[\-[:alnum:]_\.\:]+$/
67
- error(400, "Field 'revision_id' invalid")
68
- end
69
- end
70
-
71
- def validate_name(request, req_object)
72
- if !req_object.key?("name")
73
- error(400, "Field 'name' missing")
74
- elsif req_object["name"] != (uri_policy_name = URI.decode(request.rest_path[4]))
75
- error(400, "Field 'name' invalid : #{uri_policy_name} does not match #{req_object["name"]}")
76
- elsif req_object["name"].size > 255
77
- error(400, "Field 'name' invalid")
78
- elsif req_object["name"] !~ /^[\-[:alnum:]_\.\:]+$/
79
- error(400, "Field 'name' invalid")
80
- end
81
- end
82
-
83
- def validate_run_list(req_object)
84
- if !req_object.key?("run_list")
85
- error(400, "Field 'run_list' missing")
86
- elsif !req_object["run_list"].kind_of?(Array)
87
- error(400, "Field 'run_list' is not a valid run list")
88
- end
89
- end
90
-
91
- def validate_each_run_list_item(req_object)
92
- req_object["run_list"].each do |run_list_item|
93
- if res_400 = validate_run_list_item(run_list_item)
94
- return res_400
95
- end
96
- end
97
- nil
98
- end
9
+ response_data = {}
10
+ policy_names = list_data(request)
11
+ policy_names.each do |policy_name|
12
+ policy_path = request.rest_path + [policy_name]
13
+ policy_uri = build_uri(request.base_uri, policy_path)
14
+ revisions = list_data(request, policy_path + ["revisions"])
99
15
 
100
- def validate_run_list_item(run_list_item)
101
- if !run_list_item.kind_of?(String)
102
- error(400, "Field 'run_list' is not a valid run list")
103
- elsif run_list_item !~ /\Arecipe\[[^\s]+::[^\s]+\]\Z/
104
- error(400, "Field 'run_list' is not a valid run list")
16
+ response_data[policy_name] = {
17
+ uri: policy_uri,
18
+ revisions: hashify_list(revisions)
19
+ }
105
20
  end
106
- end
107
21
 
108
- def validate_cookbook_locks_collection(req_object)
109
- if !req_object.key?("cookbook_locks")
110
- error(400, "Field 'cookbook_locks' missing")
111
- elsif !req_object["cookbook_locks"].kind_of?(Hash)
112
- error(400, "Field 'cookbook_locks' invalid")
113
- end
22
+ return json_response(200, response_data)
114
23
  end
115
-
116
- def validate_each_cookbook_locks_item(req_object)
117
- req_object["cookbook_locks"].each do |cookbook_name, lock|
118
- if res_400 = validate_cookbook_locks_item(cookbook_name, lock)
119
- return res_400
120
- end
121
- end
122
- nil
123
- end
124
-
125
- def validate_cookbook_locks_item(cookbook_name, lock)
126
- if !lock.kind_of?(Hash)
127
- error(400, "cookbook_lock entries must be a JSON object")
128
- elsif !lock.key?("identifier")
129
- error(400, "Field 'identifier' missing")
130
- elsif lock["identifier"].size > 255
131
- error(400, "Field 'identifier' invalid")
132
- elsif !lock.key?("version")
133
- error(400, "Field 'version' missing")
134
- elsif lock.key?("dotted_decimal_identifier")
135
- unless valid_version?(lock["dotted_decimal_identifier"])
136
- error(400, "Field 'dotted_decimal_identifier' is not a valid version")
137
- end
138
- end
139
- end
140
-
141
- def valid_version?(version_string)
142
- Gem::Version.new(version_string)
143
- true
144
- rescue ArgumentError
145
- false
146
- end
147
-
148
24
  end
149
25
  end
150
26
  end
151
-