Vendor the external cookbooks

Knife-Zero doesn't include Berkshelf support, so vendoring everything in
the repo is convenient again
This commit is contained in:
Greg Karékinian
2019-10-13 19:17:42 +02:00
parent f4bfe31ac1
commit a32f34b408
1245 changed files with 100630 additions and 0 deletions

View File

@@ -0,0 +1,203 @@
# Changelog
## v2.8.2
* Remove support for Chef before 12.14.
* Fixed compatibility with Chef 14.3.
## v2.8.1
* Fix a missing `require` when using `subclass_providers!` on Chef 12.3.
## v2.8.0
* Chef 13 compatibility.
* Passing a symbol for the parent now works with the `include Poise(:name)`
shortcut.
* Fixed `subclass_providers!` on older versions of Chef.
## v2.7.2
* Test harness fixes for Chef.
## v2.7.1
* Minor tweak for compatability with Chef master.
## v2.7.0
* More compatibility improvements for Chef 12.9.
* New helper: `Poise::Helpers::Win32User` to automatically convert `'root'`
defaults for user and group properties to more platform-appropriate values.
* Enhanced `poise_shell_out` to better cope with Windows command parsing. Use
Bash-style commands and it will automatically convert.
* Overall compatibility fixes for Windows.
## v2.6.1
* Compatibility with Chef master to fix issues with `defined_in!` not ignoring
stack frames from Chef code.
* Setting a provider in a inversion options resource now works as (probably)
expected.
## v2.6.0
* New backwards-compatibility helper: `Poise::Backports::VERIFY_PATH`. Use it
like `verify "myapp -t #{Poise::Backports::VERIFY_PATH}" if defined?(verify)`
for backwards-compatible usage of file verifications.
* Fixed Poise's implementation of lazy defaults to more closely match Chef's
even when both are used in conjunction. Lazy defaults will no longer be
evaluated when setting a value or getting an existing non-default value.
## v2.5.0
* New property for inversion resources: `provider_no_auto`. Set one or more
provider names that will be ignored for automatic resolution for that instance.
* Support `variables` as an alias for `options` in template content properties
to match the `template` resource.
* Template content properties are no longer validated after creation for
non-default actions.
* Formalize the extra-verbose logging mode for Poise and expose it via helpers.
* Extra-verbose logging mode can now be enabled by creating a `/poise_debug` file.
* New helper: `poise_shell_out`. Like normal `shell_out` but sets group and
environment variables automatically to better defaults.
## v2.4.0
* Added return value to `Container#register_subresource` to track if the resource
was already added.
* Improve inspect output for subresources and containers.
* Ensure notifications work with subresources.
* Inversion providers process name equivalences.
## v2.3.2
* Improve handling of deeply nested subresources.
## v2.3.1
* Ensure a container with a parent link to its own type doesn't use self as the
default parent.
* Improve handling of `load_current_resource` in providers that call it via
`super`.
## v2.3.0
* New helper: `ResourceSubclass`, a helper for subclassing a resource while
still using the providers as the base class.
* New feature: Non-default containers. Use `container_default: false` to mark
a container class as ineligible for default lookup.
* New feature: parent attribute defaults. You can set a `parent_default` to
provide a default value for the parent of a resource. This supports the
`lazy { }` helper as with normal default values.
* New feature: use `forced_keys: [:name]` on an option collector property to
force keys that would otherwise be clobbered by resource methods.
* Can enable verbose logging mode via a node attribute in addition to an
environment variable.
## v2.2.3
* Add `ancestor_send` utility method for use in other helpers.
* Improve subresource support for use in mixins.
## v2.2.2
* Fix 2.2.1 for older versions of Chef.
## v2.2.1
* Fixed delayed notifications inside `notifying_block`.
* Default actions as expected within LWRPs.
## v2.2.0
* Compatibility with Chef 12.4.1 and Chefspec 4.3.0.
* New helper `ResourceCloning`: Disables resource cloning between Poise-based
resources. This is enabled by default.
* Subresource parent references can be set to nil.
## v2.1.0
* Compatibility with Chef 12.4.
* Add `#property` as an alias for `#attribute` in resources. This provides
forward compatibility with future versions of Chef.
* Freeze default resource attribute values. **This may break your code**,
however this is not a major release because any code broken by this change
was itself already a bug.
## 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!

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

