summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2019-06-17 21:07:08 -0700
committerLamont Granquist <lamont@scriptkiddie.org>2019-08-12 11:29:16 -0700
commit35a63ddc192e50c45f6e94a3b270ed0e75c93668 (patch)
treefed93952978c3b75febae5d90f80e1f0cd5fcc8b
parent84da5f7a45d7ccba250d160626e6da8762a7f222 (diff)
downloadchef-35a63ddc192e50c45f6e94a3b270ed0e75c93668.tar.gz
Add unified_mode switch for resources
This is inspired by "use_inline_resources". Setting `unified_mode false` in a resource would be the existing behavior with separate compile/converge phases. Setting `unified_mode true` in a resource will eliminate the converge phase. Reverse notifications and delayed notifications will still fire. The resource action will behave like all resources are executing at compile time. As a aside, notifications have never worked for resources firing at compile time. This implementation gets that behavior correct so that notifications will work. Of course forward immediate notifications to resources not yet declared will not be possible. Setting `resource_unified_mode_default true` in `Chef::Config` would turn off the split compile/converge mode for every custom resource. NOTE: This does not affect recipe mode at all. Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
-rw-r--r--chef-config/lib/chef-config/config.rb16
-rw-r--r--lib/chef/exceptions.rb12
-rw-r--r--lib/chef/provider.rb6
-rw-r--r--lib/chef/resource.rb48
-rw-r--r--lib/chef/resource/resource_notification.rb30
-rw-r--r--lib/chef/resource_collection.rb6
-rw-r--r--lib/chef/run_context.rb78
-rw-r--r--lib/chef/runner.rb63
-rw-r--r--spec/integration/recipes/unified_mode_spec.rb876
9 files changed, 1061 insertions, 74 deletions
diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb
index d136a8f79c..b0583b7a06 100644
--- a/chef-config/lib/chef-config/config.rb
+++ b/chef-config/lib/chef-config/config.rb
@@ -875,6 +875,7 @@ module ChefConfig
#
# NOTE: CHANGING THIS SETTING MAY CAUSE CORRUPTION, DATA LOSS AND
# INSTABILITY.
+ #
default :file_atomic_update, true
# There are 3 possible values for this configuration setting.
@@ -882,19 +883,28 @@ module ChefConfig
# false => file staging is done via tempfiles under ENV['TMP']
# :auto => file staging will try using destination directory if possible and
# will fall back to ENV['TMP'] if destination directory is not usable.
+ #
default :file_staging_uses_destdir, :auto
# Exit if another run is in progress and the chef-client is unable to
# get the lock before time expires. If nil, no timeout is enforced. (Exits
# immediately if 0.)
+ #
default :run_lock_timeout, nil
# Number of worker threads for syncing cookbooks in parallel. Increasing
# this number can result in gateway errors from the server (namely 503 and 504).
# If you are seeing this behavior while using the default setting, reducing
# the number of threads will help.
+ #
default :cookbook_sync_threads, 10
+ # True if all resources by default default to unified mode, with all resources
+ # applying in "compile" mode, with no "converge" mode. False is backwards compatible
+ # setting for Chef 11-15 behavior. This will break forward notifications.
+ #
+ default :resource_unified_mode_default, false
+
# At the beginning of the Chef Client run, the cookbook manifests are downloaded which
# contain URLs for every file in every relevant cookbook. Most of the files
# (recipes, resources, providers, libraries, etc) are immediately synchronized
@@ -920,9 +930,9 @@ module ChefConfig
default :no_lazy_load, true
# A whitelisted array of attributes you want sent over the wire when node
- # data is saved.
- # The default setting is nil, which collects all data. Setting to [] will not
- # collect any data for save.
+ # data is saved. The default setting is nil, which collects all data. Setting
+ # to [] will not collect any data for save.
+ #
default :automatic_attribute_whitelist, nil
default :default_attribute_whitelist, nil
default :normal_attribute_whitelist, nil
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index 7687e7d89c..40d20cc3ac 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -509,5 +509,17 @@ class Chef
super "Conflicting requirements for gem '#{gem_name}': Both #{value1.inspect} and #{value2.inspect} given for option #{option.inspect}"
end
end
+
+ class UnifiedModeImmediateSubscriptionEarlierResource < RuntimeError
+ def initialize(notification)
+ super "immediate subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode"
+ end
+ end
+
+ class UnifiedModeBeforeSubscriptionEarlierResource < RuntimeError
+ def initialize(notification)
+ super "before subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode"
+ end
+ end
end
end
diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb
index 6d1985bbbb..fb5697fd0c 100644
--- a/lib/chef/provider.rb
+++ b/lib/chef/provider.rb
@@ -1,7 +1,7 @@
#
# Author:: Adam Jacob (<adam@chef.io>)
# Author:: Christopher Walters (<cw@chef.io>)
-# Copyright:: Copyright 2008-2016, 2009-2018, Chef Software Inc.
+# Copyright:: Copyright 2008-2016, 2009-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -238,8 +238,10 @@ class Chef
def compile_and_converge_action(&block)
old_run_context = run_context
@run_context = run_context.create_child
+ @run_context.resource_collection.unified_mode = new_resource.class.unified_mode
+ runner = Chef::Runner.new(@run_context)
return_value = instance_eval(&block)
- Chef::Runner.new(run_context).converge
+ runner.converge
return_value
ensure
if run_context.resource_collection.any?(&:updated?)
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index 7f0895d6c9..a413e3e8d4 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -182,7 +182,7 @@ class Chef
{ action: { kind_of: Symbol, equal_to: allowed_actions } }
)
# the resource effectively sends a delayed notification to itself
- run_context.add_delayed_action(Notification.new(self, action, self))
+ run_context.add_delayed_action(Notification.new(self, action, self, run_context.unified_mode))
end
end
@@ -453,7 +453,6 @@ class Chef
#
attr_reader :elapsed_time
- #
# @return [Boolean] If the resource was executed by the runner
#
attr_accessor :executed_by_runner
@@ -985,6 +984,16 @@ class Chef
resource_name automatic_name
end
+ # If the resource's action should run in separated compile/converge mode.
+ #
+ # @param flag [Boolean] value to set unified_mode to
+ # @return [Boolean] unified_mode value
+ def self.unified_mode(flag = nil)
+ @unified_mode = Chef::Config[:resource_unified_mode_default] if @unified_mode.nil?
+ @unified_mode = flag unless flag.nil?
+ !!@unified_mode
+ end
+
#
# The list of allowed actions for the resource.
#
@@ -1038,7 +1047,6 @@ class Chef
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
@@ -1076,7 +1084,6 @@ class Chef
default_action action if Array(default_action) == [:nothing]
end
- #
# Define a method to load up this resource's properties with the current
# actual values.
#
@@ -1087,7 +1094,6 @@ class Chef
define_method(:load_current_value!, &load_block)
end
- #
# Call this in `load_current_value` to indicate that the value does not
# exist and that `current_resource` should therefore be `nil`.
#
@@ -1097,7 +1103,6 @@ class Chef
raise Chef::Exceptions::CurrentValueDoesNotExist
end
- #
# Get the current actual value of this resource.
#
# This does not cache--a new value will be returned each time.
@@ -1154,7 +1159,6 @@ class Chef
end
end
- #
# Ensure the action class actually gets created. This is called
# when the user does `action :x do ... end`.
#
@@ -1211,22 +1215,27 @@ class Chef
# @return [Chef::RunContext] The run context for this Resource. This is
# where the context for the current Chef run is stored, including the node
# and the resource collection.
+ #
attr_accessor :run_context
# @return [Mixlib::Log::Child] The logger for this resources. This is a child
# of the run context's logger, if one exists.
+ #
attr_reader :logger
# @return [String] The cookbook this resource was declared in.
+ #
attr_accessor :cookbook_name
# @return [String] The recipe this resource was declared in.
+ #
attr_accessor :recipe_name
# @return [Chef::Provider] The provider this resource was declared in (if
# it was declared in an LWRP). When you call methods that do not exist
# on this Resource, Chef will try to call the method on the provider
# as well before giving up.
+ #
attr_accessor :enclosing_provider
# @return [String] The source line where this resource was declared.
@@ -1234,6 +1243,7 @@ class Chef
# of these formats:
# /some/path/to/file.rb:80:in `wombat_tears'
# C:/some/path/to/file.rb:80 in 1`wombat_tears'
+ #
attr_accessor :source_line
# @return [String] The actual name that was used to create this resource.
@@ -1242,37 +1252,40 @@ class Chef
# user will expect to see the thing they wrote, not the type that was
# returned. May be `nil`, in which case callers should read #resource_name.
# See #declared_key.
+ #
attr_accessor :declared_type
- #
# Iterates over all immediate and delayed notifications, calling
# resolve_resource_reference on each in turn, causing them to
# resolve lazy/forward references.
- def resolve_notification_references
+ #
+ def resolve_notification_references(always_raise = false)
run_context.before_notifications(self).each do |n|
- n.resolve_resource_reference(run_context.resource_collection)
+ n.resolve_resource_reference(run_context.resource_collection, true)
end
+
run_context.immediate_notifications(self).each do |n|
- n.resolve_resource_reference(run_context.resource_collection)
+ n.resolve_resource_reference(run_context.resource_collection, always_raise)
end
+
run_context.delayed_notifications(self).each do |n|
- n.resolve_resource_reference(run_context.resource_collection)
+ n.resolve_resource_reference(run_context.resource_collection, always_raise)
end
end
# Helper for #notifies
def notifies_before(action, resource_spec)
- run_context.notifies_before(Notification.new(resource_spec, action, self))
+ run_context.notifies_before(Notification.new(resource_spec, action, self, run_context.unified_mode))
end
# Helper for #notifies
def notifies_immediately(action, resource_spec)
- run_context.notifies_immediately(Notification.new(resource_spec, action, self))
+ run_context.notifies_immediately(Notification.new(resource_spec, action, self, run_context.unified_mode))
end
# Helper for #notifies
def notifies_delayed(action, resource_spec)
- run_context.notifies_delayed(Notification.new(resource_spec, action, self))
+ run_context.notifies_delayed(Notification.new(resource_spec, action, self, run_context.unified_mode))
end
class << self
@@ -1321,7 +1334,6 @@ class Chef
end
end
- #
# This API can be used for backcompat to do:
#
# chef_version_for_provides "< 14.0" if defined?(:chef_version_for_provides)
@@ -1343,7 +1355,6 @@ class Chef
@chef_version_for_provides = constraint
end
- #
# Mark this resource as providing particular DSL.
#
# Resources have an automatic DSL based on their resource_name, equivalent to
@@ -1472,7 +1483,6 @@ class Chef
@default_description
end
- #
# The cookbook in which this Resource was defined (if any).
#
# @return Chef::CookbookVersion The cookbook in which this Resource was defined.
@@ -1498,7 +1508,6 @@ class Chef
provider
end
- #
# Preface an exception message with generic Resource information.
#
# @param e [StandardError] An exception with `e.message`
@@ -1554,7 +1563,6 @@ class Chef
klass
end
- #
# Returns the class with the given resource_name.
#
# NOTE: Chef::Resource.resource_matching_short_name(:package) returns
diff --git a/lib/chef/resource/resource_notification.rb b/lib/chef/resource/resource_notification.rb
index 7e93fff433..d3b9856332 100644
--- a/lib/chef/resource/resource_notification.rb
+++ b/lib/chef/resource/resource_notification.rb
@@ -1,6 +1,6 @@
#
# Author:: Tyler Ball (<tball@chef.io>)
-# Copyright:: Copyright 2014-2016, Chef Software, Inc.
+# Copyright:: Copyright 2014-2019, Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
@@ -26,12 +26,13 @@ class Chef
# @attr [Resource] notifying_resource the Chef resource performing the notification
class Notification
- attr_accessor :resource, :action, :notifying_resource
+ attr_accessor :resource, :action, :notifying_resource, :unified_mode
- def initialize(resource, action, notifying_resource)
+ def initialize(resource, action, notifying_resource, unified_mode = false)
@resource = resource
@action = action&.to_sym
@notifying_resource = notifying_resource
+ @unified_mode = unified_mode
end
# Is the current notification a duplicate of another notification
@@ -52,11 +53,11 @@ class Chef
# @param [ResourceCollection] resource_collection
#
# @return [void]
- def resolve_resource_reference(resource_collection)
+ def resolve_resource_reference(resource_collection, always_raise = false)
return resource if resource.is_a?(Chef::Resource) && notifying_resource.is_a?(Chef::Resource)
unless resource.is_a?(Chef::Resource)
- fix_resource_reference(resource_collection)
+ fix_resource_reference(resource_collection, always_raise)
end
unless notifying_resource.is_a?(Chef::Resource)
@@ -69,7 +70,7 @@ class Chef
# @param [ResourceCollection] resource_collection
#
# @return [void]
- def fix_resource_reference(resource_collection)
+ def fix_resource_reference(resource_collection, always_raise = false)
matching_resource = resource_collection.find(resource)
if Array(matching_resource).size > 1
msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple resources, "\
@@ -79,13 +80,16 @@ class Chef
self.resource = matching_resource
rescue Chef::Exceptions::ResourceNotFound => e
- err = Chef::Exceptions::ResourceNotFound.new(<<~FAIL)
- resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
- but #{resource} cannot be found in the resource collection. #{notifying_resource} is defined in \
- #{notifying_resource.source_line}
- FAIL
- err.set_backtrace(e.backtrace)
- raise err
+ # in unified mode we allow lazy notifications to resources not yet declared
+ if !unified_mode || always_raise
+ err = Chef::Exceptions::ResourceNotFound.new(<<~FAIL)
+ resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
+ but #{resource} cannot be found in the resource collection. #{notifying_resource} is defined in \
+ #{notifying_resource.source_line}
+ FAIL
+ err.set_backtrace(e.backtrace)
+ raise err
+ end
rescue Chef::Exceptions::InvalidResourceSpecification => e
err = Chef::Exceptions::InvalidResourceSpecification.new(<<~F)
Resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \
diff --git a/lib/chef/resource_collection.rb b/lib/chef/resource_collection.rb
index 3b6ff4297e..da675fc4de 100644
--- a/lib/chef/resource_collection.rb
+++ b/lib/chef/resource_collection.rb
@@ -32,6 +32,8 @@ class Chef
include ResourceCollectionSerialization
extend Forwardable
+ attr_accessor :unified_mode
+
attr_reader :resource_set, :resource_list
attr_accessor :run_context
@@ -41,6 +43,7 @@ class Chef
@run_context = run_context
@resource_set = ResourceSet.new
@resource_list = ResourceList.new
+ @unified_mode = false
end
# @param resource [Chef::Resource] The resource to insert
@@ -57,6 +60,9 @@ class Chef
else
resource_set.insert_as(resource)
end
+ if unified_mode
+ run_context.runner.run_all_actions(resource)
+ end
end
def delete(key)
diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb
index eb211dc5a5..7a5924a3c7 100644
--- a/lib/chef/run_context.rb
+++ b/lib/chef/run_context.rb
@@ -26,12 +26,16 @@ require_relative "recipe"
require_relative "run_context/cookbook_compiler"
require_relative "event_dispatch/events_output_stream"
require_relative "train_transport"
+require_relative "exceptions"
require "forwardable" unless defined?(Forwardable)
+require "set" unless defined?(Set)
class Chef
# Value object that loads and tracks the context of a Chef run
class RunContext
+ extend Forwardable
+
#
# Global state
#
@@ -72,14 +76,12 @@ class Chef
#
attr_reader :definitions
- #
# Event dispatcher for this run.
#
# @return [Chef::EventDispatch::Dispatcher]
#
attr_accessor :events
- #
# Hash of factoids for a reboot request.
#
# @return [Hash]
@@ -90,7 +92,6 @@ class Chef
# Scoped state
#
- #
# The parent run context.
#
# @return [Chef::RunContext] The parent run context, or `nil` if this is the
@@ -98,7 +99,6 @@ class Chef
#
attr_reader :parent_run_context
- #
# The root run context.
#
# @return [Chef::RunContext] The root run context.
@@ -109,7 +109,6 @@ class Chef
rc
end
- #
# The collection of resources intended to be converged (and able to be
# notified).
#
@@ -119,8 +118,12 @@ class Chef
#
attr_reader :resource_collection
- attr_accessor :action_collection
+ # Handle to the global action_collection of executed actions for reporting / data_collector /etc
+ #
+ # @return [Chef::ActionCollection
#
+ attr_accessor :action_collection
+
# Pointer back to the Chef::Runner that created this
#
attr_accessor :runner
@@ -129,7 +132,6 @@ class Chef
# Notification handling
#
- #
# A Hash containing the before notifications triggered by resources
# during the converge phase of the chef run.
#
@@ -138,7 +140,6 @@ class Chef
#
attr_reader :before_notification_collection
- #
# A Hash containing the immediate notifications triggered by resources
# during the converge phase of the chef run.
#
@@ -147,7 +148,6 @@ class Chef
#
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.
#
@@ -156,7 +156,6 @@ class Chef
#
attr_reader :delayed_notification_collection
- #
# An Array containing the delayed (end of run) notifications triggered by
# resources during the converge phase of the chef run.
#
@@ -164,7 +163,16 @@ class Chef
#
attr_reader :delayed_actions
+ # A Set keyed by the string name, of all the resources that are updated. We do not
+ # track actions or individual resource objects, since this matches the behavior of
+ # the notification collections which are keyed by Strings.
+ #
+ attr_reader :updated_resources
+
+ # @return [Boolean] If the resource_collection is in unified_mode (no separate converge phase)
#
+ def_delegator :resource_collection, :unified_mode
+
# A child of the root Chef::Log logging object.
#
# @return Mixlib::Log::Child A child logger
@@ -190,7 +198,6 @@ class Chef
@loaded_attributes_hash = {}
@reboot_info = {}
@cookbook_compiler = nil
- @delayed_actions = []
initialize_child_state
end
@@ -221,6 +228,7 @@ class Chef
@immediate_notification_collection = Hash.new { |h, k| h[k] = [] }
@delayed_notification_collection = Hash.new { |h, k| h[k] = [] }
@delayed_actions = []
+ @updated_resources = Set.new
end
#
@@ -232,6 +240,10 @@ class Chef
# Note for the future, notification.notifying_resource may be an instance
# of Chef::Resource::UnresolvedSubscribes when calling {Resource#subscribes}
# with a string value.
+ if unified_mode && updated_resources.include?(notification.notifying_resource.declared_key)
+ raise Chef::Exceptions::UnifiedModeBeforeSubscriptionEarlierResource.new(notification)
+ end
+
before_notification_collection[notification.notifying_resource.declared_key] << notification
end
@@ -256,11 +268,13 @@ class Chef
# Note for the future, notification.notifying_resource may be an instance
# of Chef::Resource::UnresolvedSubscribes when calling {Resource#subscribes}
# with a string value.
+ if unified_mode && updated_resources.include?(notification.notifying_resource.declared_key)
+ add_delayed_action(notification)
+ end
delayed_notification_collection[notification.notifying_resource.declared_key] << notification
end
- #
- # Adds a delayed action to the +delayed_actions+.
+ # Adds a delayed action to the delayed_actions collection
#
def add_delayed_action(notification)
if delayed_actions.any? { |existing_notification| existing_notification.duplicates?(notification) }
@@ -271,32 +285,45 @@ class Chef
end
end
- #
# Get the list of before notifications sent by the given resource.
#
# @return [Array[Notification]]
#
def before_notifications(resource)
- before_notification_collection[resource.declared_key]
+ key = resource.is_a?(String) ? resource : resource.declared_key
+ before_notification_collection[key]
end
- #
# Get the list of immediate notifications sent by the given resource.
#
# @return [Array[Notification]]
#
def immediate_notifications(resource)
- immediate_notification_collection[resource.declared_key]
+ key = resource.is_a?(String) ? resource : resource.declared_key
+ immediate_notification_collection[key]
end
+ # Get the list of immeidate notifications pending to the given resource
+ #
+ # @return [Array[Notification]]
#
+ def reverse_immediate_notifications(resource)
+ immediate_notification_collection.map do |k, v|
+ v.select do |n|
+ (n.resource.is_a?(String) && n.resource == resource.declared_key) ||
+ n.resource == resource
+ end
+ end.flatten
+ end
+
# Get the list of delayed (end of run) notifications sent by the given
# resource.
#
# @return [Array[Notification]]
#
def delayed_notifications(resource)
- delayed_notification_collection[resource.declared_key]
+ key = resource.is_a?(String) ? resource : resource.declared_key
+ delayed_notification_collection[key]
end
#
@@ -666,9 +693,9 @@ class Chef
rest=
rest_clean
rest_clean=
- unreachable_cookbook?
transport
transport_connection
+ unreachable_cookbook?
}
def initialize(parent_run_context)
@@ -681,8 +708,10 @@ class Chef
end
CHILD_STATE = %w{
- create_child
add_delayed_action
+ before_notification_collection
+ before_notifications
+ create_child
delayed_actions
delayed_notification_collection
delayed_notification_collection=
@@ -690,21 +719,22 @@ class Chef
immediate_notification_collection
immediate_notification_collection=
immediate_notifications
- before_notification_collection
- before_notifications
include_recipe
initialize_child_state
load_recipe
load_recipe_file
notifies_before
- notifies_immediately
notifies_delayed
+ notifies_immediately
parent_run_context
- root_run_context
resource_collection
resource_collection=
+ reverse_immediate_notifications
+ root_run_context
runner
runner=
+ unified_mode
+ updated_resources
}.map(&:to_sym)
# Verify that we didn't miss any methods
diff --git a/lib/chef/runner.rb b/lib/chef/runner.rb
index a0ae61fe4c..1ece2f97c3 100644
--- a/lib/chef/runner.rb
+++ b/lib/chef/runner.rb
@@ -34,7 +34,7 @@ class Chef
def initialize(run_context)
@run_context = run_context
- run_context.runner = self
+ @run_context.runner = self
end
def delayed_actions
@@ -45,6 +45,10 @@ class Chef
@run_context.events
end
+ def updated_resources
+ @run_context.updated_resources
+ end
+
# Determine the appropriate provider for the given resource, then
# execute it.
def run_action(resource, action, notification_type = nil, notifying_resource = nil)
@@ -73,33 +77,67 @@ class Chef
# associated with the resource, but only if it was updated *this time*
# we ran an action on it.
if resource.updated_by_last_action?
+ updated_resources.add(resource.declared_key) # track updated resources for unified_mode
run_context.immediate_notifications(resource).each do |notification|
- Chef::Log.info("#{resource} sending #{notification.action} action to #{notification.resource} (immediate)")
- run_action(notification.resource, notification.action, :immediate, resource)
+ if notification.resource.is_a?(String) && run_context.unified_mode
+ Chef::Log.debug("immediate notification from #{resource} to #{notification.resource} is delayed until declaration due to unified_mode")
+ else
+ Chef::Log.info("#{resource} sending #{notification.action} action to #{notification.resource} (immediate)")
+ run_action(notification.resource, notification.action, :immediate, resource)
+ end
end
run_context.delayed_notifications(resource).each do |notification|
- # send the notification to the run_context of the receiving resource
- notification.resource.run_context.add_delayed_action(notification)
+ if notification.resource.is_a?(String)
+ # for string resources that have not be declared yet in unified mode we only support notifying the current run_context
+ run_context.add_delayed_action(notification)
+ else
+ # send the notification to the run_context of the receiving resource
+ notification.resource.run_context.add_delayed_action(notification)
+ end
+ end
+ end
+ end
+
+ # Runs all of the actions on a given resource. This fires notifications and marks
+ # the resource as having been executed by the runner.
+ #
+ # @param resource [Chef::Resource] the resource to run
+ #
+ def run_all_actions(resource)
+ Array(resource.action).each { |action| run_action(resource, action) }
+ if run_context.unified_mode
+ run_context.reverse_immediate_notifications(resource).each do |n|
+ if updated_resources.include?(n.notifying_resource.declared_key)
+ n.resolve_resource_reference(run_context.resource_collection)
+ Chef::Log.info("#{resource} sent #{n.action} action to #{n.resource} (immediate at declaration time)")
+ run_action(n.resource, n.action, :immediate, n.notifying_resource)
+ end
end
end
+ ensure
+ resource.executed_by_runner = true
end
- # Iterates over the +resource_collection+ in the +run_context+ calling
- # +run_action+ for each resource in turn.
+ # Iterates over the resource_collection in the run_context calling
+ # run_action for each resource in turn.
+ #
def converge
# Resolve all lazy/forward references in notifications
run_context.resource_collection.each(&:resolve_notification_references)
# Execute each resource.
run_context.resource_collection.execute_each_resource do |resource|
- begin
- Array(resource.action).each { |action| run_action(resource, action) }
- ensure
- resource.executed_by_runner = true
+ unless run_context.resource_collection.unified_mode
+ run_all_actions(resource)
end
end
+ if run_context.resource_collection.unified_mode
+ puts "HERE WE ARE!"
+ run_context.resource_collection.each { |r| r.resolve_notification_references(true) }
+ end
+
rescue Exception => e
Chef::Log.info "Running queued delayed notifications before re-raising exception"
run_delayed_notifications(e)
@@ -126,7 +164,8 @@ class Chef
def run_delayed_notification(notification)
Chef::Log.info( "#{notification.notifying_resource} sending #{notification.action}"\
" action to #{notification.resource} (delayed)")
- # Struct of resource/action to call
+ # notifications may have lazy strings in them to resolve
+ notification.resolve_resource_reference(run_context.resource_collection)
run_action(notification.resource, notification.action, :delayed)
true
rescue Exception => e
diff --git a/spec/integration/recipes/unified_mode_spec.rb b/spec/integration/recipes/unified_mode_spec.rb
new file mode 100644
index 0000000000..944319f7bf
--- /dev/null
+++ b/spec/integration/recipes/unified_mode_spec.rb
@@ -0,0 +1,876 @@
+require "support/shared/integration/integration_helper"
+require "chef/mixin/shell_out"
+
+describe "Unified Mode" do
+ include IntegrationSupport
+ include Chef::Mixin::ShellOut
+
+ let(:chef_dir) { File.expand_path("../../../../bin", __FILE__) }
+
+ let(:chef_client) { "bundle exec chef-client --minimal-ohai" }
+
+ when_the_repository "has a cookbook with a unified_mode resource with a delayed notification from the second block to the first block" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ action :nothing
+ end
+ var = "bar"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ notifies :run, "ruby_block[first block]", :delayed
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # the "second block" runs first after "bar" is set
+ expect(result.stdout).to include("second: bar")
+ # then the "first block" runs after "baz" in the delayed phase
+ expect(result.stdout).to include("first: baz")
+ # nothing else should fire
+ expect(result.stdout).not_to include("first: foo")
+ expect(result.stdout).not_to include("first: bar")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified_mode resource with a delayed notification from the first block to the second block" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ notifies :run, "ruby_block[second block]", :delayed
+ end
+ var = "bar"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ action :nothing
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default' -l debug", cwd: chef_dir)
+ # the first block should fire first
+ expect(result.stdout).to include("first: foo")
+ # the second block should fire in delayed phase
+ expect(result.stdout).to include("second: baz")
+ # nothing else should fire
+ expect(result.stdout).not_to include("first: bar")
+ expect(result.stdout).not_to include("first: baz")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: bar")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified_mode resource with an immediate notification from the second block to the first block" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ action :nothing
+ end
+ var = "bar"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ notifies :run, "ruby_block[first block]", :immediate
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # the second resource should fire first when it is parsed
+ expect(result.stdout).to include("second: bar")
+ # the first resource should then immediately fire
+ expect(result.stdout).to include("first: bar")
+ # no other resources should fire
+ expect(result.stdout).not_to include("second: baz")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("first: foo")
+ expect(result.stdout).not_to include("first: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified_mode resource with an immediate notification from the first block to the second block" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ notifies :run, "ruby_block[second block]", :immediate
+ end
+ var = "bar"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ action :nothing
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default' -l debug", cwd: chef_dir)
+ # both blocks should run when they're declared
+ expect(result.stdout).to include("first: foo")
+ expect(result.stdout).to include("second: bar")
+ # nothing else should run
+ expect(result.stdout).not_to include("first: bar")
+ expect(result.stdout).not_to include("first: baz")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified_mode resource with an immediate notification from the first block to a block that does not exist" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ notifies :run, "ruby_block[second block]", :immediate
+ end
+ var = "bar"
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should fail the run" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # both blocks should run when they're declared
+ expect(result.stdout).to include("first: foo")
+ # nothing else should run
+ expect(result.stdout).not_to include("second: bar")
+ expect(result.stdout).not_to include("first: bar")
+ expect(result.stdout).not_to include("first: baz")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: baz")
+ expect(result.stdout).to include("Chef::Exceptions::ResourceNotFound")
+ expect(result.error?).to be true
+ end
+ end
+
+ when_the_repository "has a cookbook with a normal resource with an delayed notification with global resource unified mode on" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ resource_name :unified_mode
+ provides :unified_mode
+
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ action :nothing
+ end
+ var = "bar"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ notifies :run, "ruby_block[second block]", :delayed
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ resource_unified_mode_default true
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # the "first block" resource runs before the assignment to baz in compile time
+ expect(result.stdout).to include("first: bar")
+ # we should not run the "first block" at compile time
+ expect(result.stdout).not_to include("first: baz")
+ # (and certainly should run it this early)
+ expect(result.stdout).not_to include("first: foo")
+ # the delayed notification should still fire and run after everything else
+ expect(result.stdout).to include("second: baz")
+ # the action :nothing should suppress any other running of the second block
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: bar")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a normal resource with an immediate notification with global resource unified mode on" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ action :nothing
+ end
+ var = "bar"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ notifies :run, "ruby_block[second block]", :immediate
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ resource_unified_mode_default true
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # the "first block" resource runs before the assignment to baz in compile time
+ expect(result.stdout).to include("first: bar")
+ # we should not run the "first block" at compile time
+ expect(result.stdout).not_to include("first: baz")
+ # (and certainly should run it this early)
+ expect(result.stdout).not_to include("first: foo")
+ # the immediate notifiation fires immediately
+ expect(result.stdout).to include("second: bar")
+ # the action :nothing should suppress any other running of the second block
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified resource with an immediate subscribes from the second resource to the first" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ end
+ var = "bar"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ subscribes :run, "ruby_block[first block]", :immediate
+ action :nothing
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # the first resource fires
+ expect(result.stdout).to include("first: foo")
+ # the second resource fires when it is parsed
+ expect(result.stdout).to include("second: bar")
+ # no other actions should run
+ expect(result.stdout).not_to include("first: bar")
+ expect(result.stdout).not_to include("first: baz")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified resource with an immediate subscribes from the first resource to the second" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ subscribes :run, "ruby_block[second block]", :immediate
+ action :nothing
+ end
+ var = "bar"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # the second resource fires first after bar is set
+ expect(result.stdout).to include("second: bar")
+ # the first resource then has its immediate subscribes fire at that location
+ expect(result.stdout).to include("first: bar")
+ # no other actions should run
+ expect(result.stdout).not_to include("first: baz")
+ expect(result.stdout).not_to include("first: foo")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified resource with an delayed subscribes from the second resource to the first" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ end
+ var = "bar"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ subscribes :run, "ruby_block[first block]", :delayed
+ action :nothing
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # the first resource fires as it is parsed
+ expect(result.stdout).to include("first: foo")
+ # the second resource then fires in the delayed notifications phase
+ expect(result.stdout).to include("second: baz")
+ # no other actions should run
+ expect(result.stdout).not_to include("first: bar")
+ expect(result.stdout).not_to include("first: baz")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: bar")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified resource with an delayed subscribes from the first resource to the second" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ subscribes :run, "ruby_block[second block]", :delayed
+ action :nothing
+ end
+ var = "bar"
+ ruby_block "second block" do
+ block do
+ puts "\nsecond: \#\{var\}"
+ end
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # the second resource fires first after bar is set
+ expect(result.stdout).to include("second: bar")
+ # the first resource then fires in the delayed notifications phase
+ expect(result.stdout).to include("first: baz")
+ # no other actions should run
+ expect(result.stdout).not_to include("first: foo")
+ expect(result.stdout).not_to include("first: bar")
+ expect(result.stdout).not_to include("second: foo")
+ expect(result.stdout).not_to include("second: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified resource with a correct before notification" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "notified block" do
+ block do
+ puts "\nnotified: \#\{var\}"
+ end
+ action :nothing
+ end
+ var = "bar"
+ whyrun_safe_ruby_block "notifying block" do
+ block do
+ puts "\nnotifying: \#\{var\}"
+ end
+ notifies :run, "ruby_block[notified block]", :before
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ expect(result.stdout.scan(/notifying: bar/).length).to eql(2)
+ expect(result.stdout).to include("Would execute the whyrun_safe_ruby_block notifying block")
+ expect(result.stdout).to include("notified: bar")
+ # no other actions should run
+ expect(result.stdout).not_to include("notified: foo")
+ expect(result.stdout).not_to include("notified: baz")
+ expect(result.stdout).not_to include("notifying: foo")
+ expect(result.stdout).not_to include("notifying: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified resource with a correct before subscribes" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ ruby_block "notified block" do
+ block do
+ puts "\nnotified: \#\{var\}"
+ end
+ subscribes :run, "whyrun_safe_ruby_block[notifying block]", :before
+ action :nothing
+ end
+ var = "bar"
+ whyrun_safe_ruby_block "notifying block" do
+ block do
+ puts "\nnotifying: \#\{var\}"
+ end
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should complete with success" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ expect(result.stdout.scan(/notifying: bar/).length).to eql(2)
+ expect(result.stdout).to include("Would execute the whyrun_safe_ruby_block notifying block")
+ expect(result.stdout).to include("notified: bar")
+ # no other actions should run
+ expect(result.stdout).not_to include("notified: foo")
+ expect(result.stdout).not_to include("notified: baz")
+ expect(result.stdout).not_to include("notifying: foo")
+ expect(result.stdout).not_to include("notifying: baz")
+ result.error!
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified resource with a broken/reversed before notification" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ whyrun_safe_ruby_block "notifying block" do
+ block do
+ puts "\nnotifying: \#\{var\}"
+ end
+ notifies :run, "ruby_block[notified block]", :before
+ end
+ var = "bar"
+ ruby_block "notified block" do
+ block do
+ puts "\nnotified: \#\{var\}"
+ end
+ action :nothing
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should fail the run" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default' -l debug", cwd: chef_dir)
+ # this doesn't work and we can't tell the difference between it and if we were trying to do a correct :before notification but typo'd the name
+ # so Chef::Exceptions::ResourceNotFound is the best we can do
+ expect(result.stdout).to include("Chef::Exceptions::ResourceNotFound")
+ expect(result.error?).to be true
+ end
+ end
+
+ when_the_repository "has a cookbook with a unified resource with a broken/reversed before subscribes" do
+ before do
+ directory "cookbooks/x" do
+
+ file "resources/unified_mode.rb", <<-EOM
+ unified_mode true
+ resource_name :unified_mode
+ provides :unified_mode
+ action :doit do
+ klass = new_resource.class
+ var = "foo"
+ whyrun_safe_ruby_block "notifying block" do
+ block do
+ puts "\nnotifying: \#\{var\}"
+ end
+ end
+ var = "bar"
+ ruby_block "notified block" do
+ block do
+ puts "\nnotified: \#\{var\}"
+ end
+ subscribes :run, "whyrun_safe_ruby_block[notifying block]", :before
+ action :nothing
+ end
+ var = "baz"
+ end
+ EOM
+
+ file "recipes/default.rb", <<-EOM
+ unified_mode "whatever"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "should fail the run" do
+ file "config/client.rb", <<~EOM
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # this fires first normally before the error
+ expect(result.stdout).to include("notifying: foo")
+ # everything else does not run
+ expect(result.stdout).not_to include("notified: foo")
+ expect(result.stdout).not_to include("notified: bar")
+ expect(result.stdout).not_to include("notified: baz")
+ expect(result.stdout).not_to include("notifying: bar")
+ expect(result.stdout).not_to include("notifying: baz")
+ expect(result.stdout).to include("Chef::Exceptions::UnifiedModeBeforeSubscriptionEarlierResource")
+ expect(result.error?).to be true
+ end
+ end
+
+ when_the_repository "has global resource unified mode on" do
+ before do
+ directory "cookbooks/x" do
+
+ file "recipes/default.rb", <<-EOM
+ var = "foo"
+ ruby_block "first block" do
+ block do
+ puts "\nfirst: \#\{var\}"
+ end
+ end
+ var = "bar"
+ EOM
+
+ end # directory 'cookbooks/x'
+ end
+
+ it "recipes should still have a compile/converge mode" do
+ file "config/client.rb", <<~EOM
+ resource_unified_mode_default true
+ local_mode true
+ cookbook_path "#{path_to("cookbooks")}"
+ log_level :warn
+ EOM
+
+ result = shell_out("#{chef_client} -c \"#{path_to("config/client.rb")}\" --no-color -F doc -o 'x::default'", cwd: chef_dir)
+ # in recipe mode we should still run normally with a compile/converge mode
+ expect(result.stdout).to include("first: bar")
+ expect(result.stdout).not_to include("first: foo")
+ result.error!
+ end
+ end
+end