require "spec_helper" 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