summaryrefslogtreecommitdiff
path: root/lib/chef/cookbook/metadata.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/chef/cookbook/metadata.rb')
-rw-r--r--lib/chef/cookbook/metadata.rb629
1 files changed, 629 insertions, 0 deletions
diff --git a/lib/chef/cookbook/metadata.rb b/lib/chef/cookbook/metadata.rb
new file mode 100644
index 0000000000..8398de442c
--- /dev/null
+++ b/lib/chef/cookbook/metadata.rb
@@ -0,0 +1,629 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: AJ Christensen (<aj@opscode.com>)
+# Author:: Seth Falcon (<seth@opscode.com>)
+# Copyright:: Copyright 2008-2010 Opscode, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require 'chef/mash'
+require 'chef/mixin/from_file'
+require 'chef/mixin/params_validate'
+require 'chef/mixin/check_helper'
+require 'chef/log'
+require 'chef/version_class'
+require 'chef/version_constraint'
+
+class Chef
+ class Cookbook
+
+ # == Chef::Cookbook::Metadata
+ # Chef::Cookbook::Metadata provides a convenient DSL for declaring metadata
+ # about Chef Cookbooks.
+ class Metadata
+
+ NAME = 'name'.freeze
+ DESCRIPTION = 'description'.freeze
+ LONG_DESCRIPTION = 'long_description'.freeze
+ MAINTAINER = 'maintainer'.freeze
+ MAINTAINER_EMAIL = 'maintainer_email'.freeze
+ LICENSE = 'license'.freeze
+ PLATFORMS = 'platforms'.freeze
+ DEPENDENCIES = 'dependencies'.freeze
+ RECOMMENDATIONS = 'recommendations'.freeze
+ SUGGESTIONS = 'suggestions'.freeze
+ CONFLICTING = 'conflicting'.freeze
+ PROVIDING = 'providing'.freeze
+ REPLACING = 'replacing'.freeze
+ ATTRIBUTES = 'attributes'.freeze
+ GROUPINGS = 'groupings'.freeze
+ RECIPES = 'recipes'.freeze
+ VERSION = 'version'.freeze
+
+ COMPARISON_FIELDS = [ :name, :description, :long_description, :maintainer,
+ :maintainer_email, :license, :platforms, :dependencies,
+ :recommendations, :suggestions, :conflicting, :providing,
+ :replacing, :attributes, :groupings, :recipes, :version]
+
+ VERSION_CONSTRAINTS = {:depends => DEPENDENCIES,
+ :recommends => RECOMMENDATIONS,
+ :suggests => SUGGESTIONS,
+ :conflicts => CONFLICTING,
+ :provides => PROVIDING,
+ :replaces => REPLACING }
+
+ include Chef::Mixin::CheckHelper
+ include Chef::Mixin::ParamsValidate
+ include Chef::Mixin::FromFile
+
+ attr_reader :cookbook,
+ :platforms,
+ :dependencies,
+ :recommendations,
+ :suggestions,
+ :conflicting,
+ :providing,
+ :replacing,
+ :attributes,
+ :groupings,
+ :recipes,
+ :version
+
+ # Builds a new Chef::Cookbook::Metadata object.
+ #
+ # === Parameters
+ # cookbook<String>:: An optional cookbook object
+ # maintainer<String>:: An optional maintainer
+ # maintainer_email<String>:: An optional maintainer email
+ # license<String>::An optional license. Default is Apache v2.0
+ #
+ # === 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')
+ @platforms = Mash.new
+ @dependencies = Mash.new
+ @recommendations = Mash.new
+ @suggestions = Mash.new
+ @conflicting = Mash.new
+ @providing = Mash.new
+ @replacing = Mash.new
+ @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 if e =~ /::default$/
+ r[e] = ""
+ self.provides e
+ r
+ end
+ end
+ end
+
+ def ==(other)
+ COMPARISON_FIELDS.inject(true) do |equal_so_far, field|
+ equal_so_far && other.respond_to?(field) && (other.send(field) == send(field))
+ end
+ end
+
+ # Sets the cookbooks maintainer, or returns it.
+ #
+ # === Parameters
+ # maintainer<String>:: The maintainers name
+ #
+ # === Returns
+ # maintainer<String>:: Returns the current maintainer.
+ def maintainer(arg=nil)
+ set_or_return(
+ :maintainer,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the maintainers email address, or returns it.
+ #
+ # === Parameters
+ # maintainer_email<String>:: The maintainers email address
+ #
+ # === Returns
+ # maintainer_email<String>:: Returns the current maintainer email.
+ def maintainer_email(arg=nil)
+ set_or_return(
+ :maintainer_email,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the current license, or returns it.
+ #
+ # === Parameters
+ # license<String>:: The current license.
+ #
+ # === Returns
+ # license<String>:: Returns the current license
+ def license(arg=nil)
+ set_or_return(
+ :license,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the current description, or returns it. Should be short - one line only!
+ #
+ # === Parameters
+ # description<String>:: The new description
+ #
+ # === Returns
+ # description<String>:: Returns the description
+ def description(arg=nil)
+ set_or_return(
+ :description,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the current long description, or returns it. Might come from a README, say.
+ #
+ # === Parameters
+ # long_description<String>:: The new long description
+ #
+ # === Returns
+ # long_description<String>:: Returns the long description
+ def long_description(arg=nil)
+ set_or_return(
+ :long_description,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Sets the current cookbook version, or returns it. Can be two or three digits, seperated
+ # by dots. ie: '2.1', '1.5.4' or '0.9'.
+ #
+ # === Parameters
+ # version<String>:: The curent version, as a string
+ #
+ # === Returns
+ # version<String>:: Returns the current version
+ def version(arg=nil)
+ if arg
+ @version = Chef::Version.new(arg)
+ end
+
+ @version.to_s
+ end
+
+ # Sets the name of the cookbook, or returns it.
+ #
+ # === Parameters
+ # name<String>:: The curent cookbook name.
+ #
+ # === Returns
+ # name<String>:: Returns the current cookbook name.
+ def name(arg=nil)
+ set_or_return(
+ :name,
+ arg,
+ :kind_of => [ String ]
+ )
+ end
+
+ # Adds a supported platform, with version checking strings.
+ #
+ # === Parameters
+ # platform<String>,<Symbol>:: The platform (like :ubuntu or :mac_os_x)
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def supports(platform, *version_args)
+ version = new_args_format(:supports, platform, version_args)
+ validate_version_constraint(:supports, platform, version)
+ @platforms[platform] = version
+ @platforms[platform]
+ end
+
+ # Adds a dependency on another cookbook, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def depends(cookbook, *version_args)
+ version = new_args_format(:depends, cookbook, version_args)
+ validate_version_constraint(:depends, cookbook, version)
+ @dependencies[cookbook] = version
+ @dependencies[cookbook]
+ end
+
+ # Adds a recommendation for another cookbook, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def recommends(cookbook, *version_args)
+ version = new_args_format(:recommends, cookbook, version_args)
+ validate_version_constraint(:recommends, cookbook, version)
+ @recommendations[cookbook] = version
+ @recommendations[cookbook]
+ end
+
+ # Adds a suggestion for another cookbook, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has the
+ # formx.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def suggests(cookbook, *version_args)
+ version = new_args_format(:suggests, cookbook, version_args)
+ validate_version_constraint(:suggests, cookbook, version)
+ @suggestions[cookbook] = version
+ @suggestions[cookbook]
+ end
+
+ # Adds a conflict for another cookbook, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def conflicts(cookbook, *version_args)
+ version = new_args_format(:conflicts, cookbook, version_args)
+ validate_version_constraint(:conflicts, cookbook, version)
+ @conflicting[cookbook] = version
+ @conflicting[cookbook]
+ end
+
+ # Adds a recipe, definition, or resource provided by this cookbook.
+ #
+ # Recipes are specified as normal
+ # Definitions are followed by (), and can include :params for prototyping
+ # Resources are the stringified version (service[apache2])
+ #
+ # === Parameters
+ # recipe, definition, resource<String>:: The thing we provide
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has
+ # the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def provides(cookbook, *version_args)
+ version = new_args_format(:provides, cookbook, version_args)
+ validate_version_constraint(:provides, cookbook, version)
+ @providing[cookbook] = version
+ @providing[cookbook]
+ end
+
+ # Adds a cookbook that is replaced by this one, with version checking strings.
+ #
+ # === Parameters
+ # cookbook<String>:: The cookbook we replace
+ # version<String>:: A version constraint of the form "OP VERSION",
+ # where OP is one of < <= = > >= ~> and VERSION has the form x.y.z or x.y.
+ #
+ # === Returns
+ # versions<Array>:: Returns the list of versions for the platform
+ def replaces(cookbook, *version_args)
+ version = new_args_format(:replaces, cookbook, version_args)
+ validate_version_constraint(:replaces, cookbook, version)
+ @replacing[cookbook] = version
+ @replacing[cookbook]
+ end
+
+ # Adds a description for a recipe.
+ #
+ # === Parameters
+ # recipe<String>:: The recipe
+ # description<String>:: The description of the recipe
+ #
+ # === Returns
+ # description<String>:: Returns the current description
+ def recipe(name, description)
+ @recipes[name] = description
+ 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
+ #
+ # display_name<String>:: What a UI should show for this attribute
+ # description<String>:: A hint as to what this attr is for
+ # choice<Array>:: An array of choices to present to the user.
+ # calculated<Boolean>:: If true, the default value is calculated by the recipe and cannot be displayed.
+ # type<String>:: "string" or "array" - default is "string" ("hash" is supported for backwards compatibility)
+ # required<String>:: Whether this attr is 'required', 'recommended' or 'optional' - default 'optional' (true/false values also supported for backwards compatibility)
+ # recipes<Array>:: An array of recipes which need this attr set.
+ # default<String>,<Array>,<Hash>:: The default value
+ #
+ # === Parameters
+ # name<String>:: The name of the attribute ('foo', or 'apache2/log_dir')
+ # options<Hash>:: The description of the options
+ #
+ # === Returns
+ # options<Hash>:: Returns the current options hash
+ def attribute(name, options)
+ validate(
+ options,
+ {
+ :display_name => { :kind_of => String },
+ :description => { :kind_of => String },
+ :choice => { :kind_of => [ Array ], :default => [] },
+ :calculated => { :equal_to => [ true, false ], :default => false },
+ :type => { :equal_to => [ "string", "array", "hash", "symbol" ], :default => "string" },
+ :required => { :equal_to => [ "required", "recommended", "optional", true, false ], :default => "optional" },
+ :recipes => { :kind_of => [ Array ], :default => [] },
+ :default => { :kind_of => [ String, Array, Hash ] }
+ }
+ )
+ options[:required] = remap_required_attribute(options[:required]) unless options[:required].nil?
+ validate_string_array(options[:choice])
+ validate_calculated_default_rule(options)
+ validate_choice_default_rule(options)
+
+ @attributes[name] = options
+ @attributes[name]
+ end
+
+ def grouping(name, options)
+ validate(
+ options,
+ {
+ :title => { :kind_of => String },
+ :description => { :kind_of => String }
+ }
+ )
+ @groupings[name] = options
+ @groupings[name]
+ end
+
+ def to_hash
+ {
+ NAME => self.name,
+ DESCRIPTION => self.description,
+ LONG_DESCRIPTION => self.long_description,
+ MAINTAINER => self.maintainer,
+ MAINTAINER_EMAIL => self.maintainer_email,
+ LICENSE => self.license,
+ PLATFORMS => self.platforms,
+ DEPENDENCIES => self.dependencies,
+ RECOMMENDATIONS => self.recommendations,
+ SUGGESTIONS => self.suggestions,
+ CONFLICTING => self.conflicting,
+ PROVIDING => self.providing,
+ REPLACING => self.replacing,
+ ATTRIBUTES => self.attributes,
+ GROUPINGS => self.groupings,
+ RECIPES => self.recipes,
+ VERSION => self.version
+ }
+ end
+
+ def to_json(*a)
+ self.to_hash.to_json(*a)
+ end
+
+ def self.from_hash(o)
+ cm = self.new()
+ cm.from_hash(o)
+ cm
+ end
+
+ def from_hash(o)
+ @name = o[NAME] if o.has_key?(NAME)
+ @description = o[DESCRIPTION] if o.has_key?(DESCRIPTION)
+ @long_description = o[LONG_DESCRIPTION] if o.has_key?(LONG_DESCRIPTION)
+ @maintainer = o[MAINTAINER] if o.has_key?(MAINTAINER)
+ @maintainer_email = o[MAINTAINER_EMAIL] if o.has_key?(MAINTAINER_EMAIL)
+ @license = o[LICENSE] if o.has_key?(LICENSE)
+ @platforms = o[PLATFORMS] if o.has_key?(PLATFORMS)
+ @dependencies = handle_deprecated_constraints(o[DEPENDENCIES]) if o.has_key?(DEPENDENCIES)
+ @recommendations = handle_deprecated_constraints(o[RECOMMENDATIONS]) if o.has_key?(RECOMMENDATIONS)
+ @suggestions = handle_deprecated_constraints(o[SUGGESTIONS]) if o.has_key?(SUGGESTIONS)
+ @conflicting = handle_deprecated_constraints(o[CONFLICTING]) if o.has_key?(CONFLICTING)
+ @providing = o[PROVIDING] if o.has_key?(PROVIDING)
+ @replacing = handle_deprecated_constraints(o[REPLACING]) if o.has_key?(REPLACING)
+ @attributes = o[ATTRIBUTES] if o.has_key?(ATTRIBUTES)
+ @groupings = o[GROUPINGS] if o.has_key?(GROUPINGS)
+ @recipes = o[RECIPES] if o.has_key?(RECIPES)
+ @version = o[VERSION] if o.has_key?(VERSION)
+ self
+ end
+
+ def self.from_json(string)
+ o = Chef::JSONCompat.from_json(string)
+ self.from_hash(o)
+ end
+
+ 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|
+ metadata.send(method_name, cb_name, *Array(constraints))
+ end
+ end
+ end
+ true
+ end
+
+ def from_json(string)
+ o = Chef::JSONCompat.from_json(string)
+ from_hash(o)
+ end
+
+ private
+
+ def new_args_format(caller_name, dep_name, version_constraints)
+ if version_constraints.empty?
+ ">= 0.0.0"
+ elsif version_constraints.size == 1
+ version_constraints.first
+ else
+ msg=<<-OBSOLETED
+The dependency specification syntax you are using is no longer valid. You may not
+specify more than one version constraint for a particular cookbook.
+Consult http://wiki.opscode.com/display/chef/Metadata for the updated syntax.
+
+Called by: #{caller_name} '#{dep_name}', #{version_constraints.map {|vc| vc.inspect}.join(", ")}
+Called from:
+#{caller[0...5].map {|line| " " + line}.join("\n")}
+OBSOLETED
+ raise Exceptions::ObsoleteDependencySyntax, msg
+ end
+ end
+
+ def validate_version_constraint(caller_name, dep_name, constraint_str)
+ Chef::VersionConstraint.new(constraint_str)
+ rescue Chef::Exceptions::InvalidVersionConstraint => e
+ Log.debug(e)
+
+ msg=<<-INVALID
+The version constraint syntax you are using is not valid. If you recently
+upgraded to Chef 0.10.0, be aware that you no may longer use "<<" and ">>" for
+'less than' and 'greater than'; use '<' and '>' instead.
+Consult http://wiki.opscode.com/display/chef/Metadata for more information.
+
+Called by: #{caller_name} '#{dep_name}', '#{constraint_str}'
+Called from:
+#{caller[0...5].map {|line| " " + line}.join("\n")}
+INVALID
+ raise Exceptions::InvalidVersionConstraint, msg
+ end
+ # Verify that the given array is an array of strings
+ #
+ # Raise an exception if the members of the array are not Strings
+ #
+ # === Parameters
+ # arry<Array>:: An array to be validated
+ def validate_string_array(arry)
+ if arry.kind_of?(Array)
+ arry.each do |choice|
+ validate( {:choice => choice}, {:choice => {:kind_of => String}} )
+ end
+ end
+ end
+
+ # For backwards compatibility, remap Boolean values to String
+ # true is mapped to "required"
+ # false is mapped to "optional"
+ #
+ # === Parameters
+ # required_attr<String><Boolean>:: The value of options[:required]
+ #
+ # === Returns
+ # required_attr<String>:: "required", "recommended", or "optional"
+ def remap_required_attribute(value)
+ case value
+ when true
+ value = "required"
+ when false
+ value = "optional"
+ end
+ value
+ end
+
+ def validate_calculated_default_rule(options)
+ calculated_conflict = ((options[:default].is_a?(Array) && !options[:default].empty?) ||
+ (options[:default].is_a?(String) && !options[:default] != "")) &&
+ options[:calculated] == true
+ raise ArgumentError, "Default cannot be specified if calculated is true!" if calculated_conflict
+ end
+
+ def validate_choice_default_rule(options)
+ return if !options[:choice].is_a?(Array) || options[:choice].empty?
+
+ if options[:default].is_a?(String) && options[:default] != ""
+ raise ArgumentError, "Default must be one of your choice values!" if options[:choice].index(options[:default]) == nil
+ end
+
+ if options[:default].is_a?(Array) && !options[:default].empty?
+ options[:default].each do |val|
+ raise ArgumentError, "Default values must be a subset of your choice values!" if options[:choice].index(val) == nil
+ end
+ end
+ end
+
+ # This method translates version constraint strings from
+ # cookbooks with the old format.
+ #
+ # Before we began respecting version constraints, we allowed
+ # multiple constraints to be placed on cookbooks, as well as the
+ # << and >> operators, which are now just < and >. For
+ # specifications with more than one constraint, we return an
+ # empty array (otherwise, we're silently abiding only part of
+ # the contract they have specified to us). If there is only one
+ # constraint, we are replacing the old << and >> with the new <
+ # and >.
+ def handle_deprecated_constraints(specification)
+ specification.inject(Mash.new) do |acc, (cb, constraints)|
+ constraints = Array(constraints)
+ acc[cb] = (constraints.empty? || constraints.size > 1) ? [] : constraints.first.gsub(/>>/, '>').gsub(/<</, '<')
+ acc
+ end
+ end
+
+ end
+
+ #== Chef::Cookbook::MinimalMetadata
+ # MinimalMetadata is a duck type of Cookbook::Metadata, used
+ # internally by Chef Server when determining the optimal set of
+ # cookbooks for a node.
+ #
+ # MinimalMetadata objects typically contain only enough information
+ # to solve the cookbook collection for a run list, but not enough to
+ # generate the proper response
+ class MinimalMetadata < Metadata
+ def initialize(name, params)
+ @name = name
+ from_hash(params)
+ end
+ end
+
+
+ end
+end