Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[CHEF-2421(3)] execute other resources BEFORE main resource #1145

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/chef/exceptions.rb
Expand Up @@ -80,6 +80,7 @@ class GroupIDNotFound < ArgumentError; end
class InvalidResourceReference < RuntimeError; end
class ResourceNotFound < RuntimeError; end
class InvalidResourceSpecification < ArgumentError; end
class BeforeNotificationsWithoutWhyrunOrConditions < RuntimeError; end
class SolrConnectionError < RuntimeError; end
class IllegalChecksumRevert < RuntimeError; end
class CookbookVersionNameMismatch < ArgumentError; end
Expand Down
4 changes: 4 additions & 0 deletions lib/chef/mixin/why_run.rb
Expand Up @@ -48,6 +48,10 @@ def events
# block/proc that implements the action.
def add_action(descriptions, &block)
@actions << [descriptions, block]

# run before notifications when whyrun is supported
@resource.run_before_notifications

if !Chef::Config[:why_run]
block.call
end
Expand Down
48 changes: 46 additions & 2 deletions lib/chef/provider.rb
Expand Up @@ -92,16 +92,31 @@ def run_action(action=nil)
# TODO: it would be preferable to get the action to be executed in the
# constructor...

# run before notifications when whyrun is not supported
run_before_notifications

# user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode
if !whyrun_mode? || whyrun_supported?
load_current_resource
events.resource_current_state_loaded(@new_resource, @action, @current_resource)
elsif whyrun_mode? && !whyrun_supported?
else
events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource)
end

define_resource_requirements
process_resource_requirements
# sometimes, before notifications can avoid requirements exceptions,
# so we need to catch them here, before this kind of notifications are run
begin
process_resource_requirements
rescue StandardError => e
if whyrun_supported? && @new_resource.has_before_notifications?
# now check if the exception occurs again after running before notifications
@new_resource.run_before_notifications
process_resource_requirements
else
raise e
end
end

# user-defined providers including LWRPs may
# not include whyrun support - if they don't support it
Expand All @@ -123,6 +138,35 @@ def run_action(action=nil)
cleanup_after_converge
end

def has_before_notifications?
!run_context.before_notifications(@new_resource).empty?
end

def has_depends_notifications?
!run_context.depends_notifications(@new_resource).empty?
end

def run_before_notifications
# if whyrun is not supported run the before notifications here,
# otherside they will be run inside Chef::Mixin::WhyRun::ConvergeActions

if !whyrun_supported?

# :depends notifications seems not affected by this problem,
# but :before notifications cannot run when there is no whyrun support and no conditions defined.
if has_before_notifications?
if @new_resource.only_if.empty? and @new_resource.not_if.empty?
msg = "The #{@new_resource} has :before notifications without whyrun support or only_if/not_if conditions defined."\
"The purpose of the :before notifications is not to run always."
raise Chef::Exceptions::BeforeNotificationsWithoutWhyrunOrConditions, msg
else
Chef::Log.warn("The #{@new_resource} resource has :before notifications and no whyrun support.")
end
end
@new_resource.run_before_notifications
end
end

def process_resource_requirements
requirements.run(:all_actions) unless @action == :nothing
requirements.run(@action)
Expand Down
58 changes: 51 additions & 7 deletions lib/chef/resource.rb
Expand Up @@ -118,8 +118,8 @@ def fix_notifier_reference(resource_collection)

end

FORBIDDEN_IVARS = [:@run_context, :@node, :@not_if, :@only_if, :@enclosing_provider]
HIDDEN_IVARS = [:@allowed_actions, :@resource_name, :@source_line, :@run_context, :@name, :@node, :@not_if, :@only_if, :@elapsed_time, :@enclosing_provider]
FORBIDDEN_IVARS = [:@run_context, :@node, :@not_if, :@only_if, :@should_skip, :@enclosing_provider]
HIDDEN_IVARS = FORBIDDEN_IVARS + [:@allowed_actions, :@resource_name, :@source_line, :@name, :@elapsed_time]

include Chef::DSL::DataQuery
include Chef::Mixin::ParamsValidate
Expand Down Expand Up @@ -233,7 +233,6 @@ def initialize(name, run_context=nil)
@name = name
@run_context = run_context
@noop = nil
@before = nil
@params = Hash.new
@provider = nil
@allowed_actions = [ :nothing ]
Expand All @@ -248,6 +247,7 @@ def initialize(name, run_context=nil)
@only_if = []
@source_line = nil
@elapsed_time = 0
@should_skip = nil

