summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordanielsdeleo <dan@getchef.com>2014-08-12 11:53:46 -0700
committerdanielsdeleo <dan@getchef.com>2014-08-12 11:53:46 -0700
commite07deb9e9f5e89bb3cc144b8629fece223a70943 (patch)
tree10c49903939af55bab148b54b4e408316557ed6c
parentd188093dccf97428dcf625c57a996faada88a31b (diff)
parenta0777738b985314d1f4abadde6f7c9687178d61f (diff)
downloadchef-e07deb9e9f5e89bb3cc144b8629fece223a70943.tar.gz
Merge branch 'respect-metadata-name'
Closes CHEF-3307, CHEF-3490.
-rw-r--r--lib/chef/cookbook/cookbook_version_loader.rb157
-rw-r--r--lib/chef/cookbook/metadata.rb118
-rw-r--r--lib/chef/cookbook_loader.rb76
-rw-r--r--lib/chef/cookbook_version.rb12
-rw-r--r--lib/chef/exceptions.rb2
-rw-r--r--pedant.gemfile2
-rw-r--r--spec/data/cookbooks/angrybash/metadata.rb2
-rw-r--r--spec/data/cookbooks/apache2/metadata.rb2
-rw-r--r--spec/data/cookbooks/borken/metadata.rb2
-rw-r--r--spec/data/cookbooks/ignorken/metadata.rb2
-rw-r--r--spec/data/cookbooks/java/metadata.rb2
-rw-r--r--spec/data/cookbooks/name-mismatch-versionnumber/README.md4
-rw-r--r--spec/data/cookbooks/name-mismatch-versionnumber/metadata.rb8
-rw-r--r--spec/data/cookbooks/name-mismatch-versionnumber/recipes/default.rb8
-rw-r--r--spec/data/cookbooks/preseed/metadata.rb2
-rw-r--r--spec/data/incomplete-metadata-chef-repo/incomplete-metadata/README.md4
-rw-r--r--spec/data/incomplete-metadata-chef-repo/incomplete-metadata/metadata.rb13
-rw-r--r--spec/data/incomplete-metadata-chef-repo/incomplete-metadata/recipes/default.rb8
-rw-r--r--spec/data/invalid-metadata-chef-repo/invalid-metadata/README.md4
-rw-r--r--spec/data/invalid-metadata-chef-repo/invalid-metadata/metadata.rb10
-rw-r--r--spec/data/invalid-metadata-chef-repo/invalid-metadata/recipes/default.rb8
-rw-r--r--spec/integration/knife/chef_fs_data_store_spec.rb11
-rw-r--r--spec/integration/knife/cookbook_api_ipv6_spec.rb2
-rw-r--r--spec/integration/solo/solo_spec.rb12
-rw-r--r--spec/unit/cookbook/cookbook_version_loader_spec.rb179
-rw-r--r--spec/unit/cookbook/metadata_spec.rb340
-rw-r--r--spec/unit/cookbook/syntax_check_spec.rb5
-rw-r--r--spec/unit/cookbook_loader_spec.rb167
28 files changed, 863 insertions, 299 deletions
diff --git a/lib/chef/cookbook/cookbook_version_loader.rb b/lib/chef/cookbook/cookbook_version_loader.rb
index fac8c80993..ddbcb785dc 100644
--- a/lib/chef/cookbook/cookbook_version_loader.rb
+++ b/lib/chef/cookbook/cookbook_version_loader.rb
@@ -18,21 +18,29 @@ class Chef
UPLOADED_COOKBOOK_VERSION_FILE = ".uploaded-cookbook-version.json".freeze
- attr_reader :cookbook_name
attr_reader :cookbook_settings
attr_reader :cookbook_paths
attr_reader :metadata_filenames
attr_reader :frozen
attr_reader :uploaded_cookbook_version_file
+ attr_reader :cookbook_path
+
+ # The cookbook's name as inferred from its directory.
+ attr_reader :inferred_cookbook_name
+
+ attr_reader :metadata_error
+
def initialize(path, chefignore=nil)
@cookbook_path = File.expand_path( path ) # cookbook_path from which this was loaded
# We keep a list of all cookbook paths that have been merged in
- @cookbook_paths = [ @cookbook_path ]
- @cookbook_name = File.basename( path )
+ @cookbook_paths = [ cookbook_path ]
+
+ @inferred_cookbook_name = File.basename( path )
@chefignore = chefignore
- @metadata = Hash.new
+ @metadata = nil
@relative_path = /#{Regexp.escape(@cookbook_path)}\/(.+)$/
+ @metadata_loaded = false
@cookbook_settings = {
:attribute_filenames => {},
:definition_filenames => {},
@@ -46,9 +54,29 @@ class Chef
}
@metadata_filenames = []
+ @metadata_error = nil
+ end
+
+ # Load the cookbook. Raises an error if the cookbook_path given to the
+ # constructor doesn't point to a valid cookbook.
+ def load!
+ file_paths_map = load
+
+ if empty?
+ raise Exceptions::CookbookNotFoundInRepo, "The directory #{cookbook_path} does not contain a cookbook"
+ end
+ file_paths_map
end
- def load_cookbooks
+ # Load the cookbook. Does not raise an error if given a non-cookbook
+ # directory as the cookbook_path. This behavior is provided for
+ # compatibility, it is recommended to use #load! instead.
+ def load
+ metadata # force lazy evaluation to occur
+
+ # re-raise any exception that occurred when reading the metadata
+ raise_metadata_error!
+
load_as(:attribute_filenames, 'attributes', '*.rb')
load_as(:definition_filenames, 'definitions', '*.rb')
load_as(:recipe_filenames, 'recipes', '*.rb')
@@ -61,31 +89,37 @@ class Chef
remove_ignored_files
- if File.exists?(File.join(@cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE))
- @uploaded_cookbook_version_file = File.join(@cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE)
+ if empty?
+ Chef::Log.warn "found a directory #{cookbook_name} in the cookbook path, but it contains no cookbook files. skipping."
+ end
+ @cookbook_settings
+ end
+
+ alias :load_cookbooks :load
+
+ def metadata_filenames
+ return @metadata_filenames unless @metadata_filenames.empty?
+ if File.exists?(File.join(cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE))
+ @uploaded_cookbook_version_file = File.join(cookbook_path, UPLOADED_COOKBOOK_VERSION_FILE)
end
- if File.exists?(File.join(@cookbook_path, "metadata.rb"))
- @metadata_filenames << File.join(@cookbook_path, "metadata.rb")
- elsif File.exists?(File.join(@cookbook_path, "metadata.json"))
- @metadata_filenames << File.join(@cookbook_path, "metadata.json")
+ if File.exists?(File.join(cookbook_path, "metadata.rb"))
+ @metadata_filenames << File.join(cookbook_path, "metadata.rb")
+ elsif File.exists?(File.join(cookbook_path, "metadata.json"))
+ @metadata_filenames << File.join(cookbook_path, "metadata.json")
elsif @uploaded_cookbook_version_file
@metadata_filenames << @uploaded_cookbook_version_file
end
# Set frozen based on .uploaded-cookbook-version.json
set_frozen
-
- if empty?
- Chef::Log.warn "found a directory #{cookbook_name} in the cookbook path, but it contains no cookbook files. skipping."
- end
- @cookbook_settings
+ @metadata_filenames
end
def cookbook_version
return nil if empty?
- Chef::CookbookVersion.new(@cookbook_name.to_sym, *@cookbook_paths).tap do |c|
+ Chef::CookbookVersion.new(cookbook_name, *cookbook_paths).tap do |c|
c.attribute_filenames = cookbook_settings[:attribute_filenames].values
c.definition_filenames = cookbook_settings[:definition_filenames].values
c.recipe_filenames = cookbook_settings[:recipe_filenames].values
@@ -95,16 +129,32 @@ class Chef
c.resource_filenames = cookbook_settings[:resource_filenames].values
c.provider_filenames = cookbook_settings[:provider_filenames].values
c.root_filenames = cookbook_settings[:root_filenames].values
- c.metadata_filenames = @metadata_filenames
- c.metadata = metadata(c)
+ c.metadata_filenames = metadata_filenames
+ c.metadata = metadata
+
c.freeze_version if @frozen
end
end
+ def cookbook_name
+ # The `name` attribute is now required in metadata, so
+ # inferred_cookbook_name generally should not be used. Per CHEF-2923,
+ # we have to not raise errors in cookbook metadata immediately, so that
+ # users can still `knife cookbook upload some-cookbook` when an
+ # unrelated cookbook has an error in its metadata. This situation
+ # could prevent us from reading the `name` attribute from the metadata
+ # entirely, but the name is used as a hash key in CookbookLoader, so we
+ # fall back to the inferred name here.
+ (metadata.name || @inferred_cookbook_name).to_sym
+ end
+
# Generates the Cookbook::Metadata object
- def metadata(cookbook_version)
- @metadata = Chef::Cookbook::Metadata.new(cookbook_version)
- @metadata_filenames.each do |metadata_file|
+ def metadata
+ return @metadata unless @metadata.nil?
+
+ @metadata = Chef::Cookbook::Metadata.new
+
+ metadata_filenames.each do |metadata_file|
case metadata_file
when /\.rb$/
apply_ruby_metadata(metadata_file)
@@ -116,51 +166,75 @@ class Chef
raise RuntimeError, "Invalid metadata file: #{metadata_file} for cookbook: #{cookbook_version}"
end
end
+
+ @metadata
+
+ # Rescue errors so that users can upload cookbooks via `knife cookbook
+ # upload` even if some cookbooks in their chef-repo have errors in
+ # their metadata. We only rescue StandardError because you have to be
+ # doing something *really* terrible to raise an exception that inherits
+ # directly from Exception in your metadata.rb file.
+ rescue StandardError => e
+ @metadata_error = e
@metadata
end
+ def raise_metadata_error!
+ raise @metadata_error unless @metadata_error.nil?
+ # Metadata won't be valid if the cookbook is empty. If the cookbook is
+ # actually empty, a metadata error here would be misleading, so don't
+ # raise it (if called by #load!, a different error is raised).
+ if !empty? && !metadata.valid?
+ message = "Cookbook loaded at path(s) [#{@cookbook_paths.join(', ')}] has invalid metadata: #{metadata.errors.join('; ')}"
+ raise Exceptions::MetadataNotValid, message
+ end
+ false
+ end
+
def empty?
- @cookbook_settings.values.all? { |files_hash| files_hash.empty? } && @metadata_filenames.size == 0
+ cookbook_settings.values.all? { |files_hash| files_hash.empty? } && metadata_filenames.size == 0
end
def merge!(other_cookbook_loader)
other_cookbook_settings = other_cookbook_loader.cookbook_settings
- @cookbook_settings.each do |file_type, file_list|
+ cookbook_settings.each do |file_type, file_list|
file_list.merge!(other_cookbook_settings[file_type])
end
- @metadata_filenames.concat(other_cookbook_loader.metadata_filenames)
+ metadata_filenames.concat(other_cookbook_loader.metadata_filenames)
@cookbook_paths += other_cookbook_loader.cookbook_paths
@frozen = true if other_cookbook_loader.frozen
+ @metadata = nil # reset metadata so it gets reloaded and all metadata files applied.
+ self
end
def chefignore
- @chefignore ||= Chefignore.new(File.basename(@cookbook_path))
+ @chefignore ||= Chefignore.new(File.basename(cookbook_path))
end
def load_root_files
- Dir.glob(File.join(@cookbook_path, '*'), File::FNM_DOTMATCH).each do |file|
+ Dir.glob(File.join(cookbook_path, '*'), File::FNM_DOTMATCH).each do |file|
next if File.directory?(file)
next if File.basename(file) == UPLOADED_COOKBOOK_VERSION_FILE
- @cookbook_settings[:root_filenames][file[@relative_path, 1]] = file
+ cookbook_settings[:root_filenames][file[@relative_path, 1]] = file
end
end
def load_recursively_as(category, category_dir, glob)
- file_spec = File.join(@cookbook_path, category_dir, '**', glob)
+ file_spec = File.join(cookbook_path, category_dir, '**', glob)
Dir.glob(file_spec, File::FNM_DOTMATCH).each do |file|
next if File.directory?(file)
- @cookbook_settings[category][file[@relative_path, 1]] = file
+ cookbook_settings[category][file[@relative_path, 1]] = file
end
end
def load_as(category, *path_glob)
- Dir[File.join(@cookbook_path, *path_glob)].each do |file|
- @cookbook_settings[category][file[@relative_path, 1]] = file
+ Dir[File.join(cookbook_path, *path_glob)].each do |file|
+ cookbook_settings[category][file[@relative_path, 1]] = file
end
end
def remove_ignored_files
- @cookbook_settings.each_value do |file_list|
+ cookbook_settings.each_value do |file_list|
file_list.reject! do |relative_path, full_path|
chefignore.ignored?(relative_path)
end
@@ -171,7 +245,7 @@ class Chef
begin
@metadata.from_file(file)
rescue Chef::Exceptions::JSON::ParseError
- Chef::Log.error("Error evaluating metadata.rb for #@cookbook_name in " + file)
+ Chef::Log.error("Error evaluating metadata.rb for #@inferred_cookbook_name in " + file)
raise
end
end
@@ -180,7 +254,7 @@ class Chef
begin
@metadata.from_json(IO.read(file))
rescue Chef::Exceptions::JSON::ParseError
- Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in " + file)
+ Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in " + file)
raise
end
end
@@ -189,8 +263,17 @@ class Chef
begin
data = Chef::JSONCompat.from_json(IO.read(file), :create_additions => false)
@metadata.from_hash(data['metadata'])
+ # the JSON cookbok metadata file is only used by chef-zero.
+ # The Chef Server API currently does not enforce that the metadata
+ # have a `name` field, but that will cause an error when attempting
+ # to load the cookbook. To keep compatibility, we fake it by setting
+ # the metadata name from the cookbook version object's name.
+ #
+ # This behavior can be removed if/when Chef Server enforces that the
+ # metadata contains a name key.
+ @metadata.name(data['cookbook_name']) unless data['metadata'].key?('name')
rescue Chef::Exceptions::JSON::ParseError
- Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in " + file)
+ Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in " + file)
raise
end
end
@@ -201,7 +284,7 @@ class Chef
data = Chef::JSONCompat.from_json(IO.read(uploaded_cookbook_version_file), :create_additions => false)
@frozen = data['frozen?']
rescue Chef::Exceptions::JSON::ParseError
- Chef::Log.error("Couldn't parse cookbook metadata JSON for #@cookbook_name in #{uploaded_cookbook_version_file}")
+ Chef::Log.error("Couldn't parse cookbook metadata JSON for #@inferred_cookbook_name in #{uploaded_cookbook_version_file}")
raise
end
end
diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb
index 8d3f8b84aa..3964354d50 100644
--- a/lib/chef/cookbook/metadata.rb
+++ b/lib/chef/cookbook/metadata.rb
@@ -18,6 +18,7 @@
# limitations under the License.
#
+require 'chef/exceptions'
require 'chef/mash'
require 'chef/mixin/from_file'
require 'chef/mixin/params_validate'
@@ -67,18 +68,17 @@ class Chef
include Chef::Mixin::ParamsValidate
include Chef::Mixin::FromFile
- attr_reader :cookbook,
- :platforms,
- :dependencies,
- :recommendations,
- :suggestions,
- :conflicting,
- :providing,
- :replacing,
- :attributes,
- :groupings,
- :recipes,
- :version
+ attr_reader :platforms
+ attr_reader :dependencies
+ attr_reader :recommendations
+ attr_reader :suggestions
+ attr_reader :conflicting
+ attr_reader :providing
+ attr_reader :replacing
+ attr_reader :attributes
+ attr_reader :groupings
+ attr_reader :recipes
+ attr_reader :version
# Builds a new Chef::Cookbook::Metadata object.
#
@@ -90,14 +90,16 @@ class Chef
#
# === Returns
# metadata<Chef::Cookbook::Metadata>
- def initialize(cookbook=nil, maintainer='YOUR_COMPANY_NAME', maintainer_email='YOUR_EMAIL', license='none')
- @cookbook = cookbook
- @name = cookbook ? cookbook.name : ""
- @long_description = ""
- self.maintainer(maintainer)
- self.maintainer_email(maintainer_email)
- self.license(license)
- self.description('A fabulous new cookbook')
+ def initialize
+ @name = nil
+
+ @description = ''
+ @long_description = ''
+ @license = 'All rights reserved'
+
+ @maintainer = nil
+ @maintainer_email = nil
+
@platforms = Mash.new
@dependencies = Mash.new
@recommendations = Mash.new
@@ -108,15 +110,9 @@ class Chef
@attributes = Mash.new
@groupings = Mash.new
@recipes = Mash.new
- @version = Version.new "0.0.0"
- if cookbook
- @recipes = cookbook.fully_qualified_recipe_names.inject({}) do |r, e|
- e = self.name.to_s if e =~ /::default$/
- r[e] ||= ""
- self.provides e
- r
- end
- end
+ @version = Version.new("0.0.0")
+
+ @errors = []
end
def ==(other)
@@ -125,6 +121,32 @@ class Chef
end
end
+ # Whether this metadata is valid. In order to be valid, all required
+ # fields must be set. Chef's validation implementation checks the content
+ # of a given field when setting (and raises an error if the content does
+ # not meet the criteria), so the content of the fields is not considered
+ # when checking validity.
+ #
+ # === Returns
+ # valid<Boolean>:: Whether this metadata object is valid
+ def valid?
+ run_validation
+ @errors.empty?
+ end
+
+ # A list of validation errors for this metadata object. See #valid? for
+ # comments about the validation criteria.
+ #
+ # If there are any validation errors, one or more error strings will be
+ # returned. Otherwise an empty array is returned.
+ #
+ # === Returns
+ # error messages<Array>:: Whether this metadata object is valid
+ def errors
+ run_validation
+ @errors
+ end
+
# Sets the cookbooks maintainer, or returns it.
#
# === Parameters
@@ -365,6 +387,32 @@ class Chef
@recipes[name] = description
end
+ # Sets the cookbook's recipes to the list of recipes in the given
+ # +cookbook+. Any recipe that already has a description (if set by the
+ # #recipe method) will not be updated.
+ #
+ # === Parameters
+ # cookbook<CookbookVersion>:: CookbookVersion object representing the cookbook
+ # description<String>:: The description of the recipe
+ #
+ # === Returns
+ # recipe_unqualified_names<Array>:: An array of the recipe names given by the cookbook
+ def recipes_from_cookbook_version(cookbook)
+ cookbook.fully_qualified_recipe_names.map do |recipe_name|
+ unqualified_name =
+ if recipe_name =~ /::default$/
+ self.name.to_s
+ else
+ recipe_name
+ end
+
+ @recipes[unqualified_name] ||= ""
+ provides(unqualified_name)
+
+ unqualified_name
+ end
+ end
+
# Adds an attribute )hat a user needs to configure for this cookbook. Takes
# a name (with the / notation for a nested attribute), followed by any of
# these options
@@ -480,9 +528,9 @@ class Chef
def self.validate_json(json_str)
o = Chef::JSONCompat.from_json(json_str)
metadata = new()
- VERSION_CONSTRAINTS.each do |method_name, hash_key|
- if constraints = o[hash_key]
- constraints.each do |cb_name, constraints|
+ VERSION_CONSTRAINTS.each do |dependency_type, hash_key|
+ if dependency_group = o[hash_key]
+ dependency_group.each do |cb_name, constraints|
metadata.send(method_name, cb_name, *Array(constraints))
end
end
@@ -497,6 +545,12 @@ class Chef
private
+ def run_validation
+ if name.nil?
+ @errors = ["The `name' attribute is required in cookbook metadata"]
+ end
+ end
+
def new_args_format(caller_name, dep_name, version_constraints)
if version_constraints.empty?
">= 0.0.0"
diff --git a/lib/chef/cookbook_loader.rb b/lib/chef/cookbook_loader.rb
index 27cf978acb..d569cdd008 100644
--- a/lib/chef/cookbook_loader.rb
+++ b/lib/chef/cookbook_loader.rb
@@ -50,6 +50,9 @@ class Chef
repo_path = File.expand_path(repo_path)
end
+ @preloaded_cookbooks = false
+ @loaders_by_name = {}
+
# Used to track which cookbooks appear in multiple places in the cookbook repos
# and are merged in to a single cookbook by file shadowing. This behavior is
# deprecated, so users of this class may issue warnings to the user by checking
@@ -64,25 +67,25 @@ class Chef
end
def load_cookbooks
- @repo_paths.each do |repo_path|
- Dir[File.join(repo_path, "*")].each do |cookbook_path|
- load_cookbook(File.basename(cookbook_path), [repo_path])
- end
+ preload_cookbooks
+ @loaders_by_name.each do |cookbook_name, _loaders|
+ load_cookbook(cookbook_name)
end
@cookbooks_by_name
end
- def load_cookbook(cookbook_name, repo_paths=nil)
- repo_paths ||= @repo_paths
- repo_paths.each do |repo_path|
- @chefignores[repo_path] ||= Cookbook::Chefignore.new(repo_path)
- cookbook_path = File.join(repo_path, cookbook_name.to_s)
- next unless File.directory?(cookbook_path) and Dir[File.join(repo_path, "*")].include?(cookbook_path)
- loader = Cookbook::CookbookVersionLoader.new(cookbook_path, @chefignores[repo_path])
- loader.load_cookbooks
+ def load_cookbook(cookbook_name)
+ preload_cookbooks
+
+ return nil unless @loaders_by_name.key?(cookbook_name.to_s)
+
+ cookbook_loaders_for(cookbook_name).each do |loader|
+ loader.load
+
next if loader.empty?
- cookbook_name = loader.cookbook_name
- @cookbooks_paths[cookbook_name] << cookbook_path # for deprecation warnings
+
+ @cookbooks_paths[cookbook_name] << loader.cookbook_path # for deprecation warnings
+
if @loaded_cookbooks.key?(cookbook_name)
@merged_cookbooks << cookbook_name # for deprecation warnings
@loaded_cookbooks[cookbook_name].merge!(loader)
@@ -130,5 +133,50 @@ class Chef
end
alias :cookbooks :values
+ private
+
+ def preload_cookbooks
+ return false if @preloaded_cookbooks
+
+ all_directories_in_repo_paths.each do |cookbook_path|
+ preload_cookbook(cookbook_path)
+ end
+ @preloaded_cookbooks = true
+ true
+ end
+
+ def preload_cookbook(cookbook_path)
+ repo_path = File.dirname(cookbook_path)
+ @chefignores[repo_path] ||= Cookbook::Chefignore.new(repo_path)
+ loader = Cookbook::CookbookVersionLoader.new(cookbook_path, @chefignores[repo_path])
+ add_cookbook_loader(loader)
+ end
+
+ def all_directories_in_repo_paths
+ @all_directories_in_repo_paths ||=
+ all_files_in_repo_paths.select { |path| File.directory?(path) }
+ end
+
+ def all_files_in_repo_paths
+ @all_files_in_repo_paths ||=
+ begin
+ @repo_paths.inject([]) do |all_children, repo_path|
+ all_children += Dir[File.join(repo_path, "*")]
+ end
+ end
+ end
+
+ def add_cookbook_loader(loader)
+ cookbook_name = loader.cookbook_name
+
+ @loaders_by_name[cookbook_name.to_s] ||= []
+ @loaders_by_name[cookbook_name.to_s] << loader
+ loader
+ end
+
+ def cookbook_loaders_for(cookbook_name)
+ @loaders_by_name[cookbook_name.to_s]
+ end
+
end
end
diff --git a/lib/chef/cookbook_version.rb b/lib/chef/cookbook_version.rb
index 4482f778c1..76e6d152b2 100644
--- a/lib/chef/cookbook_version.rb
+++ b/lib/chef/cookbook_version.rb
@@ -51,10 +51,14 @@ class Chef
attr_accessor :provider_filenames
attr_accessor :root_filenames
attr_accessor :name
- attr_accessor :metadata
attr_accessor :metadata_filenames
attr_accessor :status
+ # A Chef::Cookbook::Metadata object. It has a setter that fixes up the
+ # metadata to add descriptions of the recipes contained in this
+ # CookbookVersion.
+ attr_reader :metadata
+
# attribute_filenames also has a setter that has non-default
# functionality.
attr_reader :attribute_filenames
@@ -195,6 +199,12 @@ class Chef
attribute_filenames
end
+ def metadata=(metadata)
+ @metadata = metadata
+ @metadata.recipes_from_cookbook_version(self)
+ @metadata
+ end
+
## BACKCOMPAT/DEPRECATED - Remove these and fix breakage before release [DAN - 5/20/2010]##
alias :attribute_files :attribute_filenames
alias :attribute_files= :attribute_filenames=
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index bd6d887884..194c758f37 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -132,6 +132,8 @@ class Chef
# Version constraints are not allowed in chef-solo
class IllegalVersionConstraint < NotImplementedError; end
+ class MetadataNotValid < StandardError; end
+
# File operation attempted but no permissions to perform it
class InsufficientPermissions < RuntimeError; end
diff --git a/pedant.gemfile b/pedant.gemfile
index 2ce3ed27fb..7b135bdef4 100644
--- a/pedant.gemfile
+++ b/pedant.gemfile
@@ -2,7 +2,7 @@ source "https://rubygems.org"
gemspec :name => "chef"
gem 'rest-client', :github => 'opscode/rest-client', :branch => 'lcg/1.6.7-version-lying'
-gem 'chef-pedant', :github => 'opscode/chef-pedant', :ref => 'd12864964a18994b467908daa91811eee1f1bf68'
+gem 'chef-pedant', :github => 'opscode/chef-pedant', :branch => "metadata-name-fix"
# TODO figure out how to grab this stuff from the main Gemfile
gem "activesupport", "< 4.0.0", :group => :compat_testing, :platform => "ruby"
diff --git a/spec/data/cookbooks/angrybash/metadata.rb b/spec/data/cookbooks/angrybash/metadata.rb
new file mode 100644
index 0000000000..720b8085be
--- /dev/null
+++ b/spec/data/cookbooks/angrybash/metadata.rb
@@ -0,0 +1,2 @@
+name "angrybash"
+version "1.0.0"
diff --git a/spec/data/cookbooks/apache2/metadata.rb b/spec/data/cookbooks/apache2/metadata.rb
new file mode 100644
index 0000000000..1273d20d79
--- /dev/null
+++ b/spec/data/cookbooks/apache2/metadata.rb
@@ -0,0 +1,2 @@
+name "apache2"
+version "0.0.1"
diff --git a/spec/data/cookbooks/borken/metadata.rb b/spec/data/cookbooks/borken/metadata.rb
new file mode 100644
index 0000000000..58700b2d8e
--- /dev/null
+++ b/spec/data/cookbooks/borken/metadata.rb
@@ -0,0 +1,2 @@
+name "borken"
+version "1.0.0"
diff --git a/spec/data/cookbooks/ignorken/metadata.rb b/spec/data/cookbooks/ignorken/metadata.rb
new file mode 100644
index 0000000000..f91d92de32
--- /dev/null
+++ b/spec/data/cookbooks/ignorken/metadata.rb
@@ -0,0 +1,2 @@
+name "ignorken"
+version "1.0.0"
diff --git a/spec/data/cookbooks/java/metadata.rb b/spec/data/cookbooks/java/metadata.rb
new file mode 100644
index 0000000000..7c5585304c
--- /dev/null
+++ b/spec/data/cookbooks/java/metadata.rb
@@ -0,0 +1,2 @@
+name "java"
+version "0.0.1"
diff --git a/spec/data/cookbooks/name-mismatch-versionnumber/README.md b/spec/data/cookbooks/name-mismatch-versionnumber/README.md
new file mode 100644
index 0000000000..a61dc9a390
--- /dev/null
+++ b/spec/data/cookbooks/name-mismatch-versionnumber/README.md
@@ -0,0 +1,4 @@
+# name-mismatch
+
+TODO: Enter the cookbook description here.
+
diff --git a/spec/data/cookbooks/name-mismatch-versionnumber/metadata.rb b/spec/data/cookbooks/name-mismatch-versionnumber/metadata.rb
new file mode 100644
index 0000000000..81775bdcc8
--- /dev/null
+++ b/spec/data/cookbooks/name-mismatch-versionnumber/metadata.rb
@@ -0,0 +1,8 @@
+name 'name-mismatch'
+maintainer ''
+maintainer_email ''
+license ''
+description 'Installs/Configures name-mismatch'
+long_description 'Installs/Configures name-mismatch'
+version '0.1.0'
+
diff --git a/spec/data/cookbooks/name-mismatch-versionnumber/recipes/default.rb b/spec/data/cookbooks/name-mismatch-versionnumber/recipes/default.rb
new file mode 100644
index 0000000000..01d302f043
--- /dev/null
+++ b/spec/data/cookbooks/name-mismatch-versionnumber/recipes/default.rb
@@ -0,0 +1,8 @@
+#
+# Cookbook Name:: name-mismatch
+# Recipe:: default
+#
+# Copyright (C) 2014
+#
+#
+#
diff --git a/spec/data/cookbooks/preseed/metadata.rb b/spec/data/cookbooks/preseed/metadata.rb
new file mode 100644
index 0000000000..a48deec08b
--- /dev/null
+++ b/spec/data/cookbooks/preseed/metadata.rb
@@ -0,0 +1,2 @@
+name "preseed"
+version "1.0.0"
diff --git a/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/README.md b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/README.md
new file mode 100644
index 0000000000..3ac2a4f90c
--- /dev/null
+++ b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/README.md
@@ -0,0 +1,4 @@
+# incomplete-metadata
+
+TODO: Enter the cookbook description here.
+
diff --git a/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/metadata.rb b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/metadata.rb
new file mode 100644
index 0000000000..50284be3dd
--- /dev/null
+++ b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/metadata.rb
@@ -0,0 +1,13 @@
+# ## INCOMPLETE METADATA ##
+# This cookbook is invalid b/c it does not set the `name' of the cookbook.
+
+# Commented out for illustrative purposes
+# name 'incomplete-metadata'
+
+maintainer ''
+maintainer_email ''
+license ''
+description 'Installs/Configures incomplete-metadata'
+long_description 'Installs/Configures incomplete-metadata'
+version '0.1.0'
+
diff --git a/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/recipes/default.rb b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/recipes/default.rb
new file mode 100644
index 0000000000..65ae049ce8
--- /dev/null
+++ b/spec/data/incomplete-metadata-chef-repo/incomplete-metadata/recipes/default.rb
@@ -0,0 +1,8 @@
+#
+# Cookbook Name:: incomplete-metadata
+# Recipe:: default
+#
+# Copyright (C) 2014
+#
+#
+#
diff --git a/spec/data/invalid-metadata-chef-repo/invalid-metadata/README.md b/spec/data/invalid-metadata-chef-repo/invalid-metadata/README.md
new file mode 100644
index 0000000000..a41867f17c
--- /dev/null
+++ b/spec/data/invalid-metadata-chef-repo/invalid-metadata/README.md
@@ -0,0 +1,4 @@
+# invalid-metadata
+
+TODO: Enter the cookbook description here.
+
diff --git a/spec/data/invalid-metadata-chef-repo/invalid-metadata/metadata.rb b/spec/data/invalid-metadata-chef-repo/invalid-metadata/metadata.rb
new file mode 100644
index 0000000000..f548ad1dd8
--- /dev/null
+++ b/spec/data/invalid-metadata-chef-repo/invalid-metadata/metadata.rb
@@ -0,0 +1,10 @@
+raise "THIS METADATA HAS A BUG"
+
+name 'invalid-metadata'
+maintainer ''
+maintainer_email ''
+license ''
+description 'Installs/Configures invalid-metadata'
+long_description 'Installs/Configures invalid-metadata'
+version '0.1.0'
+
diff --git a/spec/data/invalid-metadata-chef-repo/invalid-metadata/recipes/default.rb b/spec/data/invalid-metadata-chef-repo/invalid-metadata/recipes/default.rb
new file mode 100644
index 0000000000..1411b9e39f
--- /dev/null
+++ b/spec/data/invalid-metadata-chef-repo/invalid-metadata/recipes/default.rb
@@ -0,0 +1,8 @@
+#
+# Cookbook Name:: invalid-metadata
+# Recipe:: default
+#
+# Copyright (C) 2014
+#
+#
+#
diff --git a/spec/integration/knife/chef_fs_data_store_spec.rb b/spec/integration/knife/chef_fs_data_store_spec.rb
index da1c72bd0c..c6737e08cb 100644
--- a/spec/integration/knife/chef_fs_data_store_spec.rb
+++ b/spec/integration/knife/chef_fs_data_store_spec.rb
@@ -26,10 +26,13 @@ describe 'ChefFSDataStore tests' do
include IntegrationSupport
include KnifeSupport
+ let(:cookbook_x_100_metadata_rb) { cb_metadata("x", "1.0.0") }
+ let(:cookbook_z_100_metadata_rb) { cb_metadata("z", "1.0.0") }
+
when_the_repository "has one of each thing" do
before do
file 'clients/x.json', {}
- file 'cookbooks/x/metadata.rb', 'version "1.0.0"'
+ file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb
file 'data_bags/x/y.json', {}
file 'environments/x.json', {}
file 'nodes/x.json', {}
@@ -108,7 +111,7 @@ EOM
end
it 'knife show -z /cookbooks/x/metadata.rb works' do
- knife('show -z /cookbooks/x/metadata.rb').should_succeed "/cookbooks/x/metadata.rb:\nversion \"1.0.0\"\n"
+ knife('show -z /cookbooks/x/metadata.rb').should_succeed "/cookbooks/x/metadata.rb:\n#{cookbook_x_100_metadata_rb}\n"
end
it 'knife show -z /data_bags/x/y.json works' do
@@ -136,7 +139,7 @@ EOM
before do
file 'empty.json', {}
file 'rolestuff.json', '{"description":"hi there","name":"x"}'
- file 'cookbooks_to_upload/x/metadata.rb', "version '1.0.0'\n\n"
+ file 'cookbooks_to_upload/x/metadata.rb', cookbook_x_100_metadata_rb
end
it 'knife raw -z -i empty.json -m PUT /clients/x' do
@@ -196,7 +199,7 @@ EOM
file 'empty_x.json', { 'name' => 'x' }
file 'empty_id.json', { 'id' => 'z' }
file 'rolestuff.json', '{"description":"hi there","name":"x"}'
- file 'cookbooks_to_upload/z/metadata.rb', "version '1.0.0'"
+ file 'cookbooks_to_upload/z/metadata.rb', cookbook_z_100_metadata_rb
end
it 'knife raw -z -i empty.json -m POST /clients' do
diff --git a/spec/integration/knife/cookbook_api_ipv6_spec.rb b/spec/integration/knife/cookbook_api_ipv6_spec.rb
index ac2538dc38..b3efae6cd3 100644
--- a/spec/integration/knife/cookbook_api_ipv6_spec.rb
+++ b/spec/integration/knife/cookbook_api_ipv6_spec.rb
@@ -101,7 +101,7 @@ END_CLIENT_RB
it "downloads the cookbook" do
shell_out!("knife cookbook download apache2 #{knife_config_flag} -d #{cache_path}", :cwd => chef_dir)
- Dir["#{cache_path}/*"].map {|entry| File.basename(entry)}.should include("apache2-0.0.0")
+ Dir["#{cache_path}/*"].map {|entry| File.basename(entry)}.should include("apache2-0.0.1")
end
end
diff --git a/spec/integration/solo/solo_spec.rb b/spec/integration/solo/solo_spec.rb
index b10ff379e2..793789b754 100644
--- a/spec/integration/solo/solo_spec.rb
+++ b/spec/integration/solo/solo_spec.rb
@@ -11,9 +11,13 @@ describe "chef-solo" do
let(:chef_dir) { File.join(File.dirname(__FILE__), "..", "..", "..") }
+ let(:cookbook_x_100_metadata_rb) { cb_metadata("x", "1.0.0") }
+
+ let(:cookbook_ancient_100_metadata_rb) { cb_metadata("ancient", "1.0.0") }
+
when_the_repository "has a cookbook with a basic recipe" do
before do
- file 'cookbooks/x/metadata.rb', 'version "1.0.0"'
+ file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb
file 'cookbooks/x/recipes/default.rb', 'puts "ITWORKS"'
end
@@ -46,10 +50,10 @@ E
when_the_repository "has a cookbook with an undeclared dependency" do
before do
- file 'cookbooks/x/metadata.rb', 'version "1.0.0"'
+ file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb
file 'cookbooks/x/recipes/default.rb', 'include_recipe "ancient::aliens"'
- file 'cookbooks/ancient/metadata.rb', 'version "1.0.0"'
+ file 'cookbooks/ancient/metadata.rb', cookbook_ancient_100_metadata_rb
file 'cookbooks/ancient/recipes/aliens.rb', 'print "it was aliens"'
end
@@ -69,7 +73,7 @@ EOM
before do
directory 'logs'
file 'logs/runs.log', ''
- file 'cookbooks/x/metadata.rb', 'version "1.0.0"'
+ file 'cookbooks/x/metadata.rb', cookbook_x_100_metadata_rb
file 'cookbooks/x/recipes/default.rb', <<EOM
ruby_block "sleeping" do
block do
diff --git a/spec/unit/cookbook/cookbook_version_loader_spec.rb b/spec/unit/cookbook/cookbook_version_loader_spec.rb
new file mode 100644
index 0000000000..ad10f24573
--- /dev/null
+++ b/spec/unit/cookbook/cookbook_version_loader_spec.rb
@@ -0,0 +1,179 @@
+#
+# Author:: Daniel DeLeo (<dan@getchef.com>)
+# Copyright:: Copyright (c) 2014 Chef Software, 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 'spec_helper'
+
+describe Chef::Cookbook::CookbookVersionLoader do
+
+ describe "loading a cookbook" do
+
+ let(:chefignore) { nil }
+
+ let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "cookbooks/openldap") }
+
+ let(:cookbook_loader) { Chef::Cookbook::CookbookVersionLoader.new(cookbook_path, chefignore) }
+
+ let(:loaded_cookbook) do
+ cookbook_loader.load!
+ cookbook_loader.cookbook_version
+ end
+
+ def full_path(cookbook_relative_path)
+ File.join(cookbook_path, cookbook_relative_path)
+ end
+
+ it "loads attribute files of the cookbook" do
+ expect(loaded_cookbook.attribute_filenames).to include(full_path("/attributes/default.rb"))
+ expect(loaded_cookbook.attribute_filenames).to include(full_path("/attributes/smokey.rb"))
+ end
+
+ it "loads definition files" do
+ expect(loaded_cookbook.definition_filenames).to include(full_path("/definitions/client.rb"))
+ expect(loaded_cookbook.definition_filenames).to include(full_path("/definitions/server.rb"))
+ end
+
+ it "loads recipes" do
+ expect(loaded_cookbook.recipe_filenames).to include(full_path("/recipes/default.rb"))
+ expect(loaded_cookbook.recipe_filenames).to include(full_path("/recipes/gigantor.rb"))
+ expect(loaded_cookbook.recipe_filenames).to include(full_path("/recipes/one.rb"))
+ expect(loaded_cookbook.recipe_filenames).to include(full_path("/recipes/return.rb"))
+ end
+
+ it "loads static files in the files/ dir" do
+ expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/remotedir/remotesubdir/remote_subdir_file1.txt"))
+ expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/remotedir/remotesubdir/remote_subdir_file2.txt"))
+ end
+
+ it "loads files that start with a ." do
+ expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/.dotfile"))
+ expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/.ssh/id_rsa"))
+ expect(loaded_cookbook.file_filenames).to include(full_path("/files/default/remotedir/.a_dotdir/.a_dotfile_in_a_dotdir"))
+ end
+
+ it "should load the metadata for the cookbook" do
+ loaded_cookbook.metadata.name.to_s.should == "openldap"
+ loaded_cookbook.metadata.should be_a_kind_of(Chef::Cookbook::Metadata)
+ end
+
+ context "when a cookbook has ignored files" do
+
+ let(:chefignore) { Chef::Cookbook::Chefignore.new(File.join(CHEF_SPEC_DATA, "cookbooks")) }
+
+ let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "kitchen/openldap") }
+
+ it "skips ignored files" do
+ expect(loaded_cookbook.recipe_filenames).to include(full_path("recipes/gigantor.rb"))
+ expect(loaded_cookbook.recipe_filenames).to include(full_path("recipes/woot.rb"))
+ expect(loaded_cookbook.recipe_filenames).to_not include(full_path("recipes/ignoreme.rb"))
+ end
+
+ end
+
+ context "when the given path is not actually a cookbook" do
+
+ let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "cookbooks/NOTHING_HERE_FOLKS") }
+
+ it "raises an error when loading with #load!" do
+ expect { cookbook_loader.load! }.to raise_error(Chef::Exceptions::CookbookNotFoundInRepo)
+ end
+
+ it "skips the cookbook when called with #load" do
+ expect { cookbook_loader.load }.to_not raise_error
+ end
+
+ end
+
+ context "when a cookbook has a metadata name different than directory basename" do
+
+ let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "cookbooks/name-mismatch-versionnumber") }
+
+ it "prefers the metadata name to the directory basename" do
+ expect(loaded_cookbook.name).to eq(:"name-mismatch")
+ end
+
+ end
+
+ context "when a cookbook has a metadata file with a ruby error [CHEF-2923]" do
+
+ let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "invalid-metadata-chef-repo/invalid-metadata") }
+
+ it "raises an error when loading with #load!" do
+ expect { cookbook_loader.load! }.to raise_error("THIS METADATA HAS A BUG")
+ end
+
+ it "raises an error when called with #load" do
+ expect { cookbook_loader.load }.to raise_error("THIS METADATA HAS A BUG")
+ end
+
+ it "doesn't raise an error when determining the cookbook name" do
+ expect { cookbook_loader.cookbook_name }.to_not raise_error
+ end
+
+ it "doesn't raise an error when metadata is first generated" do
+ expect { cookbook_loader.metadata }.to_not raise_error
+ end
+
+ it "sets an error flag containing error information" do
+ cookbook_loader.metadata
+ expect(cookbook_loader.metadata_error).to be_a(StandardError)
+ expect(cookbook_loader.metadata_error.message).to eq("THIS METADATA HAS A BUG")
+ end
+
+ end
+
+ context "when a cookbook has a metadata file with invalid metadata" do
+
+ let(:cookbook_path) { File.join(CHEF_SPEC_DATA, "incomplete-metadata-chef-repo/incomplete-metadata") }
+
+ let(:error_message) do
+ "Cookbook loaded at path(s) [#{cookbook_path}] has invalid metadata: The `name' attribute is required in cookbook metadata"
+ end
+
+ it "raises an error when loading with #load!" do
+ expect { cookbook_loader.load! }.to raise_error(Chef::Exceptions::MetadataNotValid, error_message)
+ end
+
+ it "raises an error when called with #load" do
+ expect { cookbook_loader.load }.to raise_error(Chef::Exceptions::MetadataNotValid, error_message)
+ end
+
+ it "uses the inferred cookbook name [CHEF-2923]" do
+ # This behavior is intended to support the CHEF-2923 feature where
+ # invalid metadata doesn't prevent you from uploading other cookbooks.
+ #
+ # The metadata is the definitive source of the cookbook name, but if
+ # the metadata is incomplete/invalid, we can't read the name from it.
+ #
+ # The CookbookLoader stores CookbookVersionLoaders in a Hash with
+ # cookbook names as the keys, and finds the loader in this Hash to call
+ # #load on it when the user runs a command like `knife cookbook upload specific-cookbook`
+ #
+ # Most of the time this will work, but if the user tries to upload a
+ # specific cookbook by name, has customized that cookbook's name (so it
+ # doesn't match the inferred name), and that metadata file has a syntax
+ # error, we might report a "cookbook not found" error instead of the
+ # metadata syntax error that is the actual cause.
+ expect(cookbook_loader.cookbook_name).to eq(:"incomplete-metadata")
+ end
+
+ end
+
+ end
+
+end
+
diff --git a/spec/unit/cookbook/metadata_spec.rb b/spec/unit/cookbook/metadata_spec.rb
index 8e98610183..e61c85b42b 100644
--- a/spec/unit/cookbook/metadata_spec.rb
+++ b/spec/unit/cookbook/metadata_spec.rb
@@ -21,10 +21,8 @@ require 'spec_helper'
require 'chef/cookbook/metadata'
describe Chef::Cookbook::Metadata do
- before(:each) do
- @cookbook = Chef::CookbookVersion.new('test_cookbook')
- @meta = Chef::Cookbook::Metadata.new(@cookbook)
- end
+
+ let(:metadata) { Chef::Cookbook::Metadata.new }
describe "when comparing for equality" do
before do
@@ -35,7 +33,7 @@ describe Chef::Cookbook::Metadata do
end
it "does not depend on object identity for equality" do
- @meta.should == @meta.dup
+ metadata.should == metadata.dup
end
it "is not equal to another object if it isn't have all of the metadata fields" do
@@ -45,7 +43,7 @@ describe Chef::Cookbook::Metadata do
almost_duck_type = Struct.new(*fields_to_include).new
@fields.each do |field|
setter = "#{field}="
- metadata_value = @meta.send(field)
+ metadata_value = metadata.send(field)
almost_duck_type.send(setter, metadata_value) if almost_duck_type.respond_to?(setter)
@mets.should_not == almost_duck_type
end
@@ -56,10 +54,10 @@ describe Chef::Cookbook::Metadata do
duck_type = Struct.new(*@fields).new
@fields.each do |field|
setter = "#{field}="
- metadata_value = @meta.send(field)
+ metadata_value = metadata.send(field)
duck_type.send(setter, metadata_value)
end
- @meta.should == duck_type
+ metadata.should == duck_type
end
it "is not equal if any values are different" do
@@ -69,78 +67,117 @@ describe Chef::Cookbook::Metadata do
@fields.each do |field|
setter = "#{field}="
- metadata_value = @meta.send(field)
+ metadata_value = metadata.send(field)
duck_type.send(setter, metadata_value)
end
- field_to_change
-
duck_type.send("#{field_to_change}=".to_sym, :epic_fail)
- @meta.should_not == duck_type
+ metadata.should_not == duck_type
end
end
end
describe "when first created" do
- it "should return a Chef::Cookbook::Metadata object" do
- @meta.should be_a_kind_of(Chef::Cookbook::Metadata)
+
+ it "has no name" do
+ metadata.name.should eq(nil)
end
- it "should allow a cookbook as the first argument" do
- lambda { Chef::Cookbook::Metadata.new(@cookbook) }.should_not raise_error
+ it "has an empty description" do
+ metadata.description.should eq("")
end
- it "should allow an maintainer name for the second argument" do
- lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown') }.should_not raise_error
+ it "has an empty long description" do
+ metadata.long_description.should eq("")
end
- it "should set the maintainer name from the second argument" do
- md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown')
- md.maintainer.should == 'Bobo T. Clown'
+ it "defaults to 'all rights reserved' license" do
+ metadata.license.should eq("All rights reserved")
end
- it "should allow an maintainer email for the third argument" do
- lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co') }.should_not raise_error
+ it "has an empty maintainer field" do
+ metadata.maintainer.should eq(nil)
end
- it "should set the maintainer email from the third argument" do
- md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co')
- md.maintainer_email.should == 'bobo@clown.co'
+ it "has an empty maintainer_email field" do
+ metadata.maintainer.should eq(nil)
end
- it "should allow a license for the fourth argument" do
- lambda { Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co', 'Clown License v1') }.should_not raise_error
+ it "has an empty platforms list" do
+ metadata.platforms.should eq(Mash.new)
end
- it "should set the license from the fourth argument" do
- md = Chef::Cookbook::Metadata.new(@cookbook, 'Bobo T. Clown', 'bobo@clown.co', 'Clown License v1')
- md.license.should == 'Clown License v1'
+ it "has an empty dependencies list" do
+ metadata.dependencies.should eq(Mash.new)
end
- end
- describe "cookbook" do
- it "should return the cookbook we were initialized with" do
- @meta.cookbook.should eql(@cookbook)
+ it "has an empty recommends list" do
+ metadata.recommendations.should eq(Mash.new)
end
- end
- describe "name" do
- it "should return the name of the cookbook" do
- @meta.name.should eql(@cookbook.name)
+ it "has an empty suggestions list" do
+ metadata.suggestions.should eq(Mash.new)
end
+
+ it "has an empty conflicts list" do
+ metadata.conflicting.should eq(Mash.new)
+ end
+
+ it "has an empty replaces list" do
+ metadata.replacing.should eq(Mash.new)
+ end
+
+ it "has an empty attributes list" do
+ metadata.attributes.should eq(Mash.new)
+ end
+
+ it "has an empty groupings list" do
+ metadata.groupings.should eq(Mash.new)
+ end
+
+ it "has an empty recipes list" do
+ metadata.recipes.should eq(Mash.new)
+ end
+
end
- describe "platforms" do
- it "should return the current platform hash" do
- @meta.platforms.should be_a_kind_of(Hash)
+ describe "validation" do
+
+ context "when no required fields are set" do
+
+ it "is not valid" do
+ metadata.should_not be_valid
+ end
+
+ it "has a list of validation errors" do
+ expected_errors = ["The `name' attribute is required in cookbook metadata"]
+ metadata.errors.should eq(expected_errors)
+ end
+
end
+
+ context "when all required fields are set" do
+ before do
+ metadata.name "a-valid-name"
+ end
+
+ it "is valid" do
+ metadata.should be_valid
+ end
+
+ it "has no validation errors" do
+ metadata.errors.should be_empty
+ end
+
+ end
+
end
describe "adding a supported platform" do
it "should support adding a supported platform with a single expression" do
- @meta.supports("ubuntu", ">= 8.04")
- @meta.platforms["ubuntu"].should == '>= 8.04'
+ metadata.supports("ubuntu", ">= 8.04")
+ metadata.platforms["ubuntu"].should == '>= 8.04'
end
end
@@ -156,23 +193,23 @@ describe Chef::Cookbook::Metadata do
params.sort { |a,b| a.to_s <=> b.to_s }.each do |field, field_value|
describe field do
it "should be set-able via #{field}" do
- @meta.send(field, field_value).should eql(field_value)
+ metadata.send(field, field_value).should eql(field_value)
end
it "should be get-able via #{field}" do
- @meta.send(field, field_value)
- @meta.send(field).should eql(field_value)
+ metadata.send(field, field_value)
+ metadata.send(field).should eql(field_value)
end
end
end
describe "version transformation" do
it "should transform an '0.6' version to '0.6.0'" do
- @meta.send(:version, "0.6").should eql("0.6.0")
+ metadata.send(:version, "0.6").should eql("0.6.0")
end
it "should spit out '0.6.0' after transforming '0.6'" do
- @meta.send(:version, "0.6")
- @meta.send(:version).should eql("0.6.0")
+ metadata.send(:version, "0.6")
+ metadata.send(:version).should eql("0.6.0")
end
end
end
@@ -191,11 +228,11 @@ describe Chef::Cookbook::Metadata do
check_with = dep_args.shift
describe dep do
it "should be set-able via #{dep}" do
- @meta.send(dep, *dep_args).should == dep_args[1]
+ metadata.send(dep, *dep_args).should == dep_args[1]
end
it "should be get-able via #{check_with}" do
- @meta.send(dep, *dep_args)
- @meta.send(check_with).should == { dep_args[0] => dep_args[1] }
+ metadata.send(dep, *dep_args)
+ metadata.send(check_with).should == { dep_args[0] => dep_args[1] }
end
end
end
@@ -213,11 +250,11 @@ describe Chef::Cookbook::Metadata do
normalized_version = dep_args.pop
describe dep do
it "should be set-able and normalized via #{dep}" do
- @meta.send(dep, *dep_args).should == normalized_version
+ metadata.send(dep, *dep_args).should == normalized_version
end
it "should be get-able and normalized via #{check_with}" do
- @meta.send(dep, *dep_args)
- @meta.send(check_with).should == { dep_args[0] => normalized_version }
+ metadata.send(dep, *dep_args)
+ metadata.send(check_with).should == { dep_args[0] => normalized_version }
end
end
end
@@ -235,7 +272,7 @@ describe Chef::Cookbook::Metadata do
dep_types.each do |dep, dep_args|
it "for #{dep} raises an informative error instead of vomiting on your shoes" do
- lambda {@meta.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::ObsoleteDependencySyntax)
+ lambda {metadata.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::ObsoleteDependencySyntax)
end
end
end
@@ -253,7 +290,7 @@ describe Chef::Cookbook::Metadata do
dep_types.each do |dep, dep_args|
it "for #{dep} raises an informative error instead of vomiting on your shoes" do
- lambda {@meta.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::InvalidVersionConstraint)
+ lambda {metadata.send(dep, *dep_args)}.should raise_error(Chef::Exceptions::InvalidVersionConstraint)
end
end
end
@@ -265,23 +302,23 @@ describe Chef::Cookbook::Metadata do
"title" => "MySQL Tuning",
"description" => "Setting from the my.cnf file that allow you to tune your mysql server"
}
- @meta.grouping("/db/mysql/databases/tuning", group).should == group
+ metadata.grouping("/db/mysql/databases/tuning", group).should == group
end
it "should not accept anything but a string for display_name" do
lambda {
- @meta.grouping("db/mysql/databases", :title => "foo")
+ metadata.grouping("db/mysql/databases", :title => "foo")
}.should_not raise_error
lambda {
- @meta.grouping("db/mysql/databases", :title => Hash.new)
+ metadata.grouping("db/mysql/databases", :title => Hash.new)
}.should raise_error(ArgumentError)
end
it "should not accept anything but a string for the description" do
lambda {
- @meta.grouping("db/mysql/databases", :description => "foo")
+ metadata.grouping("db/mysql/databases", :description => "foo")
}.should_not raise_error
lambda {
- @meta.grouping("db/mysql/databases", :description => Hash.new)
+ metadata.grouping("db/mysql/databases", :description => Hash.new)
}.should raise_error(ArgumentError)
end
end
@@ -298,151 +335,151 @@ describe Chef::Cookbook::Metadata do
"recipes" => [ "mysql::server", "mysql::master" ],
"default" => [ ]
}
- @meta.attribute("/db/mysql/databases", attrs).should == attrs
+ metadata.attribute("/db/mysql/databases", attrs).should == attrs
end
it "should not accept anything but a string for display_name" do
lambda {
- @meta.attribute("db/mysql/databases", :display_name => "foo")
+ metadata.attribute("db/mysql/databases", :display_name => "foo")
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :display_name => Hash.new)
+ metadata.attribute("db/mysql/databases", :display_name => Hash.new)
}.should raise_error(ArgumentError)
end
it "should not accept anything but a string for the description" do
lambda {
- @meta.attribute("db/mysql/databases", :description => "foo")
+ metadata.attribute("db/mysql/databases", :description => "foo")
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :description => Hash.new)
+ metadata.attribute("db/mysql/databases", :description => Hash.new)
}.should raise_error(ArgumentError)
end
it "should not accept anything but an array of strings for choice" do
lambda {
- @meta.attribute("db/mysql/databases", :choice => ['dedicated', 'shared'])
+ metadata.attribute("db/mysql/databases", :choice => ['dedicated', 'shared'])
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :choice => [10, 'shared'])
+ metadata.attribute("db/mysql/databases", :choice => [10, 'shared'])
}.should raise_error(ArgumentError)
lambda {
- @meta.attribute("db/mysql/databases", :choice => Hash.new)
+ metadata.attribute("db/mysql/databases", :choice => Hash.new)
}.should raise_error(ArgumentError)
end
it "should set choice to empty array by default" do
- @meta.attribute("db/mysql/databases", {})
- @meta.attributes["db/mysql/databases"][:choice].should == []
+ metadata.attribute("db/mysql/databases", {})
+ metadata.attributes["db/mysql/databases"][:choice].should == []
end
it "should let calculated be true or false" do
lambda {
- @meta.attribute("db/mysql/databases", :calculated => true)
+ metadata.attribute("db/mysql/databases", :calculated => true)
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :calculated => false)
+ metadata.attribute("db/mysql/databases", :calculated => false)
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :calculated => Hash.new)
+ metadata.attribute("db/mysql/databases", :calculated => Hash.new)
}.should raise_error(ArgumentError)
end
it "should set calculated to false by default" do
- @meta.attribute("db/mysql/databases", {})
- @meta.attributes["db/mysql/databases"][:calculated].should == false
+ metadata.attribute("db/mysql/databases", {})
+ metadata.attributes["db/mysql/databases"][:calculated].should == false
end
it "accepts String for the attribute type" do
lambda {
- @meta.attribute("db/mysql/databases", :type => "string")
+ metadata.attribute("db/mysql/databases", :type => "string")
}.should_not raise_error
end
it "accepts Array for the attribute type" do
lambda {
- @meta.attribute("db/mysql/databases", :type => "array")
+ metadata.attribute("db/mysql/databases", :type => "array")
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :type => Array.new)
+ metadata.attribute("db/mysql/databases", :type => Array.new)
}.should raise_error(ArgumentError)
end
it "accepts symbol for the attribute type" do
lambda {
- @meta.attribute("db/mysql/databases", :type => "symbol")
+ metadata.attribute("db/mysql/databases", :type => "symbol")
}.should_not raise_error
end
it "should let type be hash (backwards compatability only)" do
lambda {
- @meta.attribute("db/mysql/databases", :type => "hash")
+ metadata.attribute("db/mysql/databases", :type => "hash")
}.should_not raise_error
end
it "should let required be required, recommended or optional" do
lambda {
- @meta.attribute("db/mysql/databases", :required => 'required')
+ metadata.attribute("db/mysql/databases", :required => 'required')
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :required => 'recommended')
+ metadata.attribute("db/mysql/databases", :required => 'recommended')
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :required => 'optional')
+ metadata.attribute("db/mysql/databases", :required => 'optional')
}.should_not raise_error
end
it "should convert required true to required" do
lambda {
- @meta.attribute("db/mysql/databases", :required => true)
+ metadata.attribute("db/mysql/databases", :required => true)
}.should_not raise_error
- #attrib = @meta.attributes["db/mysql/databases"][:required].should == "required"
+ #attrib = metadata.attributes["db/mysql/databases"][:required].should == "required"
end
it "should convert required false to optional" do
lambda {
- @meta.attribute("db/mysql/databases", :required => false)
+ metadata.attribute("db/mysql/databases", :required => false)
}.should_not raise_error
- #attrib = @meta.attributes["db/mysql/databases"][:required].should == "optional"
+ #attrib = metadata.attributes["db/mysql/databases"][:required].should == "optional"
end
it "should set required to 'optional' by default" do
- @meta.attribute("db/mysql/databases", {})
- @meta.attributes["db/mysql/databases"][:required].should == 'optional'
+ metadata.attribute("db/mysql/databases", {})
+ metadata.attributes["db/mysql/databases"][:required].should == 'optional'
end
it "should make sure recipes is an array" do
lambda {
- @meta.attribute("db/mysql/databases", :recipes => [])
+ metadata.attribute("db/mysql/databases", :recipes => [])
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :required => Hash.new)
+ metadata.attribute("db/mysql/databases", :required => Hash.new)
}.should raise_error(ArgumentError)
end
it "should set recipes to an empty array by default" do
- @meta.attribute("db/mysql/databases", {})
- @meta.attributes["db/mysql/databases"][:recipes].should == []
+ metadata.attribute("db/mysql/databases", {})
+ metadata.attributes["db/mysql/databases"][:recipes].should == []
end
it "should allow the default value to be a string, array, hash, boolean or numeric" do
lambda {
- @meta.attribute("db/mysql/databases", :default => [])
+ metadata.attribute("db/mysql/databases", :default => [])
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :default => {})
+ metadata.attribute("db/mysql/databases", :default => {})
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :default => "alice in chains")
+ metadata.attribute("db/mysql/databases", :default => "alice in chains")
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :default => 1337)
+ metadata.attribute("db/mysql/databases", :default => 1337)
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :default => true)
+ metadata.attribute("db/mysql/databases", :default => true)
}.should_not raise_error
lambda {
- @meta.attribute("db/mysql/databases", :required => :not_gonna_do_it)
+ metadata.attribute("db/mysql/databases", :required => :not_gonna_do_it)
}.should raise_error(ArgumentError)
end
@@ -453,16 +490,16 @@ describe Chef::Cookbook::Metadata do
:default => "test1"
}
lambda {
- @meta.attribute("test_cookbook/test", options)
+ metadata.attribute("test_cookbook/test", options)
}.should_not raise_error
-
+
options = {
:type => "boolean",
:choice => [ true, false ],
:default => true
}
lambda {
- @meta.attribute("test_cookbook/test", options)
+ metadata.attribute("test_cookbook/test", options)
}.should_not raise_error
options = {
@@ -471,7 +508,7 @@ describe Chef::Cookbook::Metadata do
:default => 1337
}
lambda {
- @meta.attribute("test_cookbook/test", options)
+ metadata.attribute("test_cookbook/test", options)
}.should_not raise_error
options = {
@@ -480,7 +517,7 @@ describe Chef::Cookbook::Metadata do
:default => false
}
lambda {
- @meta.attribute("test_cookbook/test", options)
+ metadata.attribute("test_cookbook/test", options)
}.should raise_error
end
@@ -490,14 +527,14 @@ describe Chef::Cookbook::Metadata do
:calculated => true,
:default => [ "I thought you said calculated" ]
}
- @meta.attribute("db/mysql/databases", attrs)
+ metadata.attribute("db/mysql/databases", attrs)
}.should raise_error(ArgumentError)
lambda {
attrs = {
:calculated => true,
:default => "I thought you said calculated"
}
- @meta.attribute("db/mysql/databases", attrs)
+ metadata.attribute("db/mysql/databases", attrs)
}.should raise_error(ArgumentError)
end
@@ -507,14 +544,14 @@ describe Chef::Cookbook::Metadata do
:choice => [ "a", "b", "c"],
:default => "b"
}
- @meta.attribute("db/mysql/databases", attrs)
+ metadata.attribute("db/mysql/databases", attrs)
}.should_not raise_error
lambda {
attrs = {
:choice => [ "a", "b", "c", "d", "e"],
:default => ["b", "d"]
}
- @meta.attribute("db/mysql/databases", attrs)
+ metadata.attribute("db/mysql/databases", attrs)
}.should_not raise_error
end
@@ -524,71 +561,74 @@ describe Chef::Cookbook::Metadata do
:choice => [ "a", "b", "c"],
:default => "d"
}
- @meta.attribute("db/mysql/databases", attrs)
+ metadata.attribute("db/mysql/databases", attrs)
}.should raise_error(ArgumentError)
lambda {
attrs = {
:choice => [ "a", "b", "c", "d", "e"],
:default => ["b", "z"]
}
- @meta.attribute("db/mysql/databases", attrs)
+ metadata.attribute("db/mysql/databases", attrs)
}.should raise_error(ArgumentError)
end
end
describe "recipes" do
+ let(:cookbook) do
+ c = Chef::CookbookVersion.new('test_cookbook')
+ c.recipe_files = [ "default.rb", "enlighten.rb" ]
+ c
+ end
+
before(:each) do
- @cookbook.recipe_files = [ "default.rb", "enlighten.rb" ]
- @meta = Chef::Cookbook::Metadata.new(@cookbook)
+ metadata.name("test_cookbook")
+ metadata.recipes_from_cookbook_version(cookbook)
end
it "should have the names of the recipes" do
- @meta.recipes["test_cookbook"].should == ""
- @meta.recipes["test_cookbook::enlighten"].should == ""
+ metadata.recipes["test_cookbook"].should == ""
+ metadata.recipes["test_cookbook::enlighten"].should == ""
end
it "should let you set the description for a recipe" do
- @meta.recipe "test_cookbook", "It, um... tests stuff?"
- @meta.recipes["test_cookbook"].should == "It, um... tests stuff?"
+ metadata.recipe "test_cookbook", "It, um... tests stuff?"
+ metadata.recipes["test_cookbook"].should == "It, um... tests stuff?"
end
it "should automatically provide each recipe" do
- @meta.providing.has_key?("test_cookbook").should == true
- @meta.providing.has_key?("test_cookbook::enlighten").should == true
+ metadata.providing.has_key?("test_cookbook").should == true
+ metadata.providing.has_key?("test_cookbook::enlighten").should == true
end
end
describe "json" do
before(:each) do
- @cookbook.recipe_files = [ "default.rb", "enlighten.rb" ]
- @meta = Chef::Cookbook::Metadata.new(@cookbook)
- @meta.version "1.0"
- @meta.maintainer "Bobo T. Clown"
- @meta.maintainer_email "bobo@example.com"
- @meta.long_description "I have a long arm!"
- @meta.supports :ubuntu, "> 8.04"
- @meta.depends "bobo", "= 1.0"
- @meta.depends "bubu", "=1.0"
- @meta.depends "bobotclown", "= 1.1"
- @meta.recommends "snark", "< 3.0"
- @meta.suggests "kindness", "> 2.0"
- @meta.conflicts "hatred"
- @meta.provides "foo(:bar, :baz)"
- @meta.replaces "snarkitron"
- @meta.recipe "test_cookbook::enlighten", "is your buddy"
- @meta.attribute "bizspark/has_login",
+ metadata.version "1.0"
+ metadata.maintainer "Bobo T. Clown"
+ metadata.maintainer_email "bobo@example.com"
+ metadata.long_description "I have a long arm!"
+ metadata.supports :ubuntu, "> 8.04"
+ metadata.depends "bobo", "= 1.0"
+ metadata.depends "bubu", "=1.0"
+ metadata.depends "bobotclown", "= 1.1"
+ metadata.recommends "snark", "< 3.0"
+ metadata.suggests "kindness", "> 2.0"
+ metadata.conflicts "hatred"
+ metadata.provides "foo(:bar, :baz)"
+ metadata.replaces "snarkitron"
+ metadata.recipe "test_cookbook::enlighten", "is your buddy"
+ metadata.attribute "bizspark/has_login",
:display_name => "You have nothing"
- @meta.version "1.2.3"
+ metadata.version "1.2.3"
end
describe "serialize" do
- before(:each) do
- @serial = Chef::JSONCompat.from_json(@meta.to_json)
- end
+
+ let(:deserialized_metadata) { Chef::JSONCompat.from_json(metadata.to_json) }
it "should serialize to a json hash" do
- Chef::JSONCompat.from_json(@meta.to_json).should be_a_kind_of(Hash)
+ deserialized_metadata.should be_a_kind_of(Hash)
end
%w{
@@ -610,18 +650,18 @@ describe Chef::Cookbook::Metadata do
version
}.each do |t|
it "should include '#{t}'" do
- @serial[t].should == @meta.send(t.to_sym)
+ deserialized_metadata[t].should == metadata.send(t.to_sym)
end
end
end
describe "deserialize" do
- before(:each) do
- @deserial = Chef::Cookbook::Metadata.from_json(@meta.to_json)
- end
+
+ let(:deserialized_metadata) { Chef::Cookbook::Metadata.from_json(metadata.to_json) }
+
it "should deserialize to a Chef::Cookbook::Metadata object" do
- @deserial.should be_a_kind_of(Chef::Cookbook::Metadata)
+ deserialized_metadata.should be_a_kind_of(Chef::Cookbook::Metadata)
end
%w{
@@ -643,14 +683,14 @@ describe Chef::Cookbook::Metadata do
version
}.each do |t|
it "should match '#{t}'" do
- @deserial.send(t.to_sym).should == @meta.send(t.to_sym)
+ deserialized_metadata.send(t.to_sym).should == metadata.send(t.to_sym)
end
end
end
describe "from_hash" do
before(:each) do
- @hash = @meta.to_hash
+ @hash = metadata.to_hash
end
[:dependencies,
diff --git a/spec/unit/cookbook/syntax_check_spec.rb b/spec/unit/cookbook/syntax_check_spec.rb
index 7f7a914115..b83bffe1c9 100644
--- a/spec/unit/cookbook/syntax_check_spec.rb
+++ b/spec/unit/cookbook/syntax_check_spec.rb
@@ -147,7 +147,8 @@ describe Chef::Cookbook::SyntaxCheck do
it "does not remove the invalid file from the list of untested files" do
syntax_check.untested_ruby_files.should include(File.join(cookbook_path, 'recipes', 'default.rb'))
- lambda { syntax_check.validate_ruby_files }.should_not change(syntax_check, :untested_ruby_files)
+ syntax_check.validate_ruby_files
+ syntax_check.untested_ruby_files.should include(File.join(cookbook_path, 'recipes', 'default.rb'))
end
it "indicates that a template file has a syntax error" do
@@ -166,7 +167,7 @@ describe Chef::Cookbook::SyntaxCheck do
cookbook_path = File.join(CHEF_SPEC_DATA, 'cookbooks', 'ignorken')
Chef::Config[:cookbook_path] = File.dirname(cookbook_path)
syntax_check.cookbook_path.replace(cookbook_path)
- @ruby_files = [File.join(cookbook_path, 'recipes/default.rb')]
+ @ruby_files = [File.join(cookbook_path, 'metadata.rb'), File.join(cookbook_path, 'recipes/default.rb')]
end
it "shows that ignored ruby files do not require a syntax check" do
diff --git a/spec/unit/cookbook_loader_spec.rb b/spec/unit/cookbook_loader_spec.rb
index 4c21c124e0..d5d585b8e1 100644
--- a/spec/unit/cookbook_loader_spec.rb
+++ b/spec/unit/cookbook_loader_spec.rb
@@ -19,39 +19,61 @@
require 'spec_helper'
describe Chef::CookbookLoader do
- before(:each) do
- @repo_paths = [ File.expand_path(File.join(CHEF_SPEC_DATA, "kitchen")),
- File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks")) ]
- @cookbook_loader = Chef::CookbookLoader.new(@repo_paths)
+
+ let(:repo_paths) do
+ [
+ File.expand_path(File.join(CHEF_SPEC_DATA, "kitchen")),
+ File.expand_path(File.join(CHEF_SPEC_DATA, "cookbooks"))
+ ]
end
- describe "loading all cookbooks" do
+ let(:cookbook_loader) { Chef::CookbookLoader.new(repo_paths) }
+
+ it "checks each directory only once when loading (CHEF-3487)" do
+ cookbook_paths = []
+ repo_paths.each do |repo_path|
+ cookbook_paths |= Dir[File.join(repo_path, "*")]
+ end
+
+ cookbook_paths.delete_if { |path| File.basename(path) == "chefignore" }
+
+ cookbook_paths.each do |cookbook_path|
+ Chef::Cookbook::CookbookVersionLoader.should_receive(:new).
+ with(cookbook_path, anything).
+ once.
+ and_call_original
+ end
+ cookbook_loader.load_cookbooks
+ end
+
+
+ context "after loading all cookbooks" do
before(:each) do
- @cookbook_loader.load_cookbooks
+ cookbook_loader.load_cookbooks
end
describe "[]" do
it "should return cookbook objects with []" do
- @cookbook_loader[:openldap].should be_a_kind_of(Chef::CookbookVersion)
+ cookbook_loader[:openldap].should be_a_kind_of(Chef::CookbookVersion)
end
it "should raise an exception if it cannot find a cookbook with []" do
- lambda { @cookbook_loader[:monkeypoop] }.should raise_error(Chef::Exceptions::CookbookNotFoundInRepo)
+ lambda { cookbook_loader[:monkeypoop] }.should raise_error(Chef::Exceptions::CookbookNotFoundInRepo)
end
it "should allow you to look up available cookbooks with [] and a symbol" do
- @cookbook_loader[:openldap].name.should eql(:openldap)
+ cookbook_loader[:openldap].name.should eql(:openldap)
end
it "should allow you to look up available cookbooks with [] and a string" do
- @cookbook_loader["openldap"].name.should eql(:openldap)
+ cookbook_loader["openldap"].name.should eql(:openldap)
end
end
describe "each" do
it "should allow you to iterate over cookbooks with each" do
seen = Hash.new
- @cookbook_loader.each do |cookbook_name, cookbook|
+ cookbook_loader.each do |cookbook_name, cookbook|
seen[cookbook_name] = true
end
seen.should have_key("openldap")
@@ -60,7 +82,7 @@ describe Chef::CookbookLoader do
it "should iterate in alphabetical order" do
seen = Array.new
- @cookbook_loader.each do |cookbook_name, cookbook|
+ cookbook_loader.each do |cookbook_name, cookbook|
seen << cookbook_name
end
seen[0].should == "angrybash"
@@ -68,106 +90,111 @@ describe Chef::CookbookLoader do
seen[2].should == "borken"
seen[3].should == "ignorken"
seen[4].should == "java"
- seen[5].should == "openldap"
+ seen[5].should == "name-mismatch"
+ seen[6].should == "openldap"
end
end
- describe "load_cookbooks" do
+ describe "referencing cookbook files" do
it "should find all the cookbooks in the cookbook path" do
- Chef::Config.cookbook_path << File.expand_path(File.join(CHEF_SPEC_DATA, "hidden-cookbooks"))
- @cookbook_loader.load_cookbooks
- @cookbook_loader.should have_key(:openldap)
- @cookbook_loader.should have_key(:apache2)
+ cookbook_loader.load_cookbooks
+ cookbook_loader.should have_key(:openldap)
+ cookbook_loader.should have_key(:apache2)
end
it "should allow you to override an attribute file via cookbook_path" do
- @cookbook_loader[:openldap].attribute_filenames.detect { |f|
+ cookbook_loader[:openldap].attribute_filenames.detect { |f|
f =~ /cookbooks\/openldap\/attributes\/default.rb/
}.should_not eql(nil)
- @cookbook_loader[:openldap].attribute_filenames.detect { |f|
+ cookbook_loader[:openldap].attribute_filenames.detect { |f|
f =~ /kitchen\/openldap\/attributes\/default.rb/
}.should eql(nil)
end
it "should load different attribute files from deeper paths" do
- @cookbook_loader[:openldap].attribute_filenames.detect { |f|
+ cookbook_loader[:openldap].attribute_filenames.detect { |f|
f =~ /kitchen\/openldap\/attributes\/robinson.rb/
}.should_not eql(nil)
end
it "should allow you to override a definition file via cookbook_path" do
- @cookbook_loader[:openldap].definition_filenames.detect { |f|
+ cookbook_loader[:openldap].definition_filenames.detect { |f|
f =~ /cookbooks\/openldap\/definitions\/client.rb/
}.should_not eql(nil)
- @cookbook_loader[:openldap].definition_filenames.detect { |f|
+ cookbook_loader[:openldap].definition_filenames.detect { |f|
f =~ /kitchen\/openldap\/definitions\/client.rb/
}.should eql(nil)
end
it "should load definition files from deeper paths" do
- @cookbook_loader[:openldap].definition_filenames.detect { |f|
+ cookbook_loader[:openldap].definition_filenames.detect { |f|
f =~ /kitchen\/openldap\/definitions\/drewbarrymore.rb/
}.should_not eql(nil)
end
it "should allow you to override a recipe file via cookbook_path" do
- @cookbook_loader[:openldap].recipe_filenames.detect { |f|
+ cookbook_loader[:openldap].recipe_filenames.detect { |f|
f =~ /cookbooks\/openldap\/recipes\/gigantor.rb/
}.should_not eql(nil)
- @cookbook_loader[:openldap].recipe_filenames.detect { |f|
+ cookbook_loader[:openldap].recipe_filenames.detect { |f|
f =~ /kitchen\/openldap\/recipes\/gigantor.rb/
}.should eql(nil)
end
it "should load recipe files from deeper paths" do
- @cookbook_loader[:openldap].recipe_filenames.detect { |f|
+ cookbook_loader[:openldap].recipe_filenames.detect { |f|
f =~ /kitchen\/openldap\/recipes\/woot.rb/
}.should_not eql(nil)
end
it "should allow you to have an 'ignore' file, which skips loading files in later cookbooks" do
- @cookbook_loader[:openldap].recipe_filenames.detect { |f|
+ cookbook_loader[:openldap].recipe_filenames.detect { |f|
f =~ /kitchen\/openldap\/recipes\/ignoreme.rb/
}.should eql(nil)
end
it "should find files that start with a ." do
- @cookbook_loader[:openldap].file_filenames.detect { |f|
+ cookbook_loader[:openldap].file_filenames.detect { |f|
f =~ /\.dotfile$/
}.should =~ /\.dotfile$/
- @cookbook_loader[:openldap].file_filenames.detect { |f|
+ cookbook_loader[:openldap].file_filenames.detect { |f|
f =~ /\.ssh\/id_rsa$/
}.should =~ /\.ssh\/id_rsa$/
end
it "should load the metadata for the cookbook" do
- @cookbook_loader.metadata[:openldap].name.to_s.should == "openldap"
- @cookbook_loader.metadata[:openldap].should be_a_kind_of(Chef::Cookbook::Metadata)
+ cookbook_loader.metadata[:openldap].name.to_s.should == "openldap"
+ cookbook_loader.metadata[:openldap].should be_a_kind_of(Chef::Cookbook::Metadata)
end
- it "should check each cookbook directory only once (CHEF-3487)" do
- cookbooks = []
- @repo_paths.each do |repo_path|
- cookbooks |= Dir[File.join(repo_path, "*")]
- end
- cookbooks.each do |cookbook|
- File.should_receive(:directory?).with(cookbook).once;
- end
- @cookbook_loader.load_cookbooks
- end
- end # load_cookbooks
+ end # referencing cookbook files
end # loading all cookbooks
+ context "loading all cookbooks when one has invalid metadata" do
+
+ let(:repo_paths) do
+ [
+ File.join(CHEF_SPEC_DATA, "kitchen"),
+ File.join(CHEF_SPEC_DATA, "cookbooks"),
+ File.join(CHEF_SPEC_DATA, "invalid-metadata-chef-repo")
+ ]
+ end
+
+ it "does not squelch the exception" do
+ expect { cookbook_loader.load_cookbooks }.to raise_error("THIS METADATA HAS A BUG")
+ end
+
+ end
+
describe "loading only one cookbook" do
before(:each) do
- @cookbook_loader = Chef::CookbookLoader.new(@repo_paths)
- @cookbook_loader.load_cookbook("openldap")
+ cookbook_loader.load_cookbook("openldap")
end
it "should have loaded the correct cookbook" do
seen = Hash.new
- @cookbook_loader.each do |cookbook_name, cookbook|
+ cookbook_loader.each do |cookbook_name, cookbook|
seen[cookbook_name] = true
end
seen.should have_key("openldap")
@@ -176,7 +203,7 @@ describe Chef::CookbookLoader do
it "should not duplicate keys when serialized to JSON" do
# Chef JSON serialization will generate duplicate keys if given
# a Hash containing matching string and symbol keys. See CHEF-4571.
- aa = @cookbook_loader["openldap"]
+ aa = cookbook_loader["openldap"]
aa.to_hash["metadata"].recipes.keys.should_not include(:openldap)
aa.to_hash["metadata"].recipes.keys.should include("openldap")
expected_desc = "Main Open LDAP configuration"
@@ -190,30 +217,50 @@ describe Chef::CookbookLoader do
end
it "should not load the cookbook again when accessed" do
- @cookbook_loader.should_not_receive('load_cookbook')
- @cookbook_loader["openldap"]
+ cookbook_loader.should_not_receive('load_cookbook')
+ cookbook_loader["openldap"]
end
it "should not load the other cookbooks" do
seen = Hash.new
- @cookbook_loader.each do |cookbook_name, cookbook|
+ cookbook_loader.each do |cookbook_name, cookbook|
seen[cookbook_name] = true
end
seen.should_not have_key("apache2")
end
it "should load another cookbook lazily with []" do
- @cookbook_loader["apache2"].should be_a_kind_of(Chef::CookbookVersion)
+ cookbook_loader["apache2"].should be_a_kind_of(Chef::CookbookVersion)
+ end
+
+ context "when an unrelated cookbook has invalid metadata" do
+
+ let(:repo_paths) do
+ [
+ File.join(CHEF_SPEC_DATA, "kitchen"),
+ File.join(CHEF_SPEC_DATA, "cookbooks"),
+ File.join(CHEF_SPEC_DATA, "invalid-metadata-chef-repo")
+ ]
+ end
+
+ it "ignores the invalid cookbook" do
+ expect { cookbook_loader["openldap"] }.to_not raise_error
+ end
+
+ it "surfaces the exception if the cookbook is loaded later" do
+ expect { cookbook_loader["invalid-metadata"] }.to raise_error("THIS METADATA HAS A BUG")
+ end
+
end
describe "loading all cookbooks after loading only one cookbook" do
before(:each) do
- @cookbook_loader.load_cookbooks
+ cookbook_loader.load_cookbooks
end
it "should load all cookbooks" do
seen = Hash.new
- @cookbook_loader.each do |cookbook_name, cookbook|
+ cookbook_loader.each do |cookbook_name, cookbook|
seen[cookbook_name] = true
end
seen.should have_key("openldap")
@@ -221,4 +268,18 @@ describe Chef::CookbookLoader do
end
end
end # loading only one cookbook
+
+ describe "loading a single cookbook with a different name than basename" do
+
+ before(:each) do
+ cookbook_loader.load_cookbook("name-mismatch")
+ end
+
+ it "loads the correct cookbook" do
+ cookbook_version = cookbook_loader["name-mismatch"]
+ cookbook_version.should be_a_kind_of(Chef::CookbookVersion)
+ cookbook_version.name.should == :"name-mismatch"
+ end
+
+ end
end