summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Keiser <john@johnkeiser.com>2015-06-23 15:14:29 -0700
committerJohn Keiser <john@johnkeiser.com>2015-06-23 15:14:29 -0700
commit2d4e68e84540c536508c9f33fc1e47c3d8ff60f6 (patch)
treecb9bc6bdd0e887aa7546cdbb750452867934fc5c
parent82ecae4cbf8bb7924da972576f23cfd267282018 (diff)
parent1f7f5577cc5cc89d210d6b3578132e2c391d1aca (diff)
downloadchef-2d4e68e84540c536508c9f33fc1e47c3d8ff60f6.tar.gz
Merge branch 'jk/resource_actions'
-rw-r--r--kitchen-tests/Gemfile1
-rw-r--r--lib/chef/dsl/recipe.rb22
-rw-r--r--lib/chef/event_dispatch/base.rb56
-rw-r--r--lib/chef/provider.rb141
-rw-r--r--lib/chef/provider/deploy.rb8
-rw-r--r--lib/chef/provider/lwrp_base.rb76
-rw-r--r--lib/chef/recipe.rb9
-rw-r--r--lib/chef/resource.rb120
-rw-r--r--lib/chef/run_context.rb480
-rw-r--r--spec/data/run_context/cookbooks/include/recipes/default.rb24
-rw-r--r--spec/data/run_context/cookbooks/include/recipes/includee.rb3
-rw-r--r--spec/integration/recipes/resource_action_spec.rb343
-rw-r--r--spec/unit/provider_spec.rb4
-rw-r--r--spec/unit/run_context/child_run_context_spec.rb133
-rw-r--r--spec/unit/run_context_spec.rb7
15 files changed, 1214 insertions, 213 deletions
diff --git a/kitchen-tests/Gemfile b/kitchen-tests/Gemfile
index 988d876417..5e1907ba1f 100644
--- a/kitchen-tests/Gemfile
+++ b/kitchen-tests/Gemfile
@@ -6,4 +6,5 @@ group :end_to_end do
gem 'kitchen-appbundle-updater', '~> 0.0.1'
gem "kitchen-vagrant", '~> 0.17.0'
gem 'kitchen-ec2', github: 'test-kitchen/kitchen-ec2'
+ gem 'vagrant-wrapper'
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/provider.rb b/lib/chef/provider.rb
index 131b72cd23..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}")
@@ -183,6 +183,121 @@ class Chef
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/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 ed0dbb50a7..bae7608f5b 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -187,8 +187,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 +530,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,21 +685,28 @@ 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)
@@ -925,7 +929,6 @@ class Chef
@resource_name = nil
end
end
-
@resource_name
end
def self.resource_name=(name)
@@ -933,6 +936,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`.
@@ -1008,6 +1024,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
@@ -1082,7 +1183,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
@@ -1208,7 +1309,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
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/spec/data/run_context/cookbooks/include/recipes/default.rb b/spec/data/run_context/cookbooks/include/recipes/default.rb
new file mode 100644
index 0000000000..8d22994252
--- /dev/null
+++ b/spec/data/run_context/cookbooks/include/recipes/default.rb
@@ -0,0 +1,24 @@
+module ::RanResources
+ def self.resources
+ @resources ||= []
+ end
+end
+class RunContextCustomResource < Chef::Resource
+ action :create do
+ ruby_block '4' do
+ block { RanResources.resources << 4 }
+ end
+ recipe_eval do
+ ruby_block '1' do
+ block { RanResources.resources << 1 }
+ end
+ include_recipe 'include::includee'
+ ruby_block '3' do
+ block { RanResources.resources << 3 }
+ end
+ end
+ ruby_block '5' do
+ block { RanResources.resources << 5 }
+ end
+ end
+end
diff --git a/spec/data/run_context/cookbooks/include/recipes/includee.rb b/spec/data/run_context/cookbooks/include/recipes/includee.rb
new file mode 100644
index 0000000000..87bb7f114e
--- /dev/null
+++ b/spec/data/run_context/cookbooks/include/recipes/includee.rb
@@ -0,0 +1,3 @@
+ruby_block '2' do
+ block { RanResources.resources << 2 }
+end
diff --git a/spec/integration/recipes/resource_action_spec.rb b/spec/integration/recipes/resource_action_spec.rb
new file mode 100644
index 0000000000..4786294803
--- /dev/null
+++ b/spec/integration/recipes/resource_action_spec.rb
@@ -0,0 +1,343 @@
+require 'support/shared/integration/integration_helper'
+
+describe "Resource.action" do
+ include IntegrationSupport
+
+ def converge(str=nil, file=nil, line=nil, &block)
+ if block
+ super(&block)
+ else
+ super() do
+ eval(str, nil, file, line)
+ end
+ end
+ end
+
+ shared_context "ActionJackson" do
+ it "The default action is the first declared action" do
+ converge <<-EOM, __FILE__, __LINE__+1
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ end
+ EOM
+ expect(ActionJackson.ran_action).to eq :access_recipe_dsl
+ expect(ActionJackson.succeeded).to eq true
+ end
+
+ it "The action can access recipe DSL" do
+ converge <<-EOM, __FILE__, __LINE__+1
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ action :access_recipe_dsl
+ end
+ EOM
+ expect(ActionJackson.ran_action).to eq :access_recipe_dsl
+ expect(ActionJackson.succeeded).to eq true
+ end
+
+ it "The action can access attributes" do
+ converge <<-EOM, __FILE__, __LINE__+1
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ action :access_attribute
+ end
+ EOM
+ expect(ActionJackson.ran_action).to eq :access_attribute
+ expect(ActionJackson.succeeded).to eq 'foo!'
+ end
+
+ it "The action can access public methods" do
+ converge <<-EOM, __FILE__, __LINE__+1
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ action :access_method
+ end
+ EOM
+ expect(ActionJackson.ran_action).to eq :access_method
+ expect(ActionJackson.succeeded).to eq 'foo_public!'
+ end
+
+ it "The action can access protected methods" do
+ converge <<-EOM, __FILE__, __LINE__+1
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ action :access_protected_method
+ end
+ EOM
+ expect(ActionJackson.ran_action).to eq :access_protected_method
+ expect(ActionJackson.succeeded).to eq 'foo_protected!'
+ end
+
+ it "The action cannot access private methods" do
+ expect {
+ converge(<<-EOM, __FILE__, __LINE__+1)
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ action :access_private_method
+ end
+ EOM
+ }.to raise_error(NameError)
+ expect(ActionJackson.ran_action).to eq :access_private_method
+ end
+
+ it "The action cannot access resource instance variables" do
+ converge <<-EOM, __FILE__, __LINE__+1
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ action :access_instance_variable
+ end
+ EOM
+ expect(ActionJackson.ran_action).to eq :access_instance_variable
+ expect(ActionJackson.succeeded).to be_nil
+ end
+
+ it "The action does not compile until the prior resource has converged" do
+ converge <<-EOM, __FILE__, __LINE__+1
+ ruby_block 'wow' do
+ block do
+ ActionJackson.ruby_block_converged = 'ruby_block_converged!'
+ end
+ end
+
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ action :access_class_method
+ end
+ EOM
+ expect(ActionJackson.ran_action).to eq :access_class_method
+ expect(ActionJackson.succeeded).to eq 'ruby_block_converged!'
+ end
+
+ it "The action's resources converge before the next resource converges" do
+ converge <<-EOM, __FILE__, __LINE__+1
+ #{resource_dsl} 'hi' do
+ foo 'foo!'
+ action :access_attribute
+ end
+
+ ruby_block 'wow' do
+ block do
+ ActionJackson.ruby_block_converged = ActionJackson.succeeded
+ end
+ end
+ EOM
+ expect(ActionJackson.ran_action).to eq :access_attribute
+ expect(ActionJackson.succeeded).to eq 'foo!'
+ expect(ActionJackson.ruby_block_converged).to eq 'foo!'
+ end
+ end
+
+ context "With resource 'action_jackson'" do
+ before(:context) {
+ class ActionJackson < Chef::Resource
+ use_automatic_resource_name
+ def foo(value=nil)
+ @foo = value if value
+ @foo
+ end
+ def blarghle(value=nil)
+ @blarghle = value if value
+ @blarghle
+ end
+
+ class <<self
+ attr_accessor :ran_action
+ attr_accessor :succeeded
+ attr_accessor :ruby_block_converged
+ end
+
+ public
+ def foo_public
+ 'foo_public!'
+ end
+ protected
+ def foo_protected
+ 'foo_protected!'
+ end
+ private
+ def foo_private
+ 'foo_private!'
+ end
+
+ public
+ action :access_recipe_dsl do
+ ActionJackson.ran_action = :access_recipe_dsl
+ ruby_block 'hi there' do
+ block do
+ ActionJackson.succeeded = true
+ end
+ end
+ end
+ action :access_attribute do
+ ActionJackson.ran_action = :access_attribute
+ ActionJackson.succeeded = foo
+ ActionJackson.succeeded += " #{blarghle}" if blarghle
+ ActionJackson.succeeded += " #{bar}" if respond_to?(:bar)
+ end
+ action :access_attribute2 do
+ ActionJackson.ran_action = :access_attribute2
+ ActionJackson.succeeded = foo
+ ActionJackson.succeeded += " #{blarghle}" if blarghle
+ ActionJackson.succeeded += " #{bar}" if respond_to?(:bar)
+ end
+ action :access_method do
+ ActionJackson.ran_action = :access_method
+ ActionJackson.succeeded = foo_public
+ end
+ action :access_protected_method do
+ ActionJackson.ran_action = :access_protected_method
+ ActionJackson.succeeded = foo_protected
+ end
+ action :access_private_method do
+ ActionJackson.ran_action = :access_private_method
+ ActionJackson.succeeded = foo_private
+ end
+ action :access_instance_variable do
+ ActionJackson.ran_action = :access_instance_variable
+ ActionJackson.succeeded = @foo
+ end
+ action :access_class_method do
+ ActionJackson.ran_action = :access_class_method
+ ActionJackson.succeeded = ActionJackson.ruby_block_converged
+ end
+ end
+ }
+ before(:each) {
+ ActionJackson.ran_action = :error
+ ActionJackson.succeeded = :error
+ ActionJackson.ruby_block_converged = :error
+ }
+
+ it_behaves_like "ActionJackson" do
+ let(:resource_dsl) { :action_jackson }
+ end
+
+ context "And 'action_jackgrandson' inheriting from ActionJackson and changing nothing" do
+ before(:context) {
+ class ActionJackgrandson < ActionJackson
+ use_automatic_resource_name
+ end
+ }
+
+ it_behaves_like "ActionJackson" do
+ let(:resource_dsl) { :action_jackgrandson }
+ end
+ end
+
+ context "And 'action_jackalope' inheriting from ActionJackson with an extra attribute and action" do
+ before(:context) {
+ class ActionJackalope < ActionJackson
+ use_automatic_resource_name
+
+ def foo(value=nil)
+ @foo = "#{value}alope" if value
+ @foo
+ end
+ def bar(value=nil)
+ @bar = "#{value}alope" if value
+ @bar
+ end
+ class <<self
+ attr_accessor :jackalope_ran
+ end
+ action :access_jackalope do
+ ActionJackalope.jackalope_ran = :access_jackalope
+ ActionJackalope.succeeded = "#{foo} #{blarghle} #{bar}"
+ end
+ action :access_attribute do
+ super()
+ ActionJackalope.jackalope_ran = :access_attribute
+ ActionJackalope.succeeded = ActionJackson.succeeded
+ end
+ end
+ }
+ before do
+ ActionJackalope.jackalope_ran = nil
+ end
+
+ context "action_jackson still behaves the same" do
+ it_behaves_like "ActionJackson" do
+ let(:resource_dsl) { :action_jackson }
+ end
+ end
+
+ it "The default action remains the same even though new actions were specified first" do
+ converge {
+ action_jackalope 'hi' do
+ foo 'foo!'
+ bar 'bar!'
+ end
+ }
+ expect(ActionJackson.ran_action).to eq :access_recipe_dsl
+ expect(ActionJackson.succeeded).to eq true
+ end
+
+ it "new actions run, and can access overridden, new, and overridden attributes" do
+ converge {
+ action_jackalope 'hi' do
+ foo 'foo!'
+ bar 'bar!'
+ blarghle 'blarghle!'
+ action :access_jackalope
+ end
+ }
+ expect(ActionJackalope.jackalope_ran).to eq :access_jackalope
+ expect(ActionJackalope.succeeded).to eq "foo!alope blarghle! bar!alope"
+ end
+
+ it "overridden actions run, call super, and can access overridden, new, and overridden attributes" do
+ converge {
+ action_jackalope 'hi' do
+ foo 'foo!'
+ bar 'bar!'
+ blarghle 'blarghle!'
+ action :access_attribute
+ end
+ }
+ expect(ActionJackson.ran_action).to eq :access_attribute
+ expect(ActionJackson.succeeded).to eq "foo!alope blarghle! bar!alope"
+ expect(ActionJackalope.jackalope_ran).to eq :access_attribute
+ expect(ActionJackalope.succeeded).to eq "foo!alope blarghle! bar!alope"
+ end
+
+ it "non-overridden actions run and can access overridden and non-overridden variables (but not necessarily new ones)" do
+ converge {
+ action_jackalope 'hi' do
+ foo 'foo!'
+ bar 'bar!'
+ blarghle 'blarghle!'
+ action :access_attribute2
+ end
+ }
+ expect(ActionJackson.ran_action).to eq :access_attribute2
+ expect(ActionJackson.succeeded).to eq("foo!alope blarghle! bar!alope").or(eq("foo!alope blarghle!"))
+ end
+ end
+ end
+
+ context "With a resource with no actions" do
+ before(:context) {
+ class NoActionJackson < Chef::Resource
+ use_automatic_resource_name
+
+ def foo(value=nil)
+ @foo = value if value
+ @foo
+ end
+
+ class <<self
+ attr_accessor :action_was
+ end
+ end
+ }
+ it "The default action is :nothing" do
+ converge {
+ no_action_jackson 'hi' do
+ foo 'foo!'
+ NoActionJackson.action_was = action
+ end
+ }
+ expect(NoActionJackson.action_was).to eq :nothing
+ end
+ end
+end
diff --git a/spec/unit/provider_spec.rb b/spec/unit/provider_spec.rb
index d7a34bc21b..97b88b1732 100644
--- a/spec/unit/provider_spec.rb
+++ b/spec/unit/provider_spec.rb
@@ -114,9 +114,7 @@ describe Chef::Provider do
end
it "does not re-load recipes when creating the temporary run context" do
- # we actually want to test that RunContext#load is never called, but we
- # can't stub all instances of an object with rspec's mocks. :/
- allow(Chef::RunContext).to receive(:new).and_raise("not supposed to happen")
+ expect_any_instance_of(Chef::RunContext).not_to receive(:load)
snitch = Proc.new {temporary_collection = @run_context.resource_collection}
@provider.send(:recipe_eval, &snitch)
end
diff --git a/spec/unit/run_context/child_run_context_spec.rb b/spec/unit/run_context/child_run_context_spec.rb
new file mode 100644
index 0000000000..63586e459c
--- /dev/null
+++ b/spec/unit/run_context/child_run_context_spec.rb
@@ -0,0 +1,133 @@
+#
+# Author:: Adam Jacob (<adam@opscode.com>)
+# Author:: Tim Hinderliter (<tim@opscode.com>)
+# Author:: Christopher Walters (<cw@opscode.com>)
+# Copyright:: Copyright (c) 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 'spec_helper'
+require 'support/lib/library_load_order'
+
+describe Chef::RunContext::ChildRunContext do
+ context "with a run context with stuff in it" do
+ let(:chef_repo_path) { File.expand_path(File.join(CHEF_SPEC_DATA, "run_context", "cookbooks")) }
+ let(:cookbook_collection) {
+ cl = Chef::CookbookLoader.new(chef_repo_path)
+ cl.load_cookbooks
+ Chef::CookbookCollection.new(cl)
+ }
+ let(:node) {
+ node = Chef::Node.new
+ node.run_list << "test" << "test::one" << "test::two"
+ node
+ }
+ let(:events) { Chef::EventDispatch::Dispatcher.new }
+ let(:run_context) { Chef::RunContext.new(node, cookbook_collection, events) }
+
+ context "and a child run context" do
+ let(:child) { run_context.create_child }
+
+ it "parent_run_context is set to the parent" do
+ expect(child.parent_run_context).to eq run_context
+ end
+
+ it "audits is not the same as the parent" do
+ expect(child.audits.object_id).not_to eq run_context.audits.object_id
+ child.audits['hi'] = 'lo'
+ expect(child.audits['hi']).to eq('lo')
+ expect(run_context.audits['hi']).not_to eq('lo')
+ end
+
+ it "resource_collection is not the same as the parent" do
+ expect(child.resource_collection.object_id).not_to eq run_context.resource_collection.object_id
+ f = Chef::Resource::File.new('hi', child)
+ child.resource_collection.insert(f)
+ expect(child.resource_collection).to include f
+ expect(run_context.resource_collection).not_to include f
+ end
+
+ it "immediate_notification_collection is not the same as the parent" do
+ expect(child.immediate_notification_collection.object_id).not_to eq run_context.immediate_notification_collection.object_id
+ src = Chef::Resource::File.new('hi', child)
+ dest = Chef::Resource::File.new('argh', child)
+ notification = Chef::Resource::Notification.new(dest, :create, src)
+ child.notifies_immediately(notification)
+ expect(child.immediate_notification_collection['file[hi]']).to eq([notification])
+ expect(run_context.immediate_notification_collection['file[hi]']).not_to eq([notification])
+ end
+
+ it "immediate_notifications is not the same as the parent" do
+ src = Chef::Resource::File.new('hi', child)
+ dest = Chef::Resource::File.new('argh', child)
+ notification = Chef::Resource::Notification.new(dest, :create, src)
+ child.notifies_immediately(notification)
+ expect(child.immediate_notifications(src)).to eq([notification])
+ expect(run_context.immediate_notifications(src)).not_to eq([notification])
+ end
+
+ it "delayed_notification_collection is not the same as the parent" do
+ expect(child.delayed_notification_collection.object_id).not_to eq run_context.delayed_notification_collection.object_id
+ src = Chef::Resource::File.new('hi', child)
+ dest = Chef::Resource::File.new('argh', child)
+ notification = Chef::Resource::Notification.new(dest, :create, src)
+ child.notifies_delayed(notification)
+ expect(child.delayed_notification_collection['file[hi]']).to eq([notification])
+ expect(run_context.delayed_notification_collection['file[hi]']).not_to eq([notification])
+ end
+
+ it "delayed_notifications is not the same as the parent" do
+ src = Chef::Resource::File.new('hi', child)
+ dest = Chef::Resource::File.new('argh', child)
+ notification = Chef::Resource::Notification.new(dest, :create, src)
+ child.notifies_delayed(notification)
+ expect(child.delayed_notifications(src)).to eq([notification])
+ expect(run_context.delayed_notifications(src)).not_to eq([notification])
+ end
+
+ it "create_child creates a child-of-child" do
+ c = child.create_child
+ expect(c.parent_run_context).to eq child
+ end
+
+ context "after load('include::default')" do
+ before do
+ run_list = Chef::RunList.new('include::default').expand('_default')
+ # TODO not sure why we had to do this to get everything to work ...
+ node.automatic_attrs[:recipes] = []
+ child.load(run_list)
+ end
+
+ it "load_recipe loads into the child" do
+ expect(child.resource_collection).to be_empty
+ child.load_recipe("include::includee")
+ expect(child.resource_collection).not_to be_empty
+ end
+
+ it "include_recipe loads into the child" do
+ expect(child.resource_collection).to be_empty
+ child.include_recipe("include::includee")
+ expect(child.resource_collection).not_to be_empty
+ end
+
+ it "load_recipe_file loads into the child" do
+ expect(child.resource_collection).to be_empty
+ child.load_recipe_file(File.expand_path("include/recipes/includee.rb", chef_repo_path))
+ expect(child.resource_collection).not_to be_empty
+ end
+ end
+ end
+ end
+end
diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb
index e20ba63b72..99801575ef 100644
--- a/spec/unit/run_context_spec.rb
+++ b/spec/unit/run_context_spec.rb
@@ -68,6 +68,9 @@ describe Chef::RunContext do
"dependency2" => {
"version" => "0.0.0",
},
+ "include" => {
+ "version" => "0.0.0",
+ },
"no-default-attr" => {
"version" => "0.0.0",
},
@@ -84,6 +87,10 @@ describe Chef::RunContext do
)
end
+ it "has a nil parent_run_context" do
+ expect(run_context.parent_run_context).to be_nil
+ end
+
describe "loading cookbooks for a run list" do
before do