diff options
author | danielsdeleo <dan@getchef.com> | 2015-02-04 16:22:40 -0800 |
---|---|---|
committer | danielsdeleo <dan@getchef.com> | 2015-02-11 10:52:49 -0800 |
commit | e36664c40c02c5dca2a77509e1a6d63689cb79fe (patch) | |
tree | 1f7a5ec3451e6c18ccda23461c5ad896c2d8a96a /lib/chef/cookbook_version.rb | |
parent | e5c31576bc579112bb85c9a30eb426cee50004b3 (diff) | |
download | chef-e36664c40c02c5dca2a77509e1a6d63689cb79fe.tar.gz |
Extract Cookbook JSON representation to class
Cookbook artifact API is going to have several behaviors that differ
from the current cookbook API. Extracting this code to one place will
give us a centralized location to handle the differences.
Diffstat (limited to 'lib/chef/cookbook_version.rb')
-rw-r--r-- | lib/chef/cookbook_version.rb | 401 |
1 files changed, 241 insertions, 160 deletions
diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb index 3bf19296c9..b2d98e7b8f 100644 --- a/lib/chef/cookbook_version.rb +++ b/lib/chef/cookbook_version.rb @@ -26,8 +26,208 @@ require 'chef/version_class' require 'pathname' require 'chef/digester' +require 'forwardable' + class Chef + # Handles the details of representing a cookbook in JSON form for uploading + # to a Chef Server. + class CookbookManifest + + # TODO: duplicates the same constant in CookbookVersion + COOKBOOK_SEGMENTS = [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates, :root_files ].freeze + + extend Forwardable + + attr_reader :cookbook_version + + def_delegator :@cookbook_version, :root_paths + def_delegator :@cookbook_version, :segment_filenames + def_delegator :@cookbook_version, :name + def_delegator :@cookbook_version, :metadata + def_delegator :@cookbook_version, :full_name + + def initialize(cookbook_version) + @cookbook_version = cookbook_version + + reset! + end + + # Resets all lazily computed values. + def reset! + @manifest = nil + @checksums = nil + @manifest_records_by_path = nil + true + end + + # Returns a 'manifest' data structure that can be uploaded to a Chef + # Server. + # + # The format is as follows: + # + # { + # :cookbook_name => name, # String + # :metadata => metadata, # Chef::Cookbook::Metadata + # :version => version, # Chef::Version + # :name => full_name, # String of "#{name}-#{version}" + # + # :recipes => Array<FileSpec>, + # :definitions => Array<FileSpec>, + # :libraries => Array<FileSpec>, + # :attributes => Array<FileSpec>, + # :files => Array<FileSpec>, + # :templates => Array<FileSpec>, + # :resources => Array<FileSpec>, + # :providers => Array<FileSpec>, + # :root_files => Array<FileSpec> + # } + # + # Where a `FileSpec` is a Hash of the form: + # + # { + # :name => file_name, + # :path => path, + # :checksum => csum, + # :specificity => specificity + # } + # + def manifest + @manifest || generate_manifest + @manifest + end + + def checksums + @manifest || generate_manifest + @checksums + end + + def manifest_records_by_path + @manifest || generate_manifest + @manifest_records_by_path + end + + # See #manifest for a description of the manifest return value. + # See #preferred_manifest_record for a description an individual manifest record. + def generate_manifest + manifest = Mash.new({ + :recipes => Array.new, + :definitions => Array.new, + :libraries => Array.new, + :attributes => Array.new, + :files => Array.new, + :templates => Array.new, + :resources => Array.new, + :providers => Array.new, + :root_files => Array.new + }) + @checksums = {} + + if !root_paths || root_paths.size == 0 + Chef::Log.error("Cookbook #{name} does not have root_paths! Cannot generate manifest.") + raise "Cookbook #{name} does not have root_paths! Cannot generate manifest." + end + + COOKBOOK_SEGMENTS.each do |segment| + segment_filenames(segment).each do |segment_file| + next if File.directory?(segment_file) + + path, specificity = parse_segment_file_from_root_paths(segment, segment_file) + file_name = File.basename(path) + + csum = checksum_cookbook_file(segment_file) + @checksums[csum] = segment_file + rs = Mash.new({ + :name => file_name, + :path => path, + :checksum => csum, + :specificity => specificity + }) + + manifest[segment] << rs + end + end + + manifest[:cookbook_name] = name.to_s + manifest[:metadata] = metadata + manifest[:version] = metadata.version + manifest[:name] = full_name + + @manifest_records_by_path = extract_manifest_records_by_path(manifest) + @manifest = manifest + end + + # TODO: This is kind of terrible. investigate removing it + def update_from(new_manifest) + @manifest = Mash.new new_manifest + @checksums = extract_checksums_from_manifest(@manifest) + @manifest_records_by_path = extract_manifest_records_by_path(@manifest) + + # TODO: this part of this method is "feature envious" it only deals with + # mutating the CookbookVersion object. + COOKBOOK_SEGMENTS.each do |segment| + next unless @manifest.has_key?(segment) + filenames = @manifest[segment].map{|manifest_record| manifest_record['name']} + + cookbook_version.replace_segment_filenames(segment, filenames) + end + end + + private + + def parse_segment_file_from_root_paths(segment, segment_file) + root_paths.each do |root_path| + pathname = Chef::Util::PathHelper.relative_path_from(root_path, segment_file) + + parts = pathname.each_filename.take(2) + # Check if path is actually under root_path + next if parts[0] == '..' + if segment == :templates || segment == :files + # Check if pathname looks like files/foo or templates/foo (unscoped) + if pathname.each_filename.to_a.length == 2 + # Use root_default in case the same path exists at root_default and default + return [ pathname.to_s, 'root_default' ] + else + return [ pathname.to_s, parts[1] ] + end + else + return [ pathname.to_s, 'default' ] + end + end + Chef::Log.error("Cookbook file #{segment_file} not under cookbook root paths #{root_paths.inspect}.") + raise "Cookbook file #{segment_file} not under cookbook root paths #{root_paths.inspect}." + end + + def extract_checksums_from_manifest(manifest) + checksums = {} + COOKBOOK_SEGMENTS.each do |segment| + next unless manifest.has_key?(segment) + manifest[segment].each do |manifest_record| + checksums[manifest_record[:checksum]] = nil + end + end + checksums + end + + # TODO: delegating to a class method like this is ugly. We should be able + # to fix this by moving logic into a class in a way that will make it easy + # to add support for SHA-2 + def checksum_cookbook_file(filepath) + CookbookVersion.checksum_cookbook_file(filepath) + end + + def extract_manifest_records_by_path(manifest) + manifest_records_by_path = {} + COOKBOOK_SEGMENTS.each do |segment| + next unless manifest.has_key?(segment) + manifest[segment].each do |manifest_record| + manifest_records_by_path[manifest_record[:path]] = manifest_record + end + end + manifest_records_by_path + end + end + # == Chef::CookbookVersion # CookbookVersion is a model object encapsulating the data about a Chef # cookbook. Chef supports maintaining multiple versions of a cookbook on a @@ -37,11 +237,40 @@ class Chef # TODO: timh/cw: 5-24-2010: mutators for files (e.g., recipe_filenames=, # recipe_filenames.insert) should dirty the manifest so it gets regenerated. class CookbookVersion + + ## METHODS DELEGATED TO CookbookResourceAdapter + # TODO: reorganize these + + private def cookbook_manifest + @cookbook_manifest ||= CookbookManifest.new(self) + end + + def manifest + cookbook_manifest.manifest + end + + # Returns a hash of checksums to either nil or the on disk path (which is + # done by generate_manifest). + def checksums + cookbook_manifest.checksums + end + + def manifest_records_by_path + cookbook_manifest.manifest_records_by_path + end + + def manifest=(new_manifest) + cookbook_manifest.update_from(new_manifest) + end + include Comparable COOKBOOK_SEGMENTS = [ :resources, :providers, :recipes, :definitions, :libraries, :attributes, :files, :templates, :root_files ] + # TODO: deprecate setter attr_accessor :root_paths + + # TODO: deprecate setter for all *_filenames, only used when consuming JSON attr_accessor :definition_filenames attr_accessor :template_filenames attr_accessor :file_filenames @@ -51,6 +280,8 @@ class Chef attr_accessor :root_filenames attr_accessor :name attr_accessor :metadata_filenames + + # TODO: unused, deprecate attr_accessor :status # A Chef::Cookbook::Metadata object. It has a setter that fixes up the @@ -95,6 +326,7 @@ class Chef @name = name @root_paths = root_paths @frozen = false + @attribute_filenames = Array.new @definition_filenames = Array.new @template_filenames = Array.new @@ -106,8 +338,9 @@ class Chef @provider_filenames = Array.new @metadata_filenames = Array.new @root_filenames = Array.new + + # TODO: unused, deprecate. @status = :ready - @manifest = nil @file_vendor = nil @metadata = Chef::Cookbook::Metadata.new end @@ -128,66 +361,10 @@ class Chef end def version=(new_version) - manifest["version"] = new_version + cookbook_manifest.reset! metadata.version(new_version) end - # A manifest is a Mash that maps segment names to arrays of manifest - # records (see #preferred_manifest_record for format of manifest records), - # as well as describing cookbook metadata. The manifest follows a form - # like the following: - # - # { - # :cookbook_name = "apache2", - # :version = "1.0", - # :name = "Apache 2" - # :metadata = ???TODO: timh/cw: 5-24-2010: describe this format, - # - # :files => [ - # { - # :name => "afile.rb", - # :path => "files/ubuntu-9.10/afile.rb", - # :checksum => "2222", - # :specificity => "ubuntu-9.10" - # }, - # ], - # :templates => [ manifest_record1, ... ], - # ... - # } - def manifest - unless @manifest - generate_manifest - end - @manifest - end - - def manifest=(new_manifest) - @manifest = Mash.new new_manifest - @checksums = extract_checksums_from_manifest(@manifest) - @manifest_records_by_path = extract_manifest_records_by_path(@manifest) - - COOKBOOK_SEGMENTS.each do |segment| - next unless @manifest.has_key?(segment) - filenames = @manifest[segment].map{|manifest_record| manifest_record['name']} - - replace_segment_filenames(segment, filenames) - end - end - - # Returns a hash of checksums to either nil or the on disk path (which is - # done by generate_manifest). - def checksums - unless @checksums - generate_manifest - end - @checksums - end - - def manifest_records_by_path - @manifest_records_by_path || generate_manifest - @manifest_records_by_path - end - def full_name "#{name}-#{version}" end @@ -310,7 +487,7 @@ class Chef def preferred_manifest_record(node, segment, filename) found_pref = find_preferred_manifest_record(node, segment, filename) if found_pref - @manifest_records_by_path[found_pref] + manifest_records_by_path[found_pref] else if segment == :files || segment == :templates error_message = "Cookbook '#{name}' (#{version}) does not contain a file at any of these locations:\n" @@ -497,7 +674,11 @@ class Chef cookbook_version end + # @deprecated This method was used by the Ruby Chef Server and is no longer + # needed. There is no replacement. def generate_manifest_with_urls(&url_generator) + Chef::Log.warn("Deprecated method #generate_manifest_with_urls called from #{caller(1).first}") + rendered_manifest = manifest.dup COOKBOOK_SEGMENTS.each do |segment| if rendered_manifest.has_key?(segment) @@ -605,12 +786,8 @@ class Chef def find_preferred_manifest_record(node, segment, filename) preferences = preferences_for_path(node, segment, filename) - # ensure that we generate the manifest, which will also generate - # @manifest_records_by_path - manifest - # in order of prefernce, look for the filename in the manifest - preferences.find {|preferred_filename| @manifest_records_by_path[preferred_filename] } + preferences.find {|preferred_filename| manifest_records_by_path[preferred_filename] } end # For each filename, produce a mapping of base filename (i.e. recipe name @@ -619,80 +796,6 @@ class Chef filenames.select{|filename| filename =~ /\.rb$/}.inject({}){|memo, filename| memo[File.basename(filename, '.rb')] = filename ; memo } end - # See #manifest for a description of the manifest return value. - # See #preferred_manifest_record for a description an individual manifest record. - def generate_manifest - manifest = Mash.new({ - :recipes => Array.new, - :definitions => Array.new, - :libraries => Array.new, - :attributes => Array.new, - :files => Array.new, - :templates => Array.new, - :resources => Array.new, - :providers => Array.new, - :root_files => Array.new - }) - checksums_to_on_disk_paths = {} - - if !root_paths || root_paths.size == 0 - Chef::Log.error("Cookbook #{name} does not have root_paths! Cannot generate manifest.") - raise "Cookbook #{name} does not have root_paths! Cannot generate manifest." - end - - COOKBOOK_SEGMENTS.each do |segment| - segment_filenames(segment).each do |segment_file| - next if File.directory?(segment_file) - - path, specificity = parse_segment_file_from_root_paths(segment, segment_file) - file_name = File.basename(path) - - csum = self.class.checksum_cookbook_file(segment_file) - checksums_to_on_disk_paths[csum] = segment_file - rs = Mash.new({ - :name => file_name, - :path => path, - :checksum => csum, - :specificity => specificity - }) - - manifest[segment] << rs - end - end - - manifest[:cookbook_name] = name.to_s - manifest[:metadata] = metadata - manifest[:version] = metadata.version - manifest[:name] = full_name - - @checksums = checksums_to_on_disk_paths - @manifest = manifest - @manifest_records_by_path = extract_manifest_records_by_path(manifest) - end - - def parse_segment_file_from_root_paths(segment, segment_file) - root_paths.each do |root_path| - pathname = Chef::Util::PathHelper.relative_path_from(root_path, segment_file) - - parts = pathname.each_filename.take(2) - # Check if path is actually under root_path - next if parts[0] == '..' - if segment == :templates || segment == :files - # Check if pathname looks like files/foo or templates/foo (unscoped) - if pathname.each_filename.to_a.length == 2 - # Use root_default in case the same path exists at root_default and default - return [ pathname.to_s, 'root_default' ] - else - return [ pathname.to_s, parts[1] ] - end - else - return [ pathname.to_s, 'default' ] - end - end - Chef::Log.error("Cookbook file #{segment_file} not under cookbook root paths #{root_paths.inspect}.") - raise "Cookbook file #{segment_file} not under cookbook root paths #{root_paths.inspect}." - end - def file_vendor unless @file_vendor @file_vendor = Chef::Cookbook::FileVendor.create_from_manifest(manifest) @@ -700,27 +803,5 @@ class Chef @file_vendor end - def extract_checksums_from_manifest(manifest) - checksums = {} - COOKBOOK_SEGMENTS.each do |segment| - next unless manifest.has_key?(segment) - manifest[segment].each do |manifest_record| - checksums[manifest_record[:checksum]] = nil - end - end - checksums - end - - def extract_manifest_records_by_path(manifest) - manifest_records_by_path = {} - COOKBOOK_SEGMENTS.each do |segment| - next unless manifest.has_key?(segment) - manifest[segment].each do |manifest_record| - manifest_records_by_path[manifest_record[:path]] = manifest_record - end - end - manifest_records_by_path - end - end end |