diff options
author | John Keiser <john@johnkeiser.com> | 2015-06-23 15:14:29 -0700 |
---|---|---|
committer | John Keiser <john@johnkeiser.com> | 2015-06-23 15:14:29 -0700 |
commit | 2d4e68e84540c536508c9f33fc1e47c3d8ff60f6 (patch) | |
tree | cb9bc6bdd0e887aa7546cdbb750452867934fc5c | |
parent | 82ecae4cbf8bb7924da972576f23cfd267282018 (diff) | |
parent | 1f7f5577cc5cc89d210d6b3578132e2c391d1aca (diff) | |
download | chef-2d4e68e84540c536508c9f33fc1e47c3d8ff60f6.tar.gz |
Merge branch 'jk/resource_actions'
-rw-r--r-- | kitchen-tests/Gemfile | 1 | ||||
-rw-r--r-- | lib/chef/dsl/recipe.rb | 22 | ||||
-rw-r--r-- | lib/chef/event_dispatch/base.rb | 56 | ||||
-rw-r--r-- | lib/chef/provider.rb | 141 | ||||
-rw-r--r-- | lib/chef/provider/deploy.rb | 8 | ||||
-rw-r--r-- | lib/chef/provider/lwrp_base.rb | 76 | ||||
-rw-r--r-- | lib/chef/recipe.rb | 9 | ||||
-rw-r--r-- | lib/chef/resource.rb | 120 | ||||
-rw-r--r-- | lib/chef/run_context.rb | 480 | ||||
-rw-r--r-- | spec/data/run_context/cookbooks/include/recipes/default.rb | 24 | ||||
-rw-r--r-- | spec/data/run_context/cookbooks/include/recipes/includee.rb | 3 | ||||
-rw-r--r-- | spec/integration/recipes/resource_action_spec.rb | 343 | ||||
-rw-r--r-- | spec/unit/provider_spec.rb | 4 | ||||
-rw-r--r-- | spec/unit/run_context/child_run_context_spec.rb | 133 | ||||
-rw-r--r-- | spec/unit/run_context_spec.rb | 7 |
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 |