@node = run_context ? deprecated_ivar(run_context.node, :node, :warn) : nil
end
Expand Down Expand Up @@ -413,6 +413,10 @@ def notifies(action, resource_spec, timing=:delayed)
notifies_delayed(action, resource)
when 'immediate', 'immediately'
notifies_immediately(action, resource)
when 'before'
notifies_before(action, resource)
when 'depends', 'before_once'
notifies_depends(action, resource)
else
raise ArgumentError, "invalid timing: #{timing} for notifies(#{action}, #{resources.inspect}, #{timing}) resource #{self} "\
"Valid timings are: :delayed, :immediate, :immediately"
Expand All @@ -422,10 +426,16 @@ def notifies(action, resource_spec, timing=:delayed)
true
end

# Iterates over all immediate and delayed notifications, calling
# resolve_resource_reference on each in turn, causing them to
# resolve lazy/forward references.
def depends(action, resource_spec)
notifies(action, resource_spec, :depends)
end

# Iterates over all immediate, delayed, before and depends notifications,
# calling resolve_resource_reference on each in turn, causing them
# to resolve lazy/forward references.
def resolve_notification_references
run_context.depends_notifications(self).each { |n| n.resolve_resource_reference(run_context.resource_collection) }
run_context.before_notifications(self).each { |n| n.resolve_resource_reference(run_context.resource_collection) }
run_context.immediate_notifications(self).each { |n| n.resolve_resource_reference(run_context.resource_collection) }
run_context.delayed_notifications(self).each {|n| n.resolve_resource_reference(run_context.resource_collection) }
end
Expand All @@ -438,6 +448,14 @@ def notifies_delayed(action, resource_spec)
run_context.notifies_delayed(Notification.new(resource_spec, action, self))
end

def notifies_before(action, resource_spec)
run_context.notifies_before(Notification.new(resource_spec, action, self))
end

def notifies_depends(action, resource_spec)
run_context.notifies_depends(Notification.new(resource_spec, action, self))
end

def immediate_notifications
run_context.immediate_notifications(self)
end
Expand All @@ -446,6 +464,14 @@ def delayed_notifications
run_context.delayed_notifications(self)
end

def before_notifications
run_context.before_notifications(self)
end

def depends_notifications
run_context.depends_notifications(self)
end

def resources(*args)
run_context.resource_collection.find(*args)
end
Expand Down Expand Up @@ -601,6 +627,21 @@ def events
run_context.events
end

def run_before(&block)
@run_before = block
end

def has_before_notifications?
!@run_before.nil?
end

def run_before_notifications
if has_before_notifications?
@run_before.call
@run_before = nil
end
end

def run_action(action, notification_type=nil, notifying_resource=nil)
# reset state in case of multiple actions on the same resource.
@elapsed_time = 0
Expand Down Expand Up @@ -640,6 +681,7 @@ def run_action(action, notification_type=nil, notifying_resource=nil)
ensure
@elapsed_time = Time.now - start_time
events.resource_completed(self)
@should_skip = nil
end
end

Expand Down Expand Up @@ -676,10 +718,11 @@ def customize_exception(e)
#
# Also skips conditional checking when the action is :nothing
def should_skip?(action)
return @should_skip unless @should_skip.nil?
conditional_action = ConditionalActionNotNothing.new(action)

conditionals = [ conditional_action ] + only_if + not_if
conditionals.find do |conditional|
found = conditionals.find do |conditional|
if conditional.continue?
false
else
Expand All @@ -688,6 +731,7 @@ def should_skip?(action)
true
end
end
@should_skip = ! found.nil?
end

def updated_by_last_action(true_or_false)
Expand Down
76 changes: 58 additions & 18 deletions lib/chef/run_context.rb
Expand Up @@ -57,6 +57,17 @@ class RunContext
# resources during the converge phase of the chef run.
attr_accessor :delayed_notification_collection

# A Hash containing the before notifications triggered by resources
# during the converge phase of the chef run.
attr_accessor :before_notification_collection

# A Hash containing the depends notifications triggered by resources
# during the converge phase of the chef run.
attr_accessor :depends_notification_collection

# A Hash containing the list of the already executed depends resources
attr_accessor :depends_executed

# Event dispatcher for this run.
attr_reader :events