@@ -0,0 +1,233 @@
# 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
provides 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
require 'poise'
require 'chef/resource'
require 'chef/provider'
module MyApp
class Resource < Chef::Resource
include Poise
provides(:my_app)
actions(:enable)
attribute(:path, kind_of: String)
# Other attribute definitions.
end
class Provider < Chef::Provider
include Poise
provides(:my_app)
def action_enable
notifying_block do
... # Normal Chef recipe code goes here
end
end
end
end
```
Starting from the top, first we require the libraries we will be using. Then we
create a module to hold our resource and provider. If your cookbook declares
multiple resources and/or providers, you might want additional nesting here.
Then 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.
We then include the `Poise` mixin to load our helpers, and then call
`provides(:my_app)` to tell Chef this class will implement the `my_app`
resource. 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 and
call `provides()` to tell Chef what provider this is. Rather than use the
`action :enable do ... end` DSL from LWRPs, we just define the action method
directly. 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.
## Debugging Poise
Poise has its own extra-verbose level of debug logging that can be enabled in
three different ways. You can either set the environment variable `$POISE_DEBUG`,
set a node attribute `node['POISE_DEBUG']`, or touch the file `/POISE_DEBUG`.
You will see a log message `Extra verbose logging enabled` at the start of the
run to confirm Poise debugging has been enabled. Make sure you also set Chef's
log level to `debug`, usually via `-l debug` on the command line.
## Upgrading from Poise 1.x
The biggest change when upgrading from Poise 1.0 is that the mixin is no longer
loaded automatically. You must add `require 'poise'` to your code is you want to
load it, as you would with normal Ruby code outside of Chef. It is also highly
recommended to add `provides(:name)` calls to your resources and providers, this
will be required in Chef 13 and will display a deprecation warning if you do
not. This also means you can move your code out of the `Chef` module namespace
and instead declare it in your own namespace. An example of this is shown above.
## Sponsors
The Poise test server infrastructure is generously sponsored by [Rackspace](https://rackspace.com/). Thanks Rackspace!
## License
Copyright 2013-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.

View File

@@ -0,0 +1,108 @@
#
# Copyright 2013-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/provider'
require 'chef/resource'
require 'chef/run_context'
require 'poise/utils/resource_provider_mixin'
module Poise
include Poise::Utils::ResourceProviderMixin
autoload :Backports, 'poise/backports'
autoload :Helpers, 'poise/helpers'
autoload :NOT_PASSED, 'poise/backports/not_passed'
autoload :Provider, 'poise/provider'
autoload :Resource, 'poise/resource'
autoload :Subcontext, 'poise/subcontext'
autoload :Utils, 'poise/utils'
autoload :VERSION, 'poise/version'
# Check if Poise's extra debugging output is enabled. This produces a *lot*
# of logging.
#
# @param node [Chef::Node, Chef::RunContext] Optional node to check for
# attributes. If not given, Chef.node is used instead.
# @return [Boolean]
def self.debug?(node=nil)
node = node.node if node.is_a?(Chef::RunContext)
node ||= Chef.node if defined?(Chef.node)
@debug_file_upper = ::File.exist?('/POISE_DEBUG') unless defined?(@debug_file_upper)
@debug_file_lower = ::File.exist?('/poise_debug') unless defined?(@debug_file_lower)
!!(
(ENV['POISE_DEBUG'] && ENV['POISE_DEBUG'] != 'false') ||
(ENV['poise_debug'] && ENV['poise_debug'] != 'false') ||
(node && node['POISE_DEBUG']) ||
(node && node['poise_debug']) ||
@debug_file_upper ||
@debug_file_lower
)
end
# Log a message only if Poise's extra debugging output is enabled.
#
# @see #debug?
# @param msg [String] Log message.
# @return [void]
def self.debug(msg)
Chef::Log.debug(msg) if debug?
end
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.is_a?(Symbol)
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], options[:container_default]) 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
# Display a message if poise_debug is enabled. Off in ChefSpec so I don't get
# extra logging stuff that I don't care about.
Poise.debug('[Poise] Extra verbose logging enabled') unless defined?(ChefSpec)

View File

@@ -0,0 +1,28 @@
#
# 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.
#
module Poise
# Backported features from Chef to be able to use them with older versions.
#
# @since 2.3.0
module Backports
autoload :NOT_PASSED, 'poise/backports/not_passed'
autoload :VERIFY_PATH, 'poise/backports/verify_path'
end
autoload :NOT_PASSED, 'poise/backports/not_passed'
end

View File

@@ -0,0 +1,52 @@
#
# 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.
#
begin
require 'chef/constants'
rescue LoadError
# This space left intentionally blank.
end
module Poise
module Backports
# A sentinel value for optional arguments where nil is a valid value.
# @since 2.3.0
# @!parse NOT_PASSED = Object.new
NOT_PASSED = if defined?(Chef::NOT_PASSED)
Chef::NOT_PASSED
else
# Copyright 2015-2016, Chef Software Inc.
# Used under Apache License, Version 2.0.
Object.new.tap do |not_passed|
def not_passed.to_s
"NOT_PASSED"
end
def not_passed.inspect
to_s
end
not_passed.freeze
end
end
end
# An alias to {Backports::NOT_PASSED} to avoid typing so much.
#
# @since 2.3.0
# @see Backports::NOT_PASSED
NOT_PASSED = Backports::NOT_PASSED
end

View File

@@ -0,0 +1,33 @@
#
# 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.
#
module Poise
module Backports
# The correct interpolation key for any version of Chef.
# @since 2.6.0
# @example
# file '/path' do
# content my_content
# verify "myapp -t #{Poise::Backports::VERIFY_PATH}"
# end
VERIFY_PATH = if Gem::Version.create(Chef::VERSION) < Gem::Version.create('12.5.0')
'%{file}'
else
'%{path}'
end
end
end

View File

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

View File

@@ -0,0 +1,36 @@
#
# 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.
#
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 :ResourceCloning, 'poise/helpers/resource_cloning'
autoload :ResourceName, 'poise/helpers/resource_name'
autoload :ResourceSubclass, 'poise/helpers/resource_subclass'
autoload :Subresources, 'poise/helpers/subresources'
autoload :TemplateContent, 'poise/helpers/template_content'
autoload :Win32User, 'poise/helpers/win32_user'
end
end

View File

@@ -0,0 +1,92 @@
#
# Copyright 2013-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.
#
# 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, *args, &block)
super(name, *args, &block)
ChefSpec.define_matcher(name) if defined?(ChefSpec)
# Call #actions here to grab any actions from a parent class.
actions.each do |action|
ChefspecMatchers.create_matcher(name, action)
end
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 if resource_name && resource_name != :resource && !names.empty?
end
end
def included(klass)
super
klass.extend ClassMethods
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,129 @@
#
# 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 'poise'
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
# Path to the root of Poise's code.
# @see #poise_defined!
# @api private
POISE_LIB_ROOT = ::File.expand_path('../..', __FILE__)
# Path to the root of Chef's code.
# @see #poise_defined!
# @api private
CHEF_LIB_ROOT = ::File.join(::Gem::Specification.find_by_name('chef').gem_dir, 'lib')
# A regex used to parse Ruby's `caller` string syntax.
# @see #poise_defined!
# @api private
CALLER_REGEXP = /^(.+):\d+:in `.+'/
# 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
Poise.debug("[#{self.name}] Checking cookbook name for #{file}")
Poise::Utils.find_cookbook_name(run_context, file).tap do |cookbook|
Poise.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
# Parse out just the filenames.
caller_paths = caller_array.map {|line| line[CALLER_REGEXP, 1] }
# Find the first non-poise, non-chef line. This assumes Halite
# transformation which I'm not thrilled about.
caller_path = caller_paths.find do |line|
line && !line.start_with?(POISE_LIB_ROOT) && !line.start_with?(CHEF_LIB_ROOT)
end
raise Poise::Error.new("Unable to find a caller path for: #{caller_array.inspect}") unless caller_path
if ::File::ALT_SEPARATOR
caller_path.gsub!(::File::ALT_SEPARATOR, ::File::SEPARATOR)
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-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/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-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 '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.flatten.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,414 @@
#
# 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

