poise 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +10 -0
  3. data/.kitchen.travis.yml +9 -0
  4. data/.kitchen.yml +18 -0
  5. data/.rubocop.yml +2 -0
  6. data/.travis.yml +25 -0
  7. data/.yardopts +5 -0
  8. data/Berksfile +26 -0
  9. data/Berksfile.lock +10 -0
  10. data/CHANGELOG.md +58 -0
  11. data/Gemfile +32 -0
  12. data/LICENSE +202 -0
  13. data/README.md +198 -0
  14. data/Rakefile +17 -0
  15. data/lib/poise.rb +71 -0
  16. data/lib/poise/error.rb +24 -0
  17. data/lib/poise/helpers.rb +33 -0
  18. data/lib/poise/helpers/chefspec_matchers.rb +92 -0
  19. data/lib/poise/helpers/defined_in.rb +111 -0
  20. data/lib/poise/helpers/fused.rb +127 -0
  21. data/lib/poise/helpers/include_recipe.rb +62 -0
  22. data/lib/poise/helpers/inversion.rb +374 -0
  23. data/lib/poise/helpers/inversion/options_provider.rb +41 -0
  24. data/lib/poise/helpers/inversion/options_resource.rb +101 -0
  25. data/lib/poise/helpers/lazy_default.rb +62 -0
  26. data/lib/poise/helpers/lwrp_polyfill.rb +96 -0
  27. data/lib/poise/helpers/notifying_block.rb +78 -0
  28. data/lib/poise/helpers/option_collector.rb +117 -0
  29. data/lib/poise/helpers/resource_name.rb +99 -0
  30. data/lib/poise/helpers/subcontext_block.rb +58 -0
  31. data/lib/poise/helpers/subresources.rb +29 -0
  32. data/lib/poise/helpers/subresources/child.rb +217 -0
  33. data/lib/poise/helpers/subresources/container.rb +165 -0
  34. data/lib/poise/helpers/subresources/default_containers.rb +73 -0
  35. data/lib/poise/helpers/template_content.rb +165 -0
  36. data/lib/poise/provider.rb +55 -0
  37. data/lib/poise/resource.rb +75 -0
  38. data/lib/poise/subcontext.rb +27 -0
  39. data/lib/poise/subcontext/resource_collection.rb +56 -0
  40. data/lib/poise/subcontext/runner.rb +50 -0
  41. data/lib/poise/utils.rb +60 -0
  42. data/lib/poise/utils/resource_provider_mixin.rb +53 -0
  43. data/lib/poise/version.rb +20 -0
  44. data/poise.gemspec +38 -0
  45. data/test/cookbooks/poise_test/attributes/default.rb +17 -0
  46. data/test/cookbooks/poise_test/libraries/app.rb +43 -0
  47. data/test/cookbooks/poise_test/libraries/app_config.rb +46 -0
  48. data/test/cookbooks/poise_test/libraries/inversion.rb +67 -0
  49. data/test/cookbooks/poise_test/metadata.rb +18 -0
  50. data/test/cookbooks/poise_test/recipes/default.rb +25 -0
  51. data/test/cookbooks/poise_test/recipes/inversion.rb +42 -0
  52. data/test/gemfiles/chef-12.0.gemfile +18 -0
  53. data/test/gemfiles/chef-12.1.gemfile +18 -0
  54. data/test/gemfiles/chef-12.2.gemfile +18 -0
  55. data/test/gemfiles/chef-12.gemfile +18 -0
  56. data/test/gemfiles/master.gemfile +20 -0
  57. data/test/integration/default/serverspec/default_spec.rb +35 -0
  58. data/test/integration/default/serverspec/inversion_spec.rb +43 -0
  59. data/test/spec/helpers/chefspec_matchers_spec.rb +45 -0
  60. data/test/spec/helpers/defined_in_spec.rb +62 -0
  61. data/test/spec/helpers/fused_spec.rb +92 -0
  62. data/test/spec/helpers/include_recipe_spec.rb +34 -0
  63. data/test/spec/helpers/inversion/options_resource_spec.rb +212 -0
  64. data/test/spec/helpers/inversion_spec.rb +273 -0
  65. data/test/spec/helpers/lazy_default_spec.rb +74 -0
  66. data/test/spec/helpers/lwrp_polyfill_spec.rb +128 -0
  67. data/test/spec/helpers/notifying_block_spec.rb +87 -0
  68. data/test/spec/helpers/option_collector_spec.rb +82 -0
  69. data/test/spec/helpers/resource_name_spec.rb +39 -0
  70. data/test/spec/helpers/subcontext_block_spec.rb +94 -0
  71. data/test/spec/helpers/subresources/child_spec.rb +339 -0
  72. data/test/spec/helpers/subresources/container_spec.rb +175 -0
  73. data/test/spec/helpers/template_context_spec.rb +147 -0
  74. data/test/spec/poise_spec.rb +185 -0
  75. data/test/spec/provider_spec.rb +28 -0
  76. data/test/spec/resource_spec.rb +85 -0
  77. data/test/spec/spec_helper.rb +18 -0
  78. data/test/spec/utils/resource_provider_mixin_spec.rb +52 -0
  79. data/test/spec/utils_spec.rb +110 -0
  80. metadata +190 -0
