chef 18.10.17 → 18.11.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +15 -7
  3. data/README.md +0 -1
  4. data/chef-universal-mingw-ucrt.gemspec +4 -2
  5. data/chef.gemspec +15 -12
  6. data/lib/chef/compliance/runner.rb +19 -1
  7. data/lib/chef/dsl/rest_resource.rb +223 -16
  8. data/lib/chef/formatters/error_description.rb +8 -1
  9. data/lib/chef/http/authenticator.rb +17 -4
  10. data/lib/chef/mixin/template.rb +24 -2
  11. data/lib/chef/monkey_patches/reline-windows.rb +116 -0
  12. data/lib/chef/provider/file/content.rb +3 -2
  13. data/lib/chef/provider/file.rb +1 -1
  14. data/lib/chef/provider/ifconfig/redhat.rb +1 -1
  15. data/lib/chef/provider/package/dnf/dnf_helper.py +355 -65
  16. data/lib/chef/provider/package/dnf/python_helper.rb +7 -4
  17. data/lib/chef/provider/package/dnf.rb +25 -6
  18. data/lib/chef/provider/package/yum.rb +1 -1
  19. data/lib/chef/resource/_rest_resource.rb +4 -2
  20. data/lib/chef/resource/archive_file.rb +52 -18
  21. data/lib/chef/resource/chef_client_config.rb +185 -19
  22. data/lib/chef/resource/chef_vault_secret.rb +1 -1
  23. data/lib/chef/resource/chocolatey_installer.rb +31 -13
  24. data/lib/chef/resource/chocolatey_package.rb +1 -1
  25. data/lib/chef/resource/habitat/habitat_package.rb +1 -1
  26. data/lib/chef/resource/habitat/habitat_sup.rb +2 -2
  27. data/lib/chef/resource/habitat_config.rb +1 -1
  28. data/lib/chef/resource/habitat_service.rb +1 -1
  29. data/lib/chef/resource/openssl_ec_private_key.rb +1 -1
  30. data/lib/chef/resource/openssl_ec_public_key.rb +2 -2
  31. data/lib/chef/resource/openssl_rsa_private_key.rb +1 -1
  32. data/lib/chef/resource/openssl_rsa_public_key.rb +2 -2
  33. data/lib/chef/resource/openssl_x509_certificate.rb +1 -1
  34. data/lib/chef/resource/openssl_x509_request.rb +1 -1
  35. data/lib/chef/resource/powershell_package_source.rb +1 -1
  36. data/lib/chef/resource/rhsm_register.rb +1 -1
  37. data/lib/chef/resource/windows_ad_join.rb +1 -1
  38. data/lib/chef/resource/windows_certificate.rb +1 -1
  39. data/lib/chef/resource/windows_service.rb +1 -1
  40. data/lib/chef/resource/windows_task.rb +1 -1
  41. data/lib/chef/resource/yum_package.rb +1 -1
  42. data/lib/chef/resource/yum_repository.rb +2 -2
  43. data/lib/chef/shell.rb +7 -0
  44. data/lib/chef/version.rb +1 -1
  45. metadata +59 -34
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea642da5a95174bc30b1f2010f05945c9a7847e1082f88c415bf5d9807f39227
4
- data.tar.gz: 0edf60effa635270d37059fd3e315d77cd97f852fce7547569fae96b13ccaf4c
3
+ metadata.gz: bf0beb126b5eeb735670c67b48e79aae54a6ba25760f99babf2468fe6ff78437
4
+ data.tar.gz: 99033f783a17e8870ccfc0f20ef3489cfed936f2882d7030beaf0d78971e8a85
5
5
  SHA512:
6
- metadata.gz: 29095f460f5a93f61dfc67cb21a76bdedd4ceb154a347583424b1d07d7f23e707cafd6a28677d81b2d9753eed323fc78ae8474027d86fb3fee9f7d3910560997
7
- data.tar.gz: f1f1869359071735d971e79faa2f2a5a0ab11ddad463c278ae1ff47a0b80db4e1c202d71ff7e10b7c6a761e45cb1c79b958ae3bbd0f02dec8901240c178cffa2
6
+ metadata.gz: c3e593233b5da46f034602581455a9ee6d32eaf77a1bbcedc91fb17d5826081c139480192887fdb3804cda29862383fe46fa74bae56af7947096d0adc42493e8
7
+ data.tar.gz: d80743280c5469a1c869fe02cce993d0649fdfecf6a3ca8a66892465b5b3239aba64378579c659800122aeb408745efd11712ff6dddb46b75d92ac000a36dc30
data/Gemfile CHANGED
@@ -7,7 +7,7 @@ gem "ohai", git: "https://github.com/chef/ohai.git", branch: "18-stable"
7
7
  # Nwed to file a bug with rest-client. In the meantime, we can use this until they accept the update.