View File

@@ -0,0 +1,41 @@
#
# 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.
#
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,115 @@
#
# 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/mash'
require 'poise/backports'
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
# Capture setting the provider and make it Do What I Mean. This does
# mean you can't set the actual provider for the options resource, which
# is fine because the provider is a no-op.
#
# @api private
def provider(val=Poise::NOT_PASSED)
if val == Poise::NOT_PASSED
super()
else
_options[:provider] = val
end
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,79 @@
#
# Copyright 2013-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/version'
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
# Check if this version of Chef already supports lazy defaults. This is
# true for Chef 12.5+.
#
# @since 2.0.3
# @api private
# @return [Boolean]
def self.needs_polyfill?
@needs_polyfill ||= Gem::Requirement.new('< 12.5.pre').satisfied_by?(Gem::Version.new(Chef::VERSION))
end
# 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 LazyDefault.needs_polyfill? && validation && validation[:default].is_a?(Chef::DelayedEvaluator)
validation = validation.dup
if (arg.nil? || arg == Poise::NOT_PASSED) && (!instance_variable_defined?(:"@#{symbol}") || instance_variable_get(:"@#{symbol}").nil?)
validation[:default] = instance_eval(&validation[:default])
else
# Clear the default.
validation.delete(:default)
end
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,163 @@
#
# Copyright 2013-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/resource'
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. Coerce @action
# to an array because this behavior may change in the future in Chef.
@action = self.class.default_action if Array(@action) == [:nothing]
(@allowed_actions << self.class.actions).flatten!.uniq!
end
module ClassMethods
# @overload default_action()
# Get the default action for this resource class. If no explicit
# default is set, the first action in the list will be used.
# @see #actions
# @return [Array<Symbol>]
# @overload default_action(name)
# Set the default action for this resource class. If this action is
# not already allowed, it will be added.
# @note It is idiomatic to use {#actions} instead, with the first
# action specified being the default.
# @param name [Symbol, Array<Symbol>] Name of the action(s).
# @return [Array<Symbol>]
# @example
# class MyApp < Chef::Resource
# include Poise
# default_action(:install)
# end
def default_action(name=nil)
if name
name = Array(name).flatten.map(&:to_sym)
@default_action = name
actions(*name)
end
if @default_action
@default_action
elsif respond_to?(:superclass) && superclass != Chef::Resource && superclass.respond_to?(:default_action) && superclass.default_action && Array(superclass.default_action) != %i{nothing}
superclass.default_action
elsif first_non_nothing = actions.find {|action| action != :nothing }
[first_non_nothing]
else
%i{nothing}
end
end
# @overload actions()
# Get all actions allowed for this resource class. This includes
# any actions allowed on parent classes.
# @return [Array<Symbol>]
# @overload actions(*names)
# Set actions as allowed for this resource class. These must
# correspond with action methods in the provider class(es).
# @param names [Array<Symbol>] One or more actions to set.
# @return [Array<Symbol>]
# @example
# class MyApp < Chef::Resource
# include Poise
# actions(:install, :uninstall)
# end
def actions(*names)
@actions ||= ( respond_to?(:superclass) && superclass.respond_to?(:actions) && superclass.actions.dup ) || ( respond_to?(:superclass) && superclass != Chef::Resource && superclass.respond_to?(:allowed_actions) && superclass.allowed_actions.dup ) || []
(@actions << names).tap {|actions| actions.flatten!; actions.uniq! }
end
# Create a resource property (née attribute) on this resource class.
# This follows the same usage as the helper of the same name in Chef
# LWRPs.
#
# @param name [Symbol] Name of the property.
# @param opts [Hash<Symbol, Object>] Validation options and flags.
# @return [void]
# @example
# class MyApp < Chef::Resource
# include Poise
# attribute(:path, name_attribute: true)
# attribute(:port, kind_of: Integer, default: 8080)
# end
def attribute(name, opts={})
# Freeze the default value. This is done upstream too in Chef 12.5+.
opts[:default].freeze if opts && opts[:default]
# 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
# For forward compat with Chef 12.5+.
alias_method :property, :attribute
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
module LoadCurrentResource
def load_current_resource
@current_resource = if new_resource
new_resource.class.new(new_resource.name, run_context)
else
# Better than nothing, subclass can overwrite anyway.
Chef::Resource.new(nil, run_context)
end
end
end
# @!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.send(:include, LoadCurrentResource)
end
# Reinstate the Chef DSL, removed in Chef 12.
klass.send(:include, Chef::DSL::Recipe)
end
end
extend ClassMethods
end
end
end
end

