require 'digest/md5' require 'hashie/mash' module ChefZero module ChefData 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(json) self.merge!(FFI_Yajl::Parser.parse(json)) 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 gem(*opts) self[:gems] ||= [] self[:gems] << opts 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, *values) if values.nil? self[key.to_sym] else if values.length > 1 store key.to_sym, values else store key.to_sym, values.first end 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', true), :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 CookbookData = ChefData::CookbookData end