8
8
  gem "rest-client", git: "https://github.com/chef/rest-client", branch: "jfm/ucrt_update1"
9
9
 
10
- gem "ffi", ">= 1.15.5", "<= 1.17.0"
10
+ gem "ffi", ">= 1.15.5", "<= 1.18.0"
11
11
  gem "chef-utils", path: File.expand_path("chef-utils", __dir__) if File.exist?(File.expand_path("chef-utils", __dir__))
12
12
  gem "chef-config", path: File.expand_path("chef-config", __dir__) if File.exist?(File.expand_path("chef-config", __dir__))
13
13
 
@@ -31,15 +31,19 @@ group(:omnibus_package) do
31
31
  gem "rb-readline"
32
32
  gem "chef-vault"
33
33
 
34
- gem "inspec-core-bin", ">= 5", "< 6"
34
+ gem "inspec-core-bin", "~> 5.24", "< 6"
35
35
  end
36
36
 
37
37
  group(:omnibus_package, :pry) do
38
38
  # Locked because pry-byebug is broken with 13+.
39
39
  # some work is ongoing? https://github.com/deivid-rodriguez/pry-byebug/issues/343
40
- gem "pry", "= 0.13.0"
41
- # byebug does not install on freebsd on ruby 3.0
42
- gem "pry-byebug" unless RUBY_PLATFORM.match?(/freebsd/i)
40
+ gem "pry", "0.15.2"
41
+ # byebug does not install on freebsd on ruby 3.0; byebug >= 12 requires ruby >= 3.1
42
+ # which is incompatible with the AIX omnibus toolchain (ruby 3.0.3)
43
+ unless RUBY_PLATFORM.match?(/freebsd|aix/i)
44
+ gem "byebug", "< 13"
45
+ gem "pry-byebug", "< 3.12"
46
+ end
43
47
  gem "pry-stack_explorer"
44
48
  end
45
49
 
@@ -60,7 +64,7 @@ group(:development, :test) do
60
64
  gem "rake", ">= 12.3.3"
61
65
  gem "rspec"
62
66
  gem "webmock"
63
- gem "crack", "< 0.4.6" # due to https://github.com/jnunemaker/crack/pull/75
67
+ gem "crack", "< 1.0.2" # due to https://github.com/jnunemaker/crack/pull/75
64
68
  gem "fauxhai-ng" # for chef-utils gem
65
69
  end
66
70
 
@@ -70,4 +74,8 @@ instance_eval(ENV["GEMFILE_MOD"]) if ENV["GEMFILE_MOD"]
70
74
 
71
75
  # If you want to load debugging tools into the bundle exec sandbox,
72
76
  # add these additional dependencies into Gemfile.local
73
- eval_gemfile("./Gemfile.local") if File.exist?("./Gemfile.local")
77
+ #
78
+ # But doing eval_gemfile("./Gemfile.local") breaks dependabot, so a
79
+ # bit of indirection here
80
+ local_gemfile = File.join(__dir__, "Gemfile.local")
81
+ eval(File.read(local_gemfile)) if File.exist?(local_gemfile)
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
1
  # Chef Infra
