Skip to content

Commit

Permalink
Chef-12 RC Provider Resolver
Browse files Browse the repository at this point in the history
makes resource and provider class resolution more dynamic.
begins deprecation of Chef::Platform static mapping.
  • Loading branch information
lamont-granquist committed Oct 24, 2014
1 parent cb1bcb1 commit 97aaf5b
Show file tree
Hide file tree
Showing 143 changed files with 1,900 additions and 1,308 deletions.
1 change: 0 additions & 1 deletion lib/chef/dsl/recipe.rb
Expand Up @@ -17,7 +17,6 @@
# limitations under the License.
#

require 'chef/resource_platform_map'
require 'chef/mixin/convert_to_class_name'
require 'chef/exceptions'

Expand Down
13 changes: 12 additions & 1 deletion lib/chef/exceptions.rb
Expand Up @@ -91,7 +91,11 @@ class InvalidResourceReference < RuntimeError; end
class ResourceNotFound < RuntimeError; end

# Can't find a Resource of this type that is valid on this platform.
class NoSuchResourceType < NameError; end
class NoSuchResourceType < NameError
def initialize(short_name, node)
super "Cannot find a resource for #{short_name} on #{node[:platform]} version #{node[:platform_version]}"
end
end

class InvalidResourceSpecification < ArgumentError; end
class SolrConnectionError < RuntimeError; end
Expand Down Expand Up @@ -355,5 +359,12 @@ class ParseError < RuntimeError; end
end

class InvalidSearchQuery < ArgumentError; end

# Raised by Chef::ProviderResolver
class AmbiguousProviderResolution < RuntimeError
def initialize(resource, classes)
super "Found more than one provider for #{resource.resource_name} resource: #{classes}"
end
end
end
end
2 changes: 1 addition & 1 deletion lib/chef/json_compat.rb
Expand Up @@ -152,7 +152,7 @@ def class_for_json_class(json_class)
when CHEF_RESOURCELIST
Chef::ResourceCollection::ResourceList
when /^Chef::Resource/
Chef::Resource.find_subclass_by_name(json_class)
Chef::Resource.find_descendants_by_name(json_class)
else
raise Chef::Exceptions::JSON::ParseError, "Unsupported `json_class` type '#{json_class}'"
end
Expand Down
54 changes: 54 additions & 0 deletions lib/chef/mixin/convert_to_class_name.rb
Expand Up @@ -61,6 +61,60 @@ def filename_to_qualified_string(base, filename)
base.to_s + (file_base == 'default' ? '' : "_#{file_base}")
end

# Copied from rails activesupport. In ruby >= 2.0 const_get will just do this, so this can
# be deprecated and removed.
#
# MIT LICENSE is here: https://github.com/rails/rails/blob/master/activesupport/MIT-LICENSE

# Tries to find a constant with the name specified in the argument string.
#
# 'Module'.constantize # => Module
# 'Test::Unit'.constantize # => Test::Unit
#
# The name is assumed to be the one of a top-level constant, no matter
# whether it starts with "::" or not. No lexical context is taken into
# account:
#
# C = 'outside'
# module M
# C = 'inside'
# C # => 'inside'
# 'C'.constantize # => 'outside', same as ::C
# end
#
# NameError is raised when the name is not in CamelCase or the constant is
# unknown.
def constantize(camel_cased_word)
names = camel_cased_word.split('::')

# Trigger a built-in NameError exception including the ill-formed constant in the message.
Object.const_get(camel_cased_word) if names.empty?

# Remove the first blank element in case of '::ClassName' notation.
names.shift if names.size > 1 && names.first.empty?

names.inject(Object) do |constant, name|
if constant == Object
constant.const_get(name)
else
candidate = constant.const_get(name)
next candidate if constant.const_defined?(name, false)
next candidate unless Object.const_defined?(name)

# Go down the ancestors to check if it is owned directly. The check
# stops when we reach Object or the end of ancestors tree.
constant = constant.ancestors.inject do |const, ancestor|
break const if ancestor == Object
break ancestor if ancestor.const_defined?(name, false)
const
end

# owner is in Object, so raise
constant.const_get(name, false)
end
end
end

end
end
end
82 changes: 82 additions & 0 deletions lib/chef/mixin/descendants_tracker.rb
@@ -0,0 +1,82 @@
#
# Copyright (c) 2005-2012 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
# "Software"), to deal in the Software without restriction, including
# without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to
# the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#


# This is lifted from rails activesupport (note the copyright above):
# https://github.com/rails/rails/blob/9f84e60ac9d7bf07d6ae1bc94f3941f5b8f1a228/activesupport/lib/active_support/descendants_tracker.rb

class Chef
module Mixin
module DescendantsTracker
@@direct_descendants = {}

class << self
def direct_descendants(klass)
@@direct_descendants[klass] || []
end

