diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2019-06-17 21:07:08 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2019-08-12 11:29:16 -0700 |
commit | 35a63ddc192e50c45f6e94a3b270ed0e75c93668 (patch) | |
tree | fed93952978c3b75febae5d90f80e1f0cd5fcc8b /lib/chef | |
parent | 84da5f7a45d7ccba250d160626e6da8762a7f222 (diff) | |
download | chef-35a63ddc192e50c45f6e94a3b270ed0e75c93668.tar.gz |
Add unified_mode switch for resources
This is inspired by "use_inline_resources".
Setting `unified_mode false` in a resource would be the existing
behavior with separate compile/converge phases.
Setting `unified_mode true` in a resource will eliminate the converge
phase. Reverse notifications and delayed notifications will still
fire. The resource action will behave like all resources are executing
at compile time.
As a aside, notifications have never worked for resources firing at
compile time. This implementation gets that behavior correct so
that notifications will work.
Of course forward immediate notifications to resources not yet declared will not
be possible.
Setting `resource_unified_mode_default true` in `Chef::Config` would
turn off the split compile/converge mode for every custom resource.
NOTE: This does not affect recipe mode at all.
Signed-off-by: Lamont Granquist <lamont@scriptkiddie.org>
Diffstat (limited to 'lib/chef')
-rw-r--r-- | lib/chef/exceptions.rb | 12 | ||||
-rw-r--r-- | lib/chef/provider.rb | 6 | ||||
-rw-r--r-- | lib/chef/resource.rb | 48 | ||||
-rw-r--r-- | lib/chef/resource/resource_notification.rb | 30 | ||||
-rw-r--r-- | lib/chef/resource_collection.rb | 6 | ||||
-rw-r--r-- | lib/chef/run_context.rb | 78 | ||||
-rw-r--r-- | lib/chef/runner.rb | 63 |
7 files changed, 172 insertions, 71 deletions
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb index 7687e7d89c..40d20cc3ac 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -509,5 +509,17 @@ class Chef super "Conflicting requirements for gem '#{gem_name}': Both #{value1.inspect} and #{value2.inspect} given for option #{option.inspect}" end end + + class UnifiedModeImmediateSubscriptionEarlierResource < RuntimeError + def initialize(notification) + super "immediate subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode" + end + end + + class UnifiedModeBeforeSubscriptionEarlierResource < RuntimeError + def initialize(notification) + super "before subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode" + end + end end end diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 6d1985bbbb..fb5697fd0c 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -1,7 +1,7 @@ # # Author:: Adam Jacob (<adam@chef.io>) # Author:: Christopher Walters (<cw@chef.io>) -# Copyright:: Copyright 2008-2016, 2009-2018, Chef Software Inc. +# Copyright:: Copyright 2008-2016, 2009-2019, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -238,8 +238,10 @@ class Chef def compile_and_converge_action(&block) old_run_context = run_context @run_context = run_context.create_child + @run_context.resource_collection.unified_mode = new_resource.class.unified_mode + runner = Chef::Runner.new(@run_context) return_value = instance_eval(&block) - Chef::Runner.new(run_context).converge + runner.converge return_value ensure if run_context.resource_collection.any?(&:updated?) diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index 7f0895d6c9..a413e3e8d4 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -182,7 +182,7 @@ class Chef { action: { kind_of: Symbol, equal_to: allowed_actions } } ) # the resource effectively sends a delayed notification to itself - run_context.add_delayed_action(Notification.new(self, action, self)) + run_context.add_delayed_action(Notification.new(self, action, self, run_context.unified_mode)) end end @@ -453,7 +453,6 @@ class Chef # attr_reader :elapsed_time - # # @return [Boolean] If the resource was executed by the runner # attr_accessor :executed_by_runner @@ -985,6 +984,16 @@ class Chef resource_name automatic_name end + # If the resource's action should run in separated compile/converge mode. + # + # @param flag [Boolean] value to set unified_mode to + # @return [Boolean] unified_mode value + def self.unified_mode(flag = nil) + @unified_mode = Chef::Config[:resource_unified_mode_default] if @unified_mode.nil? + @unified_mode = flag unless flag.nil? + !!@unified_mode + end + # # The list of allowed actions for the resource. # @@ -1038,7 +1047,6 @@ class Chef default_action action_name end - # # Define an action on this resource. # # The action is defined as a *recipe* block that will be compiled and then @@ -1076,7 +1084,6 @@ class Chef default_action action if Array(default_action) == [:nothing] end - # # Define a method to load up this resource's properties with the current # actual values. # @@ -1087,7 +1094,6 @@ class Chef define_method(:load_current_value!, &load_block) end - # # Call this in `load_current_value` to indicate that the value does not # exist and that `current_resource` should therefore be `nil`. # @@ -1097,7 +1103,6 @@ class Chef raise Chef::Exceptions::CurrentValueDoesNotExist end - # # Get the current actual value of this resource. # # This does not cache--a new value will be returned each time. @@ -1154,7 +1159,6 @@ class Chef end end - # # Ensure the action class actually gets created. This is called # when the user does `action :x do ... end`. # @@ -1211,22 +1215,27 @@ class Chef # @return [Chef::RunContext] The run context for this Resource. This is # where the context for the current Chef run is stored, including the node # and the resource collection. + # attr_accessor :run_context # @return [Mixlib::Log::Child] The logger for this resources. This is a child # of the run context's logger, if one exists. + # attr_reader :logger # @return [String] The cookbook this resource was declared in. + # attr_accessor :cookbook_name # @return [String] The recipe this resource was declared in. + # attr_accessor :recipe_name # @return [Chef::Provider] The provider this resource was declared in (if # it was declared in an LWRP). When you call methods that do not exist # on this Resource, Chef will try to call the method on the provider # as well before giving up. + # attr_accessor :enclosing_provider # @return [String] The source line where this resource was declared. @@ -1234,6 +1243,7 @@ class Chef # of these formats: # /some/path/to/file.rb:80:in `wombat_tears' # C:/some/path/to/file.rb:80 in 1`wombat_tears' + # attr_accessor :source_line # @return [String] The actual name that was used to create this resource. @@ -1242,37 +1252,40 @@ class Chef # user will expect to see the thing they wrote, not the type that was # returned. May be `nil`, in which case callers should read #resource_name. # See #declared_key. + # attr_accessor :declared_type - # # Iterates over all immediate and delayed notifications, calling # resolve_resource_reference on each in turn, causing them to # resolve lazy/forward references. - def resolve_notification_references + # + def resolve_notification_references(always_raise = false) run_context.before_notifications(self).each do |n| - n.resolve_resource_reference(run_context.resource_collection) + n.resolve_resource_reference(run_context.resource_collection, true) end + run_context.immediate_notifications(self).each do |n| - n.resolve_resource_reference(run_context.resource_collection) + n.resolve_resource_reference(run_context.resource_collection, always_raise) end + run_context.delayed_notifications(self).each do |n| - n.resolve_resource_reference(run_context.resource_collection) + n.resolve_resource_reference(run_context.resource_collection, always_raise) end end # Helper for #notifies def notifies_before(action, resource_spec) - run_context.notifies_before(Notification.new(resource_spec, action, self)) + run_context.notifies_before(Notification.new(resource_spec, action, self, run_context.unified_mode)) end # Helper for #notifies def notifies_immediately(action, resource_spec) - run_context.notifies_immediately(Notification.new(resource_spec, action, self)) + run_context.notifies_immediately(Notification.new(resource_spec, action, self, run_context.unified_mode)) end # Helper for #notifies def notifies_delayed(action, resource_spec) - run_context.notifies_delayed(Notification.new(resource_spec, action, self)) + run_context.notifies_delayed(Notification.new(resource_spec, action, self, run_context.unified_mode)) end class << self @@ -1321,7 +1334,6 @@ class Chef end end - # # This API can be used for backcompat to do: # # chef_version_for_provides "< 14.0" if defined?(:chef_version_for_provides) @@ -1343,7 +1355,6 @@ class Chef @chef_version_for_provides = constraint end - # # Mark this resource as providing particular DSL. # # Resources have an automatic DSL based on their resource_name, equivalent to @@ -1472,7 +1483,6 @@ class Chef @default_description end - # # The cookbook in which this Resource was defined (if any). # # @return Chef::CookbookVersion The cookbook in which this Resource was defined. @@ -1498,7 +1508,6 @@ class Chef provider end - # # Preface an exception message with generic Resource information. # # @param e [StandardError] An exception with `e.message` @@ -1554,7 +1563,6 @@ class Chef klass end - # # Returns the class with the given resource_name. # # NOTE: Chef::Resource.resource_matching_short_name(:package) returns diff --git a/lib/chef/resource/resource_notification.rb b/lib/chef/resource/resource_notification.rb index 7e93fff433..d3b9856332 100644 --- a/lib/chef/resource/resource_notification.rb +++ b/lib/chef/resource/resource_notification.rb @@ -1,6 +1,6 @@ # # Author:: Tyler Ball (<tball@chef.io>) -# Copyright:: Copyright 2014-2016, Chef Software, Inc. +# Copyright:: Copyright 2014-2019, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,12 +26,13 @@ class Chef # @attr [Resource] notifying_resource the Chef resource performing the notification class Notification - attr_accessor :resource, :action, :notifying_resource + attr_accessor :resource, :action, :notifying_resource, :unified_mode - def initialize(resource, action, notifying_resource) + def initialize(resource, action, notifying_resource, unified_mode = false) @resource = resource @action = action&.to_sym @notifying_resource = notifying_resource + @unified_mode = unified_mode end # Is the current notification a duplicate of another notification @@ -52,11 +53,11 @@ class Chef # @param [ResourceCollection] resource_collection # # @return [void] - def resolve_resource_reference(resource_collection) + def resolve_resource_reference(resource_collection, always_raise = false) return resource if resource.is_a?(Chef::Resource) && notifying_resource.is_a?(Chef::Resource) unless resource.is_a?(Chef::Resource) - fix_resource_reference(resource_collection) + fix_resource_reference(resource_collection, always_raise) end unless notifying_resource.is_a?(Chef::Resource) @@ -69,7 +70,7 @@ class Chef # @param [ResourceCollection] resource_collection # # @return [void] - def fix_resource_reference(resource_collection) + def fix_resource_reference(resource_collection, always_raise = false) matching_resource = resource_collection.find(resource) if Array(matching_resource).size > 1 msg = "Notification #{self} from #{notifying_resource} was created with a reference to multiple resources, "\ @@ -79,13 +80,16 @@ class Chef self.resource = matching_resource rescue Chef::Exceptions::ResourceNotFound => e - err = Chef::Exceptions::ResourceNotFound.new(<<~FAIL) - resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \ - but #{resource} cannot be found in the resource collection. #{notifying_resource} is defined in \ - #{notifying_resource.source_line} - FAIL - err.set_backtrace(e.backtrace) - raise err + # in unified mode we allow lazy notifications to resources not yet declared + if !unified_mode || always_raise + err = Chef::Exceptions::ResourceNotFound.new(<<~FAIL) + resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \ + but #{resource} cannot be found in the resource collection. #{notifying_resource} is defined in \ + #{notifying_resource.source_line} + FAIL + err.set_backtrace(e.backtrace) + raise err + end rescue Chef::Exceptions::InvalidResourceSpecification => e err = Chef::Exceptions::InvalidResourceSpecification.new(<<~F) Resource #{notifying_resource} is configured to notify resource #{resource} with action #{action}, \ diff --git a/lib/chef/resource_collection.rb b/lib/chef/resource_collection.rb index 3b6ff4297e..da675fc4de 100644 --- a/lib/chef/resource_collection.rb +++ b/lib/chef/resource_collection.rb @@ -32,6 +32,8 @@ class Chef include ResourceCollectionSerialization extend Forwardable + attr_accessor :unified_mode + attr_reader :resource_set, :resource_list attr_accessor :run_context @@ -41,6 +43,7 @@ class Chef @run_context = run_context @resource_set = ResourceSet.new @resource_list = ResourceList.new + @unified_mode = false end # @param resource [Chef::Resource] The resource to insert @@ -57,6 +60,9 @@ class Chef else resource_set.insert_as(resource) end + if unified_mode + run_context.runner.run_all_actions(resource) + end end def delete(key) diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb index eb211dc5a5..7a5924a3c7 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -26,12 +26,16 @@ require_relative "recipe" require_relative "run_context/cookbook_compiler" require_relative "event_dispatch/events_output_stream" require_relative "train_transport" +require_relative "exceptions" require "forwardable" unless defined?(Forwardable) +require "set" unless defined?(Set) class Chef # Value object that loads and tracks the context of a Chef run class RunContext + extend Forwardable + # # Global state # @@ -72,14 +76,12 @@ class Chef # attr_reader :definitions - # # Event dispatcher for this run. # # @return [Chef::EventDispatch::Dispatcher] # attr_accessor :events - # # Hash of factoids for a reboot request. # # @return [Hash] @@ -90,7 +92,6 @@ class Chef # Scoped state # - # # The parent run context. # # @return [Chef::RunContext] The parent run context, or `nil` if this is the @@ -98,7 +99,6 @@ class Chef # attr_reader :parent_run_context - # # The root run context. # # @return [Chef::RunContext] The root run context. @@ -109,7 +109,6 @@ class Chef rc end - # # The collection of resources intended to be converged (and able to be # notified). # @@ -119,8 +118,12 @@ class Chef # attr_reader :resource_collection - attr_accessor :action_collection + # Handle to the global action_collection of executed actions for reporting / data_collector /etc + # + # @return [Chef::ActionCollection # + attr_accessor :action_collection + # Pointer back to the Chef::Runner that created this # attr_accessor :runner @@ -129,7 +132,6 @@ class Chef # Notification handling # - # # A Hash containing the before notifications triggered by resources # during the converge phase of the chef run. # @@ -138,7 +140,6 @@ class Chef # attr_reader :before_notification_collection - # # A Hash containing the immediate notifications triggered by resources # during the converge phase of the chef run. # @@ -147,7 +148,6 @@ class Chef # attr_reader :immediate_notification_collection - # # A Hash containing the delayed (end of run) notifications triggered by # resources during the converge phase of the chef run. # @@ -156,7 +156,6 @@ class Chef # attr_reader :delayed_notification_collection - # # An Array containing the delayed (end of run) notifications triggered by # resources during the converge phase of the chef run. # @@ -164,7 +163,16 @@ class Chef # attr_reader :delayed_actions + # A Set keyed by the string name, of all the resources that are updated. We do not + # track actions or individual resource objects, since this matches the behavior of + # the notification collections which are keyed by Strings. + # + attr_reader :updated_resources + + # @return [Boolean] If the resource_collection is in unified_mode (no separate converge phase) # + def_delegator :resource_collection, :unified_mode + # A child of the root Chef::Log logging object. # # @return Mixlib::Log::Child A child logger @@ -190,7 +198,6 @@ class Chef @loaded_attributes_hash = {} @reboot_info = {} @cookbook_compiler = nil - @delayed_actions = [] initialize_child_state end @@ -221,6 +228,7 @@ class Chef @immediate_notification_collection = Hash.new { |h, k| h[k] = [] } @delayed_notification_collection = Hash.new { |h, k| h[k] = [] } @delayed_actions = [] + @updated_resources = Set.new end # @@ -232,6 +240,10 @@ class Chef # Note for the future, notification.notifying_resource may be an instance # of Chef::Resource::UnresolvedSubscribes when calling {Resource#subscribes} # with a string value. + if unified_mode && updated_resources.include?(notification.notifying_resource.declared_key) + raise Chef::Exceptions::UnifiedModeBeforeSubscriptionEarlierResource.new(notification) + end + before_notification_collection[notification.notifying_resource.declared_key] << notification end @@ -256,11 +268,13 @@ class Chef # Note for the future, notification.notifying_resource may be an instance # of Chef::Resource::UnresolvedSubscribes when calling {Resource#subscribes} # with a string value. + if unified_mode && updated_resources.include?(notification.notifying_resource.declared_key) + add_delayed_action(notification) + end delayed_notification_collection[notification.notifying_resource.declared_key] << notification end - # - # Adds a delayed action to the +delayed_actions+. + # Adds a delayed action to the delayed_actions collection # def add_delayed_action(notification) if delayed_actions.any? { |existing_notification| existing_notification.duplicates?(notification) } @@ -271,32 +285,45 @@ class Chef end end - # # Get the list of before notifications sent by the given resource. # # @return [Array[Notification]] # def before_notifications(resource) - before_notification_collection[resource.declared_key] + key = resource.is_a?(String) ? resource : resource.declared_key + before_notification_collection[key] end - # # Get the list of immediate notifications sent by the given resource. # # @return [Array[Notification]] # def immediate_notifications(resource) - immediate_notification_collection[resource.declared_key] + key = resource.is_a?(String) ? resource : resource.declared_key + immediate_notification_collection[key] end + # Get the list of immeidate notifications pending to the given resource + # + # @return [Array[Notification]] # + def reverse_immediate_notifications(resource) + immediate_notification_collection.map do |k, v| + v.select do |n| + (n.resource.is_a?(String) && n.resource == resource.declared_key) || + n.resource == resource + end + end.flatten + end + # Get the list of delayed (end of run) notifications sent by the given # resource. # # @return [Array[Notification]] # def delayed_notifications(resource) - delayed_notification_collection[resource.declared_key] + key = resource.is_a?(String) ? resource : resource.declared_key + delayed_notification_collection[key] end # @@ -666,9 +693,9 @@ class Chef rest= rest_clean rest_clean= - unreachable_cookbook? transport transport_connection + unreachable_cookbook? } def initialize(parent_run_context) @@ -681,8 +708,10 @@ class Chef end CHILD_STATE = %w{ - create_child add_delayed_action + before_notification_collection + before_notifications + create_child delayed_actions delayed_notification_collection delayed_notification_collection= @@ -690,21 +719,22 @@ class Chef immediate_notification_collection immediate_notification_collection= immediate_notifications - before_notification_collection - before_notifications include_recipe initialize_child_state load_recipe load_recipe_file notifies_before - notifies_immediately notifies_delayed + notifies_immediately parent_run_context - root_run_context resource_collection resource_collection= + reverse_immediate_notifications + root_run_context runner runner= + unified_mode + updated_resources }.map(&:to_sym) # Verify that we didn't miss any methods diff --git a/lib/chef/runner.rb b/lib/chef/runner.rb index a0ae61fe4c..1ece2f97c3 100644 --- a/lib/chef/runner.rb +++ b/lib/chef/runner.rb @@ -34,7 +34,7 @@ class Chef def initialize(run_context) @run_context = run_context - run_context.runner = self + @run_context.runner = self end def delayed_actions @@ -45,6 +45,10 @@ class Chef @run_context.events end + def updated_resources + @run_context.updated_resources + end + # Determine the appropriate provider for the given resource, then # execute it. def run_action(resource, action, notification_type = nil, notifying_resource = nil) @@ -73,33 +77,67 @@ class Chef # associated with the resource, but only if it was updated *this time* # we ran an action on it. if resource.updated_by_last_action? + updated_resources.add(resource.declared_key) # track updated resources for unified_mode run_context.immediate_notifications(resource).each do |notification| - Chef::Log.info("#{resource} sending #{notification.action} action to #{notification.resource} (immediate)") - run_action(notification.resource, notification.action, :immediate, resource) + if notification.resource.is_a?(String) && run_context.unified_mode + Chef::Log.debug("immediate notification from #{resource} to #{notification.resource} is delayed until declaration due to unified_mode") + else + Chef::Log.info("#{resource} sending #{notification.action} action to #{notification.resource} (immediate)") + run_action(notification.resource, notification.action, :immediate, resource) + end end run_context.delayed_notifications(resource).each do |notification| - # send the notification to the run_context of the receiving resource - notification.resource.run_context.add_delayed_action(notification) + if notification.resource.is_a?(String) + # for string resources that have not be declared yet in unified mode we only support notifying the current run_context + run_context.add_delayed_action(notification) + else + # send the notification to the run_context of the receiving resource + notification.resource.run_context.add_delayed_action(notification) + end + end + end + end + + # Runs all of the actions on a given resource. This fires notifications and marks + # the resource as having been executed by the runner. + # + # @param resource [Chef::Resource] the resource to run + # + def run_all_actions(resource) + Array(resource.action).each { |action| run_action(resource, action) } + if run_context.unified_mode + run_context.reverse_immediate_notifications(resource).each do |n| + if updated_resources.include?(n.notifying_resource.declared_key) + n.resolve_resource_reference(run_context.resource_collection) + Chef::Log.info("#{resource} sent #{n.action} action to #{n.resource} (immediate at declaration time)") + run_action(n.resource, n.action, :immediate, n.notifying_resource) + end end end + ensure + resource.executed_by_runner = true end - # Iterates over the +resource_collection+ in the +run_context+ calling - # +run_action+ for each resource in turn. + # Iterates over the resource_collection in the run_context calling + # run_action for each resource in turn. + # def converge # Resolve all lazy/forward references in notifications run_context.resource_collection.each(&:resolve_notification_references) # Execute each resource. run_context.resource_collection.execute_each_resource do |resource| - begin - Array(resource.action).each { |action| run_action(resource, action) } - ensure - resource.executed_by_runner = true + unless run_context.resource_collection.unified_mode + run_all_actions(resource) end end + if run_context.resource_collection.unified_mode + puts "HERE WE ARE!" + run_context.resource_collection.each { |r| r.resolve_notification_references(true) } + end + rescue Exception => e Chef::Log.info "Running queued delayed notifications before re-raising exception" run_delayed_notifications(e) @@ -126,7 +164,8 @@ class Chef def run_delayed_notification(notification) Chef::Log.info( "#{notification.notifying_resource} sending #{notification.action}"\ " action to #{notification.resource} (delayed)") - # Struct of resource/action to call + # notifications may have lazy strings in them to resolve + notification.resolve_resource_reference(run_context.resource_collection) run_action(notification.resource, notification.action, :delayed) true rescue Exception => e |