chef 16.9.29 → 16.12.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -3
  3. data/chef-universal-mingw32.gemspec +1 -1
  4. data/chef.gemspec +10 -1
  5. data/lib/chef/compliance/default_attributes.rb +6 -2
  6. data/lib/chef/compliance/fetcher/automate.rb +15 -4
  7. data/lib/chef/compliance/runner.rb +8 -3
  8. data/lib/chef/dsl/reboot_pending.rb +1 -1
  9. data/lib/chef/file_access_control/windows.rb +4 -4
  10. data/lib/chef/file_cache.rb +4 -4
  11. data/lib/chef/formatters/error_inspectors/resource_failure_inspector.rb +1 -1
  12. data/lib/chef/handler/json_file.rb +1 -1
  13. data/lib/chef/knife/bootstrap.rb +54 -4
  14. data/lib/chef/provider/file.rb +1 -1
  15. data/lib/chef/provider/mount.rb +7 -2
  16. data/lib/chef/provider/mount/mount.rb +1 -1
  17. data/lib/chef/provider/package.rb +2 -2
  18. data/lib/chef/provider/package/dnf/dnf_helper.py +5 -1
  19. data/lib/chef/provider/package/yum/yum_helper.py +4 -0
  20. data/lib/chef/provider/service/macosx.rb +3 -3
  21. data/lib/chef/resource.rb +27 -3
  22. data/lib/chef/resource/chef_client_cron.rb +1 -1
  23. data/lib/chef/resource/chef_client_launchd.rb +1 -1
  24. data/lib/chef/resource/windows_certificate.rb +47 -17
  25. data/lib/chef/resource_inspector.rb +5 -1
  26. data/lib/chef/shell.rb +2 -2
  27. data/lib/chef/util/dsc/configuration_generator.rb +1 -1
  28. data/lib/chef/version.rb +1 -1
  29. data/lib/chef/version_string.rb +1 -1
  30. data/spec/functional/resource/cron_spec.rb +1 -1
  31. data/spec/integration/compliance/compliance_spec.rb +2 -1
  32. data/spec/integration/recipes/resource_action_spec.rb +14 -0
  33. data/spec/spec_helper.rb +1 -0
  34. data/spec/support/platform_helpers.rb +4 -0
  35. data/spec/support/shared/unit/provider/file.rb +14 -0
  36. data/spec/unit/compliance/fetcher/automate_spec.rb +8 -0
  37. data/spec/unit/compliance/runner_spec.rb +54 -5
  38. data/spec/unit/dsl/reboot_pending_spec.rb +2 -2
  39. data/spec/unit/formatters/error_inspectors/resource_failure_inspector_spec.rb +2 -2
  40. data/spec/unit/knife/bootstrap_spec.rb +42 -3
  41. data/spec/unit/knife/supermarket_share_spec.rb +5 -6
  42. data/spec/unit/provider/mount/mount_spec.rb +52 -0
  43. data/spec/unit/provider/package/dnf/python_helper_spec.rb +7 -1
  44. data/spec/unit/provider/service/macosx_spec.rb +3 -3
  45. data/spec/unit/resource/chef_client_cron_spec.rb +8 -8
  46. data/spec/unit/resource_inspector_spec.rb +7 -2
  47. data/spec/unit/resource_spec.rb +46 -0
  48. metadata +10 -10
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a6c9dab60254692aea21043a64b8defac066d6015d89519c9f41300490a895e8
4
- data.tar.gz: 7f8211a80bc69ee02923040b39b6b43a7888dbd7e0ff2b395f40370968ddb6ea
3
+ metadata.gz: e4efb7b0bcd8972d00999b88f6ba7260cfabfc63974fcb6229b3daac599b6f72
4
+ data.tar.gz: e53c3ab65184de86e0866b539a6a6f9dc46cd50940f997f1cb8b8238e90bf1a9
5
5
  SHA512:
6
- metadata.gz: c2200a9759a88bd183f509a6ac2b8d9f8862f275d4da3bb73f67ea4aef2c39d5c053c3ed9925b9071b90d102d8cae30e83ec7d343f8e1446365250176119fb68
7
- data.tar.gz: f5afad866181ddaf33ead7ddc7b877c4c25809080e88f6f788ac78a1356cc63b930c052187ca4bfd7830ff59f6455d9cd513dd29996efb743e4afeba7595f67a
6
+ metadata.gz: 941738db47cd2aa31364f2e29075e9f26e616b80564f71746a6cacf65052f3cb0764fd7e95fdf1f59a12ece86babeb20ebe35fe720db8d0542e7f8f82e9f0350
7
+ data.tar.gz: 4a9c0b4b302d4f15a18a43688e737f3223f2d67388e7a3a27641802735d27a4d300e208776f39781524242b873756937fea87bd10d66bc5911cc472d20da05dc
data/Gemfile CHANGED
@@ -1,7 +1,7 @@
1
1
  source "https://rubygems.org"
2
2
 
3
- # pin until issues with Windows builds in 1.14.2 are resolved
4
- gem "ffi", "=1.13.1"
3
+ # 1.15+ is required for M1 mac builds
4
+ gem "ffi", ">=1.15"
5
5
 
6
6
  # Note we do not use the gemspec DSL which restricts to the
7
7
  # gemspec for the current platform and filters out other platforms
@@ -53,7 +53,7 @@ end
53
53
 
54
54
  group(:development, :test) do
55
55
  gem "rake"
56
- gem "rspec", "=3.9.0" # remove pin once https://github.com/chef/chef/issues/10817 is resolved
56
+ gem "rspec"
57
57
  gem "webmock"
58
58
  gem "fauxhai-ng" # for chef-utils gem
59
59
  end
@@ -14,7 +14,7 @@ gemspec.add_dependency "win32-service", ">= 2.1.5", "< 3.0"
14
14
  gemspec.add_dependency "wmi-lite", "~> 1.0"
15
15
  gemspec.add_dependency "win32-taskscheduler", "~> 2.0"
16
16
  gemspec.add_dependency "iso8601", ">= 0.12.1", "< 0.14" # validate 0.14 when it comes out
17
- gemspec.add_dependency "win32-certstore", "~> 0.3"
17
+ gemspec.add_dependency "win32-certstore", "~> 0.5.0" # 0.5+ required for specifying user vs. system store
18
18
  gemspec.extensions << "ext/win32-eventlog/Rakefile"
19
19
  gemspec.files += Dir.glob("{distro,ext}/**/*")
20
20
 
data/chef.gemspec CHANGED
@@ -1,4 +1,13 @@
1
1
  $:.unshift(File.dirname(__FILE__) + "/lib")
2
+ vs_path = File.expand_path("chef-utils/lib/chef-utils/version_string.rb", __dir__)
3
+
4
+ if File.exist?(vs_path)
5
+ # this is the moral equivalent of a require_relative since bundler makes require_relative here fail hard
6
+ eval(IO.read(vs_path))
7
+ else
8
+ # if the path doesn't exist then we're just in the wild gem and not in the git repo
9
+ require "chef-utils/version_string"
10
+ end
2
11
  require "chef/version"
3
12
 
4
13
  Gem::Specification.new do |s|
@@ -35,7 +44,7 @@ Gem::Specification.new do |s|
35
44
  s.add_dependency "net-ssh-multi", "~> 1.2", ">= 1.2.1"
36
45
  s.add_dependency "net-sftp", ">= 2.1.2", "< 4.0"
37
46
  s.add_dependency "ed25519", "~> 1.2" # ed25519 ssh key support
38
- s.add_dependency "bcrypt_pbkdf", "= 1.1.0.rc2" # ed25519 ssh key support
47
+ s.add_dependency "bcrypt_pbkdf", "~> 1.1" # ed25519 ssh key support
39
48
  s.add_dependency "highline", ">= 1.6.9", "< 3"
40
49
  s.add_dependency "tty-prompt", "~> 0.21" # knife ui.ask prompt
41
50
  s.add_dependency "tty-screen", "~> 0.6" # knife list
@@ -1,5 +1,5 @@
1
1
  # Author:: Stephan Renatus <srenatus@chef.io>
2
- # Copyright:: (c) 2016-2019, Chef Software Inc. <legal@chef.io>
2
+ # Copyright:: Copyright (c) Chef Software Inc. <legal@chef.io>
3
3
  #
4
4
  # Licensed under the Apache License, Version 2.0 (the "License");
5
5
  # you may not use this file except in compliance with the License.
@@ -87,7 +87,11 @@ class Chef
87
87
 
88
88
  # If enabled, a hash representation of the Chef Infra node object will be sent to Chef InSpec in an input
89
89
  # named `chef_node`.
90
- "chef_node_attribute_enabled" => false
90
+ "chef_node_attribute_enabled" => false,
91
+
92
+ # Should the built-in compliance phase run. True and false force the behavior. Nil does magic based on if you have
93
+ # profiles defined but do not have the audit cookbook enabled.
94
+ "compliance_phase" => false
91
95
  )
92
96
  end
93
97
  end
@@ -32,12 +32,12 @@ class Chef
32
32
  profile_fetch_url = target[:url]
33
33
  else
34
34
  # verifies that the target e.g base/ssh exists
35
- base_path = "/compliance/profiles/#{uri.host}#{uri.path}"
36
-
35
+ profile = sanitize_profile_name(uri)
36
+ owner, id = profile.split("/")
37
37
  profile_path = if target.respond_to?(:key?) && target.key?(:version)
38
- "#{base_path}/version/#{target[:version]}/tar"
38
+ "/compliance/profiles/#{owner}/#{id}/version/#{target[:version]}/tar"
39
39
  else
40
- "#{base_path}/tar"
40
+ "/compliance/profiles/#{owner}/#{id}/tar"
41
41
  end
42
42
 
43
43
  url = URI(Chef::Config[:data_collector][:server_url])
@@ -60,6 +60,17 @@ class Chef
60
60
  nil
61
61
  end
62
62
 
63
+ # returns a parsed url for `admin/profile` or `compliance://admin/profile`
64
+ # TODO: remove in future, copied from inspec to support older versions of inspec
65
+ def self.sanitize_profile_name(profile)
66
+ uri = if URI(profile).scheme == "compliance"
67
+ URI(profile)
68
+ else
69
+ URI("compliance://#{profile}")
70
+ end
71
+ uri.to_s.sub(%r{^compliance:\/\/}, "")
72
+ end
73
+
63
74
  def to_s
64
75
  "#{ChefUtils::Dist::Automate::PRODUCT} for #{ChefUtils::Dist::Solo::PRODUCT} Fetcher"
65
76
  end
@@ -20,10 +20,15 @@ class Chef
20
20
  # renamed from Chef Visibility in 2017, so should capture all modern versions of the audit cookbook.
21
21
  audit_cookbook_present = defined?(::Reporter::ChefAutomate)
22
22
 
23
- logger.info("#{self.class}##{__method__}: #{Inspec::Dist::PRODUCT_NAME} profiles? #{inspec_profiles.any?}")
24
- logger.info("#{self.class}##{__method__}: audit cookbook? #{audit_cookbook_present}")
23
+ logger.debug("#{self.class}##{__method__}: #{Inspec::Dist::PRODUCT_NAME} profiles? #{inspec_profiles.any?}")
24
+ logger.debug("#{self.class}##{__method__}: audit cookbook? #{audit_cookbook_present}")
25
+ logger.debug("#{self.class}##{__method__}: compliance phase attr? #{node["audit"]["compliance_phase"]}")
25
26
 
26
- inspec_profiles.any? && !audit_cookbook_present
27
+ if node["audit"]["compliance_phase"].nil?
28
+ inspec_profiles.any? && !audit_cookbook_present
29
+ else
30
+ node["audit"]["compliance_phase"]
31
+ end
27
32
  end
28
33
 
29
34
  def node=(node)
@@ -47,7 +47,7 @@ class Chef
47
47
  registry_key_exists?('HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending')
