diff options
author | Ranjib Dey <ranjib@pagerduty.com> | 2015-06-23 23:13:12 -0700 |
---|---|---|
committer | Ranjib Dey <ranjib@pagerduty.com> | 2015-06-24 10:12:45 -0700 |
commit | 720f3331f794a2ad31bee2b1113ac99fada85389 (patch) | |
tree | 4f03dfd86d9fb543cb2eaf1b6a4c3848d356b11f /lib | |
parent | 9f0ea8aa0ec05819e242dedaa85fe731dca3146c (diff) | |
parent | ab34e3cd83d545b5da19113d723eeebcab1e77e2 (diff) | |
download | chef-720f3331f794a2ad31bee2b1113ac99fada85389.tar.gz |
Merge remote-tracking branch 'origin/master' into chef_handler
Diffstat (limited to 'lib')
28 files changed, 1351 insertions, 486 deletions
diff --git a/lib/chef/chef_class.rb b/lib/chef/chef_class.rb index d9c141b85d..c017fb157c 100644 --- a/lib/chef/chef_class.rb +++ b/lib/chef/chef_class.rb @@ -65,7 +65,7 @@ class Chef # @return [Array<Class>] Priority Array of Provider Classes to use for the resource_name on the node # def get_provider_priority_array(resource_name) - result = provider_priority_map.get_priority_array(node, resource_name) + result = provider_priority_map.get_priority_array(node, resource_name.to_sym) result = result.dup if result result end @@ -78,7 +78,7 @@ class Chef # @return [Array<Class>] Priority Array of Resource Classes to use for the resource_name on the node # def get_resource_priority_array(resource_name) - result = resource_priority_map.get_priority_array(node, resource_name) + result = resource_priority_map.get_priority_array(node, resource_name.to_sym) result = result.dup if result result end @@ -93,7 +93,7 @@ class Chef # @return [Array<Class>] Modified Priority Array of Provider Classes to use for the resource_name on the node # def set_provider_priority_array(resource_name, priority_array, *filter, &block) - result = provider_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = provider_priority_map.set_priority_array(resource_name.to_sym, priority_array, *filter, &block) result = result.dup if result result end @@ -108,7 +108,7 @@ class Chef # @return [Array<Class>] Modified Priority Array of Resource Classes to use for the resource_name on the node # def set_resource_priority_array(resource_name, priority_array, *filter, &block) - result = resource_priority_map.set_priority_array(resource_name, priority_array, *filter, &block) + result = resource_priority_map.set_priority_array(resource_name.to_sym, priority_array, *filter, &block) result = result.dup if result result end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index 86e92585e3..3c86f52b4a 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -309,12 +309,18 @@ class Chef # with the proper exit status code and everything gets raised # as a RunFailedWrappingError if run_error || converge_error || audit_error - error = if run_error == converge_error - Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) - else - Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) - end - error.fill_backtrace + error = if Chef::Config[:audit_mode] == :disabled + run_error || converge_error + else + e = if run_error == converge_error + Chef::Exceptions::RunFailedWrappingError.new(converge_error, audit_error) + else + Chef::Exceptions::RunFailedWrappingError.new(run_error, converge_error, audit_error) + end + e.fill_backtrace + e + end + Chef::Application.debug_stacktrace(error) raise error end diff --git a/lib/chef/dsl/recipe.rb b/lib/chef/dsl/recipe.rb index d69f0a8f11..d00d0df247 100644 --- a/lib/chef/dsl/recipe.rb +++ b/lib/chef/dsl/recipe.rb @@ -122,9 +122,9 @@ class Chef def describe_self_for_error if respond_to?(:name) - %Q[`#{self.class.name} "#{name}"'] + %Q[`#{self.class} "#{name}"'] elsif respond_to?(:recipe_name) - %Q[`#{self.class.name} "#{recipe_name}"'] + %Q[`#{self.class} "#{recipe_name}"'] else to_s end @@ -176,6 +176,24 @@ class Chef raise NameError, "No resource, method, or local variable named `#{method_symbol}' for #{describe_self_for_error}" end end + + module FullDSL + require 'chef/dsl/data_query' + require 'chef/dsl/platform_introspection' + require 'chef/dsl/include_recipe' + require 'chef/dsl/registry_helper' + require 'chef/dsl/reboot_pending' + require 'chef/dsl/audit' + require 'chef/dsl/powershell' + include Chef::DSL::DataQuery + include Chef::DSL::PlatformIntrospection + include Chef::DSL::IncludeRecipe + include Chef::DSL::Recipe + include Chef::DSL::RegistryHelper + include Chef::DSL::RebootPending + include Chef::DSL::Audit + include Chef::DSL::Powershell + end end end end diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb index 73fe25ec13..50aee63450 100644 --- a/lib/chef/event_dispatch/base.rb +++ b/lib/chef/event_dispatch/base.rb @@ -269,26 +269,37 @@ class Chef # def notifications_resolved # end + # + # Resource events and ordering: + # + # 1. Start the action + # - resource_action_start + # 2. Check the guard + # - resource_skipped: (goto 7) if only_if/not_if say to skip + # 3. Load the current resource + # - resource_current_state_loaded + # - resource_current_state_load_bypassed (if not why-run safe) + # 4. Check if why-run safe + # - resource_bypassed: (goto 7) if not why-run safe + # 5. During processing: + # - resource_update_applied: For each actual change (many per action) + # 6. Processing complete status: + # - resource_failed if the resource threw an exception while running + # - resource_failed_retriable: (goto 3) if resource failed and will be retried + # - resource_updated if the resource was updated (resource_update_applied will have been called) + # - resource_up_to_date if the resource was up to date (no resource_update_applied) + # 7. Processing complete: + # - resource_completed + # + # Called before action is executed on a resource. def resource_action_start(resource, action, notification_type=nil, notifier=nil) end - # Called when a resource fails, but will retry. - def resource_failed_retriable(resource, action, retry_count, exception) - end - - # Called when a resource fails and will not be retried. - def resource_failed(resource, action, exception) - end - # Called when a resource action has been skipped b/c of a conditional def resource_skipped(resource, action, conditional) end - # Called when a resource action has been completed - def resource_completed(resource) - end - # Called after #load_current_resource has run. def resource_current_state_loaded(resource, action, current_resource) end @@ -302,21 +313,34 @@ class Chef def resource_bypassed(resource, action, current_resource) end - # Called when a resource has no converge actions, e.g., it was already correct. - def resource_up_to_date(resource, action) - end - # Called when a change has been made to a resource. May be called multiple # times per resource, e.g., a file may have its content updated, and then # its permissions updated. def resource_update_applied(resource, action, update) end + # Called when a resource fails, but will retry. + def resource_failed_retriable(resource, action, retry_count, exception) + end + + # Called when a resource fails and will not be retried. + def resource_failed(resource, action, exception) + end + # Called after a resource has been completely converged, but only if # modifications were made. def resource_updated(resource, action) end + # Called when a resource has no converge actions, e.g., it was already correct. + def resource_up_to_date(resource, action) + end + + # Called when a resource action has been completed + def resource_completed(resource) + end + + # A stream has opened. def stream_opened(stream, options = {}) end diff --git a/lib/chef/event_dispatch/dsl.rb b/lib/chef/event_dispatch/dsl.rb index 98854a4716..c6f21c9b45 100644 --- a/lib/chef/event_dispatch/dsl.rb +++ b/lib/chef/event_dispatch/dsl.rb @@ -26,15 +26,14 @@ class Chef def initialize(name) klass = Class.new(Chef::EventDispatch::Base) do - def self.name - @@name - end + attr_reader :name end - klass.class_variable_set(:@@name, name) @handler = klass.new - # Use current event.register API to add anonymous handler if - # run_context and associated event dispatcher is set, else fallback to - # Chef::Config[:hanlder]. + @handler.instance_variable_set(:@name, name) + + # Use event.register API to add anonymous handler if Chef.run_context + # and associated event dispatcher is set, else fallback to + # Chef::Config[:hanlder] if Chef.run_context && Chef.run_context.events Chef::Log.debug("Registering handler '#{name}' using events api") Chef.run_context.events.register(handler) diff --git a/lib/chef/mixin/params_validate.rb b/lib/chef/mixin/params_validate.rb index 78d72dc801..f7d52a19cf 100644 --- a/lib/chef/mixin/params_validate.rb +++ b/lib/chef/mixin/params_validate.rb @@ -16,6 +16,8 @@ # limitations under the License. class Chef + NOT_PASSED = Object.new + class DelayedEvaluator < Proc end module Mixin @@ -65,7 +67,7 @@ class Chef true when Hash validation.each do |check, carg| - check_method = "_pv_#{check.to_s}" + check_method = "_pv_#{check}" if self.respond_to?(check_method, true) self.send(check_method, opts, key, carg) else @@ -81,162 +83,407 @@ class Chef DelayedEvaluator.new(&block) end - def set_or_return(symbol, arg, validation) - iv_symbol = "@#{symbol.to_s}".to_sym - if arg == nil && self.instance_variable_defined?(iv_symbol) == true - ivar = self.instance_variable_get(iv_symbol) - if(ivar.is_a?(DelayedEvaluator)) - validate({ symbol => ivar.call }, { symbol => validation })[symbol] - else - ivar - end + def set_or_return(symbol, value, validation) + symbol = symbol.to_sym + iv_symbol = :"@#{symbol}" + + # Steal default, coerce, name_property and required from validation + # so that we can handle the order in which they are applied + validation = validation.dup + if validation.has_key?(:default) + default = validation.delete(:default) + elsif validation.has_key?('default') + default = validation.delete('default') else - if(arg.is_a?(DelayedEvaluator)) - val = arg + default = NOT_PASSED + end + coerce = validation.delete(:coerce) + coerce ||= validation.delete('coerce') + name_property = validation.delete(:name_property) + name_property ||= validation.delete('name_property') + name_property ||= validation.delete(:name_attribute) + name_property ||= validation.delete('name_attribute') + required = validation.delete(:required) + required ||= validation.delete('required') + + opts = {} + # If the user passed NOT_PASSED, or passed nil, then this is a get. + if value == NOT_PASSED || (value.nil? && !explicitly_allows_nil?(symbol, validation)) + + # Get the value if there is one + if self.instance_variable_defined?(iv_symbol) + opts[symbol] = self.instance_variable_get(iv_symbol) + + # Handle lazy values + if opts[symbol].is_a?(DelayedEvaluator) + if opts[symbol].arity >= 1 + opts[symbol] = opts[symbol].call(self) + else + opts[symbol] = opts[symbol].call + end + + # Coerce and validate the default value + _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required + _pv_coerce(opts, symbol, coerce) if coerce + validate(opts, { symbol => validation }) + end + + # Get the default value else - val = validate({ symbol => arg }, { symbol => validation })[symbol] + _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required + _pv_default(opts, symbol, default) unless default == NOT_PASSED + _pv_name_property(opts, symbol, name_property) + + if opts.has_key?(symbol) + # Handle lazy defaults. + if opts[symbol].is_a?(DelayedEvaluator) + if opts[symbol].arity >= 1 + opts[symbol] = opts[symbol].call(self) + else + opts[symbol] = instance_eval(&opts[symbol]) + end + end - # Handle the case where the "default" was a DelayedEvaluator. In - # this case, the block yields an optional parameter of +self+, - # which is the equivalent of "new_resource" - if val.is_a?(DelayedEvaluator) - val = val.call(self) + # Coerce and validate the default value + _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required + _pv_coerce(opts, symbol, coerce) if coerce + # We presently do not validate defaults, for backwards compatibility. +# validate(opts, { symbol => validation }) + + # Defaults are presently "stickily" set on the instance + self.instance_variable_set(iv_symbol, opts[symbol]) end end - self.instance_variable_set(iv_symbol, val) + + # Set the value + else + opts[symbol] = value + unless opts[symbol].is_a?(DelayedEvaluator) + # Coerce and validate the value + _pv_required(opts, symbol, required, explicitly_allows_nil?(symbol, validation)) if required + _pv_coerce(opts, symbol, coerce) if coerce + validate(opts, { symbol => validation }) + end + + self.instance_variable_set(iv_symbol, opts[symbol]) end + + opts[symbol] end private - # Return the value of a parameter, or nil if it doesn't exist. - def _pv_opts_lookup(opts, key) - if opts.has_key?(key.to_s) - opts[key.to_s] - elsif opts.has_key?(key.to_sym) - opts[key.to_sym] - else - nil - end + def explicitly_allows_nil?(key, validation) + validation.has_key?(:is) && _pv_is({ key => nil }, key, validation[:is], raise_error: false) + end + + # Return the value of a parameter, or nil if it doesn't exist. + def _pv_opts_lookup(opts, key) + if opts.has_key?(key.to_s) + opts[key.to_s] + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] + else + nil + end + end + + # Raise an exception if the parameter is not found. + def _pv_required(opts, key, is_required=true, explicitly_allows_nil=false) + if is_required + return true if opts.has_key?(key.to_s) && (explicitly_allows_nil || !opts[key.to_s].nil?) + return true if opts.has_key?(key.to_sym) && (explicitly_allows_nil || !opts[key.to_sym].nil?) + raise Exceptions::ValidationFailed, "Required argument #{key} is missing!" end + end - # Raise an exception if the parameter is not found. - def _pv_required(opts, key, is_required=true) - if is_required - if (opts.has_key?(key.to_s) && !opts[key.to_s].nil?) || - (opts.has_key?(key.to_sym) && !opts[key.to_sym].nil?) - true - else - raise Exceptions::ValidationFailed, "Required argument #{key} is missing!" - end + # + # List of things values must be equal to. + # + # Uses Ruby's `==` to evaluate (equal_to == value). At least one must + # match for the value to be valid. + # + # `nil` passes this validation automatically. + # + # @return [Array,nil] List of things values must be equal to, or nil if + # equal_to is unspecified. + # + def _pv_equal_to(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + to_be = Array(to_be) + to_be.each do |tb| + return true if value == tb end + raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." end + end - def _pv_equal_to(opts, key, to_be) - value = _pv_opts_lookup(opts, key) - unless value.nil? - passes = false - Array(to_be).each do |tb| - passes = true if value == tb - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key} must be equal to one of: #{to_be.join(", ")}! You passed #{value.inspect}." - end + # + # List of things values must be instances of. + # + # Uses value.kind_of?(kind_of) to evaluate. At least one must match for + # the value to be valid. + # + # `nil` automatically passes this validation. + # + def _pv_kind_of(opts, key, to_be) + value = _pv_opts_lookup(opts, key) + unless value.nil? + to_be = Array(to_be) + to_be.each do |tb| + return true if value.kind_of?(tb) end + raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." end + end - # Raise an exception if the parameter is not a kind_of?(to_be) - def _pv_kind_of(opts, key, to_be) - value = _pv_opts_lookup(opts, key) - unless value.nil? - passes = false - Array(to_be).each do |tb| - passes = true if value.kind_of?(tb) - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key} must be a kind of #{to_be}! You passed #{value.inspect}." + # + # List of method names values must respond to. + # + # Uses value.respond_to?(respond_to) to evaluate. At least one must match + # for the value to be valid. + # + def _pv_respond_to(opts, key, method_name_list) + value = _pv_opts_lookup(opts, key) + unless value.nil? + Array(method_name_list).each do |method_name| + unless value.respond_to?(method_name) + raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" end end end + end - # Raise an exception if the parameter does not respond to a given set of methods. - def _pv_respond_to(opts, key, method_name_list) - value = _pv_opts_lookup(opts, key) - unless value.nil? - Array(method_name_list).each do |method_name| - unless value.respond_to?(method_name) - raise Exceptions::ValidationFailed, "Option #{key} must have a #{method_name} method!" + # + # List of things that must not be true about the value. + # + # Calls `value.<thing>?` All responses must be false for the value to be + # valid. + # Values which do not respond to <thing>? are considered valid (because if + # a value doesn't respond to `:readable?`, then it probably isn't + # readable.) + # + # @example + # ```ruby + # property :x, cannot_be: [ :nil, :empty ] + # x [ 1, 2 ] #=> valid + # x 1 #=> valid + # x [] #=> invalid + # x nil #=> invalid + # ``` + # + def _pv_cannot_be(opts, key, predicate_method_base_name) + value = _pv_opts_lookup(opts, key) + if !value.nil? + Array(predicate_method_base_name).each do |method_name| + predicate_method = :"#{method_name}?" + + if value.respond_to?(predicate_method) + if value.send(predicate_method) + raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" end end end end + end - # Assert that parameter returns false when passed a predicate method. - # For example, :cannot_be => :blank will raise a Exceptions::ValidationFailed - # error value.blank? returns a 'truthy' (not nil or false) value. - # - # Note, this will *PASS* if the object doesn't respond to the method. - # So, to make sure a value is not nil and not blank, you need to do - # both :cannot_be => :blank *and* :cannot_be => :nil (or :required => true) - def _pv_cannot_be(opts, key, predicate_method_base_name) - value = _pv_opts_lookup(opts, key) - predicate_method = (predicate_method_base_name.to_s + "?").to_sym - - if value.respond_to?(predicate_method) - if value.send(predicate_method) - raise Exceptions::ValidationFailed, "Option #{key} cannot be #{predicate_method_base_name}" - end - end + # + # The default value for a property. + # + # When the property is not assigned, this will be used. + # + # If this is a lazy value, it will either be passed the resource as a value, + # or if the lazy proc does not take parameters, it will be run in the + # context of the instance with instance_eval. + # + # @example + # ```ruby + # property :x, default: 10 + # ``` + # + # @example + # ```ruby + # property :x + # property :y, default: lazy { x+2 } + # ``` + # + # @example + # ```ruby + # property :x + # property :y, default: lazy { |r| r.x+2 } + # ``` + # + def _pv_default(opts, key, default_value) + value = _pv_opts_lookup(opts, key) + if value.nil? + default_value = default_value.freeze if !default_value.is_a?(DelayedEvaluator) + opts[key] = default_value end + end - # Assign a default value to a parameter. - def _pv_default(opts, key, default_value) - value = _pv_opts_lookup(opts, key) - if value == nil - opts[key] = default_value + # + # List of regexes values that must match. + # + # Uses regex.match() to evaluate. At least one must match for the value to + # be valid. + # + # `nil` passes regex validation automatically. + # + # @example + # ```ruby + # property :x, regex: [ /abc/, /xyz/ ] + # ``` + # + def _pv_regex(opts, key, regex) + value = _pv_opts_lookup(opts, key) + if !value.nil? + Array(regex).each do |r| + return true if r.match(value.to_s) end + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" end + end - # Check a parameter against a regular expression. - def _pv_regex(opts, key, regex) - value = _pv_opts_lookup(opts, key) - if value != nil - passes = false - [ regex ].flatten.each do |r| - if value != nil - if r.match(value.to_s) - passes = true - end - end - end - unless passes - raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} does not match regular expression #{regex.inspect}" + # + # List of procs we pass the value to. + # + # All procs must return true for the value to be valid. If any procs do + # not return true, the key will be used for the message: `"Property x's + # value :y <message>"`. + # + # @example + # ```ruby + # property :x, callbacks: { "is bigger than 10" => proc { |v| v <= 10 }, "is not awesome" => proc { |v| !v.awesome }} + # ``` + # + def _pv_callbacks(opts, key, callbacks) + raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) + value = _pv_opts_lookup(opts, key) + if !value.nil? + callbacks.each do |message, zeproc| + if zeproc.call(value) != true + raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" end end end + end - # Check a parameter against a hash of proc's. - def _pv_callbacks(opts, key, callbacks) - raise ArgumentError, "Callback list must be a hash!" unless callbacks.kind_of?(Hash) - value = _pv_opts_lookup(opts, key) - if value != nil - callbacks.each do |message, zeproc| - if zeproc.call(value) != true - raise Exceptions::ValidationFailed, "Option #{key}'s value #{value} #{message}!" - end - end + # + # Allows a parameter to default to the value of the resource name. + # + # @example + # ```ruby + # property :x, name_property: true + # ``` + # + def _pv_name_property(opts, key, is_name_property=true) + if is_name_property + if opts[key].nil? + opts[key] = self.instance_variable_get("@name") end end + end + alias :_pv_name_attribute :_pv_name_property - # Allow a parameter to default to @name - def _pv_name_attribute(opts, key, is_name_attribute=true) - if is_name_attribute - if opts[key] == nil - opts[key] = self.instance_variable_get("@name") - end + # + # List of valid things values can be. + # + # Uses Ruby's `===` to evaluate (is === value). At least one must match + # for the value to be valid. + # + # If a proc is passed, it is instance_eval'd in the resource, passed the + # value, and must return a truthy or falsey value. + # + # @example Class + # ```ruby + # property :x, String + # x 'valid' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # + # @example Value + # ```ruby + # property :x, [ :a, :b, :c, nil ] + # x :a #=> valid + # x nil #=> valid + # ``` + # + # @example Regex + # ```ruby + # property :x, /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + # @example Proc + # ```ruby + # property :x, proc { |x| x > y } + # property :y, default: 2 + # x 3 #=> valid + # x 1 #=> invalid + # ``` + # + # @example PropertyType + # ```ruby + # type = PropertyType.new(is: String) + # property :x, type + # x 'foo' #=> valid + # x 1 #=> invalid + # x nil #=> invalid + # ``` + # + # @example RSpec Matcher + # ```ruby + # include RSpec::Matchers + # property :x, a_string_matching /bar/ + # x 'foobar' #=> valid + # x 'foo' #=> invalid + # x nil #=> invalid + # ``` + # + def _pv_is(opts, key, to_be, raise_error: true) + return true if !opts.has_key?(key.to_s) && !opts.has_key?(key.to_sym) + value = _pv_opts_lookup(opts, key) + to_be = [ to_be ].flatten(1) + to_be.each do |tb| + if tb.is_a?(Proc) + return true if instance_exec(value, &tb) + else + return true if tb === value end end + + if raise_error + raise Exceptions::ValidationFailed, "Option #{key} must be one of: #{to_be.join(", ")}! You passed #{value.inspect}." + else + false + end + end + + # + # Method to mess with a value before it is validated and stored. + # + # Allows you to transform values into a canonical form that is easy to + # work with. + # + # This is passed the value to transform, and is run in the context of the + # instance (so it has access to other resource properties). It must return + # the value that will be stored in the instance. + # + # @example + # ```ruby + # property :x, Integer, coerce: { |v| v.to_i } + # ``` + # + def _pv_coerce(opts, key, coercer) + if opts.has_key?(key.to_s) + opts[key.to_s] = instance_exec(opts[key], &coercer) + elsif opts.has_key?(key.to_sym) + opts[key.to_sym] = instance_exec(opts[key], &coercer) + end + end end end end - diff --git a/lib/chef/node_map.rb b/lib/chef/node_map.rb index f547018a38..d5eed7c215 100644 --- a/lib/chef/node_map.rb +++ b/lib/chef/node_map.rb @@ -38,30 +38,35 @@ class Chef # # @return [NodeMap] Returns self for possible chaining # - def set(key, value, platform: nil, platform_version: nil, platform_family: nil, os: nil, on_platform: nil, on_platforms: nil, canonical: nil, &block) + def set(key, value, platform: nil, platform_version: nil, platform_family: nil, os: nil, on_platform: nil, on_platforms: nil, canonical: nil, override: nil, &block) Chef::Log.deprecation "The on_platform option to node_map has been deprecated" if on_platform Chef::Log.deprecation "The on_platforms option to node_map has been deprecated" if on_platforms platform ||= on_platform || on_platforms - filters = { platform: platform, platform_version: platform_version, platform_family: platform_family, os: os } - new_matcher = { filters: filters, block: block, value: value, canonical: canonical } - @map[key] ||= [] - # Decide where to insert the matcher; the new value is preferred over - # anything more specific (see `priority_of`) and is preferred over older - # values of the same specificity. (So all other things being equal, - # newest wins.) + filters = {} + filters[:platform] = platform if platform + filters[:platform_version] = platform_version if platform_version + filters[:platform_family] = platform_family if platform_family + filters[:os] = os if os + new_matcher = { value: value, filters: filters } + new_matcher[:block] = block if block + new_matcher[:canonical] = canonical if canonical + new_matcher[:override] = override if override + + # The map is sorted in order of preference already; we just need to find + # our place in it (just before the first value with the same preference level). insert_at = nil - @map[key].each_with_index do |matcher, index| - if specificity(new_matcher) >= specificity(matcher) - insert_at = index - break - end + @map[key] ||= [] + @map[key].each_with_index do |matcher,index| + cmp = compare_matchers(key, new_matcher, matcher) + insert_at ||= index if cmp && cmp <= 0 end if insert_at @map[key].insert(insert_at, new_matcher) else @map[key] << new_matcher end - self + insert_at ||= @map[key].size - 1 + @map end # @@ -116,30 +121,7 @@ class Chef remaining end - private - - # - # Gives a value for "how specific" the matcher is. - # Things which specify more specific filters get a higher number - # (platform_version > platform > platform_family > os); things - # with a block have higher specificity than similar things without - # a block. - # - def specificity(matcher) - if matcher[:filters][:platform_version] - specificity = 8 - elsif matcher[:filters][:platform] - specificity = 6 - elsif matcher[:filters][:platform_family] - specificity = 4 - elsif matcher[:filters][:os] - specificity = 2 - else - specificity = 0 - end - specificity += 1 if matcher[:block] - specificity - end + protected # # Succeeds if: @@ -197,5 +179,48 @@ class Chef return true if canonical.nil? !!canonical == !!matcher[:canonical] end + + def compare_matchers(key, new_matcher, matcher) + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:block] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform_version] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:platform_family] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:filters][:os] } + return cmp if cmp != 0 + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:override] } + return cmp if cmp != 0 + # If all things are identical, return 0 + 0 + end + + def compare_matcher_properties(new_matcher, matcher) + a = yield(new_matcher) + b = yield(matcher) + + # Check for blcacklists ('!windows'). Those always come *after* positive + # whitelists. + a_negated = Array(a).any? { |f| f.is_a?(String) && f.start_with?('!') } + b_negated = Array(b).any? { |f| f.is_a?(String) && f.start_with?('!') } + if a_negated != b_negated + return 1 if a_negated + return -1 if b_negated + end + + # We treat false / true and nil / not-nil with the same comparison + a = nil if a == false + b = nil if b == false + cmp = a <=> b + # This is the case where one is non-nil, and one is nil. The one that is + # nil is "greater" (i.e. it should come last). + if cmp.nil? + return 1 if a.nil? + return -1 if b.nil? + end + cmp + end end end diff --git a/lib/chef/platform/priority_map.rb b/lib/chef/platform/priority_map.rb new file mode 100644 index 0000000000..d559eece78 --- /dev/null +++ b/lib/chef/platform/priority_map.rb @@ -0,0 +1,54 @@ +require 'chef/node_map' + +class Chef + class Platform + class PriorityMap < Chef::NodeMap + def priority(resource_name, priority_array, *filter) + set_priority_array(resource_name.to_sym, priority_array, *filter) + end + + # @api private + def get_priority_array(node, key) + get(node, key) + end + + # @api private + def set_priority_array(key, priority_array, *filter, &block) + priority_array = Array(priority_array) + set(key, priority_array, *filter, &block) + priority_array + end + + # @api private + def list_handlers(node, key, **filters) + list(node, key, **filters).flatten(1).uniq + end + + # + # Priority maps have one extra precedence: priority arrays override "provides," + # and "provides" lines with identical filters sort by class name (ascending). + # + def compare_matchers(key, new_matcher, matcher) + # Priority arrays come before "provides" + if new_matcher[:value].is_a?(Array) != matcher[:value].is_a?(Array) + return new_matcher[:value].is_a?(Array) ? -1 : 1 + end + + cmp = super + if cmp == 0 + # Sort by class name (ascending) as well, if all other properties + # are exactly equal + if new_matcher[:value].is_a?(Class) && !new_matcher[:override] + cmp = compare_matcher_properties(new_matcher, matcher) { |m| m[:value].name } + if cmp < 0 + Chef::Log.warn "You are overriding #{key} on #{new_matcher[:filters].inspect} with #{new_matcher[:value].inspect}: used to be #{matcher[:value].inspect}. Use override: true if this is what you intended." + elsif cmp > 0 + Chef::Log.warn "You declared a new resource #{new_matcher[:value].inspect} for resource #{key}, but it comes alphabetically after #{matcher[:value].inspect} and has the same filters (#{new_matcher[:filters].inspect}), so it will not be used. Use override: true if you want to use it for #{key}." + end + end + end + cmp + end + end + end +end diff --git a/lib/chef/platform/provider_mapping.rb b/lib/chef/platform/provider_mapping.rb index e3a894c8ac..af17d8e1b4 100644 --- a/lib/chef/platform/provider_mapping.rb +++ b/lib/chef/platform/provider_mapping.rb @@ -201,8 +201,8 @@ class Chef begin result = Chef::Provider.const_get(class_name) - Chef::Log.warn("Class Chef::Provider::#{class_name} does not declare 'provides #{convert_to_snake_case(class_name).to_sym.inspect}'.") - Chef::Log.warn("This will no longer work in Chef 13: you must use 'provides' to provide DSL.") + Chef::Log.warn("Class Chef::Provider::#{class_name} does not declare 'resource_name #{convert_to_snake_case(class_name).to_sym.inspect}'.") + Chef::Log.warn("This will no longer work in Chef 13: you must use 'resource_name' to provide DSL.") rescue NameError end end diff --git a/lib/chef/platform/provider_priority_map.rb b/lib/chef/platform/provider_priority_map.rb index 9d703c9178..5599c74c2d 100644 --- a/lib/chef/platform/provider_priority_map.rb +++ b/lib/chef/platform/provider_priority_map.rb @@ -1,29 +1,11 @@ require 'singleton' +require 'chef/platform/priority_map' class Chef class Platform - class ProviderPriorityMap + # @api private + class ProviderPriorityMap < Chef::Platform::PriorityMap include Singleton - - def get_priority_array(node, resource_name) - priority_map.get(node, resource_name.to_sym) - end - - def set_priority_array(resource_name, priority_array, *filter, &block) - priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) - end - - # @api private - def list_handlers(node, resource_name) - priority_map.list(node, resource_name.to_sym).flatten(1).uniq - end - - private - - def priority_map - require 'chef/node_map' - @priority_map ||= Chef::NodeMap.new - end end end end diff --git a/lib/chef/platform/resource_priority_map.rb b/lib/chef/platform/resource_priority_map.rb index fb08debc53..aa57e3ddf0 100644 --- a/lib/chef/platform/resource_priority_map.rb +++ b/lib/chef/platform/resource_priority_map.rb @@ -1,34 +1,17 @@ require 'singleton' +require 'chef/platform/priority_map' class Chef class Platform - class ResourcePriorityMap + # @api private + class ResourcePriorityMap < Chef::Platform::PriorityMap include Singleton - def get_priority_array(node, resource_name, canonical: nil) - priority_map.get(node, resource_name.to_sym, canonical: canonical) - end - - def set_priority_array(resource_name, priority_array, *filter, &block) - priority_map.set(resource_name.to_sym, Array(priority_array), *filter, &block) - end - # @api private - def delete_canonical(resource_name, resource_class) - priority_map.delete_canonical(resource_name, resource_class) - end - - # @api private - def list_handlers(*args) - priority_map.list(*args).flatten(1).uniq + def get_priority_array(node, resource_name, canonical: nil) + super(node, resource_name.to_sym, canonical: canonical) end - private - - def priority_map - require 'chef/node_map' - @priority_map ||= Chef::NodeMap.new - end end end end diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index e50e374804..280277d947 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -26,6 +26,7 @@ require 'chef/mixin/powershell_out' require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' +require 'forwardable' class Chef class Provider @@ -65,6 +66,7 @@ class Chef @recipe_name = nil @cookbook_name = nil + self.class.include_resource_dsl_module(new_resource) end def whyrun_mode? @@ -119,11 +121,11 @@ class Chef check_resource_semantics! # user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode - if !whyrun_mode? || whyrun_supported? + if whyrun_mode? && !whyrun_supported? + events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource) + else load_current_resource events.resource_current_state_loaded(@new_resource, @action, @current_resource) - elsif whyrun_mode? && !whyrun_supported? - events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource) end define_resource_requirements @@ -136,9 +138,7 @@ class Chef # we can't execute the action. # in non-whyrun mode, this will still cause the action to be # executed normally. - if whyrun_supported? && !requirements.action_blocked?(@action) - send("action_#{@action}") - elsif whyrun_mode? + if whyrun_mode? && (!whyrun_supported? || requirements.action_blocked?(@action)) events.resource_bypassed(@new_resource, @action, self) else send("action_#{@action}") @@ -176,13 +176,128 @@ class Chef end def self.provides(short_name, opts={}, &block) - Chef.set_provider_priority_array(short_name, self, opts, &block) + Chef.provider_priority_map.set(short_name, self, opts, &block) end def self.provides?(node, resource) Chef::ProviderResolver.new(node, resource, :nothing).provided_by?(self) end + # + # Include attributes, public and protected methods from this Resource in + # the provider. + # + # If this is set to true, delegate methods are included in the provider so + # that you can call (for example) `attrname` and it will call + # `new_resource.attrname`. + # + # The actual include does not happen until the first time the Provider + # is instantiated (so that we don't have to worry about load order issues). + # + # @param include_resource_dsl [Boolean] Whether to include resource DSL or + # not (defaults to `false`). + # + def self.include_resource_dsl(include_resource_dsl) + @include_resource_dsl = include_resource_dsl + end + + # Create the resource DSL module that forwards resource methods to new_resource + # + # @api private + def self.include_resource_dsl_module(resource) + if @include_resource_dsl && !defined?(@included_resource_dsl_module) + provider_class = self + @included_resource_dsl_module = Module.new do + extend Forwardable + define_singleton_method(:to_s) { "#{resource_class} forwarder module" } + define_singleton_method(:inspect) { to_s } + dsl_methods = + resource.class.public_instance_methods + + resource.class.protected_instance_methods - + provider_class.instance_methods + def_delegators(:new_resource, *dsl_methods) + end + include @included_resource_dsl_module + end + end + + # Enables inline evaluation of resources in provider actions. + # + # Without this option, any resources declared inside the Provider are added + # to the resource collection after the current position at the time the + # action is executed. Because they are added to the primary resource + # collection for the chef run, they can notify other resources outside + # the Provider, and potentially be notified by resources outside the Provider + # (but this is complicated by the fact that they don't exist until the + # provider executes). In this mode, it is impossible to correctly set the + # updated_by_last_action flag on the parent Provider resource, since it + # executes and returns before its component resources are run. + # + # With this option enabled, each action creates a temporary run_context + # with its own resource collection, evaluates the action's code in that + # context, and then converges the resources created. If any resources + # were updated, then this provider's new_resource will be marked updated. + # + # In this mode, resources created within the Provider cannot interact with + # external resources via notifies, though notifications to other + # resources within the Provider will work. Delayed notifications are executed + # at the conclusion of the provider's action, *not* at the end of the + # main chef run. + # + # This mode of evaluation is experimental, but is believed to be a better + # set of tradeoffs than the append-after mode, so it will likely become + # the default in a future major release of Chef. + # + def self.use_inline_resources + extend InlineResources::ClassMethods + include InlineResources + end + + # Chef::Provider::InlineResources + # Implementation of inline resource convergence for providers. See + # Provider.use_inline_resources for a longer explanation. + # + # This code is restricted to a module so that it can be selectively + # applied to providers on an opt-in basis. + # + # @api private + module InlineResources + + # Our run context is a child of the main run context; that gives us a + # whole new resource collection and notification set. + def initialize(resource, run_context) + super(resource, run_context.create_child) + end + + # Class methods for InlineResources. Overrides the `action` DSL method + # with one that enables inline resource convergence. + # + # @api private + module ClassMethods + # Defines an action method on the provider, running the block to + # compile the resources, converging them, and then checking if any + # were updated (and updating new-resource if so) + def action(name, &block) + class_eval <<-EOM, __FILE__, __LINE__+1 + def action_#{name} + return_value = compile_action_#{name} + Chef::Runner.new(run_context).converge + return_value + ensure + if run_context.resource_collection.any? {|r| r.updated? } + new_resource.updated_by_last_action(true) + end + end + EOM + # We put the action in its own method so that super() works. + define_method("compile_action_#{name}", &block) + end + end + + require 'chef/dsl/recipe' + include Chef::DSL::Recipe::FullDSL + end + protected def converge_actions @@ -200,12 +315,14 @@ class Chef # manipulating notifies. converge_by ("evaluate block and run any associated actions") do - saved_run_context = @run_context - @run_context = @run_context.dup - @run_context.resource_collection = Chef::ResourceCollection.new - instance_eval(&block) - Chef::Runner.new(@run_context).converge - @run_context = saved_run_context + saved_run_context = run_context + begin + @run_context = run_context.create_child + instance_eval(&block) + Chef::Runner.new(run_context).converge + ensure + @run_context = saved_run_context + end end end diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb index 19e7c01ab1..6d9b7f4397 100644 --- a/lib/chef/provider/deploy.rb +++ b/lib/chef/provider/deploy.rb @@ -373,11 +373,9 @@ class Chef end def gem_resource_collection_runner - gems_collection = Chef::ResourceCollection.new - gem_packages.each { |rbgem| gems_collection.insert(rbgem) } - gems_run_context = run_context.dup - gems_run_context.resource_collection = gems_collection - Chef::Runner.new(gems_run_context) + child_context = run_context.create_child + gem_packages.each { |rbgem| child_context.resource_collection.insert(rbgem) } + Chef::Runner.new(child_context) end def gem_packages diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb index b5efbb284d..a96c382a01 100644 --- a/lib/chef/provider/lwrp_base.rb +++ b/lib/chef/provider/lwrp_base.rb @@ -28,52 +28,10 @@ class Chef # Base class from which LWRP providers inherit. class LWRPBase < Provider - # Chef::Provider::LWRPBase::InlineResources - # Implementation of inline resource convergence for LWRP providers. See - # Provider::LWRPBase.use_inline_resources for a longer explanation. - # - # This code is restricted to a module so that it can be selectively - # applied to providers on an opt-in basis. - module InlineResources - - # Class methods for InlineResources. Overrides the `action` DSL method - # with one that enables inline resource convergence. - module ClassMethods - # Defines an action method on the provider, using - # recipe_eval_with_update_check to execute the given block. - def action(name, &block) - define_method("action_#{name}") do - recipe_eval_with_update_check(&block) - end - end - end - - # Executes the given block in a temporary run_context with its own - # resource collection. After the block is executed, any resources - # declared inside are converged, and if any are updated, the - # new_resource will be marked updated. - def recipe_eval_with_update_check(&block) - saved_run_context = @run_context - temp_run_context = @run_context.dup - @run_context = temp_run_context - @run_context.resource_collection = Chef::ResourceCollection.new - - return_value = instance_eval(&block) - Chef::Runner.new(@run_context).converge - return_value - ensure - @run_context = saved_run_context - if temp_run_context.resource_collection.any? {|r| r.updated? } - new_resource.updated_by_last_action(true) - end - end - - end - include Chef::DSL::Recipe # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore. - # They are not included by its replacment, Chef::DSL::Recipe, but + # They are not included by its replacement, Chef::DSL::Recipe, but # they may be used in existing LWRPs. include Chef::DSL::PlatformIntrospection include Chef::DSL::DataQuery @@ -122,38 +80,6 @@ class Chef provider_class end - # Enables inline evaluation of resources in provider actions. - # - # Without this option, any resources declared inside the LWRP are added - # to the resource collection after the current position at the time the - # action is executed. Because they are added to the primary resource - # collection for the chef run, they can notify other resources outside - # the LWRP, and potentially be notified by resources outside the LWRP - # (but this is complicated by the fact that they don't exist until the - # provider executes). In this mode, it is impossible to correctly set the - # updated_by_last_action flag on the parent LWRP resource, since it - # executes and returns before its component resources are run. - # - # With this option enabled, each action creates a temporary run_context - # with its own resource collection, evaluates the action's code in that - # context, and then converges the resources created. If any resources - # were updated, then this provider's new_resource will be marked updated. - # - # In this mode, resources created within the LWRP cannot interact with - # external resources via notifies, though notifications to other - # resources within the LWRP will work. Delayed notifications are executed - # at the conclusion of the provider's action, *not* at the end of the - # main chef run. - # - # This mode of evaluation is experimental, but is believed to be a better - # set of tradeoffs than the append-after mode, so it will likely become - # the default in a future major release of Chef. - # - def use_inline_resources - extend InlineResources::ClassMethods - include InlineResources - end - # DSL for defining a provider's actions. def action(name, &block) define_method("action_#{name}") do diff --git a/lib/chef/provider/service/debian.rb b/lib/chef/provider/service/debian.rb index 01505924cb..c67f3f05da 100644 --- a/lib/chef/provider/service/debian.rb +++ b/lib/chef/provider/service/debian.rb @@ -25,8 +25,6 @@ class Chef UPDATE_RC_D_ENABLED_MATCHES = /\/rc[\dS].d\/S|not installed/i UPDATE_RC_D_PRIORITIES = /\/rc([\dS]).d\/([SK])(\d\d)/i - provides :service, platform_family: "debian" - def self.provides?(node, resource) super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:debian) end diff --git a/lib/chef/provider/service/insserv.rb b/lib/chef/provider/service/insserv.rb index 31965a4bc6..4534e33f32 100644 --- a/lib/chef/provider/service/insserv.rb +++ b/lib/chef/provider/service/insserv.rb @@ -24,8 +24,6 @@ class Chef class Service class Insserv < Chef::Provider::Service::Init - provides :service, os: "linux" - def self.provides?(node, resource) super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:insserv) end diff --git a/lib/chef/provider/service/invokercd.rb b/lib/chef/provider/service/invokercd.rb index 5ff24e0dbb..fdf4cbc256 100644 --- a/lib/chef/provider/service/invokercd.rb +++ b/lib/chef/provider/service/invokercd.rb @@ -23,8 +23,6 @@ class Chef class Service class Invokercd < Chef::Provider::Service::Init - provides :service, platform_family: "debian" - def self.provides?(node, resource) super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:invokercd) end diff --git a/lib/chef/provider/service/redhat.rb b/lib/chef/provider/service/redhat.rb index 850953125e..a8deb13aec 100644 --- a/lib/chef/provider/service/redhat.rb +++ b/lib/chef/provider/service/redhat.rb @@ -26,8 +26,6 @@ class Chef CHKCONFIG_ON = /\d:on/ CHKCONFIG_MISSING = /No such/ - provides :service, platform_family: [ "rhel", "fedora", "suse" ] - def self.provides?(node, resource) super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:redhat) end diff --git a/lib/chef/provider/service/upstart.rb b/lib/chef/provider/service/upstart.rb index 8d4aa41035..d8ce6af649 100644 --- a/lib/chef/provider/service/upstart.rb +++ b/lib/chef/provider/service/upstart.rb @@ -27,8 +27,6 @@ class Chef class Upstart < Chef::Provider::Service::Simple UPSTART_STATE_FORMAT = /\w+ \(?(\w+)\)?[\/ ](\w+)/ - provides :service, os: "linux" - def self.provides?(node, resource) super && Chef::Platform::ServiceHelpers.service_resource_providers.include?(:upstart) end diff --git a/lib/chef/provider/user.rb b/lib/chef/provider/user.rb index ad92a72a0a..244b11db98 100644 --- a/lib/chef/provider/user.rb +++ b/lib/chef/provider/user.rb @@ -23,8 +23,6 @@ require 'etc' class Chef class Provider class User < Chef::Provider - provides :user - include Chef::Mixin::Command attr_accessor :user_exists, :locked diff --git a/lib/chef/recipe.rb b/lib/chef/recipe.rb index b4d37c2d61..262560f754 100644 --- a/lib/chef/recipe.rb +++ b/lib/chef/recipe.rb @@ -36,14 +36,7 @@ class Chef # A Recipe object is the context in which Chef recipes are evaluated. class Recipe - include Chef::DSL::DataQuery - include Chef::DSL::PlatformIntrospection - include Chef::DSL::IncludeRecipe - include Chef::DSL::Recipe - include Chef::DSL::RegistryHelper - include Chef::DSL::RebootPending - include Chef::DSL::Audit - include Chef::DSL::Powershell + include Chef::DSL::Recipe::FullDSL include Chef::Mixin::FromFile include Chef::Mixin::Deprecation diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 7fe8a52d95..8d2532dac4 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -58,8 +58,6 @@ class Chef include Chef::Mixin::ShellOut include Chef::Mixin::PowershellOut - NULL_ARG = Object.new - # # The node the current Chef run is using. # @@ -187,8 +185,7 @@ class Chef end self.action = arg else - # Pull the action from the class if it's not set - @action || self.class.default_action + @action end end @@ -531,9 +528,7 @@ class Chef # # Equivalent to #ignore_failure. # - def epic_fail(arg=nil) - ignore_failure(arg) - end + alias :epic_fail :ignore_failure # # Make this resource into an exact (shallow) copy of the other resource. @@ -688,26 +683,133 @@ class Chef # # The provider class for this resource. # + # If `action :x do ... end` has been declared on this resource or its + # superclasses, this will return the `action_provider_class`. + # # If this is not set, `provider_for_action` will dynamically determine the # provider. # # @param arg [String, Symbol, Class] Sets the provider class for this resource. # If passed a String or Symbol, e.g. `:file` or `"file"`, looks up the # provider based on the name. + # # @return The provider class for this resource. # + # @see Chef::Resource.action_provider_class + # def provider(arg=nil) klass = if arg.kind_of?(String) || arg.kind_of?(Symbol) lookup_provider_constant(arg) else arg end - set_or_return(:provider, klass, kind_of: [ Class ]) + set_or_return(:provider, klass, kind_of: [ Class ]) || + self.class.action_provider_class end def provider=(arg) provider(arg) end + # + # Create a property on this resource class. + # + # If a superclass has this property, or if this property has already been + # defined by this resource, this will *override* the previous value. + # + # @param name [Symbol] The name of the property. + # @param type [Object,Array<Object>] The type(s) of this property. + # If present, this is prepended to the `is` validation option. + # @param options [Hash<Symbol,Object>] Validation options. + # @option options [Object,Array] :is An object, or list of + # objects, that must match the value using Ruby's `===` operator + # (`options[:is].any? { |v| v === value }`). + # @option options [Object,Array] :equal_to An object, or list + # of objects, that must be equal to the value using Ruby's `==` + # operator (`options[:is].any? { |v| v == value }`) + # @option options [Regexp,Array<Regexp>] :regex An object, or + # list of objects, that must match the value with `regex.match(value)`. + # @option options [Class,Array<Class>] :kind_of A class, or + # list of classes, that the value must be an instance of. + # @option options [Hash<String,Proc>] :callbacks A hash of + # messages -> procs, all of which match the value. The proc must + # return a truthy or falsey value (true means it matches). + # @option options [Symbol,Array<Symbol>] :respond_to A method + # name, or list of method names, the value must respond to. + # @option options [Symbol,Array<Symbol>] :cannot_be A property, + # or a list of properties, that the value cannot have (such as `:nil` or + # `:empty`). The method with a questionmark at the end is called on the + # value (e.g. `value.empty?`). If the value does not have this method, + # it is considered valid (i.e. if you don't respond to `empty?` we + # assume you are not empty). + # @option options [Proc] :coerce A proc which will be called to + # transform the user input to canonical form. The value is passed in, + # and the transformed value returned as output. Lazy values will *not* + # be passed to this method until after they are evaluated. Called in the + # context of the resource (meaning you can access other properties). + # @option options [Boolean] :required `true` if this property + # must be present; `false` otherwise. This is checked after the resource + # is fully initialized. + # @option options [Boolean] :name_property `true` if this + # property defaults to the same value as `name`. Equivalent to + # `default: lazy { name }`, except that #property_is_set? will + # return `true` if the property is set *or* if `name` is set. + # @option options [Boolean] :name_attribute Same as `name_property`. + # @option options [Object] :default The value this property + # will return if the user does not set one. If this is `lazy`, it will + # be run in the context of the instance (and able to access other + # properties). + # + # @example Bare property + # property :x + # + # @example With just a type + # property :x, String + # + # @example With just options + # property :x, default: 'hi' + # + # @example With type and options + # property :x, String, default: 'hi' + # + def self.property(name, type=NOT_PASSED, **options) + name = name.to_sym + + if type != NOT_PASSED + if options[:is] + options[:is] = ([ type ] + [ options[:is] ]).flatten(1) + else + options[:is] = type + end + end + + define_method(name) do |value=NOT_PASSED| + set_or_return(name, value, options) + end + define_method("#{name}=") do |value| + set_or_return(name, value, options) + end + end + + # + # Whether this property has been set (or whether it has a default that has + # been retrieved). + # + def property_is_set?(name) + name = name.to_sym + instance_variable_defined?("@#{name}") + end + + # + # Create a lazy value for assignment to a default value. + # + # @param block The block to run when the value is retrieved. + # + # @return [Chef::DelayedEvaluator] The lazy value + # + def self.lazy(&block) + DelayedEvaluator.new(&block) + end + # Set or return the list of "state attributes" implemented by the Resource # subclass. State attributes are attributes that describe the desired state # of the system, such as file permissions or ownership. In general, state @@ -773,8 +875,8 @@ class Chef # have. # attr_accessor :allowed_actions - def allowed_actions(value=NULL_ARG) - if value != NULL_ARG + def allowed_actions(value=NOT_PASSED) + if value != NOT_PASSED self.allowed_actions = value end @allowed_actions @@ -908,9 +1010,9 @@ class Chef # # @return [Symbol] The name of this resource type (e.g. `:execute`). # - def self.resource_name(name=NULL_ARG) + def self.resource_name(name=NOT_PASSED) # Setter - if name != NULL_ARG + if name != NOT_PASSED remove_canonical_dsl # Set the resource_name and call provides @@ -924,13 +1026,7 @@ class Chef else @resource_name = nil end - else - # set resource_name automatically if it's not set - if !instance_variable_defined?(:@resource_name) && self.name - resource_name convert_to_snake_case(self.name.split('::')[-1]) - end end - @resource_name end def self.resource_name=(name) @@ -938,6 +1034,19 @@ class Chef end # + # Use the class name as the resource name. + # + # Munges the last part of the class name from camel case to snake case, + # and sets the resource_name to that: + # + # A::B::BlahDBlah -> blah_d_blah + # + def self.use_automatic_resource_name + automatic_name = convert_to_snake_case(self.name.split('::')[-1]) + resource_name automatic_name + end + + # # The module where Chef should look for providers for this resource. # The provider for `MyResource` will be looked up using # `provider_base::MyResource`. Defaults to `Chef::Provider`. @@ -965,7 +1074,7 @@ class Chef # # @param actions [Array<Symbol>] The list of actions to add to allowed_actions. # - # @return [Arrau<Symbol>] The list of actions, as symbols. + # @return [Array<Symbol>] The list of actions, as symbols. # def self.allowed_actions(*actions) @allowed_actions ||= @@ -974,10 +1083,10 @@ class Chef else [ :nothing ] end - @allowed_actions |= actions + @allowed_actions |= actions.flatten end def self.allowed_actions=(value) - @allowed_actions = value + @allowed_actions = value.uniq end # @@ -993,8 +1102,8 @@ class Chef # # @return [Symbol,Array<Symbol>] The default actions for the resource. # - def self.default_action(action_name=NULL_ARG) - unless action_name.equal?(NULL_ARG) + def self.default_action(action_name=NOT_PASSED) + unless action_name.equal?(NOT_PASSED) if action_name.is_a?(Array) @default_action = action_name.map { |arg| arg.to_sym } else @@ -1013,6 +1122,91 @@ class Chef end end def self.default_action=(action_name) + default_action action_name + end + + # + # Define an action on this resource. + # + # The action is defined as a *recipe* block that will be compiled and then + # converged when the action is taken (when Resource is converged). The recipe + # has access to the resource's attributes and methods, as well as the Chef + # recipe DSL. + # + # Resources in the action recipe may notify and subscribe to other resources + # within the action recipe, but cannot notify or subscribe to resources + # in the main Chef run. + # + # Resource actions are *inheritable*: if resource A defines `action :create` + # and B is a subclass of A, B gets all of A's actions. Additionally, + # resource B can define `action :create` and call `super()` to invoke A's + # action code. + # + # The first action defined (besides `:nothing`) will become the default + # action for the resource. + # + # @param name [Symbol] The action name to define. + # @param recipe_block The recipe to run when the action is taken. This block + # takes no parameters, and will be evaluated in a new context containing: + # + # - The resource's public and protected methods (including attributes) + # - The Chef Recipe DSL (file, etc.) + # - super() referring to the parent version of the action (if any) + # + # @return The Action class implementing the action + # + def self.action(action, &recipe_block) + action = action.to_sym + new_action_provider_class.action(action, &recipe_block) + self.allowed_actions += [ action ] + default_action action if default_action == :nothing + end + + # + # The action provider class is an automatic `Provider` created to handle + # actions declared by `action :x do ... end`. + # + # This class will be returned by `resource.provider` if `resource.provider` + # is not set. `provider_for_action` will also use this instead of calling + # out to `Chef::ProviderResolver`. + # + # If the user has not declared actions on this class or its superclasses + # using `action :x do ... end`, then there is no need for this class and + # `action_provider_class` will be `nil`. + # + # @api private + # + def self.action_provider_class + @action_provider_class || + # If the superclass needed one, then we need one as well. + if superclass.respond_to?(:action_provider_class) && superclass.action_provider_class + new_action_provider_class + end + end + + # + # Ensure the action provider class actually gets created. This is called + # when the user does `action :x do ... end`. + # + # @api private + def self.new_action_provider_class + return @action_provider_class if @action_provider_class + + if superclass.respond_to?(:action_provider_class) + base_provider = superclass.action_provider_class + end + base_provider ||= Chef::Provider + + resource_class = self + @action_provider_class = Class.new(base_provider) do + use_inline_resources + include_resource_dsl true + define_singleton_method(:to_s) { "#{resource_class} action provider" } + define_singleton_method(:inspect) { to_s } + define_method(:load_current_resource) {} + end + end + def self.default_action=(action_name) default_action(action_name) end @@ -1087,7 +1281,7 @@ class Chef class << self # back-compat - # NOTE: that we do not support unregistering classes as descendents like + # NOTE: that we do not support unregistering classes as descendants like # we used to for LWRP unloading because that was horrible and removed in # Chef-12. # @deprecated @@ -1111,7 +1305,12 @@ class Chef def self.inherited(child) super @sorted_descendants = nil - child.resource_name + # set resource_name automatically if it's not set + if child.name && !child.resource_name + if child.name =~ /^Chef::Resource::(\w+)$/ + child.resource_name(convert_to_snake_case($1)) + end + end end @@ -1143,7 +1342,7 @@ class Chef remove_canonical_dsl end - result = Chef.set_resource_priority_array(name, self, options, &block) + result = Chef.resource_priority_map.set(name, self, options, &block) Chef::DSL::Resources.add_resource_dsl(name) result end @@ -1208,7 +1407,8 @@ class Chef end def provider_for_action(action) - provider = Chef::ProviderResolver.new(node, self, action).resolve.new(self, run_context) + provider_class = Chef::ProviderResolver.new(node, self, action).resolve + provider = provider_class.new(self, run_context) provider.action = action provider end @@ -1318,23 +1518,22 @@ class Chef # when Chef::Resource::MyLwrp # end # - resource_subclass = class_eval <<-EOM, __FILE__, __LINE__+1 - class Chef::Resource::#{class_name} < resource_class - resource_name nil # we do not actually provide anything - def initialize(*args, &block) - Chef::Log.deprecation("Using an LWRP by its name (#{class_name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") + resource_subclass = Class.new(resource_class) do + resource_name nil # we do not actually provide anything + def initialize(*args, &block) + Chef::Log.deprecation("Using an LWRP by its name (#{self.class.name}) directly is no longer supported in Chef 13 and will be removed. Use Chef::Resource.resource_for_node(node, name) instead.") + super + end + def self.resource_name(*args) + if args.empty? + @resource_name ||= superclass.resource_name + else super end - def self.resource_name(*args) - if args.empty? - @resource_name ||= superclass.resource_name - else - super - end - end - self end - EOM + self + end + eval("Chef::Resource::#{class_name} = resource_subclass") # Make case, is_a and kind_of work with the new subclass, for backcompat. # Any subclass of Chef::Resource::ResourceClass is already a subclass of resource_class # Any subclass of resource_class is considered a subclass of Chef::Resource::ResourceClass diff --git a/lib/chef/resource/lwrp_base.rb b/lib/chef/resource/lwrp_base.rb index c486233020..443e0ed819 100644 --- a/lib/chef/resource/lwrp_base.rb +++ b/lib/chef/resource/lwrp_base.rb @@ -74,19 +74,14 @@ class Chef resource_class end - # Define an attribute on this resource, including optional validation - # parameters. - def attribute(attr_name, validation_opts={}) - define_method(attr_name) do |arg=nil| - set_or_return(attr_name.to_sym, arg, validation_opts) - end - end + alias :attribute :property # Adds +action_names+ to the list of valid actions for this resource. # Does not include superclass's action list when appending. def actions(*action_names) + action_names = action_names.flatten if !action_names.empty? && !@allowed_actions - self.allowed_actions = action_names + self.allowed_actions = ([ :nothing ] + action_names).uniq else allowed_actions(*action_names) end diff --git a/lib/chef/resource/macports_package.rb b/lib/chef/resource/macports_package.rb index 937839b6e1..5843016897 100644 --- a/lib/chef/resource/macports_package.rb +++ b/lib/chef/resource/macports_package.rb @@ -16,10 +16,11 @@ # limitations under the License. # +require 'chef/resource/package' + class Chef class Resource class MacportsPackage < Chef::Resource::Package - provides :package, os: "darwin" end end end diff --git a/lib/chef/resource/package.rb b/lib/chef/resource/package.rb index 1c6da75678..5be1c34b89 100644 --- a/lib/chef/resource/package.rb +++ b/lib/chef/resource/package.rb @@ -100,8 +100,3 @@ class Chef end end end - -require 'chef/chef_class' -require 'chef/resource/homebrew_package' - -Chef.set_resource_priority_array :package, Chef::Resource::HomebrewPackage, os: "darwin" diff --git a/lib/chef/resource_resolver.rb b/lib/chef/resource_resolver.rb index 31b39f7e24..9df627beb2 100644 --- a/lib/chef/resource_resolver.rb +++ b/lib/chef/resource_resolver.rb @@ -18,6 +18,7 @@ require 'chef/exceptions' require 'chef/platform/resource_priority_map' +require 'chef/mixin/convert_to_class_name' class Chef class ResourceResolver diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index 44b05f0cc0..f55d1740b1 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -25,118 +25,223 @@ require 'chef/log' require 'chef/recipe' require 'chef/run_context/cookbook_compiler' require 'chef/event_dispatch/events_output_stream' +require 'forwardable' class Chef # == Chef::RunContext # Value object that loads and tracks the context of a Chef run class RunContext + # + # Global state + # - # Chef::Node object for this run + # + # The node for this run + # + # @return [Chef::Node] + # attr_reader :node - # Chef::CookbookCollection for this run + # + # The set of cookbooks involved in this run + # + # @return [Chef::CookbookCollection] + # attr_reader :cookbook_collection + # # Resource Definitions for this run. Populated when the files in # +definitions/+ are evaluated (this is triggered by #load). + # + # @return [Array[Chef::ResourceDefinition]] + # attr_reader :definitions - ### - # These need to be settable so deploy can run a resource_collection - # independent of any cookbooks via +recipe_eval+ + # + # Event dispatcher for this run. + # + # @return [Chef::EventDispatch::Dispatcher] + # + attr_reader :events - # The Chef::ResourceCollection for this run. Populated by evaluating - # recipes, which is triggered by #load. (See also: CookbookCompiler) - attr_accessor :resource_collection + # + # Hash of factoids for a reboot request. + # + # @return [Hash] + # + attr_accessor :reboot_info + # + # Scoped state + # + + # + # The parent run context. + # + # @return [Chef::RunContext] The parent run context, or `nil` if this is the + # root context. + # + attr_reader :parent_run_context + + # + # The collection of resources intended to be converged (and able to be + # notified). + # + # @return [Chef::ResourceCollection] + # + # @see CookbookCompiler + # + attr_reader :resource_collection + + # # The list of control groups to execute during the audit phase - attr_accessor :audits + # + attr_reader :audits + # + # Notification handling + # + + # # A Hash containing the immediate notifications triggered by resources # during the converge phase of the chef run. - attr_accessor :immediate_notification_collection + # + # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from + # <notifying resource name> => <list of notifications it sent> + # + attr_reader :immediate_notification_collection + # # A Hash containing the delayed (end of run) notifications triggered by # resources during the converge phase of the chef run. - attr_accessor :delayed_notification_collection - - # Event dispatcher for this run. - attr_reader :events - - # Hash of factoids for a reboot request. - attr_reader :reboot_info + # + # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from + # <notifying resource name> => <list of notifications it sent> + # + attr_reader :delayed_notification_collection # Creates a new Chef::RunContext object and populates its fields. This object gets # used by the Chef Server to generate a fully compiled recipe list for a node. # - # === Returns - # object<Chef::RunContext>:: Duh. :) + # @param node [Chef::Node] The node to run against. + # @param cookbook_collection [Chef::CookbookCollection] The cookbooks + # involved in this run. + # @param events [EventDispatch::Dispatcher] The event dispatcher for this + # run. + # def initialize(node, cookbook_collection, events) @node = node @cookbook_collection = cookbook_collection - @resource_collection = Chef::ResourceCollection.new - @audits = {} - @immediate_notification_collection = Hash.new {|h,k| h[k] = []} - @delayed_notification_collection = Hash.new {|h,k| h[k] = []} - @definitions = Hash.new - @loaded_recipes = {} - @loaded_attributes = {} @events = events - @reboot_info = {} - @node.run_context = self - @node.set_cookbook_attribute + node.run_context = self + node.set_cookbook_attribute + + @definitions = Hash.new + @loaded_recipes_hash = {} + @loaded_attributes_hash = {} + @reboot_info = {} @cookbook_compiler = nil + + initialize_child_state end - # Triggers the compile phase of the chef run. Implemented by - # Chef::RunContext::CookbookCompiler + # + # Triggers the compile phase of the chef run. + # + # @param run_list_expansion [Chef::RunList::RunListExpansion] The run list. + # @see Chef::RunContext::CookbookCompiler + # def load(run_list_expansion) @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events) - @cookbook_compiler.compile + cookbook_compiler.compile end - # Adds an immediate notification to the - # +immediate_notification_collection+. The notification should be a - # Chef::Resource::Notification or duck type. + # + # Initialize state that applies to both Chef::RunContext and Chef::ChildRunContext + # + def initialize_child_state + @audits = {} + @resource_collection = Chef::ResourceCollection.new + @immediate_notification_collection = Hash.new {|h,k| h[k] = []} + @delayed_notification_collection = Hash.new {|h,k| h[k] = []} + end + + # + # Adds an immediate notification to the +immediate_notification_collection+. + # + # @param [Chef::Resource::Notification] The notification to add. + # def notifies_immediately(notification) nr = notification.notifying_resource if nr.instance_of?(Chef::Resource) - @immediate_notification_collection[nr.name] << notification + immediate_notification_collection[nr.name] << notification else - @immediate_notification_collection[nr.declared_key] << notification + immediate_notification_collection[nr.declared_key] << notification end end - # Adds a delayed notification to the +delayed_notification_collection+. The - # notification should be a Chef::Resource::Notification or duck type. + # + # Adds a delayed notification to the +delayed_notification_collection+. + # + # @param [Chef::Resource::Notification] The notification to add. + # def notifies_delayed(notification) nr = notification.notifying_resource if nr.instance_of?(Chef::Resource) - @delayed_notification_collection[nr.name] << notification + delayed_notification_collection[nr.name] << notification else - @delayed_notification_collection[nr.declared_key] << notification + delayed_notification_collection[nr.declared_key] << notification end end + # + # Get the list of immediate notifications sent by the given resource. + # + # TODO seriously, this is actually wrong. resource.name is not unique, + # you need the type as well. + # + # @return [Array[Notification]] + # def immediate_notifications(resource) if resource.instance_of?(Chef::Resource) - return @immediate_notification_collection[resource.name] + return immediate_notification_collection[resource.name] else - return @immediate_notification_collection[resource.declared_key] + return immediate_notification_collection[resource.declared_key] end end + # + # Get the list of delayed (end of run) notifications sent by the given + # resource. + # + # TODO seriously, this is actually wrong. resource.name is not unique, + # you need the type as well. + # + # @return [Array[Notification]] + # def delayed_notifications(resource) if resource.instance_of?(Chef::Resource) - return @delayed_notification_collection[resource.name] + return delayed_notification_collection[resource.name] else - return @delayed_notification_collection[resource.declared_key] + return delayed_notification_collection[resource.declared_key] end end + # + # Cookbook and recipe loading + # + + # # Evaluates the recipes +recipe_names+. Used by DSL::IncludeRecipe + # + # @param recipe_names [Array[String]] The list of recipe names (e.g. + # 'my_cookbook' or 'my_cookbook::my_resource'). + # @param current_cookbook The cookbook we are currently running in. + # + # @see DSL::IncludeRecipe#include_recipe + # def include_recipe(*recipe_names, current_cookbook: nil) result_recipes = Array.new recipe_names.flatten.each do |recipe_name| @@ -147,7 +252,21 @@ class Chef result_recipes end + # # Evaluates the recipe +recipe_name+. Used by DSL::IncludeRecipe + # + # TODO I am sort of confused why we have both this and include_recipe ... + # I don't see anything different beyond accepting and returning an + # array of recipes. + # + # @param recipe_names [Array[String]] The recipe name (e.g 'my_cookbook' or + # 'my_cookbook::my_resource'). + # @param current_cookbook The cookbook we are currently running in. + # + # @return A truthy value if the load occurred; `false` if already loaded. + # + # @see DSL::IncludeRecipe#load_recipe + # def load_recipe(recipe_name, current_cookbook: nil) Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe") @@ -175,6 +294,15 @@ ERROR_MESSAGE end end + # + # Load the given recipe from a filename. + # + # @param recipe_file [String] The recipe filename. + # + # @return [Chef::Recipe] The loaded recipe. + # + # @raise [Chef::Exceptions::RecipeNotFound] If the file does not exist. + # def load_recipe_file(recipe_file) if !File.exist?(recipe_file) raise Chef::Exceptions::RecipeNotFound, "could not find recipe file #{recipe_file}" @@ -186,8 +314,19 @@ ERROR_MESSAGE recipe end - # Looks up an attribute file given the +cookbook_name+ and - # +attr_file_name+. Used by DSL::IncludeAttribute + # + # Look up an attribute filename. + # + # @param cookbook_name [String] The cookbook name of the attribute file. + # @param attr_file_name [String] The attribute file's name (not path). + # + # @return [String] The filename. + # + # @see DSL::IncludeAttribute#include_attribute + # + # @raise [Chef::Exceptions::CookbookNotFound] If the cookbook could not be found. + # @raise [Chef::Exceptions::AttributeNotFound] If the attribute file could not be found. + # def resolve_attribute(cookbook_name, attr_file_name) cookbook = cookbook_collection[cookbook_name] raise Chef::Exceptions::CookbookNotFound, "could not find cookbook #{cookbook_name} while loading attribute #{name}" unless cookbook @@ -198,76 +337,152 @@ ERROR_MESSAGE attribute_filename end - # An Array of all recipes that have been loaded. This is stored internally - # as a Hash, so ordering is predictable. # - # Recipe names are given in fully qualified form, e.g., the recipe "nginx" - # will be given as "nginx::default" + # A list of all recipes that have been loaded. + # + # This is stored internally as a Hash, so ordering is predictable. + # + # TODO is the above statement true in a 1.9+ ruby world? Is it relevant? + # + # @return [Array[String]] A list of recipes in fully qualified form, e.g. + # the recipe "nginx" will be given as "nginx::default". + # + # @see #loaded_recipe? To determine if a particular recipe has been loaded. # - # To determine if a particular recipe has been loaded, use #loaded_recipe? def loaded_recipes - @loaded_recipes.keys + loaded_recipes_hash.keys end - # An Array of all attributes files that have been loaded. Stored internally - # using a Hash, so order is predictable. # - # Attribute file names are given in fully qualified form, e.g., - # "nginx::default" instead of "nginx". + # A list of all attributes files that have been loaded. + # + # Stored internally using a Hash, so order is predictable. + # + # TODO is the above statement true in a 1.9+ ruby world? Is it relevant? + # + # @return [Array[String]] A list of attribute file names in fully qualified + # form, e.g. the "nginx" will be given as "nginx::default". + # def loaded_attributes - @loaded_attributes.keys + loaded_attributes_hash.keys end + # + # Find out if a given recipe has been loaded. + # + # @param cookbook [String] Cookbook name. + # @param recipe [String] Recipe name. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_fully_qualified_recipe?(cookbook, recipe) - @loaded_recipes.has_key?("#{cookbook}::#{recipe}") + loaded_recipes_hash.has_key?("#{cookbook}::#{recipe}") end - # Returns true if +recipe+ has been loaded, false otherwise. Default recipe - # names are expanded, so `loaded_recipe?("nginx")` and - # `loaded_recipe?("nginx::default")` are valid and give identical results. + # + # Find out if a given recipe has been loaded. + # + # @param recipe [String] Recipe name. "nginx" and "nginx::default" yield + # the same results. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_recipe?(recipe) cookbook, recipe_name = Chef::Recipe.parse_recipe_name(recipe) loaded_fully_qualified_recipe?(cookbook, recipe_name) end + # + # Mark a given recipe as having been loaded. + # + # @param cookbook [String] Cookbook name. + # @param recipe [String] Recipe name. + # + def loaded_recipe(cookbook, recipe) + loaded_recipes_hash["#{cookbook}::#{recipe}"] = true + end + + # + # Find out if a given attribute file has been loaded. + # + # @param cookbook [String] Cookbook name. + # @param attribute_file [String] Attribute file name. + # + # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise. + # def loaded_fully_qualified_attribute?(cookbook, attribute_file) - @loaded_attributes.has_key?("#{cookbook}::#{attribute_file}") + loaded_attributes_hash.has_key?("#{cookbook}::#{attribute_file}") end + # + # Mark a given attribute file as having been loaded. + # + # @param cookbook [String] Cookbook name. + # @param attribute_file [String] Attribute file name. + # def loaded_attribute(cookbook, attribute_file) - @loaded_attributes["#{cookbook}::#{attribute_file}"] = true + loaded_attributes_hash["#{cookbook}::#{attribute_file}"] = true end ## # Cookbook File Introspection + # + # Find out if the cookbook has the given template. + # + # @param cookbook [String] Cookbook name. + # @param template_name [String] Template name. + # + # @return [Boolean] `true` if the template is in the cookbook, `false` + # otherwise. + # @see Chef::CookbookVersion#has_template_for_node? + # def has_template_in_cookbook?(cookbook, template_name) cookbook = cookbook_collection[cookbook] cookbook.has_template_for_node?(node, template_name) end + # + # Find out if the cookbook has the given file. + # + # @param cookbook [String] Cookbook name. + # @param cb_file_name [String] File name. + # + # @return [Boolean] `true` if the file is in the cookbook, `false` + # otherwise. + # @see Chef::CookbookVersion#has_cookbook_file_for_node? + # def has_cookbook_file_in_cookbook?(cookbook, cb_file_name) cookbook = cookbook_collection[cookbook] cookbook.has_cookbook_file_for_node?(node, cb_file_name) end - # Delegates to CookbookCompiler#unreachable_cookbook? - # Used to raise an error when attempting to load a recipe belonging to a - # cookbook that is not in the dependency graph. See also: CHEF-4367 + # + # Find out whether the given cookbook is in the cookbook dependency graph. + # + # @param cookbook_name [String] Cookbook name. + # + # @return [Boolean] `true` if the cookbook is reachable, `false` otherwise. + # + # @see Chef::CookbookCompiler#unreachable_cookbook? def unreachable_cookbook?(cookbook_name) - @cookbook_compiler.unreachable_cookbook?(cookbook_name) + cookbook_compiler.unreachable_cookbook?(cookbook_name) end + # # Open a stream object that can be printed into and will dispatch to events # - # == Arguments - # options is a hash with these possible options: - # - name: a string that identifies the stream to the user. Preferably short. + # @param name [String] The name of the stream. + # @param options [Hash] Other options for the stream. + # + # @return [EventDispatch::EventsOutputStream] The created stream. # - # Pass a block and the stream will be yielded to it, and close on its own - # at the end of the block. - def open_stream(options = {}) - stream = EventDispatch::EventsOutputStream.new(events, options) + # @yield If a block is passed, it will be run and the stream will be closed + # afterwards. + # @yieldparam stream [EventDispatch::EventsOutputStream] The created stream. + # + def open_stream(name: nil, **options) + stream = EventDispatch::EventsOutputStream.new(events, name: name, **options) if block_given? begin yield stream @@ -280,31 +495,130 @@ ERROR_MESSAGE end # there are options for how to handle multiple calls to these functions: - # 1. first call always wins (never change @reboot_info once set). - # 2. last call always wins (happily change @reboot_info whenever). + # 1. first call always wins (never change reboot_info once set). + # 2. last call always wins (happily change reboot_info whenever). # 3. raise an exception on the first conflict. # 4. disable reboot after this run if anyone ever calls :cancel. # 5. raise an exception on any second call. # 6. ? def request_reboot(reboot_info) - Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to #{reboot_info.inspect}" + Chef::Log::info "Changing reboot status from #{self.reboot_info.inspect} to #{reboot_info.inspect}" @reboot_info = reboot_info end def cancel_reboot - Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to {}" + Chef::Log::info "Changing reboot status from #{reboot_info.inspect} to {}" @reboot_info = {} end def reboot_requested? - @reboot_info.size > 0 + reboot_info.size > 0 + end + + # + # Create a child RunContext. + # + def create_child + ChildRunContext.new(self) end - private + protected - def loaded_recipe(cookbook, recipe) - @loaded_recipes["#{cookbook}::#{recipe}"] = true + attr_reader :cookbook_compiler + attr_reader :loaded_attributes_hash + attr_reader :loaded_recipes_hash + + module Deprecated + ### + # These need to be settable so deploy can run a resource_collection + # independent of any cookbooks via +recipe_eval+ + + def audits=(value) + Chef::Log.deprecation("Setting run_context.audits will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + end + + def immediate_notification_collection=(value) + Chef::Log.deprecation("Setting run_context.immediate_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + end + + def delayed_notification_collection=(value) + Chef::Log.deprecation("Setting run_context.delayed_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.") + end end + prepend Deprecated + + + # + # A child run context. Delegates all root context calls to its parent. + # + # @api private + # + class ChildRunContext < RunContext + extend Forwardable + def_delegators :parent_run_context, *%w( + cancel_reboot + config + cookbook_collection + cookbook_compiler + definitions + events + has_cookbook_file_in_cookbook? + has_template_in_cookbook? + load + loaded_attribute + loaded_attributes + loaded_attributes_hash + loaded_fully_qualified_attribute? + loaded_fully_qualified_recipe? + loaded_recipe + loaded_recipe? + loaded_recipes + loaded_recipes_hash + node + open_stream + reboot_info + reboot_info= + reboot_requested? + request_reboot + resolve_attribute + unreachable_cookbook? + ) + + def initialize(parent_run_context) + @parent_run_context = parent_run_context + + # We don't call super, because we don't bother initializing stuff we're + # going to delegate to the parent anyway. Just initialize things that + # every instance needs. + initialize_child_state + end + CHILD_STATE = %w( + audits + audits= + create_child + delayed_notification_collection + delayed_notification_collection= + delayed_notifications + immediate_notification_collection + immediate_notification_collection= + immediate_notifications + include_recipe + initialize_child_state + load_recipe + load_recipe_file + notifies_immediately + notifies_delayed + parent_run_context + resource_collection + resource_collection= + ).map { |x| x.to_sym } + + # Verify that we didn't miss any methods + missing_methods = superclass.instance_methods(false) - instance_methods(false) - CHILD_STATE + if !missing_methods.empty? + raise "ERROR: not all methods of RunContext accounted for in ChildRunContext! All methods must be marked as child methods with CHILD_STATE or delegated to the parent_run_context. Missing #{missing_methods.join(", ")}." + end + end end end diff --git a/lib/chef/version.rb b/lib/chef/version.rb index 80fd422c55..743a99824d 100644 --- a/lib/chef/version.rb +++ b/lib/chef/version.rb @@ -21,7 +21,7 @@ class Chef CHEF_ROOT = File.dirname(File.expand_path(File.dirname(__FILE__))) - VERSION = '12.4.0.rc.2' + VERSION = '12.5.0.current.0' end # |