summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordanielsdeleo <dan@getchef.com>2015-03-31 12:19:05 -0700
committerdanielsdeleo <dan@getchef.com>2015-04-01 13:35:01 -0700
commitaab0ccb5d41913e050707bd6b40e5b820649c566 (patch)
tree0eb3839f664afb6f5eade15e32f85313cf41defa
parent8b42ac0374fb075fbcb21df73742e81e69d9bf6f (diff)
downloadchef-aab0ccb5d41913e050707bd6b40e5b820649c566.tar.gz
Extract socketless client and add specs
-rw-r--r--lib/chef/http/socketless_chef_zero_client.rb205
-rw-r--r--lib/chef/rest.rb145
-rw-r--r--spec/unit/http/socketless_chef_zero_client_spec.rb174
3 files changed, 379 insertions, 145 deletions
diff --git a/lib/chef/http/socketless_chef_zero_client.rb b/lib/chef/http/socketless_chef_zero_client.rb
new file mode 100644
index 0000000000..a64bfc8d4d
--- /dev/null
+++ b/lib/chef/http/socketless_chef_zero_client.rb
@@ -0,0 +1,205 @@
+#--
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+# ---
+# Some portions of the code in this file are verbatim copies of code from the
+# fakeweb project: https://github.com/chrisk/fakeweb
+#
+# fakeweb is distributed under the MIT license, which is copied below:
+# ---
+#
+# Copyright 2006-2010 Blaine Cook, Chris Kampmeier, and other contributors
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+require 'chef_zero/server'
+
+class Chef
+ class HTTP
+
+ # HTTP Client class that talks directly to Zero via the Rack interface.
+ class SocketlessChefZeroClient
+
+ # This module is extended into Net::HTTP Response objects created from
+ # Socketless Chef Zero responses.
+ module ResponseExts
+
+ # Net::HTTP raises an error if #read_body is called with a block or
+ # file argument after the body has already been read from the network.
+ #
+ # Since we always set the body to the string response from Chef Zero
+ # and set the `@read` indicator variable, we have to patch this method
+ # or else streaming-style responses won't work.
+ def read_body(dest = nil, &block)
+ if dest
+ raise "responses from socketless chef zero can't be written to specific destination"
+ end
+
+ if block_given?
+ block.call(@body)
+ else
+ super
+ end
+ end
+
+ end
+
+ # copied verbatim from webrick (2-clause BSD License)
+ #
+ # HTTP status codes and descriptions
+ STATUS_MESSAGE = {
+ 100 => 'Continue',
+ 101 => 'Switching Protocols',
+ 200 => 'OK',
+ 201 => 'Created',
+ 202 => 'Accepted',
+ 203 => 'Non-Authoritative Information',
+ 204 => 'No Content',
+ 205 => 'Reset Content',
+ 206 => 'Partial Content',
+ 207 => 'Multi-Status',
+ 300 => 'Multiple Choices',
+ 301 => 'Moved Permanently',
+ 302 => 'Found',
+ 303 => 'See Other',
+ 304 => 'Not Modified',
+ 305 => 'Use Proxy',
+ 307 => 'Temporary Redirect',
+ 400 => 'Bad Request',
+ 401 => 'Unauthorized',
+ 402 => 'Payment Required',
+ 403 => 'Forbidden',
+ 404 => 'Not Found',
+ 405 => 'Method Not Allowed',
+ 406 => 'Not Acceptable',
+ 407 => 'Proxy Authentication Required',
+ 408 => 'Request Timeout',
+ 409 => 'Conflict',
+ 410 => 'Gone',
+ 411 => 'Length Required',
+ 412 => 'Precondition Failed',
+ 413 => 'Request Entity Too Large',
+ 414 => 'Request-URI Too Large',
+ 415 => 'Unsupported Media Type',
+ 416 => 'Request Range Not Satisfiable',
+ 417 => 'Expectation Failed',
+ 422 => 'Unprocessable Entity',
+ 423 => 'Locked',
+ 424 => 'Failed Dependency',
+ 426 => 'Upgrade Required',
+ 428 => 'Precondition Required',
+ 429 => 'Too Many Requests',
+ 431 => 'Request Header Fields Too Large',
+ 500 => 'Internal Server Error',
+ 501 => 'Not Implemented',
+ 502 => 'Bad Gateway',
+ 503 => 'Service Unavailable',
+ 504 => 'Gateway Timeout',
+ 505 => 'HTTP Version Not Supported',
+ 507 => 'Insufficient Storage',
+ 511 => 'Network Authentication Required',
+ }
+
+ STATUS_MESSAGE.values.each {|v| v.freeze }
+ STATUS_MESSAGE.freeze
+
+ def initialize(base_url)
+ @url = base_url
+ end
+
+ def host
+ @url.hostname
+ end
+
+ def port
+ @url.port
+ end
+
+ def request(method, url, body, headers, &handler_block)
+ request = req_to_rack(method, url, body, headers)
+ res = ChefZero::SocketlessServerMap.request(port, request)
+
+ net_http_response = to_net_http(res[0], res[1], res[2])
+
+ yield net_http_response if block_given?
+
+ [self, net_http_response]
+ end
+
+ def req_to_rack(method, url, body, headers)
+ body_str = body || ""
+ {
+ "SCRIPT_NAME" => "",
+ "SERVER_NAME" => "localhost",
+ "REQUEST_METHOD" => method.to_s.upcase,
+ "PATH_INFO" => url.path,
+ "QUERY_STRING" => url.query,
+ "SERVER_PORT" => url.port,
+ "HTTP_HOST" => "localhost:#{url.port}",
+ "rack.url_scheme" => "chefzero",
+ "rack.input" => StringIO.new(body_str),
+ }
+ end
+
+ def to_net_http(code, headers, chunked_body)
+ body = chunked_body.join('')
+ msg = STATUS_MESSAGE[code] or raise "Cannot determine HTTP status message for code #{code}"
+ response = Net::HTTPResponse.send(:response_class, code.to_s).new("1.0", code.to_s, msg)
+ response.instance_variable_set(:@body, body)
+ headers.each do |name, value|
+ if value.respond_to?(:each)
+ value.each { |v| response.add_field(name, v) }
+ else
+ response[name] = value
+ end
+ end
+
+ response.instance_variable_set(:@read, true)
+ response.extend(ResponseExts)
+ response
+ end
+
+ private
+
+ def headers_extracted_from_options
+ options.reject {|name, _| KNOWN_OPTIONS.include?(name) }.map { |name, value|
+ [name.to_s.split("_").map { |segment| segment.capitalize }.join("-"), value]
+ }
+ end
+
+
+ end
+
+ end
+end
diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb
index 855608385d..2612714a19 100644
--- a/lib/chef/rest.rb
+++ b/lib/chef/rest.rb
@@ -40,151 +40,6 @@ require 'chef/http/remote_request_id'
class Chef
- class SocketlessChefZeroClient
-
- module Response
-
- def read_body(dest = nil, &block)
- if dest
- raise "responses from socketless chef zero can't be written to specific destination"
- end
-
- if block_given?
- block.call(@body)
- else
- super
- end
- end
-
- end
-
- # copied verbatim from webrick
- #
- # HTTP status codes and descriptions
- STATUS_MESSAGE = { # :nodoc:
- 100 => 'Continue',
- 101 => 'Switching Protocols',
- 200 => 'OK',
- 201 => 'Created',
- 202 => 'Accepted',
- 203 => 'Non-Authoritative Information',
- 204 => 'No Content',
- 205 => 'Reset Content',
- 206 => 'Partial Content',
- 207 => 'Multi-Status',
- 300 => 'Multiple Choices',
- 301 => 'Moved Permanently',
- 302 => 'Found',
- 303 => 'See Other',
- 304 => 'Not Modified',
- 305 => 'Use Proxy',
- 307 => 'Temporary Redirect',
- 400 => 'Bad Request',
- 401 => 'Unauthorized',
- 402 => 'Payment Required',
- 403 => 'Forbidden',
- 404 => 'Not Found',
- 405 => 'Method Not Allowed',
- 406 => 'Not Acceptable',
- 407 => 'Proxy Authentication Required',
- 408 => 'Request Timeout',
- 409 => 'Conflict',
- 410 => 'Gone',
- 411 => 'Length Required',
- 412 => 'Precondition Failed',
- 413 => 'Request Entity Too Large',
- 414 => 'Request-URI Too Large',
- 415 => 'Unsupported Media Type',
- 416 => 'Request Range Not Satisfiable',
- 417 => 'Expectation Failed',
- 422 => 'Unprocessable Entity',
- 423 => 'Locked',
- 424 => 'Failed Dependency',
- 426 => 'Upgrade Required',
- 428 => 'Precondition Required',
- 429 => 'Too Many Requests',
- 431 => 'Request Header Fields Too Large',
- 500 => 'Internal Server Error',
- 501 => 'Not Implemented',
- 502 => 'Bad Gateway',
- 503 => 'Service Unavailable',
- 504 => 'Gateway Timeout',
- 505 => 'HTTP Version Not Supported',
- 507 => 'Insufficient Storage',
- 511 => 'Network Authentication Required',
- }
-
- STATUS_MESSAGE.values.each {|v| v.freeze }
- STATUS_MESSAGE.freeze
-
- def initialize(base_url)
- @url = base_url
- end
-
- def host
- @url.hostname
- end
-
- def port
- @url.port
- end
-
- # request, response = client.request(method, url, body, headers) {|r| r.read_body }
- def request(method, url, body, headers, &handler_block)
- #pp req: [method, url, body, headers]
- body_str = body || ""
- r = {}
- r["REQUEST_METHOD"] = method.to_s.upcase
- r["SCRIPT_NAME"] = ""
- r["PATH_INFO"] = url.path
- r["QUERY_STRING"] = url.query
- r["SERVER_NAME"] = "localhost"
- r["SERVER_PORT"] = url.port
- r["HTTP_HOST"] = "localhost:#{url.port}"
- r["rack.url_scheme"] = "chefzero"
- r["rack.input"] = StringIO.new(body_str)
-
- res = ChefZero::SocketlessServerMap.request(port, r)
-
- net_http_response = to_net_http(res[0], res[1], res[2])
-
- yield net_http_response if block_given?
-
- [self, net_http_response]
- end
-
- # TODO: this is copied verbatim from the fakeweb project, MIT licensed
- # Add credits where appropriate
-
- def to_net_http(code, headers, chunked_body)
- body = chunked_body.join('')
- msg = STATUS_MESSAGE[code] or raise "Cannot determine HTTP status message for code #{code}"
- response = Net::HTTPResponse.send(:response_class, code.to_s).new("1.0", code.to_s, msg)
- response.instance_variable_set(:@body, body)
- headers.each do |name, value|
- if value.respond_to?(:each)
- value.each { |v| response.add_field(name, v) }
- else
- response[name] = value
- end
- end
-
- response.instance_variable_set(:@read, true)
- response.extend(Response)
- response
- end
-
- private
-
- def headers_extracted_from_options
- options.reject {|name, _| KNOWN_OPTIONS.include?(name) }.map { |name, value|
- [name.to_s.split("_").map { |segment| segment.capitalize }.join("-"), value]
- }
- end
-
-
- end
-
# == Chef::REST
# Chef's custom REST client with built-in JSON support and RSA signed header
# authentication.
diff --git a/spec/unit/http/socketless_chef_zero_client_spec.rb b/spec/unit/http/socketless_chef_zero_client_spec.rb
new file mode 100644
index 0000000000..963cc9e8c4
--- /dev/null
+++ b/spec/unit/http/socketless_chef_zero_client_spec.rb
@@ -0,0 +1,174 @@
+#--
+# Author:: Daniel DeLeo (<dan@chef.io>)
+# Copyright:: Copyright (c) 2015 Chef Software, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/http/socketless_chef_zero_client'
+
+describe Chef::HTTP::SocketlessChefZeroClient do
+
+ let(:relative_url) { "" }
+ let(:uri_str) { "chefzero://localhost:1/#{relative_url}" }
+ let(:uri) { URI(uri_str) }
+
+ subject(:zero_client) { Chef::HTTP::SocketlessChefZeroClient.new(uri) }
+
+ it "has a host" do
+ expect(zero_client.host).to eq("localhost")
+ end
+
+ it "has a port" do
+ expect(zero_client.port).to eq(1)
+ end
+
+ describe "converting requests to rack format" do
+
+ let(:expected_rack_req) do
+ {
+ "SCRIPT_NAME" => "",
+ "SERVER_NAME" => "localhost",
+ "REQUEST_METHOD" => method.to_s.upcase,
+ "PATH_INFO" => uri.path,
+ "QUERY_STRING" => uri.query,
+ "SERVER_PORT" => uri.port,
+ "HTTP_HOST" => "localhost:#{uri.port}",
+ "rack.url_scheme" => "chefzero",
+ }
+ end
+
+ context "when the request has no body" do
+
+ let(:method) { :GET }
+ let(:relative_url) { "clients" }
+ let(:headers) { { "Accept" => "application/json" } }
+ let(:body) { false }
+ let(:expected_body_str) { "" }
+
+ let(:rack_req) { zero_client.req_to_rack(method, uri, body, headers) }
+
+ it "creates a rack request env" do
+ # StringIO doesn't implement == in a way that we can compare, so we
+ # check rack.input individually and then iterate over everything else
+ expect(rack_req["rack.input"].string).to eq(expected_body_str)
+ expected_rack_req.each do |key, value|
+ expect(rack_req[key]).to eq(value)
+ end
+ end
+
+ end
+
+ context "when the request has a body" do
+
+ let(:method) { :PUT }
+ let(:relative_url) { "clients/foo" }
+ let(:headers) { { "Accept" => "application/json" } }
+ let(:body) { "bunch o' JSON" }
+ let(:expected_body_str) { "bunch o' JSON" }
+
+ let(:rack_req) { zero_client.req_to_rack(method, uri, body, headers) }
+
+ it "creates a rack request env" do
+ # StringIO doesn't implement == in a way that we can compare, so we
+ # check rack.input individually and then iterate over everything else
+ expect(rack_req["rack.input"].string).to eq(expected_body_str)
+ expected_rack_req.each do |key, value|
+ expect(rack_req[key]).to eq(value)
+ end
+ end
+
+ end
+
+ end
+
+ describe "converting responses to Net::HTTP objects" do
+
+ let(:net_http_response) { zero_client.to_net_http(code, headers, body) }
+
+ context "when the request was successful (2XX)" do
+
+ let(:code) { 200 }
+ let(:headers) { { "Content-Type" => "Application/JSON" } }
+ let(:body) { [ "bunch o' JSON" ] }
+
+ it "creates a Net::HTTP success response object" do
+ expect(net_http_response).to be_a_kind_of(Net::HTTPOK)
+ expect(net_http_response.read_body).to eq("bunch o' JSON")
+ expect(net_http_response["content-type"]).to eq("Application/JSON")
+ end
+
+ it "does not fail when calling read_body with a block" do
+ expect(net_http_response.read_body {|chunk| chunk }).to eq("bunch o' JSON")
+ end
+
+ end
+
+ context "when the requested object doesn't exist (404)" do
+
+ let(:code) { 404 }
+ let(:headers) { { "Content-Type" => "Application/JSON" } }
+ let(:body) { [ "nope" ] }
+
+ it "creates a Net::HTTPNotFound response object" do
+ expect(net_http_response).to be_a_kind_of(Net::HTTPNotFound)
+ end
+ end
+
+ end
+
+ describe "request-response round trip" do
+
+ let(:method) { :GET }
+ let(:relative_url) { "clients" }
+ let(:headers) { { "Accept" => "application/json" } }
+ let(:body) { false }
+
+ let(:expected_rack_req) do
+ {
+ "SCRIPT_NAME" => "",
+ "SERVER_NAME" => "localhost",
+ "REQUEST_METHOD" => method.to_s.upcase,
+ "PATH_INFO" => uri.path,
+ "QUERY_STRING" => uri.query,
+ "SERVER_PORT" => uri.port,
+ "HTTP_HOST" => "localhost:#{uri.port}",
+ "rack.url_scheme" => "chefzero",
+ "rack.input" => an_instance_of(StringIO),
+ }
+ end
+
+
+ let(:response_code) { 200 }
+ let(:response_headers) { { "Content-Type" => "Application/JSON" } }
+ let(:response_body) { [ "bunch o' JSON" ] }
+
+ let(:rack_response) { [ response_code, response_headers, response_body ] }
+
+ let(:response) { zero_client.request(method, uri, body, headers) }
+
+ before do
+ expect(ChefZero::SocketlessServerMap).to receive(:request).with(1, expected_rack_req).and_return(rack_response)
+ end
+
+ it "makes a rack request to Chef Zero and returns the response as a Net::HTTP object" do
+ _client, net_http_response = response
+ expect(net_http_response).to be_a_kind_of(Net::HTTPOK)
+ expect(net_http_response.code).to eq("200")
+ expect(net_http_response.body).to eq("bunch o' JSON")
+ end
+
+ end
+
+end