48
48
  elsif platform?("ubuntu")
49
49
  # This should work for Debian as well if update-notifier-common happens to be installed. We need an API for that.
50
- File.exists?("/var/run/reboot-required")
50
+ File.exist?("/var/run/reboot-required")
51
51
  else
52
52
  false
53
53
  end
@@ -33,7 +33,7 @@ class Chef
33
33
  module ClassMethods
34
34
  # We want to mix these in as class methods
35
35
  def writable?(path)
36
- ::File.exists?(path) && Chef::ReservedNames::Win32::File.file_access_check(
36
+ ::File.exist?(path) && Chef::ReservedNames::Win32::File.file_access_check(
37
37
  path, Chef::ReservedNames::Win32::API::Security::FILE_GENERIC_WRITE
38
38
  )
39
39
  end
@@ -136,7 +136,7 @@ class Chef
136
136
  end
137
137
 
138
138
  def should_update_dacl?
139
- return true unless ::File.exists?(file) || ::File.symlink?(file)
139
+ return true unless ::File.exist?(file) || ::File.symlink?(file)
140
140
 
141
141
  dacl = target_dacl
142
142
  existing_dacl = existing_descriptor.dacl
@@ -170,7 +170,7 @@ class Chef
170
170
  end
171
171
 
172
172
  def should_update_group?
173
- return true unless ::File.exists?(file) || ::File.symlink?(file)
173
+ return true unless ::File.exist?(file) || ::File.symlink?(file)
174
174
 
175
175
  (group = target_group) && (group != existing_descriptor.group)
176
176
  end
@@ -190,7 +190,7 @@ class Chef
190
190
  end
191
191
 
192
192
  def should_update_owner?
193
- return true unless ::File.exists?(file) || ::File.symlink?(file)
193
+ return true unless ::File.exist?(file) || ::File.symlink?(file)
194
194
 
195
195
  (owner = target_owner) && (owner != existing_descriptor.owner)
196
196
  end
@@ -79,7 +79,7 @@ class Chef
79
79
 
80
80
  file_path_array = File.split(path)
81
81
  file_name = file_path_array.pop
82
- if File.exists?(file) && File.writable?(file)
82
+ if File.exist?(file) && File.writable?(file)
83
83
  FileUtils.mv(
84
84
  file,
85
85
  File.join(create_cache_path(File.join(file_path_array), true), file_name)
@@ -112,7 +112,7 @@ class Chef
112
112
  }
113
113
  )
114
114
  cache_path = create_cache_path(path, false)
115
- raise Chef::Exceptions::FileNotFound, "Cannot find #{cache_path} for #{path}!" unless File.exists?(cache_path)
115
+ raise Chef::Exceptions::FileNotFound, "Cannot find #{cache_path} for #{path}!" unless File.exist?(cache_path)
116
116
 
117
117
  if read
118
118
  File.read(cache_path)
@@ -139,7 +139,7 @@ class Chef
139
139
  }
140
140
  )
141
141
  cache_path = create_cache_path(path, false)
142
- if File.exists?(cache_path)
142
+ if File.exist?(cache_path)
143
143
  File.unlink(cache_path)
144
144
  end
145
145
  true
@@ -186,7 +186,7 @@ class Chef
186
186
  }
187
187
  )
188
188
  full_path = create_cache_path(path, false)
189
- if File.exists?(full_path)
189
+ if File.exist?(full_path)
190
190
  true
191
191
  else
192
192
  false
@@ -66,7 +66,7 @@ class Chef
66
66
 
67
67
  @snippet ||= begin
68
68
  if (file = parse_source) && (line = parse_line(file))
69
- return nil unless ::File.exists?(file)
69
+ return nil unless ::File.exist?(file)
70
70
 
71
71
  lines = IO.readlines(file)
72
72
 
@@ -51,7 +51,7 @@ class Chef
51
51
  end
52
52
 
53
53
  def build_report_dir
54
- unless File.exists?(config[:path])
54
+ unless File.exist?(config[:path])
55
55
  FileUtils.mkdir_p(config[:path])
