summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJohn Keiser <john@johnkeiser.com>2015-05-15 23:05:55 -0700
committerJohn Keiser <john@johnkeiser.com>2015-06-23 14:42:27 -0700
commit87a8b49efbccb6934ff2bacb8f8df53d1caf5e46 (patch)
tree98857dc3e898a417def568db35296b3e8fbf9243
parentd505a314fee15e5b5de411e2c27a7cdbd2a0e48b (diff)
downloadchef-87a8b49efbccb6934ff2bacb8f8df53d1caf5e46.tar.gz
Give run contexts children instead of using external duping
-rw-r--r--lib/chef/provider.rb14
-rw-r--r--lib/chef/provider/deploy.rb4
-rw-r--r--lib/chef/provider/lwrp_base.rb39
-rw-r--r--lib/chef/run_context.rb452
4 files changed, 387 insertions, 122 deletions
diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb
index 131b72cd23..12f11d26af 100644
--- a/lib/chef/provider.rb
+++ b/lib/chef/provider.rb
@@ -200,12 +200,14 @@ class Chef
# manipulating notifies.
converge_by ("evaluate block and run any associated actions") do
- saved_run_context = @run_context
- @run_context = @run_context.dup
- @run_context.resource_collection = Chef::ResourceCollection.new
- instance_eval(&block)
- Chef::Runner.new(@run_context).converge
- @run_context = saved_run_context
+ saved_run_context = run_context
+ begin
+ @run_context = run_context.create_child
+ instance_eval(&block)
+ Chef::Runner.new(run_context).converge
+ ensure
+ @run_context = saved_run_context
+ end
end
end
diff --git a/lib/chef/provider/deploy.rb b/lib/chef/provider/deploy.rb
index 19e7c01ab1..387441699e 100644
--- a/lib/chef/provider/deploy.rb
+++ b/lib/chef/provider/deploy.rb
@@ -375,9 +375,7 @@ class Chef
def gem_resource_collection_runner
gems_collection = Chef::ResourceCollection.new
gem_packages.each { |rbgem| gems_collection.insert(rbgem) }
- gems_run_context = run_context.dup
- gems_run_context.resource_collection = gems_collection
- Chef::Runner.new(gems_run_context)
+ Chef::Runner.new(run_context.create_child)
end
def gem_packages
diff --git a/lib/chef/provider/lwrp_base.rb b/lib/chef/provider/lwrp_base.rb
index b5efbb284d..94c79918b7 100644
--- a/lib/chef/provider/lwrp_base.rb
+++ b/lib/chef/provider/lwrp_base.rb
@@ -39,35 +39,30 @@ class Chef
# Class methods for InlineResources. Overrides the `action` DSL method
# with one that enables inline resource convergence.
module ClassMethods
- # Defines an action method on the provider, using
- # recipe_eval_with_update_check to execute the given block.
+ # Defines an action method on the provider, running the block to
+ # compile the resources, converging them, and then checking if any
+ # were updated (and updating new-resource if so)
def action(name, &block)
define_method("action_#{name}") do
- recipe_eval_with_update_check(&block)
+ begin
+ return_value = instance_eval(&block)
+ Chef::Runner.new(run_context).converge
+ return_value
+ ensure
+ if run_context.resource_collection.any? {|r| r.updated? }
+ new_resource.updated_by_last_action(true)
+ end
+ end
end
end
end
- # Executes the given block in a temporary run_context with its own
- # resource collection. After the block is executed, any resources
- # declared inside are converged, and if any are updated, the
- # new_resource will be marked updated.
- def recipe_eval_with_update_check(&block)
- saved_run_context = @run_context
- temp_run_context = @run_context.dup
- @run_context = temp_run_context
- @run_context.resource_collection = Chef::ResourceCollection.new
-
- return_value = instance_eval(&block)
- Chef::Runner.new(@run_context).converge
- return_value
- ensure
- @run_context = saved_run_context
- if temp_run_context.resource_collection.any? {|r| r.updated? }
- new_resource.updated_by_last_action(true)
- end
+ # Our run context is a child of the main run context; that gives us a
+ # whole new resource collection and notification set.
+ def initialize(*args, &block)
+ super
+ @run_context = @run_context.create_child
end
-
end
include Chef::DSL::Recipe
diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb
index 44b05f0cc0..ed5d7aaa79 100644
--- a/lib/chef/run_context.rb
+++ b/lib/chef/run_context.rb
@@ -25,118 +25,197 @@ require 'chef/log'
require 'chef/recipe'
require 'chef/run_context/cookbook_compiler'
require 'chef/event_dispatch/events_output_stream'
+require 'forwardable'
class Chef
# == Chef::RunContext
# Value object that loads and tracks the context of a Chef run
class RunContext
+ #
+ # Global state
+ #
- # Chef::Node object for this run
+ #
+ # The node for this run
+ #
+ # @return [Chef::Node]
+ #
attr_reader :node
- # Chef::CookbookCollection for this run
+ #
+ # The set of cookbooks involved in this run
+ #
+ # @return [Chef::CookbookCollection]
+ #
attr_reader :cookbook_collection
+ #
# Resource Definitions for this run. Populated when the files in
# +definitions/+ are evaluated (this is triggered by #load).
+ #
+ # @return [Array[Chef::ResourceDefinition]]
+ #
attr_reader :definitions
- ###
- # These need to be settable so deploy can run a resource_collection
- # independent of any cookbooks via +recipe_eval+
+ #
+ # Event dispatcher for this run.
+ #
+ # @return [Chef::EventDispatch::Dispatcher]
+ #
+ attr_reader :events
- # The Chef::ResourceCollection for this run. Populated by evaluating
- # recipes, which is triggered by #load. (See also: CookbookCompiler)
- attr_accessor :resource_collection
+ #
+ # Hash of factoids for a reboot request.
+ #
+ # @return [Hash]
+ #
+ attr_reader :reboot_info
+ #
+ # Scoped state
+ #
+
+ #
+ # The parent run context.
+ #
+ # @return [Chef::RunContext] The parent run context, or `nil` if this is the
+ # root context.
+ #
+ attr_reader :parent_run_context
+
+ #
+ # The collection of resources intended to be converged (and able to be
+ # notified).
+ #
+ # @return [Chef::ResourceCollection]
+ #
+ # @see CookbookCompiler
+ #
+ attr_reader :resource_collection
+
+ #
# The list of control groups to execute during the audit phase
- attr_accessor :audits
+ #
+ attr_reader :audits
+
+ #
+ # Notification handling
+ #
+ #
# A Hash containing the immediate notifications triggered by resources
# during the converge phase of the chef run.
- attr_accessor :immediate_notification_collection
+ #
+ # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from
+ # <notifying resource name> => <list of notifications it sent>
+ #
+ attr_reader :immediate_notification_collection
+ #
# A Hash containing the delayed (end of run) notifications triggered by
# resources during the converge phase of the chef run.
- attr_accessor :delayed_notification_collection
-
- # Event dispatcher for this run.
- attr_reader :events
-
- # Hash of factoids for a reboot request.
- attr_reader :reboot_info
+ #
+ # @return [Hash[String, Array[Chef::Resource::Notification]]] A hash from
+ # <notifying resource name> => <list of notifications it sent>
+ #
+ attr_reader :delayed_notification_collection
- # Creates a new Chef::RunContext object and populates its fields. This object gets
- # used by the Chef Server to generate a fully compiled recipe list for a node.
#
- # === Returns
- # object<Chef::RunContext>:: Duh. :)
- def initialize(node, cookbook_collection, events)
- @node = node
- @cookbook_collection = cookbook_collection
- @resource_collection = Chef::ResourceCollection.new
+ # Create non-shared state.
+ #
+ def initialize
+ # This is all non-shared state.
@audits = {}
+ @resource_collection = Chef::ResourceCollection.new
@immediate_notification_collection = Hash.new {|h,k| h[k] = []}
@delayed_notification_collection = Hash.new {|h,k| h[k] = []}
- @definitions = Hash.new
- @loaded_recipes = {}
- @loaded_attributes = {}
- @events = events
- @reboot_info = {}
-
- @node.run_context = self
- @node.set_cookbook_attribute
- @cookbook_compiler = nil
end
- # Triggers the compile phase of the chef run. Implemented by
- # Chef::RunContext::CookbookCompiler
- def load(run_list_expansion)
- @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events)
- @cookbook_compiler.compile
+ def self.new(node, cookbook_collection, events)
+ Chef::Log.deprecation("RunContext.new will be removed in a future Chef version. Use RootRunContext instead.")
+ RootRunContext.new(node, cookbook_collection, events)
end
- # Adds an immediate notification to the
- # +immediate_notification_collection+. The notification should be a
- # Chef::Resource::Notification or duck type.
+ #
+ # Adds an immediate notification to the +immediate_notification_collection+.
+ #
+ # @param [Chef::Resource::Notification] The notification to add.
+ #
def notifies_immediately(notification)
nr = notification.notifying_resource
if nr.instance_of?(Chef::Resource)
- @immediate_notification_collection[nr.name] << notification
+ # TODO is there any point at all to keying on name? Do we really want
+ # to categorize notifications from execute[do it] with file[do it]
+ # and directory[do it]?
+ immediate_notification_collection[nr.name] << notification
else
- @immediate_notification_collection[nr.declared_key] << notification
+ # TODO this is only declared on Chef::Resource. Does it even run?
+ immediate_notification_collection[nr.declared_key] << notification
end
end
- # Adds a delayed notification to the +delayed_notification_collection+. The
- # notification should be a Chef::Resource::Notification or duck type.
+ #
+ # Adds a delayed notification to the +delayed_notification_collection+.
+ #
+ # @param [Chef::Resource::Notification] The notification to add.
+ #
def notifies_delayed(notification)
nr = notification.notifying_resource
if nr.instance_of?(Chef::Resource)
- @delayed_notification_collection[nr.name] << notification
+ # TODO this seems odd and possibly even wrong.
+ delayed_notification_collection[nr.name] << notification
else
- @delayed_notification_collection[nr.declared_key] << notification
+ delayed_notification_collection[nr.declared_key] << notification
end
end
+ #
+ # Get the list of immediate notifications sent by the given resource.
+ #
+ # TODO seriously, this is actually wrong. resource.name is not unique,
+ # you need the type as well.
+ #
+ # @return [Array[Notification]]
+ #
def immediate_notifications(resource)
if resource.instance_of?(Chef::Resource)
- return @immediate_notification_collection[resource.name]
+ return immediate_notification_collection[resource.name]
else
- return @immediate_notification_collection[resource.declared_key]
+ return immediate_notification_collection[resource.declared_key]
end
end
+ #
+ # Get the list of delayed (end of run) notifications sent by the given
+ # resource.
+ #
+ # TODO seriously, this is actually wrong. resource.name is not unique,
+ # you need the type as well.
+ #
+ # @return [Array[Notification]]
+ #
def delayed_notifications(resource)
if resource.instance_of?(Chef::Resource)
- return @delayed_notification_collection[resource.name]
+ return delayed_notification_collection[resource.name]
else
- return @delayed_notification_collection[resource.declared_key]
+ return delayed_notification_collection[resource.declared_key]
end
end
+ #
+ # Cookbook and recipe loading
+ #
+
+ #
# Evaluates the recipes +recipe_names+. Used by DSL::IncludeRecipe
+ #
+ # @param recipe_names [Array[String]] The list of recipe names (e.g.
+ # 'my_cookbook' or 'my_cookbook::my_resource').
+ # @param current_cookbook The cookbook we are currently running in.
+ #
+ # @see DSL::IncludeRecipe#include_recipe
+ #
def include_recipe(*recipe_names, current_cookbook: nil)
result_recipes = Array.new
recipe_names.flatten.each do |recipe_name|
@@ -147,7 +226,21 @@ class Chef
result_recipes
end
+ #
# Evaluates the recipe +recipe_name+. Used by DSL::IncludeRecipe
+ #
+ # TODO I am sort of confused why we have both this and include_recipe ...
+ # I don't see anything different beyond accepting and returning an
+ # array of recipes.
+ #
+ # @param recipe_names [Array[String]] The recipe name (e.g 'my_cookbook' or
+ # 'my_cookbook::my_resource').
+ # @param current_cookbook The cookbook we are currently running in.
+ #
+ # @return A truthy value if the load occurred; `false` if already loaded.
+ #
+ # @see DSL::IncludeRecipe#load_recipe
+ #
def load_recipe(recipe_name, current_cookbook: nil)
Chef::Log.debug("Loading Recipe #{recipe_name} via include_recipe")
@@ -175,6 +268,15 @@ ERROR_MESSAGE
end
end
+ #
+ # Load the given recipe from a filename.
+ #
+ # @param recipe_file [String] The recipe filename.
+ #
+ # @return [Chef::Recipe] The loaded recipe.
+ #
+ # @raise [Chef::Exceptions::RecipeNotFound] If the file does not exist.
+ #
def load_recipe_file(recipe_file)
if !File.exist?(recipe_file)
raise Chef::Exceptions::RecipeNotFound, "could not find recipe file #{recipe_file}"
@@ -186,8 +288,19 @@ ERROR_MESSAGE
recipe
end
- # Looks up an attribute file given the +cookbook_name+ and
- # +attr_file_name+. Used by DSL::IncludeAttribute
+ #
+ # Look up an attribute filename.
+ #
+ # @param cookbook_name [String] The cookbook name of the attribute file.
+ # @param attr_file_name [String] The attribute file's name (not path).
+ #
+ # @return [String] The filename.
+ #
+ # @see DSL::IncludeAttribute#include_attribute
+ #
+ # @raise [Chef::Exceptions::CookbookNotFound] If the cookbook could not be found.
+ # @raise [Chef::Exceptions::AttributeNotFound] If the attribute file could not be found.
+ #
def resolve_attribute(cookbook_name, attr_file_name)
cookbook = cookbook_collection[cookbook_name]
raise Chef::Exceptions::CookbookNotFound, "could not find cookbook #{cookbook_name} while loading attribute #{name}" unless cookbook
@@ -198,76 +311,152 @@ ERROR_MESSAGE
attribute_filename
end
- # An Array of all recipes that have been loaded. This is stored internally
- # as a Hash, so ordering is predictable.
#
- # Recipe names are given in fully qualified form, e.g., the recipe "nginx"
- # will be given as "nginx::default"
+ # A list of all recipes that have been loaded.
+ #
+ # This is stored internally as a Hash, so ordering is predictable.
+ #
+ # TODO is the above statement true in a 1.9+ ruby world? Is it relevant?
+ #
+ # @return [Array[String]] A list of recipes in fully qualified form, e.g.
+ # the recipe "nginx" will be given as "nginx::default".
+ #
+ # @see #loaded_recipe? To determine if a particular recipe has been loaded.
#
- # To determine if a particular recipe has been loaded, use #loaded_recipe?
def loaded_recipes
- @loaded_recipes.keys
+ loaded_recipes_hash.keys
end
- # An Array of all attributes files that have been loaded. Stored internally
- # using a Hash, so order is predictable.
#
- # Attribute file names are given in fully qualified form, e.g.,
- # "nginx::default" instead of "nginx".
+ # A list of all attributes files that have been loaded.
+ #
+ # Stored internally using a Hash, so order is predictable.
+ #
+ # TODO is the above statement true in a 1.9+ ruby world? Is it relevant?
+ #
+ # @return [Array[String]] A list of attribute file names in fully qualified
+ # form, e.g. the "nginx" will be given as "nginx::default".
+ #
def loaded_attributes
- @loaded_attributes.keys
+ loaded_attributes_hash.keys
end
+ #
+ # Find out if a given recipe has been loaded.
+ #
+ # @param cookbook [String] Cookbook name.
+ # @param recipe [String] Recipe name.
+ #
+ # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise.
+ #
def loaded_fully_qualified_recipe?(cookbook, recipe)
- @loaded_recipes.has_key?("#{cookbook}::#{recipe}")
+ loaded_recipes_hash.has_key?("#{cookbook}::#{recipe}")
end
- # Returns true if +recipe+ has been loaded, false otherwise. Default recipe
- # names are expanded, so `loaded_recipe?("nginx")` and
- # `loaded_recipe?("nginx::default")` are valid and give identical results.
+ #
+ # Find out if a given recipe has been loaded.
+ #
+ # @param recipe [String] Recipe name. "nginx" and "nginx::default" yield
+ # the same results.
+ #
+ # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise.
+ #
def loaded_recipe?(recipe)
cookbook, recipe_name = Chef::Recipe.parse_recipe_name(recipe)
loaded_fully_qualified_recipe?(cookbook, recipe_name)
end
+ #
+ # Mark a given recipe as having been loaded.
+ #
+ # @param cookbook [String] Cookbook name.
+ # @param recipe [String] Recipe name.
+ #
+ def loaded_recipe(cookbook, recipe)
+ loaded_recipes["#{cookbook}::#{recipe}"] = true
+ end
+
+ #
+ # Find out if a given attribute file has been loaded.
+ #
+ # @param cookbook [String] Cookbook name.
+ # @param attribute_file [String] Attribute file name.
+ #
+ # @return [Boolean] `true` if the recipe has been loaded, `false` otherwise.
+ #
def loaded_fully_qualified_attribute?(cookbook, attribute_file)
- @loaded_attributes.has_key?("#{cookbook}::#{attribute_file}")
+ loaded_attributes.has_key?("#{cookbook}::#{attribute_file}")
end
+ #
+ # Mark a given attribute file as having been loaded.
+ #
+ # @param cookbook [String] Cookbook name.
+ # @param attribute_file [String] Attribute file name.
+ #
def loaded_attribute(cookbook, attribute_file)
- @loaded_attributes["#{cookbook}::#{attribute_file}"] = true
+ loaded_attributes_hash["#{cookbook}::#{attribute_file}"] = true
end
##
# Cookbook File Introspection
+ #
+ # Find out if the cookbook has the given template.
+ #
+ # @param cookbook [String] Cookbook name.
+ # @param template_name [String] Template name.
+ #
+ # @return [Boolean] `true` if the template is in the cookbook, `false`
+ # otherwise.
+ # @see Chef::CookbookVersion#has_template_for_node?
+ #
def has_template_in_cookbook?(cookbook, template_name)
cookbook = cookbook_collection[cookbook]
cookbook.has_template_for_node?(node, template_name)
end
+ #
+ # Find out if the cookbook has the given file.
+ #
+ # @param cookbook [String] Cookbook name.
+ # @param cb_file_name [String] File name.
+ #
+ # @return [Boolean] `true` if the file is in the cookbook, `false`
+ # otherwise.
+ # @see Chef::CookbookVersion#has_cookbook_file_for_node?
+ #
def has_cookbook_file_in_cookbook?(cookbook, cb_file_name)
cookbook = cookbook_collection[cookbook]
cookbook.has_cookbook_file_for_node?(node, cb_file_name)
end
- # Delegates to CookbookCompiler#unreachable_cookbook?
- # Used to raise an error when attempting to load a recipe belonging to a
- # cookbook that is not in the dependency graph. See also: CHEF-4367
+ #
+ # Find out whether the given cookbook is in the cookbook dependency graph.
+ #
+ # @param cookbook_name [String] Cookbook name.
+ #
+ # @return [Boolean] `true` if the cookbook is reachable, `false` otherwise.
+ #
+ # @see Chef::CookbookCompiler#unreachable_cookbook?
def unreachable_cookbook?(cookbook_name)
- @cookbook_compiler.unreachable_cookbook?(cookbook_name)
+ cookbook_compiler.unreachable_cookbook?(cookbook_name)
end
+ #
# Open a stream object that can be printed into and will dispatch to events
#
- # == Arguments
- # options is a hash with these possible options:
- # - name: a string that identifies the stream to the user. Preferably short.
+ # @param name [String] The name of the stream.
+ # @param options [Hash] Other options for the stream.
#
- # Pass a block and the stream will be yielded to it, and close on its own
- # at the end of the block.
- def open_stream(options = {})
- stream = EventDispatch::EventsOutputStream.new(events, options)
+ # @return [EventDispatch::EventsOutputStream] The created stream.
+ #
+ # @yield If a block is passed, it will be run and the stream will be closed
+ # afterwards.
+ # @yieldparam stream [EventDispatch::EventsOutputStream] The created stream.
+ #
+ def open_stream(name: nil, **options)
+ stream = EventDispatch::EventsOutputStream.new(events, name: name, **options)
if block_given?
begin
yield stream
@@ -280,31 +469,112 @@ ERROR_MESSAGE
end
# there are options for how to handle multiple calls to these functions:
- # 1. first call always wins (never change @reboot_info once set).
- # 2. last call always wins (happily change @reboot_info whenever).
+ # 1. first call always wins (never change reboot_info once set).
+ # 2. last call always wins (happily change reboot_info whenever).
# 3. raise an exception on the first conflict.
# 4. disable reboot after this run if anyone ever calls :cancel.
# 5. raise an exception on any second call.
# 6. ?
def request_reboot(reboot_info)
- Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to #{reboot_info.inspect}"
- @reboot_info = reboot_info
+ Chef::Log::info "Changing reboot status from #{self.reboot_info.inspect} to #{reboot_info.inspect}"
+ self.reboot_info = reboot_info
end
def cancel_reboot
- Chef::Log::info "Changing reboot status from #{@reboot_info.inspect} to {}"
- @reboot_info = {}
+ Chef::Log::info "Changing reboot status from #{reboot_info.inspect} to {}"
+ reboot_info = {}
end
def reboot_requested?
- @reboot_info.size > 0
+ reboot_info.size > 0
+ end
+
+ #
+ # Create a child RunContext.
+ #
+ def push
+ ChildRunContext.new(self)
end
private
- def loaded_recipe(cookbook, recipe)
- @loaded_recipes["#{cookbook}::#{recipe}"] = true
+ module Deprecated
+ ###
+ # These need to be settable so deploy can run a resource_collection
+ # independent of any cookbooks via +recipe_eval+
+
+ def resource_collection=(value)
+ Chef::Log.deprecation("Setting run_context.resource_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.")
+ end
+
+ def audits=(value)
+ Chef::Log.deprecation("Setting run_context.audits will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.")
+ end
+
+ def immediate_notification_collection=(value)
+ Chef::Log.deprecation("Setting run_context.immediate_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.")
+ end
+
+ def delayed_notification_collection=(value)
+ Chef::Log.deprecation("Setting run_context.delayed_notification_collection will be removed in a future Chef. Use run_context.create_child to create a new RunContext instead.")
+ end
+ end
+ prepend Deprecated
+
+
+ #
+ # A child run context. Delegates all root context calls to its parent.
+ #
+ # @api private
+ #
+ class ChildRunContext < RunContext
+ extend Forwardable
+ def_delegator :parent_run_context, :node, :cookbook_collection, :definitions, :events, :reboot_info
+
+ def initialize(parent_run_context)
+ @parent_run_context = parent_run_context
+ end
end
+ #
+ # The root run context. Contains all top-level state for the run.
+ #
+ # @api private
+ #
+ class RootRunContext < RunContext
+ # Creates a new Chef::RunContext object and populates its fields. This object gets
+ # used by the Chef Server to generate a fully compiled recipe list for a node.
+ #
+ # @param node [Chef::Node] The node to run against.
+ # @param cookbook_collection [Chef::CookbookCollection] The cookbooks
+ # involved in this run.
+ # @param events [EventDispatch::Dispatcher] The event dispatcher for this
+ # run.
+ #
+ def initialize(node, cookbook_collection, events)
+ node.run_context = self
+ @node = node
+ @cookbook_collection = cookbook_collection
+ @definitions = Hash.new
+ @loaded_recipes_hash = {}
+ @loaded_attributes_hash = {}
+ @events = events
+ @reboot_info = {}
+ @cookbook_compiler = nil
+
+ @node.set_cookbook_attribute
+ end
+
+ #
+ # Triggers the compile phase of the chef run.
+ #
+ # @param run_list_expansion [Chef::RunList::RunListExpansion] The run list.
+ # @see Chef::RunContext::CookbookCompiler
+ #
+ def load(run_list_expansion)
+ @cookbook_compiler = CookbookCompiler.new(self, run_list_expansion, events)
+ cookbook_compiler.compile
+ end
+ end
end
end