chef-zero 4.0 → 4.1.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: 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: