Greg Karékinian a32f34b408 Vendor the external cookbooks
Knife-Zero doesn't include Berkshelf support, so vendoring everything in
the repo is convenient again
2019-10-13 19:17:42 +02:00

415 lines
17 KiB
Ruby

#
# Copyright 2015-2016, Noah Kantrowitz
#
# 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/node'
require 'chef/node_map'
require 'chef/provider'
require 'chef/resource'
require 'poise/backports'
require 'poise/helpers/defined_in'
require 'poise/error'
require 'poise/helpers/inversion/options_resource'
require 'poise/utils/resource_provider_mixin'
module Poise
module Helpers
# A mixin for dependency inversion in Chef.
#
# @since 2.0.0
module Inversion
autoload :OptionsResource, 'poise/helpers/inversion/options_resource'
autoload :OptionsProvider, 'poise/helpers/inversion/options_provider'
include Poise::Utils::ResourceProviderMixin
# Resource implementation for {Poise::Helpers::Inversion}.
# @see Poise::Helpers::Inversion
module Resource
# @overload options(val=nil)
# Set or return provider options for all providers.
# @param val [Hash] Provider options to set.
# @return [Hash]
# @example
# my_resource 'thing_one' do
# options depends: 'thing_two'
# end
# @overload options(provider, val=nil)
# Set or return provider options for a specific provider.
# @param provider [Symbol] Provider to set for.
# @param val [Hash] Provider options to set.
# @return [Hash]
# @example
# my_resource 'thing_one' do
# options :my_provider, depends: 'thing_two'
# end
def options(provider=nil, val=nil)
key = :options
if !val && provider.is_a?(Hash)
val = provider
elsif provider
key = :"options_#{provider}"
end
set_or_return(key, val ? Mash.new(val) : val, kind_of: Hash, default: lazy { Mash.new })
end
# Allow setting the provider directly using the same names as the attribute
# settings.
#
# @param val [String, Symbol, Class, nil] Value to set the provider to.
# @return [Class]
# @example
# my_resource 'thing_one' do
# provider :my_provider
# end
def provider(val=nil)
if val && !val.is_a?(Class)
resource_names = [resource_name]
# If subclass_providers! might be in play, check for those names too.
resource_names.concat(self.class.subclass_resource_equivalents) if self.class.respond_to?(:subclass_resource_equivalents)
# Silly ruby tricks to find the first provider that exists and no more.
provider_class = resource_names.lazy.map {|name| Poise::Helpers::Inversion.provider_for(name, node, val) }.select {|x| x }.first
Poise.debug("[#{self}] Checking for an inversion provider for #{val}: #{provider_class && provider_class.name}")
val = provider_class if provider_class
end
super
end
# Set or return the array of provider names to be blocked from
# auto-resolution.
#
# @param val [String, Array<String>] Value to set.
# @return [Array<String>]
def provider_no_auto(val=nil)
# Coerce to an array.
val = Array(val).map(&:to_s) if val
set_or_return(:provider_no_auto, val, kind_of: Array, default: [])
end
# @!classmethods
module ClassMethods
# Options resource class.
attr_reader :inversion_options_resource_class
# Options provider class.
attr_reader :inversion_options_provider_class
# @overload inversion_options_resource()
# Return the options resource mode for this class.
# @return [Boolean]
# @overload inversion_options_resource(val)
# Set the options resource mode for this class. Set to true to
# automatically create an options resource. Defaults to true.
# @param val [Boolean] Enable/disable setting.
# @return [Boolean]
def inversion_options_resource(val=nil)
@poise_inversion_options_resource = val unless val.nil?
@poise_inversion_options_resource
end
# Create resource and provider classes for an options resource.
#
# @param name [String, Symbol] DSL name for the base resource.
# @return [void]
def create_inversion_options_resource!(name)
enclosing_class = self
options_resource_name = :"#{name}_options"
# Create the resource class.
@inversion_options_resource_class = Class.new(Chef::Resource) do
include Poise::Helpers::Inversion::OptionsResource
define_singleton_method(:name) do
"#{enclosing_class}::OptionsResource"
end
define_singleton_method(:inversion_resource_class) do
enclosing_class
end
provides(options_resource_name)
inversion_resource(name)
end
# Create the provider class.
@inversion_options_provider_class = Class.new(Chef::Provider) do
include Poise::Helpers::Inversion::OptionsProvider
define_singleton_method(:name) do
"#{enclosing_class}::OptionsProvider"
end
define_singleton_method(:inversion_resource_class) do
enclosing_class
end
provides(options_resource_name)
end
end
# Wrap #provides() to create an options resource if desired.
#
# @param name [Symbol] Resource name
# return [void]
def provides(name, *args, &block)
create_inversion_options_resource!(name) if inversion_options_resource
super(name, *args, &block) if defined?(super)
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
# Provider implementation for {Poise::Helpers::Inversion}.
# @see Poise::Helpers::Inversion
module Provider
include DefinedIn
# Compile all the different levels of inversion options together.
#
# @return [Hash]
# @example
# def action_run
# if options['depends']
# # ...
# end
# end
def options
@options ||= self.class.inversion_options(node, new_resource)
end
# @!classmethods
module ClassMethods
# @overload inversion_resource()
# Return the inversion resource name for this class.
# @return [Symbo, nill]
# @overload inversion_resource(val)
# Set the inversion resource name for this class. You can pass either
# a symbol in DSL format or a resource class that uses Poise. This
# name is used to determine which resources the inversion provider is
# a candidate for.
# @param val [Symbol, Class] Name to set.
# @return [Symbol, nil]
def inversion_resource(val=Poise::NOT_PASSED)
if val != Poise::NOT_PASSED
val = val.resource_name if val.is_a?(Class)
Chef::Log.debug("[#{self.name}] Setting inversion resource to #{val}")
@poise_inversion_resource = val.to_sym
end
if defined?(@poise_inversion_resource)
@poise_inversion_resource
else
Poise::Utils.ancestor_send(self, :inversion_resource, default: nil)
end
end
# @overload inversion_attribute()
# Return the inversion attribute name(s) for this class.
# @return [Array<String>, nil]
# @overload inversion_attribute(val)
# Set the inversion attribute name(s) for this class. This is
# used by {.resolve_inversion_attribute} to load configuration data
# from node attributes. To specify a nested attribute pass an array
# of strings corresponding to the keys.
# @param val [String, Array<String>] Attribute path.
# @return [Array<String>, nil]
def inversion_attribute(val=Poise::NOT_PASSED)
if val != Poise::NOT_PASSED
# Coerce to an array of strings.
val = Array(val).map {|name| name.to_s }
@poise_inversion_attribute = val
end
if defined?(@poise_inversion_attribute)
@poise_inversion_attribute
else
Poise::Utils.ancestor_send(self, :inversion_attribute, default: nil)
end
end
# Default attribute paths to check for inversion options. Based on
# the cookbook this class and its superclasses are defined in.
#
# @param node [Chef::Node] Node to load from.
# @return [Array<Array<String>>]
def default_inversion_attributes(node)
klass = self
tried = []
while klass.respond_to?(:poise_defined_in_cookbook)
cookbook = klass.poise_defined_in_cookbook(node.run_context)
if node[cookbook]
return [cookbook]
end
tried << cookbook
klass = klass.superclass
end
raise Poise::Error.new("Unable to find inversion attributes, tried: #{tried.join(', ')}")
end
# Resolve the node attribute used as the base for inversion options
# for this class. This can be set explicitly with {.inversion_attribute}
# or the default is to use the name of the cookbook the provider is
# defined in.
#
# @param node [Chef::Node] Node to load from.
# @return [Chef::Node::Attribute]
def resolve_inversion_attribute(node)
# Default to using just the name of the cookbook.
attribute_names = inversion_attribute || default_inversion_attributes(node)
return {} if attribute_names.empty?
attribute_names.inject(node) do |memo, key|
memo[key] || begin
raise Poise::Error.new("Attribute #{key} not set when expanding inversion attribute for #{self.name}: #{memo}")
end
end
end
# Compile all the different levels of inversion options together.
#
# @param node [Chef::Node] Node to load from.
# @param resource [Chef::Resource] Resource to load from.
# @return [Hash]
def inversion_options(node, resource)
Mash.new.tap do |opts|
attrs = resolve_inversion_attribute(node)
# Cast the run state to a Mash because string vs. symbol keys. I can
# at least promise poise_inversion will be a str so cut down on the
# amount of data to convert.
run_state = Mash.new(node.run_state.fetch('poise_inversion', {}).fetch(inversion_resource, {}))[resource.name] || {}
# Class-level defaults.
opts.update(default_inversion_options(node, resource))
# Resource options for all providers.
opts.update(resource.options) if resource.respond_to?(:options)
# Global provider from node attributes.
opts.update(provider: attrs['provider']) if attrs['provider']
# Attribute options for all providers.
opts.update(attrs['options']) if attrs['options']
# Resource options for this provider.
opts.update(resource.options(provides)) if resource.respond_to?(:options)
# Attribute options for this resource name.
opts.update(attrs[resource.name]) if attrs[resource.name]
# Options resource options for all providers.
opts.update(run_state['*']) if run_state['*']
# Options resource options for this provider.
opts.update(run_state[provides]) if run_state[provides]
# Vomitdebug output for tracking down weirdness.
Poise.debug("[#{resource}] Resolved inversion options: #{opts.inspect}")
end
end
# Default options data for this provider class.
#
# @param node [Chef::Node] Node to load from.
# @param resource [Chef::Resource] Resource to load from.
# @return [Hash]
def default_inversion_options(node, resource)
{}
end
# Resolve which provider name should be used for a resource.
#
# @param node [Chef::Node] Node to load from.
# @param resource [Chef::Resource] Resource to query.
# @return [String]
def resolve_inversion_provider(node, resource)
inversion_options(node, resource)['provider'] || 'auto'
end
# Override the normal #provides to set the inversion provider name
# instead of adding to the normal provider map.
#
# @overload provides()
# Return the inversion provider name for the class.
# @return [Symbol]
# @overload provides(name, opts={}, &block)
# Set the inversion provider name for the class.
# @param name [Symbol] Provider name.
# @param opts [Hash] NodeMap filter options.
# @param block [Proc] NodeMap filter proc.
# @return [Symbol]
def provides(name=nil, opts={}, &block)
if name
raise Poise::Error.new("Inversion resource name not set for #{self.name}") unless inversion_resource
@poise_inversion_provider = name
Chef::Log.debug("[#{self.name}] Setting inversion provider name to #{name}")
Poise::Helpers::Inversion.provider_map(inversion_resource).set(name.to_sym, self, opts, &block)
# Set the actual Chef-level provides name for DSL dispatch.
super(inversion_resource)
end
@poise_inversion_provider
end
# Override the default #provides? to check for our inverted providers.
#
# @api private
# @param node [Chef::Node] Node to use for attribute checks.
# @param resource [Chef::Resource] Resource instance to match.
# @return [Boolean]
def provides?(node, resource)
raise Poise::Error.new("Inversion resource name not set for #{self.name}") unless inversion_resource
resource_name_equivalents = {resource.resource_name => true}
# If subclass_providers! might be in play, check for those names too.
if resource.class.respond_to?(:subclass_resource_equivalents)
resource.class.subclass_resource_equivalents.each do |name|
resource_name_equivalents[name] = true
end
end
return false unless resource_name_equivalents[inversion_resource]
provider_name = resolve_inversion_provider(node, resource).to_s
Poise.debug("[#{resource}] Checking provides? on #{self.name}. Got provider_name #{provider_name.inspect}")
provider_name == provides.to_s || ( provider_name == 'auto' && !resource.provider_no_auto.include?(provides.to_s) && provides_auto?(node, resource) )
end
# Subclass hook to provide auto-detection for providers.
#
# @param node [Chef::Node] Node to check against.
# @param resource [Chef::Resource] Resource to check against.
# @return [Boolean]
def provides_auto?(node, resource)
false
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
# The provider map for a given resource type.
#
# @param resource_type [Symbol] Resource type in DSL format.
# @return [Chef::NodeMap]
# @example
# Poise::Helpers::Inversion.provider_map(:my_resource)
def self.provider_map(resource_type)
@provider_maps ||= {}
@provider_maps[resource_type.to_sym] ||= Chef::NodeMap.new
end
# Find a specific provider class for a resource.
#
# @param resource_type [Symbol] Resource type in DSL format.
# @param node [Chef::Node] Node to use for the lookup.
# @param provider_type [Symbol] Provider type in DSL format.
# @return [Class]
# @example
# Poise::Helpers::Inversion.provider_for(:my_resource, node, :my_provider)
def self.provider_for(resource_type, node, provider_type)
provider_map(resource_type).get(node, provider_type.to_sym)
end
end
end
end