Expand All @@ -71,6 +82,9 @@ def initialize(node, cookbook_collection, events)
@resource_collection = Chef::ResourceCollection.new
@immediate_notification_collection = Hash.new {|h,k| h[k] = []}
@delayed_notification_collection = Hash.new {|h,k| h[k] = []}
@before_notification_collection = Hash.new {|h,k| h[k] = []}
@depends_notification_collection = Hash.new {|h,k| h[k] = []}
@depends_executed = Hash.new {|h,k| h[k] = {}}
@definitions = Hash.new
@loaded_recipes = {}
@loaded_attributes = {}
Expand All @@ -91,38 +105,56 @@ def load(run_list_expansion)
# Chef::Resource::Notification or duck type.
def notifies_immediately(notification)
nr = notification.notifying_resource
if nr.instance_of?(Chef::Resource)
@immediate_notification_collection[nr.name] << notification
else
@immediate_notification_collection[nr.to_s] << notification
end
@immediate_notification_collection[resource_name_key(nr)] << notification
end

# Adds a delayed notification to the +delayed_notification_collection+. The
# notification should be a Chef::Resource::Notification or duck type.
def notifies_delayed(notification)
nr = notification.notifying_resource
if nr.instance_of?(Chef::Resource)
@delayed_notification_collection[nr.name] << notification
@delayed_notification_collection[resource_name_key(nr)] << notification
end

def notifies_before(notification)
nr = notification.notifying_resource
@before_notification_collection[resource_name_key(nr)] << notification
end

def notifies_depends(notification)
nr = notification.notifying_resource
@depends_notification_collection[resource_name_key(nr)] << notification

name = resource_name_key(notification.resource)
@depends_executed[name][notification.action] ||= false
end

def depends_executed(resource, action, value=nil)
if value.nil?
@depends_executed[resource_name_key(resource)][action] if is_depends_resource(resource, action)
else
@delayed_notification_collection[nr.to_s] << notification
@depends_executed[resource_name_key(resource)][action] = value
end
end

def is_depends_resource(resource, action)
name = resource_name_key(resource)
@depends_executed.has_key?(name) and @depends_executed[name].has_key?(action)
end

def immediate_notifications(resource)
if resource.instance_of?(Chef::Resource)
return @immediate_notification_collection[resource.name]
else
return @immediate_notification_collection[resource.to_s]
end
return @immediate_notification_collection[resource_name_key(resource)]
end

def delayed_notifications(resource)
if resource.instance_of?(Chef::Resource)
return @delayed_notification_collection[resource.name]
else
return @delayed_notification_collection[resource.to_s]
end
return @delayed_notification_collection[resource_name_key(resource)]
end

def before_notifications(resource)
return @before_notification_collection[resource_name_key(resource)]
end

def depends_notifications(resource)
return @depends_notification_collection[resource_name_key(resource)]
end

# Evaluates the recipes +recipe_names+. Used by DSL::IncludeRecipe
Expand Down Expand Up @@ -224,5 +256,13 @@ def loaded_recipe(cookbook, recipe)
@loaded_recipes["#{cookbook}::#{recipe}"] = true
end

def resource_name_key(resource)
if resource.instance_of?(Chef::Resource)
resource.name
else
resource.to_s
end
end

end
end
34 changes: 34 additions & 0 deletions lib/chef/runner.rb
Expand Up @@ -46,17 +46,51 @@ def events
# Determine the appropriate provider for the given resource, then
# execute it.
def run_action(resource, action, notification_type=nil, notifying_resource=nil)

# do not run if executed before in a depends notification
if run_context.depends_executed(resource, action) == true
Chef::Log.info("#{resource} action #{action} has been run before, it will not be executed again")
return
end

# Execute any depends and before notifications if the conditions are met
if not resource.should_skip?(action)

resource.run_before do

# notifies :depends
run_context.depends_notifications(resource).each do |notification|
Chef::Log.info("#{resource} sending #{notification.action} action to #{notification.resource} (depends)")
run_action(notification.resource, notification.action, :depends, resource)
end

# notifies :before
run_context.before_notifications(resource).each do |notification|
Chef::Log.info("#{resource} sending #{notification.action} action to #{notification.resource} (before)")
run_action(notification.resource, notification.action, :before, resource)
end
end
end

resource.run_action(action, notification_type, notifying_resource)

# sets execution as true if it is a depends notification
if @run_context.is_depends_resource(resource, action)
run_context.depends_executed(resource, action, true)
end

# Execute any immediate and queue up any delayed notifications
# associated with the resource, but only if it was updated *this time*
# we ran an action on it.
if resource.updated_by_last_action?

# notifies :immediate
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)
end

# notifies :delayed
run_context.delayed_notifications(resource).each do |notification|
if delayed_actions.any? { |existing_notification| existing_notification.duplicates?(notification) }
Chef::Log.info( "#{resource} not queuing delayed action #{notification.action} on #{notification.resource}"\
Expand Down