chef 12.4.3 → 12.5.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -2
  3. data/lib/chef.rb +1 -1
  4. data/lib/chef/application/solo.rb +1 -1
  5. data/lib/chef/application/windows_service_manager.rb +17 -12
  6. data/lib/chef/chef_class.rb +7 -0
  7. data/lib/chef/chef_fs/config.rb +22 -24
  8. data/lib/chef/chef_fs/file_pattern.rb +4 -15
  9. data/lib/chef/chef_fs/file_system/cookbook_dir.rb +1 -0
  10. data/lib/chef/chef_fs/knife.rb +35 -7
  11. data/lib/chef/chef_fs/path_utils.rb +65 -34
  12. data/lib/chef/constants.rb +27 -0
  13. data/lib/chef/delayed_evaluator.rb +21 -0
  14. data/lib/chef/dsl/recipe.rb +20 -2
  15. data/lib/chef/event_dispatch/base.rb +40 -16
  16. data/lib/chef/event_dispatch/dsl.rb +64 -0
  17. data/lib/chef/exceptions.rb +6 -1
  18. data/lib/chef/formatters/doc.rb +3 -1
  19. data/lib/chef/guard_interpreter/resource_guard_interpreter.rb +3 -1
  20. data/lib/chef/http/http_request.rb +1 -1
  21. data/lib/chef/knife/bootstrap/templates/chef-full.erb +1 -1
  22. data/lib/chef/knife/ssl_check.rb +3 -2
  23. data/lib/chef/knife/user_edit.rb +1 -2
  24. data/lib/chef/mixin/params_validate.rb +362 -135
  25. data/lib/chef/node.rb +19 -0
  26. data/lib/chef/platform/handler_map.rb +0 -5
  27. data/lib/chef/platform/rebooter.rb +1 -1
  28. data/lib/chef/property.rb +539 -0
  29. data/lib/chef/provider.rb +129 -12
  30. data/lib/chef/provider/deploy.rb +3 -5
  31. data/lib/chef/provider/lwrp_base.rb +1 -75
  32. data/lib/chef/provider/package.rb +1 -1
  33. data/lib/chef/provider/powershell_script.rb +32 -19
  34. data/lib/chef/provider/registry_key.rb +5 -5
  35. data/lib/chef/provider/service/macosx.rb +5 -1
  36. data/lib/chef/recipe.rb +1 -8
  37. data/lib/chef/resource.rb +499 -84
  38. data/lib/chef/resource/file/verification.rb +7 -1
  39. data/lib/chef/resource/lwrp_base.rb +1 -7
  40. data/lib/chef/run_context.rb +404 -83
  41. data/lib/chef/version.rb +1 -1
  42. data/lib/chef/win32/registry.rb +10 -2
  43. data/lib/chef/workstation_config_loader.rb +3 -158
  44. data/spec/data/run_context/cookbooks/include/recipes/default.rb +24 -0
  45. data/spec/data/run_context/cookbooks/include/recipes/includee.rb +3 -0
  46. data/spec/functional/rebooter_spec.rb +1 -1
  47. data/spec/functional/resource/{powershell_spec.rb → powershell_script_spec.rb} +3 -3
  48. data/spec/functional/win32/registry_helper_spec.rb +12 -0
  49. data/spec/functional/win32/service_manager_spec.rb +2 -2
  50. data/spec/integration/knife/chef_repo_path_spec.rb +13 -11
  51. data/spec/integration/recipes/recipe_dsl_spec.rb +0 -15
  52. data/spec/integration/recipes/resource_action_spec.rb +343 -0
  53. data/spec/spec_helper.rb +1 -0
  54. data/spec/support/shared/functional/win32_service.rb +2 -1
  55. data/spec/unit/application/solo_spec.rb +4 -3
  56. data/spec/unit/chef_class_spec.rb +23 -0
  57. data/spec/unit/chef_fs/path_util_spec.rb +108 -0
  58. data/spec/unit/event_dispatch/dsl_spec.rb +87 -0
  59. data/spec/unit/json_compat_spec.rb +4 -3
  60. data/spec/unit/knife/ssl_check_spec.rb +4 -0
  61. data/spec/unit/mixin/params_validate_spec.rb +4 -2
  62. data/spec/unit/node_spec.rb +7 -0
  63. data/spec/unit/property/state_spec.rb +506 -0
  64. data/spec/unit/property/validation_spec.rb +658 -0
  65. data/spec/unit/property_spec.rb +968 -0
  66. data/spec/unit/provider/{powershell_spec.rb → powershell_script_spec.rb} +0 -0
  67. data/spec/unit/provider/registry_key_spec.rb +12 -0
  68. data/spec/unit/provider/service/macosx_spec.rb +4 -4
  69. data/spec/unit/provider_spec.rb +1 -3
  70. data/spec/unit/recipe_spec.rb +0 -4
  71. data/spec/unit/registry_helper_spec.rb +15 -1
  72. data/spec/unit/resource/file/verification_spec.rb +33 -5
  73. data/spec/unit/resource/{powershell_spec.rb → powershell_script_spec.rb} +0 -0
  74. data/spec/unit/resource_spec.rb +2 -2
  75. data/spec/unit/run_context/child_run_context_spec.rb +133 -0
  76. data/spec/unit/run_context_spec.rb +7 -0
  77. metadata +25 -25
  78. data/spec/unit/workstation_config_loader_spec.rb +0 -283
@@ -26,6 +26,7 @@ require 'chef/mixin/powershell_out'
26
26
  require 'chef/mixin/provides'
27
27
  require 'chef/platform/service_helpers'
28
28
  require 'chef/node_map'
29
+ require 'forwardable'
29
30
 
30
31
  class Chef
31
32
  class Provider
@@ -65,6 +66,7 @@ class Chef
65
66
 
66
67
  @recipe_name = nil
67
68
  @cookbook_name = nil
69
+ self.class.include_resource_dsl_module(new_resource)
68
70
  end
69
71
 
70
72
  def whyrun_mode?
@@ -119,11 +121,11 @@ class Chef
119
121
  check_resource_semantics!
120
122
 
121
123
  # user-defined LWRPs may include unsafe load_current_resource methods that cannot be run in whyrun mode
122
- if !whyrun_mode? || whyrun_supported?
124
+ if whyrun_mode? && !whyrun_supported?
125
+ events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource)
126
+ else
123
127
  load_current_resource
124
128
  events.resource_current_state_loaded(@new_resource, @action, @current_resource)
125
- elsif whyrun_mode? && !whyrun_supported?
126
- events.resource_current_state_load_bypassed(@new_resource, @action, @current_resource)
127
129
  end
128
130
 
129
131
  define_resource_requirements
@@ -136,9 +138,7 @@ class Chef
136
138
  # we can't execute the action.
137
139
  # in non-whyrun mode, this will still cause the action to be
138
140
  # executed normally.
139
- if whyrun_supported? && !requirements.action_blocked?(@action)
140
- send("action_#{@action}")
141
- elsif whyrun_mode?
141
+ if whyrun_mode? && (!whyrun_supported? || requirements.action_blocked?(@action))
142
142
  events.resource_bypassed(@new_resource, @action, self)
143
143
  else
144
144
  send("action_#{@action}")
@@ -183,6 +183,121 @@ class Chef
183
183
  Chef::ProviderResolver.new(node, resource, :nothing).provided_by?(self)
184
184
  end
185
185
 
186
+ #
187
+ # Include attributes, public and protected methods from this Resource in
188
+ # the provider.
189
+ #
190
+ # If this is set to true, delegate methods are included in the provider so
191
+ # that you can call (for example) `attrname` and it will call
192
+ # `new_resource.attrname`.
193
+ #
194
+ # The actual include does not happen until the first time the Provider
195
+ # is instantiated (so that we don't have to worry about load order issues).
196
+ #
197
+ # @param include_resource_dsl [Boolean] Whether to include resource DSL or
198
+ # not (defaults to `false`).
199
+ #
200
+ def self.include_resource_dsl(include_resource_dsl)
201
+ @include_resource_dsl = include_resource_dsl
202
+ end
203
+
204
+ # Create the resource DSL module that forwards resource methods to new_resource
205
+ #
206
+ # @api private
207
+ def self.include_resource_dsl_module(resource)
208
+ if @include_resource_dsl && !defined?(@included_resource_dsl_module)
209
+ provider_class = self
210
+ @included_resource_dsl_module = Module.new do
211
+ extend Forwardable
212
+ define_singleton_method(:to_s) { "#{resource_class} forwarder module" }
213
+ define_singleton_method(:inspect) { to_s }
214
+ dsl_methods =
215
+ resource.class.public_instance_methods +
216
+ resource.class.protected_instance_methods -
217
+ provider_class.instance_methods
218
+ def_delegators(:new_resource, *dsl_methods)
219
+ end
220
+ include @included_resource_dsl_module
221
+ end
222
+ end
223
+
224
+ # Enables inline evaluation of resources in provider actions.
225
+ #
226
+ # Without this option, any resources declared inside the Provider are added
227
+ # to the resource collection after the current position at the time the
228
+ # action is executed. Because they are added to the primary resource
229
+ # collection for the chef run, they can notify other resources outside
230
+ # the Provider, and potentially be notified by resources outside the Provider
231
+ # (but this is complicated by the fact that they don't exist until the
232
+ # provider executes). In this mode, it is impossible to correctly set the
233
+ # updated_by_last_action flag on the parent Provider resource, since it
234
+ # executes and returns before its component resources are run.
235
+ #
236
+ # With this option enabled, each action creates a temporary run_context
237
+ # with its own resource collection, evaluates the action's code in that
238
+ # context, and then converges the resources created. If any resources
239
+ # were updated, then this provider's new_resource will be marked updated.
240
+ #
241
+ # In this mode, resources created within the Provider cannot interact with
242
+ # external resources via notifies, though notifications to other
243
+ # resources within the Provider will work. Delayed notifications are executed
244
+ # at the conclusion of the provider's action, *not* at the end of the
245
+ # main chef run.
246
+ #
247
+ # This mode of evaluation is experimental, but is believed to be a better
248
+ # set of tradeoffs than the append-after mode, so it will likely become
249
+ # the default in a future major release of Chef.
250
+ #
251
+ def self.use_inline_resources
252
+ extend InlineResources::ClassMethods
253
+ include InlineResources
254
+ end
255
+
256
+ # Chef::Provider::InlineResources
257
+ # Implementation of inline resource convergence for providers. See
258
+ # Provider.use_inline_resources for a longer explanation.
259
+ #
260
+ # This code is restricted to a module so that it can be selectively
261
+ # applied to providers on an opt-in basis.
262
+ #
263
+ # @api private
264
+ module InlineResources
265
+
266
+ # Our run context is a child of the main run context; that gives us a
267
+ # whole new resource collection and notification set.
268
+ def initialize(resource, run_context)
269
+ super(resource, run_context.create_child)
270
+ end
271
+
272
+ # Class methods for InlineResources. Overrides the `action` DSL method
273
+ # with one that enables inline resource convergence.
274
+ #
275
+ # @api private
276
+ module ClassMethods
277
+ # Defines an action method on the provider, running the block to
278
+ # compile the resources, converging them, and then checking if any
279
+ # were updated (and updating new-resource if so)
280
+ def action(name, &block)
281
+ class_eval <<-EOM, __FILE__, __LINE__+1
282
+ def action_#{name}
283
+ return_value = compile_action_#{name}
284
+ Chef::Runner.new(run_context).converge
285
+ return_value
286
+ ensure
287
+ if run_context.resource_collection.any? {|r| r.updated? }
288
+ new_resource.updated_by_last_action(true)
289
+ end
290
+ end
291
+ EOM
292
+ # We put the action in its own method so that super() works.
293
+ define_method("compile_action_#{name}", &block)
294
+ end
295
+ end
296
+
297
+ require 'chef/dsl/recipe'
298
+ include Chef::DSL::Recipe::FullDSL
299
+ end
300
+
186
301
  protected
187
302
 
188
303
  def converge_actions
@@ -200,12 +315,14 @@ class Chef
200
315
  # manipulating notifies.
201
316
 
202
317
  converge_by ("evaluate block and run any associated actions") do
203
- saved_run_context = @run_context
204
- @run_context = @run_context.dup
205
- @run_context.resource_collection = Chef::ResourceCollection.new
206
- instance_eval(&block)
207
- Chef::Runner.new(@run_context).converge
208
- @run_context = saved_run_context
318
+ saved_run_context = run_context
319
+ begin
320
+ @run_context = run_context.create_child
321
+ instance_eval(&block)
322
+ Chef::Runner.new(run_context).converge
323
+ ensure
324
+ @run_context = saved_run_context
325
+ end
209
326
  end
210
327
  end
211
328
 
@@ -373,11 +373,9 @@ class Chef
373
373
  end
374
374
 
375
375
  def gem_resource_collection_runner
376
- gems_collection = Chef::ResourceCollection.new
377
- gem_packages.each { |rbgem| gems_collection.insert(rbgem) }
378
- gems_run_context = run_context.dup
379
- gems_run_context.resource_collection = gems_collection
380
- Chef::Runner.new(gems_run_context)
376
+ child_context = run_context.create_child
377
+ gem_packages.each { |rbgem| child_context.resource_collection.insert(rbgem) }
378
+ Chef::Runner.new(child_context)
381
379
  end
382
380
 
383
381
  def gem_packages
@@ -28,52 +28,10 @@ class Chef
28
28
  # Base class from which LWRP providers inherit.
29
29
  class LWRPBase < Provider
30
30
 
31
- # Chef::Provider::LWRPBase::InlineResources
32
- # Implementation of inline resource convergence for LWRP providers. See
33
- # Provider::LWRPBase.use_inline_resources for a longer explanation.
34
- #
35
- # This code is restricted to a module so that it can be selectively
36
- # applied to providers on an opt-in basis.
37
- module InlineResources
38
-
39
- # Class methods for InlineResources. Overrides the `action` DSL method
40
- # with one that enables inline resource convergence.
41
- module ClassMethods
42
- # Defines an action method on the provider, using
43
- # recipe_eval_with_update_check to execute the given block.
44
- def action(name, &block)
45
- define_method("action_#{name}") do
46
- recipe_eval_with_update_check(&block)
47
- end
48
- end
49
- end
50
-
51
- # Executes the given block in a temporary run_context with its own
52
- # resource collection. After the block is executed, any resources
53
- # declared inside are converged, and if any are updated, the
54
- # new_resource will be marked updated.
55
- def recipe_eval_with_update_check(&block)
56
- saved_run_context = @run_context
57
- temp_run_context = @run_context.dup
58
- @run_context = temp_run_context
59
- @run_context.resource_collection = Chef::ResourceCollection.new
60
-
61
- return_value = instance_eval(&block)
62
- Chef::Runner.new(@run_context).converge
63
- return_value
64
- ensure
65
- @run_context = saved_run_context
66
- if temp_run_context.resource_collection.any? {|r| r.updated? }
67
- new_resource.updated_by_last_action(true)
68
- end
69
- end
70
-
71
- end
72
-
73
31
  include Chef::DSL::Recipe
74
32
 
75
33
  # These were previously provided by Chef::Mixin::RecipeDefinitionDSLCore.
76
- # They are not included by its replacment, Chef::DSL::Recipe, but
34
+ # They are not included by its replacement, Chef::DSL::Recipe, but
77
35
  # they may be used in existing LWRPs.
78
36
  include Chef::DSL::PlatformIntrospection
79
37
  include Chef::DSL::DataQuery
@@ -122,38 +80,6 @@ class Chef
122
80
  provider_class
123
81
  end
124
82
 
125
- # Enables inline evaluation of resources in provider actions.
126
- #
127
- # Without this option, any resources declared inside the LWRP are added
128
- # to the resource collection after the current position at the time the
129
- # action is executed. Because they are added to the primary resource
130
- # collection for the chef run, they can notify other resources outside
131
- # the LWRP, and potentially be notified by resources outside the LWRP
132
- # (but this is complicated by the fact that they don't exist until the
133
- # provider executes). In this mode, it is impossible to correctly set the
134
- # updated_by_last_action flag on the parent LWRP resource, since it
135
- # executes and returns before its component resources are run.
136
- #
137
- # With this option enabled, each action creates a temporary run_context
138
- # with its own resource collection, evaluates the action's code in that
139
- # context, and then converges the resources created. If any resources
140
- # were updated, then this provider's new_resource will be marked updated.
141
- #
142
- # In this mode, resources created within the LWRP cannot interact with
143
- # external resources via notifies, though notifications to other
144
- # resources within the LWRP will work. Delayed notifications are executed
145
- # at the conclusion of the provider's action, *not* at the end of the
146
- # main chef run.
147
- #
148
- # This mode of evaluation is experimental, but is believed to be a better
149
- # set of tradeoffs than the append-after mode, so it will likely become
150
- # the default in a future major release of Chef.
151
- #
152
- def use_inline_resources
153
- extend InlineResources::ClassMethods
154
- include InlineResources
155
- end
156
-
157
83
  # DSL for defining a provider's actions.
158
84
  def action(name, &block)
159
85
  define_method("action_#{name}") do
@@ -142,7 +142,7 @@ class Chef
142
142
  def action_remove
143
143
  if removing_package?
144
144
  description = @new_resource.version ? "version #{@new_resource.version} of " : ""
145
- converge_by("remove #{description} package #{@current_resource.package_name}") do
145
+ converge_by("remove #{description}package #{@current_resource.package_name}") do
146
146
  remove_package(@current_resource.package_name, @new_resource.version)
147
147
  Chef::Log.info("#{@new_resource} removed")
148
148
  end
@@ -30,8 +30,8 @@ class Chef
30
30
  end
31
31
 
32
32
  def action_run
33
- valid_syntax = validate_script_syntax!
34
- super if valid_syntax
33
+ validate_script_syntax!
34
+ super
35
35
  end
36
36
 
37
37
  def flags
@@ -62,30 +62,43 @@ class Chef
62
62
  def validate_script_syntax!
63
63
  interpreter_arguments = default_interpreter_flags.join(' ')
64
64
  Tempfile.open(['chef_powershell_script-user-code', '.ps1']) do | user_script_file |
65
- user_script_file.puts("{#{@new_resource.code}}")
66
- user_script_file.close
65
+ # Wrap the user's code in a PowerShell script block so that
66
+ # it isn't executed. However, syntactically invalid script
67
+ # in that block will still trigger a syntax error which is
68
+ # exactly what we want here -- verify the syntax without
69
+ # actually running the script.
70
+ user_code_wrapped_in_powershell_script_block = <<-EOH
71
+ {
72
+ #{@new_resource.code}
73
+ }
74
+ EOH
75
+ user_script_file.puts user_code_wrapped_in_powershell_script_block
67
76
 
77
+ # A .close or explicit .flush required to ensure the file is
78
+ # written to the file system at this point, which is required since
79
+ # the intent is to execute the code just written to it.
80
+ user_script_file.close
68
81
  validation_command = "\"#{interpreter}\" #{interpreter_arguments} -Command #{user_script_file.path}"
69
82
 
70
- # For consistency with other script resources, allow even syntax errors
71
- # to be suppressed if the returns attribute would have suppressed it
72
- # at converge.
73
- valid_returns = [0]
74
- specified_returns = @new_resource.returns.is_a?(Integer) ?
75
- [@new_resource.returns] :
76
- @new_resource.returns
77
- valid_returns.concat([1]) if specified_returns.include?(1)
78
-
79
- result = shell_out!(validation_command, {returns: valid_returns})
80
- result.exitstatus == 0
83
+ # Note that other script providers like bash allow syntax errors
84
+ # to be suppressed by setting 'returns' to a value that the
85
+ # interpreter would return as a status code in the syntax
86
+ # error case. We explicitly don't do this here -- syntax
87
+ # errors will not be suppressed, since doing so could make
88
+ # it harder for users to detect / debug invalid scripts.
89
+
90
+ # Therefore, the only return value for a syntactically valid
91
+ # script is 0. If an exception is raised by shellout, this
92
+ # means a non-zero return and thus a syntactically invalid script.
93
+ shell_out!(validation_command, {returns: [0]})
81
94
  end
