diff options
author | Tim Smith <tsmith@chef.io> | 2019-08-22 21:11:30 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-08-22 21:11:30 -0400 |
commit | 1403e97035b36acf8a6af88180a021fa1a196dfb (patch) | |
tree | ffa76b0db7fd5d89e43990f57e142d65d5e23ebf | |
parent | eb51b3d1391dcbb6b870cb90ff8554c7d4f39b2f (diff) | |
parent | 510555343fdd182be9a9870b036ed679c8ccc021 (diff) | |
download | chef-1403e97035b36acf8a6af88180a021fa1a196dfb.tar.gz |
Merge pull request #8841 from chef/lcg/unified-mode-backport
unified mode backport to Chef-14
-rw-r--r-- | chef-config/lib/chef-config/config.rb | 16 | ||||
-rw-r--r-- | lib/chef/exceptions.rb | 12 | ||||
-rw-r--r-- | lib/chef/provider.rb | 6 | ||||
-rw-r--r-- | lib/chef/resource.rb | 47 | ||||
-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 | 72 | ||||
-rw-r--r-- | lib/chef/runner.rb | 60 | ||||
-rw-r--r-- | spec/integration/recipes/unified_mode_spec.rb | 876 |
9 files changed, 1055 insertions, 70 deletions
diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb index ad846e8223..09c7ab8a28 100644 --- a/chef-config/lib/chef-config/config.rb +++ b/chef-config/lib/chef-config/config.rb @@ -829,6 +829,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. @@ -836,19 +837,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 @@ -874,9 +884,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 dafd445d2d..747e3fa607 100644 --- a/lib/chef/exceptions.rb +++ b/lib/chef/exceptions.rb @@ -527,5 +527,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 1751c35129..d85039ebcd 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"); @@ -233,8 +233,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? { |r| r.updated? } diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index edc6279fc3..90acafc770 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -181,7 +181,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 @@ -973,6 +973,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. # @@ -1026,7 +1036,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 @@ -1064,7 +1073,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. # @@ -1075,7 +1083,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`. # @@ -1085,7 +1092,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. @@ -1141,7 +1147,6 @@ class Chef end end - # # Ensure the action class actually gets created. This is called # when the user does `action :x do ... end`. # @@ -1196,22 +1201,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. @@ -1219,6 +1229,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. @@ -1227,37 +1238,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 @@ -1306,7 +1320,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) @@ -1328,7 +1341,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 @@ -1456,7 +1468,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. @@ -1482,7 +1493,6 @@ class Chef provider end - # # Preface an exception message with generic Resource information. # # @param e [StandardError] An exception with `e.message` @@ -1537,7 +1547,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 a3475e3301..64f1d1854c 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.kind_of?(Chef::Resource) && notifying_resource.kind_of?(Chef::Resource) if not(resource.kind_of?(Chef::Resource)) - fix_resource_reference(resource_collection) + fix_resource_reference(resource_collection, always_raise) end if not(notifying_resource.kind_of?(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 df0e6ddff6..8a938cd0f7 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 657caf0f81..05bfc6130f 100644 --- a/lib/chef/run_context.rb +++ b/lib/chef/run_context.rb @@ -2,7 +2,7 @@ # Author:: Adam Jacob (<adam@chef.io>) # Author:: Christopher Walters (<cw@chef.io>) # Author:: Tim Hinderliter (<tim@chef.io>) -# Copyright:: Copyright 2008-2017, Chef Software Inc. +# Copyright:: Copyright 2008-2019, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -26,11 +26,15 @@ require "chef/recipe" require "chef/run_context/cookbook_compiler" require "chef/event_dispatch/events_output_stream" require "forwardable" +require "set" +require "chef/exceptions" class Chef # Value object that loads and tracks the context of a Chef run class RunContext + extend Forwardable + # # Global state # @@ -57,14 +61,12 @@ class Chef # attr_reader :definitions - # # Event dispatcher for this run. # # @return [Chef::EventDispatch::Dispatcher] # attr_reader :events - # # Hash of factoids for a reboot request. # # @return [Hash] @@ -75,7 +77,6 @@ class Chef # Scoped state # - # # The parent run context. # # @return [Chef::RunContext] The parent run context, or `nil` if this is the @@ -83,7 +84,6 @@ class Chef # attr_reader :parent_run_context - # # The root run context. # # @return [Chef::RunContext] The root run context. @@ -94,7 +94,6 @@ class Chef rc end - # # The collection of resources intended to be converged (and able to be # notified). # @@ -109,7 +108,6 @@ class Chef # attr_reader :audits - # # Pointer back to the Chef::Runner that created this # attr_accessor :runner @@ -118,7 +116,6 @@ class Chef # Notification handling # - # # A Hash containing the before notifications triggered by resources # during the converge phase of the chef run. # @@ -127,7 +124,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. # @@ -136,7 +132,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. # @@ -145,7 +140,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. # @@ -153,7 +147,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 @@ -183,7 +186,6 @@ class Chef @loaded_attributes_hash = {} @reboot_info = {} @cookbook_compiler = nil - @delayed_actions = [] initialize_child_state end @@ -209,6 +211,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 # @@ -220,6 +223,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 @@ -244,11 +251,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) } @@ -259,32 +268,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 # @@ -646,6 +668,9 @@ ERROR_MESSAGE audits= create_child add_delayed_action + before_notification_collection + before_notifications + create_child delayed_actions delayed_notification_collection delayed_notification_collection= @@ -653,21 +678,22 @@ ERROR_MESSAGE 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 { |x| x.to_sym } # Verify that we didn't miss any methods diff --git a/lib/chef/runner.rb b/lib/chef/runner.rb index 1c82439b57..e15fe10966 100644 --- a/lib/chef/runner.rb +++ b/lib/chef/runner.rb @@ -2,7 +2,7 @@ # Author:: Adam Jacob (<adam@chef.io>) # Author:: Christopher Walters (<cw@chef.io>) # Author:: Tim Hinderliter (<tim@chef.io>) -# Copyright:: Copyright 2008-2017, Chef Software Inc. +# Copyright:: Copyright 2008-2019, Chef Software Inc. # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -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,20 +77,49 @@ 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 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 do |resource| @@ -95,7 +128,13 @@ class Chef # Execute each resource. run_context.resource_collection.execute_each_resource do |resource| - Array(resource.action).each { |action| run_action(resource, action) } + 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 @@ -124,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 |