Initial Chef repository

This commit is contained in:
Greg Karékinian
2015-07-21 19:45:23 +02:00
parent 7e5401fc71
commit ee4079fa85
1151 changed files with 185163 additions and 0 deletions

View File

@@ -0,0 +1,78 @@
# Changelog
## v2.0.1
* Make the ChefspecHelpers helper a no-op if chefspec is not already loaded.
* Fix for finding the correct cookbook for a file when using vendored gems.
* New flag for the OptionCollector helper, `parser`:
```ruby
class Resource < Chef::Resource
include Poise
attribute(:options, option_collector: true, parser: proc {|val| parse(val) })
def parse(val)
{name: val}
end
end
```
* Fix for a possible infinite loop when using `ResourceProviderMixin` in a nested
module structure.
## v2.0.0
Major overhaul! Poise is now a Halite gem/cookbook. New helpers:
* ChefspecMatchers Automatically create Chefspec matchers for Poise resources.
* DefinedIn Track which file (and cookbook) a resource or provider is defined in.
* Fused Experimental support for defining provider actions in the resource class.
* Inversion Support for end-user dependency inversion with providers.
All helpers are compatible with Chef >= 12.0. Chef 11 is now deprecated, if you
need to support Chef 11 please continue to use Poise 1.
## v1.0.12
* Correctly propagate errors from inside notifying_block.
## v1.0.10
* Fixes an issue with the LWRPPolyfill helper and false values.
## v1.0.8
* Delayed notifications from nested converges will still only run at the end of
the main converge.
## v1.0.6
* The include_recipe helper now works correctly when used at compile time.
## v1.0.4
* Redeclaring a template attribute with the same name as a parent class will
inherit its options.
## v1.0.2
* New template attribute pattern.
```ruby
attribute(:config, template: true)
...
resource 'name' do
config_source 'template.erb'
end
...
new_resource.config_content
```
## v1.0.0
* Initial release!

198
cookbooks/poise/README.md Normal file
View File

@@ -0,0 +1,198 @@
# Poise
[![Build Status](https://img.shields.io/travis/poise/poise.svg)](https://travis-ci.org/poise/poise)
[![Gem Version](https://img.shields.io/gem/v/poise.svg)](https://rubygems.org/gems/poise)
[![Cookbook Version](https://img.shields.io/cookbook/v/poise.svg)](https://supermarket.chef.io/cookbooks/poise)
[![Coverage](https://img.shields.io/codecov/c/github/poise/poise.svg)](https://codecov.io/github/poise/poise)
[![Gemnasium](https://img.shields.io/gemnasium/poise/poise.svg)](https://gemnasium.com/poise/poise)
[![License](https://img.shields.io/badge/license-Apache_2-blue.svg)](https://www.apache.org/licenses/LICENSE-2.0)
## What is Poise?
The poise cookbook is a set of libraries for writing reusable cookbooks. It
providers helpers for common patterns and a standard structure to make it easier to create flexible cookbooks.
## Writing your first resource
Rather than LWRPs, Poise promotes the idea of using normal, or "heavy weight"
resources, while including helpers to reduce much of boilerplate needed for this. Each resource goes in its own file under `libraries/` named to match
the resource, which is in turn based on the class name. This means that the file `libraries/my_app.rb` would contain `Chef::Resource::MyApp` which maps to the resource `my_app`.
An example of a simple shell to start from:
```ruby
class Chef
class Resource::MyApp < Resource
include Poise
actions(:enable)
attribute(:path, kind_of: String)
... # Other attribute definitions
end
class Provider::MyApp < Provider
include Poise
def action_enable
converge_by("enable resource #{new_resource.name}") do
notifying_block do
... # Normal Chef recipe code goes here
end
end
end
end
end
```
Starting from the top, first we declare the resource class, which inherits from
`Chef::Resource`. This is similar to the `resources/` file in an LWRP, and a similar DSL can be used. In order to load the helpers into the class, we
include the `Poise` mixin. Then we use the familiar DSL, though with a few additions we'll cover later.
Then we declare the provider class, again similar to the `providers/` file in an LWRP. We include the `Poise` mixin again to get access to all the helpers. Rather than use the `action :enable do ... end` DSL from LWRPs, we just define the action method directly, and use the `converge_by` method to provide a description of what the action does. The implementation of action comes from a block of recipe code wrapped with `notifying_block` to capture changes in much the same way as `use_inline_resources`, see below for more information about all the features of `notifying_block`.
We can then use this resource like any other Chef resource:
```ruby
my_app 'one' do
path '/tmp'
end
```
## Helpers
While not exposed as a specific method, Poise will automatically set the
`resource_name` based on the class name.
### Notifying Block
As mentioned above, `notifying_block` is similar to `use_inline_resources` in LWRPs. Any Chef resource created inside the block will be converged in a sub-context and if any have updated it will trigger notifications on the current resource. Unlike `use_inline_resources`, resources inside the sub-context can still see resources outside of it, with lookups propagating up sub-contexts until a match is found. Also any delayed notifications are scheduled to run at the end of the main converge cycle, instead of the end of this inner converge.
This can be used to write action methods using the normal Chef recipe DSL, while still offering more flexibility through subclassing and other forms of code reuse.
### Include Recipe
In keeping with `notifying_block` to implement action methods using the Chef DSL, Poise adds an `include_recipe` helper to match the method of the same name in recipes. This will load and converge the requested recipe.
### Resource DSL
To make writing resource classes easier, Poise exposes a DSL similar to LWRPs for defining actions and attributes. Both `actions` and
`default_action` are just like in LWRPs, though `default_action` is rarely needed as the first action becomes the default. `attribute` is also available just like in LWRPs, but with some enhancements noted below.
One notable difference over the standard DSL method is that Poise attributes
can take a block argument.
#### Template Content
A common pattern with resources is to allow passing either a template filename or raw file content to be used in a configuration file. Poise exposes a new attribute flag to help with this behavior:
```ruby
attribute(:name, template: true)
```
This creates four methods on the class, `name_source`, `name_cookbook`,
`name_content`, and `name_options`. If the name is set to `''`, no prefix is applied to the function names. The content method can be set directly, but if not set and source is set, then it will render the template and return it as a string. Default values can also be set for any of these:
```ruby
attribute(:name, template: true, default_source: 'app.cfg.erb',
default_options: {host: 'localhost'})
```
As an example, you can replace this:
```ruby
if new_resource.source
template new_resource.path do
source new_resource.source
owner 'app'
group 'app'
variables new_resource.options
end
else
file new_resource.path do
content new_resource.content
owner 'app'
group 'app'
end
end
```
with simply:
```ruby
file new_resource.path do
content new_resource.content
owner 'app'
group 'app'
end
```
As the content method returns the rendered template as a string, this can also
be useful within other templates to build from partials.
#### Lazy Initializers
One issue with Poise-style resources is that when the class definition is executed, Chef hasn't loaded very far so things like the node object are not
yet available. This means setting defaults based on node attributes does not work directly:
```ruby
attribute(:path, default: node['myapp']['path'])
...
NameError: undefined local variable or method 'node'
```
To work around this, Poise extends the idea of lazy initializers from Chef recipes to work with resource definitions as well:
```ruby
attribute(:path, default: lazy { node['myapp']['path'] })
```
These initializers are run in the context of the resource object, allowing
complex default logic to be moved to a method if desired:
```ruby
attribute(:path, default: lazy { my_default_path })
def my_default_path
...
end
```
#### Option Collector
Another common pattern with resources is to need a set of key/value pairs for
configuration data or options. This can done with a simple Hash, but an option collector attribute can offer a nicer syntax:
```ruby
attribute(:mydata, option_collector: true)
...
my_app 'name' do
mydata do
key1 'value1'
key2 'value2'
end
end
```
This will be converted to `{key1: 'value1', key2: 'value2'}`. You can also pass a Hash to an option collector attribute just as you would with a normal attribute.
## Sponsors
The Poise test server infrastructure is generously sponsored by [Rackspace](https://rackspace.com/). Thanks Rackspace!
## License
Copyright 2013-2015, 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.

View File

@@ -0,0 +1,71 @@
#
# Copyright 2013-2015, 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/provider'
require 'chef/resource'
require 'poise/utils/resource_provider_mixin'
module Poise
include Poise::Utils::ResourceProviderMixin
autoload :Helpers, 'poise/helpers'
autoload :Provider, 'poise/provider'
autoload :Resource, 'poise/resource'
autoload :Subcontext, 'poise/subcontext'
autoload :Utils, 'poise/utils'
autoload :VERSION, 'poise/version'
end
# Callable form to allow passing in options:
# include Poise(ParentResource)
# include Poise(parent: ParentResource)
# include Poise(container: true)
def Poise(options={})
# Allow passing a class as a shortcut
if options.is_a?(Class)
options = {parent: options}
end
# Create a new anonymous module
mod = Module.new
# Fake the name.
mod.define_singleton_method(:name) do
super() || 'Poise'
end
mod.define_singleton_method(:included) do |klass|
super(klass)
# Pull in the main helper to cover most of the needed logic.
klass.class_exec { include Poise }
# Set the defined_in values as needed.
klass.poise_defined!(caller)
# Resource-specific options.
if klass < Chef::Resource
klass.poise_subresource(options[:parent], options[:parent_optional], options[:parent_auto]) if options[:parent]
klass.poise_subresource_container(options[:container_namespace]) if options[:container]
klass.poise_fused if options[:fused]
klass.poise_inversion(options[:inversion_options_resource]) if options[:inversion]
end
# Provider-specific options.
if klass < Chef::Provider
klass.poise_inversion(options[:inversion], options[:inversion_attribute]) if options[:inversion]
end
end
mod
end

View File

@@ -0,0 +1,24 @@
#
# Copyright 2015, 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.
#
module Poise
# Base exception class for Poise errors.
#
# @since 2.0.0
class Error < Exception
end
end

View File

@@ -0,0 +1,33 @@
#
# Copyright 2015, 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.
#
module Poise
module Helpers
autoload :ChefspecMatchers, 'poise/helpers/chefspec_matchers'
autoload :DefinedIn, 'poise/helpers/defined_in'
autoload :Fused, 'poise/helpers/fused'
autoload :IncludeRecipe, 'poise/helpers/include_recipe'
autoload :Inversion, 'poise/helpers/inversion'
autoload :LazyDefault, 'poise/helpers/lazy_default'
autoload :LWRPPolyfill, 'poise/helpers/lwrp_polyfill'
autoload :NotifyingBlock, 'poise/helpers/notifying_block'
autoload :OptionCollector, 'poise/helpers/option_collector'
autoload :ResourceName, 'poise/helpers/resource_name'
autoload :Subresources, 'poise/helpers/subresources'
autoload :TemplateContent, 'poise/helpers/template_content'
end
end

View File

@@ -0,0 +1,88 @@
#
# Copyright 2013-2015, 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.
#
# Not requiring chefspec or rspec/expectations since this code should only
# activate if they are already loaded.
require 'poise/helpers/lwrp_polyfill'
require 'poise/helpers/resource_name'
module Poise
module Helpers
# A resource mixin to register ChefSpec matchers for a resource
# automatically.
#
# If you are using the provides() form for naming resources, ensure that is
# set before declaring actions.
#
# @since 2.0.0
# @example Define a class
# class Chef::Resource::MyResource < Chef::Resource
# include Poise::Helpers::ChefspecMatchers
# actions(:run)
# end
# @example Use a matcher
# expect(chef_run).to run_my_resource('...')
module ChefspecMatchers
include Poise::Helpers::LWRPPolyfill::Resource
include Poise::Helpers::ResourceName
# Create a matcher for a given resource type and action. This is
# idempotent so if a matcher already exists, it will not be recreated.
#
# @api private
def self.create_matcher(resource, action)
# Check that we have everything we need.
return unless defined?(ChefSpec) && defined?(RSpec::Matchers) && resource
method = :"#{action}_#{resource}"
return if RSpec::Matchers.method_defined?(method)
RSpec::Matchers.send(:define_method, method) do |resource_name|
ChefSpec::Matchers::ResourceMatcher.new(resource, action, resource_name)
end
end
# @!classmethods
module ClassMethods
# Create a resource-level matcher for this resource.
#
# @see Resource::ResourceName.provides
def provides(name)
ChefSpec.define_matcher(name) if defined?(ChefSpec)
super
end
# Create matchers for all declared actions.
#
# @see Resource::LWRPPolyfill.actions
def actions(*names)
super.tap do |actions|
actions.each do |action|
ChefspecMatchers.create_matcher(resource_name, action)
end
end
end
def included(klass)
super
klass.extend ClassMethods
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,111 @@
#
# Copyright 2015, 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 'poise/error'
require 'poise/utils'
module Poise
module Helpers
# A mixin to track where a resource or provider was defined. This can
# provide either the filename of the class or the cookbook it was defined in.
#
# @since 2.0.0
# @example
# class MyProvider < Chef::provider
# include Poise::Helpers::DefinedIn
#
# def action_create
# template '...' do
# # ...
# cookbook new_resource.poise_defined_in
# end
# end
# end
module DefinedIn
# Wrapper for {.poise_defined_in_cookbook} to pass the run context for you.
#
# @see .poise_defined_in_cookbook
# @param file [String, nil] Optional file path to check instead of the path
# this class was defined in.
# @return [String]
def poise_defined_in_cookbook(file=nil)
self.class.poise_defined_in_cookbook(run_context, file)
end
# @!classmethods
module ClassMethods
# The file this class or module was defined in, or nil if it isn't found.
#
# @return [String]
def poise_defined_in
raise Poise::Error.new("Unable to determine location of #{self.name}") unless @poise_defined_in
@poise_defined_in
end
# The cookbook this class or module was defined in. Can pass a file to
# check that instead.
#
# @param run_context [Chef::RunContext] Run context to check cookbooks in.
# @param file [String, nil] Optional file path to check instead of the
# path this class was defined in.
# @return [String]
def poise_defined_in_cookbook(run_context, file=nil)
file ||= poise_defined_in
Chef::Log.debug("[#{self.name}] Checking cookbook name for #{file}")
Poise::Utils.find_cookbook_name(run_context, file).tap do |cookbook|
Chef::Log.debug("[#{self.name}] found cookbook #{cookbook.inspect}")
end
end
# Record that the class/module was defined. Called automatically by Ruby
# for all normal cases.
#
# @param caller_array [Array<String>] A strack trace returned by #caller.
# @return [void]
def poise_defined!(caller_array)
# Only try to set this once.
return if @poise_defined_in
# Path to ignore, assumes Halite transformation which I'm not thrilled
# about.
poise_libraries = File.expand_path('../..', __FILE__)
# Parse out just the filenames.
caller_array = caller_array.map {|line| line.split(/:/, 2).first }
# Find the first non-poise line.
caller_path = caller_array.find do |line|
!line.start_with?(poise_libraries)
end
Chef::Log.debug("[#{self.name}] Recording poise_defined_in as #{caller_path}")
@poise_defined_in = caller_path
end
# @api private
def inherited(klass)
super
klass.poise_defined!(caller)
end
def included(klass)
super
klass.extend(ClassMethods)
klass.poise_defined!(caller)
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,127 @@
#
# Copyright 2013-2015, 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/provider'
module Poise
module Helpers
# Resource mixin to create "fused" resources where the resource and provider
# are implemented in the same class.
#
# @since 2.0.0
# @example
# class Chef::Resource::MyResource < Chef::Resource
# include Poise(fused: true)
# attribute(:path, kind_of: String)
# attribute(:message, kind_of: String)
# action(:run) do
# file new_resource.path do
# content new_resource.message
# end
# end
# end
module Fused
# Hack is_a? so that the DSL will consider this a Provider for the
# purposes of attaching enclosing_provider.
#
# @api private
# @param klass [Class]
# @return [Boolean]
def is_a?(klass)
if klass == Chef::Provider
# Lies, damn lies, and Ruby code.
true
else
super
end
end
# Hack provider_for_action so that the resource is also the provider.
#
# @api private
# @param action [Symbol]
# @return [Chef::Provider]
def provider_for_action(action)
provider(self.class.fused_provider_class) unless provider
super
end
# @!classmethods
module ClassMethods
# Define a provider action. The block should contain the usual provider
# code.
#
# @param name [Symbol] Name of the action.
# @param block [Proc] Action implementation.
# @example
# action(:run) do
# file '/temp' do
# user 'root'
# content 'temp'
# end
# end
def action(name, &block)
fused_actions[name.to_sym] = block
# Make sure this action is allowed, also sets the default if first.
if respond_to?(:actions)
actions(name.to_sym)
end
end
# Storage accessor for fused action blocks. Maps action name to proc.
#
# @api private
# @return [Hash<Symbol, Proc>]
def fused_actions
(@fused_actions ||= {})
end
# Create a provider class for the fused actions in this resource.
# Inherits from the fused provider class of the resource's superclass if
# present.
#
# @api private
# @return [Class]
def fused_provider_class
@fused_provider_class ||= begin
provider_superclass = begin
self.superclass.fused_provider_class
rescue NoMethodError
Chef::Provider
end
actions = fused_actions
class_name = self.name
Class.new(provider_superclass) do
include Poise
define_singleton_method(:name) { class_name + ' (fused)' }
actions.each do |action, block|
define_method(:"action_#{action}", &block)
end
end
end
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,62 @@
#
# Copyright 2013-2015, 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 'poise/helpers/subcontext_block'
require 'poise/subcontext/runner'
module Poise
module Helpers
# A provider mixin to add #include_recipe that can be called from action
# methods.
#
# @since 2.0.0
module IncludeRecipe
include Poise::Helpers::SubcontextBlock
def include_recipe(*recipes)
loaded_recipes = []
subcontext = subcontext_block do
recipes.each do |recipe|
case recipe
when String
# Process normally
Chef::Log.debug("Loading recipe #{recipe} via include_recipe (poise)")
loaded_recipes += run_context.include_recipe(recipe)
when Proc
# Pretend its a block of recipe code
fake_recipe = Chef::Recipe.new(cookbook_name, new_resource.recipe_name, run_context)
fake_recipe.instance_eval(&recipe)
loaded_recipes << fake_recipe
end
end
end
# Converge the new context.
Poise::Subcontext::Runner.new(new_resource, subcontext).converge
collection = global_resource_collection
subcontext.resource_collection.each do |r|
Chef::Log.debug("Poise::IncludeRecipe: Adding #{r} to global collection #{collection.object_id}")
# Insert the local resource into the global context
collection.insert(r)
# Skip the iterator forward so we don't double-execute the inserted resource
# If running at compile time, the iterator is nil
collection.iterator.skip_forward if collection.iterator
end
loaded_recipes
end
end
end
end

View File

@@ -0,0 +1,374 @@
#
# Copyright 2015, 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/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)
provider_class = Poise::Helpers::Inversion.provider_for(resource_name, node, val)
Chef::Log.debug("[#{self}] Checking for an inversion provider for #{val}: #{provider_class && provider_class.name}")
val = provider_class if provider_class
end
super
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
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
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)
create_inversion_options_resource!(name) if inversion_options_resource
super 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 [Symbol]
# @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]
def inversion_resource(val=nil)
if val
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
@poise_inversion_resource || (superclass.respond_to?(:inversion_resource) ? superclass.inversion_resource : nil)
end
# @overload inversion_attribute()
# Return the inversion attribute name(s) for this class.
# @return [Array<String>]
# @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>]
def inversion_attribute(val=nil)
if val
# Coerce to an array of strings.
val = Array(val).map {|name| name.to_s }
@poise_inversion_attribute = val
end
@poise_inversion_attribute || (superclass.respond_to?(:inversion_attribute) ? superclass.inversion_attribute : nil)
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)
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)
# 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))
# 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]
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
return false unless resource.resource_name == inversion_resource
provider_name = resolve_inversion_provider(node, resource)
Chef::Log.debug("[#{resource}] Checking provides? on #{self.name}. Got provider_name #{provider_name.inspect}")
provider_name == provides.to_s || ( provider_name == 'auto' && 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

View File

@@ -0,0 +1,41 @@
#
# Copyright 2015, 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.
#
module Poise
module Helpers
module Inversion
# A mixin for inversion options providers.
#
# @api private
# @since 2.0.0
# @see Poise::Helper::Inversion
module OptionsProvider
# @api private
def self.included(klass)
klass.class_exec { include Poise }
end
# A blank run action.
#
# @return [void]
def action_run
# This space left intentionally blank.
end
end
end
end
end

View File

@@ -0,0 +1,101 @@
#
# Copyright 2015, 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/mash'
require 'poise/error'
module Poise
module Helpers
module Inversion
# A mixin for inversion options resources.
#
# @api private
# @since 2.0.0
# @see Poise::Helpers::Inversion
module OptionsResource
include Poise
# Method missing delegation to allow DSL-style options.
#
# @example
# my_app_options 'app' do
# key1 'value1'
# key2 'value2'
# end
def method_missing(method_sym, *args, &block)
super(method_sym, *args, &block)
rescue NoMethodError
# First time we've seen this key and using it as an rvalue, NOPE.GIF.
raise unless !args.empty? || block || _options[method_sym]
if !args.empty? || block
_options[method_sym] = block || args.first
end
_options[method_sym]
end
# Insert the options data in to the run state. This has to match the
# layout used in {Poise::Helpers::Inversion::Provider.inversion_options}.
#
# @api private
def after_created
raise Poise::Error.new("Inversion resource name not set for #{self.class.name}") unless self.class.inversion_resource
node.run_state['poise_inversion'] ||= {}
node.run_state['poise_inversion'][self.class.inversion_resource] ||= {}
node.run_state['poise_inversion'][self.class.inversion_resource][resource] ||= {}
node.run_state['poise_inversion'][self.class.inversion_resource][resource][for_provider] ||= {}
node.run_state['poise_inversion'][self.class.inversion_resource][resource][for_provider].update(_options)
end
module ClassMethods
# @overload inversion_resource()
# Return the inversion resource name for this class.
# @return [Symbol]
# @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]
def inversion_resource(val=nil)
if val
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
@poise_inversion_resource || (superclass.respond_to?(:inversion_resource) ? superclass.inversion_resource : nil)
end
# @api private
def included(klass)
super
klass.extend(ClassMethods)
klass.class_exec do
actions(:run)
attribute(:resource, kind_of: String, name_attribute: true)
attribute(:for_provider, kind_of: [String, Symbol], default: '*')
attribute(:_options, kind_of: Hash, default: lazy { Mash.new })
end
end
end
extend ClassMethods
end
end
end
end

View File

@@ -0,0 +1,62 @@
#
# Copyright 2013-2015, 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.
#
module Poise
module Helpers
# Resource mixin to allow lazyily-evaluated defaults in resource attributes.
# This is designed to be used with {LWRPPolyfill} or a similar #attributes
# method.
#
# @since 1.0.0
# @example
# class MyResource < Chef::Resource
# include Poise::Helpers::LWRPPolyfill
# include Poise::Helpers::LazyDefault
# attribute(:path, default: lazy { name + '_temp' })
# end
module LazyDefault
# Override the default set_or_return to support lazy evaluation of the
# default value. This only actually matters when it is called from a class
# level context via #attributes.
def set_or_return(symbol, arg, validation)
if validation && validation[:default].is_a?(Chef::DelayedEvaluator)
validation = validation.dup
validation[:default] = instance_eval(&validation[:default])
end
super(symbol, arg, validation)
end
# @!classmethods
module ClassMethods
# Create a lazyily-evaluated block.
#
# @param block [Proc] Callable to return the default value.
# @return [Chef::DelayedEvaluator]
def lazy(&block)
Chef::DelayedEvaluator.new(&block)
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,96 @@
#
# Copyright 2013-2015, 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 'poise/utils/resource_provider_mixin'
module Poise
module Helpers
# A resource and provider mixin to add back some compatability with Chef's
# LWRPBase classes.
#
# @since 1.0.0
module LWRPPolyfill
include Poise::Utils::ResourceProviderMixin
# Provide default_action and actions like LWRPBase but better equipped for subclassing.
module Resource
def initialize(*args)
super
# Try to not stomp on stuff if already set in a parent
@action = self.class.default_action if @action == :nothing
(@allowed_actions << self.class.actions).flatten!.uniq!
end
# @!classmethods
module ClassMethods
def default_action(name=nil)
if name
@default_action = name
actions(name)
end
@default_action || ( respond_to?(:superclass) && superclass.respond_to?(:default_action) && superclass.default_action ) || actions.first || :nothing
end
def actions(*names)
@actions ||= ( respond_to?(:superclass) && superclass.respond_to?(:actions) ? superclass.actions.dup : [] )
(@actions << names).flatten!.uniq!
@actions
end
def attribute(name, opts)
# Ruby 1.8 can go to hell
define_method(name) do |arg=nil, &block|
arg = block if arg.nil? # Try to allow passing either
set_or_return(name, arg, opts)
end
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
# Helper to handle load_current_resource for direct subclasses of Provider
module Provider
# @!classmethods
module ClassMethods
def included(klass)
super
klass.extend(ClassMethods)
# Mask Chef::Provider#load_current_resource because it throws NotImplementedError.
if klass.is_a?(Class) && klass.superclass == Chef::Provider
klass.class_exec do
def load_current_resource
end
end
end
# Reinstate the Chef DSL, removed in Chef 12.
klass.class_exec { include Chef::DSL::Recipe }
end
end
extend ClassMethods
end
end
end
end

View File

@@ -0,0 +1,78 @@
#
# Copyright 2013-2015, 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 'poise/helpers/subcontext_block'
require 'poise/subcontext/runner'
module Poise
module Helpers
# A provider mixin to provide #notifying_block, a scoped form of Chef's
# use_inline_resources.
#
# @since 1.0.0
# @example
# class MyProvider < Chef::Provider
# include Chef::Helpers::NotifyingBlock
#
# def action_run
# notifying_block do
# template '/etc/myapp.conf' do
# # ...
# end
# end
# end
# end
module NotifyingBlock
include Poise::Helpers::SubcontextBlock
private
# Create and converge a subcontext for the recipe DSL. This is similar to
# Chef's use_inline_resources but is scoped to a block. All DSL resources
# declared inside the block will be converged when the block returns, and
# the updated_by_last_action flag will be set if any of the inner
# resources are updated.
#
# @api public
# @param block [Proc] Block to run in the subcontext.
# @return [void]
# @example
# def action_run
# notifying_block do
# template '/etc/myapp.conf' do
# # ...
# end
# end
# end
def notifying_block(&block)
# Make sure to mark the resource as updated-by-last-action if
# any sub-run-context resources were updated (any actual
# actions taken against the system) during the
# sub-run-context convergence.
begin
subcontext = subcontext_block(&block)
# Converge the new context.
Poise::Subcontext::Runner.new(new_resource, subcontext).converge
ensure
new_resource.updated_by_last_action(
subcontext && subcontext.resource_collection.any?(&:updated?)
)
end
end
end
end
end

View File

@@ -0,0 +1,131 @@
#
# Copyright 2013-2015, 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/mash'
require 'poise/error'
module Poise
module Helpers
# A resource mixin to add a new kind of attribute, an option collector.
# These attributes can act as mini-DSLs for things which would otherwise be
# key/value pairs.
#
# @since 1.0.0
# @example Defining an option collector
# class MyResource < Chef::Resource
# include Poise::Helpers::OptionCollector
# attribute(:my_options, option_collector: true)
# end
# @example Using an option collector
# my_resource 'name' do
# my_options do
# key1 'value1'
# key2 'value2'
# end
# end
module OptionCollector
# Instance context used to eval option blocks.
# @api private
class OptionEvalContext
attr_reader :_options
def initialize(parent)
@parent = parent
@_options = {}
end
def method_missing(method_sym, *args, &block)
@parent.send(method_sym, *args, &block)
rescue NameError
# Even though method= in the block will set a variable instead of
# calling method_missing, still try to cope in case of self.method=.
method_sym = method_sym.to_s.chomp('=').to_sym
if args.length > 0 || block
@_options[method_sym] = args.first || block
elsif !@_options.include?(method_sym)
# We haven't seen this name before, re-raise the NameError.
raise
end
@_options[method_sym]
end
end
# @!classmethods
module ClassMethods
# Override the normal #attribute() method to support defining option
# collectors too.
def attribute(name, options={})
# If present but false-y, make sure it is removed anyway so it
# doesn't confuse ParamsValidate.
if options.delete(:option_collector)
option_collector_attribute(name, options)
else
super
end
end
# Define an option collector attribute. Normally used via {.attribute}.
#
# @param name [String, Symbol] Name of the attribute to define.
# @param default [Hash] Default value for the options.
# @param parser [Proc, Symbol] Optional parser method. If a symbol it is
# called as a method on self. Takes a non-hash value and returns a
# hash of its parsed representation.
def option_collector_attribute(name, default: {}, parser: nil)
raise Poise::Error.new("Parser must be a Proc or Symbol: #{parser.inspect}") if parser && !(parser.is_a?(Proc) || parser.is_a?(Symbol))
# Unlike LWRPBase.attribute, I don't care about Ruby 1.8. Worlds tiniest violin.
define_method(name.to_sym) do |arg=nil, &block|
iv_sym = :"@#{name}"
value = instance_variable_get(iv_sym) || begin
default = instance_eval(&default) if default.is_a?(Chef::DelayedEvaluator) # Handle lazy{}
Mash.new(default) # Wrap in a mash because fuck str vs sym.
end
if arg
if !arg.is_a?(Hash) && parser
arg = case parser
when Proc
instance_exec(arg, &parser)
when Symbol
send(parser, arg)
end
end
raise Exceptions::ValidationFailed, "Option #{name} must be a Hash" if !arg.is_a?(Hash)
# Should this and the update below be a deep merge?
value.update(arg)
end
if block
ctx = OptionEvalContext.new(self)
ctx.instance_exec(&block)
value.update(ctx._options)
end
instance_variable_set(iv_sym, value)
value
end
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,99 @@
#
# Copyright 2013-2015, 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/mixin/convert_to_class_name'
module Poise
module Helpers
# A resource mixin to automatically set @resource_name.
#
# @since 1.0.0
# @example
# class MyResource < Chef::Resource
# include Poise::Helpers::ResourceName
# provides(:my_resource)
# end
module ResourceName
def initialize(*args)
super
# If provides() was explicitly set, unconditionally set @resource_name.
# This helps when subclassing core Chef resources which set it
# themselves in #initialize.
if self.class.resource_name(false)
@resource_name = self.class.resource_name
else
@resource_name ||= self.class.resource_name
end
end
# @!classmethods
module ClassMethods
# Set the DSL name for the the resource class.
#
# @param name [Symbol] Name of the resource.
# @return [void]
# @example
# class MyResource < Chef::Resource
# include Poise::Resource::ResourceName
# provides(:my_resource)
# end
def provides(name)
# Patch self.constantize so this can cope with anonymous classes.
# This does require that the anonymous class define self.name though.
if self.name && respond_to?(:constantize)
old_constantize = instance_method(:constantize)
define_singleton_method(:constantize) do |const_name|
( const_name == self.name ) ? self : old_constantize.bind(self).call(const_name)
end
end
# Store the name for later.
@provides_name = name
# Call the original if present. The defined? is for old Chef.
super if defined?(super)
end
# Retreive the DSL name for the resource class. If not set explicitly
# via {provides} this will try to auto-detect based on the class name.
#
# @param auto [Boolean] Try to auto-detect based on class name.
# @return [Symbol]
def resource_name(auto=true)
return @provides_name if @provides_name
@provides_name || if name && name.start_with?('Chef::Resource')
Chef::Mixin::ConvertToClassName.convert_to_snake_case(name, 'Chef::Resource').to_sym
elsif name
Chef::Mixin::ConvertToClassName.convert_to_snake_case(name.split('::').last).to_sym
end
end
# Used by Resource#to_text to find the human name for the resource.
#
# @api private
def dsl_name
resource_name.to_s
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,58 @@
#
# Copyright 2013-2015, 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 'poise/subcontext/resource_collection'
module Poise
module Helpers
# A provider mixin to help with creating subcontexts. Mostly for internal
# use within Poise.
#
# @since 1.0.0
module SubcontextBlock
private
def subcontext_block(parent_context=nil, &block)
# Setup a sub-run-context.
parent_context ||= @run_context
sub_run_context = parent_context.dup
sub_run_context.resource_collection = Poise::Subcontext::ResourceCollection.new(parent_context.resource_collection)
# Declare sub-resources within the sub-run-context. Since they
# are declared here, they do not pollute the parent run-context.
begin
outer_run_context = @run_context
@run_context = sub_run_context
instance_eval(&block)
ensure
@run_context = outer_run_context
end
# Return the inner context to do other things with
sub_run_context
end
def global_resource_collection
collection = @run_context.resource_collection
while collection.respond_to?(:parent) && collection.parent
collection = collection.parent
end
collection
end
end
end
end

View File

@@ -0,0 +1,29 @@
#
# Copyright 2015, 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.
#
module Poise
module Helpers
# Mixins and helpers for managing subresources, resources with a
# parent/child relationship.
#
# @since 2.0.0
module Subresources
autoload :Child, 'poise/helpers/subresources/child'
autoload :Container, 'poise/helpers/subresources/container'
end
end
end

View File

@@ -0,0 +1,217 @@
#
# Copyright 2013-2015, 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/resource'
require 'poise/error'
require 'poise/helpers/subresources/default_containers'
module Poise
module Helpers
module Subresources
# A resource mixin for child subresources.
#
# @since 1.0.0
module Child
# Little class used to fix up the display of subresources in #to_text.
# Without this you get the full parent resource shown for @parent et al.
# @api private
class ParentRef
attr_accessor :resource
def initialize(resource)
@resource = resource
end
def to_text
if @resource.nil?
'nil'
else
@resource.to_s
end
end
end
# @overload parent()
# Get the parent resource for this child. This may be nil if the
# resource is set to parent_optional = true.
# @return [Chef::Resource, nil]
# @overload parent(val)
# Set the parent resource. The parent can be set as resource
# object, a string (either a bare resource name or a type[name]
# string), or a type:name hash.
# @param val [String, Hash, Chef::Resource] Parent resource to set.
# @return [Chef::Resource, nil]
def parent(val=nil)
_parent(:parent, self.class.parent_type, self.class.parent_optional, self.class.parent_auto, val)
end
# Register ourself with parents in case this is not a nested resource.
#
# @api private
def after_created
super
self.class.parent_attributes.each_key do |name|
parent = self.send(name)
parent.register_subresource(self) if parent
end
end
private
# Generic form of the parent getter/setter.
#
# @since 2.0.0
# @see #parent
def _parent(name, parent_type, parent_optional, parent_auto, val=nil)
# Allow using a DSL symbol as the parent type.
if parent_type.is_a?(Symbol)
parent_type = Chef::Resource.resource_for_node(parent_type, node)
end
# Grab the ivar for local use.
parent_ref = instance_variable_get(:"@#{name}")
if val
if val.is_a?(String) && !val.include?('[')
raise Poise::Error.new('Cannot use a string parent without defining a parent type') if parent_type == Chef::Resource
val = "#{parent_type.resource_name}[#{val}]"
end
if val.is_a?(String) || val.is_a?(Hash)
parent = @run_context.resource_collection.find(val)
else
parent = val
end
if !parent.is_a?(parent_type)
raise Poise::Error.new("Parent resource is not an instance of #{parent_type.name}: #{val.inspect}")
end
parent_ref = ParentRef.new(parent)
elsif !parent_ref
if parent_auto
# Automatic sibling lookup for sequential composition.
# Find the last instance of the parent class as the default parent.
# This is super flaky and should only be a last resort.
parent = Poise::Helpers::Subresources::DefaultContainers.find(parent_type, run_context)
end
# Can't find a valid parent, if it wasn't optional raise an error.
raise Poise::Error.new("No parent found for #{self}") unless parent || parent_optional
parent_ref = ParentRef.new(parent)
else
parent = parent_ref.resource
end
# Store the ivar back.
instance_variable_set(:"@#{name}", parent_ref)
# Return the actual resource.
parent
end
module ClassMethods
# @overload parent_type()
# Get the class of the default parent link on this resource.
# @return [Class, Symbol]
# @overload parent_type(type)
# Set the class of the default parent link on this resource.
# @param type [Class, Symbol] Class to set.
# @return [Class, Symbol]
def parent_type(type=nil)
if type
raise Poise::Error.new("Parent type must be a class, symbol, or true, got #{type.inspect}") unless type.is_a?(Class) || type.is_a?(Symbol) || type == true
@parent_type = type
end
@parent_type || (superclass.respond_to?(:parent_type) ? superclass.parent_type : Chef::Resource)
end
# @overload parent_optional()
# Get the optional mode for the default parent link on this resource.
# @return [Boolean]
# @overload parent_optional(val)
# Set the optional mode for the default parent link on this resource.
# @param val [Boolean] Mode to set.
# @return [Boolean]
def parent_optional(val=nil)
unless val.nil?
@parent_optional = val
end
if @parent_optional.nil?
superclass.respond_to?(:parent_optional) ? superclass.parent_optional : false
else
@parent_optional
end
end
# @overload parent_auto()
# Get the auto-detect mode for the default parent link on this resource.
# @return [Boolean]
# @overload parent_auto(val)
# Set the auto-detect mode for the default parent link on this resource.
# @param val [Boolean] Mode to set.
# @return [Boolean]
def parent_auto(val=nil)
unless val.nil?
@parent_auto = val
end
if @parent_auto.nil?
superclass.respond_to?(:parent_auto) ? superclass.parent_auto : true
else
@parent_auto
end
end
# Create a new kind of parent link.
#
# @since 2.0.0
# @param name [Symbol] Name of the relationship. This becomes a method
# name on the resource instance.
# @param type [Class] Class of the parent.
# @param optional [Boolean] If the parent is optional.
# @param auto [Boolean] If the parent is auto-detected.
# @return [void]
def parent_attribute(name, type: Chef::Resource, optional: false, auto: true)
name = :"parent_#{name}"
(@parent_attributes ||= {})[name] = type
define_method(name) do |val=nil|
_parent(name, type, optional, auto, val)
end
end
# Return the name of all parent relationships on this class.
#
# @since 2.0.0
# @return [Hash<Symbol, Class>]
def parent_attributes
{}.tap do |attrs|
# Grab superclass's attributes if possible.
attrs.update(superclass.parent_attributes) if superclass.respond_to?(:parent_attributes)
# Local default parent.
attrs[:parent] = parent_type
# Extra locally defined parents.
attrs.update(@parent_attributes) if @parent_attributes
# Remove anything with the type set to true.
attrs.reject! {|name, type| type == true }
end
end
# @api private
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end
end

View File

@@ -0,0 +1,165 @@
#
# Copyright 2013-2015, 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/dsl/recipe'
require 'poise/helpers/subcontext_block'
require 'poise/helpers/subresources/default_containers'
module Poise
module Helpers
module Subresources
# A resource mixin for subresource containers.
#
# @since 1.0.0
module Container
# A resource collection that has much more condensed text output. This
# is used to show the value of @subresources during Chef's error formatting.
# @api private
class NoPrintingResourceCollection < Chef::ResourceCollection
def to_text
"[#{all_resources.map(&:to_s).join(', ')}]"
end
end
include SubcontextBlock
include Chef::DSL::Recipe
attr_reader :subresources
def initialize(*args)
super
@subresources = NoPrintingResourceCollection.new
end
def after_created
super
# Register
Poise::Helpers::Subresources::DefaultContainers.register!(self, run_context)
unless @subresources.empty?
Chef::Log.debug("[#{self}] Adding subresources to collection:")
# Because after_create is run before adding the container to the resource collection
# we need to jump through some hoops to get it swapped into place.
self_ = self
order_fixer = Chef::Resource::RubyBlock.new('subresource_order_fixer', @run_context)
order_fixer.block do
collection = self_.run_context.resource_collection
# Delete the current container resource from its current position.
collection.all_resources.delete(self_)
# Replace the order fixer with the container so it runs before all
# subresources.
collection.all_resources[collection.iterator.position] = self_
# Hack for Chef 11 to reset the resources_by_name position too.
# @todo Remove this when I drop support for Chef 11.
if resources_by_name = collection.instance_variable_get(:@resources_by_name)
resources_by_name[self_.to_s] = collection.iterator.position
end
# Step back so we re-run the "current" resource, which is now the
# container.
collection.iterator.skip_back
end
@run_context.resource_collection.insert(order_fixer)
@subresources.each do |r|
Chef::Log.debug(" * #{r}")
@run_context.resource_collection.insert(r)
end
end
end
def declare_resource(type, name, created_at=nil, &block)
Chef::Log.debug("[#{self}] Creating subresource from #{type}(#{name})")
self_ = self
# Used to break block context, non-local return from subcontext_block.
resource = []
# Grab the caller so we can make the subresource look like it comes from
# correct place.
created_at ||= caller[0]
# Run this inside a subcontext to avoid adding to the current resource collection.
# It will end up added later, indirected via @subresources to ensure ordering.
subcontext_block do
namespace = if self.class.container_namespace == true
# If the value is true, use the name of the container resource.
self.name
elsif self.class.container_namespace.is_a?(Proc)
instance_eval(&self.class.container_namespace)
else
self.class.container_namespace
end
sub_name = if name && !name.empty?
if namespace
"#{namespace}::#{name}"
else
name
end
else
# If you pass in nil or '', you just get the namespace or parent name.
namespace || self.name
end
resource << super(type, sub_name, created_at) do
# Apply the correct parent before anything else so it is available
# in after_created for the subresource.
parent(self_) if respond_to?(:parent)
# Run the resource block.
instance_exec(&block) if block
end
end
# Try and add to subresources. For normal subresources this is handled
# in the after_created.
register_subresource(resource.first) if resource.first
# Return whatever we have
resource.first
end
def register_subresource(resource)
subresources.lookup(resource)
rescue Chef::Exceptions::ResourceNotFound
Chef::Log.debug("[#{self}] Adding #{resource} to subresources")
subresources.insert(resource)
end
private
# Thanks Array.flatten, big help you are. Specifically the
# method_missing in the recipe DSL will make a flatten on an array of
# resources fail, so make this safe.
def to_ary
nil
end
# @!classmethods
module ClassMethods
def container_namespace(val=nil)
@container_namespace = val unless val.nil?
if @container_namespace.nil?
# Not set here, look at the superclass of true by default for backwards compat.
superclass.respond_to?(:container_namespace) ? superclass.container_namespace : true
else
@container_namespace
end
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end
end

View File

@@ -0,0 +1,73 @@
#
# Copyright 2015, 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.
#
module Poise
module Helpers
module Subresources
# Helpers to track default container resources. This is used to find a
# default parent for a child with no parent set. It flat out violates
# encapsulation to allow for the use of default parents to act as
# system-level defaults even when created in a nested scope.
#
# @api private
# @since 2.0.0
module DefaultContainers
# Mutex to sync access to the containers array.
#
# @see .containers
CONTAINER_MUTEX = Mutex.new
# Add a resource to the array of default containers.
#
# @param resource [Chef::Resource] Resource to add.
# @param run_context [Chef::RunContext] Context of the current run.
# @return [void]
def self.register!(resource, run_context)
CONTAINER_MUTEX.synchronize do
containers(run_context) << resource
end
end
# Find a default container for a resource class.
#
# @param klass [Class] Resource class to search for.
# @param run_context [Chef::RunContext] Context of the current run.
# @return [Chef::Resource]
def self.find(klass, run_context)
CONTAINER_MUTEX.synchronize do
containers(run_context).reverse_each do |resource|
return resource if resource.is_a?(klass)
end
# Nothing found.
nil
end
end
private
# Get the array of all default container resources.
#
# @note MUST BE CALLED FROM A LOCKED CONTEXT!
# @param run_context [Chef::RunContext] Context of the current run.
# @return [Array<Chef::Resource>]
def self.containers(run_context)
run_context.node.run_state[:poise_default_containers] ||= []
end
end
end
end
end

View File

@@ -0,0 +1,165 @@
#
# Copyright 2013-2015, 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/provider/template_finder'
require 'chef/mixin/template'
require 'poise/helpers/lazy_default'
require 'poise/helpers/lwrp_polyfill'
require 'poise/helpers/option_collector'
require 'poise/utils'
module Poise
module Helpers
# A resource mixin to add a new kind of attribute, template content. TODO
#
# @since 1.0.0
module TemplateContent
include LazyDefault
include LWRPPolyfill
include OptionCollector
# @!classmethods
module ClassMethods
def attribute(name, options={})
if options.delete(:template)
name_prefix = name.empty? ? '' : "#{name}_"
# If you are reading this, I'm so sorry
# This is used for computing the default cookbook below
parent_filename = caller.first.reverse.split(':', 4).last.reverse
# If our parent class also declared a template_content attribute on the same name, inherit its options
if superclass.respond_to?("_#{name_prefix}_template_content_options")
options = superclass.send("_#{name_prefix}_template_content_options").merge(options)
end
# Template source path if using a template
attribute("#{name_prefix}source", kind_of: String)
define_method("_#{name_prefix}source") do
send("#{name_prefix}source") || maybe_eval(options[:default_source])
end
# Template cookbook name if using a template
attribute("#{name_prefix}cookbook", kind_of: [String, Symbol], default: lazy do
if send("#{name_prefix}source")
cookbook_name
elsif options[:default_cookbook]
maybe_eval(options[:default_cookbook])
else
Poise::Utils.find_cookbook_name(run_context, parent_filename)
end
end)
# Template variables if using a template
attribute("#{name_prefix}options", option_collector: true)
# The big one, get/set content, but if you are getting and no
# explicit content was given, try to render the template
define_method("#{name_prefix}content") do |arg=nil, no_compute=false|
ret = set_or_return("#{name_prefix}content", arg, kind_of: String)
if !ret && !arg && !no_compute
ret = send("_#{name_prefix}content")
# Cache the results for next time
set_or_return("#{name_prefix}content", ret, {}) if ret
end
ret
end
# Validate that arguments work
define_method("_#{name_prefix}validate") do
if options[:required] && !send("_#{name_prefix}source") && !send("#{name_prefix}content", nil, true)
raise Chef::Exceptions::ValidationFailed, "#{self}: One of #{name_prefix}source or #{name_prefix}content is required"
end
if send("#{name_prefix}source") && send("#{name_prefix}content", nil, true)
raise Chef::Exceptions::ValidationFailed, "#{self}: Only one of #{name_prefix}source or #{name_prefix}content can be specified"
end
end
# Monkey patch #after_create to run best-effort validation. Arguments
# could be changed after creation, but this gives nicer errors for
# most cases.
unless options[:no_validate_on_create]
old_after_created = instance_method(:after_created)
define_method(:after_created) do
old_after_created.bind(self).call
send("_#{name_prefix}validate")
end
end
# Compile the needed content
define_method("_#{name_prefix}content") do
# Run validation again
send("_#{name_prefix}validate")
# Get all the relevant parameters
content = send("#{name_prefix}content", nil, true)
source = send("_#{name_prefix}source")
if content
content # I don't think it can ever hit this branch
elsif source
cookbook = send("#{name_prefix}cookbook")
template_options = send("#{name_prefix}options")
send("_#{name_prefix}render_template", source, cookbook, template_options)
else
maybe_eval(options[:default])
end
end
# Actually render a template
define_method("_#{name_prefix}render_template") do |source, cookbook, template_options|
all_template_options = {}
all_template_options.update(maybe_eval(options[:default_options])) if options[:default_options]
all_template_options.update(template_options)
all_template_options[:new_resource] = self
finder = Chef::Provider::TemplateFinder.new(run_context, cookbook, node)
context = Chef::Mixin::Template::TemplateContext.new(all_template_options)
context[:node] = node
context[:template_finder] = finder
context.render_template(finder.find(source))
end
# Used to check if a parent class already defined a template_content thing here
define_singleton_method("_#{name_prefix}_template_content_options") do
options
end
else
super if defined?(super)
end
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
private
# Evaluate lazy blocks if needed
def maybe_eval(val)
if val.is_a?(Chef::DelayedEvaluator)
instance_eval(&val)
else
val
end
end
end
end
end

View File

@@ -0,0 +1,55 @@
#
# Copyright 2013-2015, 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 'poise/helpers'
module Poise
# Master provider mixin for Poise-based providers.
#
# @since 1.0.0
# @example Default helpers.
# class MyProvider < Chef::Provider
# include Poise::Provider
# end
# @example With optional helpers.
# class MyProvider < Chef::Provider
# include Poise::Provider
# poise_inversion(MyResource)
# end
module Provider
include Poise::Helpers::DefinedIn
include Poise::Helpers::IncludeRecipe
include Poise::Helpers::LWRPPolyfill
include Poise::Helpers::NotifyingBlock
# @!classmethods
module ClassMethods
def poise_inversion(resource, attribute=nil)
include Poise::Helpers::Inversion
inversion_resource(resource)
inversion_attribute(attribute) if attribute
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end

View File

@@ -0,0 +1,75 @@
#
# Copyright 2013-2015, 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 'poise/helpers'
module Poise
# Master resource mixin for Poise-based resources.
#
# @since 1.0.0
# @example Default helpers.
# class MyResource < Chef::Resource
# include Poise::Resource
# end
# @example With optional helpers.
# class MyResource < Chef::Resource
# include Poise::Resource
# poise_subresource(MyParent)
# poise_fused
# end
module Resource
include Poise::Helpers::ChefspecMatchers
include Poise::Helpers::DefinedIn
include Poise::Helpers::LazyDefault
include Poise::Helpers::LWRPPolyfill
include Poise::Helpers::OptionCollector
include Poise::Helpers::ResourceName
include Poise::Helpers::TemplateContent
# @!classmethods
module ClassMethods
def poise_subresource_container(namespace=nil)
include Poise::Helpers::Subresources::Container
# false is a valid value.
container_namespace(namespace) unless namespace.nil?
end
def poise_subresource(parent_type=nil, parent_optional=nil, parent_auto=nil)
include Poise::Helpers::Subresources::Child
parent_type(parent_type) if parent_type
parent_optional(parent_optional) unless parent_optional.nil?
parent_auto(parent_auto) unless parent_auto.nil?
end
def poise_fused
include Poise::Helpers::Fused
end
def poise_inversion(options_resource=nil)
include Poise::Helpers::Inversion
inversion_options_resource(true) unless options_resource == false
end
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end

View File

@@ -0,0 +1,27 @@
#
# Copyright 2015, 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.
#
module Poise
# Helpers and whatnot for dealing with subcontexts.
#
# @api private
# @since 2.0.0
module Subcontext
autoload :ResourceCollection, 'poise/subcontext/resource_collection'
autoload :Runner, 'poise/subcontext/runner'
end
end

View File

@@ -0,0 +1,56 @@
#
# Copyright 2013-2015, 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/resource_collection'
module Poise
module Subcontext
# A subclass of the normal Chef ResourceCollection that creates a partially
# isolated set of resources. Notifications and other resources lookups can
# propagate out to parent contexts but not back in. This is used to allow
# black-box resources that are still aware of things in upper contexts.
#
# @api private
# @since 1.0.0
class ResourceCollection < Chef::ResourceCollection
attr_accessor :parent
def initialize(parent)
@parent = parent
super()
end
def lookup(resource)
super
rescue Chef::Exceptions::ResourceNotFound
@parent.lookup(resource)
end
# Iterate and expand all nested contexts
def recursive_each(&block)
if @parent
if @parent.respond_to?(:recursive_each)
@parent.recursive_each(&block)
else
@parent.each(&block)
end
end
each(&block)
end
end
end
end

View File

@@ -0,0 +1,50 @@
#
# Copyright 2013-2015, 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/runner'
module Poise
module Subcontext
# A subclass of the normal Chef Runner that migrates delayed notifications
# to the enclosing run_context instead of running them at the end of the
# subcontext convergence.
#
# @api private
# @since 1.0.0
class Runner < Chef::Runner
def initialize(resource, *args)
super(*args)
@resource = resource
end
def run_delayed_notifications(error=nil)
# If there is an error, just do the normal thing. The return shouldn't
# ever fire because the superclass re-raises if there is an error.
return super if error
delayed_actions.each do |notification|
notifications = run_context.delayed_notifications(@resource)
if run_context.delayed_notifications(@resource).any? { |existing_notification| existing_notification.duplicates?(notification) }
Chef::Log.info( "#{@resource} not queuing delayed action #{notification.action} on #{notification.resource}"\
" (delayed), as it's already been queued")
else
notifications << notification
end
end
end
end
end
end

View File

@@ -0,0 +1,70 @@
#
# Copyright 2015, 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 'poise/error'
module Poise
module Utils
autoload :ResourceProviderMixin, 'poise/utils/resource_provider_mixin'
extend self
# Find the cookbook name for a given filename. The can used to find the
# cookbook that corresponds to a caller of a file.
#
# @param run_context [Chef::RunContext] Context to check.
# @param filename [String] Absolute filename to check for.
# @return [String]
# @example
# def my_thing
# caller_filename = caller.first.split(':').first
# cookbook = Poise::Utils.find_cookbook_name(run_context, caller_filename)
# # ...
# end
def find_cookbook_name(run_context, filename)
possibles = {}
Chef::Log.debug("[Poise] Checking cookbook for #{filename.inspect}")
run_context.cookbook_collection.each do |name, ver|
# This special method is added by Halite::Gem#as_cookbook_version.
if ver.respond_to?(:halite_root)
# The join is there because ../poise-ruby/lib starts with ../poise so
# we want a trailing /.
Chef::Log.debug("")
if filename.start_with?(File.join(ver.halite_root, ''))
Chef::Log.debug("[Poise] Found matching halite_root in #{name}: #{ver.halite_root.inspect}")
possibles[ver.halite_root] = name
end
else
Chef::CookbookVersion::COOKBOOK_SEGMENTS.each do |seg|
ver.segment_filenames(seg).each do |file|
# Put this behind an environment variable because it is verbose
# even for normal debugging-level output.
Chef::Log.debug("[Poise] Checking #{seg} in #{name}: #{file.inspect}") if ENV['POISE_DEBUG']
if file == filename
Chef::Log.debug("[Poise] Found matching #{seg} in #{name}: #{file.inspect}")
possibles[file] = name
end
end
end
end
end
raise Poise::Error.new("Unable to find cookbook for file #{filename.inspect}") if possibles.empty?
# Sort the items by matching path length, pick the name attached to the longest.
possibles.sort_by{|key, value| key.length }.last[1]
end
end
end

View File

@@ -0,0 +1,65 @@
#
# Copyright 2015, 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.
#
module Poise
module Utils
# A mixin to dispatch other mixins with resource and provider
# implementations. The module this is included in must have Resource and
# Provider sub-modules.
#
# @since 2.0.0
# @example
# module MyHelper
# include Poise::Utils::ResourceProviderMixin
# module Resource
# # ...
# end
#
# module Provider
# # ...
# end
# end
module ResourceProviderMixin
def self.included(klass)
# Warning here be dragons.
# Create a new anonymous module, klass will be the module that
# actually included ResourceProviderMixin. We want to keep a reference
# to that locked down so that we can close over it and use it in the
# "real" .included defined below to find the original relative consts.
mod = Module.new do
# Use define_method instead of def so we can close over klass and mod.
define_method(:included) do |inner_klass|
# Has to be explicit because super inside define_method.
super(inner_klass)
# Cargo this .included to things which include us.
inner_klass.extend(mod)
# Dispatch to submodules, inner_klass is the most recent includer.
if inner_klass < Chef::Resource
# Use klass::Resource to look up relative to the original module.
inner_klass.class_exec { include klass::Resource }
elsif inner_klass < Chef::Provider
# As above, klass::Provider.
inner_klass.class_exec { include klass::Provider }
end
end
end
# Add our .included to the original includer.
klass.extend(mod)
end
end
end
end

View File

@@ -0,0 +1,20 @@
#
# Copyright 2013-2015, 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.
#
module Poise
VERSION = '2.0.1'
end

View File

@@ -0,0 +1,18 @@
#
# Copyright 2013-2015, 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.
#
raise 'Halite is not compatible with no_lazy_load false, please set no_lazy_load true in your Chef configuration file.' unless Chef::Config[:no_lazy_load]
$LOAD_PATH << File.expand_path('../../files/halite_gem', __FILE__)

File diff suppressed because one or more lines are too long