summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@chef.io>2019-08-15 17:10:06 -0700
committerGitHub <noreply@github.com>2019-08-15 17:10:06 -0700
commitf24218b63b44353cdaa2b1bbe1a6eddaf60bbb01 (patch)
treebb2eadb1f0878f58e6df8bfe1a278ab8311bf283
parent1ac98e9fd013496e131ec98201e05f675e444374 (diff)
parentcd32e8e600bd2ee48de9d9032bac5726635033df (diff)
downloadchef-f24218b63b44353cdaa2b1bbe1a6eddaf60bbb01.tar.gz
Add unified_mode for resources (#8668)
Add unified_mode for resources
-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.rb62
-rw-r--r--spec/integration/recipes/unified_mode_spec.rb876
9 files changed, 1060 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..8c68554af5 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,66 @@ 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
+ 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 +163,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