82
95
  end
83
96
 
84
97
  def default_interpreter_flags
85
- # 'Bypass' is preferable since it doesn't require user input confirmation
86
- # for files such as PowerShell modules downloaded from the
87
- # Internet. However, 'Bypass' is not supported prior to
88
- # PowerShell 3.0, so the fallback is 'Unrestricted'
98
+ # Execution policy 'Bypass' is preferable since it doesn't require
99
+ # user input confirmation for files such as PowerShell modules
100
+ # downloaded from the Internet. However, 'Bypass' is not supported
101
+ # prior to PowerShell 3.0, so the fallback is 'Unrestricted'
89
102
  execution_policy = Chef::Platform.supports_powershell_execution_bypass?(run_context.node) ? 'Bypass' : 'Unrestricted'
90
103
 
91
104
  [
@@ -64,7 +64,7 @@ class Chef
64
64
 
65
65
  def values_to_hash(values)
66
66
  if values
67
- @name_hash = Hash[values.map { |val| [val[:name], val] }]
67
+ @name_hash = Hash[values.map { |val| [val[:name].downcase, val] }]
68
68
  else
69
69
  @name_hash = {}
70
70
  end
@@ -100,8 +100,8 @@ class Chef
100
100
  end
101
101
  end
102
102
  @new_resource.unscrubbed_values.each do |value|
103
- if @name_hash.has_key?(value[:name])
104
- current_value = @name_hash[value[:name]]
103
+ if @name_hash.has_key?(value[:name].downcase)
104
+ current_value = @name_hash[value[:name].downcase]
105
105
  unless current_value[:type] == value[:type] && current_value[:data] == value[:data]
106
106
  converge_by("set value #{value}") do
107
107
  registry.set_value(@new_resource.key, value)
@@ -122,7 +122,7 @@ class Chef
122
122
  end
123
123
  end
124
124
  @new_resource.unscrubbed_values.each do |value|
125
- unless @name_hash.has_key?(value[:name])
125
+ unless @name_hash.has_key?(value[:name].downcase)
126
126
  converge_by("create value #{value}") do
127
127
  registry.set_value(@new_resource.key, value)
128
128
  end
@@ -133,7 +133,7 @@ class Chef
133
133
  def action_delete
134
134
  if registry.key_exists?(@new_resource.key)
135
135
  @new_resource.unscrubbed_values.each do |value|
136
- if @name_hash.has_key?(value[:name])
136
+ if @name_hash.has_key?(value[:name].downcase)
137
137
  converge_by("delete value #{value}") do
138
138
  registry.delete_value(@new_resource.key, value)
139
139
  end
@@ -42,6 +42,10 @@ class Chef
42
42
 
43
43
  PLIST_DIRS = gather_plist_dirs
44
44
 
45
+ def this_version_or_newer?(this_version)
46
+ Gem::Version.new(node['platform_version']) >= Gem::Version.new(this_version)
47
+ end
48
+
45
49
  def load_current_resource
46
50
  @current_resource = Chef::Resource::MacosxService.new(@new_resource.name)
47
51
  @current_resource.service_name(@new_resource.service_name)
@@ -56,7 +60,7 @@ class Chef
56
60
  @console_user = Etc.getlogin
57
61
  Chef::Log.debug("#{new_resource} console_user: '#{@console_user}'")
58
62
  cmd = "su "
59
- param = !node['platform_version'].include?('10.10') ? '-l ' : ''
63
+ param = this_version_or_newer?('10.10') ? '' : '-l '
60
64
  @base_user_cmd = cmd + param + "#{@console_user} -c"
61
65
  # Default LauchAgent session should be Aqua
62
66
  @session_type = 'Aqua' if @session_type.nil?
@@ -36,14 +36,7 @@ class Chef
36
36
  # A Recipe object is the context in which Chef recipes are evaluated.
37
37
  class Recipe
38
38
 
39
- include Chef::DSL::DataQuery
40
- include Chef::DSL::PlatformIntrospection
41
- include Chef::DSL::IncludeRecipe
42
- include Chef::DSL::Recipe
43
- include Chef::DSL::RegistryHelper
44
- include Chef::DSL::RebootPending
45
- include Chef::DSL::Audit
46
- include Chef::DSL::Powershell
39
+ include Chef::DSL::Recipe::FullDSL
47
40
 
48
41
  include Chef::Mixin::FromFile
49
42
  include Chef::Mixin::Deprecation
@@ -58,8 +58,6 @@ class Chef
58
58
  include Chef::Mixin::ShellOut
59
59
  include Chef::Mixin::PowershellOut
60
60
 
61
- NULL_ARG = Object.new
62
-
63
61
  #
64
62
  # The node the current Chef run is using.
65
63
  #
@@ -103,7 +101,7 @@ class Chef
103
101
  # @param run_context The context of the Chef run. Corresponds to #run_context.
104
102
  #
105
103
  def initialize(name, run_context=nil)
106
- name(name)
104
+ name(name) unless name.nil?
107
105
  @run_context = run_context
108
106
  @noop = nil
109
107
  @before = nil
@@ -132,37 +130,27 @@ class Chef
132
130
  end
133
131
 
134
132
  #
135
- # The name of this particular resource.
136
- #
137
- # This special resource attribute is set automatically from the declaration
138
- # of the resource, e.g.
139
- #
140
- # execute 'Vitruvius' do
141
- # command 'ls'
142
- # end
143
- #
144
- # Will set the name to "Vitruvius".
145
- #
146
- # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`.
133
+ # The list of properties defined on this resource.
147
134
  #
148
- # This is also used for resource notifications and subscribes in the same manner.
135
+ # Everything defined with `property` is in this list.
149
136
  #
150
- # This will coerce any object into a string via #to_s. Arrays are a special case
151
- # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more
152
- # awkward `package[["foo", "bar"]]` that #to_s would produce.
137
+ # @param include_superclass [Boolean] `true` to include properties defined
138
+ # on superclasses; `false` or `nil` to return the list of properties
139
+ # directly on this class.
153
140
  #
154
- # @param name [Object] The name to set, typically a String or Array
155
- # @return [String] The name of this Resource.
141
+ # @return [Hash<Symbol,Property>] The list of property names and types.
156
142
  #
157
- def name(name=nil)
158
- if !name.nil?
159
- if name.is_a?(Array)
160
- @name = name.join(', ')
143
+ def self.properties(include_superclass=true)
144
+ @properties ||= {}
145
+ if include_superclass
146
+ if superclass.respond_to?(:properties)
147
+ superclass.properties.merge(@properties)
161
148
  else
162
- @name = name.to_s
149
+ @properties.dup
163
150
  end
151
+ else
152
+ @properties
164
153
  end
165
- @name
166
154
  end
167
155
 
168
156
  #
@@ -182,8 +170,7 @@ class Chef
182
170
  end
183
171
  @action = arg
184
172
  else
185
- # Pull the action from the class if it's not set
186
- @action || self.class.default_action
173
+ @action
187
174
  end
188
175
  end
189
176
 
@@ -478,13 +465,21 @@ class Chef
478
465
  #
479
466
  # Get the value of the state attributes in this resource as a hash.
480
467
  #
468
+ # Does not include properties that are not set (unless they are identity
469
+ # properties).
470
+ #
481
471
  # @return [Hash{Symbol => Object}] A Hash of attribute => value for the
482
472
  # Resource class's `state_attrs`.
473
+ #
483
474
  def state_for_resource_reporter
484
- self.class.state_attrs.inject({}) do |state_attrs, attr_name|
485
- state_attrs[attr_name] = send(attr_name)
486
- state_attrs
475
+ state = {}
476
+ state_properties = self.class.state_properties
477
+ state_properties.each do |property|
478
+ if property.identity? || property.is_set?(self)
479
+ state[property.name] = send(property.name)
480
+ end
487
481
  end
482
+ state
488
483
  end
489
484
 
490
485
  #
@@ -497,17 +492,22 @@ class Chef
497
492
  alias_method :state, :state_for_resource_reporter
498
493
 
499
494
  #
500
- # The value of the identity attribute, if declared. Falls back to #name if
501
- # no identity attribute is declared.
495
+ # The value of the identity of this resource.
496
+ #
497
+ # - If there are no identity properties on the resource, `name` is returned.
498
+ # - If there is exactly one identity property on the resource, it is returned.
499
+ # - If there are more than one, they are returned in a hash.
502
500
  #
503
- # @return The value of the identity attribute.
501
+ # @return [Object,Hash<Symbol,Object>] The identity of this resource.
504
502
  #
505
503
  def identity
506
- if identity_attr = self.class.identity_attr
507
- send(identity_attr)
508
- else
509
- name
504
+ result = {}
505
+ identity_properties = self.class.identity_properties
506
+ identity_properties.each do |property|
507
+ result[property.name] = send(property.name)
510
508
  end
509
+ return result.values.first if identity_properties.size == 1
510
+ result
511
511
  end
512
512
 
513
513
  #
@@ -529,9 +529,7 @@ class Chef
529
529
  #
530
530
  # Equivalent to #ignore_failure.
531
531
  #
532
- def epic_fail(arg=nil)
533
- ignore_failure(arg)
534
- end
532
+ alias :epic_fail :ignore_failure
535
533
 
536
534
  #
537
535
  # Make this resource into an exact (shallow) copy of the other resource.
@@ -686,66 +684,389 @@ class Chef
686
684
  #
687
685
  # The provider class for this resource.
688
686
  #
687
+ # If `action :x do ... end` has been declared on this resource or its
688
+ # superclasses, this will return the `action_provider_class`.
689
+ #
689
690
  # If this is not set, `provider_for_action` will dynamically determine the
690
691
  # provider.
691
692
  #
692
693
  # @param arg [String, Symbol, Class] Sets the provider class for this resource.
693
694
  # If passed a String or Symbol, e.g. `:file` or `"file"`, looks up the
694
695
  # provider based on the name.
696
+ #
695
697
  # @return The provider class for this resource.
696
698
  #
699
+ # @see Chef::Resource.action_provider_class
700
+ #
697
701
  def provider(arg=nil)
698
702
  klass = if arg.kind_of?(String) || arg.kind_of?(Symbol)
699
703
  lookup_provider_constant(arg)
700
704
  else
701
705
  arg
702
706
  end
703
- set_or_return(:provider, klass, kind_of: [ Class ])
707
+ set_or_return(:provider, klass, kind_of: [ Class ]) ||
708
+ self.class.action_provider_class
704
709
  end
705
710
  def provider=(arg)
706
711
  provider(arg)
707
712
  end
708
713
 
709
- # Set or return the list of "state attributes" implemented by the Resource
710
- # subclass. State attributes are attributes that describe the desired state
711
- # of the system, such as file permissions or ownership. In general, state
712
- # attributes are attributes that could be populated by examining the state
713
- # of the system (e.g., File.stat can tell you the permissions on an
714
- # existing file). Contrarily, attributes that are not "state attributes"
715
- # usually modify the way Chef itself behaves, for example by providing
716
- # additional options for a package manager to use when installing a
717
- # package.
714
+ #
715
+ # Create a property on this resource class.
716
+ #
717
+ # If a superclass has this property, or if this property has already been
718
+ # defined by this resource, this will *override* the previous value.
719
+ #
720
+ # @param name [Symbol] The name of the property.
721
+ # @param type [Object,Array<Object>] The type(s) of this property.
722
+ # If present, this is prepended to the `is` validation option.
723
+ # @param options [Hash<Symbol,Object>] Validation options.
724
+ # @option options [Object,Array] :is An object, or list of
725
+ # objects, that must match the value using Ruby's `===` operator
726
+ # (`options[:is].any? { |v| v === value }`).
727
+ # @option options [Object,Array] :equal_to An object, or list
728
+ # of objects, that must be equal to the value using Ruby's `==`
729
+ # operator (`options[:is].any? { |v| v == value }`)
730
+ # @option options [Regexp,Array<Regexp>] :regex An object, or
731
+ # list of objects, that must match the value with `regex.match(value)`.
732
+ # @option options [Class,Array<Class>] :kind_of A class, or
733
+ # list of classes, that the value must be an instance of.
734
+ # @option options [Hash<String,Proc>] :callbacks A hash of
735
+ # messages -> procs, all of which match the value. The proc must
736
+ # return a truthy or falsey value (true means it matches).
737
+ # @option options [Symbol,Array<Symbol>] :respond_to A method
738
+ # name, or list of method names, the value must respond to.
739
+ # @option options [Symbol,Array<Symbol>] :cannot_be A property,
740
+ # or a list of properties, that the value cannot have (such as `:nil` or
741
+ # `:empty`). The method with a questionmark at the end is called on the
742
+ # value (e.g. `value.empty?`). If the value does not have this method,
743
+ # it is considered valid (i.e. if you don't respond to `empty?` we
744
+ # assume you are not empty).
745
+ # @option options [Proc] :coerce A proc which will be called to
746
+ # transform the user input to canonical form. The value is passed in,
747
+ # and the transformed value returned as output. Lazy values will *not*
748
+ # be passed to this method until after they are evaluated. Called in the
749
+ # context of the resource (meaning you can access other properties).
750
+ # @option options [Boolean] :required `true` if this property
751
+ # must be present; `false` otherwise. This is checked after the resource
752
+ # is fully initialized.
753
+ # @option options [Boolean] :name_property `true` if this
754
+ # property defaults to the same value as `name`. Equivalent to
755
+ # `default: lazy { name }`, except that #property_is_set? will
756
+ # return `true` if the property is set *or* if `name` is set.
757
+ # @option options [Boolean] :name_attribute Same as `name_property`.
758
+ # @option options [Object] :default The value this property
759
+ # will return if the user does not set one. If this is `lazy`, it will
760
+ # be run in the context of the instance (and able to access other
761
+ # properties).
762
+ # @option options [Boolean] :desired_state `true` if this property is
763
+ # part of desired state. Defaults to `true`.
764
+ # @option options [Boolean] :identity `true` if this property
765
+ # is part of object identity. Defaults to `false`.
766
+ #
767
+ # @example Bare property
768
+ # property :x
769
+ #
770
+ # @example With just a type
771
+ # property :x, String
772
+ #
773
+ # @example With just options
774
+ # property :x, default: 'hi'
775
+ #
776
+ # @example With type and options
777
+ # property :x, String, default: 'hi'
778
+ #
779
+ def self.property(name, type=NOT_PASSED, **options)
780
+ name = name.to_sym
781
+
782
+ options[:instance_variable_name] = :"@#{name}" if !options.has_key?(:instance_variable_name)
783
+ options.merge!(name: name, declared_in: self)
784
+
785
+ if type == NOT_PASSED
786
+ # If a type is not passed, the property derives from the
787
+ # superclass property (if any)
788
+ if properties.has_key?(name)
789
+ property = properties[name].derive(**options)
790
+ else
791
+ property = property_type(**options)
792
+ end
793
+
794
+ # If a Property is specified, derive a new one from that.
795
+ elsif type.is_a?(Property) || (type.is_a?(Class) && type <= Property)
796
+ property = type.derive(**options)
797
+
798
+ # If a primitive type was passed, combine it with "is"
799
+ else
800
+ if options[:is]
801
+ options[:is] = ([ type ] + [ options[:is] ]).flatten(1)
802
+ else
803
+ options[:is] = type
804
+ end
805
+ property = property_type(**options)
806
+ end
807
+
808
+ local_properties = properties(false)
809
+ local_properties[name] = property
810
+
811
+ property.emit_dsl
812
+ end
813
+
814
+ #
815
+ # Create a reusable property type that can be used in multiple properties
816
+ # in different resources.
817
+ #
818
+ # @param options [Hash<Symbol,Object>] Validation options. see #property for
819
+ # the list of options.
820
+ #
821
+ # @example
822
+ # property_type(default: 'hi')
823
+ #
824
+ def self.property_type(**options)
825
+ Property.derive(**options)
826
+ end
827
+
828
+ #
829
+ # The name of this particular resource.
830
+ #
831
+ # This special resource attribute is set automatically from the declaration
832
+ # of the resource, e.g.
833
+ #
834
+ # execute 'Vitruvius' do
835
+ # command 'ls'
836
+ # end
837
+ #
838
+ # Will set the name to "Vitruvius".
839
+ #
840
+ # This is also used in to_s to show the resource name, e.g. `execute[Vitruvius]`.
841
+ #
842
+ # This is also used for resource notifications and subscribes in the same manner.
843
+ #
844
+ # This will coerce any object into a string via #to_s. Arrays are a special case
845
+ # so that `package ["foo", "bar"]` becomes package[foo, bar] instead of the more
846
+ # awkward `package[["foo", "bar"]]` that #to_s would produce.
847
+ #
848
+ # @param name [Object] The name to set, typically a String or Array
849
+ # @return [String] The name of this Resource.
850
+ #
851
+ property :name, String, coerce: proc { |v| v.is_a?(Array) ? v.join(', ') : v.to_s }, desired_state: false
852
+
853
+ #
854
+ # Whether this property has been set (or whether it has a default that has
855
+ # been retrieved).
856
+ #
857
+ # @param name [Symbol] The name of the property.
858
+ # @return [Boolean] `true` if the property has been set.
859
+ #
860
+ def property_is_set?(name)
861
+ property = self.class.properties[name.to_sym]
862
+ raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property
863
+ property.is_set?(self)
864
+ end
865
+
866
+ #
867
+ # Clear this property as if it had never been set. It will thereafter return
868
+ # the default.
869
+ # been retrieved).
870
+ #
871
+ # @param name [Symbol] The name of the property.
872
+ #
873
+ def reset_property(name)
874
+ property = self.class.properties[name.to_sym]
875
+ raise ArgumentError, "Property #{name} is not defined in class #{self}" if !property
876
+ property.reset(self)
877
+ end
878
+
879
+ #
880
+ # Create a lazy value for assignment to a default value.
881
+ #
882
+ # @param block The block to run when the value is retrieved.
883
+ #
884
+ # @return [Chef::DelayedEvaluator] The lazy value
885
+ #
886
+ def self.lazy(&block)
887
+ DelayedEvaluator.new(&block)
888
+ end
889
+
890
+ #
891
+ # Get or set the list of desired state properties for this resource.
892
+ #
893
+ # State properties are properties that describe the desired state
894
+ # of the system, such as file permissions or ownership.
895
+ # In general, state properties are properties that could be populated by
896
+ # examining the state of the system (e.g., File.stat can tell you the
897
+ # permissions on an existing file). Contrarily, properties that are not
898
+ # "state properties" usually modify the way Chef itself behaves, for example
899
+ # by providing additional options for a package manager to use when
900
+ # installing a package.
718
901
  #
719
902
  # This list is used by the Chef client auditing system to extract
720
903
  # information from resources to describe changes made to the system.
721
- def self.state_attrs(*attr_names)
722
- @state_attrs ||= []
723
- @state_attrs = attr_names unless attr_names.empty?
904
+ #
905
+ # This method is unnecessary when declaring properties with `property`;
906
+ # properties are added to state_properties by default, and can be turned off
907
+ # with `desired_state: false`.
908
+ #
909
+ # ```ruby
910
+ # property :x # part of desired state
911
+ # property :y, desired_state: false # not part of desired state
912
+ # ```
913
+ #
914
+ # @param names [Array<Symbol>] A list of property names to set as desired
915
+ # state.
916
+ #
917
+ # @return [Array<Property>] All properties in desired state.
918
+ #
919
+ def self.state_properties(*names)
920
+ if !names.empty?
921
+ names = names.map { |name| name.to_sym }.uniq
724
922
 
725
- # Return *all* state_attrs that this class has, including inherited ones
726
- if superclass.respond_to?(:state_attrs)
727
- superclass.state_attrs + @state_attrs
728
- else
729
- @state_attrs
923
+ local_properties = properties(false)
924
+ # Add new properties to the list.
925
+ names.each do |name|
926
+ property = properties[name]
927
+ if !property
928
+ self.property name, instance_variable_name: false, desired_state: true
929
+ elsif !property.desired_state?
930
+ self.property name, desired_state: true
931
+ end
932
+ end
933
+
934
+ # If state_attrs *excludes* something which is currently desired state,
935
+ # mark it as desired_state: false.
936
+ local_properties.each do |name,property|
937
+ if property.desired_state? && !names.include?(name)
938
+ self.property name, desired_state: false
939
+ end
940
+ end
730
941
  end
942
+
943
+ properties.values.select { |property| property.desired_state? }
731
944
  end
732
945
 
733
- # Set or return the "identity attribute" for this resource class. This is
734
- # generally going to be the "name attribute" for this resource. In other
735
- # words, the resource type plus this attribute uniquely identify a given
736
- # bit of state that chef manages. For a File resource, this would be the
737
- # path, for a package resource, it will be the package name. This will show
738
- # up in chef-client's audit records as a searchable field.
739
- def self.identity_attr(attr_name=nil)
740
- @identity_attr ||= nil
741
- @identity_attr = attr_name if attr_name
946
+ #
947
+ # Set or return the list of "state properties" implemented by the Resource
948
+ # subclass.
949
+ #
950
+ # Equivalent to calling #state_properties and getting `state_properties.keys`.
951
+ #
952
+ # @deprecated Use state_properties.keys instead. Note that when you declare
953
+ # properties with `property`: properties are added to state_properties by
954
+ # default, and can be turned off with `desired_state: false`
955
+ #
956
+ # ```ruby
957
+ # property :x # part of desired state
958
+ # property :y, desired_state: false # not part of desired state
959
+ # ```
960
+ #
961
+ # @param names [Array<Symbol>] A list of property names to set as desired
962
+ # state.
963
+ #
964
+ # @return [Array<Symbol>] All property names with desired state.
965
+ #
966
+ def self.state_attrs(*names)
967
+ state_properties(*names).map { |property| property.name }
968
+ end
742
969
 
743
- # If this class doesn't have an identity attr, we'll defer to the superclass:
744
- if @identity_attr || !superclass.respond_to?(:identity_attr)
745
- @identity_attr
746
- else
747
- superclass.identity_attr
970
+ #
971
+ # Set the identity of this resource to a particular set of properties.
972
+ #
973
+ # This drives #identity, which returns data that uniquely refers to a given
974
+ # resource on the given node (in such a way that it can be correlated
975
+ # across Chef runs).
976
+ #
977
+ # This method is unnecessary when declaring properties with `property`;
978
+ # properties can be added to identity during declaration with
979
+ # `identity: true`.
980
+ #
981
+ # ```ruby
982
+ # property :x, identity: true # part of identity
983
+ # property :y # not part of identity
984
+ # ```
985
+ #
986
+ # If no properties are marked as identity, "name" is considered the identity.
987
+ #
988
+ # @param names [Array<Symbol>] A list of property names to set as the identity.
989
+ #
990
+ # @return [Array<Property>] All identity properties.
991
+ #
992
+ def self.identity_properties(*names)
993
+ if !names.empty?
994
+ names = names.map { |name| name.to_sym }
995
+
996
+ # Add or change properties that are not part of the identity.
997
+ names.each do |name|
998
+ property = properties[name]
999
+ if !property
1000
+ self.property name, instance_variable_name: false, identity: true
1001
+ elsif !property.identity?
1002
+ self.property name, identity: true
1003
+ end
1004
+ end
1005
+
1006
+ # If identity_properties *excludes* something which is currently part of
1007
+ # the identity, mark it as identity: false.
1008
+ properties.each do |name,property|
1009
+ if property.identity? && !names.include?(name)
1010
+ self.property name, identity: false
1011
+ end
1012
+ end
748
1013
  end
1014
+
1015
+ result = properties.values.select { |property| property.identity? }
1016
+ result = [ properties[:name] ] if result.empty?
1017
+ result
1018
+ end
1019
+
1020
+ #
1021
+ # Set the identity of this resource to a particular property.
1022
+ #
1023
+ # This drives #identity, which returns data that uniquely refers to a given
1024
+ # resource on the given node (in such a way that it can be correlated
1025
+ # across Chef runs).
1026
+ #
1027
+ # This method is unnecessary when declaring properties with `property`;
1028
+ # properties can be added to identity during declaration with
1029
+ # `identity: true`.
1030
+ #
1031
+ # ```ruby
1032
+ # property :x, identity: true # part of identity
1033
+ # property :y # not part of identity
1034
+ # ```
1035
+ #
1036
+ # @param name [Symbol] A list of property names to set as the identity.
1037
+ #
1038
+ # @return [Symbol] The identity property if there is only one; or `nil` if
1039
+ # there are more than one.
1040
+ #
1041
+ # @raise [ArgumentError] If no arguments are passed and the resource has
1042
+ # more than one identity property.
1043
+ #
1044
+ def self.identity_property(name=nil)
1045
+ result = identity_properties(*Array(name))
1046
+ if result.size > 1
1047
+ raise Chef::Exceptions::MultipleIdentityError, "identity_property cannot be called on an object with more than one identity property (#{result.map { |r| r.name }.join(", ")})."
1048
+ end
1049
+ result.first
1050
+ end
1051
+
1052
+ #
1053
+ # Set a property as the "identity attribute" for this resource.
1054
+ #
1055
+ # Identical to calling #identity_property.first.key.
1056
+ #
1057
+ # @param name [Symbol] The name of the property to set.
1058
+ #
1059
+ # @return [Symbol]
1060
+ #
1061
+ # @deprecated `identity_property` should be used instead.
1062
+ #
1063
+ # @raise [ArgumentError] If no arguments are passed and the resource has
1064
+ # more than one identity property.
1065
+ #
1066
+ def self.identity_attr(name=nil)
1067
+ property = identity_property(name)
1068
+ return nil if !property
1069
+ property.name
749
1070
  end
750
1071
 
751
1072
  #
@@ -771,8 +1092,8 @@ class Chef
771
1092
  # have.
772
1093
  #
773
1094
  attr_accessor :allowed_actions
774
- def allowed_actions(value=NULL_ARG)
775
- if value != NULL_ARG
1095
+ def allowed_actions(value=NOT_PASSED)
1096
+ if value != NOT_PASSED
776
1097
  self.allowed_actions = value
777
1098
  end
778
1099
  @allowed_actions
@@ -906,9 +1227,9 @@ class Chef
906
1227
  #
907
1228
  # @return [Symbol] The name of this resource type (e.g. `:execute`).
908
1229
  #
909
- def self.resource_name(name=NULL_ARG)
1230
+ def self.resource_name(name=NOT_PASSED)
910
1231
  # Setter
911
- if name != NULL_ARG
1232
+ if name != NOT_PASSED
912
1233
  remove_canonical_dsl
913
1234
 
914
1235
  # Set the resource_name and call provides
@@ -923,13 +1244,25 @@ class Chef
923
1244
  @resource_name = nil
924
1245
  end
925
1246
  end
926
-
927
1247
  @resource_name
928
1248
  end
929
1249
  def self.resource_name=(name)
930
1250
  resource_name(name)
931
1251
  end
932
1252
 
1253
+ #
1254
+ # Use the class name as the resource name.
1255
+ #
1256
+ # Munges the last part of the class name from camel case to snake case,
1257
+ # and sets the resource_name to that:
1258
+ #
1259
+ # A::B::BlahDBlah -> blah_d_blah
1260
+ #
1261
+ def self.use_automatic_resource_name
1262
+ automatic_name = convert_to_snake_case(self.name.split('::')[-1])
1263
+ resource_name automatic_name
1264
+ end
1265
+
933
1266
  #
934
1267
  # The module where Chef should look for providers for this resource.
935
1268
  # The provider for `MyResource` will be looked up using
@@ -986,8 +1319,8 @@ class Chef
986
1319
  #
987
1320
  # @return [Array<Symbol>] The default actions for the resource.
988
1321
  #
989
- def self.default_action(action_name=NULL_ARG)
990
- unless action_name.equal?(NULL_ARG)
1322
+ def self.default_action(action_name=NOT_PASSED)
1323
+ unless action_name.equal?(NOT_PASSED)
991
1324
  @default_action = Array(action_name).map(&:to_sym)
992
1325
  self.allowed_actions |= @default_action
993
1326
  end
@@ -1001,9 +1334,90 @@ class Chef
1001
1334
  end
1002
1335
  end
1003
1336
  def self.default_action=(action_name)
1004
- default_action(action_name)
1337
+ default_action action_name
1338
+ end
1339
+
1340
+ #
1341
+ # Define an action on this resource.
1342
+ #
1343
+ # The action is defined as a *recipe* block that will be compiled and then
1344
+ # converged when the action is taken (when Resource is converged). The recipe
1345
+ # has access to the resource's attributes and methods, as well as the Chef
1346
+ # recipe DSL.
1347
+ #
1348
+ # Resources in the action recipe may notify and subscribe to other resources
1349
+ # within the action recipe, but cannot notify or subscribe to resources
1350
+ # in the main Chef run.
1351
+ #
1352
+ # Resource actions are *inheritable*: if resource A defines `action :create`
1353
+ # and B is a subclass of A, B gets all of A's actions. Additionally,
1354
+ # resource B can define `action :create` and call `super()` to invoke A's
1355
+ # action code.
1356
+ #
1357
+ # The first action defined (besides `:nothing`) will become the default
1358
+ # action for the resource.
1359
+ #
1360
+ # @param name [Symbol] The action name to define.
1361
+ # @param recipe_block The recipe to run when the action is taken. This block
1362
+ # takes no parameters, and will be evaluated in a new context containing:
1363
+ #
1364
+ # - The resource's public and protected methods (including attributes)
1365
+ # - The Chef Recipe DSL (file, etc.)
1366
+ # - super() referring to the parent version of the action (if any)
1367
+ #
1368
+ # @return The Action class implementing the action
1369
+ #
1370
+ def self.action(action, &recipe_block)
1371
+ action = action.to_sym
1372
+ new_action_provider_class.action(action, &recipe_block)
1373
+ self.allowed_actions += [ action ]
1374
+ default_action action if Array(default_action) == [:nothing]
1005
1375
  end
1006
1376
 
1377
+ #
1378
+ # The action provider class is an automatic `Provider` created to handle
1379
+ # actions declared by `action :x do ... end`.
1380
+ #
1381
+ # This class will be returned by `resource.provider` if `resource.provider`
1382
+ # is not set. `provider_for_action` will also use this instead of calling
1383
+ # out to `Chef::ProviderResolver`.
1384
+ #
1385
+ # If the user has not declared actions on this class or its superclasses
1386
+ # using `action :x do ... end`, then there is no need for this class and
1387
+ # `action_provider_class` will be `nil`.
1388
+ #
1389
+ # @api private
1390
+ #
1391
+ def self.action_provider_class
1392
+ @action_provider_class ||
1393
+ # If the superclass needed one, then we need one as well.
1394
+ if superclass.respond_to?(:action_provider_class) && superclass.action_provider_class
1395
+ new_action_provider_class
1396
+ end
1397
+ end
1398
+
1399
+ #
1400
+ # Ensure the action provider class actually gets created. This is called
1401
+ # when the user does `action :x do ... end`.
1402
+ #
1403
+ # @api private
1404
+ def self.new_action_provider_class
1405
+ return @action_provider_class if @action_provider_class
1406
+
1407
+ if superclass.respond_to?(:action_provider_class)
1408
+ base_provider = superclass.action_provider_class
1409
+ end
1410
+ base_provider ||= Chef::Provider
1411
+
1412
+ resource_class = self
1413
+ @action_provider_class = Class.new(base_provider) do
1414
+ use_inline_resources
1415
+ include_resource_dsl true
1416
+ define_singleton_method(:to_s) { "#{resource_class} action provider" }
1417
+ define_singleton_method(:inspect) { to_s }
1418
+ define_method(:load_current_resource) {}
1419
+ end
1420
+ end
1007
1421
 
1008
1422
  #
1009
1423
  # Internal Resource Interface (for Chef)
@@ -1076,7 +1490,7 @@ class Chef
1076
1490
 
1077
1491
  class << self
1078
1492
  # back-compat
1079
- # NOTE: that we do not support unregistering classes as descendents like
1493
+ # NOTE: that we do not support unregistering classes as descendants like
1080
1494
  # we used to for LWRP unloading because that was horrible and removed in
1081
1495
  # Chef-12.
1082
1496
  # @deprecated
@@ -1202,7 +1616,8 @@ class Chef
1202
1616
  end
1203
1617
 
1204
1618
  def provider_for_action(action)
1205
- provider = Chef::ProviderResolver.new(node, self, action).resolve.new(self, run_context)
1619
+ provider_class = Chef::ProviderResolver.new(node, self, action).resolve
1620
+ provider = provider_class.new(self, run_context)
1206
1621
  provider.action = action
1207
1622
  provider
1208
1623
  end