View File

@@ -0,0 +1,78 @@
#
# Copyright 2013-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 '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,144 @@
#
# Copyright 2013-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/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, forced_keys)
@parent = parent
@forced_keys = forced_keys
@_options = {}
end
def method_missing(method_sym, *args, &block)
# Deal with forced keys.
if @forced_keys.include?(method_sym)
@_options[method_sym] = args.first || block if !args.empty? || block
return @_options[method_sym]
end
# Try the resource context.
@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.empty? || 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.
# @param forced_keys [Array<Symbol>, Set<Symbol>] Method names that will be forced
# to be options rather than calls to the parent resource.
def option_collector_attribute(name, default: {}, parser: nil, forced_keys: Set.new)
raise Poise::Error.new("Parser must be a Proc or Symbol: #{parser.inspect}") if parser && !(parser.is_a?(Proc) || parser.is_a?(Symbol))
# Cast to a set at definition time.
forced_keys = Set.new(forced_keys) unless forced_keys.is_a?(Set)
# Never allow name to be called accidentally since it does really wonky things.
forced_keys.add(:name)
# 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, forced_keys)
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,72 @@
#
# Copyright 2013-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.
#
module Poise
module Helpers
# A resource mixin to disable resource cloning.
#
# @since 2.2.0
# @example
# class MyResource < Chef::Resource
# include Poise::Helpers::ResourceCloning
# end
module ResourceCloning
# Override to disable resource cloning on Chef 12.0.
#
# @api private
def load_prior_resource(*args)
# Do nothing.
end
# Override to disable resource cloning on Chef 12.1+.
#
# @api private
def load_from(*args)
# Do nothing.
end
# Monkeypatch for Chef::ResourceBuilder to silence the warning if needed.
#
# @api private
module ResourceBuilderPatch
# @api private
def self.install!
begin
require 'chef/resource_builder'
Chef::ResourceBuilder.send(:prepend, ResourceBuilderPatch)
rescue LoadError
# For 12.0, this is already taken care of.
end
end
# @api private
def emit_cloned_resource_warning
super unless resource.is_a?(ResourceCloning)
end
# @api private
def emit_harmless_cloning_debug
super unless resource.is_a?(ResourceCloning)
end
end
# Install the patch.
ResourceBuilderPatch.install!
end
end
end

View File

@@ -0,0 +1,107 @@
#
# Copyright 2013-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/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, *args, &block)
# 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(name, *args, &block) 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)
# In 12.4+ we need to proxy through the super class for setting.
return super(auto) if defined?(super) && (auto.is_a?(Symbol) || auto.is_a?(String))
return @provides_name unless auto
@provides_name || if name
mode = if name.start_with?('Chef::Resource')
[name, 'Chef::Resource']
else
[name.split('::').last]
end
Chef::Mixin::ConvertToClassName.convert_to_snake_case(*mode).to_sym
elsif defined?(super)
# No name on 12.4+ probably means this is an LWRP, use super().
super()
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,93 @@
#
# 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.
#
begin
require 'chef/chef_class'
rescue LoadError
# This space left intentionally blank, fallback is below.
end
require 'poise/error'
require 'poise/helpers/resource_name'
module Poise
module Helpers
# A resource mixin to help subclass existing resources.
#
# @since 2.3.0
module ResourceSubclass
include ResourceName
module ClassMethods
def subclass_providers!(superclass_resource_name=nil, resource_name: nil)
resource_name ||= self.resource_name
superclass_resource_name ||= if superclass.respond_to?(:resource_name)
superclass.resource_name
elsif superclass.respond_to?(:dsl_name)
superclass.dsl_name
else
raise Poise::Error.new("Unable to determine superclass resource name for #{superclass}. Please specify name manually via subclass_providers!('name').")
end.to_sym
# Deal with the node maps.
node_maps = {}
node_maps['handler map'] = Chef.provider_handler_map if defined?(Chef.provider_handler_map)
node_maps['priority map'] = if defined?(Chef.provider_priority_map)
Chef.provider_priority_map
else
require 'chef/platform/provider_priority_map'
Chef::Platform::ProviderPriorityMap.instance.send(:priority_map)
end
# Patch anything in the descendants tracker.
Chef::Provider.descendants.each do |provider|
node_maps["#{provider} node map"] = provider.node_map if defined?(provider.node_map)
end if defined?(Chef::Provider.descendants)
node_maps.each do |map_name, node_map|
map = node_map.respond_to?(:map, true) ? node_map.send(:map) : node_map.instance_variable_get(:@map)
if map.include?(superclass_resource_name)
Chef::Log.debug("[#{self}] Copying provider mapping in #{map_name} from #{superclass_resource_name} to #{resource_name}")
map[resource_name] = map[superclass_resource_name].dup
end
end
# Add any needed equivalent names.
if superclass.respond_to?(:subclass_resource_equivalents)
subclass_resource_equivalents.concat(superclass.subclass_resource_equivalents)
else
subclass_resource_equivalents << superclass_resource_name
end
subclass_resource_equivalents.uniq!
end
# An array of names for the resources this class is equivalent to for
# the purposes of provider resolution.
#
# @return [Array<Symbol>]
def subclass_resource_equivalents
@subclass_resource_names ||= [resource_name.to_sym]
end
# @api private
def included(klass)
super
klass.extend(ClassMethods)
end
end
extend ClassMethods
end
end
end

View File

@@ -0,0 +1,72 @@
#
# Copyright 2013-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 '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 subcontext.
parent_context ||= @run_context
sub_run_context = parent_context.dup
# Reset state for the subcontext. In 12.4+ this uses the built-in
# support, otherwise do it manually.
if defined?(sub_run_context.initialize_child_state)
sub_run_context.initialize_child_state
else
# Audits was added in 12.1 I think.
sub_run_context.audits = {} if defined?(sub_run_context.audits)
# Dup and clear to preserve the default behavior without copy-pasta.
sub_run_context.immediate_notification_collection = parent_context.immediate_notification_collection.dup.clear
sub_run_context.delayed_notification_collection = parent_context.delayed_notification_collection.dup.clear
end
# Create the subcollection.
sub_run_context.resource_collection = Poise::Subcontext::ResourceCollection.new(parent_context.resource_collection)
# Create an accessor for the parent run context.
sub_run_context.define_singleton_method(:parent_run_context) { parent_context }
# 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-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.
#
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,276 @@
#
# Copyright 2013-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/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 inspect
to_text
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(*args)
# Lie about this method if the parent type is true.
if self.class.parent_type == true
raise NoMethodError.new("undefined method `parent' for #{self}")
end
_parent(:parent, self.class.parent_type, self.class.parent_optional, self.class.parent_auto, self.class.parent_default, *args)
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 && parent.respond_to?(:register_subresource)
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, parent_default, *args)
# 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 !args.empty?
val = args.first
if val.nil?
# Unsetting the parent.
parent = parent_ref = nil
else
if val.is_a?(String) && !val.include?('[')
raise Poise::Error.new("Cannot use a string #{name} without defining a parent type") if parent_type == Chef::Resource
# Try to find the most recent instance of parent_type with a
# matching name. This takes subclassing parent_type into account.
found_val = nil
iterator = run_context.resource_collection.respond_to?(:recursive_each) ? :recursive_each : :each
# This will find the last matching value due to overwriting
# found_val as it goes. Will be the nearest match.
run_context.resource_collection.public_send(iterator) do |res|
found_val = res if res.is_a?(parent_type) && res.name == val
end
# If found_val is nil, fall back to using lookup even though
# it won't work with subclassing, better than nothing?
val = found_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)
end
elsif !parent_ref || !parent_ref.resource
if parent_default
parent = if parent_default.is_a?(Chef::DelayedEvaluator)
instance_eval(&parent_default)
else
parent_default
end
end
# The @parent_ref means we won't run this if we previously set
# ParentRef.new(nil). This means auto-lookup only happens during
# after_created.
if !parent && !parent_ref && 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, self_resource: self)
end
# Can't find a valid parent, if it wasn't optional raise an error.
raise Poise::Error.new("No #{name} found for #{self}") unless parent || parent_optional
parent_ref = ParentRef.new(parent)
else
parent = parent_ref.resource
end
raise Poise::Error.new("Cannot set the #{name} of #{self} to itself") if parent.equal?(self)
# 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
# Setting to true shouldn't actually do anything if a type was already set.
@parent_type = type unless type == true && !@parent_type.nil?
end
# First ancestor_send looks for a non-true && non-default value,
# second one is to check for default vs true if no real value is found.
@parent_type || Poise::Utils.ancestor_send(self, :parent_type, ignore: [Chef::Resource, true]) || Poise::Utils.ancestor_send(self, :parent_type, default: 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?
Poise::Utils.ancestor_send(self, :parent_optional, default: 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?
Poise::Utils.ancestor_send(self, :parent_auto, default: true)
else
@parent_auto
end
end
# @overload parent_default()
# Get the default value for the default parent link on this resource.
# @since 2.3.0
# @return [Object, Chef::DelayedEvaluator]
# @overload parent_default(val)
# Set the default value for the default parent link on this resource.
# @since 2.3.0
# @param val [Object, Chef::DelayedEvaluator] Default value to set.
# @return [Object, Chef::DelayedEvaluator]
def parent_default(*args)
unless args.empty?
@parent_default = args.first
end
if defined?(@parent_default)
@parent_default
else
Poise::Utils.ancestor_send(self, :parent_default)
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, default: nil)
name = :"parent_#{name}"
(@parent_attributes ||= {})[name] = type
define_method(name) do |*args|
_parent(name, type, optional, auto, default, *args)
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(Poise::Utils.ancestor_send(self, :parent_attributes, default: {}))
# 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,229 @@
#
# Copyright 2013-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/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 inspect
to_text
end
def to_text
"[#{all_resources.map(&:to_s).join(', ')}]"
end
end
include SubcontextBlock
include Chef::DSL::Recipe
attr_reader :subresources
attr_reader :subcontexts
def initialize(*args)
super
@subresources = NoPrintingResourceCollection.new
@subcontexts = []
end
def after_created
super
# Register as a default container if needed.
Poise::Helpers::Subresources::DefaultContainers.register!(self, run_context) if self.class.container_default
# Add all internal subresources to the resource collection.
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)
# respond_to? is for <= 12.0.2, remove some day when I stop caring.
order_fixer.declared_type = 'ruby_block' if order_fixer.respond_to?(:declared_type=)
order_fixer.block do
Chef::Log.debug("[#{self_}] Running order fixer")
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
Chef::Log.debug("Collection: #{@run_context.resource_collection.map(&:to_s).join(', ')}")
end
@run_context.resource_collection.insert(order_fixer)
@subcontexts.each do |ctx|
# Copy all resources to the outer context.
ctx.resource_collection.each do |r|
Chef::Log.debug(" * #{r}")
# Fix the subresource to use the outer run context.
r.run_context = @run_context
@run_context.resource_collection.insert(r)
end
# Copy all notifications to the outer context.
%w{immediate delayed}.each do |notification_type|
ctx.send(:"#{notification_type}_notification_collection").each do |key, notifications|
notifications.each do |notification|
parent_notifications = @run_context.send(:"#{notification_type}_notification_collection")[key]
unless parent_notifications.any? { |existing_notification| existing_notification.duplicates?(notification) }
parent_notifications << notification
end
end
end
end
end
Chef::Log.debug("Collection: #{@run_context.resource_collection.map(&:to_s).join(', ')}")
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.
@subcontexts << 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. It might raise
# NoMethodError is there isn't a real parent.
begin
parent(self_) if respond_to?(:parent)
rescue NoMethodError
# This space left intentionally blank.
end
# 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
# Register a resource as part of this container. Returns true if the
# resource was added to the collection and false if it was already
# known.
#
# @note Return value added in 2.4.0.
# @return [Boolean]
def register_subresource(resource)
subresources.lookup(resource)
false
rescue Chef::Exceptions::ResourceNotFound
Chef::Log.debug("[#{self}] Adding #{resource} to subresources")
subresources.insert(resource)
true
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 or true by default for backwards compat.
Poise::Utils.ancestor_send(self, :container_namespace, default: true)
else
@container_namespace
end
end
# @overload container_default()
# Get the default mode for this resource. If false, this resource
# class will not be used for default container lookups. Defaults to
# true.
# @since 2.3.0
# @return [Boolean]
# @overload container_default(val)
# Set the default mode for this resource.
# @since 2.3.0
# @param val [Boolean] Default mode to set.
# @return [Boolean]
def container_default(val=nil)
@container_default = val unless val.nil?
if @container_default.nil?
# Not set here, look at the superclass or true by default for backwards compat.
Poise::Utils.ancestor_send(self, :container_default, default: true)
else
@container_default
end
end
def included(klass)
super
klass.extend(ClassMethods)
klass.const_set(:HIDDEN_VARS, klass.const_get(:HIDDEN_IVARS) + [:@subcontexts])
klass.const_set(:FORBIDDEN_IVARS, klass.const_get(:FORBIDDEN_IVARS) + [:@subcontexts])
end
end
extend ClassMethods
end
end
end
end