def descendants(klass)
arr = []
accumulate_descendants(klass, arr)
arr
end

def find_descendants_by_name(klass, name)
descendants(klass).first {|c| c.name == name }
end

# This is the only method that is not thread safe, but is only ever called
# during the eager loading phase.
def store_inherited(klass, descendant)
(@@direct_descendants[klass] ||= []) << descendant
end

private

def accumulate_descendants(klass, acc)
if direct_descendants = @@direct_descendants[klass]
acc.concat(direct_descendants)
direct_descendants.each { |direct_descendant| accumulate_descendants(direct_descendant, acc) }
end
end
end

def inherited(base)
DescendantsTracker.store_inherited(self, base)
super
end

def direct_descendants
DescendantsTracker.direct_descendants(self)
end

def find_descendants_by_name(name)
DescendantsTracker.find_descendants_by_name(self, name)
end

def descendants
DescendantsTracker.descendants(self)
end
end
end
end
146 changes: 146 additions & 0 deletions lib/chef/node_map.rb
@@ -0,0 +1,146 @@
#
# Author:: Lamont Granquist (<lamont@chef.io>)
# Copyright:: Copyright (c) 2014 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.
#

class Chef
class NodeMap

VALID_OPTS = [
:on_platform,
:on_platforms,
:platform,
:os,
:platform_family,
]

DEPRECATED_OPTS = [
:on_platform,
:on_platforms,
]

# Create a new NodeMap
#
def initialize
@map = {}
end

# Set a key/value pair on the map with a filter. The filter must be true
# when applied to the node in order to retrieve the value.
#
# @param key [Object] Key to store
# @param value [Object] Value associated with the key
# @param filters [Hash] Node filter options to apply to key retrieval
# @yield [node] Arbitrary node filter as a block which takes a node argument
# @return [NodeMap] Returns self for possible chaining
#
def set(key, value, filters = {}, &block)
validate_filter!(filters)
deprecate_filter!(filters)
@map[key] ||= []
# we match on the first value we find, so we want to unshift so that the
# last setter wins
# FIXME: need a test for this behavior
@map[key].unshift({ filters: filters, block: block, value: value })
self
end

# Get a value from the NodeMap via applying the node to the filters that
# were set on the key.
#
# @param node [Chef::Node] The Chef::Node object for the run
# @param key [Object] Key to look up
# @return [Object] Value
#
def get(node, key)
# FIXME: real exception
raise "first argument must be a Chef::Node" unless node.is_a?(Chef::Node)
return nil unless @map.has_key?(key)
@map[key].each do |matcher|
if filters_match?(node, matcher[:filters]) &&
block_matches?(node, matcher[:block])
return matcher[:value]
end
end
nil
end

private

# only allow valid filter options
def validate_filter!(filters)
filters.each_key do |key|
# FIXME: real exception
raise "Bad key #{key} in Chef::NodeMap filter expression" unless VALID_OPTS.include?(key)
end
end

# warn on deprecated filter options
def deprecate_filter!(filters)
filters.each_key do |key|
Chef::Log.warn "The #{key} option to node_map has been deprecated" if DEPRECATED_OPTS.include?(key)
end
end

# @todo: this works fine, but is probably hard to understand
def negative_match(filter, param)
# We support strings prefaced by '!' to mean 'not'. In particular, this is most useful
# for os matching on '!windows'.
negative_matches = filter.select { |f| f[0] == '!' }
return true if !negative_matches.empty? && negative_matches.include?('!' + param)

# We support the symbol :all to match everything, for backcompat, but this can and should
# simply be ommitted.
positive_matches = filter.reject { |f| f[0] == '!' || f == :all }
return true if !positive_matches.empty? && !positive_matches.include?(param)

# sorry double-negative: this means we pass this filter.
false
end

def filters_match?(node, filters)
return true if filters.empty?

# each filter is applied in turn. if any fail, then it shortcuts and returns false.
# if it passes or does not exist it succeeds and continues on. so multiple filters are
# effectively joined by 'and'. all filters can be single strings, or arrays which are
# effectively joined by 'or'.

os_filter = [ filters[:os] ].flatten.compact
unless os_filter.empty?
return false if negative_match(os_filter, node[:os])
end

platform_family_filter = [ filters[:platform_family] ].flatten.compact
unless platform_family_filter.empty?
return false if negative_match(platform_family_filter, node[:platform_family])
end

# :on_platform and :on_platforms here are synonyms which are deprecated
platform_filter = [ filters[:platform] || filters[:on_platform] || filters[:on_platforms] ].flatten.compact
unless platform_filter.empty?
return false if negative_match(platform_filter, node[:platform])
end

return true
end

def block_matches?(node, block)
return true if block.nil?
block.call node
end
end
end

0 comments on commit 97aaf5b

Please sign in to comment.