poise 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
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