View File

@@ -0,0 +1,75 @@
#
# 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.
#
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, self_resource: nil)
CONTAINER_MUTEX.synchronize do
containers(run_context).reverse_each do |resource|
return resource if resource.is_a?(klass) && (!self_resource || self_resource != resource)
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)
# For test cases where nil gets used sometimes.
return [] unless run_context && run_context.node && run_context.node.run_state
run_context.node.run_state[:poise_default_containers] ||= []
end
end
end
end
end

View File

@@ -0,0 +1,168 @@
#
# Copyright 2013-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/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)
# Make an alias for #variables to match the template resource.
alias_method("#{name_prefix}variables", "#{name_prefix}options")
# 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") if Array(action) == Array(self.class.default_action)
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,64 @@
#
# Copyright 2013-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 'poise/utils/win32'
module Poise
module Helpers
# A resource mixin to intercept properties named `user`, `group`, or `owner`,
# if their default value is `'root'` and make it work on Windows (and
# FreeBSD, AIX).
#
# @since 2.7.0
# @example
# class MyResource < Chef::Resource
# include Poise::Helpers::Win32User
# attribute(:user, default: 'root')
# attribute(:group, default: 'root')
# end
# @example Avoiding automatic translation
# class MyResource < Chef::Resource
# include Poise::Helpers::Win32User
# attribute(:user, default: lazy { 'root' })
# attribute(:group, default: lazy { 'root' })
# end
module Win32User
# User-ish property names.
# @api private
USER_PROPERTIES = ['user', :user, 'owner', :owner]
# Group-ish property names.
# @api private
GROUP_PROPERTIES = ['group', :group]
# Intercept property access to swap out the default value.
# @api private
def set_or_return(symbol, arg, options={})
if options && options[:default] == 'root'
if USER_PROPERTIES.include?(symbol) && node.platform_family?('windows')
options = options.dup
options[:default] = Poise::Utils::Win32.admin_user
elsif GROUP_PROPERTIES.include?(symbol)
options = options.dup
options[:default] = node['root_group']
end
end
super(symbol, arg, options)
end
end
end
end

View File

@@ -0,0 +1,59 @@
#
# Copyright 2013-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 'poise/helpers'
require 'poise/utils'
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::LWRPPolyfill
# IncludeRecipe must come after LWRPPolyfill because that pulls in the
# recipe DSL which has its own #include_recipe.
include Poise::Helpers::IncludeRecipe
include Poise::Helpers::NotifyingBlock
include Poise::Utils::ShellOut
# @!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,81 @@
#
# Copyright 2013-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 'poise/helpers'
require 'poise/utils'
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 if Poise::Helpers::LazyDefault.needs_polyfill?
include Poise::Helpers::LWRPPolyfill
include Poise::Helpers::OptionCollector
include Poise::Helpers::ResourceCloning
include Poise::Helpers::ResourceName
include Poise::Helpers::ResourceSubclass
include Poise::Helpers::TemplateContent
include Poise::Helpers::Win32User # Must be after LazyDefault.
include Poise::Utils::ShellOut
# @!classmethods
module ClassMethods
def poise_subresource_container(namespace=nil, default=nil)
include Poise::Helpers::Subresources::Container
# false is a valid value.
container_namespace(namespace) unless namespace.nil?
container_default(default) unless default.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-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.
#
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,75 @@
#
# Copyright 2013-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/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 over all resources, expanding parent context in order.
#
# @param block [Proc] Iteration block
# @return [void]
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
# Iterate over all resources in reverse order.
#
# @since 2.3.0
# @param block [Proc] Iteration block
# @return [void]
def reverse_recursive_each(&block)
reverse_each(&block)
if @parent
if @parent.respond_to?(:recursive_each)
@parent.reverse_recursive_each(&block)
else
@parent.reverse_each(&block)
end
end
end
end
end
end

