diff options
-rw-r--r-- | CHANGELOG.md | 2 | ||||
-rw-r--r-- | RELEASE_NOTES.md | 25 | ||||
-rw-r--r-- | lib/chef.rb | 1 | ||||
-rw-r--r-- | lib/chef/chef_class.rb | 130 | ||||
-rw-r--r-- | lib/chef/client.rb | 19 | ||||
-rw-r--r-- | lib/chef/mixin/provides.rb | 32 | ||||
-rw-r--r-- | lib/chef/platform/provider_priority_map.rb | 23 | ||||
-rw-r--r-- | lib/chef/platform/resource_priority_map.rb | 37 | ||||
-rw-r--r-- | lib/chef/policy_builder/expand_node_object.rb | 14 | ||||
-rw-r--r-- | lib/chef/policy_builder/policyfile.rb | 1 | ||||
-rw-r--r-- | lib/chef/provider.rb | 25 | ||||
-rw-r--r-- | lib/chef/provider_resolver.rb | 15 | ||||
-rw-r--r-- | lib/chef/resource.rb | 44 | ||||
-rw-r--r-- | lib/chef/resource_resolver.rb | 101 | ||||
-rw-r--r-- | spec/data/lwrp/providers/buck_passer.rb | 3 | ||||
-rw-r--r-- | spec/data/lwrp/resources/bar.rb | 2 | ||||
-rw-r--r-- | spec/unit/chef_class_spec.rb | 91 | ||||
-rw-r--r-- | spec/unit/provider_resolver_spec.rb | 1 | ||||
-rw-r--r-- | spec/unit/recipe_spec.rb | 39 | ||||
-rw-r--r-- | spec/unit/resource_spec.rb | 12 |
20 files changed, 531 insertions, 86 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 0acc5ba8d2..db07af7989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ * [**Mike Dodge**](https://github.com/mikedodge04) MacOSX services: Load LaunchAgents as console user, adding plist and session_type options. +* Add dynamic resource resolution similar to dynamic provider resolution +* Add Chef class fascade to internal structures ## 12.2.1 * [Issue 3153](https://github.com/chef/chef/issues/3153): Fix bug where unset HOME would cause chef to crash diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 510e457b33..8a7b537670 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -18,3 +18,28 @@ however advanced users may find it useful in certain use cases. Any cookbook that relies on other ohai data will absolutely not work in this mode unless the user implements workarounds such as running the ohai resource during the compile phase. + +## Dynamic Resource Resolution and Chef Class Fascade + +Resolution of Resources is now dynamic and similar to Providers and handles +multiple resources having the same provides line on a given platform. When +the user types a resource like 'package' into the DSL that is resolved via +the provides lines, and if multiple classes provide the same resource (like +Homebrew and MacPorts package resources on Mac) then which one is selected +is governed by the Chef::Platform::ResourcePriorityMap. + +In order to change the priorities in both the ResourcePriorityMap and +ProviderPriorityMap a helper API has been constructed off of the Chef class: + +* `Chef.get_provider_priority_array(resource_name)` +* `Chef.get_resource_priority_array(resource_name)` +* `Chef.set_provider_priority_array(resource_name, Array<Class>, *filter)` +* `Chef.set_resoruce_priority_array(resource_name, Array<Class>, *filter)` + +In order to change the `package` resource globally on MacOSX to the MacPorts provider: + +`Chef.set_resource_priority_array(:package, [ Chef::Resource::MacportsPackage ], os: 'darwin')` + +That line can be placed into a library file in a cookbook so that it is applied before +any recipes are compiled. + diff --git a/lib/chef.rb b/lib/chef.rb index 7f54b91f14..6bce976439 100644 --- a/lib/chef.rb +++ b/lib/chef.rb @@ -32,3 +32,4 @@ require 'chef/run_status' require 'chef/handler' require 'chef/handler/json_file' +require 'chef/chef_class' diff --git a/lib/chef/chef_class.rb b/lib/chef/chef_class.rb new file mode 100644 index 0000000000..d3f7ee55c7 --- /dev/null +++ b/lib/chef/chef_class.rb @@ -0,0 +1,130 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# NOTE: This class is not intended for internal use by the chef-client code. Classes in +# chef-client should still have objects like the node and run_context injected into them +# via their initializers. This class is still global state and will complicate writing +# unit tests for internal Chef objects. It is intended to be used only by recipe code. + +# NOTE: When adding require lines here you are creating tight coupling on a global that may be +# included in many different situations and ultimately that ends in tears with circular requires. +# Note the way that the run_context, provider_priority_map and resource_priority_map are "dependency +# injected" into this class by other objects and do not reference the class symbols in those files +# directly and we do not need to require those files here. + +class Chef + class << self + + # + # Public API + # + + # Get the node object + # + # @return [Chef::Node] node object of the chef-client run + attr_reader :node + + # Get the run context + # + # @return [Chef::RunContext] run_context of the chef-client run + attr_reader :run_context + + # Get the array of providers associated with a resource_name for the current node + # + # @param resource_name [Symbol] name of the resource as a symbol + # @return [Array<Class>] Priority Array of Provider Classes to use for the resource_name on the node + def get_provider_priority_array(resource_name) + @provider_priority_map.get_priority_array(node, resource_name).dup + end + + # Get the array of resources associated with a resource_name for the current node + # + # @param resource_name [Symbol] name of the resource as a symbol + # @return [Array<Class>] Priority Array of Resource Classes to use for the resource_name on the node + def get_resource_priority_array(resource_name) + @resource_priority_map.get_priority_array(node, resource_name).dup + end + + # Set the array of providers associated with a resource_name for the current node + # + # @param resource_name [Symbol] name of the resource as a symbol + # @param priority_array [Array<Class>] Array of Classes to set as the priority for resource_name on the node + # @param filter [Hash] Chef::Nodearray-style filter + # @return [Array<Class>] Modified Priority Array of Provider Classes to use for the resource_name on the node + def set_provider_priority_array(resource_name, priority_array, *filter) + @provider_priority_map.set_priority_array(resource_name, priority_array, *filter).dup + end + + # Get the array of resources associated with a resource_name for the current node + # + # @param resource_name [Symbol] name of the resource as a symbol + # @param priority_array [Array<Class>] Array of Classes to set as the priority for resource_name on the node + # @param filter [Hash] Chef::Nodearray-style filter + # @return [Array<Class>] Modified Priority Array of Resource Classes to use for the resource_name on the node + def set_resource_priority_array(resource_name, priority_array, *filter) + @resource_priority_map.set_priority_array(resource_name, priority_array, *filter).dup + end + + # + # Dependency Injection API (Private not Public) + # [ in the ruby sense these have to be public methods, but they are + # *NOT* for public consumption ] + # + + # Sets the resource_priority_map + # + # @api private + # @param resource_priority_map [Chef::Platform::ResourcePriorityMap] + def set_resource_priority_map(resource_priority_map) + @resource_priority_map = resource_priority_map + end + + # Sets the provider_priority_map + # + # @api private + # @param provider_priority_map [Chef::Platform::providerPriorityMap] + def set_provider_priority_map(provider_priority_map) + @provider_priority_map = provider_priority_map + end + + # Sets the node object + # + # @api private + # @param node [Chef::Node] + def set_node(node) + @node = node + end + + # Sets the run_context object + # + # @api private + # @param run_context [Chef::RunContext] + def set_run_context(run_context) + @run_context = run_context + end + + # Resets the internal state + # + # @api private + def reset! + @run_context = nil + @node = nil + @provider_priority_map = nil + @resource_priority_map = nil + end + end +end diff --git a/lib/chef/client.rb b/lib/chef/client.rb index a4f15c271f..d04a3dbbd5 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -166,6 +166,13 @@ class Chef if new_runlist = args.delete(:runlist) @json_attribs["run_list"] = new_runlist end + + # these slurp in the resource+provider world, so be exceedingly lazy about requiring them + require 'chef/platform/provider_priority_map' unless defined? Chef::Platform::ProviderPriorityMap + require 'chef/platform/resource_priority_map' unless defined? Chef::Platform::ResourcePriorityMap + + Chef.set_provider_priority_map(Chef::Platform::ProviderPriorityMap.instance) + Chef.set_resource_priority_map(Chef::Platform::ResourcePriorityMap.instance) end def configure_formatters @@ -224,21 +231,21 @@ class Chef end # Instantiates a Chef::Node object, possibly loading the node's prior state - # when using chef-client. Delegates to policy_builder - # + # when using chef-client. Delegates to policy_builder. Injects the built node + # into the Chef class. # - # === Returns - # Chef::Node:: The node object for this chef run + # @return [Chef::Node] The node object for this Chef run def load_node policy_builder.load_node @node = policy_builder.node + Chef.set_node(@node) + node end # Mutates the `node` object to prepare it for the chef run. Delegates to # policy_builder # - # === Returns - # Chef::Node:: The updated node object + # @return [Chef::Node] The updated node object def build_node policy_builder.build_node @run_status = Chef::RunStatus.new(node, events) diff --git a/lib/chef/mixin/provides.rb b/lib/chef/mixin/provides.rb new file mode 100644 index 0000000000..e5bb2c2005 --- /dev/null +++ b/lib/chef/mixin/provides.rb @@ -0,0 +1,32 @@ + +require 'chef/mixin/descendants_tracker' + +class Chef + module Mixin + module Provides + include Chef::Mixin::DescendantsTracker + + def node_map + @node_map ||= Chef::NodeMap.new + end + + def provides(short_name, opts={}, &block) + if !short_name.kind_of?(Symbol) + # YAGNI: this is probably completely unnecessary and can be removed? + Chef::Log.deprecation "Passing a non-Symbol to Chef::Resource#provides will be removed" + if short_name.kind_of?(String) + short_name.downcase! + short_name.gsub!(/\s/, "_") + end + short_name = short_name.to_sym + end + node_map.set(short_name, true, opts, &block) + end + + # provides a node on the resource (early binding) + def provides?(node, resource_name) + node_map.get(node, resource_name) + end + end + end +end diff --git a/lib/chef/platform/provider_priority_map.rb b/lib/chef/platform/provider_priority_map.rb index 2517f46dfd..1539f61900 100644 --- a/lib/chef/platform/provider_priority_map.rb +++ b/lib/chef/platform/provider_priority_map.rb @@ -1,6 +1,4 @@ -require 'chef/providers' - class Chef class Platform class ProviderPriorityMap @@ -10,7 +8,22 @@ class Chef load_default_map end + def get_priority_array(node, resource_name) + priority_map.get(node, resource_name.to_sym) + end + + def set_priority_array(resource_name, priority_array, *filter) + priority(resource_name.to_sym, priority_array.to_a, *filter) + end + + def priority(*args) + priority_map.set(*args) + end + + private + def load_default_map + require 'chef/providers' # # Linux @@ -71,13 +84,9 @@ class Chef end def priority_map + require 'chef/node_map' @priority_map ||= Chef::NodeMap.new end - - def priority(*args) - priority_map.set(*args) - end - end end end diff --git a/lib/chef/platform/resource_priority_map.rb b/lib/chef/platform/resource_priority_map.rb new file mode 100644 index 0000000000..fc43b3e7db --- /dev/null +++ b/lib/chef/platform/resource_priority_map.rb @@ -0,0 +1,37 @@ +class Chef + class Platform + class ResourcePriorityMap + include Singleton + + def initialize + load_default_map + end + + def get_priority_array(node, resource_name) + priority_map.get(node, resource_name.to_sym) + end + + def set_priority_array(resource_name, priority_array, *filter) + priority resource_name.to_sym, priority_array.to_a, *filter + end + + def priority(*args) + priority_map.set(*args) + end + + private + + def load_default_map + require 'chef/resources' + + # MacOSX + priority :package, Chef::Resource::HomebrewPackage, os: "darwin" + end + + def priority_map + require 'chef/node_map' + @priority_map ||= Chef::NodeMap.new + end + end + end +end diff --git a/lib/chef/policy_builder/expand_node_object.rb b/lib/chef/policy_builder/expand_node_object.rb index 2ee8a23258..524bdd95b1 100644 --- a/lib/chef/policy_builder/expand_node_object.rb +++ b/lib/chef/policy_builder/expand_node_object.rb @@ -24,6 +24,7 @@ require 'chef/rest' require 'chef/run_context' require 'chef/config' require 'chef/node' +require 'chef/chef_class' class Chef module PolicyBuilder @@ -54,6 +55,15 @@ class Chef @run_list_expansion = nil end + # This method injects the run_context and provider and resource priority + # maps into the Chef class. The run_context has to be injected here, the provider and + # resource maps could be moved if a better place can be found to do this work. + # + # @param run_context [Chef::RunContext] the run_context to inject + def setup_chef_class(run_context) + Chef.set_run_context(run_context) + end + def setup_run_context(specific_recipes=nil) if Chef::Config[:solo] Chef::Cookbook::FileVendor.fetch_from_disk(Chef::Config[:cookbook_path]) @@ -68,6 +78,10 @@ class Chef run_context = Chef::RunContext.new(node, cookbook_collection, @events) end + # TODO: this is really obviously not the place for this + # FIXME: need same edits + setup_chef_class(run_context) + # TODO: this is not the place for this. It should be in Runner or # CookbookCompiler or something. run_context.load(@run_list_expansion) diff --git a/lib/chef/policy_builder/policyfile.rb b/lib/chef/policy_builder/policyfile.rb index 0c7c07f9f3..ac25b549be 100644 --- a/lib/chef/policy_builder/policyfile.rb +++ b/lib/chef/policy_builder/policyfile.rb @@ -385,4 +385,3 @@ class Chef end end end - diff --git a/lib/chef/provider.rb b/lib/chef/provider.rb index 680fe9782f..65a56cf726 100644 --- a/lib/chef/provider.rb +++ b/lib/chef/provider.rb @@ -22,7 +22,7 @@ require 'chef/mixin/convert_to_class_name' require 'chef/mixin/enforce_ownership_and_permissions' require 'chef/mixin/why_run' require 'chef/mixin/shell_out' -require 'chef/mixin/descendants_tracker' +require 'chef/mixin/provides' require 'chef/platform/service_helpers' require 'chef/node_map' @@ -30,26 +30,11 @@ class Chef class Provider include Chef::Mixin::WhyRun include Chef::Mixin::ShellOut - extend Chef::Mixin::DescendantsTracker + extend Chef::Mixin::Provides - class << self - def node_map - @node_map ||= Chef::NodeMap.new - end - - def provides(resource_name, opts={}, &block) - node_map.set(resource_name.to_sym, true, opts, &block) - end - - # provides a node on the resource (early binding) - def provides?(node, resource) - node_map.get(node, resource.resource_name) - end - - # supports the given resource and action (late binding) - def supports?(resource, action) - true - end + # supports the given resource and action (late binding) + def self.supports?(resource, action) + true end attr_accessor :new_resource diff --git a/lib/chef/provider_resolver.rb b/lib/chef/provider_resolver.rb index d83a3e0468..867c3deca8 100644 --- a/lib/chef/provider_resolver.rb +++ b/lib/chef/provider_resolver.rb @@ -47,7 +47,7 @@ class Chef def enabled_handlers @enabled_handlers ||= providers.select do |klass| - klass.provides?(node, resource) + klass.provides?(node, resource.resource_name) end.sort {|a,b| a.to_s <=> b.to_s } end @@ -84,8 +84,13 @@ class Chef if handlers.count >= 2 # this magic stack ranks the providers by where they appear in the provider_priority_map, it is mostly used # to pick amongst N different ways to start init scripts on different debian/ubuntu systems. - priority_list = [ get_provider_priority_map(resource.resource_name, node) ].flatten.compact + priority_list = [ get_priority_array(node, resource.resource_name) ].flatten.compact handlers = handlers.sort_by { |x| i = priority_list.index x; i.nil? ? Float::INFINITY : i } + if priority_list.index(handlers.first).nil? + # if we had more than one and we picked one with a precidence of infinity that means that the resource_priority_map + # entry for this resource is missing -- we should probably raise here and force resolution of the ambiguity. + Chef::Log.warn "Ambiguous provider precedence: #{handlers}, please use Chef.set_provider_priority_array to provide determinism" + end handlers = [ handlers.first ] end @@ -106,12 +111,12 @@ class Chef end # dep injection hooks - def get_provider_priority_map(resource_name, node) - provider_priority_map.get(node, resource_name) + def get_priority_array(node, resource_name) + provider_priority_map.get_priority_array(node, resource_name) end def provider_priority_map - Chef::Platform::ProviderPriorityMap.instance.priority_map + Chef::Platform::ProviderPriorityMap.instance end end end diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb index ea220b6c70..d934ec8c47 100644 --- a/lib/chef/resource.rb +++ b/lib/chef/resource.rb @@ -33,7 +33,7 @@ require 'chef/platform' require 'chef/resource/resource_notification' require 'chef/mixin/deprecation' -require 'chef/mixin/descendants_tracker' +require 'chef/mixin/provides' class Chef class Resource @@ -46,6 +46,7 @@ class Chef include Chef::DSL::PlatformIntrospection include Chef::DSL::RegistryHelper include Chef::DSL::RebootPending + extend Chef::Mixin::Provides # # The node the current Chef run is using. @@ -879,7 +880,6 @@ class Chef include Chef::Mixin::ConvertToClassName extend Chef::Mixin::ConvertToClassName - extend Chef::Mixin::DescendantsTracker # XXX: this is required for definition params inside of the scope of a # subresource to work correctly. @@ -1016,6 +1016,7 @@ class Chef end def provider_for_action(action) + require 'chef/provider_resolver' provider = Chef::ProviderResolver.new(node, self, action).resolve.new(self, run_context) provider.action = action provider @@ -1080,33 +1081,6 @@ class Chef end end - # Maps a short_name (and optionally a platform and version) to a - # Chef::Resource. This allows finer grained per platform resource - # attributes and the end of overloaded resource definitions - # (I'm looking at you Chef::Resource::Package) - # Ex: - # class WindowsFile < Chef::Resource - # provides :file, os: "linux", platform_family: "rhel", platform: "redhat" - # provides :file, os: "!windows - # provides :file, os: [ "linux", "aix" ] - # provides :file, os: "solaris2" do |node| - # node['platform_version'].to_f <= 5.11 - # end - # # ...other stuff - # end - # - def self.provides(short_name, opts={}, &block) - short_name_sym = short_name - if short_name.kind_of?(String) - # YAGNI: this is probably completely unnecessary and can be removed? - Chef::Log.warn "[DEPRECATION] Passing a String to Chef::Resource#provides will be removed" - short_name.downcase! - short_name.gsub!(/\s/, "_") - short_name_sym = short_name.to_sym - end - node_map.set(short_name_sym, constantize(self.name), opts, &block) - end - # Returns a resource based on a short_name and node # # ==== Parameters @@ -1116,16 +1090,12 @@ class Chef # === Returns # <Chef::Resource>:: returns the proper Chef::Resource class def self.resource_for_node(short_name, node) - klass = node_map.get(node, short_name) || - resource_matching_short_name(short_name) + require 'chef/resource_resolver' + klass = Chef::ResourceResolver.new(node, short_name).resolve raise Chef::Exceptions::NoSuchResourceType.new(short_name, node) if klass.nil? klass end - def self.node_map - @@node_map ||= NodeMap.new - end - # Returns the class of a Chef::Resource based on the short name # ==== Parameters # short_name<Symbol>:: short_name of the resource (ie :directory) @@ -1156,7 +1126,3 @@ class Chef end end end - -# We require this at the BOTTOM of this file to avoid circular requires (it is used -# at runtime but not load time) -require 'chef/provider_resolver' diff --git a/lib/chef/resource_resolver.rb b/lib/chef/resource_resolver.rb new file mode 100644 index 0000000000..ff9d7aeb9f --- /dev/null +++ b/lib/chef/resource_resolver.rb @@ -0,0 +1,101 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# Copyright:: Copyright (c) 2015 Chef Software, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'chef/exceptions' +require 'chef/platform/resource_priority_map' + +class Chef + class ResourceResolver + + attr_reader :node + attr_reader :resource + attr_reader :action + + def initialize(node, resource) + @node = node + @resource = resource + end + + # return a deterministically sorted list of Chef::Resource subclasses + def resources + @resources ||= Chef::Resource.descendants + end + + def resolve + maybe_dynamic_resource_resolution(resource) || + maybe_chef_platform_lookup(resource) + end + + # this cut looks at if the resource can handle the resource type on the node + def enabled_handlers + @enabled_handlers ||= + resources.select do |klass| + klass.provides?(node, resource) + end.sort {|a,b| a.to_s <=> b.to_s } + @enabled_handlers + end + + private + + # try dynamically finding a resource based on querying the resources to see what they support + def maybe_dynamic_resource_resolution(resource) + # log this so we know what resources will work for the generic resource on the node (early cut) + Chef::Log.debug "resources for generic #{resource} resource enabled on node include: #{enabled_handlers}" + + # if none of the resources specifically support the resource, we still need to pick one of the resources that are + # enabled on the node to handle the why-run use case. + handlers = enabled_handlers + + if handlers.count >= 2 + # this magic stack ranks the resources by where they appear in the resource_priority_map + priority_list = [ get_priority_array(node, resource) ].flatten.compact + handlers = handlers.sort_by { |x| i = priority_list.index x; i.nil? ? Float::INFINITY : i } + if priority_list.index(handlers.first).nil? + # if we had more than one and we picked one with a precidence of infinity that means that the resource_priority_map + # entry for this resource is missing -- we should probably raise here and force resolution of the ambiguity. + Chef::Log.warn "Ambiguous resource precedence: #{handlers}, please use Chef.set_resource_priority_array to provide determinism" + end + handlers = [ handlers.first ] + end + + Chef::Log.debug "resources that survived replacement include: #{handlers}" + + raise Chef::Exceptions::AmbiguousResourceResolution.new(resource, handlers) if handlers.count >= 2 + + Chef::Log.debug "dynamic resource resolver FAILED to resolve a resouce" if handlers.empty? + + return nil if handlers.empty? + + handlers[0] + end + + # try the old static lookup of resources by mangling name to resource klass + def maybe_chef_platform_lookup(resource) + Chef::Resource.resource_matching_short_name(resource) + end + + # dep injection hooks + def get_priority_array(node, resource_name) + resource_priority_map.get_priority_array(node, resource_name) + end + + def resource_priority_map + Chef::Platform::ResourcePriorityMap.instance + end + end +end diff --git a/spec/data/lwrp/providers/buck_passer.rb b/spec/data/lwrp/providers/buck_passer.rb index c56ab94f85..9792e2c824 100644 --- a/spec/data/lwrp/providers/buck_passer.rb +++ b/spec/data/lwrp/providers/buck_passer.rb @@ -1,4 +1,5 @@ -provides 'buck_passer' +provides :buck_passer + action :pass_buck do lwrp_foo :prepared_thumbs do action :prepare_thumbs diff --git a/spec/data/lwrp/resources/bar.rb b/spec/data/lwrp/resources/bar.rb index b6359648db..2ff35efd08 100644 --- a/spec/data/lwrp/resources/bar.rb +++ b/spec/data/lwrp/resources/bar.rb @@ -1,2 +1,2 @@ -provides "lwrp_bar" # This makes sure that we cover the case of lwrps using provides +provides :lwrp_bar # This makes sure that we cover the case of lwrps using provides actions :pass_buck, :prepare_eyes, :watch_paint_dry diff --git a/spec/unit/chef_class_spec.rb b/spec/unit/chef_class_spec.rb new file mode 100644 index 0000000000..2528246be6 --- /dev/null +++ b/spec/unit/chef_class_spec.rb @@ -0,0 +1,91 @@ +# +# Author:: Lamont Granquist (<lamont@chef.io>) +# Copyright:: Copyright (c) 2015 Opscode, Inc. +# License:: Apache License, Version 2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +require 'spec_helper' + +describe "Chef class" do + let(:platform) { "debian" } + + let(:node) do + node = Chef::Node.new + node.automatic['platform'] = platform + node + end + + let(:run_context) do + Chef::RunContext.new(node, nil, nil) + end + + let(:resource_priority_map) do + double("Chef::Platform::ResourcePriorityMap") + end + + let(:provider_priority_map) do + double("Chef::Platform::ProviderPriorityMap") + end + + before do + Chef.set_run_context(run_context) + Chef.set_node(node) + Chef.set_resource_priority_map(resource_priority_map) + Chef.set_provider_priority_map(provider_priority_map) + end + + after do + Chef.reset! + end + + context "priority maps" do + context "#get_provider_priority_array" do + it "should use the current node to get the right priority_map" do + expect(provider_priority_map).to receive(:get_priority_array).with(node, :http_request).and_return("stuff") + expect(Chef.get_provider_priority_array(:http_request)).to eql("stuff") + end + end + context "#get_resource_priority_array" do + it "should use the current node to get the right priority_map" do + expect(resource_priority_map).to receive(:get_priority_array).with(node, :http_request).and_return("stuff") + expect(Chef.get_resource_priority_array(:http_request)).to eql("stuff") + end + end + context "#set_provider_priority_array" do + it "should delegate to the provider_priority_map" do + expect(provider_priority_map).to receive(:set_priority_array).with(:http_request, ["a", "b"], platform: "debian").and_return("stuff") + expect(Chef.set_provider_priority_array(:http_request, ["a", "b"], platform: "debian")).to eql("stuff") + end + end + context "#set_priority_map_for_resource" do + it "should delegate to the resource_priority_map" do + expect(resource_priority_map).to receive(:set_priority_array).with(:http_request, ["a", "b"], platform: "debian").and_return("stuff") + expect(Chef.set_resource_priority_array(:http_request, ["a", "b"], platform: "debian")).to eql("stuff") + end + end + end + + context "#run_context" do + it "should return the injected RunContext" do + expect(Chef.run_context).to eql(run_context) + end + end + + context "#node" do + it "should return the injected Node" do + expect(Chef.node).to eql(node) + end + end +end diff --git a/spec/unit/provider_resolver_spec.rb b/spec/unit/provider_resolver_spec.rb index 63c381f08e..718eebfdf4 100644 --- a/spec/unit/provider_resolver_spec.rb +++ b/spec/unit/provider_resolver_spec.rb @@ -18,6 +18,7 @@ require 'spec_helper' require 'chef/mixin/convert_to_class_name' +require 'chef/provider_resolver' include Chef::Mixin::ConvertToClassName diff --git a/spec/unit/recipe_spec.rb b/spec/unit/recipe_spec.rb index b370e12732..7442f4477e 100644 --- a/spec/unit/recipe_spec.rb +++ b/spec/unit/recipe_spec.rb @@ -20,6 +20,7 @@ # require 'spec_helper' +require 'chef/platform/resource_priority_map' describe Chef::Recipe do @@ -136,6 +137,44 @@ describe Chef::Recipe do res.kind_of?(YourMom) end + describe "when there is more than one resource that resolves on a node" do + before do + node.automatic[:platform] = "nbc_sports" + Sounders = Class.new(Chef::Resource) + Sounders.provides :football, platform: "nbc_sports" + TottenhamHotspur = Class.new(Chef::Resource) + TottenhamHotspur.provides :football, platform: "nbc_sports" + end + + after do + Object.send(:remove_const, :Sounders) + Object.send(:remove_const, :TottenhamHotspur) + end + + it "warns if resolution of the two resources is ambiguous" do + expect(Chef::Log).to receive(:warn).at_least(:once).with(/Ambiguous resource precedence/) + res1 = recipe.football "club world cup" + expect(res1.name).to eql("club world cup") + # the class of res1 is not defined. + end + + it "selects one if it is given priority" do + expect(Chef::Log).not_to receive(:warn) + Chef::Platform::ResourcePriorityMap.instance.send(:priority, :football, TottenhamHotspur, platform: "nbc_sports") + res1 = recipe.football "club world cup" + expect(res1.name).to eql("club world cup") + expect(res1).to be_a_kind_of(TottenhamHotspur) + end + + it "selects the other one if it is given priority" do + expect(Chef::Log).not_to receive(:warn) + Chef::Platform::ResourcePriorityMap.instance.send(:priority, :football, Sounders, platform: "nbc_sports") + res1 = recipe.football "club world cup" + expect(res1.name).to eql("club world cup") + expect(res1).to be_a_kind_of(Sounders) + end + end + end end diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb index 8214021f65..6b2d6c89d3 100644 --- a/spec/unit/resource_spec.rb +++ b/spec/unit/resource_spec.rb @@ -709,22 +709,22 @@ describe Chef::Resource do end it 'adds mappings for a single platform' do - expect(Chef::Resource.node_map).to receive(:set).with( - :dinobot, Chef::Resource::Klz, { platform: ['autobots'] } + expect(Chef::Resource::Klz.node_map).to receive(:set).with( + :dinobot, true, { platform: ['autobots'] } ) klz.provides :dinobot, platform: ['autobots'] end it 'adds mappings for multiple platforms' do - expect(Chef::Resource.node_map).to receive(:set).with( - :energy, Chef::Resource::Klz, { platform: ['autobots', 'decepticons']} + expect(Chef::Resource::Klz.node_map).to receive(:set).with( + :energy, true, { platform: ['autobots', 'decepticons']} ) klz.provides :energy, platform: ['autobots', 'decepticons'] end it 'adds mappings for all platforms' do - expect(Chef::Resource.node_map).to receive(:set).with( - :tape_deck, Chef::Resource::Klz, {} + expect(Chef::Resource::Klz.node_map).to receive(:set).with( + :tape_deck, true, {} ) klz.provides :tape_deck end |