@@ -0,0 +1,62 @@
1
+ #
2
+ # Copyright 2013-2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'poise/helpers/subcontext_block'
18
+ require 'poise/subcontext/runner'
19
+
20
+
21
+ module Poise
22
+ module Helpers
23
+ # A provider mixin to add #include_recipe that can be called from action
24
+ # methods.
25
+ #
26
+ # @since 2.0.0
27
+ module IncludeRecipe
28
+ include Poise::Helpers::SubcontextBlock
29
+
30
+ def include_recipe(*recipes)
31
+ loaded_recipes = []
32
+ subcontext = subcontext_block do
33
+ recipes.each do |recipe|
34
+ case recipe
35
+ when String
36
+ # Process normally
37
+ Chef::Log.debug("Loading recipe #{recipe} via include_recipe (poise)")
38
+ loaded_recipes += run_context.include_recipe(recipe)
39
+ when Proc
40
+ # Pretend its a block of recipe code
41
+ fake_recipe = Chef::Recipe.new(cookbook_name, new_resource.recipe_name, run_context)
42
+ fake_recipe.instance_eval(&recipe)
43
+ loaded_recipes << fake_recipe
44
+ end
45
+ end
46
+ end
47
+ # Converge the new context.
48
+ Poise::Subcontext::Runner.new(new_resource, subcontext).converge
49
+ collection = global_resource_collection
50
+ subcontext.resource_collection.each do |r|
51
+ Chef::Log.debug("Poise::IncludeRecipe: Adding #{r} to global collection #{collection.object_id}")
52
+ # Insert the local resource into the global context
53
+ collection.insert(r)
54
+ # Skip the iterator forward so we don't double-execute the inserted resource
55
+ # If running at compile time, the iterator is nil
56
+ collection.iterator.skip_forward if collection.iterator
57
+ end
58
+ loaded_recipes
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,374 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+ require 'chef/node'
18
+ require 'chef/node_map'
19
+ require 'chef/provider'
20
+ require 'chef/resource'
21
+
22
+ require 'poise/helpers/defined_in'
23
+ require 'poise/error'
24
+ require 'poise/helpers/inversion/options_resource'
25
+ require 'poise/utils/resource_provider_mixin'
26
+
27
+
28
+ module Poise
29
+ module Helpers
30
+ # A mixin for dependency inversion in Chef.
31
+ #
32
+ # @since 2.0.0
33
+ module Inversion
34
+ autoload :OptionsResource, 'poise/helpers/inversion/options_resource'
35
+ autoload :OptionsProvider, 'poise/helpers/inversion/options_provider'
36
+
37
+ include Poise::Utils::ResourceProviderMixin
38
+
39
+ # Resource implementation for {Poise::Helpers::Inversion}.
40
+ # @see Poise::Helpers::Inversion
41
+ module Resource
42
+ # @overload options(val=nil)
43
+ # Set or return provider options for all providers.
44
+ # @param val [Hash] Provider options to set.
45
+ # @return [Hash]
46
+ # @example
47
+ # my_resource 'thing_one' do
48
+ # options depends: 'thing_two'
49
+ # end
50
+ # @overload options(provider, val=nil)
51
+ # Set or return provider options for a specific provider.
52
+ # @param provider [Symbol] Provider to set for.
53
+ # @param val [Hash] Provider options to set.
54
+ # @return [Hash]
55
+ # @example
56
+ # my_resource 'thing_one' do
57
+ # options :my_provider, depends: 'thing_two'
58
+ # end
59
+ def options(provider=nil, val=nil)
60
+ key = :options
61
+ if !val && provider.is_a?(Hash)
62
+ val = provider
63
+ elsif provider
64
+ key = :"options_#{provider}"
65
+ end
66
+ set_or_return(key, val ? Mash.new(val) : val, kind_of: Hash, default: lazy { Mash.new })
67
+ end
68
+
69
+ # Allow setting the provider directly using the same names as the attribute
70
+ # settings.
71
+ #
72
+ # @param val [String, Symbol, Class, nil] Value to set the provider to.
73
+ # @return [Class]
74
+ # @example
75
+ # my_resource 'thing_one' do
76
+ # provider :my_provider
77
+ # end
78
+ def provider(val=nil)
79
+ if val && !val.is_a?(Class)
80
+ provider_class = Poise::Helpers::Inversion.provider_for(resource_name, node, val)
81
+ Chef::Log.debug("[#{self}] Checking for an inversion provider for #{val}: #{provider_class && provider_class.name}")
82
+ val = provider_class if provider_class
83
+ end
84
+ super
85
+ end
86
+
87
+ # @!classmethods
88
+ module ClassMethods
89
+ # Options resource class.
90
+ attr_reader :inversion_options_resource_class
91
+ # Options provider class.
92
+ attr_reader :inversion_options_provider_class
93
+
94
+ # @overload inversion_options_resource()
95
+ # Return the options resource mode for this class.
96
+ # @return [Boolean]
97
+ # @overload inversion_options_resource(val)
98
+ # Set the options resource mode for this class. Set to true to
99
+ # automatically create an options resource. Defaults to true.
100
+ # @param val [Boolean] Enable/disable setting.
101
+ # @return [Boolean]
102
+ def inversion_options_resource(val=nil)
103
+ @poise_inversion_options_resource = val unless val.nil?
104
+ @poise_inversion_options_resource
105
+ end
106
+
107
+ # Create resource and provider classes for an options resource.
108
+ #
109
+ # @param name [String, Symbol] DSL name for the base resource.
110
+ # @return [void]
111
+ def create_inversion_options_resource!(name)
112
+ enclosing_class = self
113
+ options_resource_name = :"#{name}_options"
114
+ # Create the resource class.
115
+ @inversion_options_resource_class = Class.new(Chef::Resource) do
116
+ include Poise::Helpers::Inversion::OptionsResource
117
+ define_singleton_method(:name) do
118
+ "#{enclosing_class}::OptionsResource"
119
+ end
120
+ provides(options_resource_name)
121
+ inversion_resource(name)
122
+ end
123
+ # Create the provider class.
124
+ @inversion_options_provider_class = Class.new(Chef::Provider) do
125
+ include Poise::Helpers::Inversion::OptionsProvider
126
+ define_singleton_method(:name) do
127
+ "#{enclosing_class}::OptionsProvider"
128
+ end
129
+ provides(options_resource_name)
130
+ end
131
+ end
132
+
133
+ # Wrap #provides() to create an options resource if desired.
134
+ #
135
+ # @param name [Symbol] Resource name
136
+ # return [void]
137
+ def provides(name)
138
+ create_inversion_options_resource!(name) if inversion_options_resource
139
+ super if defined?(super)
140
+ end
141
+
142
+ def included(klass)
143
+ super
144
+ klass.extend(ClassMethods)
145
+ end
146
+ end
147
+
148
+ extend ClassMethods
149
+ end
150
+
151
+ # Provider implementation for {Poise::Helpers::Inversion}.
152
+ # @see Poise::Helpers::Inversion
153
+ module Provider
154
+ include DefinedIn
155
+
156
+ # Compile all the different levels of inversion options together.
157
+ #
158
+ # @return [Hash]
159
+ # @example
160
+ # def action_run
161
+ # if options['depends']
162
+ # # ...
163
+ # end
164
+ # end
165
+ def options
166
+ @options ||= self.class.inversion_options(node, new_resource)
167
+ end
168
+
169
+ # @!classmethods
170
+ module ClassMethods
171
+ # @overload inversion_resource()
172
+ # Return the inversion resource name for this class.
173
+ # @return [Symbol]
174
+ # @overload inversion_resource(val)
175
+ # Set the inversion resource name for this class. You can pass either
176
+ # a symbol in DSL format or a resource class that uses Poise. This
177
+ # name is used to determine which resources the inversion provider is
178
+ # a candidate for.
179
+ # @param val [Symbol, Class] Name to set.
180
+ # @return [Symbol]
181
+ def inversion_resource(val=nil)
182
+ if val
183
+ val = val.resource_name if val.is_a?(Class)
184
+ Chef::Log.debug("[#{self.name}] Setting inversion resource to #{val}")
185
+ @poise_inversion_resource = val.to_sym
186
+ end
187
+ @poise_inversion_resource || (superclass.respond_to?(:inversion_resource) ? superclass.inversion_resource : nil)
188
+ end
189
+
190
+ # @overload inversion_attribute()
191
+ # Return the inversion attribute name(s) for this class.
192
+ # @return [Array<String>]
193
+ # @overload inversion_attribute(val)
194
+ # Set the inversion attribute name(s) for this class. This is
195
+ # used by {.resolve_inversion_attribute} to load configuration data
196
+ # from node attributes. To specify a nested attribute pass an array
197
+ # of strings corresponding to the keys.
198
+ # @param val [String, Array<String>] Attribute path.
199
+ # @return [Array<String>]
200
+ def inversion_attribute(val=nil)
201
+ if val
202
+ # Coerce to an array of strings.
203
+ val = Array(val).map {|name| name.to_s }
204
+ @poise_inversion_attribute = val
205
+ end
206
+ @poise_inversion_attribute || (superclass.respond_to?(:inversion_attribute) ? superclass.inversion_attribute : nil)
207
+ end
208
+
209
+ # Default attribute paths to check for inversion options. Based on
210
+ # the cookbook this class and its superclasses are defined in.
211
+ #
212
+ # @param node [Chef::Node] Node to load from.
213
+ # @return [Array<Array<String>>]
214
+ def default_inversion_attributes(node)
215
+ klass = self
216
+ tried = []
217
+ while klass.respond_to?(:poise_defined_in_cookbook)
218
+ cookbook = klass.poise_defined_in_cookbook(node.run_context)
219
+ if node[cookbook]
220
+ return [cookbook]
221
+ end
222
+ tried << cookbook
223
+ klass = klass.superclass
224
+ end
225
+ raise Poise::Error.new("Unable to find inversion attributes, tried: #{tried.join(', ')}")
226
+ end
227
+
228
+ # Resolve the node attribute used as the base for inversion options
229
+ # for this class. This can be set explicitly with {.inversion_attribute}
230
+ # or the default is to use the name of the cookbook the provider is
231
+ # defined in.
232
+ #
233
+ # @param node [Chef::Node] Node to load from.
234
+ # @return [Chef::Node::Attribute]
235
+ def resolve_inversion_attribute(node)
236
+ # Default to using just the name of the cookbook.
237
+ attribute_names = inversion_attribute || default_inversion_attributes(node)
238
+ attribute_names.inject(node) do |memo, key|
239
+ memo[key] || begin
240
+ raise Poise::Error.new("Attribute #{key} not set when expanding inversion attribute for #{self.name}: #{memo}")
241
+ end
242
+ end
243
+ end
244
+
245
+ # Compile all the different levels of inversion options together.
246
+ #
247
+ # @param node [Chef::Node] Node to load from.
248
+ # @param resource [Chef::Resource] Resource to load from.
249
+ # @return [Hash]
250
+ def inversion_options(node, resource)
251
+ Mash.new.tap do |opts|
252
+ attrs = resolve_inversion_attribute(node)
253
+ # Cast the run state to a Mash because string vs. symbol keys. I can
254
+ # at least promise poise_inversion will be a str so cut down on the
255
+ # amount of data to convert.
256
+ run_state = Mash.new(node.run_state.fetch('poise_inversion', {}).fetch(inversion_resource, {}))[resource.name] || {}
257
+ # Class-level defaults.
258
+ opts.update(default_inversion_options(node, resource))
259
+ # Resource options for all providers.
260
+ opts.update(resource.options)
261
+ # Global provider from node attributes.
262
+ opts.update(provider: attrs['provider']) if attrs['provider']
263
+ # Attribute options for all providers.
264
+ opts.update(attrs['options']) if attrs['options']
265
+ # Resource options for this provider.
266
+ opts.update(resource.options(provides))
267
+ # Attribute options for this resource name.
268
+ opts.update(attrs[resource.name]) if attrs[resource.name]
269
+ # Options resource options for all providers.
270
+ opts.update(run_state['*']) if run_state['*']
271
+ # Options resource options for this provider.
272
+ opts.update(run_state[provides]) if run_state[provides]
273
+ end
274
+ end
275
+
276
+ # Default options data for this provider class.
277
+ #
278
+ # @param node [Chef::Node] Node to load from.
279
+ # @param resource [Chef::Resource] Resource to load from.
280
+ # @return [Hash]
281
+ def default_inversion_options(node, resource)
282
+ {}
283
+ end
284
+
285
+ # Resolve which provider name should be used for a resource.
286
+ #
287
+ # @param node [Chef::Node] Node to load from.
288
+ # @param resource [Chef::Resource] Resource to query.
289
+ # @return [String]
290
+ def resolve_inversion_provider(node, resource)
291
+ inversion_options(node, resource)['provider'] || 'auto'
292
+ end
293
+
294
+ # Override the normal #provides to set the inversion provider name
295
+ # instead of adding to the normal provider map.
296
+ #
297
+ # @overload provides()
298
+ # Return the inversion provider name for the class.
299
+ # @return [Symbol]
300
+ # @overload provides(name, opts={}, &block)
301
+ # Set the inversion provider name for the class.
302
+ # @param name [Symbol] Provider name.
303
+ # @param opts [Hash] NodeMap filter options.
304
+ # @param block [Proc] NodeMap filter proc.
305
+ # @return [Symbol]
306
+ def provides(name=nil, opts={}, &block)
307
+ if name
308
+ raise Poise::Error.new("Inversion resource name not set for #{self.name}") unless inversion_resource
309
+ @poise_inversion_provider = name
310
+ Chef::Log.debug("[#{self.name}] Setting inversion provider name to #{name}")
311
+ Poise::Helpers::Inversion.provider_map(inversion_resource).set(name.to_sym, self, opts, &block)
312
+ # Set the actual Chef-level provides name for DSL dispatch.
313
+ super(inversion_resource)
314
+ end
315
+ @poise_inversion_provider
316
+ end
317
+
318
+ # Override the default #provides? to check for our inverted providers.
319
+ #
320
+ # @api private
321
+ # @param node [Chef::Node] Node to use for attribute checks.
322
+ # @param resource [Chef::Resource] Resource instance to match.
323
+ # @return [Boolean]
324
+ def provides?(node, resource)
325
+ raise Poise::Error.new("Inversion resource name not set for #{self.name}") unless inversion_resource
326
+ return false unless resource.resource_name == inversion_resource
327
+ provider_name = resolve_inversion_provider(node, resource)
328
+ Chef::Log.debug("[#{resource}] Checking provides? on #{self.name}. Got provider_name #{provider_name.inspect}")
329
+ provider_name == provides.to_s || ( provider_name == 'auto' && provides_auto?(node, resource) )
330
+ end
331
+
332
+ # Subclass hook to provide auto-detection for providers.
333
+ #
334
+ # @param node [Chef::Node] Node to check against.
335
+ # @param resource [Chef::Resource] Resource to check against.
336
+ # @return [Boolean]
337
+ def provides_auto?(node, resource)
338
+ false
339
+ end
340
+
341
+ def included(klass)
342
+ super
343
+ klass.extend(ClassMethods)
344
+ end
345
+ end
346
+
347
+ extend ClassMethods
348
+ end
349
+
350
+ # The provider map for a given resource type.
351
+ #
352
+ # @param resource_type [Symbol] Resource type in DSL format.
353
+ # @return [Chef::NodeMap]
354
+ # @example
355
+ # Poise::Helpers::Inversion.provider_map(:my_resource)
356
+ def self.provider_map(resource_type)
357
+ @provider_maps ||= {}
358
+ @provider_maps[resource_type.to_sym] ||= Chef::NodeMap.new
359
+ end
360
+
361
+ # Find a specific provider class for a resource.
362
+ #
363
+ # @param resource_type [Symbol] Resource type in DSL format.
364
+ # @param node [Chef::Node] Node to use for the lookup.
365
+ # @param provider_type [Symbol] Provider type in DSL format.
366
+ # @return [Class]
367
+ # @example
368
+ # Poise::Helpers::Inversion.provider_for(:my_resource, node, :my_provider)
369
+ def self.provider_for(resource_type, node, provider_type)
370
+ provider_map(resource_type).get(node, provider_type.to_sym)
371
+ end
372
+ end
373
+ end
374
+ end
@@ -0,0 +1,41 @@
1
+ #
2
+ # Copyright 2015, Noah Kantrowitz
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ #
16
+
17
+
18
+ module Poise
19
+ module Helpers
20
+ module Inversion
21
+ # A mixin for inversion options providers.
22
+ #
23
+ # @api private
24
+ # @since 2.0.0
25
+ # @see Poise::Helper::Inversion
26
+ module OptionsProvider
27
+ # @api private
28
+ def self.included(klass)
29
+ klass.class_exec { include Poise }
30
+ end
31
+
32
+ # A blank run action.
33
+ #
34
+ # @return [void]
35
+ def action_run
36
+ # This space left intentionally blank.
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end