2
- [![Code Climate](https://codeclimate.com/github/chef/chef.svg)](https://codeclimate.com/github/chef/chef)
3
2
  [![Build Status](https://badge.buildkite.com/c82093430ceec7d27af05febb9dcafe3aa331fff9d74c0ab9d.svg?branch=main)](https://buildkite.com/chef-oss/chef-chef-main-verify)
4
3
  [![Gem Version](https://badge.fury.io/rb/chef.svg)](https://badge.fury.io/rb/chef)
5
4
  [![](https://img.shields.io/badge/Release%20Policy-Cadence%20Release-brightgreen.svg)](https://github.com/chef/chef/blob/main/docs/dev/design_documents/client_release_cadence.md)
@@ -1,4 +1,6 @@
1
- gemspec = instance_eval(File.read(File.expand_path("chef.gemspec", __dir__)))
1
+ # rubocop:disable Chef/Ruby/GemspecLicense
2
+ # License is in the included gemspec.
3
+ gemspec = Gem::Specification.load(File.expand_path("chef.gemspec", __dir__))
2
4
 
3
5
  gemspec.platform = Gem::Platform.new(%w{universal mingw-ucrt})
4
6
 
@@ -6,7 +8,7 @@ gemspec.add_dependency "win32-api", "~> 1.10.0"
6
8
  gemspec.add_dependency "win32-event", "~> 0.6.1"
7
9
  # TODO: Relax this pin and make the necessary updaets. The issue originally
8
10
  # leading to this pin has been fixed in 0.6.5.
9
- gemspec.add_dependency "win32-eventlog", "0.6.3"
11
+ gemspec.add_dependency "win32-eventlog", "0.6.7"
10
12
  gemspec.add_dependency "win32-mmap", "~> 0.4.1"
11
13
  gemspec.add_dependency "win32-mutex", "~> 0.4.2"
12
14
  gemspec.add_dependency "win32-process", "~> 0.9"
data/chef.gemspec CHANGED
@@ -25,10 +25,11 @@ Gem::Specification.new do |s|
25
25
  s.email = "adam@chef.io"
26
26
  s.homepage = "https://www.chef.io"
27
27
 
28
+ # help dependabot by specifying a default. If the default is in
29
+ # the 'else', dependabot misses it and falls back to a very old ruby
30
+ s.required_ruby_version = ">= 3.1.0"
28
31
  if RUBY_PLATFORM =~ /aix/
29
32
  s.required_ruby_version = ">= 3.0.3"
30
- else
31
- s.required_ruby_version = ">= 3.1.0"
32
33
  end
33
34
 
34
35
  s.add_dependency "chef-config", "= #{Chef::VERSION}"
@@ -47,35 +48,37 @@ Gem::Specification.new do |s|
47
48
 
48
49
  # AIX Gemfile.aix.lock uses this, but currently a manual process
49
50
  if RUBY_PLATFORM.include?("aix") || ENV["GENERATE_AIX"] == "true"
50
- s.add_dependency "inspec-core", ">= 5", "<= 5.22.80"
51
+ s.add_dependency "inspec-core", ">= 5", "< 8"
51
52
  else
52
- s.add_dependency "inspec-core", ">= 5", "< 6"
53
+ s.add_dependency "inspec-core", ">= 5", "< 8"
53
54
  end
54
55
 
55
56
  s.add_dependency "ffi", ">= 1.15.5", "<= 1.16.3"
56
- s.add_dependency "ffi-yajl", "~> 2.2"
57
+ s.add_dependency "ffi-yajl", ">= 2.2", "< 4.0"
57
58
  s.add_dependency "net-sftp", ">= 2.1.2", "< 5.0" # remote_file resource
58
59
  s.add_dependency "net-ftp" # remote_file resource
59
60
  s.add_dependency "erubis", "~> 2.7" # template resource / cookbook syntax check
60
- s.add_dependency "diff-lcs", ">= 1.2.4", "!= 1.4.0", "< 1.6.0" # 1.4 breaks output. Used in lib/chef/util/diff
61
+ s.add_dependency "diff-lcs", ">= 1.2.4", "!= 1.4.0", "< 2.1.0" # 1.4 breaks output. Used in lib/chef/util/diff
61
62
  s.add_dependency "ffi-libarchive", "~> 1.0", ">= 1.0.3" # archive_file resource
62
- s.add_dependency "chef-zero", ">= 15.0.21", "< 15.2"
63
+ s.add_dependency "chef-zero", "~> 15.1.11"
63
64
  s.add_dependency "chef-vault" # chef-vault resources and helpers
64
65
 
65
66
  s.add_dependency "plist", "~> 3.2" # launchd, dscl/mac user, macos_userdefaults, osx_profile and plist resources
66
67
  s.add_dependency "iniparse", "~> 1.4" # systemd_unit resource
67
68
  s.add_dependency "addressable"
68
69
  s.add_dependency "syslog-logger", "~> 1.6"
69
- s.add_dependency "uuidtools", ">= 2.1.5", "< 3.0" # osx_profile resource
70
- s.add_dependency "unf_ext", "~> 0.0.8.2" # older platforms
71
- s.add_dependency "uri", "~> 1.0.4" # This is used by net-http. We are forcing a version to get past a CVE in 1.0.2
70
+ s.add_dependency "uuidtools", ">= 2.1.5", "< 4.0" # osx_profile resource
71
+ s.add_dependency "unf_ext", ">= 0.0.8.2", "< 0.0.10.0" # older platforms
72
+ s.add_dependency "uri", ">= 1.0.4", "< 1.2.0" # This is used by net-http. We are forcing a version to get past a CVE in 1.0.2
72
73
  s.add_dependency "corefoundation", "~> 0.3.4" # macos_userdefaults resource
73
74
 
74
75
  s.add_dependency "proxifier2", "~> 1.1"
75
76
 
76
- s.add_dependency "aws-sdk-s3", "~> 1.91" # s3 recipe-url support
77
+ # 1.219 depends (transitively) on bigdecimal 4.1.2 which dodesn't compile
78
+ # on CentOS7
79
+ s.add_dependency "aws-sdk-s3", ">= 1.218.0", "< 1.219.0"
77
80
  s.add_dependency "aws-sdk-secretsmanager", "~> 1.46"
78
- s.add_dependency "vault", "~> 0.18.2" # hashi vault official client gem
81
+ s.add_dependency "vault", ">= 0.18.2", "< 0.21.0" # hashi vault official client gem
79
82
 
80
83
  s.bindir = "bin"
81
84
  s.executables = %w{ }
@@ -72,14 +72,32 @@ class Chef
72
72
  logger.debug("#{self.class}##{__method__}: enabling Compliance Phase")
73
73
 
74
74
  report_with_interval
75
+ @compliance_phase_completed = true
75
76
  end
76
77
 
77
- def run_failed(_exception, _run_status)
78
+ def run_failed(exception, _run_status)
78
79
  # If the run has failed because our own validation of compliance
79
80
  # phase configuration has failed, we don't want to submit a report
80
81
  # because we're still not configured correctly.
81
82
  return unless enabled? && @validation_passed
82
83
 
84
+ # A reboot exception is not a real failure — it is an expected outcome
85
+ # when a reboot resource fires :reboot_now. In that case run_completed
86
+ # has already executed the Compliance Phase, so running it again here
87
+ # would produce a duplicate scan (and the second scan may be killed by
88
+ # the pending reboot).
89
+ if exception.is_a?(Chef::Exceptions::Reboot)
90
+ logger.debug("#{self.class}##{__method__}: skipping Compliance Phase because a reboot was requested")
91
+ return
92
+ end
93
+
94
+ # Guard against any other code path that might trigger both run_completed
95
+ # and run_failed in the same client run.
96
+ if @compliance_phase_completed
97
+ logger.debug("#{self.class}##{__method__}: skipping Compliance Phase because it has already run in this client run")
98
+ return
99
+ end
100
+
83
101
  logger.debug("#{self.class}##{__method__}: enabling Compliance Phase")
84
102
 
85
103
  report_with_interval
@@ -20,56 +20,263 @@ require "chef/constants" unless defined?(NOT_PASSED)
20
20
  class Chef
21
21
  module DSL
22
22
  module RestResource
23
+ private
24
+
25
+ def inherited_accessor(name)
26
+ superclass.public_send(name) if superclass.respond_to?(name)
27
+ end
28
+
29
+ public
30
+
31
+ # Define property mapping between resource properties and JSON API fields
32
+ #
33
+ # Maps resource properties to their corresponding locations in the JSON
34
+ # API payload. Supports both simple 1:1 mappings and complex transformations.
35
+ #
36
+ # @param rest_property_map [Hash, Array, NOT_PASSED] The property mapping configuration
37
+ # - Hash: Keys are property symbols, values are JMESPath strings or symbol references
38
+ # - Array: Simple 1:1 mapping (property name matches JSON field name)
39
+ # - NOT_PASSED: Acts as getter, returns current mapping
40
+ #
41
+ # @return [Hash] The current property mapping
42
+ #
43
+ # @example Simple 1:1 mapping
44
+ # rest_property_map [:username, :email, :role]
45
+ # # Equivalent to: { username: 'username', email: 'email', role: 'role' }
46
+ #
47
+ # @example Nested JSON paths
48
+ # rest_property_map({
49
+ # username: 'user.name',
50
+ # email: 'user.contact.email',
51
+ # role: 'permissions.role'
52
+ # })
53
+ #
54
+ # @example Custom mapping functions
55
+ # rest_property_map({
56
+ # username: 'username',
57
+ # tags: :tags_mapping # Calls tags_from_json and tags_to_json methods
58
+ # })
59
+ #
60
+ # @see #json_to_property Method that uses this mapping to extract values
61
+ # @see #property_to_json Method that uses this mapping to create JSON
23
62
  def rest_property_map(rest_property_map = NOT_PASSED)
24
- if rest_property_map != NOT_PASSED
63
+ unless rest_property_map.equal?(NOT_PASSED)
25
64
  rest_property_map = rest_property_map.to_h { |k| [k.to_sym, k] } if rest_property_map.is_a? Array
26
65
 
27
66
  @rest_property_map = rest_property_map
28
67
  end
29
- @rest_property_map
68
+ @rest_property_map || inherited_accessor(:rest_property_map)
30
69
  end
31
70
 
32
- # URL to collection
71
+ # Define the REST API collection URL
72
+ #
73
+ # Sets the base URL for the collection of resources. This URL is used for:
74
+ # - GET requests to list all resources
75
+ # - POST requests to create new resources
76
+ #
77
+ # @param rest_api_collection [String, NOT_PASSED] The collection URL path
78
+ # - String: Must be an absolute path starting with '/'
79
+ # - NOT_PASSED: Acts as getter, returns current collection URL
80
+ #
81
+ # @return [String] The current collection URL
82
+ #
83
+ # @raise [ArgumentError] If the path doesn't start with '/'
84
+ #
85
+ # @example
86
+ # rest_api_collection '/api/v1/users'
87
+ # # GET /api/v1/users # List all users
88
+ # # POST /api/v1/users # Create new user
33
89
  def rest_api_collection(rest_api_collection = NOT_PASSED)
34
- if rest_api_collection != NOT_PASSED
90
+ unless rest_api_collection.equal?(NOT_PASSED)
35
91
  raise ArgumentError, "You must pass an absolute path to rest_api_collection" unless rest_api_collection.start_with? "/"
36
92
 
37
93
  @rest_api_collection = rest_api_collection
38
94
  end
39
95
 
40
- @rest_api_collection
96
+ @rest_api_collection || inherited_accessor(:rest_api_collection)
41
97
  end
42
98
 
43
- # RFC6570-Templated URL to document
99
+ # Define the REST API document URL with RFC 6570 template support
100
+ #
101
+ # Sets the URL pattern for individual resource documents. The URL can include
102
+ # RFC 6570 URI templates that will be expanded using property values. This URL
103
+ # is used for:
104
+ # - GET requests to retrieve a specific resource
105
+ # - PATCH/PUT requests to update a resource
106
+ # - DELETE requests to remove a resource
107
+ #
108
+ # @param rest_api_document [String, NOT_PASSED] The document URL pattern
109
+ # - String: Must be an absolute path starting with '/'
110
+ # - Can include RFC 6570 templates like {property_name}
111
+ # - NOT_PASSED: Acts as getter, returns current document URL
112
+ #
113
+ # @param first_element_only [Boolean] If true and API returns array, extract first element only
114
+ #
115
+ # @return [String] The current document URL pattern
116
+ #
117
+ # @raise [ArgumentError] If the path doesn't start with '/'
118
+ #
119
+ # @example Path-based URL
120
+ # rest_api_document '/api/v1/users/{username}'
121
+ # # With username='john':
122
+ # # GET /api/v1/users/john
123
+ # # PATCH /api/v1/users/john
124
+ # # DELETE /api/v1/users/john
125
+ #
126
+ # @example Query-based URL
127
+ # rest_api_document '/api/v1/users?name={username}&email={email}'
128
+ # # With username='john', email='john@example.com':
129
+ # # GET /api/v1/users?name=john&email=john@example.com
130
+ #
131
+ # @example With first_element_only
132
+ # rest_api_document '/api/v1/users?name={username}', first_element_only: true
133
+ # # API returns: [{"name": "john", ...}]
134
+ # # Resource sees: {"name": "john", ...}
135
+ #
136
+ # @see https://tools.ietf.org/html/rfc6570 RFC 6570 URI Template specification
44
137
  def rest_api_document(rest_api_document = NOT_PASSED, first_element_only: false)
45
- if rest_api_document != NOT_PASSED
138
+ unless rest_api_document.equal?(NOT_PASSED)
46
139
  raise ArgumentError, "You must pass an absolute path to rest_api_document" unless rest_api_document.start_with? "/"
47
140
 
48
141
  @rest_api_document = rest_api_document
49
142
  @rest_api_document_first_element_only = first_element_only
50
143
  end
51
- @rest_api_document
144
+ @rest_api_document ||
145
+ inherited_accessor(:rest_api_document) ||
146
+ (rest_api_collection && rest_identity_property ? "#{rest_api_collection}/{#{rest_identity_property}}" : nil)
52
147
  end
53
148
 
54
- # Explicit REST document identity mapping
149
+ # Define explicit identity mapping for resource identification
150
+ #
151
+ # Explicitly specifies which JSON fields and resource properties uniquely
152
+ # identify a resource. Use this when automatic identity inference from the
153
+ # document URL is insufficient or when dealing with composite keys.
154
+ #
155
+ # If not specified, the identity is automatically inferred from RFC 6570
156
+ # templates in the document URL.
157
+ #
158
+ # @param rest_identity_map [Hash, NOT_PASSED] Mapping of JSON paths to properties
159
+ # - Hash: Keys are JMESPath-like strings, values are property symbols
160
+ # - NOT_PASSED: Acts as getter, returns current identity mapping
161
+ #
162
+ # @return [Hash, nil] The current identity mapping or nil if using auto-inference
163
+ #
164
+ # @example Simple identity
165
+ # property :username, String, identity: true
166
+ # rest_identity_map({ 'username' => :username })
167
+ #
168
+ # @example Composite identity
169
+ # property :name, String
170
+ # property :namespace, String
171
+ #
172
+ # rest_identity_map({
173
+ # 'name' => :name,
174
+ # 'namespace' => :namespace
175
+ # })
176
+ #
177
+ # @example Nested identity fields
178
+ # property :user_id, String
179
+ # property :org_id, String
180
+ #
181
+ # rest_identity_map({
182
+ # 'user.id' => :user_id,
183
+ # 'organization.id' => :org_id
184
+ # })
55
185
  def rest_identity_map(rest_identity_map = NOT_PASSED)
56
- @rest_identity_map = rest_identity_map if rest_identity_map != NOT_PASSED
57
- @rest_identity_map
186
+ @rest_identity_map = rest_identity_map unless rest_identity_map.equal?(NOT_PASSED)
187
+ @rest_identity_map || inherited_accessor(:rest_identity_map)
58
188
  end
59
189
 
60
- # Mark up properties for POST only, not PATCH/PUT
190
+ # Declare properties that should only be sent during resource creation
191
+ #
192
+ # Specifies which properties should only be included in POST (create) requests
193
+ # and excluded from PATCH/PUT (update) requests. This is useful for properties
194
+ # that can only be set during initial creation or would cause errors if
195
+ # included in updates.
196
+ #
197
+ # @param rest_post_only_properties [Symbol, Array<Symbol>, NOT_PASSED] Properties to mark
198
+ # - Symbol: Single property name
199
+ # - Array<Symbol>: Multiple property names
200
+ # - NOT_PASSED: Acts as getter, returns current post-only properties
201
+ #
202
+ # @return [Array<Symbol>] Current list of post-only properties (empty array if none)
203
+ #
204
+ # @example Single property
205
+ # property :password, String, sensitive: true
206
+ # property :username, String
207
+ #
208
+ # rest_post_only_properties :password
209
+ # # Password only sent when creating user, not when updating
210
+ #
211
+ # @example Multiple properties
212
+ # property :password, String, sensitive: true
213
+ # property :initial_role, String
214
+ # property :username, String
215
+ #
216
+ # rest_post_only_properties [:password, :initial_role]
217
+ #
218
+ # @example Common use cases
219
+ # # Passwords that can't be updated via API
220
+ # rest_post_only_properties :admin_password
221
+ #
222
+ # # Resource size that can't be changed after creation
223
+ # rest_post_only_properties :disk_size_gb
224
+ #
225
+ # # Initialization parameters
226
+ # rest_post_only_properties [:template_id, :source_snapshot]
61
227
  def rest_post_only_properties(rest_post_only_properties = NOT_PASSED)
62
- if rest_post_only_properties != NOT_PASSED
228
+ unless rest_post_only_properties.equal?(NOT_PASSED)
63
229
  @rest_post_only_properties = Array(rest_post_only_properties).map(&:to_sym)
64
230
  end
65
- @rest_post_only_properties || []
231
+ @rest_post_only_properties || inherited_accessor(:rest_post_only_properties) || []
66
232
  end
67
233
 
68
234
  def rest_api_document_first_element_only(rest_api_document_first_element_only = NOT_PASSED)
69
- if rest_api_document_first_element_only != NOT_PASSED
235
+ unless rest_api_document_first_element_only.equal?(NOT_PASSED)
70
236
  @rest_api_document_first_element_only = rest_api_document_first_element_only
71
237
  end
72
- @rest_api_document_first_element_only
238
+ @rest_api_document_first_element_only || inherited_accessor(:rest_api_document_first_element_only)
239
+ end
240
+
241
+ # Define the base URL for the REST API
242
+ #
243
+ # Sets the base endpoint URL that is prepended to all collection and document
244
+ # URLs. This allows resource definitions to be self-contained without requiring
245
+ # the Train transport endpoint to be pre-configured.
246
+ #
247
+ # @param rest_api_endpoint [String, NOT_PASSED] The base URL of the REST API
248
+ # - NOT_PASSED: Acts as getter, returns current endpoint URL
249
+ #
250
+ # @return [String, nil] The current endpoint URL
251
+ #
252
+ # @example
253
+ # rest_api_endpoint "https://api.example.com"
254
+ # rest_api_collection "/api/v1/users"
255
+ # # GET https://api.example.com/api/v1/users
256
+ def rest_api_endpoint(rest_api_endpoint = NOT_PASSED)
257
+ @rest_api_endpoint = rest_api_endpoint unless rest_api_endpoint.equal?(NOT_PASSED)
258
+ @rest_api_endpoint || inherited_accessor(:rest_api_endpoint)
259
+ end
260
+
261
+ # Declare the property that uniquely identifies a resource in the REST API
262
+ #
263
+ # Sets the identity property for the resource and auto-generates the document
264
+ # URL as "#{rest_api_collection}/{property}" when no explicit rest_api_document
265
+ # is provided. This is a convenience alternative to setting rest_api_document
266
+ # manually.
267
+ #
268
+ # @param property [Symbol, NOT_PASSED] The property name used as the resource identifier
269
+ # - NOT_PASSED: Acts as getter, returns current identity property
270
+ #
271
+ # @return [Symbol, nil] The current identity property name
272
+ #
273
+ # @example
274
+ # rest_api_collection "/api/v1/users"
275
+ # rest_identity_property :username
276
+ # # Auto-generates rest_api_document as "/api/v1/users/{username}"
277
+ def rest_identity_property(property = NOT_PASSED)
278
+ @rest_identity_property = property unless property.equal?(NOT_PASSED)
279
+ @rest_identity_property || inherited_accessor(:rest_identity_property)
73
280
  end
74
281
 
75
282
  end
@@ -59,10 +59,17 @@ class Chef
59
59
 
60
60
  private
61
61
 
62
+ MAX_DISPLAY_TEXT_LENGTH = 10_000
63
+
62
64
  def display_section(heading, text, out)
63
65
  out.puts heading
64
66
  out.puts "-" * heading.size
65
- out.puts text
67
+ if text.is_a?(String) && text.length > MAX_DISPLAY_TEXT_LENGTH
68
+ out.puts text[0, MAX_DISPLAY_TEXT_LENGTH]
69
+ out.puts "\n... [truncated #{text.length - MAX_DISPLAY_TEXT_LENGTH} characters of output]"
70
+ else
71
+ out.puts text
72
+ end
66
73
  out.puts "\n"
67
74
  end
68
75
 
@@ -150,8 +150,9 @@ class Chef
150
150
 
151
151
  if !!results
152
152
  @raw_key = results
153
- elsif key_file == nil? && raw_key == nil?
153
+ elsif key_file.nil? && raw_key.nil?
154
154
  puts "\nNo key detected\n"
155
+ return
155
156
  elsif !!key_file
156
157
  @raw_key = IO.read(key_file).strip
157
158
  elsif !!raw_key
@@ -305,14 +306,26 @@ class Chef
305
306
 
306
307
  if !!check_certstore_for_key(client_name)
307
308
  ps_blob = powershell_exec!(get_the_key_ps(client_name, password)).result
309
+ # ps_blob can be false when the PowerShell Catch block fires (e.g. cert export
310
+ # race condition or temporary store lock). Guard before calling Hash#[] to
311
+ # avoid NoMethodError: undefined method `[]' for false:FalseClass.
312
+ return false unless ps_blob.is_a?(Hash) && ps_blob["PSPath"]
313
+
308
314
  file_path = ps_blob["PSPath"].split("::")[1]
309
- pkcs = OpenSSL::PKCS12.new(File.binread(file_path), password)
315
+
316
+ # Use begin/ensure to guarantee the unique temp file is deleted even if
317
+ # File.binread or OpenSSL::PKCS12.new raises an exception, preventing
318
+ # orphaned PFX files on disk.
319
+ begin
320
+ pkcs = OpenSSL::PKCS12.new(File.binread(file_path), password)
321
+ ensure
322
+ File.delete(file_path) if File.exist?(file_path)
323
+ end
310
324
 
311
325
  # We check the pfx we just extracted the private key from
312
326
  # if that cert is expiring in 7 days or less we generate a new pfx/p12 object
313
327
  # then we post the new public key from that to the client endpoint on
314
328
  # chef server.
315
- File.delete(file_path)
316
329
  key_expiring = is_certificate_expiring?(pkcs)
317
330
  if key_expiring
318
331
  powershell_exec!(delete_old_key_ps(client_name))
@@ -338,7 +351,7 @@ class Chef
338
351
  Try {
339
352
  $my_pwd = ConvertTo-SecureString -String "#{password}" -Force -AsPlainText;
340
353
  $cert = Get-ChildItem -path cert:\\#{store}\\My -Recurse | Where-Object { $_.Subject -match "chef-#{client_name}$" } -ErrorAction Stop;
341
- $tempfile = [System.IO.Path]::GetTempPath() + "export_pfx.pfx";
354
+ $tempfile = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), [System.IO.Path]::GetRandomFileName() + ".pfx");
342
355
  Export-PfxCertificate -Cert $cert -Password $my_pwd -FilePath $tempfile;
343
356
  }
344
357
  Catch {
@@ -93,6 +93,12 @@ class Chef
93
93
  @_extension_modules = []
94
94
  end
95
95
 
96
+ # Provide a compact inspect so exception messages do not dump the full
97
+ # template context, including node attributes and secrets.
98
+ def inspect
99
+ "#<#{self.class} template_name=#{@template_name.inspect} cookbook_name=#{@cookbook_name.inspect}>"
100
+ end
101
+
96
102
  ###
97
103
  # USER FACING API
98
104
  ###
@@ -220,8 +226,15 @@ class Chef
220
226
  @original_exception, @template, @context, @options = original_exception, template, context, options
221
227
  end
222
228
 
229
+ MAX_MESSAGE_LENGTH = 10_000
230
+
223
231
  def message
224
- @original_exception.message
232
+ msg = @original_exception.message
233
+ if msg.length > MAX_MESSAGE_LENGTH
234
+ msg[0, MAX_MESSAGE_LENGTH] + "\n... [truncated #{msg.length - MAX_MESSAGE_LENGTH} characters]"
235
+ else
236
+ msg
237
+ end
225
238
  end
226
239
 
227
240
  def line_number
@@ -258,7 +271,16 @@ class Chef
258
271
  end
259
272
 
260
273
  def to_s
261
- "\n\n#{self.class} (#{message}) #{source_location}:\n\n" +
274
+ location_parts = []
275
+ if @context.respond_to?(:cookbook_name) && @context.cookbook_name
276
+ location_parts << "cookbook: #{@context.cookbook_name}"
277
+ end
278
+ if @context.respond_to?(:template_name) && @context.template_name
279
+ location_parts << "template: #{@context.template_name}"
280
+ end
281
+ location_hint = location_parts.empty? ? "" : " (#{location_parts.join(", ")})"
282
+
283
+ "\n\n#{self.class} (#{message}) #{source_location}#{location_hint}:\n\n" +
262
284
  "#{source_listing}\n\n #{original_exception.backtrace.join("\n ")}\n\n"
263
285
  end
264
286
  end