chef-zero-2.0.1/0000755000175000017500000000000012263533061013621 5ustar stefanorstefanorchef-zero-2.0.1/metadata.yml0000644000175000017500000001362112263533061016127 0ustar stefanorstefanor--- !ruby/object:Gem::Specification name: chef-zero version: !ruby/object:Gem::Version version: 2.0.1 platform: ruby authors: - John Keiser autorequire: bindir: bin cert_chain: [] date: 2014-01-03 00:00:00.000000000 Z dependencies: - !ruby/object:Gem::Dependency name: mixlib-log requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '1.3' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '1.3' - !ruby/object:Gem::Dependency name: hashie requirement: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '2.0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - ~> - !ruby/object:Gem::Version version: '2.0' - !ruby/object:Gem::Dependency name: moneta requirement: !ruby/object:Gem::Requirement requirements: - - < - !ruby/object:Gem::Version version: 0.7.0 type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - < - !ruby/object:Gem::Version version: 0.7.0 - !ruby/object:Gem::Dependency name: json requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rack requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :runtime prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rake requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' - !ruby/object:Gem::Dependency name: rspec requirement: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' type: :development prerelease: false version_requirements: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' description: Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes email: jkeiser@opscode.com executables: - chef-zero extensions: [] extra_rdoc_files: [] files: - LICENSE - README.md - Rakefile - bin/chef-zero - lib/chef_zero.rb - lib/chef_zero/cookbook_data.rb - lib/chef_zero/data_normalizer.rb - lib/chef_zero/data_store/data_already_exists_error.rb - lib/chef_zero/data_store/data_error.rb - lib/chef_zero/data_store/data_not_found_error.rb - lib/chef_zero/data_store/memory_store.rb - lib/chef_zero/endpoints/actor_endpoint.rb - lib/chef_zero/endpoints/actors_endpoint.rb - lib/chef_zero/endpoints/authenticate_user_endpoint.rb - lib/chef_zero/endpoints/cookbook_endpoint.rb - lib/chef_zero/endpoints/cookbook_version_endpoint.rb - lib/chef_zero/endpoints/cookbooks_base.rb - lib/chef_zero/endpoints/cookbooks_endpoint.rb - lib/chef_zero/endpoints/data_bag_endpoint.rb - lib/chef_zero/endpoints/data_bag_item_endpoint.rb - lib/chef_zero/endpoints/data_bags_endpoint.rb - lib/chef_zero/endpoints/environment_cookbook_endpoint.rb - lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb - lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb - lib/chef_zero/endpoints/environment_endpoint.rb - lib/chef_zero/endpoints/environment_nodes_endpoint.rb - lib/chef_zero/endpoints/environment_recipes_endpoint.rb - lib/chef_zero/endpoints/environment_role_endpoint.rb - lib/chef_zero/endpoints/file_store_file_endpoint.rb - lib/chef_zero/endpoints/node_endpoint.rb - lib/chef_zero/endpoints/not_found_endpoint.rb - lib/chef_zero/endpoints/principal_endpoint.rb - lib/chef_zero/endpoints/rest_list_endpoint.rb - lib/chef_zero/endpoints/rest_object_endpoint.rb - lib/chef_zero/endpoints/role_endpoint.rb - lib/chef_zero/endpoints/role_environments_endpoint.rb - lib/chef_zero/endpoints/sandbox_endpoint.rb - lib/chef_zero/endpoints/sandboxes_endpoint.rb - lib/chef_zero/endpoints/search_endpoint.rb - lib/chef_zero/endpoints/searches_endpoint.rb - lib/chef_zero/log.rb - lib/chef_zero/rest_base.rb - lib/chef_zero/rest_error_response.rb - lib/chef_zero/rest_request.rb - lib/chef_zero/rest_router.rb - lib/chef_zero/rspec.rb - lib/chef_zero/server.rb - lib/chef_zero/solr/query/binary_operator.rb - lib/chef_zero/solr/query/phrase.rb - lib/chef_zero/solr/query/range_query.rb - lib/chef_zero/solr/query/regexpable_query.rb - lib/chef_zero/solr/query/subquery.rb - lib/chef_zero/solr/query/term.rb - lib/chef_zero/solr/query/unary_operator.rb - lib/chef_zero/solr/solr_doc.rb - lib/chef_zero/solr/solr_parser.rb - lib/chef_zero/version.rb - spec/run.rb - spec/search_spec.rb - spec/support/pedant.rb - spec/support/stickywicket.pem homepage: http://www.opscode.com licenses: - Apache 2.0 metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - '>=' - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.2.0 signing_key: specification_version: 4 summary: Self-contained, easy-setup, fast-start in-memory Chef server for testing and solo setup purposes test_files: [] has_rdoc: chef-zero-2.0.1/lib/0000755000175000017500000000000012263533061014367 5ustar stefanorstefanorchef-zero-2.0.1/lib/chef_zero.rb0000644000175000017500000000660512263533061016667 0ustar stefanorstefanormodule ChefZero require 'chef_zero/log' CERTIFICATE = "-----BEGIN CERTIFICATE-----\nMIIDMzCCApygAwIBAgIBATANBgkqhkiG9w0BAQUFADCBnjELMAkGA1UEBhMCVVMx\nEzARBgNVBAgMCldhc2hpbmd0b24xEDAOBgNVBAcMB1NlYXR0bGUxFjAUBgNVBAoM\nDU9wc2NvZGUsIEluYy4xHDAaBgNVBAsME0NlcnRpZmljYXRlIFNlcnZpY2UxMjAw\nBgNVBAMMKW9wc2NvZGUuY29tL2VtYWlsQWRkcmVzcz1hdXRoQG9wc2NvZGUuY29t\nMB4XDTEyMTEyMTAwMzQyMVoXDTIyMTExOTAwMzQyMVowgZsxEDAOBgNVBAcTB1Nl\nYXR0bGUxEzARBgNVBAgTCldhc2hpbmd0b24xCzAJBgNVBAYTAlVTMRwwGgYDVQQL\nExNDZXJ0aWZpY2F0ZSBTZXJ2aWNlMRYwFAYDVQQKEw1PcHNjb2RlLCBJbmMuMS8w\nLQYDVQQDFCZVUkk6aHR0cDovL29wc2NvZGUuY29tL0dVSURTL3VzZXJfZ3VpZDCC\nASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANLDmPbR71bS2esZlZh/HfC6\n0azXFjl2677wq2ovk9xrUb0Ui4ZLC66TqQ9C/RBzOjXU4TRf3hgPTqvlCgHusl0d\nIcLCrsSl6kPEhJpYWWfRoroIAwf82A9yLQekhqXZEXu5EKkwoUMqyF6m0ZCasaE1\ny8niQxdLAsk3ady/CGQlFqHTPKFfU5UASR2LRtYC1MCIvJHDFRKAp9kPJbQo9P37\nZ8IU7cDudkZFgNLmDixlWsh7C0ghX8fgAlj1P6FgsFufygam973k79GhIP54dELB\nc0S6E8ekkRSOXU9jX/IoiXuFglBvFihAdhvED58bMXzj2AwXUyeAlxItnvs+NVUC\nAwEAATANBgkqhkiG9w0BAQUFAAOBgQBkFZRbMoywK3hb0/X7MXmPYa7nlfnd5UXq\nr2n32ettzZNmEPaI2d1j+//nL5qqhOlrWPS88eKEPnBOX/jZpUWOuAAddnrvFzgw\nrp/C2H7oMT+29F+5ezeViLKbzoFYb4yECHBoi66IFXNae13yj7taMboBeUmE664G\nTB/MZpRr8g==\n-----END CERTIFICATE-----\n" PUBLIC_KEY = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0sOY9tHvVtLZ6xmVmH8d\n8LrRrNcWOXbrvvCrai+T3GtRvRSLhksLrpOpD0L9EHM6NdThNF/eGA9Oq+UKAe6y\nXR0hwsKuxKXqQ8SEmlhZZ9GiuggDB/zYD3ItB6SGpdkRe7kQqTChQyrIXqbRkJqx\noTXLyeJDF0sCyTdp3L8IZCUWodM8oV9TlQBJHYtG1gLUwIi8kcMVEoCn2Q8ltCj0\n/ftnwhTtwO52RkWA0uYOLGVayHsLSCFfx+ACWPU/oWCwW5/KBqb3veTv0aEg/nh0\nQsFzRLoTx6SRFI5dT2Nf8iiJe4WCUG8WKEB2G8QPnxsxfOPYDBdTJ4CXEi2e+z41\nVQIDAQAB\n-----END PUBLIC KEY-----\n" PRIVATE_KEY = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA0sOY9tHvVtLZ6xmVmH8d8LrRrNcWOXbrvvCrai+T3GtRvRSL\nhksLrpOpD0L9EHM6NdThNF/eGA9Oq+UKAe6yXR0hwsKuxKXqQ8SEmlhZZ9GiuggD\nB/zYD3ItB6SGpdkRe7kQqTChQyrIXqbRkJqxoTXLyeJDF0sCyTdp3L8IZCUWodM8\noV9TlQBJHYtG1gLUwIi8kcMVEoCn2Q8ltCj0/ftnwhTtwO52RkWA0uYOLGVayHsL\nSCFfx+ACWPU/oWCwW5/KBqb3veTv0aEg/nh0QsFzRLoTx6SRFI5dT2Nf8iiJe4WC\nUG8WKEB2G8QPnxsxfOPYDBdTJ4CXEi2e+z41VQIDAQABAoIBAALhqbW2KQ+G0nPk\nZacwFbi01SkHx8YBWjfCEpXhEKRy0ytCnKW5YO+CFU2gHNWcva7+uhV9OgwaKXkw\nKHLeUJH1VADVqI4Htqw2g5mYm6BPvWnNsjzpuAp+BR+VoEGkNhj67r9hatMAQr0I\nitTvSH5rvd2EumYXIHKfz1K1SegUk1u1EL1RcMzRmZe4gDb6eNBs9Sg4im4ybTG6\npPIytA8vBQVWhjuAR2Tm+wZHiy0Az6Vu7c2mS07FSX6FO4E8SxWf8idaK9ijMGSq\nFvIS04mrY6XCPUPUC4qm1qNnhDPpOr7CpI2OO98SqGanStS5NFlSFXeXPpM280/u\nfZUA0AECgYEA+x7QUnffDrt7LK2cX6wbvn4mRnFxet7bJjrfWIHf+Rm0URikaNma\nh0/wNKpKBwIH+eHK/LslgzcplrqPytGGHLOG97Gyo5tGAzyLHUWBmsNkRksY2sPL\nuHq6pYWJNkqhnWGnIbmqCr0EWih82x/y4qxbJYpYqXMrit0wVf7yAgkCgYEA1twI\ngFaXqesetTPoEHSQSgC8S4D5/NkdriUXCYb06REcvo9IpFMuiOkVUYNN5d3MDNTP\nIdBicfmvfNELvBtXDomEUD8ls1UuoTIXRNGZ0VsZXu7OErXCK0JKNNyqRmOwcvYL\nJRqLfnlei5Ndo1lu286yL74c5rdTLs/nI2p4e+0CgYB079ZmcLeILrmfBoFI8+Y/\ngJLmPrFvXBOE6+lRV7kqUFPtZ6I3yQzyccETZTDvrnx0WjaiFavUPH27WMjY01S2\nTMtO0Iq1MPsbSrglO1as8MvjB9ldFcvp7gy4Q0Sv6XT0yqJ/S+vo8Df0m+H4UBpU\nf5o6EwBSd/UQxwtZIE0lsQKBgQCswfjX8Eg8KL/lJNpIOOE3j4XXE9ptksmJl2sB\njxDnQYoiMqVO808saHVquC/vTrpd6tKtNpehWwjeTFuqITWLi8jmmQ+gNTKsC9Gn\n1Pxf2Gb67PqnEpwQGln+TRtgQ5HBrdHiQIi+5am+gnw89pDrjjO5rZwhanAo6KPJ\n1zcPNQKBgQDxFu8v4frDmRNCVaZS4f1B6wTrcMrnibIDlnzrK9GG6Hz1U7dDv8s8\nNf4UmeMzDXjlPWZVOvS5+9HKJPdPj7/onv8B2m18+lcgTTDJBkza7R1mjL1Cje/Z\nKcVGsryKN6cjE7yCDasnA7R2rVBV/7NWeJV77bmzT5O//rW4yIfUIg==\n-----END RSA PRIVATE KEY-----\n" end chef-zero-2.0.1/lib/chef_zero/0000755000175000017500000000000012263533061016333 5ustar stefanorstefanorchef-zero-2.0.1/lib/chef_zero/rest_error_response.rb0000644000175000017500000000034212263533061022763 0ustar stefanorstefanormodule ChefZero class RestErrorResponse < Exception def initialize(response_code, error) @response_code = response_code @error = error end attr_reader :response_code attr_reader :error end end chef-zero-2.0.1/lib/chef_zero/rest_router.rb0000644000175000017500000000223112263533061021233 0ustar stefanorstefanormodule ChefZero class RestRouter def initialize(routes) @routes = routes.map do |route, endpoint| if route =~ /\*\*$/ pattern = Regexp.new("^#{route[0..-3].gsub('*', '[^/]*')}") else pattern = Regexp.new("^#{route.gsub('*', '[^/]*')}$") end [ pattern, endpoint ] end end attr_reader :routes attr_accessor :not_found def call(request) begin ChefZero::Log.debug(request) 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")}"] end end private def find_endpoint(clean_path) _, endpoint = routes.find { |route, endpoint| route.match(clean_path) } endpoint || not_found end end end chef-zero-2.0.1/lib/chef_zero/data_normalizer.rb0000644000175000017500000001166612263533061022045 0ustar stefanorstefanorrequire 'chef_zero' require 'chef_zero/rest_base' module ChefZero class DataNormalizer def self.normalize_client(client, name) client['name'] ||= name client['admin'] ||= false client['public_key'] ||= PUBLIC_KEY client['validator'] ||= false client['json_class'] ||= "Chef::ApiClient" client['chef_type'] ||= "client" client end def self.normalize_user(user, name) user['name'] ||= name user['admin'] ||= false user['public_key'] ||= PUBLIC_KEY user end def self.normalize_data_bag_item(data_bag_item, data_bag_name, id, method) if method == 'DELETE' # TODO SERIOUSLY, WHO DOES THIS MANY EXCEPTIONS IN THEIR INTERFACE if !(data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data']) data_bag_item['id'] ||= id data_bag_item = { 'raw_data' => data_bag_item } data_bag_item['chef_type'] ||= 'data_bag_item' data_bag_item['json_class'] ||= 'Chef::DataBagItem' data_bag_item['data_bag'] ||= data_bag_name data_bag_item['name'] ||= "data_bag_item_#{data_bag_name}_#{id}" end else # If it's not already wrapped with raw_data, wrap it. if data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data'] data_bag_item = data_bag_item['raw_data'] end # Argh. We don't do this on GET, but we do on PUT and POST???? if %w(PUT POST).include?(method) data_bag_item['chef_type'] ||= 'data_bag_item' data_bag_item['data_bag'] ||= data_bag_name end data_bag_item['id'] ||= id end data_bag_item end def self.normalize_environment(environment, name) environment['name'] ||= name environment['description'] ||= '' environment['cookbook_versions'] ||= {} environment['json_class'] ||= "Chef::Environment" environment['chef_type'] ||= "environment" environment['default_attributes'] ||= {} environment['override_attributes'] ||= {} environment end def self.normalize_cookbook(cookbook, name, version, base_uri, method) # TODO I feel dirty if method != 'PUT' cookbook.each_pair do |key, value| if value.is_a?(Array) value.each do |file| if file.is_a?(Hash) && file.has_key?('checksum') file['url'] ||= RestBase::build_uri(base_uri, ['file_store', 'checksums', file['checksum']]) end end end end cookbook['name'] ||= "#{name}-#{version}" # TODO this feels wrong, but the real chef server doesn't expand this default # cookbook['version'] ||= version cookbook['cookbook_name'] ||= name cookbook['frozen?'] ||= false cookbook['metadata'] ||= {} cookbook['metadata']['version'] ||= version # Sad to not be expanding defaults just because Chef doesn't :( # cookbook['metadata']['name'] ||= name # cookbook['metadata']['description'] ||= "A fabulous new cookbook" cookbook['metadata']['long_description'] ||= "" # cookbook['metadata']['maintainer'] ||= "YOUR_COMPANY_NAME" # cookbook['metadata']['maintainer_email'] ||= "YOUR_EMAIL" # cookbook['metadata']['license'] ||= "none" cookbook['metadata']['dependencies'] ||= {} cookbook['metadata']['attributes'] ||= {} cookbook['metadata']['recipes'] ||= {} end cookbook['json_class'] ||= 'Chef::CookbookVersion' cookbook['chef_type'] ||= 'cookbook_version' if method == 'MIN' cookbook['metadata'].delete('attributes') cookbook['metadata'].delete('long_description') end cookbook end def self.normalize_node(node, name) node['name'] ||= name node['json_class'] ||= 'Chef::Node' node['chef_type'] ||= 'node' node['chef_environment'] ||= '_default' node['override'] ||= {} node['normal'] ||= {} node['default'] ||= {} node['automatic'] ||= {} node['run_list'] ||= [] node['run_list'] = normalize_run_list(node['run_list']) node end def self.normalize_role(role, name) role['name'] ||= name role['description'] ||= '' role['json_class'] ||= 'Chef::Role' role['chef_type'] ||= 'role' role['default_attributes'] ||= {} role['override_attributes'] ||= {} role['run_list'] ||= [] role['run_list'] = normalize_run_list(role['run_list']) role['env_run_lists'] ||= {} role['env_run_lists'].each_pair do |env, run_list| role['env_run_lists'][env] = normalize_run_list(run_list) end role end def self.normalize_run_list(run_list) run_list.map{|item| case item when /^recipe\[.*\]$/ item # explicit recipe when /^role\[.*\]$/ item # explicit role else "recipe[#{item}]" end }.uniq end end end chef-zero-2.0.1/lib/chef_zero/solr/0000755000175000017500000000000012263533061017312 5ustar stefanorstefanorchef-zero-2.0.1/lib/chef_zero/solr/solr_doc.rb0000644000175000017500000000260612263533061021447 0ustar stefanorstefanormodule ChefZero module Solr # This does what expander does, flattening the json doc into keys and values # so that solr can search them. class SolrDoc def initialize(json, id) @json = json @id = id end def [](key) matching_values { |match_key| match_key == key } end def matching_values(&block) result = [] key_values(nil, @json) do |key, value| if block.call(key) result << value.to_s end end # Handle manufactured value(s) if block.call('X_CHEF_id_CHEF_X') result << @id.to_s end result.uniq end private def key_values(key_so_far, value, &block) if value.is_a?(Hash) value.each_pair do |child_key, child_value| block.call(child_key, child_value.to_s) if key_so_far new_key = "#{key_so_far}_#{child_key}" key_values(new_key, child_value, &block) else key_values(child_key, child_value, &block) if child_value.is_a?(Hash) || child_value.is_a?(Array) end end elsif value.is_a?(Array) value.each do |child_value| key_values(key_so_far, child_value, &block) end else block.call(key_so_far || 'text', value.to_s) end end end end end chef-zero-2.0.1/lib/chef_zero/solr/solr_parser.rb0000644000175000017500000001371012263533061022174 0ustar stefanorstefanorrequire 'chef_zero/solr/query/binary_operator' require 'chef_zero/solr/query/unary_operator' require 'chef_zero/solr/query/term' require 'chef_zero/solr/query/phrase' require 'chef_zero/solr/query/range_query' require 'chef_zero/solr/query/subquery' module ChefZero module Solr class SolrParser def initialize(query_string) @query_string = query_string @index = 0 end def parse read_expression end # # Tokenization # def peek_token @next_token ||= parse_token end def next_token result = peek_token @next_token = nil result end def parse_token # Skip whitespace skip_whitespace return nil if eof? # Operators operator = peek_operator_token if operator @index+=operator.length operator else # Everything that isn't whitespace or an operator, is part of a term # (characters plus backslashed escaped characters) start_index = @index begin if @query_string[@index] == '\\' @index+=1 end @index+=1 if !eof? end while !eof? && peek_term_token @query_string[start_index..@index-1] end end def skip_whitespace if @query_string[@index] =~ /\s/ whitespace = /\s+/.match(@query_string, @index) || peek @index += whitespace[0].length end end def peek_term_token return nil if @query_string[@index] =~ /\s/ op = peek_operator_token return !op || op == '-' end def peek_operator_token if ['"', '+', '-', '!', '(', ')', '{', '}', '[', ']', '^', ':'].include?(@query_string[@index]) return @query_string[@index] else result = @query_string[@index..@index+1] if ['&&', '||'].include?(result) return result end end nil end def eof? !@next_token && @index >= @query_string.length end # Parse tree creation def read_expression result = read_single_expression # Expression is over when we hit a close paren or eof # (peek_token has the side effect of skipping whitespace for us, so we # really know if we're at eof or not) until peek_token == ')' || eof? operator = peek_token if binary_operator?(operator) next_token else # If 2 terms are next to each other, the default operator is OR operator = 'OR' end next_expression = read_single_expression # Build the operator, taking precedence into account if result.is_a?(Query::BinaryOperator) && binary_operator_precedence(operator) > binary_operator_precedence(result.operator) # a+b*c -> a+(b*c) new_right = Query::BinaryOperator.new(result.right, operator, next_expression) result = Query::BinaryOperator.new(result.left, result.operator, new_right) else # a*b+c -> (a*b)+c result = Query::BinaryOperator.new(result, operator, next_expression) end end result end def parse_error(token, str) raise "Error on token '#{token}' at #{@index} of '#{@query_string}': #{str}" end def read_single_expression token = next_token # If EOF, we have a problem Houston if !token parse_error(nil, "Expected expression!") # If it's an unary operand, build that elsif unary_operator?(token) operand = read_single_expression # TODO We rely on all unary operators having higher precedence than all # binary operators. Check if this is the case. Query::UnaryOperator.new(token, operand) # If it's the start of a phrase, read the terms in the phrase elsif token == '"' # Read terms until close " phrase_terms = [] until (term = next_token) == '"' phrase_terms << Query::Term.new(term) end Query::Phrase.new(phrase_terms) # If it's the start of a range query, build that elsif token == '{' || token == '[' left = next_token parse_error(left, "Expected left term in range query") if !left to = next_token parse_error(left, "Expected TO in range query") if to != "TO" right = next_token parse_error(right, "Expected left term in range query") if !right end_range = next_token parse_error(right, "Expected end range '#{end_range}") if !['}', ']'].include?(end_range) Query::RangeQuery.new(left, right, token == '[', end_range == ']') elsif token == '(' subquery = read_expression close_paren = next_token parse_error(close_paren, "Expected ')'") if close_paren != ')' Query::Subquery.new(subquery) # If it's the end of a closure, raise an exception elsif ['}',']',')'].include?(token) parse_error(token, "Unexpected end paren") # If it's a binary operator, raise an exception elsif binary_operator?(token) parse_error(token, "Unexpected binary operator") # Otherwise it's a term. else term = Query::Term.new(token) if peek_token == ':' Query::BinaryOperator.new(term, next_token, read_single_expression) else term end end end def unary_operator?(token) [ 'NOT', '+', '-' ].include?(token) end def binary_operator?(token) [ 'AND', 'OR', '^', ':'].include?(token) end def binary_operator_precedence(token) case token when '^' 4 when ':' 3 when 'AND' 2 when 'OR' 1 end end DEFAULT_FIELD = 'text' end end end chef-zero-2.0.1/lib/chef_zero/solr/query/0000755000175000017500000000000012263533061020457 5ustar stefanorstefanorchef-zero-2.0.1/lib/chef_zero/solr/query/phrase.rb0000644000175000017500000000113212263533061022263 0ustar stefanorstefanorrequire 'chef_zero/solr/query/regexpable_query' module ChefZero module Solr module Query class Phrase < RegexpableQuery def initialize(terms) # Phrase is terms separated by whitespace if terms.size == 0 && terms[0].literal_string literal_string = terms[0].literal_string else literal_string = nil end super(terms.map { |term| term.regexp_string }.join("#{NON_WORD_CHARACTER}+"), literal_string) end def to_s "Phrase(\"#{@regexp_string}\")" end end end end end chef-zero-2.0.1/lib/chef_zero/solr/query/term.rb0000644000175000017500000000245512263533061021761 0ustar stefanorstefanorrequire 'chef_zero/solr/query/regexpable_query' module ChefZero module Solr module Query class Term < RegexpableQuery def initialize(term) # Get rid of escape characters, turn * and ? into .* and . for regex, and # escape everything that needs escaping literal_string = "" regexp_string = "" index = 0 while index < term.length if term[index] == '*' regexp_string << "#{WORD_CHARACTER}*" literal_string = nil index += 1 elsif term[index] == '?' regexp_string << WORD_CHARACTER literal_string = nil index += 1 elsif term[index] == '~' raise "~ unsupported" else if term[index] == '\\' index = index+1 if index >= term.length raise "Backslash at end of string '#{term}'" end end literal_string << term[index] if literal_string regexp_string << Regexp.escape(term[index]) index += 1 end end super(regexp_string, literal_string) end def to_s "Term(#{regexp_string})" end end end end end chef-zero-2.0.1/lib/chef_zero/solr/query/range_query.rb0000644000175000017500000000210612263533061023324 0ustar stefanorstefanormodule ChefZero module Solr module Query class RangeQuery def initialize(from, to, from_inclusive, to_inclusive) @from = from @to = to @from_inclusive = from_inclusive @to_inclusive = to_inclusive end def to_s "#{@from_inclusive ? '[' : '{'}#{@from} TO #{@to}#{@to_inclusive ? ']' : '}'}" end def matches_values?(values) values.any? do |value| unless @from == '*' case @from <=> value when -1 return false when 0 return false if !@from_inclusive end end unless @to == '*' case value <=> @to when 1 return false when 0 return false if !@to_inclusive end end return true end end def matches_doc?(doc) matches_values?(doc[DEFAULT_FIELD]) end DEFAULT_FIELD = "text" end end end end chef-zero-2.0.1/lib/chef_zero/solr/query/regexpable_query.rb0000644000175000017500000000150412263533061024347 0ustar stefanorstefanormodule ChefZero module Solr module Query class RegexpableQuery def initialize(regexp_string, literal_string) @regexp_string = regexp_string # Surround the regexp with word boundaries @regexp = Regexp.new("(^|#{NON_WORD_CHARACTER})#{regexp_string}($|#{NON_WORD_CHARACTER})", true) @literal_string = literal_string end attr_reader :literal_string attr_reader :regexp_string attr_reader :regexp def matches_doc?(doc) matches_values?(doc[DEFAULT_FIELD]) end def matches_values?(values) values.any? { |value| !@regexp.match(value).nil? } end DEFAULT_FIELD = "text" WORD_CHARACTER = "[A-Za-z0-9@._':]" NON_WORD_CHARACTER = "[^A-Za-z0-9@._':]" end end end end chef-zero-2.0.1/lib/chef_zero/solr/query/subquery.rb0000644000175000017500000000117312263533061022665 0ustar stefanorstefanormodule ChefZero module Solr module Query class Subquery def initialize(subquery) @subquery = subquery end attr_reader :subquery def to_s "(#{subquery})" end def literal_string subquery.literal_string end def regexp subquery.regexp end def regexp_string subquery.regexp_string end def matches_doc?(doc) subquery.matches_doc?(doc) end def matches_values?(values) subquery.matches_values?(values) end end end end end chef-zero-2.0.1/lib/chef_zero/solr/query/unary_operator.rb0000644000175000017500000000220412263533061024053 0ustar stefanorstefanormodule ChefZero module Solr module Query class UnaryOperator def initialize(operator, operand) @operator = operator @operand = operand end def to_s "#{operator} #{operand}" end attr_reader :operator attr_reader :operand def matches_doc?(doc) case @operator when '-' when 'NOT' !operand.matches_doc?(doc) when '+' # TODO This operator uses relevance to eliminate other, unrelated # expressions. +a OR b means "if it has b but not a, don't return it" raise "+ not supported yet, because it is hard." end end def matches_values?(values) case @operator when '-' when 'NOT' !operand.matches_values?(values) when '+' # TODO This operator uses relevance to eliminate other, unrelated # expressions. +a OR b means "if it has b but not a, don't return it" raise "+ not supported yet, because it is hard." end end end end end end chef-zero-2.0.1/lib/chef_zero/solr/query/binary_operator.rb0000644000175000017500000000257312263533061024212 0ustar stefanorstefanormodule ChefZero module Solr module Query class BinaryOperator def initialize(left, operator, right) @left = left @operator = operator @right = right end def to_s "(#{left} #{operator} #{right})" end attr_reader :left attr_reader :operator attr_reader :right def matches_doc?(doc) case @operator when 'AND' left.matches_doc?(doc) && right.matches_doc?(doc) when 'OR' left.matches_doc?(doc) || right.matches_doc?(doc) when '^' left.matches_doc?(doc) when ':' if left.respond_to?(:literal_string) && left.literal_string values = doc[left.literal_string] else values = doc.matching_values { |key| left.matches_values?([key]) } end right.matches_values?(values) end end def matches_values?(values) case @operator when 'AND' left.matches_values?(values) && right.matches_values?(values) when 'OR' left.matches_values?(values) || right.matches_values?(values) when '^' left.matches_values?(values) when ':' raise ": does not work inside a : or term" end end end end end end chef-zero-2.0.1/lib/chef_zero/rspec.rb0000644000175000017500000000747012263533061020004 0ustar stefanorstefanorrequire 'tempfile' require 'chef_zero/server' require 'chef_zero/rest_request' module ChefZero module RSpec def self.server @server end def self.server=(value) @server = value end def self.client_key @client_key end def self.client_key=(value) @client_key = value end def self.request_log @request_log ||= [] end def self.clear_request_log @request_log = [] end def when_the_chef_server(description, *tags, &block) context "When the Chef server #{description}", *tags do before :each do unless ChefZero::RSpec.server default_opts = {:port => 8889, :signals => false, :log_requests => true} server_opts = if self.respond_to?(:chef_zero_opts) default_opts.merge(chef_zero_opts) else default_opts end # TODO: can this be logged easily? # pp :zero_opts => server_opts # Set up configuration so that clients will point to the server ChefZero::RSpec.server = ChefZero::Server.new(server_opts) ChefZero::RSpec.client_key = Tempfile.new(['chef_zero_client_key', '.pem']) ChefZero::RSpec.client_key.write(ChefZero::PRIVATE_KEY) ChefZero::RSpec.client_key.close # Start the server ChefZero::RSpec.server.start_background ChefZero::RSpec.server.on_response do |request, response| ChefZero::RSpec.request_log << [ request, response ] end else ChefZero::RSpec.server.clear_data end ChefZero::RSpec.clear_request_log if defined?(Chef::Config) @old_chef_server_url = Chef::Config.chef_server_url @old_node_name = Chef::Config.node_name @old_client_key = Chef::Config.client_key Chef::Config.chef_server_url = ChefZero::RSpec.server.url Chef::Config.node_name = 'admin' Chef::Config.client_key = ChefZero::RSpec.client_key.path Chef::Config.http_retry_count = 0 end end if defined?(Chef::Config) after :each do Chef::Config.chef_server_url = @old_chef_server_url Chef::Config.node_name = @old_node_name Chef::Config.client_key = @old_client_key end end def self.client(name, client) before(:each) { ChefZero::RSpec.server.load_data({ 'clients' => { name => client }}) } end def self.cookbook(name, version, cookbook, options = {}) before(:each) { ChefZero::RSpec.server.load_data({ 'cookbooks' => { "#{name}-#{version}" => cookbook.merge(options) }}) } end def self.data_bag(name, data_bag) before(:each) { ChefZero::RSpec.server.load_data({ 'data' => { name => data_bag }}) } end def self.environment(name, environment) before(:each) { ChefZero::RSpec.server.load_data({ 'environments' => { name => environment }}) } end def self.node(name, node) before(:each) { ChefZero::RSpec.server.load_data({ 'nodes' => { name => node }}) } end def self.role(name, role) before(:each) { ChefZero::RSpec.server.load_data({ 'roles' => { name => role }}) } end def self.user(name, user) before(:each) { ChefZero::RSpec.server.load_data({ 'users' => { name => user }}) } end # after :each do # if @@ChefZero::RSpec.server # @@ChefZero::RSpec.server.stop # @@ChefZero::RSpec.server = nil # end # if @@ChefZero::RSpec.client_key # @@ChefZero::RSpec.client_key.unlink # @@ChefZero::RSpec.client_key = nil # end # end instance_eval(&block) end end end end chef-zero-2.0.1/lib/chef_zero/version.rb0000644000175000017500000000005012263533061020340 0ustar stefanorstefanormodule ChefZero VERSION = '2.0.1' end chef-zero-2.0.1/lib/chef_zero/rest_request.rb0000644000175000017500000000220012263533061021377 0ustar stefanorstefanorrequire 'rack/request' module ChefZero class RestRequest def initialize(env) @env = env end attr_reader :env def base_uri @base_uri ||= "#{env['rack.url_scheme']}://#{env['HTTP_HOST']}#{env['SCRIPT_NAME']}" end def method @env['REQUEST_METHOD'] end def rest_path @rest_path ||= env['PATH_INFO'].split('/').select { |part| part != "" } end def body=(body) @body = body end def body @body ||= env['rack.input'].read end def query_params @query_params ||= begin params = Rack::Request.new(env).GET params.keys.each do |key| params[key] = URI.unescape(params[key]) end params end end def to_s result = "#{method} #{rest_path.join('/')}" if query_params.size > 0 result << "?#{query_params.map { |k,v| "#{k}=#{v}" }.join('&') }" end if body.chomp != '' result << "\n--- #{method} BODY ---\n" result << body result << "\n" if !body.end_with?("\n") result << "--- END #{method} BODY ---" end result end end end chef-zero-2.0.1/lib/chef_zero/rest_base.rb0000644000175000017500000001024112263533061020625 0ustar stefanorstefanorrequire 'chef_zero/rest_request' require 'chef_zero/rest_error_response' require 'chef_zero/data_store/data_not_found_error' module ChefZero class RestBase def initialize(server) @server = server end attr_reader :server def data_store server.data_store end def call(request) method = request.method.downcase.to_sym if !self.respond_to?(method) accept_methods = [:get, :put, :post, :delete].select { |m| self.respond_to?(m) } accept_methods_str = accept_methods.map { |m| m.to_s.upcase }.join(', ') return [405, {"Content-Type" => "text/plain", "Allow" => accept_methods_str}, "Bad request method for '#{request.env['REQUEST_PATH']}': #{request.env['REQUEST_METHOD']}"] end if json_only && request.env['HTTP_ACCEPT'] && !request.env['HTTP_ACCEPT'].split(';').include?('application/json') return [406, {"Content-Type" => "text/plain"}, "Must accept application/json"] end # Dispatch to get()/post()/put()/delete() begin self.send(method, request) rescue RestErrorResponse => e error(e.response_code, e.error) end end def json_only true end def get_data(request, rest_path=nil, *options) rest_path ||= request.rest_path begin data_store.get(rest_path, request) rescue DataStore::DataNotFoundError if options.include?(:nil) nil else raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end end def list_data(request, rest_path=nil) rest_path ||= request.rest_path begin data_store.list(rest_path) rescue DataStore::DataNotFoundError raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, rest_path)}") end end def delete_data(request, rest_path=nil) rest_path ||= request.rest_path begin data_store.delete(rest_path) rescue DataStore::DataNotFoundError raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end end def delete_data_dir(request, rest_path, *options) rest_path ||= request.rest_path begin data_store.delete_dir(rest_path, *options) rescue DataStore::DataNotFoundError raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end end def set_data(request, rest_path, data, *options) rest_path ||= request.rest_path begin data_store.set(rest_path, request.body, *options) rescue DataStore::DataNotFoundError raise RestErrorResponse.new(404, "Object not found: #{build_uri(request.base_uri, request.rest_path)}") end end def create_data(request, rest_path, name, data, *options) rest_path ||= request.rest_path begin data_store.create(rest_path, name, data, *options) rescue DataStore::DataNotFoundError raise RestErrorResponse.new(404, "Parent not found: #{build_uri(request.base_uri, request.rest_path)}") rescue DataStore::DataAlreadyExistsError raise RestErrorResponse.new(409, "Object already exists: #{build_uri(request.base_uri, request.rest_path + [name])}") end end def exists_data?(request, rest_path=nil) rest_path ||= request.rest_path data_store.exists?(rest_path) end def exists_data_dir?(request, rest_path=nil) rest_path ||= request.rest_path data_store.exists_dir?(rest_path) end def error(response_code, error) json_response(response_code, {"error" => [error]}) end def json_response(response_code, json) already_json_response(response_code, JSON.pretty_generate(json)) end def already_json_response(response_code, json_text) [response_code, {"Content-Type" => "application/json"}, json_text] end def build_uri(base_uri, rest_path) RestBase::build_uri(base_uri, rest_path) end def self.build_uri(base_uri, rest_path) "#{base_uri}/#{rest_path.join('/')}" end def populate_defaults(request, response) response end end end chef-zero-2.0.1/lib/chef_zero/data_store/0000755000175000017500000000000012263533061020460 5ustar stefanorstefanorchef-zero-2.0.1/lib/chef_zero/data_store/data_error.rb0000644000175000017500000000162612263533061023134 0ustar stefanorstefanor# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, 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. # module ChefZero module DataStore class DataError < StandardError def initialize(path, cause = nil) @path = path @cause = cause end attr_reader :path attr_reader :cause end end end chef-zero-2.0.1/lib/chef_zero/data_store/memory_store.rb0000644000175000017500000001100612263533061023527 0ustar stefanorstefanor# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, 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_zero/data_store/data_already_exists_error' require 'chef_zero/data_store/data_not_found_error' module ChefZero module DataStore class MemoryStore def initialize clear end def clear @data = {} # Create containers create_dir([], 'clients') create_dir([], 'cookbooks') create_dir([], 'data') create_dir([], 'environments') create_dir([], 'file_store') create_dir(['file_store'], 'checksums') create_dir([], 'nodes') create_dir([], 'roles') create_dir([], 'sandboxes') create_dir([], 'users') # Set defaults create(['clients'], 'chef-validator', '{ "validator": true }') create(['clients'], 'chef-webui', '{ "admin": true }') create(['environments'], '_default', '{ "description": "The default Chef environment" }') create(['users'], 'admin', '{ "admin": true }') end def create_dir(path, name, *options) parent = _get(path, options.include?(:recursive)) if parent.has_key?(name) if !options.include?(:recursive) raise DataAlreadyExistsError.new(path + [name]) end else parent[name] = {} end end def create(path, name, data, *options) if !data.is_a?(String) raise "set only works with strings" end parent = _get(path, options.include?(:create_dir)) if parent.has_key?(name) raise DataAlreadyExistsError.new(path + [name]) end parent[name] = data end def get(path, request=nil) value = _get(path) if value.is_a?(Hash) raise "get() called on directory #{path.join('/')}" end value end def set(path, data, *options) if !data.is_a?(String) raise "set only works with strings: #{path} = #{data.inspect}" end # Get the parent parent = _get(path[0..-2], options.include?(:create_dir)) if !options.include?(:create) && !parent[path[-1]] raise DataNotFoundError.new(path) end parent[path[-1]] = data end def delete(path) parent = _get(path[0,path.length-1]) if !parent.has_key?(path[-1]) raise DataNotFoundError.new(path) end if !parent[path[-1]].is_a?(String) raise "delete only works with strings: #{path}" end parent.delete(path[-1]) end def delete_dir(path, *options) parent = _get(path[0,path.length-1]) if !parent.has_key?(path[-1]) raise DataNotFoundError.new(path) end if !parent[path[-1]].is_a?(Hash) raise "delete_dir only works with directories: #{path}" end parent.delete(path[-1]) end def list(path) dir = _get(path) if !dir.is_a? Hash raise "list only works with directories (#{path} = #{dir.class}" end dir.keys.sort end def exists?(path) begin get(path) return true rescue DataNotFoundError return false end end def exists_dir?(path) begin dir = _get(path) if !dir.is_a? Hash raise "exists_dir? only works with directories (#{path} = #{dir.class}" end return true rescue DataNotFoundError return false end end private def _get(path, create_dir=false) value = @data path.each_with_index do |path_part, index| if !value.has_key?(path_part) if create_dir value[path_part] = {} else raise DataNotFoundError.new(path[0,index+1]) end end value = value[path_part] end value end end end endchef-zero-2.0.1/lib/chef_zero/data_store/data_not_found_error.rb0000644000175000017500000000156412263533061025210 0ustar stefanorstefanor# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, 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_zero/data_store/data_error' module ChefZero module DataStore class DataNotFoundError < DataError def initialize(path, cause = nil) super end end end endchef-zero-2.0.1/lib/chef_zero/data_store/data_already_exists_error.rb0000644000175000017500000000157212263533061026234 0ustar stefanorstefanor# # Author:: John Keiser () # Copyright:: Copyright (c) 2013 Opscode, 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_zero/data_store/data_error' module ChefZero module DataStore class DataAlreadyExistsError < DataError def initialize(path, cause = nil) super end end end end chef-zero-2.0.1/lib/chef_zero/log.rb0000644000175000017500000000012312263533061017435 0ustar stefanorstefanorrequire 'mixlib/log' module ChefZero class Log extend Mixlib::Log end end chef-zero-2.0.1/lib/chef_zero/server.rb0000644000175000017500000003074612263533061020200 0ustar stefanorstefanor# # Author:: John Keiser () # Copyright:: Copyright (c) 2012 Opscode, 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 'openssl' require 'open-uri' require 'rubygems' require 'timeout' require 'stringio' require 'rack' require 'webrick' require 'chef_zero' require 'chef_zero/cookbook_data' require 'chef_zero/rest_router' require 'chef_zero/data_store/memory_store' require 'chef_zero/version' require 'chef_zero/endpoints/authenticate_user_endpoint' require 'chef_zero/endpoints/actors_endpoint' require 'chef_zero/endpoints/actor_endpoint' require 'chef_zero/endpoints/cookbooks_endpoint' require 'chef_zero/endpoints/cookbook_endpoint' require 'chef_zero/endpoints/cookbook_version_endpoint' require 'chef_zero/endpoints/data_bags_endpoint' require 'chef_zero/endpoints/data_bag_endpoint' require 'chef_zero/endpoints/data_bag_item_endpoint' require 'chef_zero/endpoints/rest_list_endpoint' require 'chef_zero/endpoints/environment_endpoint' require 'chef_zero/endpoints/environment_cookbooks_endpoint' require 'chef_zero/endpoints/environment_cookbook_endpoint' require 'chef_zero/endpoints/environment_cookbook_versions_endpoint' require 'chef_zero/endpoints/environment_nodes_endpoint' require 'chef_zero/endpoints/environment_recipes_endpoint' require 'chef_zero/endpoints/environment_role_endpoint' require 'chef_zero/endpoints/node_endpoint' require 'chef_zero/endpoints/principal_endpoint' require 'chef_zero/endpoints/role_endpoint' require 'chef_zero/endpoints/role_environments_endpoint' require 'chef_zero/endpoints/sandboxes_endpoint' require 'chef_zero/endpoints/sandbox_endpoint' require 'chef_zero/endpoints/searches_endpoint' require 'chef_zero/endpoints/search_endpoint' require 'chef_zero/endpoints/file_store_file_endpoint' require 'chef_zero/endpoints/not_found_endpoint' module ChefZero class Server DEFAULT_OPTIONS = { :host => '127.0.0.1', :port => 8889, :log_level => :info, :generate_real_keys => true }.freeze def initialize(options = {}) @options = DEFAULT_OPTIONS.merge(options) @options[:host] = "[#{@options[:host]}]" if @options[:host].include?(':') @options.freeze ChefZero::Log.level = @options[:log_level].to_sym end # @return [Hash] attr_reader :options # @return [WEBrick::HTTPServer] attr_reader :server include ChefZero::Endpoints # # The URL for this Chef Zero server. # # @return [String] # def url "http://#{@options[:host]}:#{@options[:port]}" end # # The data store for this server (default is in-memory). # # @return [~ChefZero::DataStore] # def data_store @data_store ||= @options[:data_store] || DataStore::MemoryStore.new end # # Boolean method to determine if real Public/Private keys should be # generated. # # @return [Boolean] # true if real keys should be created, false otherwise # def generate_real_keys? !!@options[:generate_real_keys] end # # Start a Chef Zero server in the current thread. You can stop this server # by canceling the current thread. # # @param [Boolean] publish # publish the server information to STDOUT # # @return [nil] # this method will block the main thread until interrupted # def start(publish = true) publish = publish[:publish] if publish.is_a?(Hash) # Legacy API if publish puts <<-EOH.gsub(/^ {10}/, '') >> Starting Chef Zero (v#{ChefZero::VERSION})... >> WEBrick (v#{WEBrick::VERSION}) on Rack (v#{Rack.release}) is listening at #{url} >> Press CTRL+C to stop EOH end thread = start_background %w[INT TERM].each do |signal| Signal.trap(signal) do puts "\n>> Stopping Chef Zero..." @server.shutdown end end # Move the background process to the main thread thread.join end # # Start a Chef Zero server in a forked process. This method returns the PID # to the forked process. # # @param [Fixnum] wait # the number of seconds to wait for the server to start # # @return [Thread] # the thread the background process is running in # def start_background(wait = 5) @server = WEBrick::HTTPServer.new( :BindAddress => @options[:host], :Port => @options[:port], :AccessLog => [], :Logger => WEBrick::Log.new(StringIO.new, 7) ) @server.mount('/', Rack::Handler::WEBrick, app) @thread = Thread.new { @server.start } @thread.abort_on_exception = true @thread end # # Boolean method to determine if the server is currently ready to accept # requests. This method will attempt to make an HTTP request against the # server. If this method returns true, you are safe to make a request. # # @return [Boolean] # true if the server is accepting requests, false otherwise # def running? if @server.nil? || @server.status != :Running return false end uri = URI.join(url, 'cookbooks') headers = { 'Accept' => 'application/json' } Timeout.timeout(0.1) { !open(uri, headers).nil? } rescue SocketError, Errno::ECONNREFUSED, Timeout::Error false end # # Gracefully stop the Chef Zero server. # # @param [Fixnum] wait # the number of seconds to wait before raising force-terminating the # server # def stop(wait = 5) Timeout.timeout(wait) do @server.shutdown @thread.join(wait) if @thread end rescue Timeout::Error if @thread ChefZero::Log.error("Chef Zero did not stop within #{wait} seconds! Killing...") @thread.kill end ensure @server = nil @thread = nil end def gen_key_pair if generate_real_keys? private_key = OpenSSL::PKey::RSA.new(2048) public_key = private_key.public_key.to_s public_key.sub!(/^-----BEGIN RSA PUBLIC KEY-----/, '-----BEGIN PUBLIC KEY-----') public_key.sub!(/-----END RSA PUBLIC KEY-----(\s+)$/, '-----END PUBLIC KEY-----\1') [private_key.to_s, public_key] else [PRIVATE_KEY, PUBLIC_KEY] end end def on_request(&block) @on_request_proc = block end def on_response(&block) @on_response_proc = block 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 environments nodes roles users).each do |data_type| if contents[data_type] dejsonize_children(contents[data_type]).each_pair do |name, data| data_store.set([data_type, name], data, :create) end end end if contents['data'] contents['data'].each_pair do |key, data_bag| data_store.create_dir(['data'], key, :recursive) dejsonize_children(data_bag).each do |item_name, item| data_store.set(['data', key, item_name], item, :create) end end end if contents['cookbooks'] contents['cookbooks'].each_pair do |name_version, cookbook| if name_version =~ /(.+)-(\d+\.\d+\.\d+)$/ cookbook_data = CookbookData.to_hash(cookbook, $1, $2) else cookbook_data = CookbookData.to_hash(cookbook, name_version) end raise "No version specified" if !cookbook_data[:version] data_store.create_dir(['cookbooks'], cookbook_data[:cookbook_name], :recursive) data_store.set(['cookbooks', cookbook_data[:cookbook_name], cookbook_data[:version]], JSON.pretty_generate(cookbook_data), :create) cookbook_data.values.each do |files| next unless files.is_a? Array files.each do |file| data_store.set(['file_store', 'checksums', file[:checksum]], get_file(cookbook, file[:path]), :create) end end end end end def clear_data data_store.clear end def request_handler(&block) @request_handler = block end def to_s "#<#{self.class} #{url}>" end def inspect "#<#{self.class} @url=#{url.inspect}>" end private def app router = RestRouter.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 return proc do |env| request = RestRequest.new(env) if @on_request_proc @on_request_proc.call(request) end response = nil if @request_handler response = @request_handler.call(request) end unless response response = router.call(request) end if @on_response_proc @on_response_proc.call(request, response) end # Insert Server header response[1]['Server'] = 'chef-zero' # Puma expects the response to be an array (chunked responses). Since # we are statically generating data, we won't ever have said chunked # response, so fake it. response[-1] = Array(response[-1]) response end end def dejsonize_children(hash) result = {} hash.each_pair do |key, value| result[key] = value.is_a?(Hash) ? JSON.pretty_generate(value) : value end result end def get_file(directory, path) value = directory path.split('/').each do |part| value = value[part] end value end end end chef-zero-2.0.1/lib/chef_zero/endpoints/0000755000175000017500000000000012263533061020336 5ustar stefanorstefanorchef-zero-2.0.1/lib/chef_zero/endpoints/cookbooks_base.rb0000644000175000017500000000422012263533061023644 0ustar stefanorstefanorrequire 'json' require 'chef_zero/rest_base' require 'chef_zero/data_normalizer' module ChefZero module Endpoints # Common code for endpoints that return cookbook lists class CookbooksBase < RestBase def format_cookbooks_list(request, cookbooks_list, constraints = {}, num_versions = nil) results = {} filter_cookbooks(cookbooks_list, constraints, num_versions) do |name, versions| versions_list = versions.map do |version| { 'url' => build_uri(request.base_uri, ['cookbooks', name, version]), 'version' => version } end results[name] = { 'url' => build_uri(request.base_uri, ['cookbooks', name]), 'versions' => versions_list } end results end def all_cookbooks_list result = {} # Race conditions exist here (if someone deletes while listing). I don't care. data_store.list(['cookbooks']).each do |name| result[name] = data_store.list(['cookbooks', name]) end result end def filter_cookbooks(cookbooks_list, constraints = {}, num_versions = nil) cookbooks_list.keys.sort.each do |name| constraint = Gem::Requirement.new(constraints[name]) versions = [] cookbooks_list[name].sort_by { |version| Gem::Version.new(version.dup) }.reverse.each do |version| break if num_versions && versions.size >= num_versions if constraint.satisfied_by?(Gem::Version.new(version.dup)) versions << version end end yield [name, versions] end end def recipe_names(cookbook_name, cookbook) result = [] if cookbook['recipes'] cookbook['recipes'].each do |recipe| if recipe['path'] == "recipes/#{recipe['name']}" && recipe['name'][-3..-1] == '.rb' if recipe['name'] == 'default.rb' result << cookbook_name end result << "#{cookbook_name}::#{recipe['name'][0..-4]}" end end end result end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/environment_cookbook_versions_endpoint.rb0000644000175000017500000001266412263533061030756 0ustar stefanorstefanorrequire 'json' require 'chef_zero/rest_base' require 'chef_zero/rest_error_response' module ChefZero module Endpoints # /environments/NAME/cookbook_versions class EnvironmentCookbookVersionsEndpoint < RestBase def post(request) cookbook_names = list_data(request, ['cookbooks']) # Get the list of cookbooks and versions desired by the runlist desired_versions = {} run_list = JSON.parse(request.body, :create_additions => false)['run_list'] run_list.each do |run_list_entry| if run_list_entry =~ /(.+)::.+\@(.+)/ || run_list_entry =~ /(.+)\@(.+)/ raise RestErrorResponse.new(412, "No such cookbook: #{$1}") if !cookbook_names.include?($1) raise RestErrorResponse.new(412, "No such cookbook version for cookbook #{$1}: #{$2}") if !list_data(request, ['cookbooks', $1]).include?($2) desired_versions[$1] = [ $2 ] else desired_cookbook = run_list_entry.split('::')[0] raise RestErrorResponse.new(412, "No such cookbook: #{desired_cookbook}") if !cookbook_names.include?(desired_cookbook) desired_versions[desired_cookbook] = list_data(request, ['cookbooks', desired_cookbook]) end end # Filter by environment constraints environment = JSON.parse(get_data(request, request.rest_path[0..1]), :create_additions => false) environment_constraints = environment['cookbook_versions'] || {} desired_versions.each_key do |name| desired_versions = filter_by_constraint(desired_versions, name, environment_constraints[name]) end # Depsolve! solved = depsolve(request, desired_versions.keys, desired_versions, environment_constraints) if !solved if @last_missing_dep && !cookbook_names.include?(@last_missing_dep) return raise RestErrorResponse.new(412, "No such cookbook: #{@last_missing_dep}") elsif @last_constraint_failure return raise RestErrorResponse.new(412, "Could not satisfy version constraints for: #{@last_constraint_failure}") else return raise RestErrorResponse.new(412, "Unsolvable versions!") end end result = {} solved.each_pair do |name, versions| cookbook = JSON.parse(get_data(request, ['cookbooks', name, versions[0]]), :create_additions => false) result[name] = DataNormalizer.normalize_cookbook(cookbook, name, versions[0], request.base_uri, 'MIN') end json_response(200, result) end def depsolve(request, unsolved, desired_versions, environment_constraints) desired_versions.each do |cb, ver| if ver.empty? @last_constraint_failure = cb return nil end end # If everything is already solve_for = unsolved[0] return desired_versions if !solve_for # Go through each desired version of this cookbook, starting with the latest, # until we find one we can solve successfully with sort_versions(desired_versions[solve_for]).each do |desired_version| new_desired_versions = desired_versions.clone new_desired_versions[solve_for] = [ desired_version ] new_unsolved = unsolved[1..-1] # Pick this cookbook, and add dependencies cookbook_obj = JSON.parse(get_data(request, ['cookbooks', solve_for, desired_version]), :create_additions => false) cookbook_metadata = cookbook_obj['metadata'] || {} cookbook_dependencies = cookbook_metadata['dependencies'] || {} dep_not_found = false cookbook_dependencies.each_pair do |dep_name, dep_constraint| # If the dep is not already in the list, add it to the list to solve # and bring in all environment-allowed cookbook versions to desired_versions if !new_desired_versions.has_key?(dep_name) new_unsolved = new_unsolved + [dep_name] # If the dep is missing, we will try other versions of the cookbook that might not have the bad dep. if !exists_data_dir?(request, ['cookbooks', dep_name]) @last_missing_dep = dep_name.to_s dep_not_found = true break end new_desired_versions[dep_name] = list_data(request, ['cookbooks', dep_name]) new_desired_versions = filter_by_constraint(new_desired_versions, dep_name, environment_constraints[dep_name]) end new_desired_versions = filter_by_constraint(new_desired_versions, dep_name, dep_constraint) end next if dep_not_found # Depsolve children with this desired version! First solution wins. result = depsolve(request, new_unsolved, new_desired_versions, environment_constraints) return result if result end return nil end def sort_versions(versions) result = versions.sort_by { |version| Gem::Version.new(version.dup) } result.reverse end def filter_by_constraint(versions, cookbook_name, constraint) return versions if !constraint constraint = Gem::Requirement.new(constraint) new_versions = versions[cookbook_name] new_versions = new_versions.select { |version| constraint.satisfied_by?(Gem::Version.new(version.dup)) } result = versions.clone result[cookbook_name] = new_versions result end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/environment_role_endpoint.rb0000644000175000017500000000214112263533061026146 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /environments/NAME/roles/NAME # /roles/NAME/environments/NAME class EnvironmentRoleEndpoint < CookbooksBase def get(request) # 404 if environment does not exist if request.rest_path[0] == 'environments' environment_path = request.rest_path[0..1] role_path = request.rest_path[2..3] else environment_path = request.rest_path[2..3] role_path = request.rest_path[0..1] end # Verify that the environment exists get_data(request, environment_path) role = JSON.parse(get_data(request, role_path), :create_additions => false) environment_name = environment_path[1] if environment_name == '_default' run_list = role['run_list'] else if role['env_run_lists'] run_list = role['env_run_lists'][environment_name] else run_list = nil end end json_response(200, { 'run_list' => run_list }) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/role_environments_endpoint.rb0000644000175000017500000000060312263533061026332 0ustar stefanorstefanorrequire 'json' require 'chef_zero/rest_base' module ChefZero module Endpoints # /roles/NAME/environments class RoleEnvironmentsEndpoint < RestBase def get(request) role = JSON.parse(get_data(request, request.rest_path[0..1]), :create_additions => false) json_response(200, [ '_default' ] + (role['env_run_lists'].keys || [])) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/authenticate_user_endpoint.rb0000644000175000017500000000127112263533061026300 0ustar stefanorstefanorrequire 'json' require 'chef_zero/rest_base' module ChefZero module Endpoints # /authenticate_user class AuthenticateUserEndpoint < RestBase def post(request) request_json = JSON.parse(request.body, :create_additions => false) name = request_json['name'] password = request_json['password'] begin user = data_store.get(['users', name]) verified = JSON.parse(user, :create_additions => false)['password'] == password rescue DataStore::DataNotFoundError verified = false end json_response(200, { 'name' => name, 'verified' => !!verified }) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/cookbook_endpoint.rb0000644000175000017500000000240212263533061024367 0ustar stefanorstefanorrequire 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /cookbooks/NAME class CookbookEndpoint < CookbooksBase def get(request) filter = request.rest_path[1] case filter when '_latest' result = {} filter_cookbooks(all_cookbooks_list, {}, 1) do |name, versions| if versions.size > 0 result[name] = build_uri(request.base_uri, ['cookbooks', name, versions[0]]) end end json_response(200, result) when '_recipes' result = [] filter_cookbooks(all_cookbooks_list, {}, 1) do |name, versions| if versions.size > 0 cookbook = JSON.parse(get_data(request, ['cookbooks', name, versions[0]]), :create_additions => false) result += recipe_names(name, cookbook) end end json_response(200, result.sort) else cookbook_list = { filter => list_data(request, request.rest_path) } json_response(200, format_cookbooks_list(request, cookbook_list)) end end def latest_version(versions) sorted = versions.sort_by { |version| Gem::Version.new(version.dup) } sorted[-1] end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/environment_cookbooks_endpoint.rb0000644000175000017500000000137512263533061027206 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /environments/NAME/cookbooks class EnvironmentCookbooksEndpoint < CookbooksBase def get(request) environment = JSON.parse(get_data(request, request.rest_path[0..1]), :create_additions => false) constraints = environment['cookbook_versions'] || {} if request.query_params['num_versions'] == 'all' num_versions = nil elsif request.query_params['num_versions'] num_versions = request.query_params['num_versions'].to_i else num_versions = 1 end json_response(200, format_cookbooks_list(request, all_cookbooks_list, constraints, num_versions)) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/actor_endpoint.rb0000644000175000017500000000444512263533061023702 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/data_normalizer' module ChefZero module Endpoints # /clients/* and /users/* class ActorEndpoint < RestObjectEndpoint def put(request) # Find out if we're updating the public key. request_body = JSON.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 request_body.delete('public_key') else updating_public_key = true end # Generate private_key if requested. if request_body.has_key?('private_key') body_modified = true if request_body['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 request.body = JSON.pretty_generate(request_body) if body_modified # PUT /clients is patchy request.body = patch_request_body(request) result = super(request) # Inject private_key into response, delete public_key/password if applicable if result[0] == 200 response = JSON.parse(result[2], :create_additions => false) response['private_key'] = private_key if private_key response.delete('public_key') if !updating_public_key && request.rest_path[0] == 'users' response.delete('password') # For PUT /clients, a rename returns 201. if request_body['name'] && request.rest_path[1] != request_body['name'] json_response(201, response) else json_response(200, response) end else result end end def populate_defaults(request, response_json) response = JSON.parse(response_json, :create_additions => false) if request.rest_path[0] == 'clients' response = DataNormalizer.normalize_client(response, request.rest_path[1]) else response = DataNormalizer.normalize_user(response, request.rest_path[1]) end JSON.pretty_generate(response) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/file_store_file_endpoint.rb0000644000175000017500000000101612263533061025713 0ustar stefanorstefanorrequire 'chef_zero/rest_base' module ChefZero module Endpoints # The minimum amount of S3 necessary to support cookbook upload/download # /file_store/FILE class FileStoreFileEndpoint < RestBase def json_only false end def get(request) [200, {"Content-Type" => 'application/x-binary'}, get_data(request) ] end def put(request) data_store.set(request.rest_path, request.body, :create, :create_dir) json_response(200, {}) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/environment_endpoint.rb0000644000175000017500000000163412263533061025133 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/data_normalizer' module ChefZero module Endpoints # /environments/NAME class EnvironmentEndpoint < RestObjectEndpoint def delete(request) if request.rest_path[1] == "_default" # 405, really? error(405, "The '_default' environment cannot be modified.") else super(request) end end def put(request) if request.rest_path[1] == "_default" error(405, "The '_default' environment cannot be modified.") else super(request) end end def populate_defaults(request, response_json) response = JSON.parse(response_json, :create_additions => false) response = DataNormalizer.normalize_environment(response, request.rest_path[1]) JSON.pretty_generate(response) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/node_endpoint.rb0000644000175000017500000000071012263533061023506 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/data_normalizer' module ChefZero module Endpoints # /nodes/ID class NodeEndpoint < RestObjectEndpoint def populate_defaults(request, response_json) node = JSON.parse(response_json, :create_additions => false) node = DataNormalizer.normalize_node(node, request.rest_path[1]) JSON.pretty_generate(node) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/rest_list_endpoint.rb0000644000175000017500000000211712263533061024574 0ustar stefanorstefanorrequire 'json' require 'chef_zero/rest_base' module ChefZero module Endpoints # Typical REST list endpoint (/roles or /data/BAG) class RestListEndpoint < RestBase def initialize(server, identity_key = 'name') super(server) @identity_key = identity_key end attr_reader :identity_key def get(request) # Get the result result_hash = {} list_data(request).sort.each do |name| result_hash[name] = "#{build_uri(request.base_uri, request.rest_path + [name])}" end json_response(200, result_hash) end def post(request) contents = request.body key = get_key(contents) if key.nil? error(400, "Must specify '#{identity_key}' in JSON") else create_data(request, request.rest_path, key, contents) json_response(201, {'uri' => "#{build_uri(request.base_uri, request.rest_path + [key])}"}) end end def get_key(contents) JSON.parse(contents, :create_additions => false)[identity_key] end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/sandbox_endpoint.rb0000644000175000017500000000152412263533061024223 0ustar stefanorstefanorrequire 'chef_zero/rest_base' require 'chef_zero/rest_error_response' require 'json' module ChefZero module Endpoints # /sandboxes/ID class SandboxEndpoint < RestBase def put(request) existing_sandbox = JSON.parse(get_data(request), :create_additions => false) existing_sandbox['checksums'].each do |checksum| if !exists_data?(request, ['file_store', 'checksums', checksum]) raise RestErrorResponse.new(503, "Checksum not uploaded: #{checksum}") end end delete_data(request) json_response(200, { :guid => request.rest_path[1], :name => request.rest_path[1], :checksums => existing_sandbox['checksums'], :create_time => existing_sandbox['create_time'], :is_completed => true }) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/environment_cookbook_endpoint.rb0000644000175000017500000000161412263533061027017 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /environments/NAME/cookbooks/NAME class EnvironmentCookbookEndpoint < CookbooksBase def get(request) cookbook_name = request.rest_path[3] environment = JSON.parse(get_data(request, request.rest_path[0..1]), :create_additions => false) constraints = environment['cookbook_versions'] || {} cookbook_versions = list_data(request, request.rest_path[2..3]) if request.query_params['num_versions'] == 'all' num_versions = nil elsif request.query_params['num_versions'] num_versions = request.query_params['num_versions'].to_i else num_versions = nil end json_response(200, format_cookbooks_list(request, { cookbook_name => cookbook_versions }, constraints, num_versions)) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/searches_endpoint.rb0000644000175000017500000000073512263533061024365 0ustar stefanorstefanorrequire 'chef_zero/rest_base' module ChefZero module Endpoints # /search class SearchesEndpoint < RestBase def get(request) # Get the result result_hash = {} indices = (%w(client environment node role) + data_store.list(['data'])).sort indices.each do |index| result_hash[index] = build_uri(request.base_uri, request.rest_path + [index]) end json_response(200, result_hash) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/data_bag_item_endpoint.rb0000644000175000017500000000151112263533061025321 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/endpoints/data_bag_item_endpoint' require 'chef_zero/data_normalizer' module ChefZero module Endpoints # /data/NAME/NAME class DataBagItemEndpoint < RestObjectEndpoint def initialize(server) super(server, 'id') end def populate_defaults(request, response_json) DataBagItemEndpoint::populate_defaults(request, response_json, request.rest_path[1], request.rest_path[2]) end def self.populate_defaults(request, response_json, data_bag, data_bag_item) response = JSON.parse(response_json, :create_additions => false) response = DataNormalizer.normalize_data_bag_item(response, data_bag, data_bag_item, request.method) JSON.pretty_generate(response) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/actors_endpoint.rb0000644000175000017500000000200212263533061024050 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_list_endpoint' module ChefZero module Endpoints # /clients or /users class ActorsEndpoint < RestListEndpoint def post(request) # First, find out if the user actually posted a public key. If not, make # one. request_body = JSON.parse(request.body, :create_additions => false) public_key = request_body['public_key'] if !public_key private_key, public_key = server.gen_key_pair request_body['public_key'] = public_key request.body = JSON.pretty_generate(request_body) end result = super(request) if result[0] == 201 # If we generated a key, stuff it in the response. response = JSON.parse(result[2], :create_additions => false) response['private_key'] = private_key if private_key response['public_key'] = public_key json_response(201, response) else result end end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/rest_object_endpoint.rb0000644000175000017500000000401512263533061025066 0ustar stefanorstefanorrequire 'json' require 'chef_zero/rest_base' require 'chef_zero/rest_error_response' module ChefZero module Endpoints # Typical REST leaf endpoint (/roles/NAME or /data/BAG/NAME) class RestObjectEndpoint < RestBase def initialize(server, identity_key = 'name') super(server) @identity_key = identity_key end attr_reader :identity_key def get(request) already_json_response(200, populate_defaults(request, get_data(request))) end def put(request) # We grab the old body to trigger a 404 if it doesn't exist old_body = get_data(request) request_json = JSON.parse(request.body, :create_additions => false) key = request_json[identity_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 begin data_store.create(request.rest_path[0..-2], key, request.body) rescue DataStore::DataAlreadyExistsError return error(409, "Cannot rename '#{request.rest_path[-1]}' to '#{key}': '#{key}' already exists") end delete_data(request) else set_data(request, request.rest_path, request.body) end already_json_response(200, populate_defaults(request, request.body)) end def delete(request) result = get_data(request) delete_data(request) already_json_response(200, populate_defaults(request, result)) end def patch_request_body(request) existing_value = get_data(request, nil, :nil) if existing_value request_json = JSON.parse(request.body, :create_additions => false) existing_json = JSON.parse(existing_value, :create_additions => false) merged_json = existing_json.merge(request_json) if merged_json.size > request_json.size return JSON.pretty_generate(merged_json) end end request.body end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/cookbooks_endpoint.rb0000644000175000017500000000104212263533061024551 0ustar stefanorstefanorrequire 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /cookbooks class CookbooksEndpoint < CookbooksBase def get(request) if request.query_params['num_versions'] == 'all' num_versions = nil elsif request.query_params['num_versions'] num_versions = request.query_params['num_versions'].to_i else num_versions = 1 end json_response(200, format_cookbooks_list(request, all_cookbooks_list, {}, num_versions)) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/principal_endpoint.rb0000644000175000017500000000132012263533061024540 0ustar stefanorstefanorrequire 'json' require 'chef_zero' require 'chef_zero/rest_base' module ChefZero module Endpoints # /principals/NAME class PrincipalEndpoint < RestBase def get(request) name = request.rest_path[-1] json = get_data(request, [ 'users', name ], :nil) if json type = 'user' else json = get_data(request, [ 'clients', name ], :nil) type = 'client' end if json json_response(200, { 'name' => name, 'type' => type, 'public_key' => JSON.parse(json)['public_key'] || PUBLIC_KEY }) else error(404, 'Principal not found') end end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/role_endpoint.rb0000644000175000017500000000071112263533061023523 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/data_normalizer' module ChefZero module Endpoints # /roles/NAME class RoleEndpoint < RestObjectEndpoint def populate_defaults(request, response_json) role = JSON.parse(response_json, :create_additions => false) role = DataNormalizer.normalize_role(role, request.rest_path[1]) JSON.pretty_generate(role) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/environment_recipes_endpoint.rb0000644000175000017500000000137212263533061026644 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/cookbooks_base' module ChefZero module Endpoints # /environment/NAME/recipes class EnvironmentRecipesEndpoint < CookbooksBase def get(request) environment = JSON.parse(get_data(request, request.rest_path[0..1]), :create_additions => false) constraints = environment['cookbook_versions'] || {} result = [] filter_cookbooks(all_cookbooks_list, constraints, 1) do |name, versions| if versions.size > 0 cookbook = JSON.parse(get_data(request, ['cookbooks', name, versions[0]]), :create_additions => false) result += recipe_names(name, cookbook) end end json_response(200, result.sort) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/environment_nodes_endpoint.rb0000644000175000017500000000123012263533061026313 0ustar stefanorstefanorrequire 'json' require 'chef_zero/rest_base' module ChefZero module Endpoints # /environment/NAME/nodes class EnvironmentNodesEndpoint < RestBase def get(request) # 404 if environment does not exist get_data(request, request.rest_path[0..1]) result = {} list_data(request, ['nodes']).each do |name| node = JSON.parse(get_data(request, ['nodes', name]), :create_additions => false) if node['chef_environment'] == request.rest_path[1] result[name] = build_uri(request.base_uri, ['nodes', name]) end end json_response(200, result) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/sandboxes_endpoint.rb0000644000175000017500000000274312263533061024557 0ustar stefanorstefanorrequire 'json' require 'chef_zero/rest_base' module ChefZero module Endpoints # /sandboxes class SandboxesEndpoint < RestBase def initialize(server) super(server) @next_id = 1 end def post(request) sandbox_checksums = [] needed_checksums = JSON.parse(request.body, :create_additions => false)['checksums'] result_checksums = {} needed_checksums.keys.each do |needed_checksum| if list_data(request, ['file_store', 'checksums']).include?(needed_checksum) result_checksums[needed_checksum] = { :needs_upload => false } else result_checksums[needed_checksum] = { :needs_upload => true, :url => build_uri(request.base_uri, ['file_store', 'checksums', needed_checksum]) } sandbox_checksums << needed_checksum end end # There is an obvious race condition here. id = @next_id.to_s @next_id+=1 time_str = Time.now.utc.strftime('%Y-%m-%dT%H:%M:%S%z') time_str = "#{time_str[0..21]}:#{time_str[22..23]}" create_data(request, request.rest_path, id, JSON.pretty_generate({ :create_time => time_str, :checksums => sandbox_checksums })) json_response(201, { :uri => build_uri(request.base_uri, request.rest_path + [id]), :checksums => result_checksums, :sandbox_id => id }) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/not_found_endpoint.rb0000644000175000017500000000043012263533061024553 0ustar stefanorstefanorrequire 'json' module ChefZero module Endpoints class NotFoundEndpoint def call(request) return [404, {"Content-Type" => "application/json"}, JSON.pretty_generate({"error" => ["Object not found: #{request.env['REQUEST_PATH']}"]})] end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/data_bag_endpoint.rb0000644000175000017500000000237012263533061024307 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_list_endpoint' require 'chef_zero/endpoints/data_bag_item_endpoint' require 'chef_zero/rest_error_response' module ChefZero module Endpoints # /data/NAME class DataBagEndpoint < RestListEndpoint def initialize(server) super(server, 'id') end def post(request) key = JSON.parse(request.body, :create_additions => false)[identity_key] response = super(request) if response[0] == 201 already_json_response(201, DataBagItemEndpoint::populate_defaults(request, request.body, request.rest_path[1], key)) else response end end def get_key(contents) data_bag_item = JSON.parse(contents, :create_additions => false) if data_bag_item['json_class'] == 'Chef::DataBagItem' && data_bag_item['raw_data'] data_bag_item['raw_data']['id'] else data_bag_item['id'] end end def delete(request) key = request.rest_path[1] delete_data_dir(request, request.rest_path, :recursive) json_response(200, { 'chef_type' => 'data_bag', 'json_class' => 'Chef::DataBag', 'name' => key }) end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/cookbook_version_endpoint.rb0000644000175000017500000001063212263533061026140 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/rest_error_response' require 'chef_zero/data_normalizer' require 'chef_zero/data_store/data_not_found_error' module ChefZero module Endpoints # /cookbooks/NAME/VERSION class CookbookVersionEndpoint < RestObjectEndpoint def get(request) if request.rest_path[2] == "_latest" || request.rest_path[2] == "latest" request.rest_path[2] = latest_version(list_data(request, request.rest_path[0..1])) end super(request) end def put(request) name = request.rest_path[1] version = request.rest_path[2] existing_cookbook = get_data(request, request.rest_path, :nil) # Honor frozen if existing_cookbook existing_cookbook_json = JSON.parse(existing_cookbook, :create_additions => false) if existing_cookbook_json['frozen?'] if request.query_params['force'] != "true" raise RestErrorResponse.new(409, "The cookbook #{name} at version #{version} is frozen. Use the 'force' option to override.") end # For some reason, you are forever unable to modify "frozen?" on a frozen cookbook. request_body = JSON.parse(request.body, :create_additions => false) if !request_body['frozen?'] request_body['frozen?'] = true request.body = JSON.pretty_generate(request_body) end end end # Set the cookbook set_data(request, ['cookbooks', name, version], request.body, :create_dir, :create) # If the cookbook was updated, check for deleted files and clean them up if existing_cookbook missing_checksums = get_checksums(existing_cookbook) - get_checksums(request.body) if missing_checksums.size > 0 hoover_unused_checksums(missing_checksums, request) end end already_json_response(existing_cookbook ? 200 : 201, populate_defaults(request, request.body)) end def delete(request) if request.rest_path[2] == "_latest" || request.rest_path[2] == "latest" request.rest_path[2] = latest_version(list_data(request, request.rest_path[0..1])) end deleted_cookbook = get_data(request) response = super(request) cookbook_name = request.rest_path[1] if exists_data_dir?(request, [ 'cookbooks', cookbook_name ]) && list_data(request, ['cookbooks', cookbook_name]).size == 0 delete_data_dir(request, ['cookbooks', cookbook_name]) end # Hoover deleted files, if they exist hoover_unused_checksums(get_checksums(deleted_cookbook), request) response end def get_checksums(cookbook) result = [] JSON.parse(cookbook, :create_additions => false).each_pair do |key, value| if value.is_a?(Array) value.each do |file| if file.is_a?(Hash) && file.has_key?('checksum') result << file['checksum'] end end end end result.uniq end private def hoover_unused_checksums(deleted_checksums, request) data_store.list(['cookbooks']).each do |cookbook_name| data_store.list(['cookbooks', cookbook_name]).each do |version| cookbook = data_store.get(['cookbooks', cookbook_name, version], request) deleted_checksums = deleted_checksums - get_checksums(cookbook) end end deleted_checksums.each do |checksum| # There can be a race here if multiple cookbooks are uploading. # This deals with an exception on delete, but things can still get deleted # that shouldn't be. begin data_store.delete(['file_store', 'checksums', checksum]) rescue ChefZero::DataStore::DataNotFoundError end end end def populate_defaults(request, response_json) # Inject URIs into each cookbook file cookbook = JSON.parse(response_json, :create_additions => false) cookbook = DataNormalizer.normalize_cookbook(cookbook, request.rest_path[1], request.rest_path[2], request.base_uri, request.method) JSON.pretty_generate(cookbook) end def latest_version(versions) sorted = versions.sort_by { |version| Gem::Version.new(version.dup) } sorted[-1] end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/data_bags_endpoint.rb0000644000175000017500000000126512263533061024474 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_list_endpoint' module ChefZero module Endpoints # /data class DataBagsEndpoint < RestListEndpoint def post(request) contents = request.body name = JSON.parse(contents, :create_additions => false)[identity_key] if name.nil? error(400, "Must specify '#{identity_key}' in JSON") elsif exists_data_dir?(request, ['data', name]) error(409, "Object already exists") else data_store.create_dir(['data'], name, :recursive) json_response(201, {"uri" => "#{build_uri(request.base_uri, request.rest_path + [name])}"}) end end end end end chef-zero-2.0.1/lib/chef_zero/endpoints/search_endpoint.rb0000644000175000017500000001536012263533061024035 0ustar stefanorstefanorrequire 'json' require 'chef_zero/endpoints/rest_object_endpoint' require 'chef_zero/data_normalizer' require 'chef_zero/rest_error_response' require 'chef_zero/solr/solr_parser' require 'chef_zero/solr/solr_doc' module ChefZero module Endpoints # /search/INDEX class SearchEndpoint < RestBase def get(request) results = search(request) results['rows'] = results['rows'].map { |name,uri,value,search_value| value } json_response(200, results) end def post(request) full_results = search(request) keys = JSON.parse(request.body, :create_additions => false) partial_results = full_results['rows'].map do |name, uri, doc, search_value| data = {} keys.each_pair do |key, path| if path.size > 0 value = search_value path.each do |path_part| value = value[path_part] if !value.nil? end data[key] = value else data[key] = nil end end { 'url' => uri, 'data' => data } end json_response(200, { 'rows' => partial_results, 'start' => full_results['start'], 'total' => full_results['total'] }) end private def search_container(request, index) case index when 'client' [ ['clients'], Proc.new { |client, name| DataNormalizer.normalize_client(client, name) }, build_uri(request.base_uri, [ 'clients' ]) ] when 'node' [ ['nodes'], Proc.new { |node, name| DataNormalizer.normalize_node(node, name) }, build_uri(request.base_uri, [ 'nodes' ]) ] when 'environment' [ ['environments'], Proc.new { |environment, name| DataNormalizer.normalize_environment(environment, name) }, build_uri(request.base_uri, [ 'environments' ]) ] when 'role' [ ['roles'], Proc.new { |role, name| DataNormalizer.normalize_role(role, name) }, build_uri(request.base_uri, [ 'roles' ]) ] else [ ['data', index], Proc.new { |data_bag_item, id| DataNormalizer.normalize_data_bag_item(data_bag_item, index, id, 'DELETE') }, build_uri(request.base_uri, [ 'data', index ]) ] end end def expand_for_indexing(value, index, id) if index == 'node' result = {} deep_merge!(value['default'] || {}, result) deep_merge!(value['normal'] || {}, result) deep_merge!(value['override'] || {}, result) deep_merge!(value['automatic'] || {}, result) result['recipe'] = [] result['role'] = [] if value['run_list'] value['run_list'].each do |run_list_entry| if run_list_entry =~ /^(recipe|role)\[(.*)\]/ result[$1] << $2 end end end value.each_pair do |key, value| result[key] = value unless %w(default normal override automatic).include?(key) end result elsif !%w(client environment role).include?(index) DataNormalizer.normalize_data_bag_item(value, index, id, 'GET') else value end end def search(request) # Extract parameters index = request.rest_path[1] query_string = request.query_params['q'] || '*:*' solr_query = ChefZero::Solr::SolrParser.new(query_string).parse sort_string = request.query_params['sort'] start = request.query_params['start'] start = start.to_i if start rows = request.query_params['rows'] rows = rows.to_i if rows # Get the search container container, expander, base_uri = search_container(request, index) # Search! result = [] list_data(request, container).each do |name| value = get_data(request, container + [name]) expanded = expander.call(JSON.parse(value, :create_additions => false), name) result << [ name, build_uri(base_uri, [name]), expanded, expand_for_indexing(expanded, index, name) ] end result = result.select do |name, uri, value, search_value| solr_query.matches_doc?(ChefZero::Solr::SolrDoc.new(search_value, name)) end total = result.size # Sort if sort_string sort_key, sort_order = sort_string.split(/\s+/, 2) result = result.sort_by { |name,uri,value,search_value| ChefZero::Solr::SolrDoc.new(search_value, name)[sort_key] } result = result.reverse if sort_order == "DESC" end # Paginate if start result = result[start..start+(rows||-1)] end { 'rows' => result, 'start' => start || 0, 'total' => total } end private # Deep Merge core documentation. # deep_merge! method permits merging of arbitrary child elements. The two top level # elements must be hashes. These hashes can contain unlimited (to stack limit) levels # of child elements. These child elements to not have to be of the same types. # Where child elements are of the same type, deep_merge will attempt to merge them together. # Where child elements are not of the same type, deep_merge will skip or optionally overwrite # the destination element with the contents of the source element at that level. # So if you have two hashes like this: # source = {:x => [1,2,3], :y => 2} # dest = {:x => [4,5,'6'], :y => [7,8,9]} # dest.deep_merge!(source) # Results: {:x => [1,2,3,4,5,'6'], :y => 2} # By default, "deep_merge!" will overwrite any unmergeables and merge everything else. # To avoid this, use "deep_merge" (no bang/exclamation mark) def deep_merge!(source, dest) # if dest doesn't exist, then simply copy source to it if dest.nil? dest = source; return dest end case source when nil dest when Hash source.each do |src_key, src_value| if dest.kind_of?(Hash) if dest[src_key] dest[src_key] = deep_merge!(src_value, dest[src_key]) else # dest[src_key] doesn't exist so we take whatever source has dest[src_key] = src_value end else # dest isn't a hash, so we overwrite it completely dest = source end end when Array if dest.kind_of?(Array) dest = dest | source else dest = source end when String dest = source else # src_hash is not an array or hash, so we'll have to overwrite dest dest = source end dest end # deep_merge! end end end chef-zero-2.0.1/lib/chef_zero/cookbook_data.rb0000644000175000017500000001604712263533061021467 0ustar stefanorstefanorrequire 'digest/md5' require 'hashie/mash' module ChefZero module CookbookData def self.to_hash(cookbook, name, version=nil) frozen = false if cookbook.has_key?(:frozen) frozen = cookbook[:frozen] cookbook = cookbook.dup cookbook.delete(:frozen) end 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[:frozen?] = true if frozen result end def self.metadata_from(directory, name, version, recipe_names) metadata = PretendCookbookMetadata.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 has_child(directory, 'metadata.rb') begin file = filename(directory, 'metadata.rb') || "(#{name}/metadata.rb)" metadata.instance_eval(read_file(directory, 'metadata.rb'), file) rescue ChefZero::Log.error("Error loading cookbook #{name}: #{$!}\n #{$!.backtrace.join("\n ")}") end elsif has_child(directory, 'metadata.json') metadata.from_json(read_file(directory, 'metadata.json')) end result = {} metadata.to_hash.each_pair do |key,value| result[key.to_sym] = value end result[:version] = version if 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 # Handles loading configuration values from a Chef config file # # @author Justin Campbell class PretendCookbookMetadata < Hash # @param [String] path def initialize(cookbook) self.name(cookbook.name) self.recipes(cookbook.fully_qualified_recipe_names) %w(attributes grouping dependencies supports recommendations suggestions conflicting providing replacing recipes).each do |hash_arg| self[hash_arg.to_sym] = Hashie::Mash.new end end def from_json(filepath) self.merge!(JSON.parse(File.read(filepath))) end private def depends(cookbook, *version_constraints) cookbook_arg(:dependencies, cookbook, version_constraints) end def supports(cookbook, *version_constraints) cookbook_arg(:supports, cookbook, version_constraints) end def recommends(cookbook, *version_constraints) cookbook_arg(:recommendations, cookbook, version_constraints) end def suggests(cookbook, *version_constraints) cookbook_arg(:suggestions, cookbook, version_constraints) end def conflicts(cookbook, *version_constraints) cookbook_arg(:conflicting, cookbook, version_constraints) end def provides(cookbook, *version_constraints) cookbook_arg(:providing, cookbook, version_constraints) end def replaces(cookbook, *version_constraints) cookbook_arg(:replacing, cookbook, version_constraints) end def recipe(recipe, description) self[:recipes][recipe] = description end def attribute(name, options) self[:attributes][name] = options end def grouping(name, options) self[:grouping][name] = options end def cookbook_arg(key, cookbook, version_constraints) self[key][cookbook] = version_constraints.first || ">= 0.0.0" end def method_missing(key, value = nil) if value.nil? self[key.to_sym] else store key.to_sym, value end end end def self.files_from(directory) # TODO some support .rb only result = { :attributes => load_child_files(directory, 'attributes', false), :definitions => load_child_files(directory, 'definitions', false), :recipes => load_child_files(directory, 'recipes', false), :libraries => load_child_files(directory, 'libraries', false), :templates => load_child_files(directory, 'templates', true), :files => load_child_files(directory, 'files', true), :resources => load_child_files(directory, 'resources', true), :providers => load_child_files(directory, 'providers', true), :root_files => load_files(directory, false) } set_specificity(result[:templates]) set_specificity(result[:files]) result end def self.has_child(directory, name) if directory.is_a?(Hash) directory.has_key?(name) else directory.child(name).exists? end end def self.read_file(directory, name) if directory.is_a?(Hash) directory[name] else directory.child(name).read end end def self.filename(directory, name) if directory.respond_to?(:file_path) File.join(directory.file_path, name) else nil end end def self.get_directory(directory, name) if directory.is_a?(Hash) directory[name].is_a?(Hash) ? directory[name] : nil else result = directory.child(name) result.dir? ? result : nil end end def self.list(directory) if directory.is_a?(Hash) directory.keys else directory.children.map { |c| c.name } end end def self.load_child_files(parent, key, recursive) result = load_files(get_directory(parent, key), recursive) result.each do |file| file[:path] = "#{key}/#{file[:path]}" end result end def self.load_files(directory, recursive) result = [] if directory list(directory).each do |child_name| dir = get_directory(directory, child_name) if dir if recursive result += load_child_files(directory, child_name, recursive) end else result += load_file(read_file(directory, child_name), child_name) end end end result end def self.load_file(value, name) [{ :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 chef-zero-2.0.1/spec/0000755000175000017500000000000012263533061014553 5ustar stefanorstefanorchef-zero-2.0.1/spec/support/0000755000175000017500000000000012263533061016267 5ustar stefanorstefanorchef-zero-2.0.1/spec/support/stickywicket.pem0000644000175000017500000000321712263533061021512 0ustar stefanorstefanor-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEApNCkX2k+lFGDWRVhX4uClaVQrumG9XXvk6X7M2izrIg7RzMP Dk4thhZkpx5gr22By7PZQdMEjWC/Zo8MBjtoJ0GV0jw8npefbU1MGKs2dtpYgo0N Fq8fX8MdFPu4h2W3g0dMEdhT8icc2H4EjhZmdeUhUn3RIEt2duCgp3YDYnUUZx3j N7MHcTIdzD58ikr6zQrZzHOv+OOI86Xk9EpyEEQizOLoQxkICNrhqN7ElQDuvXaX BSBrYDRKH2umBMMcXzvsR/SvkmqxoEESSpIlW8zeKAWQ+znNjDC0tmTg7jZmgSP7 siKrwo4t4ebjcmjpIoi/JKww/nGN3Uhz1ZOZuwIDAQABAoIBAQCaJQD2s0nyEeKU uKhfYe155Cl3zbWJcQnmv4AXbr9MiAVY6+oS6Q8ur1bn7kNjDzoruENjiuZhC7E3 TGZklb8tp+tluyy+7vQOmBKpp8fClSfewekR5CultqhGbb8B8yIVR+NfdUHd4rLZ z9KWyWB+txPZQQ8L80gSmrfmpzs3IuT7oPvmtBU1Wq9QapC4n/rUohHUpUV1du4G 0wCIF4zQTg6cbYW2YXozwVQvw+P7P3RVEqZt+aZlbVcy0fNr6jNao0hi1KFC9OH2 VjjU+PioreoA/NU3aZPIUzmJpWtsu31yuOZxXmytAkYooCZgiEQNEHnJlNPv0RmC 6BPMzVoBAoGBAM7yZoSNJpzdP/q1/4+H3zyy7o4I0VTW9u/GqUzhnbjm5poK30X9 YXh/7WOVV0OoVqdO6ljRKygP3Oggf41ZEbi1C6bbsO57pksBWgx9bD9V35XscZ0J F1ERe//kMHwVQy74R8/cIuRwm75haLSBj5/fwGbLeeVDglJkCVqPjtuBAoGBAMvh qsAGG5k9u6voTcXlFwS+B5YjULhK4NSxdJ2BnOxzYzxQ3IYQZMlb2xt8yZYx/ZZK wjkr9rcAPEQIQZ2A6NUbGq6qCD7sSmg6UAi0CgiqTokQ/Wtag0UDvFMzwerdg/On 37uxffpxpte8z1jYi/MxRaoTYueuc1UVnqofVIM7AoGBALZJzwPzUY/bVAADUJmd lYZiFsAGBF42/E05MOgH1GaK/ZWy/fkouDLsfK67XaK7JZk6ajLSDLG9R1kxRym6 y2FoGFtiKPfo8xIenrNhx3gCrG/jVjB9UYyXWiKNXifukr9M8/SkdBfFGWsZYqGd fmXVMiVaFoVcce8hLxwWWEABAoGBAKcyhKX/HEj6YFqlIoqkydDAylXs1jicZ27l rF2yum8KXZpMMdzbutuKsdAD8Ql0K6NB4a+jByuiTMn5/11cJxUEqkgM9sArZQW+ tH2+r+/VQpyTS0/rpXVGj/2nl2K1kI2T4R36e/aTl6CanWweAf9JK/lC9rxKyxg+ p6SaFuObAoGACP6TKCkp2oymXlKgdUUgPrnsaz2VAw8jD5QHtx10U4wty0C8gxsk MLe00h09iLPyFmvJpD+MgbxV/r6RrZeVdsKdU/5LG52YgiVSTaizyy+ciEfW7xoQ CL5EtZd8Cn5OKinBEzzFpELqunlqepIKCIDOcLKz/cjR+3a+E6Zx5Wo= -----END RSA PRIVATE KEY----- chef-zero-2.0.1/spec/support/pedant.rb0000644000175000017500000001056212263533061020073 0ustar stefanorstefanor# Copyright: Copyright (c) 2012 Opscode, 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. # This annotated Pedant configuration file details the various # configuration settings available to you. It is separate from the # actual Pedant::Config class because not all settings have sane # defaults, and not all settings are appropriate in all settings. ################################################################################ # You MUST specify the address of the server the API requests will be # sent to. Only specify protocol, hostname, and port. chef_server 'http://127.0.0.1:8889' # If you are doing development testing, you can specify the address of # the Solr server. The presence of this parameter will enable tests # to force commits to Solr, greatly decreasing the amout of time # needed for testing the search endpoint. This is only an # optimization for development! If you are testing a "live" Chef # Server, or otherwise do not have access to the Solr server from your # testing location, you should not specify a value for this parameter. # The tests will still run, albeit slower, as they will now need to # poll for a period to ensure they are querying committed results. #search_server "http://localhost:8983" # Related to the 'search_server' parameter, this specifies the maximum # amout of time (in seconds) that search endpoint requests should be # retried before giving up. If not explicitly set, it will default to # 65 seconds; only set it if you know that your Solr commit interval # differs significantly from this. maximum_search_time 0 # We're starting to break tests up into groups based on different # criteria. The proper API tests (the results of which are viewable # to OPC customers) should be the only ones run by Pedant embedded in # OPC installs. There are other specs that help us keep track of API # cruft that we want to come back and fix later; these shouldn't be # viewable to customers, but we should be able to run them in # development and CI environments. If this parameter is missing or # explicitly `false` only the customer-friendly tests will be run. # # This is mainly here for documentation purposes, since the # command-line `opscode-pedant` utility ultimately determines this # value. include_internal false # Test users. The five users specified below are required; their # names (:user, :non_org_user, etc.) are indicative of their role # within the tests. All users must have a ':name' key. If they have # a ':create_me' key, Pedant will create these users for you. If you # are using pre-existing users, you must supply a ':key_file' key, # which should be the fully-qualified path /on the machine Pedant is # running on/ to a private key for that user. key = 'spec/support/stickywicket.pem' superuser_name 'admin' superuser_key key webui_key key # Set the platform_class platform_class Pedant::OpenSourcePlatform requestors({ :clients => { # The the admin user, for the purposes of getting things rolling :admin => { :name => "pedant_admin_client", :create_me => true, :create_knife => true, :admin => true }, :non_admin => { :name => 'pedant_client', :create_me => true, :create_knife => true }, :bad => { :name => 'bad_client', :bogus => true } }, :users => { :admin => { :name => "admin", :key_file => key, :create_me => false, :create_knife => false, :admin => true }, :non_admin => { :name => "pedant_non_admin_user", :create_me => true, :create_knife => true, :admin => false }, # A user for Knife tests. A knife.rb and key files will be set up # for this user :knife_user => { :name => "knifey", :create_me => true, :create_knife => true } } }) self[:tags] = [:validation, :authentication, :authorization] verify_error_messages false chef-zero-2.0.1/spec/search_spec.rb0000644000175000017500000000137512263533061017365 0ustar stefanorstefanorrequire 'chef_zero/solr/solr_parser' require 'chef_zero/solr/solr_doc' describe ChefZero::Solr::SolrParser do let (:all_docs) do docs = [] [{'foo' => 'a'}, {'foo' => 'd'}].each_with_index do |h, i| docs.push ChefZero::Solr::SolrDoc.new(h, i) end docs end def search_for(query) q = ChefZero::Solr::SolrParser.new(query).parse all_docs.select {|doc| q.matches_doc?(doc) } end it "handles terms" do search_for('foo:d').size.should eq(1) end it "handles ranges" do search_for('foo:[a TO c]').size.should eq(1) end it "handles wildcard ranges" do search_for('foo:[* TO c]').size.should eq(1) search_for('foo:[c TO *]').size.should eq(1) search_for('foo:[* TO *]').size.should eq(2) end end chef-zero-2.0.1/spec/run.rb0000644000175000017500000000150512263533061015705 0ustar stefanorstefanor#!/usr/bin/env ruby require 'bundler' require 'bundler/setup' require 'chef_zero/server' require 'rspec/core' server = ChefZero::Server.new(:port => 8889) server.start_background unless ENV['SKIP_PEDANT'] require 'pedant' require 'pedant/opensource' Pedant.config.suite = 'api' Pedant.config[:config_file] = 'spec/support/pedant.rb' Pedant.setup([ '--skip-validation', '--skip-authentication', '--skip-authorization', '--skip-omnibus' ]) result = RSpec::Core::Runner.run(Pedant.config.rspec_args) else require 'net/http' response = Net::HTTP.new('127.0.0.1', 8889).get("/environments", { 'Accept' => 'application/json'}).body if response =~ /_default/ result = 0 else puts "GET /environments returned #{response}. Expected _default!" result = 1 end end server.stop exit(result) chef-zero-2.0.1/bin/0000755000175000017500000000000012263533061014371 5ustar stefanorstefanorchef-zero-2.0.1/bin/chef-zero0000755000175000017500000000252712263533061016207 0ustar stefanorstefanor#!/usr/bin/env ruby # Trap interrupts to quit cleanly. Signal.trap('INT') { exit 1 } require 'rubygems' $:.unshift(File.expand_path(File.join(File.dirname(__FILE__), "..", "lib"))) require 'chef_zero/version' require 'chef_zero/server' require 'optparse' options = {} 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 end opts.on("-p", "--port PORT", Integer, "Port to listen on") do |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 end opts.on("-d", "--daemon", "Run as a daemon process") do |value| options[:daemon] = value end opts.on("-l", "--log-level LEVEL", "Set the output log level") do |value| options[:log_level] = value end opts.on_tail("-h", "--help", "Show this message") do puts opts exit end opts.on_tail("--version", "Show version") do puts ChefZero::VERSION exit end end.parse! server = ChefZero::Server.new(options) if options[:daemon] if Process.respond_to?(:daemon) Process.daemon(true) server.start(true) else abort 'Process.daemon requires Ruby >= 1.9' end else server.start(true) end chef-zero-2.0.1/Rakefile0000644000175000017500000000020512263533061015263 0ustar stefanorstefanorrequire 'bundler' require 'bundler/gem_tasks' require 'chef_zero/version' task :spec do require File.expand_path('spec/run') end chef-zero-2.0.1/LICENSE0000644000175000017500000002514212263533061014632 0ustar stefanorstefanor Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] 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. chef-zero-2.0.1/README.md0000644000175000017500000001056312263533061015105 0ustar stefanorstefanorChef Zero ========= Description ----------- Chef Zero is a simple, easy-install, in-memory Chef server that can be useful for Chef Client testing and chef-solo-like tasks that require a full Chef Server. It IS intended to be simple, Chef 11 compliant, easy to run and fast to start. It is NOT intended to be secure, scalable, performant or persistent. It does NO input validation, authentication or authorization (it will not throw a 400, 401 or 403). It does not save data, and will start up empty each time you start it. Because Chef Zero runs in memory, it's super fast and lightweight. This makes it perfect for testing against a "real" Chef Server without mocking the entire Internet. Installation ------------ This server can be installed as a Ruby Gem. $ gem install chef-zero If you're using bundler, add `chef-zero` as a development dependency: ```ruby group :development do gem 'chef-zero' end ``` Or in a `.gemspec` ```ruby s.add_development_dependency 'chef-zero' ``` You can also clone the source repository and install it using `rake install`. Usage ----- One of chef-zero's primary uses is as a small test server for people writing and testing clients. Here's a simple example of starting it up: ```ruby require 'chef_zero/server' server = ChefZero::Server.new(port: 4000) server.start ``` This will create a server instance in the foreground. To stop the server: ```ruby server.stop ``` This is great for debugging and logging requests, but you'll probably want to run this in the background so you have full control of your thread. To run Chef Zero in the background, simply issue the `start_background` command: ```ruby require 'chef_zero/server' server = ChefZero::Server.new(port: 4000) server.start_background ``` You can stop the server the same way: ```ruby server.stop ``` ### Valid Options You may currently pass the following options to the initializer: - `host` - the host to run on (Default: '127.0.0.1') - `port` - the port to run on (Default: 8889) - `debug` - run in debug mode to see all requests and responses (Default: false) CLI (Comand Line) ----------------- If you don't want to use Chef Zero as a library, you can simply start an instance with the included `chef-zero` executable: $ chef-zero Note, this will run in the foreground. You now have a fully functional (empty) Chef Server running. To try it out, go into the `chef-zero/playground` directory and run `knife`. It will behave the same as a normal Chef Server, and all normal knife commands will work (show, list, delete, from file, upload, download, diff ...). For example, with +knife-essentials+ (or Chef 11) you can upload everything in the repo: chef-zero/playground> knife upload . Created nodes/desktop.json Created data_bags/dns Created environments/production.json Created nodes/lb.json Created nodes/dns.json Created nodes/ldap.json Created nodes/www.json Created data_bags/dns/services.json Created environments/staging.json Created data_bags/passwords Created data_bags/users Created data_bags/users/jkeiser.json Created data_bags/passwords/github.json Created data_bags/passwords/twitter.json Created data_bags/users/schisamo.json Created data_bags/users/sethvargo.json Created cookbooks/apache2 Created cookbooks/php chef-zero/playground> knife environment list _default production staging To use it in your own repository, create a `knife.rb` like so: chef_server_url 'http://127.0.0.1:8889' node_name 'stickywicket' client_key 'path_to_any_pem_file.pem' And use knife like you normally would. Since Chef Zero does no authentication, any `.pem` file will do. The client just needs something to sign requests with (which will be ignored on the server). Even though it's ignored, the `.pem` must still be a valid format. Now, stop the Chef Zero server and all the data is gone! Run `chef-zero --help` to see a list of the supported flags and options: ```text Usage: chef-zero [ARGS] -H, --host HOST Host to bind to (default: 127.0.0.1) -p, --port PORT Port to listen on --[no-]generate-keys Whether to generate actual keys or fake it (faster). Default: false. -l, --log-level LEVEL Set the output log level -h, --help Show this message --version Show version ``` chef-zero-2.0.1/checksums.yaml.gz0000444000175000017500000000041212263533061017104 0ustar stefanorstefanorhRe1V@"oatvXn,~? Y%zF(DeIfoFO=/_nF*?tҍ8ӟV@s#6Noa.ݺW>Hw ^󀩫p둢B'luzܛ  em"3p(aBkM 2H\\5xIiSep3\{@s>_isLQ!@ 8hu/YVY3r