From 2469894eab12f24893916b571a981e082dfe97df Mon Sep 17 00:00:00 2001 From: Jordan Running Date: Thu, 11 Feb 2016 13:06:56 -0600 Subject: Make user and client keys endpoints pass Pedant specs - Implement ActorKeyEndpoint, ActorKeysEndpoint. - Implement user, client keys in `ActorEndpoint#delete`, `#put`. - RestBase - Fix RestErrorResponse exceptions to report actual `rest_path` instead associated with the failed data store operation instead of `request.rest_path`. - Move `json_response`, `already_json_response` args `request_version` and `response_version` into options hash; add docs. - DataError, RestErrorResponse: Pass useful message text to `super`. - RestRouter: Clean up logging - Print request methods, paths and bodies more readably for log_level >= INFO. - Pretty-print RestRequest objects (only printed when log_level == DEBUG). - Server: Change default log_level to `:warn` (to enable logging cleanup above). - `Rakefile`, `spec/run_oc_pedant.rb` - Consume RSpec, Pedant options from `ENV['RSPEC_OPTS']`, `ENV['PEDANT_OPTS']` (see `rake -D`). - Consume `ENV['LOG_LEVEL'` (see `rake -D`). - Clean up ChefZero::Server default opts and move duplicated logic to `start_chef_server` method. --- Rakefile | 36 +++--- lib/chef_zero/chef_data/data_normalizer.rb | 4 +- lib/chef_zero/data_store/data_error.rb | 7 +- lib/chef_zero/endpoints/actor_endpoint.rb | 113 +++++++++++++++--- lib/chef_zero/endpoints/actor_key_endpoint.rb | 97 ++++++++++++++++ lib/chef_zero/endpoints/actor_keys_endpoint.rb | 126 +++++++++++++++++++++ lib/chef_zero/endpoints/rest_object_endpoint.rb | 24 +++- .../endpoints/server_api_version_endpoint.rb | 2 +- lib/chef_zero/rest_base.rb | 99 +++++++++++----- lib/chef_zero/rest_error_response.rb | 6 +- lib/chef_zero/rest_router.rb | 61 +++++++--- lib/chef_zero/rspec.rb | 2 +- lib/chef_zero/server.rb | 8 +- spec/run_oc_pedant.rb | 105 +++++++++++------ 14 files changed, 566 insertions(+), 124 deletions(-) create mode 100644 lib/chef_zero/endpoints/actor_key_endpoint.rb create mode 100644 lib/chef_zero/endpoints/actor_keys_endpoint.rb diff --git a/Rakefile b/Rakefile index d8a0282..bbb984c 100644 --- a/Rakefile +++ b/Rakefile @@ -3,33 +3,43 @@ require 'bundler/gem_tasks' require 'chef_zero/version' +def run_oc_pedant(env={}) + ENV.update(env) + require File.expand_path('spec/run_oc_pedant') +end + +ENV_DOCS = < :pedant -desc "run specs" +desc "Run specs" task :spec do system('rspec spec/*_spec.rb') end -desc "run oc pedant" -task :pedant do - require File.expand_path('spec/run_oc_pedant') -end +desc "Run oc-chef-pedant\n\n#{ENV_DOCS}" +task :pedant => :oc_pedant -desc "run pedant with CHEF_FS set" +desc "Run oc-chef-pedant with CHEF_FS set\n\n#{ENV_DOCS}" task :cheffs do - ENV['CHEF_FS'] = "yes" - require File.expand_path('spec/run_oc_pedant') + run_oc_pedant('CHEF_FS' => 'yes') end -desc "run pedant with FILE_STORE set" +desc "Run oc-chef-pedant with FILE_STORE set\n\n#{ENV_DOCS}" task :filestore do - ENV['FILE_STORE'] = "yes" - require File.expand_path('spec/run_oc_pedant') + run_oc_pedant('FILE_STORE' => 'yes') end -desc "run oc pedant" task :oc_pedant do - require File.expand_path('spec/run_oc_pedant') + run_oc_pedant end task :chef_spec do diff --git a/lib/chef_zero/chef_data/data_normalizer.rb b/lib/chef_zero/chef_data/data_normalizer.rb index 95f3daa..9cec4f4 100644 --- a/lib/chef_zero/chef_data/data_normalizer.rb +++ b/lib/chef_zero/chef_data/data_normalizer.rb @@ -17,8 +17,7 @@ module ChefZero def self.normalize_client(client, name, orgname = nil) client['name'] ||= name client['clientname'] ||= name - client['admin'] = !!client['admin'] if client.has_key?('admin') - client['public_key'] ||= PUBLIC_KEY + client['admin'] = !!client['admin'] if client.key?('admin') client['orgname'] ||= orgname client['validator'] ||= false client['validator'] = !!client['validator'] @@ -36,7 +35,6 @@ module ChefZero def self.normalize_user(user, name, identity_keys, osc_compat, method=nil) user[identity_keys.first] ||= name - user['public_key'] ||= PUBLIC_KEY user['admin'] ||= false user['admin'] = !!user['admin'] user['openid'] ||= nil diff --git a/lib/chef_zero/data_store/data_error.rb b/lib/chef_zero/data_store/data_error.rb index 9822a6b..b392e58 100644 --- a/lib/chef_zero/data_store/data_error.rb +++ b/lib/chef_zero/data_store/data_error.rb @@ -19,13 +19,14 @@ module ChefZero module DataStore class DataError < StandardError + attr_reader :path, :cause + def initialize(path, cause = nil) @path = path @cause = cause + path_for_msg = path.nil? ? "nil" : "/#{path.join('/')}" + super "Data path: #{path_for_msg}" end - - attr_reader :path - attr_reader :cause end end end diff --git a/lib/chef_zero/endpoints/actor_endpoint.rb b/lib/chef_zero/endpoints/actor_endpoint.rb index 1572ac1..12fbe11 100644 --- a/lib/chef_zero/endpoints/actor_endpoint.rb +++ b/lib/chef_zero/endpoints/actor_endpoint.rb @@ -10,6 +10,7 @@ module ChefZero class ActorEndpoint < RestObjectEndpoint def delete(request) result = super + if request.rest_path[0] == 'users' list_data(request, [ 'organizations' ]).each do |org| begin @@ -18,12 +19,15 @@ module ChefZero end end end + + delete_actor_keys!(request) result end def put(request) # Find out if we're updating the public key. request_body = FFI_Yajl::Parser.parse(request.body, :create_additions => false) + if request_body['public_key'].nil? # If public_key is null, then don't overwrite it. Weird patchiness. body_modified = true @@ -33,17 +37,17 @@ module ChefZero end # Generate private_key if requested. - if request_body.has_key?('private_key') + if request_body.key?('private_key') body_modified = true - if request_body['private_key'] + + if request_body.delete('private_key') private_key, public_key = server.gen_key_pair updating_public_key = true request_body['public_key'] = public_key end - request_body.delete('private_key') end - # Save request + # Put modified body back in `request.body` request.body = FFI_Yajl::Encoder.encode(request_body, :pretty => true) if body_modified # PUT /clients is patchy @@ -53,27 +57,29 @@ module ChefZero # Inject private_key into response, delete public_key/password if applicable if result[0] == 200 || result[0] == 201 + client_or_user_name = identity_key_value(request) || request.rest_path[-1] + + if is_rename?(request) + rename_keys!(request, client_or_user_name) + end + if request.rest_path[0] == 'users' - key = nil - identity_keys.each do |identity_key| - key ||= request_body[identity_key] - end - key ||= request.rest_path[-1] response = { - 'uri' => build_uri(request.base_uri, [ 'users', key ]) + 'uri' => build_uri(request.base_uri, [ 'users', client_or_user_name ]) } else response = FFI_Yajl::Parser.parse(result[2], :create_additions => false) end - if request.rest_path[2] == 'clients' + if client?(request) response['private_key'] = private_key ? private_key : false else response['private_key'] = private_key if private_key + response.delete('public_key') unless updating_public_key end - response.delete('public_key') if !updating_public_key && request.rest_path[2] == 'users' response.delete('password') + json_response(result[0], response) else result @@ -81,13 +87,86 @@ module ChefZero end def populate_defaults(request, response_json) - response = FFI_Yajl::Parser.parse(response_json, :create_additions => false) - if request.rest_path[2] == 'clients' - response = ChefData::DataNormalizer.normalize_client(response,request.rest_path[3], request.rest_path[1]) + response = FFI_Yajl::Parser.parse(response_json, create_additions: false) + + populated_response = + if client?(request) + ChefData::DataNormalizer.normalize_client( + response, + response["name"] || request.rest_path[-1], + request.rest_path[1] + ) + else + ChefData::DataNormalizer.normalize_user( + response, + response["username"] || request.rest_path[-1], + identity_keys, + server.options[:osc_compat], + request.method + ) + end + + FFI_Yajl::Encoder.encode(populated_response, pretty: true) + end + + private + + # Move key data to new path + def rename_keys!(request, new_client_or_user_name) + orig_keys_path = keys_path_base(request) + new_keys_path = orig_keys_path.dup + .tap {|path| path[-2] = new_client_or_user_name } + + key_names = list_data_or_else(request, orig_keys_path, nil) + return unless key_names # No keys to move + + key_names.each do |key_name| + # Get old data + orig_path = [ *orig_keys_path, key_name ] + data = get_data(request, orig_path, :data_store_exceptions) + + # Copy data to new path + create_data( + request, + new_keys_path, key_name, + data, + :create_dir + ) + end + + # Delete original data + delete_data_dir(request, orig_keys_path, :recursive, :data_store_exceptions) + end + + def delete_actor_keys!(request) + path = keys_path_base(request)[0..-2] + delete_data_dir(request, path, :recursive, :data_store_exceptions) + rescue DataStore::DataNotFoundError + end + + def client?(request, rest_path=nil) + rest_path ||= request.rest_path + request.rest_path[2] == "clients" + end + + # Return the data store keys path for the request client or user, e.g. + # + # [ "organizations", , "client_keys", , "keys" ] + # + # Or: + # + # [ "user_keys", , "keys" ] + # + def keys_path_base(request, client_or_user_name=nil) + rest_path = (rest_path || request.rest_path).dup + rest_path[-1] = client_or_user_name if client_or_user_name + + if client?(request, rest_path) + [ *rest_path[0..1], "client_keys" ] else - response = ChefData::DataNormalizer.normalize_user(response, request.rest_path[3], identity_keys, server.options[:osc_compat], request.method) + [ "user_keys" ] end - FFI_Yajl::Encoder.encode(response, :pretty => true) + .push(rest_path.last, "keys") end end end diff --git a/lib/chef_zero/endpoints/actor_key_endpoint.rb b/lib/chef_zero/endpoints/actor_key_endpoint.rb new file mode 100644 index 0000000..d45570d --- /dev/null +++ b/lib/chef_zero/endpoints/actor_key_endpoint.rb @@ -0,0 +1,97 @@ +require 'ffi_yajl' +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # /users/USER/keys/NAME + # /organizations/ORG/clients/CLIENT/keys/NAME + class ActorKeyEndpoint < RestBase + DEFAULT_PUBLIC_KEY_NAME = "default".freeze + + def get(request) + # Try to get the actor so a 404 is returned if it doesn't exist + actor_json = get_actor_json(request) + + if request.rest_path[-1] == DEFAULT_PUBLIC_KEY_NAME + actor_data = FFI_Yajl::Parser.parse(actor_json, create_additions: false) + default_public_key = default_public_key_from_actor(actor_data) + return json_response(200, default_public_key) + end + + key_path = data_path(request) + already_json_response(200, get_data(request, key_path)) + end + + def delete(request) + # Try to get the actor so a 404 is returned if it doesn't exist + actor_json = get_actor_json(request) + + if request.rest_path[-1] == DEFAULT_PUBLIC_KEY_NAME + actor_data = FFI_Yajl::Parser.parse(actor_json, create_additions: false) + default_public_key = delete_actor_default_public_key!(request, actor_data) + return json_response(200, default_public_key) + end + + key_path = data_path(request) + + data = get_data(request, key_path) + delete_data(request, key_path) + + already_json_response(200, data) + end + + def put(request) + # We grab the old data to trigger a 404 if it doesn't exist + get_data(request, data_path(request)) + + set_data(request, path, request.body) + end + + private + + # Returns the keys data store path, which is the same as + # `request.rest_path` except with "user_keys" instead of "users" or + # "client_keys" instead of "clients." + def data_path(request) + request.rest_path.dup.tap do |path| + if client?(request) + path[2] = "client_keys" + else + path[0] = "user_keys" + end + end + end + + def default_public_key_from_actor(actor_data) + { "name" => DEFAULT_PUBLIC_KEY_NAME, + "public_key" => actor_data["public_key"], + "expiration_date" => "infinity" } + end + + def delete_actor_default_public_key!(request, actor_data) + new_actor_data = actor_data.merge("public_key" => nil) + + set_data( + request, + actor_path(request), + FFI_Yajl::Encoder.encode(new_actor_data, pretty: true) + ) + + default_public_key_from_actor(actor_data) + end + + def get_actor_json(request) + get_data(request, actor_path(request)) + end + + def client?(request) + request.rest_path[2] == "clients" + end + + def actor_path(request) + return request.rest_path[0..3] if client?(request) + request.rest_path[0..1] + end + end + end +end diff --git a/lib/chef_zero/endpoints/actor_keys_endpoint.rb b/lib/chef_zero/endpoints/actor_keys_endpoint.rb new file mode 100644 index 0000000..8bf3981 --- /dev/null +++ b/lib/chef_zero/endpoints/actor_keys_endpoint.rb @@ -0,0 +1,126 @@ +require 'ffi_yajl' +require 'chef_zero/rest_base' + +module ChefZero + module Endpoints + # /users/USER/keys + # /organizations/ORG/clients/CLIENT/keys + class ActorKeysEndpoint < RestBase + DEFAULT_PUBLIC_KEY_NAME = "default" + DATE_FORMAT = "%FT%TZ" # e.g. 2015-12-24T21:00:00Z + + def get(request) + path = data_path(request) + + # Get actor or 404 if it doesn't exist + actor_path = request.rest_path[ client?(request) ? 0..3 : 0..1 ] + actor_json = get_data(request, actor_path) + + key_names = list_data_or_else(request, path, []) + key_names.unshift(DEFAULT_PUBLIC_KEY_NAME) if actor_has_default_public_key?(actor_json) + + result = key_names.map do |key_name| + list_key(request, [ *path, key_name ]) + end + + json_response(200, result) + end + + def post(request) + request_body = FFI_Yajl::Parser.parse(request.body, create_additions: false) + + # Try loading the client or user so a 404 is returned if it doesn't exist + actor_path = request.rest_path[ client?(request) ? 0..3 : 0..1 ] + actor_json = get_data(request, actor_path) + + generate_keys = request_body["public_key"].nil? + + if generate_keys + private_key, public_key = server.gen_key_pair + else + public_key = request_body['public_key'] + end + + key_name = request_body["name"] + + if key_name == DEFAULT_PUBLIC_KEY_NAME + store_actor_default_public_key!(request, actor_path, actor_json, public_key) + else + store_actor_public_key!(request, key_name, public_key, request_body["expiration_date"]) + end + + response_body = { "uri" => key_uri(request, key_name) } + response_body["private_key"] = private_key if generate_keys + + json_response(201, response_body, + headers: { "Location" => response_body["uri"] }) + end + + private + + def store_actor_public_key!(request, name, public_key, expiration_date) + data = FFI_Yajl::Encoder.encode( + "name" => name, + "public_key" => public_key, + "expiration_date" => expiration_date + ) + + create_data(request, data_path(request), name, data, :create_dir) + end + + def store_actor_default_public_key!(request, actor_path, actor_json, public_key) + actor_data = FFI_Yajl::Parser.parse(actor_json, create_additions: false) + + if actor_data["public_key"] + raise RestErrorResponse.new(409, "Object already exists: #{key_uri(request, DEFAULT_PUBLIC_KEY_NAME)}") + end + + actor_data["public_key"] = public_key + set_data(request, actor_path, FFI_Yajl::Encoder.encode(actor_data, pretty: true)) + end + + # Returns the keys data store path, which is the same as + # `request.rest_path` except with "user_keys" instead of "users" or + # "client_keys" instead of "clients." + def data_path(request) + request.rest_path.dup.tap do |path| + if client?(request) + path[2] = "client_keys" + else + path[0] = "user_keys" + end + end + end + + def list_key(request, data_path) + key_name, expiration_date = + if data_path[-1] == DEFAULT_PUBLIC_KEY_NAME + [ DEFAULT_PUBLIC_KEY_NAME, "infinity" ] + else + FFI_Yajl::Parser.parse(get_data(request, data_path), create_additions: false) + .values_at("name", "expiration_date") + end + + expired = expiration_date != "infinity" && + DateTime.now > DateTime.strptime(expiration_date, DATE_FORMAT) + + { "name" => key_name, + "uri" => key_uri(request, key_name), + "expired" => expired } + end + + def client?(request) + request.rest_path[2] == "clients" + end + + def key_uri(request, key_name) + build_uri(request.base_uri, [ *request.rest_path, key_name ]) + end + + def actor_has_default_public_key?(actor_json) + actor_data = FFI_Yajl::Parser.parse(actor_json, create_additions: false) + !!actor_data["public_key"] + end + end + end +end diff --git a/lib/chef_zero/endpoints/rest_object_endpoint.rb b/lib/chef_zero/endpoints/rest_object_endpoint.rb index 9e978b4..95a3122 100644 --- a/lib/chef_zero/endpoints/rest_object_endpoint.rb +++ b/lib/chef_zero/endpoints/rest_object_endpoint.rb @@ -21,12 +21,11 @@ module ChefZero def put(request) # We grab the old body to trigger a 404 if it doesn't exist old_body = get_data(request) - request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) - key = identity_keys.map { |k| request_json[k] }.select { |v| v }.first - key ||= request.rest_path[-1] + # If it's a rename, check for conflict and delete the old value - rename = key != request.rest_path[-1] - if rename + if is_rename?(request) + key = identity_key_value(request) + begin create_data(request, request.rest_path[0..-2], key, request.body, :data_store_exceptions) rescue DataStore::DataAlreadyExistsError @@ -56,8 +55,23 @@ module ChefZero return FFI_Yajl::Encoder.encode(merged_json, :pretty => true) end end + request.body end + + private + + # Get the value of the (first existing) identity key from the request body or nil + def identity_key_value(request) + request_json = FFI_Yajl::Parser.parse(request.body, :create_additions => false) + identity_keys.map { |k| request_json[k] }.compact.first + end + + # Does this request change the value of the identity key? + def is_rename?(request) + return false unless key = identity_key_value(request) + key != request.rest_path[-1] + end end end end diff --git a/lib/chef_zero/endpoints/server_api_version_endpoint.rb b/lib/chef_zero/endpoints/server_api_version_endpoint.rb index 631f105..8ddeaba 100644 --- a/lib/chef_zero/endpoints/server_api_version_endpoint.rb +++ b/lib/chef_zero/endpoints/server_api_version_endpoint.rb @@ -7,7 +7,7 @@ module ChefZero API_VERSION = 1 def get(request) json_response(200, {"min_api_version"=>MIN_API_VERSION, "max_api_version"=>MAX_API_VERSION}, - request.api_version, API_VERSION) + request_version: request.api_version, response_version: API_VERSION) end end end diff --git a/lib/chef_zero/rest_base.rb b/lib/chef_zero/rest_base.rb index a03f4aa..a0c9633 100644 --- a/lib/chef_zero/rest_base.rb +++ b/lib/chef_zero/rest_base.rb @@ -5,6 +5,9 @@ require 'chef_zero/chef_data/acl_path' module ChefZero class RestBase + DEFAULT_REQUEST_VERSION = 0 + DEFAULT_RESPONSE_VERSION = 0 + def initialize(server) @server = server end @@ -16,21 +19,28 @@ module ChefZero end def check_api_version(request) - version = request.api_version - return nil if version.nil? # Not present in headers + return if request.api_version.nil? # Not present in headers + version = request.api_version.to_i + + unless version.to_s == request.api_version.to_s # Version is not an Integer + return json_response(406, + { "username" => request.requestor }, + request_version: -1, response_version: -1 + ) + end - if version.to_i.to_s != version.to_s # Version is not an Integer - return json_response(406, { "username" => request.requestor }, -1, -1) - elsif version.to_i > MAX_API_VERSION or version.to_i < MIN_API_VERSION + if version > MAX_API_VERSION || version < MIN_API_VERSION response = { "error" => "invalid-x-ops-server-api-version", "message" => "Specified version #{version} not supported", "min_api_version" => MIN_API_VERSION, "max_api_version" => MAX_API_VERSION } - return json_response(406, response, version, -1) - else - return nil + + return json_response(406, + response, + request_version: version, response_version: -1 + ) end end @@ -51,7 +61,7 @@ module ChefZero begin self.send(method, request) rescue RestErrorResponse => e - ChefZero::Log.debug("#{e.inspect}\n#{e.backtrace.join("\n")}") + ChefZero::Log.info("#{e.inspect}\n#{e.backtrace.join("\n")}") error(e.response_code, e.error) end end @@ -104,7 +114,7 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end @@ -123,7 +133,7 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end @@ -142,7 +152,7 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end end @@ -155,13 +165,13 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}") end rescue DataStore::DataAlreadyExistsError if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") + raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}") end end end @@ -174,13 +184,13 @@ module ChefZero if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") + raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, rest_path)}") end rescue DataStore::DataAlreadyExistsError if options.include?(:data_store_exceptions) raise else - raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") + raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, rest_path + [name])}") end end end @@ -196,26 +206,59 @@ module ChefZero end def error(response_code, error, opts={}) - json_response(response_code, {"error" => [error]}, 0, 0, opts) + json_response(response_code, { "error" => [ error ] }, opts) end - def json_response(response_code, json, request_version=0, response_version=0, opts={pretty: true}) - do_pretty_json = !!opts[:pretty] # make sure we have a proper Boolean. - already_json_response(response_code, FFI_Yajl::Encoder.encode(json, :pretty => do_pretty_json), request_version, response_version) + # Serializes `data` to JSON and returns an Array with the + # response code, HTTP headers and JSON body. + # + # @param [Fixnum] response_code HTTP response code + # @param [Hash] data The data for the response body as a Hash + # @param [Hash] options + # @option options [Hash] :headers (see #already_json_response) + # @option options [Boolean] :pretty (true) Pretty-format the JSON + # @option options [Fixnum] :request_version (see #already_json_response) + # @option options [Fixnum] :response_version (see #already_json_response) + # + # @return (see #already_json_response) + # + def json_response(response_code, data, options={}) + options = { pretty: true }.merge(options) + do_pretty_json = !!options.delete(:pretty) # make sure we have a proper Boolean. + json = FFI_Yajl::Encoder.encode(data, pretty: do_pretty_json) + already_json_response(response_code, json, options) end def text_response(response_code, text) [response_code, {"Content-Type" => "text/plain"}, text] end - def already_json_response(response_code, json_text, request_version=0, response_version=0) - header = { "min_version" => MIN_API_VERSION.to_s, "max_version" => MAX_API_VERSION.to_s, - "request_version" => request_version.to_s, - "response_version" => response_version.to_s } - [ response_code, - { "Content-Type" => "application/json", - "X-Ops-Server-API-Version" => FFI_Yajl::Encoder.encode(header) }, - json_text ] + # Returns an Array with the response code, HTTP headers, and JSON body. + # + # @param [Fixnum] response_code The HTTP response code + # @param [String] json_text The JSON body for the response + # @param [Hash] options + # @option options [Hash] :headers ({}) HTTP headers (may override default headers) + # @option options [Fixnum] :request_version (0) Request API version + # @option options [Fixnum] :response_version (0) Response API version + # + # @return [Array(Fixnum, Hash{String => String}, String)] + # + def already_json_response(response_code, json_text, options={}) + version_header = FFI_Yajl::Encoder.encode( + "min_version" => MIN_API_VERSION.to_s, + "max_version" => MAX_API_VERSION.to_s, + "request_version" => options[:request_version] || DEFAULT_REQUEST_VERSION.to_s, + "response_version" => options[:response_version] || DEFAULT_RESPONSE_VERSION.to_s + ) + + headers = { + "Content-Type" => "application/json", + "X-Ops-Server-API-Version" => version_header + } + headers.merge!(options[:headers]) if options[:headers] + + [ response_code, headers, json_text ] end # To be called from inside rest endpoints diff --git a/lib/chef_zero/rest_error_response.rb b/lib/chef_zero/rest_error_response.rb index e75d427..8859650 100644 --- a/lib/chef_zero/rest_error_response.rb +++ b/lib/chef_zero/rest_error_response.rb @@ -1,11 +1,11 @@ module ChefZero class RestErrorResponse < StandardError + attr_reader :response_code, :error + def initialize(response_code, error) @response_code = response_code @error = error + super "#{response_code}: #{error}" end - - attr_reader :response_code - attr_reader :error end end diff --git a/lib/chef_zero/rest_router.rb b/lib/chef_zero/rest_router.rb index f2770d3..889c810 100644 --- a/lib/chef_zero/rest_router.rb +++ b/lib/chef_zero/rest_router.rb @@ -1,3 +1,5 @@ +require 'pp' + module ChefZero class RestRouter def initialize(routes) @@ -15,24 +17,18 @@ module ChefZero attr_accessor :not_found def call(request) - begin - ChefZero::Log.debug(request) - ChefZero::Log.debug(request.body) if request.body - - clean_path = "/" + request.rest_path.join("/") - - response = find_endpoint(clean_path).call(request) - ChefZero::Log.debug([ - "", - "--- RESPONSE (#{response[0]}) ---", - response[2], - "--- END RESPONSE ---", - ].join("\n")) - return response - rescue - ChefZero::Log.error("#{$!.inspect}\n#{$!.backtrace.join("\n")}") - [500, {"Content-Type" => "text/plain"}, "Exception raised! #{$!.inspect}\n#{$!.backtrace.join("\n")}"] + log_request(request) + + clean_path = "/" + request.rest_path.join("/") + + find_endpoint(clean_path).call(request).tap do |response| + log_response(response) end + rescue => ex + exception = "#{ex.inspect}\n#{ex.backtrace.join("\n")}" + + ChefZero::Log.error(exception) + [ 500, { "Content-Type" => "text/plain" }, "Exception raised! #{exception}" ] end private @@ -41,5 +37,36 @@ module ChefZero _, endpoint = routes.find { |route, endpoint| route.match(clean_path) } endpoint || not_found end + + def log_request(request) + ChefZero::Log.info do + "#{request.method} /#{request.rest_path.join("/")}".tap do |msg| + next unless request.method =~ /^(POST|PUT)$/ + + if request.body.nil? || request.body.empty? + msg << " (no body)" + else + msg << [ + "", + "--- #{request.method} BODY ---", + request.body.chomp, + "--- END #{request.method} BODY ---" + ].join("\n") + end + end + end + + ChefZero::Log.debug { request.pretty_inspect } + end + + def log_response(response) + ChefZero::Log.info { + [ "", + "--- RESPONSE (#{response[0]}) ---", + response[2].chomp, + "--- END RESPONSE ---", + ].join("\n") + } + end end end diff --git a/lib/chef_zero/rspec.rb b/lib/chef_zero/rspec.rb index fe8cf30..8867f37 100644 --- a/lib/chef_zero/rspec.rb +++ b/lib/chef_zero/rspec.rb @@ -67,7 +67,7 @@ module ChefZero if chef_server_options[:server_scope] != self.class.chef_server_options[:server_scope] raise "server_scope: #{chef_server_options[:server_scope]} will not be honored: it can only be set on when_the_chef_server!" end - Log.debug("Starting Chef server with options #{chef_server_options}") + Log.info("Starting Chef server with options #{chef_server_options}") ChefZero::RSpec.set_server_options(chef_server_options) diff --git a/lib/chef_zero/server.rb b/lib/chef_zero/server.rb index d6f27c4..da277b6 100644 --- a/lib/chef_zero/server.rb +++ b/lib/chef_zero/server.rb @@ -94,6 +94,8 @@ require 'chef_zero/endpoints/system_recovery_endpoint' require 'chef_zero/endpoints/user_association_requests_endpoint' require 'chef_zero/endpoints/user_association_requests_count_endpoint' require 'chef_zero/endpoints/user_association_request_endpoint' +require 'chef_zero/endpoints/actor_key_endpoint' +require 'chef_zero/endpoints/actor_keys_endpoint' require 'chef_zero/endpoints/user_organizations_endpoint' require 'chef_zero/endpoints/file_store_file_endpoint' require 'chef_zero/endpoints/not_found_endpoint' @@ -107,7 +109,7 @@ module ChefZero DEFAULT_OPTIONS = { :host => '127.0.0.1', :port => 8889, - :log_level => :info, + :log_level => :warn, :generate_real_keys => true, :single_org => 'chef', :ssl => false @@ -537,6 +539,8 @@ module ChefZero [ "/users/*/association_requests", UserAssociationRequestsEndpoint.new(self) ], [ "/users/*/association_requests/count", UserAssociationRequestsCountEndpoint.new(self) ], [ "/users/*/association_requests/*", UserAssociationRequestEndpoint.new(self) ], + [ "/users/*/keys", ActorKeysEndpoint.new(self) ], + [ "/users/*/keys/*", ActorKeyEndpoint.new(self) ], [ "/users/*/organizations", UserOrganizationsEndpoint.new(self) ], [ "/authenticate_user", AuthenticateUserEndpoint.new(self) ], [ "/system_recovery", SystemRecoveryEndpoint.new(self) ], @@ -564,6 +568,8 @@ module ChefZero [ "/dummy", DummyEndpoint.new(self) ], [ "/organizations/*/clients", ActorsEndpoint.new(self) ], [ "/organizations/*/clients/*", ActorEndpoint.new(self) ], + [ "/organizations/*/clients/*/keys", ActorKeysEndpoint.new(self) ], + [ "/organizations/*/clients/*/keys/*", ActorKeyEndpoint.new(self) ], [ "/organizations/*/controls", ControlsEndpoint.new(self) ], [ "/organizations/*/cookbooks", CookbooksEndpoint.new(self) ], [ "/organizations/*/cookbooks/*", CookbookEndpoint.new(self) ], diff --git a/spec/run_oc_pedant.rb b/spec/run_oc_pedant.rb index 92ef136..c3d4e7f 100644 --- a/spec/run_oc_pedant.rb +++ b/spec/run_oc_pedant.rb @@ -5,6 +5,42 @@ require 'bundler/setup' require 'chef_zero/server' require 'rspec/core' +# This file runs oc-chef-pedant specs and is invoked by `rake pedant` +# and other Rake tasks. Run `rake -T` to list tasks. +# +# Options for oc-chef-pedant and rspec can be specified via +# ENV['PEDANT_OPTS'] and ENV['RSPEC_OPTS'], respectively. +# +# The log level can be specified via ENV['LOG_LEVEL']. +# +# Example: +# +# $ PEDANT_OPTS="--focus-users --skip-keys" \ +# > RSPEC_OPTS="--fail-fast --profile 5" \ +# > LOG_LEVEL=debug \ +# > rake pedant +# + +DEFAULT_SERVER_OPTIONS = { + port: 8889, + single_org: false, +}.freeze + +DEFAULT_LOG_LEVEL = :warn + +def log_level + return ENV['LOG_LEVEL'].downcase.to_sym if ENV['LOG_LEVEL'] + return :debug if ENV['DEBUG'] + DEFAULT_LOG_LEVEL +end + +def start_chef_server(opts={}) + opts = DEFAULT_SERVER_OPTIONS.merge(opts) + opts[:log_level] = log_level + + ChefZero::Server.new(opts).tap {|server| server.start_background } +end + def start_cheffs_server(chef_repo_path) require 'chef/version' require 'chef/config' @@ -34,37 +70,42 @@ def start_cheffs_server(chef_repo_path) data_store.set(%w(organizations pedant-testorg groups admins), '{ "users": [ "pivotal" ] }') data_store.set(%w(organizations pedant-testorg groups users), '{ "users": [ "pivotal" ] }') - server = ChefZero::Server.new( - port: 8889, - data_store: data_store, - single_org: false, - #log_level: :debug - ) - server.start_background - server + start_chef_server(data_store: data_store) +end + +def pedant_args_from_env + args_from_env('PEDANT_OPTS') end -tmpdir = nil +def rspec_args_from_env + args_from_env('RSPEC_OPTS') +end + +def args_from_env(key) + return [] unless ENV[key] + ENV[key].split +end begin - if ENV['FILE_STORE'] - require 'tmpdir' - require 'chef_zero/data_store/raw_file_store' - tmpdir = Dir.mktmpdir - data_store = ChefZero::DataStore::RawFileStore.new(tmpdir, true) - data_store = ChefZero::DataStore::DefaultFacade.new(data_store, false, false) - server = ChefZero::Server.new(:port => 8889, :single_org => false, :data_store => data_store) - server.start_background - - elsif ENV['CHEF_FS'] - require 'tmpdir' - tmpdir = Dir.mktmpdir - server = start_cheffs_server(tmpdir) + tmpdir = nil + server = + if ENV['FILE_STORE'] + require 'tmpdir' + require 'chef_zero/data_store/raw_file_store' + tmpdir = Dir.mktmpdir + data_store = ChefZero::DataStore::RawFileStore.new(tmpdir, true) + data_store = ChefZero::DataStore::DefaultFacade.new(data_store, false, false) + + start_chef_server(data_store: data_store) + + elsif ENV['CHEF_FS'] + require 'tmpdir' + tmpdir = Dir.mktmpdir + start_cheffs_server(tmpdir) - else - server = ChefZero::Server.new(:port => 8889, :single_org => false)#, :log_level => :debug) - server.start_background - end + else + start_chef_server + end require 'rspec/core' require 'pedant' @@ -83,6 +124,7 @@ begin '--skip-users', '--skip-organizations', '--skip-multiuser', + '--skip-user-keys', # chef-zero has some non-removable quirks, such as the fact that files # with 255-character names cannot be stored in local mode. This is @@ -104,7 +146,8 @@ begin # are turned off" - @jkeiser # # ...but we're not there yet - '--skip-keys', + '--skip-controls', + '--skip-acl', # Chef Zero does not intend to support validation the way erchef does. '--skip-validation', @@ -143,12 +186,10 @@ begin default_skips + chef_fs_skips + %w{ --skip-knife } end - Pedant.setup(pedant_args) - - fail_fast = %w()#--fail-fast) - #fail_fast = ["--fail-fast"] + Pedant.setup(pedant_args + pedant_args_from_env) - result = RSpec::Core::Runner.run(Pedant.config.rspec_args + fail_fast) + rspec_args = Pedant.config.rspec_args + rspec_args_from_env + result = RSpec::Core::Runner.run(rspec_args) server.stop if server.running? ensure -- cgit v1.2.1