diff options
author | danielsdeleo <dan@getchef.com> | 2015-04-01 13:35:24 -0700 |
---|---|---|
committer | danielsdeleo <dan@getchef.com> | 2015-04-01 13:35:24 -0700 |
commit | b89d75f769ba336449f204f77311a1d43bc22de3 (patch) | |
tree | 870211dcd72b19e13fb36fd47f4b19a3c9a6ab1b | |
parent | 6813675a20f232afbe440800a56d4385be55e9fe (diff) | |
parent | 3c3702b1cf4920844066a5e0c81e04251ae6d120 (diff) | |
download | chef-b89d75f769ba336449f204f77311a1d43bc22de3.tar.gz |
Merge branch 'socketless-zero'
-rw-r--r-- | chef.gemspec | 2 | ||||
-rw-r--r-- | lib/chef/application/client.rb | 5 | ||||
-rw-r--r-- | lib/chef/application/knife.rb | 6 | ||||
-rw-r--r-- | lib/chef/config.rb | 8 | ||||
-rw-r--r-- | lib/chef/http.rb | 16 | ||||
-rw-r--r-- | lib/chef/http/socketless_chef_zero_client.rb | 207 | ||||
-rw-r--r-- | lib/chef/knife.rb | 3 | ||||
-rw-r--r-- | lib/chef/local_mode.rb | 16 | ||||
-rw-r--r-- | lib/chef/rest.rb | 9 | ||||
-rw-r--r-- | lib/chef/server_api.rb | 1 | ||||
-rw-r--r-- | spec/integration/client/client_spec.rb | 11 | ||||
-rw-r--r-- | spec/integration/knife/common_options_spec.rb | 6 | ||||
-rw-r--r-- | spec/unit/application/client_spec.rb | 10 | ||||
-rw-r--r-- | spec/unit/http/socketless_chef_zero_client_spec.rb | 174 | ||||
-rw-r--r-- | spec/unit/http_spec.rb | 14 | ||||
-rw-r--r-- | spec/unit/knife_spec.rb | 5 | ||||
-rw-r--r-- | spec/unit/rest_spec.rb | 9 |
17 files changed, 486 insertions, 16 deletions
diff --git a/chef.gemspec b/chef.gemspec index 17861bc4a9..dc0e59ad3e 100644 --- a/chef.gemspec +++ b/chef.gemspec @@ -30,7 +30,7 @@ Gem::Specification.new do |s| s.add_dependency "erubis", "~> 2.7" s.add_dependency "diff-lcs", "~> 1.2", ">= 1.2.4" - s.add_dependency "chef-zero", "~> 4.0" + s.add_dependency "chef-zero", "~> 4.1" s.add_dependency "pry", "~> 0.9" s.add_dependency 'plist', '~> 3.1.0' diff --git a/lib/chef/application/client.rb b/lib/chef/application/client.rb index 9984ad5b9d..a5faee9d35 100644 --- a/lib/chef/application/client.rb +++ b/lib/chef/application/client.rb @@ -258,6 +258,11 @@ class Chef::Application::Client < Chef::Application :description => "Only run the bare minimum ohai plugins chef needs to function", :boolean => true + option :listen, + :long => "--[no-]listen", + :description => "Whether a local mode (-z) server binds to a port", + :boolean => true + IMMEDIATE_RUN_SIGNAL = "1".freeze attr_reader :chef_client_json diff --git a/lib/chef/application/knife.rb b/lib/chef/application/knife.rb index 1a19a45598..af5216ae00 100644 --- a/lib/chef/application/knife.rb +++ b/lib/chef/application/knife.rb @@ -121,6 +121,11 @@ class Chef::Application::Knife < Chef::Application :long => "--chef-zero-port PORT", :description => "Port (or port range) to start chef-zero on. Port ranges like 1000,1010 or 8889-9999 will try all given ports until one works." + option :listen, + :long => "--[no-]listen", + :description => "Whether a local mode (-z) server binds to a port", + :boolean => true + option :version, :short => "-v", :long => "--version", @@ -129,7 +134,6 @@ class Chef::Application::Knife < Chef::Application :proc => lambda {|v| puts "Chef: #{::Chef::VERSION}"}, :exit => 0 - # Run knife def run Mixlib::Log::Formatter.show_time = false diff --git a/lib/chef/config.rb b/lib/chef/config.rb index b897f9fdbd..25557b077f 100644 --- a/lib/chef/config.rb +++ b/lib/chef/config.rb @@ -305,6 +305,14 @@ class Chef default :pid_file, nil + # Whether Chef Zero local mode should bind to a port. All internal requests + # will go through the socketless code path regardless, so the socket is + # only needed if other processes will connect to the local mode server. + # + # For compatibility this is set to true but it will be changed to false in + # the future. + default :listen, true + config_context :chef_zero do config_strict_mode true default(:enabled) { Chef::Config.local_mode } diff --git a/lib/chef/http.rb b/lib/chef/http.rb index 5e52337aff..16a826a3db 100644 --- a/lib/chef/http.rb +++ b/lib/chef/http.rb @@ -25,6 +25,7 @@ require 'tempfile' require 'net/https' require 'uri' require 'chef/http/basic_client' +require 'chef/http/socketless_chef_zero_client' require 'chef/monkey_patches/net_http' require 'chef/config' require 'chef/platform/query_helpers' @@ -196,14 +197,18 @@ class Chef def http_client(base_url=nil) base_url ||= url - BasicClient.new(base_url) + if chef_zero_uri?(base_url) + SocketlessChefZeroClient.new(base_url) + else + BasicClient.new(base_url, :ssl_policy => Chef::HTTP::APISSLPolicy) + end end protected def create_url(path) return path if path.is_a?(URI) - if path =~ /^(http|https):\/\//i + if path =~ /^(http|https|chefzero):\/\//i URI.parse(path) elsif path.nil? or path.empty? URI.parse(@url) @@ -292,7 +297,7 @@ class Chef http_attempts += 1 response, request, return_value = yield # handle HTTP 50X Error - if response.kind_of?(Net::HTTPServerError) + if response.kind_of?(Net::HTTPServerError) && !Chef::Config.local_mode if http_retry_count - http_attempts + 1 > 0 sleep_time = 1 + (2 ** http_attempts) + rand(2 ** http_attempts) Chef::Log.error("Server returned error #{response.code} for #{url}, retrying #{http_attempts}/#{http_retry_count} in #{sleep_time}s") @@ -351,6 +356,11 @@ class Chef private + def chef_zero_uri?(uri) + uri = URI.parse(uri) unless uri.respond_to?(:scheme) + uri.scheme == "chefzero" + end + def redirected_to(response) return nil unless response.kind_of?(Net::HTTPRedirection) # Net::HTTPNotModified is undesired subclass of Net::HTTPRedirection so test for this 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..8f5543a16f --- /dev/null +++ b/lib/chef/http/socketless_chef_zero_client.rb @@ -0,0 +1,207 @@ +#-- +# 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 + + attr_reader :url + + # 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/knife.rb b/lib/chef/knife.rb index e13f80b5a8..2e0694aebc 100644 --- a/lib/chef/knife.rb +++ b/lib/chef/knife.rb @@ -373,6 +373,9 @@ class Chef Chef::Config[:environment] = config[:environment] if config[:environment] Chef::Config.local_mode = config[:local_mode] if config.has_key?(:local_mode) + + Chef::Config.listen = config[:listen] if config.has_key?(:listen) + if Chef::Config.local_mode && !Chef::Config.has_key?(:cookbook_path) && !Chef::Config.has_key?(:chef_repo_path) Chef::Config.chef_repo_path = Chef::Config.find_chef_repo_path(Dir.pwd) end diff --git a/lib/chef/local_mode.rb b/lib/chef/local_mode.rb index e66acb6b66..79fb750dd8 100644 --- a/lib/chef/local_mode.rb +++ b/lib/chef/local_mode.rb @@ -18,6 +18,7 @@ require 'chef/config' class Chef module LocalMode + # Create a chef local server (if the configuration requires one) for the # duration of the given block. # @@ -59,12 +60,21 @@ class Chef server_options = {} server_options[:data_store] = data_store server_options[:log_level] = Chef::Log.level + server_options[:host] = Chef::Config.chef_zero.host server_options[:port] = parse_port(Chef::Config.chef_zero.port) @chef_zero_server = ChefZero::Server.new(server_options) - @chef_zero_server.start_background - Chef::Log.info("Started chef-zero at #{@chef_zero_server.url} with #{@chef_fs.fs_description}") - Chef::Config.chef_server_url = @chef_zero_server.url + + if Chef::Config[:listen] + @chef_zero_server.start_background + else + @chef_zero_server.start_socketless + end + + local_mode_url = @chef_zero_server.local_mode_url + + Chef::Log.info("Started chef-zero at #{local_mode_url} with #{@chef_fs.fs_description}") + Chef::Config.chef_server_url = local_mode_url end end diff --git a/lib/chef/rest.rb b/lib/chef/rest.rb index f0de443058..2612714a19 100644 --- a/lib/chef/rest.rb +++ b/lib/chef/rest.rb @@ -39,6 +39,7 @@ require 'chef/platform/query_helpers' require 'chef/http/remote_request_id' class Chef + # == Chef::REST # Chef's custom REST client with built-in JSON support and RSA signed header # authentication. @@ -57,6 +58,9 @@ class Chef # http://localhost:4000, a call to +get_rest+ with 'nodes' will make an # HTTP GET request to http://localhost:4000/nodes def initialize(url, client_name=Chef::Config[:node_name], signing_key_filename=Chef::Config[:client_key], options={}) + + signing_key_filename = nil if chef_zero_uri?(url) + options = options.dup options[:client_name] = client_name options[:signing_key_filename] = signing_key_filename @@ -188,11 +192,6 @@ class Chef public :create_url - def http_client(base_url=nil) - base_url ||= url - BasicClient.new(base_url, :ssl_policy => Chef::HTTP::APISSLPolicy) - end - ############################################################################ # DEPRECATED ############################################################################ diff --git a/lib/chef/server_api.rb b/lib/chef/server_api.rb index 8cdcd7a09d..ec4a864cb3 100644 --- a/lib/chef/server_api.rb +++ b/lib/chef/server_api.rb @@ -30,6 +30,7 @@ class Chef def initialize(url = Chef::Config[:chef_server_url], options = {}) options[:client_name] ||= Chef::Config[:node_name] options[:signing_key_filename] ||= Chef::Config[:client_key] + options[:signing_key_filename] = nil if chef_zero_uri?(url) super(url, options) end diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb index aa9aefe1a7..b5c5e12781 100644 --- a/spec/integration/client/client_spec.rb +++ b/spec/integration/client/client_spec.rb @@ -60,6 +60,17 @@ EOM result.error! end + it "should complete successfully with --no-listen" do + file 'config/client.rb', <<EOM +local_mode true +cookbook_path "#{path_to('cookbooks')}" +EOM + + result = shell_out("#{chef_client} --no-listen -c \"#{path_to('config/client.rb')}\" -o 'x::default'", :cwd => chef_dir) + result.error! + end + + context 'and no config file' do it 'should complete with success when cwd is just above cookbooks and paths are not specified' do result = shell_out("#{chef_client} -z -o 'x::default' --disable-config", :cwd => path_to('')) diff --git a/spec/integration/knife/common_options_spec.rb b/spec/integration/knife/common_options_spec.rb index ec76738b6f..b2e2e3fc2a 100644 --- a/spec/integration/knife/common_options_spec.rb +++ b/spec/integration/knife/common_options_spec.rb @@ -39,7 +39,7 @@ describe 'knife common options', :workstation do it 'knife raw /nodes/x should retrieve the node' do knife('raw /nodes/x').should_succeed( /"name": "x"/ ) - expect(Chef::Config.chef_server_url).to eq('http://localhost:9999') + expect(Chef::Config.chef_server_url).to eq('chefzero://localhost:9999') end end @@ -101,7 +101,7 @@ EOM it 'knife raw -z --chef-zero-port=9999 /nodes/x retrieves the node' do knife('raw -z --chef-zero-port=9999 /nodes/x').should_succeed( /"name": "x"/ ) - expect(Chef::Config.chef_server_url).to eq('http://localhost:9999') + expect(Chef::Config.chef_server_url).to eq('chefzero://localhost:9999') end context 'when the default port (8889) is already bound' do @@ -149,7 +149,7 @@ EOM it 'knife raw -z --chef-zero-port=9999 /nodes/x retrieves the node' do knife('raw -z --chef-zero-port=9999 /nodes/x').should_succeed( /"name": "x"/ ) - expect(Chef::Config.chef_server_url).to eq('http://localhost:9999') + expect(Chef::Config.chef_server_url).to eq('chefzero://localhost:9999') end end end diff --git a/spec/unit/application/client_spec.rb b/spec/unit/application/client_spec.rb index 501251b7fe..c753ca0ab8 100644 --- a/spec/unit/application/client_spec.rb +++ b/spec/unit/application/client_spec.rb @@ -131,6 +131,16 @@ Enable chef-client interval runs by setting `:client_fork = true` in your config end + describe "when --no-listen is set" do + + it "configures listen = false" do + app.config[:listen] = false + app.reconfigure + expect(Chef::Config[:listen]).to eq(false) + end + + end + describe "when the json_attribs configuration option is specified" do let(:json_attribs) { {"a" => "b"} } 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 diff --git a/spec/unit/http_spec.rb b/spec/unit/http_spec.rb index ddfc56583d..4d851df951 100644 --- a/spec/unit/http_spec.rb +++ b/spec/unit/http_spec.rb @@ -20,6 +20,7 @@ require 'spec_helper' require 'chef/http' require 'chef/http/basic_client' +require 'chef/http/socketless_chef_zero_client' class Chef::HTTP public :create_url @@ -27,6 +28,19 @@ end describe Chef::HTTP do + context "when given a chefzero:// URL" do + + let(:uri) { URI("chefzero://localhost:1") } + + subject(:http) { Chef::HTTP.new(uri) } + + it "uses the SocketlessChefZeroClient to handle requests" do + expect(http.http_client).to be_a_kind_of(Chef::HTTP::SocketlessChefZeroClient) + expect(http.http_client.url).to eq(uri) + end + + end + describe "create_url" do it 'should return a correctly formatted url 1/3 CHEF-5261' do diff --git a/spec/unit/knife_spec.rb b/spec/unit/knife_spec.rb index 2ccf8493ad..b748232081 100644 --- a/spec/unit/knife_spec.rb +++ b/spec/unit/knife_spec.rb @@ -271,6 +271,11 @@ describe Chef::Knife do expect(knife_command.config[:opt_with_default]).to eq("from-cli") end + it "merges `listen` config to Chef::Config" do + Chef::Knife.run(%w[test yourself --no-listen], Chef::Application::Knife.options) + expect(Chef::Config[:listen]).to be(false) + end + context "verbosity is greater than zero" do let(:fake_config) { "/does/not/exist/knife.rb" } diff --git a/spec/unit/rest_spec.rb b/spec/unit/rest_spec.rb index 1aa7ac84ee..85c9e3df8f 100644 --- a/spec/unit/rest_spec.rb +++ b/spec/unit/rest_spec.rb @@ -92,6 +92,15 @@ describe Chef::REST do Chef::REST.new(base_url, nil, nil, options) end + context 'when created with a chef zero URL' do + + let(:url) { "chefzero://localhost:1" } + + it "does not load the signing key" do + expect { Chef::REST.new(url) }.to_not raise_error + end + end + describe "calling an HTTP verb on a path or absolute URL" do it "adds a relative URL to the base url it was initialized with" do expect(rest.create_url("foo/bar/baz")).to eq(URI.parse(base_url + "/foo/bar/baz")) |