56
56
  File.chmod(00700, config[:path])
57
57
  end
@@ -217,6 +217,16 @@ class Chef
217
217
  description: "Execute the bootstrap via sudo with password.",
218
218
  boolean: false
219
219
 
220
+ # runtime - su user
221
+ option :su_user,
222
+ long: "--su-user NAME",
223
+ description: "The su - USER name to perform bootstrap command using a non-root user."
224
+
225
+ # runtime - su user password
226
+ option :su_password,
227
+ long: "--su-password PASSWORD",
228
+ description: "The su USER password for authentication."
229
+
220
230
  # runtime - client_builder
221
231
  option :chef_node_name,
222
232
  short: "-N NAME",
@@ -591,13 +601,31 @@ class Chef
591
601
  def perform_bootstrap(remote_bootstrap_script_path)
592
602
  ui.info("Bootstrapping #{ui.color(server_name, :bold)}")
593
603
  cmd = bootstrap_command(remote_bootstrap_script_path)
594
- r = connection.run_command(cmd) do |data|
604
+ bootstrap_run_command(cmd)
605
+ end
606
+
607
+ # Actual bootstrap command to be run on the node.
608
+ # Handles recursive calls if su USER failed to authenticate.
609
+ def bootstrap_run_command(cmd)
610
+ r = connection.run_command(cmd) do |data, channel|
595
611
  ui.msg("#{ui.color(" [#{connection.hostname}]", :cyan)} #{data}")
612
+ channel.send_data("#{config[:su_password] || config[:connection_password]}\n") if data.match?("Password:")
596
613
  end
614
+
597
615
  if r.exit_status != 0
598
616
  ui.error("The following error occurred on #{server_name}:")
599
- ui.error(r.stderr)
600
- exit 1
617
+ ui.error("#{r.stdout} #{r.stderr}".strip)
618
+ exit(r.exit_status)
619
+ end
620
+ rescue Train::UserError => e
621
+ limit ||= 0
622
+ if e.reason == :bad_su_user_password && limit < 3
623
+ limit += 1
624
+ ui.warn("Failed to authenticate su - #{config[:su_user]} to #{server_name}")
625
+ config[:su_password] = ui.ask("Enter password for su - #{config[:su_user]}@#{server_name}:", echo: false)
626
+ retry
627
+ else
628
+ raise
601
629
  end
602
630
  end
603
631
 
@@ -1082,7 +1110,17 @@ class Chef
1082
1110
  if connection.windows?
1083
1111
  "cmd.exe /C #{remote_path}"
1084
1112
  else
1085
- "sh #{remote_path}"
1113
+ cmd = "sh #{remote_path}"
1114
+
1115
+ if config[:su_user]
1116
+ # su - USER is subject to required an interactive console
1117
+ # Otherwise, it will raise: su: must be run from a terminal
1118
+ set_transport_options(pty: true)
1119
+ cmd = "su - #{config[:su_user]} -c '#{cmd}'"
1120
+ cmd = "sudo " << cmd if config[:use_sudo]
1121
+ end
1122
+
1123
+ cmd
1086
1124
  end
1087
1125
  end
1088
1126
 
@@ -1137,6 +1175,18 @@ class Chef
1137
1175
 
1138
1176
  timeout.to_i
1139
1177
  end
1178
+
1179
+ # Train::Transports::SSH::Connection#transport_options
1180
+ # Append the options to connection transport_options
1181
+ #
1182
+ # @param opts [Hash] the opts to be added to connection transport_options.
1183
+ # @return [Hash] transport_options if the opts contains any option to be set.
1184
+ #
1185
+ def set_transport_options(opts)
1186
+ return unless opts.is_a?(Hash) || !opts.empty?
1187
+
1188
+ connection&.connection&.transport_options&.merge! opts
1189
+ end
1140
1190
  end
1141
1191
  end
1142
1192
  end
@@ -338,7 +338,7 @@ class Chef
338
338
  raise Chef::Exceptions::ChecksumMismatch.new(short_cksum(new_resource.checksum), short_cksum(tempfile_checksum))
339
339
  end
340
340
 
341
- if tempfile
341
+ if tempfile && contents_changed?
342
342
  new_resource.verify.each do |v|
343
343
  unless v.verify(tempfile.path)
344
344
  backupfile = "#{Chef::Config[:file_cache_path]}/failed_validations/#{::File.basename(tempfile.path)}"
@@ -175,8 +175,13 @@ class Chef
175
175
 
176
176
  # Returns the new_resource device as per device_type
177
177
  def device_fstab
178
- # Removed "/" from the end of str, because it was causing idempotency issue.
179
- device = @new_resource.device == "/" ? @new_resource.device : @new_resource.device.chomp("/")
178
+ # Removed "/" from the end of str unless it's a network mount, because it was causing idempotency issue.
179
+ device =
180
+ if @new_resource.device == "/" || @new_resource.device.match?(":/$")
181
+ @new_resource.device
182
+ else
183
+ @new_resource.device.chomp("/")
184
+ end
180
185
  case @new_resource.device_type
181
186
  when :device
182
187
  device
@@ -203,7 +203,7 @@ class Chef
203
203
  end
204
204
  end
205
205
  # Removed "/" from the end of str, because it was causing idempotency issue.
206
- @real_device == "/" ? @real_device : @real_device.chomp("/")
206
+ (@real_device == "/" || @real_device.match?(":/$")) ? @real_device : @real_device.chomp("/")
207
207
  end
208
208
 
209
209
  def device_logstring
@@ -446,8 +446,8 @@ class Chef
446
446
  # requested new_resource.version constraints
447
447
  logger.trace("#{new_resource} has no existing installed version. Installing install #{candidate_version}")
448
448
  target_version_array.push(candidate_version)
449
- elsif version_equals?(current_version, new_version)
450
- # this is a short-circuit to avoid needing to (expensively) query the candidate_version which must come later
449
+ elsif !use_magic_version? && version_equals?(current_version, new_version)
450
+ # this is a short-circuit (mostly for the rubygems provider) to avoid needing to expensively query the candidate_version which must come later
451
451
  logger.trace("#{new_resource} #{package_name} #{new_version} is already installed")
452
452
  target_version_array.push(nil)
453
453
  elsif candidate_version.nil?
@@ -64,7 +64,7 @@ def version_tuple(versionstr):
64
64
  tmp = versionstr[colon_index + 1:dash_index]
65
65
  if tmp != '':
66
66
  v = tmp
67
- arch_index = versionstr.find('.', dash_index)
67
+ arch_index = versionstr.rfind('.', dash_index)
68
68
  if arch_index > 0:
69
69
  r = versionstr[dash_index + 1:arch_index]
70
70
  else:
@@ -168,6 +168,10 @@ try:
168
168
  setup_exit_handler()
169
169
  line = inpipe.readline()
170
170
 
171
+ # only way to detect EOF in python
172
+ if line == "":
173
+ break
174
+
171
175
  try:
172
176
  command = json.loads(line)
173
177
  except ValueError:
@@ -196,6 +196,10 @@ try:
196
196
  setup_exit_handler()
197
197
  line = inpipe.readline()
198
198
 
199
+ # only way to detect EOF in python
200
+ if line == "":
201
+ break
202
+
199
203
  try:
200
204
  command = json.loads(line)
201
205
  except ValueError, e:
@@ -169,12 +169,12 @@ class Chef
169
169
 
170
170
  def load_service
171
171
  session = @session_type ? "-S #{@session_type} " : ""
172
- cmd = "launchctl load -w " + session + @plist
172
+ cmd = "/bin/launchctl load -w " + session + @plist
173
173
  shell_out_as_user(cmd)
174
174
  end
175
175
 
176
176
  def unload_service
177
- cmd = "launchctl unload -w " + @plist
177
+ cmd = "/bin/launchctl unload -w " + @plist
178
178
  shell_out_as_user(cmd)
179
179
  end
180
180
 
@@ -190,7 +190,7 @@ class Chef
190
190
  def set_service_status
191
191
  return if @plist.nil? || @service_label.to_s.empty?
192
192
 
193
- cmd = "launchctl list #{@service_label}"
193
+ cmd = "/bin/launchctl list #{@service_label}"
194
194
  res = shell_out_as_user(cmd)
195
195
 
196
196
  if res.exitstatus == 0
data/lib/chef/resource.rb CHANGED
@@ -1062,6 +1062,7 @@ class Chef
1062
1062
  # action for the resource.
1063
1063
  #
1064
1064
  # @param name [Symbol] The action name to define.
1065
+ # @param description [String] optional description for the action
1065
1066
  # @param recipe_block The recipe to run when the action is taken. This block
1066
1067
  # takes no parameters, and will be evaluated in a new context containing:
1067
1068
  #
@@ -1071,14 +1072,37 @@ class Chef
1071
1072
  #
1072
1073
  # @return The Action class implementing the action
1073
1074
  #
1074
- def self.action(action, &recipe_block)
1075
+ def self.action(action, description: nil, &recipe_block)
1075
1076
  action = action.to_sym
1076
1077
  declare_action_class
1077
1078
  action_class.action(action, &recipe_block)
1078
1079
  self.allowed_actions += [ action ]
1080
+ # Accept any non-nil description, which will correctly override
1081
+ # any specific inherited description.
1082
+ action_descriptions[action] = description unless description.nil?
1079
1083
  default_action action if Array(default_action) == [:nothing]
1080
1084
  end
1081
1085
 
1086
+ # Retrieve the description for a resource's action, if
1087
+ # any description has been included in the definition.
1088
+ #
1089
+ # @param action [Symbol,String] the action name
1090
+ # @return the description of the action provided, or nil if no description
1091
+ # was defined
1092
+ def self.action_description(action)
1093
+ action_descriptions[action.to_sym]
1094
+ end
1095
+
1096
+ # @api private
1097
+ #
1098
+ # @return existing action description hash, or newly-initialized
1099
+ # hash containing action descriptions inherited from parent Resource,
1100
+ # if any.
1101
+ def self.action_descriptions
1102
+ @action_descriptions ||=
1103
+ superclass.respond_to?(:action_descriptions) ? superclass.action_descriptions.dup : { nothing: nil }
1104
+ end
1105
+
1082
1106
  # Define a method to load up this resource's properties with the current
1083
1107
  # actual values.
1084
1108
  #
@@ -1196,9 +1220,9 @@ class Chef
1196
1220
  #
1197
1221
 
1198
1222
  # FORBIDDEN_IVARS do not show up when the resource is converted to JSON (ie. hidden from data_collector and sending to the chef server via #to_json/to_h/as_json/inspect)
1199
- FORBIDDEN_IVARS = %i{@run_context @logger @not_if @only_if @enclosing_provider @description @introduced @examples @validation_message @deprecated @default_description @skip_docs @executed_by_runner}.freeze
1223
+ FORBIDDEN_IVARS = %i{@run_context @logger @not_if @only_if @enclosing_provider @description @introduced @examples @validation_message @deprecated @default_description @skip_docs @executed_by_runner @action_descriptions}.freeze
1200
1224
  # HIDDEN_IVARS do not show up when the resource is displayed to the user as text (ie. in the error inspector output via #to_text)
1201
- HIDDEN_IVARS = %i{@allowed_actions @resource_name @source_line @run_context @logger @name @not_if @only_if @elapsed_time @enclosing_provider @description @introduced @examples @validation_message @deprecated @default_description @skip_docs @executed_by_runner}.freeze
1225
+ HIDDEN_IVARS = %i{@allowed_actions @resource_name @source_line @run_context @logger @name @not_if @only_if @elapsed_time @enclosing_provider @description @introduced @examples @validation_message @deprecated @default_description @skip_docs @executed_by_runner @action_descriptions}.freeze
1202
1226
 
1203
1227
  include Chef::Mixin::ConvertToClassName
1204
1228
  extend Chef::Mixin::ConvertToClassName