diff options
author | Lamont Granquist <lamont@scriptkiddie.org> | 2015-04-11 12:48:22 -0700 |
---|---|---|
committer | Lamont Granquist <lamont@scriptkiddie.org> | 2015-04-15 17:50:15 -0700 |
commit | e3a6565927e854cd5968bd3a6bd2248ec1245549 (patch) | |
tree | 590bfa3f9c3a4992096c0ccb679fcc7deda74243 /lib/chef | |
parent | a959404b15ba6bdc98063cfa0c70e6f9eec9ccee (diff) | |
download | chef-e3a6565927e854cd5968bd3a6bd2248ec1245549.tar.gz |
add resource_resolver and resource_priority_map
also wire them up through the Chef class.
Diffstat (limited to 'lib/chef')
-rw-r--r-- | lib/chef/chef_class.rb | 130 | ||||
-rw-r--r-- | lib/chef/client.rb | 12 | ||||
-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 | 20 | ||||
-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 |
11 files changed, 362 insertions, 78 deletions
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..098345a9e2 100644 --- a/lib/chef/client.rb +++ b/lib/chef/client.rb @@ -224,21 +224,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..aa761926d3 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,21 @@ 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) + # these slurp in the resource+provider world, so be exceedingly lazy about requiring them + require 'chef/platform/provider_priority_map' + require 'chef/platform/resource_priority_map' + + Chef.set_run_context(run_context) + Chef.set_provider_priority_map(Chef::Platform::ProviderPriorityMap.instance) + Chef.set_resource_priority_map(Chef::Platform::ResourcePriorityMap.instance) + end + def setup_run_context(specific_recipes=nil) if Chef::Config[:solo] Chef::Cookbook::FileVendor.fetch_from_disk(Chef::Config[:cookbook_path]) @@ -68,6 +84,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 |