require "digest/md5" require "hashie" module ChefZero class Mash < ::Hashie::Mash disable_warnings end module ChefData module CookbookData class << self def to_hash(cookbook, name, version = nil) frozen = false if cookbook.key?(:frozen) frozen = cookbook[:frozen] cookbook = cookbook.dup cookbook.delete(:frozen) end result = files_from(cookbook) recipe_names = result[:all_files].select do |file| file[:name].start_with?("recipes/") end.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 metadata_from(directory, name, version, recipe_names) metadata = PretendCookbookMetadata.new(PretendCookbook.new(name, recipe_names)) # If both .rb and .json exist, read .json if has_child(directory, "metadata.json") metadata.from_json(read_file(directory, "metadata.json")) elsif 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 end result = {} metadata.to_hash.each_pair do |key, value| result[key.to_sym] = value end result[:version] = version if version result end private def files_from(directory) # TODO some support .rb only result = load_files(directory) set_specificity(result, :templates) set_specificity(result, :files) result = { all_files: result, } result end def has_child(directory, name) if directory.is_a?(Hash) directory.key?(name) else directory.child(name).exists? end end def read_file(directory, name) if directory.is_a?(Hash) directory[name] else directory.child(name).read end end def filename(directory, name) if directory.respond_to?(:file_path) File.join(directory.file_path, name) else nil end end def 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 list(directory) if directory.is_a?(Hash) directory.keys else directory.children.map(&:name) end end def load_child_files(parent, key, recursive, part) result = load_files(get_directory(parent, key), recursive, part) result.each do |file| file[:path] = "#{key}/#{file[:path]}" end result end def load_files(directory, recursive = true, part = nil) result = [] if directory list(directory).each do |child_name| dir = get_directory(directory, child_name) if dir child_part = child_name if part.nil? if recursive result += load_child_files(directory, child_name, recursive, child_part) end else result += load_file(read_file(directory, child_name), child_name, part) end end end result end def load_file(value, name, part = nil) specific_name = part ? "#{part}/#{name}" : name [{ name: specific_name, path: name, checksum: Digest::MD5.hexdigest(value), specificity: "default", }] end def set_specificity(files, type) files.each do |file| next unless file[:name].split("/")[0] == type.to_s parts = file[:path].split("/") file[:specificity] = if parts.size == 2 "default" else parts[1] end end end end # 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) name(cookbook.name) 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] = Mash.new end end def from_json(json) 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 provides(cookbook, *version_constraints) cookbook_arg(:providing, 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 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 end end CookbookData = ChefData::CookbookData end