diff options
author | jkeiser <jkeiser@opscode.com> | 2012-12-23 16:13:04 -0800 |
---|---|---|
committer | jkeiser <jkeiser@opscode.com> | 2012-12-23 16:13:04 -0800 |
commit | 5ef2a78077ef11431ef1625246ff14642ac556d3 (patch) | |
tree | 7c8ff7cafb178f053750f546341ef18f98f1f9f9 | |
parent | 7d1fab82ff572fdde5a53650880bee50ae214894 (diff) | |
download | chef-zero-5ef2a78077ef11431ef1625246ff14642ac556d3.tar.gz |
Add test start/stop/data APIs, use Thin directly, use Chef::Log instead of puts
-rw-r--r-- | README.rdoc | 2 | ||||
-rwxr-xr-x | bin/chef-zero | 8 | ||||
-rw-r--r-- | lib/chef_zero/cookbook_data.rb | 105 | ||||
-rw-r--r-- | lib/chef_zero/rest_base.rb | 4 | ||||
-rw-r--r-- | lib/chef_zero/router.rb | 4 | ||||
-rw-r--r-- | lib/chef_zero/server.rb | 177 | ||||
-rw-r--r-- | test/run-pedant.rb | 2 |
7 files changed, 248 insertions, 54 deletions
diff --git a/README.rdoc b/README.rdoc index 5a1044c..1c00dcb 100644 --- a/README.rdoc +++ b/README.rdoc @@ -24,7 +24,7 @@ One of chef-zero's primary uses is as a small test server for people writing and testing clients. To bring it up, simply do: require 'chef_zero/server' - server = ChefZero::Server.new(:Port => 8889) + server = ChefZero::Server.new(:port => 8889) server.start To bring it up in the background, do: diff --git a/bin/chef-zero b/bin/chef-zero index 5f41dae..29de409 100755 --- a/bin/chef-zero +++ b/bin/chef-zero @@ -8,18 +8,18 @@ require 'chef_zero/server' require 'optparse' options = { - :Host => '127.0.0.1', - :Port => 8889, + :host => '127.0.0.1', + :port => 8889, :generate_real_keys => false } OptionParser.new do |opts| opts.banner = "Usage: chef-zero [ARGS]" opts.on("-H", "--host HOST", "Host to bind to (default: 127.0.0.1)") do |value| - options[:Host] = value + options[:host] = value end opts.on("-p", "--port PORT", Integer, "Port to listen on") do |value| - options[:Port] = value + options[:port] = value end opts.on("--[no-]generate-keys", "Whether to generate actual keys or fake it (faster). Default: false.") do |value| options[:generate_real_keys] = value diff --git a/lib/chef_zero/cookbook_data.rb b/lib/chef_zero/cookbook_data.rb new file mode 100644 index 0000000..c83afaa --- /dev/null +++ b/lib/chef_zero/cookbook_data.rb @@ -0,0 +1,105 @@ +require 'digest/md5' +require 'chef/cookbook/metadata' # for ruby metadata.rb dsl + +module ChefZero + module CookbookData + def self.to_json(cookbook, name, version=nil) + result = files_from(cookbook) + recipe_names = result[:recipes].map do |recipe| + recipe_name = recipe[:name][0..-2] + recipe_name == 'default' ? name : "#{name}::#{recipe_name}" + end + result[:metadata] = metadata_from(cookbook, name, version, recipe_names) + result[:name] = "#{name}-#{result[:metadata][:version]}" + result[:json_class] = 'Chef::CookbookVersion' + result[:cookbook_name] = name + result[:version] = result[:metadata][:version] + result[:chef_type] = 'cookbook_version' + result + end + + private + + # Just enough cookbook to make a Metadata object + class PretendCookbook + def initialize(name, fully_qualified_recipe_names) + @name = name + @fully_qualified_recipe_names = fully_qualified_recipe_names + end + attr_reader :name, :fully_qualified_recipe_names + end + + def self.metadata_from(directory, name, version, recipe_names) + metadata = Chef::Cookbook::Metadata.new(PretendCookbook.new(name, recipe_names)) + # If both .rb and .json exist, read .rb + # TODO if recipes has 3 recipes in it, and the Ruby/JSON has only one, should + # the resulting recipe list have 1, or 3-4 recipes in it? + if cookbook['metadata.rb'] + metadata.instance_eval(cookbook['metadata.rb']) + elsif cookbook['metadata.json'] + metadata.from_json(cookbook['metadata.json']) + end + metadata.to_json + end + + def self.files_from(cookbook) + # TODO some support .rb only + result = { + :attributes => load_child_files(cookbook, 'attributes', false), + :definitions => load_child_files(cookbook, 'definitions', false), + :recipes => load_child_files(cookbook, 'recipes', false), + :libraries => load_child_files(cookbook, 'libraries', false), + :templates => load_child_files(cookbook, 'templates', true, true), + :files => load_child_files(cookbook, 'files', true, true), + :resources => load_child_files(cookbook, 'resources', true), + :providers => load_child_files(cookbook, 'providers', true), + :root_files => load_files(cookbook, false) + } + set_specificity(result[:templates]) + set_specificity(result[:files]) + result + end + + def self.load_child_files(parent, key, recursive) + result = load_files(parent[key], recursive) + result.each do |file| + file[:path] = "#{key}/#{file[:path]}" + end + result + end + + def self.load_files(directory, recursive) + result = [] + directory.each_pair do |child_key, child| + if child.is_a? Hash + if recursive + result += load_child_files(directory, child_key, recursive, false) + end + else + result += load_file(child, child_key) + end + end + result.each do |file| + file[:path] = "key/#{file[:path]}" + end + result + end + + def self.load_file(value, name) + result = { + :name => name, + :path => name, + :checksum => Digest::MD5.hexdigest(value), + :specificity => 'default' + } + end + + def self.set_specificity(files) + files.each do |file| + parts = file[:path].split('/') + raise "Only directories are allowed directly under templates or files: #{file[:path]}" if parts.size == 2 + file[:specificity] = parts[1] + end + end + end +end diff --git a/lib/chef_zero/rest_base.rb b/lib/chef_zero/rest_base.rb index 068a55d..ca041b5 100644 --- a/lib/chef_zero/rest_base.rb +++ b/lib/chef_zero/rest_base.rb @@ -1,5 +1,6 @@ require 'chef_zero/rest_request' require 'chef_zero/rest_error_response' +require 'chef/log' module ChefZero class RestBase @@ -32,8 +33,7 @@ module ChefZero error(e.response_code, e.error) end rescue - puts $!.inspect - puts $!.backtrace + Chef::Log.error("#{$!.inspect}\n#{$!.backtrace}") raise end end diff --git a/lib/chef_zero/router.rb b/lib/chef_zero/router.rb index 0389c8a..1b39b79 100644 --- a/lib/chef_zero/router.rb +++ b/lib/chef_zero/router.rb @@ -1,3 +1,5 @@ +require 'chef/log' + module ChefZero class Router def initialize(routes) @@ -11,7 +13,7 @@ module ChefZero attr_accessor :not_found def call(env) - puts "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}#{env['QUERY_STRING'] != '' ? "?" + env['QUERY_STRING'] : ''}" + Chef::Log.debug "#{env['REQUEST_METHOD']} #{env['PATH_INFO']}#{env['QUERY_STRING'] != '' ? "?" + env['QUERY_STRING'] : ''}" clean_path = "/" + env['PATH_INFO'].split('/').select { |part| part != "" }.join("/") routes.each do |route, endpoint| if route.match(clean_path) diff --git a/lib/chef_zero/server.rb b/lib/chef_zero/server.rb index 78f8e14..4f6c22d 100644 --- a/lib/chef_zero/server.rb +++ b/lib/chef_zero/server.rb @@ -17,11 +17,11 @@ # require 'rubygems' -require 'webrick' -require 'rack' +require 'thin' require 'openssl' require 'chef_zero' require 'chef_zero/router' +require 'timeout' require 'chef_zero/endpoints/authenticate_user_endpoint' require 'chef_zero/endpoints/actors_endpoint' @@ -52,12 +52,13 @@ require 'chef_zero/endpoints/file_store_file_endpoint' require 'chef_zero/endpoints/not_found_endpoint' module ChefZero - class Server < Rack::Server - def initialize(options) - options[:Host] ||= "localhost" # TODO 0.0.0.0? - options[:Port] ||= 80 + class Server + def initialize(options = {}) + @options = options + options[:host] ||= '127.0.0.1' + options[:port] ||= 80 options[:generate_real_keys] = true if !options.has_key?(:generate_real_keys) - super(options) + @server = Thin::Server.new(options[:host], options[:port], make_app) @generate_real_keys = options[:generate_real_keys] @data = { 'clients' => { @@ -79,49 +80,43 @@ module ChefZero } end + attr_reader :server attr_reader :data + attr_reader :options attr_reader :generate_real_keys include ChefZero::Endpoints - def app - @app ||= begin - router = Router.new([ - [ '/authenticate_user', AuthenticateUserEndpoint.new(self) ], - [ '/clients', ActorsEndpoint.new(self) ], - [ '/clients/*', ActorEndpoint.new(self) ], - [ '/cookbooks', CookbooksEndpoint.new(self) ], - [ '/cookbooks/*', CookbookEndpoint.new(self) ], - [ '/cookbooks/*/*', CookbookVersionEndpoint.new(self) ], - [ '/data', DataBagsEndpoint.new(self) ], - [ '/data/*', DataBagEndpoint.new(self) ], - [ '/data/*/*', DataBagItemEndpoint.new(self) ], - [ '/environments', RestListEndpoint.new(self) ], - [ '/environments/*', EnvironmentEndpoint.new(self) ], - [ '/environments/*/cookbooks', EnvironmentCookbooksEndpoint.new(self) ], - [ '/environments/*/cookbooks/*', EnvironmentCookbookEndpoint.new(self) ], - [ '/environments/*/cookbook_versions', EnvironmentCookbookVersionsEndpoint.new(self) ], - [ '/environments/*/nodes', EnvironmentNodesEndpoint.new(self) ], - [ '/environments/*/recipes', EnvironmentRecipesEndpoint.new(self) ], - [ '/environments/*/roles/*', EnvironmentRoleEndpoint.new(self) ], - [ '/nodes', RestListEndpoint.new(self) ], - [ '/nodes/*', NodeEndpoint.new(self) ], - [ '/principals/*', PrincipalEndpoint.new(self) ], - [ '/roles', RestListEndpoint.new(self) ], - [ '/roles/*', RoleEndpoint.new(self) ], - [ '/roles/*/environments', RoleEnvironmentsEndpoint.new(self) ], - [ '/roles/*/environments/*', EnvironmentRoleEndpoint.new(self) ], - [ '/sandboxes', SandboxesEndpoint.new(self) ], - [ '/sandboxes/*', SandboxEndpoint.new(self) ], - [ '/search', SearchesEndpoint.new(self) ], - [ '/search/*', SearchEndpoint.new(self) ], - [ '/users', ActorsEndpoint.new(self) ], - [ '/users/*', ActorEndpoint.new(self) ], - - [ '/file_store/*', FileStoreFileEndpoint.new(self) ], - ]) - router.not_found = NotFoundEndpoint.new - router + def url + "http://#{options[:host]}:#{options[:port]}" + end + + def start + server.start + end + + def start_background(timeout = 5) + @thread = Thread.new do + server.start + end + Timeout::timeout(timeout) do + until server.running? + sleep(0.01) + end + end + end + + def running? + server.running? + end + + def stop(timeout = 5) + Timeout::timeout(timeout) do + server.stop + end + if @thread + @thread.kill + @thread = nil end end @@ -136,5 +131,97 @@ module ChefZero [PRIVATE_KEY, PUBLIC_KEY] end end + + # Load data in a nice, friendly form: + # { + # 'roles' => { + # 'desert' => '{ "description": "Hot and dry"' }, + # 'rainforest' => { "description" => 'Wet and humid' } + # }, + # 'cookbooks' => { + # 'apache2-1.0.1' => { + # 'templates' => { 'default' => { 'blah.txt' => 'hi' }} + # 'recipes' => { 'default.rb' => 'template "blah.txt"' } + # 'metadata.rb' => 'depends "mysql"' + # }, + # 'apache2-1.2.0' => { + # 'templates' => { 'default' => { 'blah.txt' => 'lo' }} + # 'recipes' => { 'default.rb' => 'template "blah.txt"' } + # 'metadata.rb' => 'depends "mysql"' + # }, + # 'mysql' => { + # 'recipes' => { 'default.rb' => 'file { contents "hi" }' }, + # 'metadata.rb' => 'version "1.0.0"' + # } + # } + # } + def load_data(contents) + %w(clients data environments nodes roles users).each do |data_type| + server.data[data_type].merge!(contents[data_type]) if contents[data_type] + end + if contents['cookbooks'] + contents['cookbooks'].each_pair do |name_version, cookbook| + if name_version =~ /(.+)-(\d+\.\d+\.\d+)$/ + cookbook_data = CookbookData.to_json(cookbook, $1, $2) + else + cookbook_data = CookbookData.to_json(cookbook, name_version) + end + server.data['cookbooks'][cookbook_data['cookbook_name']][cookbook_data['version']] = cookbook_data + cookbook_data.each do |files| + next unless files.is_a? Array + server.data['file_store'][file[:checksum]] = get_file(cookbook, file[:path]) + end + end + end + end + + private + + def make_app + router = Router.new([ + [ '/authenticate_user', AuthenticateUserEndpoint.new(self) ], + [ '/clients', ActorsEndpoint.new(self) ], + [ '/clients/*', ActorEndpoint.new(self) ], + [ '/cookbooks', CookbooksEndpoint.new(self) ], + [ '/cookbooks/*', CookbookEndpoint.new(self) ], + [ '/cookbooks/*/*', CookbookVersionEndpoint.new(self) ], + [ '/data', DataBagsEndpoint.new(self) ], + [ '/data/*', DataBagEndpoint.new(self) ], + [ '/data/*/*', DataBagItemEndpoint.new(self) ], + [ '/environments', RestListEndpoint.new(self) ], + [ '/environments/*', EnvironmentEndpoint.new(self) ], + [ '/environments/*/cookbooks', EnvironmentCookbooksEndpoint.new(self) ], + [ '/environments/*/cookbooks/*', EnvironmentCookbookEndpoint.new(self) ], + [ '/environments/*/cookbook_versions', EnvironmentCookbookVersionsEndpoint.new(self) ], + [ '/environments/*/nodes', EnvironmentNodesEndpoint.new(self) ], + [ '/environments/*/recipes', EnvironmentRecipesEndpoint.new(self) ], + [ '/environments/*/roles/*', EnvironmentRoleEndpoint.new(self) ], + [ '/nodes', RestListEndpoint.new(self) ], + [ '/nodes/*', NodeEndpoint.new(self) ], + [ '/principals/*', PrincipalEndpoint.new(self) ], + [ '/roles', RestListEndpoint.new(self) ], + [ '/roles/*', RoleEndpoint.new(self) ], + [ '/roles/*/environments', RoleEnvironmentsEndpoint.new(self) ], + [ '/roles/*/environments/*', EnvironmentRoleEndpoint.new(self) ], + [ '/sandboxes', SandboxesEndpoint.new(self) ], + [ '/sandboxes/*', SandboxEndpoint.new(self) ], + [ '/search', SearchesEndpoint.new(self) ], + [ '/search/*', SearchEndpoint.new(self) ], + [ '/users', ActorsEndpoint.new(self) ], + [ '/users/*', ActorEndpoint.new(self) ], + + [ '/file_store/*', FileStoreFileEndpoint.new(self) ], + ]) + router.not_found = NotFoundEndpoint.new + router + end + + def get_file(directory, path) + value = directory + path.split('/').each do |part| + value = value[part] + end + value + end end end diff --git a/test/run-pedant.rb b/test/run-pedant.rb index cc23414..a454508 100644 --- a/test/run-pedant.rb +++ b/test/run-pedant.rb @@ -5,7 +5,7 @@ $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))) require 'chef_zero/server' thread = Thread.new do - server = ChefZero::Server.new(:Port => 8889) + server = ChefZero::Server.new(:port => 8889) server.start end |