View File

@@ -0,0 +1,55 @@
#
# Copyright 2013-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/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|
if @resource.run_context.respond_to?(:add_delayed_action)
@resource.run_context.add_delayed_action(notification)
else
notifications = run_context.parent_run_context.delayed_notifications(@resource)
if notifications.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
end

View File

@@ -0,0 +1,181 @@
#
# 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 'poise/error'
module Poise
module Utils
autoload :ResourceProviderMixin, 'poise/utils/resource_provider_mixin'
autoload :ShellOut, 'poise/utils/shell_out'
autoload :Win32, 'poise/utils/win32'
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 = {}
Poise.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 /.
if filename.start_with?(File.join(ver.halite_root, ''))
Poise.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|
if ::File::ALT_SEPARATOR
file = file.gsub(::File::ALT_SEPARATOR, ::File::SEPARATOR)
end
# Put this behind an environment variable because it is verbose
# even for normal debugging-level output.
Poise.debug("[Poise] Checking #{seg} in #{name}: #{file.inspect}")
if file == filename
Poise.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
# Try to find an ancestor to call a method on.
#
# @since 2.2.3
# @since 2.3.0
# Added ignore parameter.
# @param obj [Object] Self from the caller.
# @param msg [Symbol] Method to try to call.
# @param args [Array<Object>] Method arguments.
# @param default [Object] Default return value if no valid ancestor exists.
# @param ignore [Array<Object>] Return value to ignore when scanning ancesors.
# @return [Object]
# @example
# val = @val || Poise::Utils.ancestor_send(self, :val)
def ancestor_send(obj, msg, *args, default: nil, ignore: [default])
# Class is a subclass of Module, if we get something else use its class.
obj = obj.class unless obj.is_a?(Module)
ancestors = []
if obj.respond_to?(:superclass)
# Check the superclass first if present.
ancestors << obj.superclass
end
# Make sure we don't check obj itself.
ancestors.concat(obj.ancestors.drop(1))
ancestors.each do |mod|
if mod.respond_to?(msg)
val = mod.send(msg, *args)
# If we get the default back, assume we should keep trying.
return val unless ignore.include?(val)
end
end
# Nothing valid found, use the default.
default
end
# Create a helper to invoke a module with some parameters.
#
# @since 2.3.0
# @param mod [Module] The module to wrap.
# @param block [Proc] The module to implement to parameterization.
# @return [void]
# @example
# module MyMixin
# def self.my_mixin_name(name)
# # ...
# end
# end
#
# Poise::Utils.parameterized_module(MyMixin) do |name|
# my_mixin_name(name)
# end
def parameterized_module(mod, &block)
raise Poise::Error.new("Cannot parameterize an anonymous module") unless mod.name && !mod.name.empty?
parent_name_parts = mod.name.split(/::/)
# Grab the last piece which will be the method name.
mod_name = parent_name_parts.pop
# Find the enclosing module or class object.
parent = parent_name_parts.inject(Object) {|memo, name| memo.const_get(name) }
# Object is a special case since we need #define_method instead.
method_type = if parent == Object
:define_method
else
:define_singleton_method
end
# Scoping hack.
self_ = self
# Construct the method.
parent.send(method_type, mod_name) do |*args|
self_.send(:check_block_arity!, block, args)
# Create a new anonymous module to be returned from the method.
Module.new do
# Fake the name.
define_singleton_method(:name) do
super() || mod.name
end
# When the stub module gets included, activate our behaviors.
define_singleton_method(:included) do |klass|
super(klass)
klass.send(:include, mod)
klass.instance_exec(*args, &block)
end
end
end
end
private
# Check that the given arguments match the given block. This is needed
# because Ruby will nil-pad mismatched argspecs on blocks rather than error.
#
# @since 2.3.0
# @param block [Proc] Block to check.
# @param args [Array<Object>] Arguments to check.
# @return [void]
def check_block_arity!(block, args)
# Convert the block to a lambda-style proc. You can't make this shit up.
obj = Object.new
obj.define_singleton_method(:block, &block)
block = obj.method(:block).to_proc
# Check
required_args = block.arity < 0 ? ~block.arity : block.arity
if args.length < required_args || (block.arity >= 0 && args.length > block.arity)
raise ArgumentError.new("wrong number of arguments (#{args.length} for #{required_args}#{block.arity < 0 ? '+' : ''})")
end
end
end
end

View File

