chef-zero 4.0 → 4.1.0

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: 9303597115a96be3764f56348da326bd663b4950
4
- data.tar.gz: 0d6db19f14312ea7cad130819512153ec56eb9b8
3
+ metadata.gz: 960c8c81923f89a55fd64b0de21517f5c2fde8e5
4
+ data.tar.gz: ace9ada169514cdf654d7f359131c2103367da2d
5
5
  SHA512:
6
- metadata.gz: 9591446989226c5792ffe7c7bb9e0b3489b2601d1829d1dfdce7f6081fd3c8a11f68524ef7c59775ff35193ba82f3d13655c65360b95aa4b3326831acac520cf
7
- data.tar.gz: 878fcd44f84e77c8ba2906336d6a9f1a101cbdf0b9476670baf2be8f7bef3c6ca7effc886119ffc936995680a197ec4c384dd1dad4c87c77dbf81d56c1fb7122
6
+ metadata.gz: 07af5a8a644ac4476ae551d4512e51ca5ba3a069bce3dae3afcbbf4299c9ef8b593113acbe15699da3fa7a991a7c0689606099642ff7c3773386bd279a024f86
7
+ data.tar.gz: 92f6a384a72be04439eeb29f1046217a835b26741336dfad4a6d3dd6e1cb88e8860f234d55d2e2daca36a6a822f2792d89d4a267666412d9ce18fff17ee53a16
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- [![Stories in Ready](https://badge.waffle.io/opscode/chef-zero.png?label=ready&title=Ready)](https://waffle.io/opscode/chef-zero)[![Stories in Progress](https://badge.waffle.io/opscode/chef-zero.png?label=in+progress&title=In+Progress)](https://waffle.io/opscode/chef-zero)
1
+ [![Stories in Ready](https://badge.waffle.io/chef/chef-zero.png?label=ready&title=Ready)](https://waffle.io/chef/chef-zero)[![Stories in Progress](https://badge.waffle.io/chef/chef-zero.png?label=in+progress&title=In+Progress)](https://waffle.io/chef/chef-zero)
2
2
 
3
3
  Chef Zero
4
4
  =========
@@ -1,4 +1,8 @@
1
1
  require 'ffi_yajl'
2
+
3
+ require 'chef/version_class'
4
+ require 'chef/exceptions'
5
+
2
6
  require 'chef_zero/endpoints/rest_object_endpoint'
3
7
  require 'chef_zero/chef_data/data_normalizer'
4
8
 
@@ -41,7 +45,8 @@ module ChefZero
41
45
 
42
46
  def validate(request)
43
47
  req_object = validate_json(request.body)
44
- validate_name(request, req_object) ||
48
+ validate_revision_id(request, req_object) ||
49
+ validate_name(request, req_object) ||
45
50
  validate_run_list(req_object) ||
46
51
  validate_each_run_list_item(req_object) ||
47
52
  validate_cookbook_locks_collection(req_object) ||
@@ -54,23 +59,35 @@ module ChefZero
54
59
  # error(400, "Must specify #{identity_keys.map { |k| k.inspect }.join(' or ')} in JSON")
55
60
  end
56
61
 
62
+ def validate_revision_id(request, req_object)
63
+ if !req_object.key?("revision_id")
64
+ error(400, "Field 'revision_id' missing")
65
+ elsif req_object["revision_id"].empty?
66
+ error(400, "Field 'revision_id' invalid")
67
+ elsif req_object["revision_id"].size > 255
68
+ error(400, "Field 'revision_id' invalid")
69
+ elsif req_object["revision_id"] !~ /^[\-[:alnum:]_\.\:]+$/
70
+ error(400, "Field 'revision_id' invalid")
71
+ end
72
+ end
73
+
57
74
  def validate_name(request, req_object)
58
75
  if !req_object.key?("name")
59
- error(400, "Must specify 'name' in JSON")
60
- elsif req_object["name"] != URI.decode(request.rest_path[4])
61
- error(400, "'name' field in JSON must match the policy name in the URL")
76
+ error(400, "Field 'name' missing")
77
+ elsif req_object["name"] != (uri_policy_name = URI.decode(request.rest_path[4]))
78
+ error(400, "Field 'name' invalid : #{uri_policy_name} does not match #{req_object["name"]}")
62
79
  elsif req_object["name"].size > 255
63
- error(400, "'name' field in JSON must be 255 characters or fewer")
80
+ error(400, "Field 'name' invalid")
64
81
  elsif req_object["name"] !~ /^[\-[:alnum:]_\.\:]+$/
65
- error(400, "'name' field in JSON must be contain only alphanumeric, hypen, underscore, and dot characters")
82
+ error(400, "Field 'name' invalid")
66
83
  end
67
84
  end
68
85
 
69
86
  def validate_run_list(req_object)
70
87
  if !req_object.key?("run_list")
71
- error(400, "Must specify 'run_list' in JSON")
88
+ error(400, "Field 'run_list' missing")
72
89
  elsif !req_object["run_list"].kind_of?(Array)
73
- error(400, "'run_list' must be an Array of run list items")
90
+ error(400, "Field 'run_list' is not a valid run list")
74
91
  end
75
92
  end
76
93
 
@@ -85,17 +102,17 @@ module ChefZero
85
102
 
86
103
  def validate_run_list_item(run_list_item)
87
104
  if !run_list_item.kind_of?(String)
88
- error(400, "Items in run_list must be strings in fully qualified recipe format, like recipe[cookbook::recipe]")
105
+ error(400, "Field 'run_list' is not a valid run list")
89
106
  elsif run_list_item !~ /\Arecipe\[[^\s]+::[^\s]+\]\Z/
90
- error(400, "Items in run_list must be strings in fully qualified recipe format, like recipe[cookbook::recipe]")
107
+ error(400, "Field 'run_list' is not a valid run list")
91
108
  end
92
109
  end
93
110
 
94
111
  def validate_cookbook_locks_collection(req_object)
95
112
  if !req_object.key?("cookbook_locks")
96
- error(400, "Must specify 'cookbook_locks' in JSON")
113
+ error(400, "Field 'cookbook_locks' missing")
97
114
  elsif !req_object["cookbook_locks"].kind_of?(Hash)
98
- error(400, "'cookbook_locks' must be a JSON object of cookbook_name: lock_data pairs")
115
+ error(400, "Field 'cookbook_locks' invalid")
99
116
  end
100
117
  end
101
118
 
@@ -112,14 +129,25 @@ module ChefZero
112
129
  if !lock.kind_of?(Hash)
113
130
  error(400, "cookbook_lock entries must be a JSON object")
114
131
  elsif !lock.key?("identifier")
115
- error(400, "cookbook_lock entries must contain an 'identifier' field")
116
- elsif !lock.key?("dotted_decimal_identifier")
117
- error(400, "cookbook_lock entries must contain an 'dotted_decimal_identifier' field")
132
+ error(400, "Field 'identifier' missing")
118
133
  elsif lock["identifier"].size > 255
119
- error(400, "cookbook_lock entries 'identifier' field must be 255 or fewer characters")
134
+ error(400, "Field 'identifier' invalid")
135
+ elsif !lock.key?("version")
136
+ error(400, "Field 'version' missing")
137
+ elsif lock.key?("dotted_decimal_identifier")
138
+ unless valid_version?(lock["dotted_decimal_identifier"])
139
+ error(400, "Field 'dotted_decimal_identifier' is not a valid version")
140
+ end
120
141
  end
121
142
  end
122
143
 
144
+ def valid_version?(version_string)
145
+ Chef::Version.new(version_string)
146
+ true
147
+ rescue Chef::Exceptions::InvalidCookbookVersion
148
+ false
149
+ end
150
+
123
151
  end
124
152
  end
125
153
  end
@@ -0,0 +1,12 @@
1
+ require 'chef_zero/rest_base'
2
+
3
+ module ChefZero
4
+ module Endpoints
5
+ # /version
6
+ class VersionEndpoint < RestBase
7
+ def get(request)
8
+ text_response(200, "chef-zero #{ChefZero::VERSION}\n")
9
+ end
10
+ end
11
+ end
12
+ end
@@ -181,6 +181,10 @@ module ChefZero
181
181
  already_json_response(response_code, FFI_Yajl::Encoder.encode(json, :pretty => true))
182
182
  end
183
183
 
184
+ def text_response(response_code, text)
185
+ [response_code, {"Content-Type" => "text/plain"}, text]
186
+ end
187
+
184
188
  def already_json_response(response_code, json_text)
185
189
  [response_code, {"Content-Type" => "application/json"}, json_text]
186
190
  end
@@ -191,8 +195,9 @@ module ChefZero
191
195
  # Strip off /organizations/chef if we are in single org mode
192
196
  if rest_path[0..1] != [ 'organizations', server.options[:single_org] ]
193
197
  raise "Unexpected URL #{rest_path[0..1]} passed to build_uri in single org mode"
198
+ else
199
+ "#{base_uri}/#{rest_path[2..-1].join('/')}"
194
200
  end
195
- "#{base_uri}/#{rest_path[2..-1].join('/')}"
196
201
  else
197
202
  "#{base_uri}/#{rest_path.join('/')}"
198
203
  end
@@ -27,6 +27,7 @@ require 'webrick'
27
27
  require 'webrick/https'
28
28
 
29
29
  require 'chef_zero'
30
+ require 'chef_zero/socketless_server_map'
30
31
  require 'chef_zero/chef_data/cookbook_data'
31
32
  require 'chef_zero/chef_data/acl_path'
32
33
  require 'chef_zero/rest_router'
@@ -83,9 +84,11 @@ require 'chef_zero/endpoints/user_association_request_endpoint'
83
84
  require 'chef_zero/endpoints/user_organizations_endpoint'
84
85
  require 'chef_zero/endpoints/file_store_file_endpoint'
85
86
  require 'chef_zero/endpoints/not_found_endpoint'
87
+ require 'chef_zero/endpoints/version_endpoint'
86
88
 
87
89
  module ChefZero
88
90
  class Server
91
+
89
92
  DEFAULT_OPTIONS = {
90
93
  :host => '127.0.0.1',
91
94
  :port => 8889,
@@ -95,6 +98,11 @@ module ChefZero
95
98
  :ssl => false
96
99
  }.freeze
97
100
 
101
+ GLOBAL_ENDPOINTS = [
102
+ '/license',
103
+ '/version',
104
+ ]
105
+
98
106
  def initialize(options = {})
99
107
  @options = DEFAULT_OPTIONS.merge(options)
100
108
  if @options[:single_org] && !@options.has_key?(:osc_compat)
@@ -102,6 +110,7 @@ module ChefZero
102
110
  end
103
111
  @options.freeze
104
112
  ChefZero::Log.level = @options[:log_level].to_sym
113
+ @app = nil
105
114
  end
106
115
 
107
116
  # @return [Hash]
@@ -140,6 +149,12 @@ module ChefZero
140
149
  end
141
150
  end
142
151
 
152
+ def local_mode_url
153
+ raise "Port not yet set, cannot generate URL" unless port.kind_of?(Integer)
154
+ "chefzero://localhost:#{port}"
155
+ end
156
+
157
+
143
158
  #
144
159
  # The data store for this server (default is in-memory).
145
160
  #
@@ -218,7 +233,6 @@ module ChefZero
218
233
  thread.join
219
234
  end
220
235
 
221
-
222
236
  #
223
237
  # Start a Chef Zero server in a forked process. This method returns the PID
224
238
  # to the forked process.
@@ -278,9 +292,19 @@ module ChefZero
278
292
  sleep(0.01)
279
293
  end
280
294
 
295
+ SocketlessServerMap.instance.register_port(@port, self)
296
+
281
297
  @thread
282
298
  end
283
299
 
300
+ def start_socketless
301
+ @port = SocketlessServerMap.instance.register_no_listen_server(self)
302
+ end
303
+
304
+ def handle_socketless_request(request_env)
305
+ app.call(request_env)
306
+ end
307
+
284
308
  #
285
309
  # Boolean method to determine if the server is currently ready to accept
286
310
  # requests. This method will attempt to make an HTTP request against the
@@ -309,6 +333,7 @@ module ChefZero
309
333
  if @thread
310
334
  ChefZero::Log.error("Chef Zero did not stop within #{wait} seconds! Killing...")
311
335
  @thread.kill
336
+ SocketlessServerMap.deregister(port)
312
337
  end
313
338
  ensure
314
339
  @server = nil
@@ -495,8 +520,7 @@ module ChefZero
495
520
  [ "/organizations/*/*/*/_acl/*", AclEndpoint.new(self) ]
496
521
  ]
497
522
  end
498
- result +
499
- [
523
+ result + [
500
524
  # Both
501
525
  [ "/organizations/*/clients", ActorsEndpoint.new(self) ],
502
526
  [ "/organizations/*/clients/*", ActorEndpoint.new(self) ],
@@ -526,13 +550,21 @@ module ChefZero
526
550
  [ "/organizations/*/sandboxes/*", SandboxEndpoint.new(self) ],
527
551
  [ "/organizations/*/search", SearchesEndpoint.new(self) ],
528
552
  [ "/organizations/*/search/*", SearchEndpoint.new(self) ],
553
+ [ "/version", VersionEndpoint.new(self) ],
529
554
 
530
555
  # Internal
531
556
  [ "/organizations/*/file_store/**", FileStoreFileEndpoint.new(self) ]
532
557
  ]
533
558
  end
534
559
 
560
+ def global_endpoint?(ep)
561
+ GLOBAL_ENDPOINTS.any? do |g_ep|
562
+ ep.start_with?(g_ep)
563
+ end
564
+ end
565
+
535
566
  def app
567
+ return @app if @app
536
568
  router = RestRouter.new(open_source_endpoints)
537
569
  router.not_found = NotFoundEndpoint.new
538
570
 
@@ -541,9 +573,11 @@ module ChefZero
541
573
  else
542
574
  rest_base_prefix = []
543
575
  end
544
- return proc do |env|
576
+ @app = proc do |env|
545
577
  begin
546
- request = RestRequest.new(env, rest_base_prefix)
578
+ prefix = global_endpoint?(env['PATH_INFO']) ? [] : rest_base_prefix
579
+
580
+ request = RestRequest.new(env, prefix)
547
581
  if @on_request_proc
548
582
  @on_request_proc.call(request)
549
583
  end
@@ -577,6 +611,7 @@ module ChefZero
577
611
  end
578
612
  end
579
613
  end
614
+ @app
580
615
  end
581
616
 
582
617
  def dejsonize_children(hash)
@@ -0,0 +1,84 @@
1
+ #
2
+ # Author:: Daniel DeLeo (<dan@chef.io>)
3
+ # Copyright:: Copyright (c) 2012 Opscode, Inc.
4
+ # License:: Apache License, Version 2.0
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+
19
+ require 'thread'
20
+ require 'singleton'
21
+
22
+ module ChefZero
23
+
24
+ class ServerNotFound < StandardError
25
+ end
26
+
27
+ class NoSocketlessPortAvailable < StandardError
28
+ end
29
+
30
+ class SocketlessServerMap
31
+
32
+ def self.request(port, request_env)
33
+ instance.request(port, request_env)
34
+ end
35
+
36
+ MUTEX = Mutex.new
37
+
38
+ include Singleton
39
+
40
+ def initialize()
41
+ reset!
42
+ end
43
+
44
+ def reset!
45
+ @servers_by_port = {}
46
+ end
47
+
48
+ def register_port(port, server)
49
+ MUTEX.synchronize do
50
+ @servers_by_port[port] = server
51
+ end
52
+ end
53
+
54
+ def register_no_listen_server(server)
55
+ MUTEX.synchronize do
56
+ 1.upto(1000) do |port|
57
+ unless @servers_by_port.key?(port)
58
+ @servers_by_port[port] = server
59
+ return port
60
+ end
61
+ end
62
+ raise NoSocketlessPortAvailable, "No socketless ports left to register"
63
+ end
64
+ end
65
+
66
+ def has_server_on_port?(port)
67
+ @servers_by_port.key?(port)
68
+ end
69
+
70
+ def deregister(port)
71
+ MUTEX.synchronize do
72
+ @servers_by_port.delete(port)
73
+ end
74
+ end
75
+
76
+ def request(port, request_env)
77
+ server = @servers_by_port[port]
78
+ raise ServerNotFound, "No socketless chef-zero server on given port #{port.inspect}" unless server
79
+ server.handle_socketless_request(request_env)
80
+ end
81
+
82
+ end
83
+ end
84
+
@@ -1,3 +1,3 @@
1
1
  module ChefZero
2
- VERSION = '4.0'
2
+ VERSION = '4.1.0'
3
3
  end
data/spec/run_pedant.rb CHANGED
@@ -79,7 +79,8 @@ begin
79
79
  '--skip-authentication',
80
80
  '--skip-authorization',
81
81
  '--skip-keys',
82
- '--skip-omnibus'
82
+ '--skip-omnibus',
83
+ '--skip-cookbook_artifacts'
83
84
  ])
84
85
 
85
86
  result = RSpec::Core::Runner.run(Pedant.config.rspec_args)
data/spec/server_spec.rb CHANGED
@@ -33,6 +33,12 @@ describe ChefZero::Server do
33
33
  httpcall.get('/nodes', 'Accept' => accepts)
34
34
  end
35
35
 
36
+ def get_version
37
+ uri = URI(@server.url)
38
+ httpcall = Net::HTTP.new(uri.host, uri.port)
39
+ httpcall.get('/version', 'Accept' => 'text/plain, application/json')
40
+ end
41
+
36
42
  it 'accepts requests with no accept header' do
37
43
  request = Net::HTTP::Get.new('/nodes')
38
44
  request.delete('Accept')
@@ -77,6 +83,9 @@ describe ChefZero::Server do
77
83
  expect(get_nodes('a/b;a=b;c=d, application/json;a=b, application/xml;a=b').code).to eq '200'
78
84
  end
79
85
 
86
+ it 'accepts /version' do
87
+ expect(get_version.body.start_with?('chef-zero')).to be true
88
+ end
80
89
  end
81
90
  end
82
91
  end
@@ -0,0 +1,71 @@
1
+ require 'chef_zero/socketless_server_map'
2
+
3
+
4
+ describe "Socketless Mode" do
5
+
6
+ let(:server_map) { ChefZero::SocketlessServerMap.instance.tap { |i| i.reset! } }
7
+
8
+ let(:server) { instance_double("ChefZero::Server") }
9
+
10
+ let(:second_server) { instance_double("ChefZero::Server") }
11
+
12
+ it "registers a socketful server" do
13
+ server_map.register_port(8889, server)
14
+ expect(server_map).to have_server_on_port(8889)
15
+ end
16
+
17
+ context "when a no-listen server is registered" do
18
+
19
+ let!(:port) { server_map.register_no_listen_server(server) }
20
+
21
+ it "assigns the server a low port number" do
22
+ expect(port).to eq(1)
23
+ end
24
+
25
+ context "and another server is registered" do
26
+
27
+ let!(:next_port) { server_map.register_no_listen_server(second_server) }
28
+
29
+ it "assigns another port when another server is registered" do
30
+ expect(next_port).to eq(2)
31
+ end
32
+
33
+ it "raises NoSocketlessPortAvailable when too many servers are registered" do
34
+ expect { 1000.times { server_map.register_no_listen_server(server) } }.to raise_error(ChefZero::NoSocketlessPortAvailable)
35
+ end
36
+
37
+ it "deregisters a server" do
38
+ expect(server_map).to have_server_on_port(1)
39
+ server_map.deregister(1)
40
+ expect(server_map).to_not have_server_on_port(1)
41
+ end
42
+
43
+ describe "routing requests to a server" do
44
+
45
+ let(:rack_req) do
46
+ r = {}
47
+ r["REQUEST_METHOD"] = "GET"
48
+ r["SCRIPT_NAME"] = ""
49
+ r["PATH_INFO"] = "/clients"
50
+ r["QUERY_STRING"] = ""
51
+ r["rack.input"] = StringIO.new("")
52
+ r
53
+ end
54
+
55
+ let(:rack_response) { [200, {}, ["this is the response body"] ] }
56
+
57
+ it "routes a request to the registered port" do
58
+ expect(server).to receive(:handle_socketless_request).with(rack_req).and_return(rack_response)
59
+ response = server_map.request(1, rack_req)
60
+ expect(response).to eq(rack_response)
61
+ end
62
+
63
+ it "raises ServerNotFound when a request is sent to an unregistered port" do
64
+ expect { server_map.request(99, rack_req) }.to raise_error(ChefZero::ServerNotFound)
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+
71
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chef-zero
3
3
  version: !ruby/object:Gem::Version
4
- version: '4.0'
4
+ version: 4.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Keiser
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-02-11 00:00:00.000000000 Z
11
+ date: 2015-04-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: mixlib-log
@@ -186,6 +186,7 @@ files:
186
186
  - lib/chef_zero/endpoints/user_association_requests_count_endpoint.rb
187
187
  - lib/chef_zero/endpoints/user_association_requests_endpoint.rb
188
188
  - lib/chef_zero/endpoints/user_organizations_endpoint.rb
189
+ - lib/chef_zero/endpoints/version_endpoint.rb
189
190
  - lib/chef_zero/log.rb
190
191
  - lib/chef_zero/rest_base.rb
191
192
  - lib/chef_zero/rest_error_response.rb
@@ -193,6 +194,7 @@ files:
193
194
  - lib/chef_zero/rest_router.rb
194
195
  - lib/chef_zero/rspec.rb
195
196
  - lib/chef_zero/server.rb
197
+ - lib/chef_zero/socketless_server_map.rb
196
198
  - lib/chef_zero/solr/query/binary_operator.rb
197
199
  - lib/chef_zero/solr/query/phrase.rb
198
200
  - lib/chef_zero/solr/query/range_query.rb
@@ -207,6 +209,7 @@ files:
207
209
  - spec/run_pedant.rb
208
210
  - spec/search_spec.rb
209
211
  - spec/server_spec.rb
212
+ - spec/socketless_server_map_spec.rb
210
213
  - spec/support/oc_pedant.rb
211
214
  - spec/support/pedant.rb
212
215
  - spec/support/stickywicket.pem
@@ -236,4 +239,3 @@ specification_version: 4
236
239
  summary: Self-contained, easy-setup, fast-start in-memory Chef server for testing
237
240
  and solo setup purposes
238
241
  test_files: []
239
- has_rdoc: