summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJordan Running <jr@getchef.com>2016-02-11 13:06:56 -0600
committerJordan Running <jr@getchef.com>2016-02-24 13:35:19 -0600
commit2469894eab12f24893916b571a981e082dfe97df (patch)
treebc657d22f7f41dad46a68f9825189c2dd20053b9
parent86e99a48cc39a0b5c931c29fbfef9e196252c9c2 (diff)
downloadchef-zero-2469894eab12f24893916b571a981e082dfe97df.tar.gz
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.
-rw-r--r--Rakefile36
-rw-r--r--lib/chef_zero/chef_data/data_normalizer.rb4
-rw-r--r--lib/chef_zero/data_store/data_error.rb7
-rw-r--r--lib/chef_zero/endpoints/actor_endpoint.rb113
-rw-r--r--lib/chef_zero/endpoints/actor_key_endpoint.rb97
-rw-r--r--lib/chef_zero/endpoints/actor_keys_endpoint.rb126
-rw-r--r--lib/chef_zero/endpoints/rest_object_endpoint.rb24
-rw-r--r--lib/chef_zero/endpoints/server_api_version_endpoint.rb2
-rw-r--r--lib/chef_zero/rest_base.rb99
-rw-r--r--lib/chef_zero/rest_error_response.rb6
-rw-r--r--lib/chef_zero/rest_router.rb61
-rw-r--r--lib/chef_zero/rspec.rb2
-rw-r--r--lib/chef_zero/server.rb8
-rw-r--r--spec/run_oc_pedant.rb105
14 files changed, 566 insertions, 124 deletions
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 = <<END
+Environment:
+ - RSPEC_OPTS Options to pass to RSpec
+ e.g. RSPEC_OPTS="--fail-fast --profile 5"
+ - PEDANT_OPTS Options to pass to oc-chef-pedant
+ e.g. PEDANT_OPTS="--focus-keys --skip-users"
+ - LOG_LEVEL Set the log level (default: warn)
+ e.g. LOG_LEVEL=debug
+END
+
task :default => :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", <org>, "client_keys", <client>, "keys" ]
+ #
+ # Or:
+ #
+ # [ "user_keys", <user>, "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