@@ -0,0 +1,65 @@
#
# 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.
#
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 || inner_klass.name.to_s.end_with?('::Resource')
# Use klass::Resource to look up relative to the original module.
inner_klass.class_exec { include klass::Resource }
elsif inner_klass < Chef::Provider || inner_klass.name.to_s.end_with?('::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,90 @@
#
# 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 'etc'
require 'chef/mixin/shell_out'
module Poise
module Utils
# A mixin to provider a better shell_out.
#
# @since 2.5.0
# @example
# Poise::Utils::ShellOut.poise_shell_out('ruby myapp.rb', user: 'myuser')
module ShellOut
extend self
include Chef::Mixin::ShellOut
# An enhanced version of Chef's `shell_out` which sets some default
# parameters. If possible it will set $HOME, $USER, $LOGNAME, and the
# group to run as.
#
# @param command_args [Array] Command arguments to be passed to `shell_out`.
# @param options [Hash<Symbol, Object>] Options to be passed to `shell_out`,
# with modifications.
# @return [Mixlib::ShellOut]
def poise_shell_out(*command_args, **options)
# Allow the env option shorthand.
options[:environment] ||= {}
if options[:env]
options[:environment].update(options[:env])
options.delete(:env)
end
# Convert environment keys to strings to be safe.
options[:environment] = options[:environment].inject({}) do |memo, (key, value)|
memo[key.to_s] = value.to_s
memo
end
# Populate some standard environment variables.
ent = begin
if options[:user].is_a?(Integer)
Etc.getpwuid(options[:user])
elsif options[:user]
Etc.getpwnam(options[:user])
end
rescue ArgumentError
nil
end
username = ent ? ent.name : options[:name]
if username
options[:environment]['HOME'] ||= Dir.home(username)
options[:environment]['USER'] ||= username
# On the off chance they set one manually but not the other.
options[:environment]['LOGNAME'] ||= options[:environment]['USER']
end
# Set the default group on Unix.
options[:group] ||= ent.gid if ent
# Mixlib-ShellOut doesn't support array commands on Windows and has
# super wonky escaping for cmd.exe.
if respond_to?(:node) && node.platform_family?('windows')
command_args = [Poise::Utils::Win32.reparse_command(*command_args)]
end
# Call Chef's shell_out wrapper.
shell_out(*command_args, **options)
end
# The `error!` version of {#poise_shell_out}.
#
# @see #poise_shell_out
# @return [Mixlib::ShellOut]
def poise_shell_out!(*command_args)
poise_shell_out(*command_args).tap(&:error!)
end
end
end
end

View File

@@ -0,0 +1,127 @@
#
# Copyright 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 'shellwords'
module Poise
module Utils
# Utilities for working with Windows.
#
# @since 2.7.0
module Win32
extend self
# Code borrowed from https://github.com/chef-cookbooks/chef-client/blob/master/libraries/helpers.rb
# Used under the terms of the Apache v2 license.
# Copyright 2012-2016, John Dewey
# Run a WMI query and extracts a property. This assumes Chef has already
# loaded the win32 libraries.
#
# @api private
# @param wmi_property [Symbol] Property to extract.
# @param wmi_query [String] Query to run.
# @return [String]
def wmi_property_from_query(wmi_property, wmi_query)
@wmi = ::WIN32OLE.connect('winmgmts://')
result = @wmi.ExecQuery(wmi_query)
return nil unless result.each.count > 0
result.each.next.send(wmi_property)
end
# Find the name of the Administrator user, give or take localization.
#
# @return [String]
def admin_user
if defined?(::WIN32OLE)
wmi_property_from_query(:name, "select * from Win32_UserAccount where sid like 'S-1-5-21-%-500' and LocalAccount=True")
else
# Warn except under ChefSpec because it will just annoy people.
Chef::Log.warn('[Poise::Utils::Win32] Unable to query admin user, WIN32OLE not available') unless defined?(ChefSpec)
'Administrator'
end
end
# Escaping that is compatible with CommandLineToArgvW. Based on
# https://blogs.msdn.microsoft.com/twistylittlepassagesallalike/2011/04/23/everyone-quotes-command-line-arguments-the-wrong-way/
#
# @api private
# @param string [String] String to escape.
# @return [String]
def argv_quote(string, force_quote: false)
if !force_quote && !string.empty? && string !~ /[ \t\n\v"]/
# Nothing fancy, no escaping needed.
string
else
command_line = '"'
i = 0
while true
number_backslashes = 0
while i != string.size && string[i] == '\\'
i += 1
number_backslashes += 1
end
if i == string.size
# Escape all backslashes, but let the terminating
# double quotation mark we add below be interpreted
# as a metacharacter.
command_line << '\\' * (number_backslashes * 2)
break
elsif string[i] == '"'
# Escape all backslashes and the following
# double quotation mark.
command_line << '\\' * ((number_backslashes * 2) + 1)
command_line << '"'
else
# Backslashes aren't special here.
command_line << '\\' * number_backslashes
command_line << string[i]
end
i += 1
end
command_line << '"'
command_line
end
end
# Take a string or array command in the format used by shell_out et al and
# create something we can use on Windows.
#
# @
def reparse_command(*args)
array_mode = !(args.length == 1 && args.first.is_a?(String))
# At some point when mixlib-shellout groks array commands on Windows,
# we should support that here.
parsed_args = array_mode ? args.flatten : Shellwords.split(args.first)
cmd = parsed_args.map {|s| argv_quote(s) }.join(' ')
if array_mode
# This fails on non-Windows because of win32/process.
require 'mixlib/shellout/windows'
if Mixlib::ShellOut::Windows::Utils.should_run_under_cmd?(cmd)
# If we are in array mode, try to make cmd.exe keep its grubby paws
# off our metacharacters.
cmd = cmd.each_char.map {|c| '^'+c }.join('')
end
end
cmd
end
end
end
end

View File

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

View File

@@ -0,0 +1,18 @@
#
# Copyright 2013-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.
#
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