File Manager
Viewing File: action_collection.rb
#
# Copyright:: Copyright (c) Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
require_relative "event_dispatch/base"
class Chef
class ActionCollection < EventDispatch::Base
include Enumerable
extend Forwardable
class ActionRecord
# @return [Chef::Resource] The declared resource state.
#
attr_accessor :new_resource
# @return [Chef::Resource] The current_resource object (before-state). This can be nil
# for non-why-run-safe resources in why-run mode, or if load_current_resource itself
# threw an exception (which should be considered a bug in that load_current_resource
# implementation, but must be handled), or for unprocessed resources.
attr_accessor :current_resource
# @return [Chef::Resource] the after_resource object (after-state). This can be nil for
# non custom-resources or resources that do not implement load_after_resource.
attr_accessor :after_resource
# @return [Symbol] # The action that was run (or scheduled to run in the case of "unprocessed" resources).
attr_accessor :action
# @return [Exception] The exception that was thrown
attr_accessor :exception
# @return [Numeric] The elapsed time in seconds with machine precision
attr_accessor :elapsed_time
# @return [Chef::Resource::Conditional] The conditional that caused the resource to be skipped
attr_accessor :conditional
# The status of the resource:
# - updated: ran and converged
# - up_to_date: skipped due to idempotency
# - skipped: skipped due to a conditional
# - failed: failed with an exception
# - unprocessed: resources that were not touched by a run that failed
#
# @return [Symbol] status
#
attr_accessor :status
# The "nesting" level. Outer resources in recipe context are 0 here, while for every
# sub-resource_collection inside of a custom resource this number is incremented by 1.
# Resources that are fired via build-resource or manually creating and firing
#
# @return [Integer]
#
attr_accessor :nesting_level
def initialize(new_resource, action, nesting_level)
@new_resource = new_resource
@action = action
@nesting_level = nesting_level
end
# @return [Boolean] true if there was no exception
def success?
!exception
end
end
attr_reader :action_records
attr_reader :pending_updates
attr_reader :run_context
attr_reader :consumers
attr_reader :events
def initialize(events, run_context = nil, action_records = [])
@action_records = action_records
@pending_updates = []
@consumers = []
@events = events
@run_context = run_context
end
def_delegators :@action_records, :each, :last
# Allows getting at the action_records collection filtered by nesting level and status.
#
# TODO: filtering by resource type+name
#
# @return [Chef::ActionCollection]
#
def filtered_collection(max_nesting: nil, up_to_date: true, skipped: true, updated: true, failed: true, unprocessed: true)
subrecords = action_records.select do |rec|
( max_nesting.nil? || rec.nesting_level <= max_nesting ) &&
( rec.status == :up_to_date && up_to_date ||
rec.status == :skipped && skipped ||
rec.status == :updated && updated ||
rec.status == :failed && failed ||
rec.status == :unprocessed && unprocessed )
end
self.class.new(events, run_context, subrecords)
end
# This hook gives us the run_context immediately after it is created so that we can wire up this object to it.
#
# This also causes the action_collection_registration event to fire, all consumers that have not yet registered with the
# action_collection must register via this callback. This is the latest point before resources actually start to get
# evaluated.
#
# (see EventDispatch::Base#)
#
def cookbook_compilation_start(run_context)
run_context.action_collection = self
# fire the action_colleciton_registration hook after cookbook_compilation_start -- last chance for consumers to register
run_context.events.enqueue(:action_collection_registration, self)
@run_context = run_context
end
# Consumers must call register -- either directly or through the action_collection_registration hook. If
# nobody has registered any interest, then no action tracking will be done.
#
# @params object [Object] callers should call with `self`
#
def register(object)
consumers << object
end
# End of an unsuccessful converge used to fire off detect_unprocessed_resources.
#
# (see EventDispatch::Base#)
#
def converge_failed(exception)
return if consumers.empty?
detect_unprocessed_resources
end
# Hook to start processing a resource. May be called within processing of an outer resource
# so the pending_updates array forms a stack that sub-resources are popped onto and off of.
# This is always called.
#
# (see EventDispatch::Base#)
#
def resource_action_start(new_resource, action, notification_type = nil, notifier = nil)
return if consumers.empty?
pending_updates << ActionRecord.new(new_resource, action, pending_updates.length)
end
# Hook called after a current resource is loaded. If load_current_resource fails, this hook will
# not be called and current_resource will be nil, and the resource_failed hook will be called.
#
# (see EventDispatch::Base#)
#
def resource_current_state_loaded(new_resource, action, current_resource)
return if consumers.empty?
current_record.current_resource = current_resource
end
# Hook called after an after resource is loaded. If load_after_resource fails, this hook will
# not be called and after_resource will be nil, and the resource_failed hook will be called.
#
# (see EventDispatch::Base#)
#
def resource_after_state_loaded(new_resource, action, after_resource)
return if consumers.empty?
current_record.after_resource = after_resource
end
# Hook called after an action is determined to be up to date.
#
# (see EventDispatch::Base#)
#
def resource_up_to_date(new_resource, action)
return if consumers.empty?
current_record.status = :up_to_date
end
# Hook called after an action is determined to be skipped due to a conditional.
#
# (see EventDispatch::Base#)
#
def resource_skipped(resource, action, conditional)
return if consumers.empty?
current_record.status = :skipped
current_record.conditional = conditional
end
# Hook called after an action modifies the system and is marked updated.
#
# (see EventDispatch::Base#)
#
def resource_updated(new_resource, action)
return if consumers.empty?
current_record.status = :updated
end
# Hook called after an action fails.
#
# (see EventDispatch::Base#)
#
def resource_failed(new_resource, action, exception)
return if consumers.empty?
current_record.status = :failed
current_record.exception = exception
end
# Hook called after an action is completed. This is always called, even if the action fails.
#
# (see EventDispatch::Base#)
#
def resource_completed(new_resource)
return if consumers.empty?
current_record.elapsed_time = new_resource.elapsed_time
# Verify if the resource has sensitive data and create a new blank resource with only
# the name so we can report it back without sensitive data
# XXX?: what about sensitive data in the current_resource?
# FIXME: this needs to be display-logic
if current_record.new_resource.sensitive
klass = current_record.new_resource.class
resource_name = current_record.new_resource.name
current_record.new_resource = klass.new(resource_name)
end
action_records << pending_updates.pop
end
private
# @return [Chef::ActionCollection::ActionRecord] the current record we are working on at the top of the stack
def current_record
pending_updates[-1]
end
# If the chef-client run fails in the middle, we are left with a half-completed resource_collection, this
# method is responsible for adding all of the resources which have not yet been touched. They are marked
# as being "unprocessed".
#
def detect_unprocessed_resources
run_context.resource_collection.all_resources.select { |resource| resource.executed_by_runner == false }.each do |resource|
Array(resource.action).each do |action|
record = ActionRecord.new(resource, action, 0)
record.status = :unprocessed
action_records << record
end
end
end
end
end