summaryrefslogtreecommitdiff
path: root/lib/chef
diff options
context:
space:
mode:
authorJohn McCrae <john.mccrae@progress.com>2022-04-07 08:46:41 -0700
committerGitHub <noreply@github.com>2022-04-07 08:46:41 -0700
commitbf652837e634a0271d726a7b3dfe6f50e8e04cbc (patch)
tree80e424184d748bda1b8bba69d473d1344064d1b1 /lib/chef
parent09ef2bba5251b018ea477f5158b55918382b0e79 (diff)
parentf61168f78a52f28c170feb3c6a65061d91e7cb25 (diff)
downloadchef-bf652837e634a0271d726a7b3dfe6f50e8e04cbc.tar.gz
Merge branch 'main' into jfm/platform_updates
Diffstat (limited to 'lib/chef')
-rw-r--r--lib/chef/api_client_v1.rb10
-rw-r--r--lib/chef/client.rb114
-rw-r--r--lib/chef/dsl/rest_resource.rb70
-rw-r--r--lib/chef/dsl/secret.rb114
-rw-r--r--lib/chef/event_dispatch/base.rb3
-rw-r--r--lib/chef/exceptions.rb8
-rw-r--r--lib/chef/http/authenticator.rb100
-rw-r--r--lib/chef/node/attribute.rb23
-rw-r--r--lib/chef/node/mixin/deep_merge_cache.rb8
-rw-r--r--lib/chef/resource.rb16
-rw-r--r--lib/chef/resource/_rest_resource.rb386
-rw-r--r--lib/chef/resource/chef_client_config.rb5
-rw-r--r--lib/chef/resource/plist.rb9
-rw-r--r--lib/chef/resource/support/client.erb1
-rw-r--r--lib/chef/run_context.rb16
-rw-r--r--lib/chef/version.rb2
16 files changed, 828 insertions, 57 deletions
diff --git a/lib/chef/api_client_v1.rb b/lib/chef/api_client_v1.rb
index 6178cb91c3..51c915218e 100644
--- a/lib/chef/api_client_v1.rb
+++ b/lib/chef/api_client_v1.rb
@@ -64,6 +64,10 @@ class Chef
@chef_rest_v1 ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], { api_version: "1", inflate_json_class: false })
end
+ def chef_rest_v1_with_validator
+ @chef_rest_v1_with_validator ||= Chef::ServerAPI.new(Chef::Config[:chef_server_url], { client_name: Chef::Config[:validation_client_name], signing_key_filename: Chef::Config[:validation_key], api_version: "1", inflate_json_class: false })
+ end
+
def self.http_api
Chef::ServerAPI.new(Chef::Config[:chef_server_url], { api_version: "1", inflate_json_class: false })
end
@@ -293,7 +297,11 @@ class Chef
payload[:public_key] = public_key unless public_key.nil?
payload[:create_key] = create_key unless create_key.nil?
- new_client = chef_rest_v1.post("clients", payload)
+ new_client = if Chef::Config[:migrate_key_to_keystore] == true
+ chef_rest_v1_with_validator.post("clients", payload)
+ else
+ chef_rest_v1.post("clients", payload)
+ end
# get the private_key out of the chef_key hash if it exists
if new_client["chef_key"]
diff --git a/lib/chef/client.rb b/lib/chef/client.rb
index 5ec15fb582..07798798b9 100644
--- a/lib/chef/client.rb
+++ b/lib/chef/client.rb
@@ -64,6 +64,10 @@ class Chef
# The main object in a Chef run. Preps a Chef::Node and Chef::RunContext,
# syncs cookbooks if necessary, and triggers convergence.
class Client
+ CRYPT_EXPORTABLE = 0x00000001
+
+ attr_reader :local_context
+
extend Chef::Mixin::Deprecation
extend Forwardable
@@ -229,7 +233,7 @@ class Chef
start_profiling
runlock = RunLock.new(Chef::Config.lockfile)
- # TODO: feels like acquire should have its own block arg for this
+ # TODO feels like acquire should have its own block arg for this
runlock.acquire
# don't add code that may fail before entering this section to be sure to release lock
begin
@@ -637,16 +641,19 @@ class Chef
# @api private
#
def register(client_name = node_name, config = Chef::Config)
- if Chef::HTTP::Authenticator.detect_certificate_key(client_name)
- if File.exists?(config[:client_key])
- logger.warn("WARNING - Client key #{client_name} is present on disk, ignoring that in favor of key stored in CertStore")
- end
- events.skipping_registration(client_name, config)
- logger.trace("Client key #{client_name} is present in certificate repository - skipping registration")
- config[:client_key] = "Cert:\\LocalMachine\\My\\chef-#{client_name}"
- elsif !config[:client_key]
+ if !config[:client_key]
events.skipping_registration(client_name, config)
logger.trace("Client key is unspecified - skipping registration")
+ elsif ::Chef::Config[:migrate_key_to_keystore] == true && ChefUtils.windows?
+ cert_name = "chef-#{client_name}"
+ result = check_certstore_for_key(cert_name)
+ if result.rassoc("#{cert_name}")
+ logger.trace("Client key #{config[:client_key]} is present in Certificate Store - skipping registration")
+ else
+ create_new_key_and_register(cert_name)
+ logger.trace("New client keys created in the Certificate Store - skipping registration")
+ end
+ events.skipping_registration(client_name, config)
elsif File.exists?(config[:client_key])
events.skipping_registration(client_name, config)
logger.trace("Client key #{config[:client_key]} is present - skipping registration")
@@ -665,6 +672,94 @@ class Chef
raise
end
+ # In the brave new world of No Certs On Disk, we want to put the pem file into Keychain or the Certstore
+ # But is it already there?
+ def check_certstore_for_key(cert_name)
+ require "win32-certstore"
+ win32certstore = ::Win32::Certstore.open("MY")
+ win32certstore.search("#{cert_name}")
+ end
+
+ def generate_pfx_package(cert_name, date)
+ self.class.generate_pfx_package(cert_name, date)
+ end
+
+ def self.generate_pfx_package(cert_name, date)
+ require "openssl" unless defined?(OpenSSL)
+
+ key = OpenSSL::PKey::RSA.new(2048)
+ public_key = key.public_key
+
+ subject = "CN=#{cert_name}"
+
+ cert = OpenSSL::X509::Certificate.new
+ cert.subject = cert.issuer = OpenSSL::X509::Name.parse(subject)
+ cert.not_before = Time.now
+ cert.not_after = Time.parse(date)
+ cert.public_key = public_key
+ cert.serial = 0x0
+ cert.version = 2
+
+ ef = OpenSSL::X509::ExtensionFactory.new
+ ef.subject_certificate = cert
+ ef.issuer_certificate = cert
+ cert.extensions = [
+ ef.create_extension("subjectKeyIdentifier", "hash"),
+ ef.create_extension("keyUsage", "digitalSignature,keyEncipherment", true),
+ ]
+ cert.add_extension(ef.create_ext_from_string("extendedKeyUsage=critical,serverAuth,clientAuth"))
+
+ cert.sign key, OpenSSL::Digest.new("SHA256")
+ password = ::Chef::HTTP::Authenticator.get_cert_password
+ pfx = OpenSSL::PKCS12.create(password, subject, key, cert)
+ pfx
+ end
+
+ def create_new_key_and_register(cert_name)
+ require "time" unless defined?(Time)
+ autoload :URI, "uri"
+
+ # KeyMigration.instance.key_migrated = true
+
+ node = Chef::Config[:node_name]
+ d = Time.now
+ if d.month == 10 || d.month == 11 || d.month == 12
+ end_date = Time.new(d.year + 1, d.month - 9, d.day, d.hour, d.min, d.sec).utc.iso8601
+ else
+ end_date = Time.new(d.year, d.month + 3, d.day, d.hour, d.min, d.sec).utc.iso8601
+ end
+
+ payload = {
+ name: node,
+ clientname: node,
+ public_key: "",
+ expiration_date: end_date,
+ }
+
+ new_pfx = generate_pfx_package(cert_name, end_date)
+ payload[:public_key] = new_pfx.certificate.public_key.to_pem
+ base_url = "#{Chef::Config[:chef_server_url]}"
+ client = Chef::ServerAPI.new(base_url, client_name: Chef::Config[:validation_client_name], signing_key_filename: Chef::Config[:validation_key])
+ client.post(base_url + "/clients", payload)
+ Chef::Log.trace("Updated client data: #{client.inspect}")
+ import_pfx_to_store(new_pfx)
+ end
+
+ def import_pfx_to_store(new_pfx)
+ self.class.import_pfx_to_store(new_pfx)
+ end
+
+ def self.import_pfx_to_store(new_pfx)
+ password = ::Chef::HTTP::Authenticator.get_cert_password
+ require "win32-certstore"
+ tempfile = Tempfile.new("#{Chef::Config[:node_name]}.pfx")
+ File.open(tempfile, "wb") { |f| f.print new_pfx.to_der }
+
+ store = ::Win32::Certstore.open("MY")
+ store.add_pfx(tempfile, password, CRYPT_EXPORTABLE)
+ tempfile.unlink
+ end
+
#
# Converges all compiled resources.
#
@@ -929,3 +1024,4 @@ end
require_relative "cookbook_loader"
require_relative "cookbook_version"
require_relative "cookbook/synchronizer"
+
diff --git a/lib/chef/dsl/rest_resource.rb b/lib/chef/dsl/rest_resource.rb
new file mode 100644
index 0000000000..96eba24eac
--- /dev/null
+++ b/lib/chef/dsl/rest_resource.rb
@@ -0,0 +1,70 @@
+#
+# Copyright:: Copyright 2008-2016, Chef, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "chef/constants" unless defined?(NOT_PASSED)
+
+class Chef
+ module DSL
+ module RestResource
+ def rest_property_map(rest_property_map = NOT_PASSED)
+ if rest_property_map != NOT_PASSED
+ rest_property_map = rest_property_map.to_h { |k| [k.to_sym, k] } if rest_property_map.is_a? Array
+
+ @rest_property_map = rest_property_map
+ end
+ @rest_property_map
+ end
+
+ # URL to collection
+ def rest_api_collection(rest_api_collection = NOT_PASSED)
+ @rest_api_collection = rest_api_collection if rest_api_collection != NOT_PASSED
+ @rest_api_collection
+ end
+
+ # RFC6570-Templated URL to document
+ def rest_api_document(rest_api_document = NOT_PASSED, first_element_only: false)
+ if rest_api_document != NOT_PASSED
+ @rest_api_document = rest_api_document
+ @rest_api_document_first_element_only = first_element_only
+ end
+ @rest_api_document
+ end
+
+ # Explicit REST document identity mapping
+ def rest_identity_map(rest_identity_map = NOT_PASSED)
+ @rest_identity_map = rest_identity_map if rest_identity_map != NOT_PASSED
+ @rest_identity_map
+ end
+
+ # Mark up properties for POST only, not PATCH/PUT
+ def rest_post_only_properties(rest_post_only_properties = NOT_PASSED)
+ if rest_post_only_properties != NOT_PASSED
+ @rest_post_only_properties = Array(rest_post_only_properties).map(&:to_sym)
+ end
+ @rest_post_only_properties || []
+ end
+
+ def rest_api_document_first_element_only(rest_api_document_first_element_only = NOT_PASSED)
+ if rest_api_document_first_element_only != NOT_PASSED
+ @rest_api_document_first_element_only = rest_api_document_first_element_only
+ end
+ @rest_api_document_first_element_only
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/dsl/secret.rb b/lib/chef/dsl/secret.rb
index 9d616ff35f..2377ae36fe 100644
--- a/lib/chef/dsl/secret.rb
+++ b/lib/chef/dsl/secret.rb
@@ -21,6 +21,118 @@ class Chef
module DSL
module Secret
+ #
+ # This allows you to set the default secret service that is used when
+ # fetching secrets.
+ #
+ # @example
+ #
+ # default_secret_service :hashi_vault
+ # val1 = secret(name: "test1", config: { region: "us-west-1" })
+ #
+ # @example
+ #
+ # default_secret_service #=> nil
+ # default_secret_service :hashi_vault
+ # default_secret_service #=> :hashi_vault
+ #
+ # @param [Symbol] service default secret service to use when fetching secrets
+ # @return [Symbol, nil] default secret service to use when fetching secrets
+ #
+ def default_secret_service(service = nil)
+ return run_context.default_secret_service if service.nil?
+ raise Chef::Exceptions::Secret::InvalidFetcherService.new("Unsupported secret service: #{service.inspect}", Chef::SecretFetcher::SECRET_FETCHERS) unless Chef::SecretFetcher::SECRET_FETCHERS.include?(service)
+
+ run_context.default_secret_service = service
+ end
+
+ #
+ # This allows you to set the secret service for the scope of the block
+ # passed into this method.
+ #
+ # @example
+ #
+ # with_secret_service :hashi_vault do
+ # val1 = secret(name: "test1", config: { region: "us-west-1" })
+ # val2 = secret(name: "test2", config: { region: "us-west-1" })
+ # end
+ #
+ # @example Combine with #with_secret_config
+ #
+ # with_secret_service :hashi_vault do
+ # with_secret_config region: "us-west-1" do
+ # val1 = secret(name: "test1")
+ # val2 = secret(name: "test2")
+ # end
+ # end
+ #
+ # @param [Symbol] service The default secret service to use when fetching secrets
+ #
+ def with_secret_service(service)
+ raise ArgumentError, "You must pass a block to #with_secret_service" unless block_given?
+
+ begin
+ old_service = default_secret_service
+ # Use "public" API for input validation
+ default_secret_service(service)
+ yield
+ ensure
+ # Use "private" API so we can set back to nil
+ run_context.default_secret_service = old_service
+ end
+ end
+
+ #
+ # This allows you to set the default secret config that is used when
+ # fetching secrets.
+ #
+ # @example
+ #
+ # default_secret_config region: "us-west-1"
+ # val1 = secret(name: "test1", service: :hashi_vault)
+ #
+ # @example
+ #
+ # default_secret_config #=> {}
+ # default_secret_service region: "us-west-1"
+ # default_secret_service #=> { region: "us-west-1" }
+ #
+ # @param [Hash<Symbol,Object>] config The default configuration options to apply when fetching secrets
+ # @return [Hash<Symbol,Object>]
+ #
+ def default_secret_config(**config)
+ return run_context.default_secret_config if config.empty?
+
+ run_context.default_secret_config = config
+ end
+
+ #
+ # This allows you to set the secret config for the scope of the block
+ # passed into this method.
+ #
+ # @example
+ #
+ # with_secret_config region: "us-west-1" do
+ # val1 = secret(name: "test1", service: :hashi_vault)
+ # val2 = secret(name: "test2", service: :hashi_vault)
+ # end
+ #
+ # @param [Hash<Symbol,Object>] config The default configuration options to use when fetching secrets
+ #
+ def with_secret_config(**config)
+ raise ArgumentError, "You must pass a block to #with_secret_config" unless block_given?
+
+ begin
+ old_config = default_secret_config
+ # Use "public" API for input validation
+ default_secret_config(**config)
+ yield
+ ensure
+ # Use "private" API so we can set back to nil
+ run_context.default_secret_config = old_config
+ end
+ end
+
# Helper method which looks up a secret using the given service and configuration,
# and returns the retrieved secret value.
# This DSL providers a wrapper around [Chef::SecretFetcher]
@@ -49,7 +161,7 @@ class Chef
#
# value = secret(name: "test1", service: :aws_secrets_manager, version: "v1", config: { region: "us-west-1" })
# log "My secret is #{value}"
- def secret(name: nil, version: nil, service: nil, config: {})
+ def secret(name: nil, version: nil, service: default_secret_service, config: default_secret_config)
Chef::Log.warn <<~EOM.gsub("\n", " ")
The secrets Chef Infra language helper is currently in beta. If you have feedback or you would
like to be part of the future design of this helper e-mail us at secrets_management_beta@progress.com"
diff --git a/lib/chef/event_dispatch/base.rb b/lib/chef/event_dispatch/base.rb
index a973c31612..669c1d6286 100644
--- a/lib/chef/event_dispatch/base.rb
+++ b/lib/chef/event_dispatch/base.rb
@@ -273,6 +273,9 @@ class Chef
# Called if the converge phase fails
def converge_failed(exception); end
+ # Called when migrating from a pem on disk to a pem stored in Keychain or Windows Certstore
+ def key_migration_status(key_migrated = false); end
+
# TODO: need events for notification resolve?
# def notifications_resolved
# end
diff --git a/lib/chef/exceptions.rb b/lib/chef/exceptions.rb
index ffdbdcbaba..c60b7fc888 100644
--- a/lib/chef/exceptions.rb
+++ b/lib/chef/exceptions.rb
@@ -561,5 +561,13 @@ class Chef
super "before subscription from #{notification.resource} resource cannot be setup to #{notification.notifying_resource} resource, which has already fired while in unified mode"
end
end
+
+ class RestError < RuntimeError; end
+
+ class RestTargetError < RestError; end
+
+ class RestTimeout < RestError; end
+
+ class RestOperationFailed < RestError; end
end
end
diff --git a/lib/chef/http/authenticator.rb b/lib/chef/http/authenticator.rb
index 26413c283b..3b9d49f42a 100644
--- a/lib/chef/http/authenticator.rb
+++ b/lib/chef/http/authenticator.rb
@@ -100,6 +100,18 @@ class Chef
self.class.retrieve_certificate_key(client_name)
end
+ def get_cert_password
+ self.class.get_cert_password
+ end
+
+ def encrypt_pfx_pass
+ self.class.encrypt_pfx_pass
+ end
+
+ def decrypt_pfx_pass
+ self.class.decrypt_pfx_pass
+ end
+
# Detects if a private key exists in a certificate repository like Keychain (macOS) or Certificate Store (Windows)
#
# @param client_name - we're using the node name to store and retrieve any keys
@@ -115,7 +127,7 @@ class Chef
def self.check_certstore_for_key(client_name)
powershell_code = <<~CODE
- $cert = Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "#{client_name}" } -ErrorAction Stop
+ $cert = Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "chef-#{client_name}" } -ErrorAction Stop
if (($cert.HasPrivateKey -eq $true) -and ($cert.PrivateKey.Key.ExportPolicy -ne "NonExportable")) {
return $true
}
@@ -129,17 +141,16 @@ class Chef
def load_signing_key(key_file, raw_key = nil)
results = retrieve_certificate_key(Chef::Config[:node_name])
- if key_file == nil? && raw_key == nil?
- puts "\nNo key detected\n"
- elsif !!results
- # results variable can be 1 of 2 values - "False" or the contents of a key.
+ if !!results
@raw_key = results
+ elsif key_file == nil? && raw_key == nil?
+ puts "\nNo key detected\n"
elsif !!key_file
@raw_key = IO.read(key_file).strip
elsif !!raw_key
@raw_key = raw_key.strip
else
- return nil
+ return
end
# Pass in '' as the passphrase to avoid OpenSSL prompting on the TTY if
# given an encrypted key. This also helps if using a single file for
@@ -154,7 +165,6 @@ class Chef
raise Chef::Exceptions::InvalidPrivateKey, msg
end
- # takes no parameters. Checks for the password in the registry and returns it if there, otherwise returns false
def self.get_cert_password
@win32registry = Chef::Win32::Registry.new
path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
@@ -166,60 +176,86 @@ class Chef
present.each do |secret|
if secret[:name] == "PfxPass"
- return secret[:data]
+ password = decrypt_pfx_pass(secret[:data])
+ return password
end
end
- # if we make it this far, that means there is no valid password in the Registry. Fail out to correct that.
raise Chef::Exceptions::Win32RegKeyMissing
rescue Chef::Exceptions::Win32RegKeyMissing
# if we don't have a password, log that and generate one
- Chef::Log.warn "Authentication Hive and value not present in registry, creating it now"
+ Chef::Log.warn "Authentication Hive and values not present in registry, creating them now"
new_path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
unless @win32registry.key_exists?(new_path)
@win32registry.create_key(new_path, true)
end
- password = SOME_CHARS.sample(1 + rand(SOME_CHARS.count)).join[0...14]
- values = { name: "PfxPass", type: :string, data: password }
+ size = 14
+ password = SOME_CHARS.sample(size).join
+ encrypted_pass = encrypt_pfx_pass(password)
+ values = { name: "PfxPass", type: :string, data: encrypted_pass }
@win32registry.set_value(new_path, values)
password
end
+ def self.encrypt_pfx_pass(password)
+ powershell_code = <<~CODE
+ $encrypted_string = ConvertTo-SecureString "#{password}" -AsPlainText -Force
+ $secure_string = ConvertFrom-SecureString $encrypted_string
+ return $secure_string
+ CODE
+ powershell_exec!(powershell_code).result
+ end
+
+ def self.decrypt_pfx_pass(password)
+ powershell_code = <<~CODE
+ $secure_string = "#{password}" | ConvertTo-SecureString
+ $string = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR((($secure_string))))
+ return $string
+ CODE
+ powershell_exec!(powershell_code).result
+ end
+
def self.retrieve_certificate_key(client_name)
require "openssl" unless defined?(OpenSSL)
if ChefUtils.windows?
-
password = get_cert_password
-
return false unless password
if check_certstore_for_key(client_name)
- powershell_code = <<~CODE
- Try {
- $my_pwd = ConvertTo-SecureString -String "#{password}" -Force -AsPlainText;
- $cert = Get-ChildItem -path cert:\\LocalMachine\\My -Recurse | Where-Object { $_.Subject -match "#{client_name}" } -ErrorAction Stop;
- $tempfile = [System.IO.Path]::GetTempPath() + "export_pfx.pfx";
- Export-PfxCertificate -Cert $cert -Password $my_pwd -FilePath $tempfile;
- }
- Catch {
- return $false
- }
- CODE
- my_result = powershell_exec!(powershell_code).result
-
- if !!my_result
- pkcs = OpenSSL::PKCS12.new(File.binread(my_result["PSPath"].split("::")[1]), password)
- ::File.delete(my_result["PSPath"].split("::")[1])
- return OpenSSL::PKey::RSA.new(pkcs.key.to_pem)
- end
+ ps_blob = powershell_exec!(get_the_key_ps(client_name, password)).result
+ file_path = ps_blob["PSPath"].split("::")[1]
+ pkcs = OpenSSL::PKCS12.new(File.binread(file_path), password)
+
+ # We test the pfx we just extracted the private key from
+ # if that cert is expiring in 7 days or less we generate a new pfx/p12 object
+ # then we post the new public key from that to the client endpoint on
+ # chef server.
+ # is_certificate_expiring(pkcs)
+ File.delete(file_path)
+
+ return pkcs.key.private_to_pem
end
end
false
end
+ def self.get_the_key_ps(client_name, password)
+ powershell_code = <<~CODE
+ Try {
+ $my_pwd = ConvertTo-SecureString -String "#{password}" -Force -AsPlainText;
+ $cert = Get-ChildItem -path cert:\\LocalMachine\\My -Recurse | Where-Object { $_.Subject -match "chef-#{client_name}$" } -ErrorAction Stop;
+ $tempfile = [System.IO.Path]::GetTempPath() + "export_pfx.pfx";
+ Export-PfxCertificate -Cert $cert -Password $my_pwd -FilePath $tempfile;
+ }
+ Catch {
+ return $false
+ }
+ CODE
+ end
+
def authentication_headers(method, url, json_body = nil, headers = nil)
request_params = {
http_method: method,
diff --git a/lib/chef/node/attribute.rb b/lib/chef/node/attribute.rb
index 6a8e72004b..d235ce4faa 100644
--- a/lib/chef/node/attribute.rb
+++ b/lib/chef/node/attribute.rb
@@ -452,17 +452,34 @@ class Chef
# method-style access to attributes (has to come after the prepended ImmutablizeHash)
def read(*path)
- merged_attributes.read(*path)
+ if path[0].nil?
+ Chef::Log.warn "Calling node.read() without any path argument is very slow, probably a bug, and should be avoided"
+ merged_attributes.read(*path) # re-merges everything, slow edge case
+ else
+ self[path[0]] unless path[0].nil? # force deep_merge_cache key construction if necessary
+ deep_merge_cache.read(*path)
+ end
end
alias :dig :read
def read!(*path)
- merged_attributes.read!(*path)
+ if path[0].nil?
+ Chef::Log.warn "Calling node.read!() without any path argument is very slow, probably a bug, and should be avoided"
+ merged_attributes.read!(*path) # re-merges everything, slow edge case
+ else
+ self[path[0]] unless path[0].nil? # force deep_merge_cache key construction if necessary
+ deep_merge_cache.read!(*path)
+ end
end
def exist?(*path)
- merged_attributes.exist?(*path)
+ if path[0].nil?
+ true
+ else
+ self[path[0]] unless path[0].nil? # force deep_merge_cache key construction if necessary
+ deep_merge_cache.exist?(*path)
+ end
end
def write(level, *args, &block)
diff --git a/lib/chef/node/mixin/deep_merge_cache.rb b/lib/chef/node/mixin/deep_merge_cache.rb
index 8978d77ea0..be16197850 100644
--- a/lib/chef/node/mixin/deep_merge_cache.rb
+++ b/lib/chef/node/mixin/deep_merge_cache.rb
@@ -30,7 +30,7 @@ class Chef
@merged_attributes = nil
@combined_override = nil
@combined_default = nil
- @deep_merge_cache = {}
+ @deep_merge_cache = Chef::Node::ImmutableMash.new
end
# Invalidate a key in the deep_merge_cache. If called with nil, or no arg, this will invalidate
@@ -39,9 +39,9 @@ class Chef
# must invalidate the entire cache and re-deep-merge the entire node object.
def reset_cache(path = nil)
if path.nil?
- deep_merge_cache.clear
+ deep_merge_cache.regular_clear
else
- deep_merge_cache.delete(path.to_s)
+ deep_merge_cache.regular_delete(path.to_s)
end
end
@@ -53,7 +53,7 @@ class Chef
deep_merge_cache[key.to_s]
else
# save all the work of computing node[key]
- deep_merge_cache[key.to_s] = merged_attributes(key)
+ deep_merge_cache.internal_set(key.to_s, merged_attributes(key))
end
ret = ret.call while ret.is_a?(::Chef::DelayedEvaluator)
ret
diff --git a/lib/chef/resource.rb b/lib/chef/resource.rb
index 7f65f28fdd..d6c5fe7cdf 100644
--- a/lib/chef/resource.rb
+++ b/lib/chef/resource.rb
@@ -1498,10 +1498,18 @@ class Chef
# @param partial [String] the code fragment to eval against the class
#
def self.use(partial)
- dirname = ::File.dirname(partial)
- basename = ::File.basename(partial, ".rb")
- basename = basename[1..] if basename.start_with?("_")
- class_eval IO.read(::File.expand_path("#{dirname}/_#{basename}.rb", ::File.dirname(caller_locations.first.path)))
+ if partial =~ /^core::(.*)/
+ partial = $1
+ dirname = ::File.dirname(partial)
+ basename = ::File.basename(partial, ".rb")
+ basename = basename[1..] if basename.start_with?("_")
+ class_eval IO.read(::File.expand_path("resource/#{dirname}/_#{basename}.rb", __dir__))
+ else
+ dirname = ::File.dirname(partial)
+ basename = ::File.basename(partial, ".rb")
+ basename = basename[1..] if basename.start_with?("_")
+ class_eval IO.read(::File.expand_path("#{dirname}/_#{basename}.rb", ::File.dirname(caller_locations.first.path)))
+ end
end
# The cookbook in which this Resource was defined (if any).
diff --git a/lib/chef/resource/_rest_resource.rb b/lib/chef/resource/_rest_resource.rb
new file mode 100644
index 0000000000..f14e586eb2
--- /dev/null
+++ b/lib/chef/resource/_rest_resource.rb
@@ -0,0 +1,386 @@
+#
+# Copyright:: Copyright 2008-2016, Chef, Inc.
+# License:: Apache License, Version 2.0
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+require "rest-client" unless defined?(RestClient)
+require "jmespath" unless defined?(JMESPath)
+require "chef/dsl/rest_resource" unless defined?(Chef::DSL::RestResource)
+
+extend Chef::DSL::RestResource
+
+action_class do
+ def load_current_resource
+ @current_resource = new_resource.class.new(new_resource.name)
+
+ required_properties.each do |name|
+ requested = new_resource.send(name)
+ current_resource.send(name, requested)
+ end
+
+ return @current_resource if rest_get_all.data.empty?
+
+ resource_data = rest_get.data rescue nil
+ return @current_resource if resource_data.nil? || resource_data.empty?
+
+ @resource_exists = true
+
+ # Map JSON contents to defined properties
+ current_resource.class.rest_property_map.each do |property, match_instruction|
+ property_value = json_to_property(match_instruction, property, resource_data)
+ current_resource.send(property, property_value) unless property_value.nil?
+ end
+
+ current_resource
+ end
+end
+
+action :configure do
+ if resource_exists?
+ converge_if_changed do
+ data = {}
+
+ new_resource.class.rest_property_map.each do |property, match_instruction|
+ # Skip "creation-only" properties on modifications
+ next if new_resource.class.rest_post_only_properties.include?(property)
+
+ deep_merge! data, property_to_json(property, match_instruction)
+ end
+
+ deep_compact!(data)
+
+ rest_patch(data)
+ end
+ else
+ converge_by "creating resource" do
+ data = {}
+
+ new_resource.class.rest_property_map.each do |property, match_instruction|
+ deep_merge! data, property_to_json(property, match_instruction)
+ end
+
+ deep_compact!(data)
+
+ rest_post(data)
+ end
+ end
+end
+
+action :delete do
+ if resource_exists?
+ converge_by "deleting resource" do
+ rest_delete
+ end
+ else
+ logger.debug format("REST resource %<name>s of type %<type>s does not exist. Skipping.",
+ type: new_resource.name, name: id_property)
+ end
+end
+
+action_class do
+ # Override this for postprocessing device-specifics (paging, data conversion)
+ def rest_postprocess(response)
+ response
+ end
+
+ # Override this for error handling of device-specifics (readable error messages)
+ def rest_errorhandler(error_obj)
+ error_obj
+ end
+
+ private
+
+ def resource_exists?
+ @resource_exists
+ end
+
+ def required_properties
+ current_resource.class.properties.select { |_, v| v.required? }.except(:name).keys
+ end
+
+ # Return changed value or nil for delta current->new
+ def changed_value(property)
+ new_value = new_resource.send(property)
+ return new_value if current_resource.nil?
+
+ current_value = current_resource.send(property)
+
+ return current_value if required_properties.include? property
+
+ new_value == current_value ? nil : new_value
+ end
+
+ def id_property
+ current_resource.class.identity_attr
+ end
+
+ # Map properties to their current values
+ def property_map
+ map = {}
+
+ current_resource.class.state_properties.each do |property|
+ name = property.options[:name]
+
+ map[name] = current_resource.send(name)
+ end
+
+ map[id_property] = current_resource.send(id_property)
+
+ map
+ end
+
+ # Map part of a JSON (Hash) to resource property via JMESPath or user-supplied function
+ def json_to_property(match_instruction, property, resource_data)
+ case match_instruction
+ when String
+ JMESPath.search(match_instruction, resource_data)
+ when Symbol
+ function = "#{property}_from_json".to_sym
+ raise "#{new_resource.name} missing #{function} method" unless self.class.protected_method_defined?(function)
+
+ send(function, resource_data) || {}
+ else
+ raise TypeError, "Did not expect match type #{match_instruction.class}"
+ end
+ end
+
+ # Map resource contents into a JSON (Hash) via JMESPath-like syntax or user-supplied function
+ def property_to_json(property, match_instruction)
+ case match_instruction
+ when String
+ bury(match_instruction, changed_value(property))
+ when Symbol
+ function = "#{property}_to_json".to_sym
+ raise "#{new_resource.name} missing #{function} method" unless self.class.protected_method_defined?(function)
+
+ value = new_resource.send(property)
+ changed_value(property).nil? ? {} : send(function, value)
+ else
+ raise TypeError, "Did not expect match type #{match_instruction.class}"
+ end
+ end
+
+ def rest_url_collection
+ current_resource.class.rest_api_collection
+ end
+
+ # Resource document URL after RFC 6570 template evaluation via properties substitution
+ def rest_url_document
+ template = ::Addressable::Template.new(current_resource.class.rest_api_document)
+ template.expand(property_map).to_s
+ end
+
+ # Convenience method for conditional requires
+ def conditionally_require_on_setting(property, dependent_properties)
+ dependent_properties = Array(dependent_properties)
+
+ requirements.assert(:configure) do |a|
+ a.assertion do
+ # Needs to be set and truthy to require dependent properties
+ if new_resource.send(property)
+ dependent_properties.all? { |dep_prop| new_resource.property_is_set?(dep_prop) }
+ else
+ true
+ end
+ end
+
+ message = format("Setting property :%<property>s requires properties :%<properties>s to be set as well on resource %<resource_name>s",
+ property: property,
+ properties: dependent_properties.join(", :"),
+ resource_name: current_resource.to_s)
+
+ a.failure_message message
+ end
+ end
+
+ # Generic REST helpers
+
+ def rest_get_all
+ response = api_connection.get(rest_url_collection)
+
+ rest_postprocess(response)
+ rescue RestClient::Exception => e
+ rest_errorhandler(e)
+ end
+
+ def rest_get
+ response = api_connection.get(rest_url_document)
+
+ response = rest_postprocess(response)
+
+ first_only = current_resource.class.rest_api_document_first_element_only
+ first_only && response.is_a?(Array) ? response.first : response
+ rescue RestClient::Exception => e
+ rest_errorhandler(e)
+ end
+
+ def rest_post(data)
+ data.merge! rest_identity_values
+
+ response = api_connection.post(rest_url_collection, data: data)
+
+ rest_postprocess(response)
+ rescue RestClient::Exception => e
+ rest_errorhandler(e)
+ end
+
+ def rest_put(data)
+ data.merge! rest_identity_values
+
+ response = api_connection.put(rest_url_collection, data: data)
+
+ rest_postprocess(response)
+ rescue RestClient::Exception => e
+ rest_errorhandler(e)
+ end
+
+ def rest_patch(data)
+ response = api_connection.patch(rest_url_document, data: data)
+
+ rest_postprocess(response)
+ rescue RestClient::Exception => e
+ rest_errorhandler(e)
+ end
+
+ def rest_delete
+ response = api_connection.delete(rest_url_document)
+
+ rest_postprocess(response)
+ rescue RestClient::Exception => e
+ rest_errorhandler(e)
+ end
+
+ # REST parameter mapping
+
+ # Return number of parameters needed to identify a resource (pre- and post-creation)
+ def rest_arity
+ rest_identity_map.keys.count
+ end
+
+ # Return mapping of template placeholders to property value of identity parameters
+ def rest_identity_values
+ data = {}
+
+ rest_identity_map.each do |rfc_template, property|
+ property_value = new_resource.send(property)
+ data.merge! bury(rfc_template, property_value)
+ end
+
+ data
+ end
+
+ def rest_identity_map
+ rest_identity_explicit || rest_identity_implicit
+ end
+
+ # Accept direct mapping like { "svm.name" => :name } for specifying the x-ary identity of a resource
+ def rest_identity_explicit
+ current_resource.class.rest_identity_map
+ end
+
+ # Parse document URL for RFC 6570 templates and map them to resource properties.
+ #
+ # Examples:
+ # Query based: "/api/protocols/san/igroups?name={name}&svm.name={svm}": { "name" => :name, "svm.name" => :svm }
+ # Path based: "/api/v1/{address}": { "address" => :address }
+ #
+ def rest_identity_implicit
+ template_url = current_resource.class.rest_api_document
+
+ rfc_template = ::Addressable::Template.new(template_url)
+ rfc_template_vars = rfc_template.variables
+
+ # Shortcut for 0-ary resources
+ return {} if rfc_template_vars.empty?
+
+ if query_based_selection?
+ uri_query = URI.parse(template_url).query
+
+ if CGI.parse(uri_query).values.any?(&:empty?)
+ raise "Need explicit identity mapping, as URL does not contain query parameters for all templates"
+ end
+
+ path_variables = CGI.parse(uri_query).keys
+ elsif path_based_selection?
+ path_variables = rfc_template_vars
+ else
+ # There is also
+ raise "Unknown type of resource selection. Document URL does not seem to be path- or query-based?"
+ end
+
+ identity_map = {}
+ path_variables.each_with_index do |v, i|
+ next if rfc_template_vars[i].nil? # Not mapped to property, assume metaparameter
+
+ identity_map[v] = rfc_template_vars[i].to_sym
+ end
+
+ identity_map
+ end
+
+ def query_based_selection?
+ template_url = current_resource.class.rest_api_document
+
+ # Will throw exception on presence of RFC 6570 templates
+ URI.parse(template_url)
+ true
+ rescue URI::InvalidURIError => _e
+ false
+ end
+
+ def path_based_selection?
+ !query_based_selection?
+ end
+
+ def api_connection
+ Chef.run_context.transport.connection
+ end
+
+ # Remove all empty keys (recusively) from Hash.
+ # @see https://stackoverflow.com/questions/56457020/#answer-56458673
+ def deep_compact!(hsh)
+ raise TypeError unless hsh.is_a? Hash
+
+ hsh.each do |_, v|
+ deep_compact!(v) if v.is_a? Hash
+ end.reject! { |_, v| v.nil? || (v.respond_to?(:empty?) && v.empty?) }
+ end
+
+ # Deep merge two hashes
+ # @see https://stackoverflow.com/questions/41109599#answer-41109737
+ def deep_merge!(hsh1, hsh2)
+ raise TypeError unless hsh1.is_a?(Hash) && hsh2.is_a?(Hash)
+
+ hsh1.merge!(hsh2) { |_, v1, v2| deep_merge!(v1, v2) }
+ end
+
+ # Create nested hashes from JMESPath syntax.
+ def bury(path, value)
+ raise TypeError unless path.is_a?(String)
+
+ arr = path.split(".")
+ ret = {}
+
+ if arr.count == 1
+ ret[arr.first] = value
+
+ ret
+ else
+ partial_path = arr[0..-2].join(".")
+
+ bury(partial_path, bury(arr.last, value))
+ end
+ end
+end
diff --git a/lib/chef/resource/chef_client_config.rb b/lib/chef/resource/chef_client_config.rb
index a9b1fa825e..916dbffe7e 100644
--- a/lib/chef/resource/chef_client_config.rb
+++ b/lib/chef/resource/chef_client_config.rb
@@ -208,6 +208,10 @@ class Chef
description: %q(An array of hashes that contain a report handler class and the arguments to pass to that class on initialization. The hash should include `class` and `argument` keys where `class` is a String and `argument` is an array of quoted String values. For example: `[{'class' => 'MyHandler', %w('"argument1"', '"argument2"')}]`),
default: []
+ property :rubygems_url, [String, Array],
+ description: "The location to source rubygems. It can be set to a string or array of strings for URIs to set as rubygems sources. This allows individuals to setup an internal mirror of rubygems for “airgapped” environments.",
+ introduced: "17.11"
+
property :exception_handlers, Array,
description: %q(An array of hashes that contain a exception handler class and the arguments to pass to that class on initialization. The hash should include `class` and `argument` keys where `class` is a String and `argument` is an array of quoted String values. For example: `[{'class' => 'MyHandler', %w('"argument1"', '"argument2"')}]`),
default: []
@@ -295,6 +299,7 @@ class Chef
policy_group: new_resource.policy_group,
policy_name: new_resource.policy_name,
report_handlers: format_handler(new_resource.report_handlers),
+ rubygems_url: new_resource.rubygems_url,
ssl_verify_mode: new_resource.ssl_verify_mode,
start_handlers: format_handler(new_resource.start_handlers),
additional_config: new_resource.additional_config,
diff --git a/lib/chef/resource/plist.rb b/lib/chef/resource/plist.rb
index 6783ae5f51..d316d81b2a 100644
--- a/lib/chef/resource/plist.rb
+++ b/lib/chef/resource/plist.rb
@@ -84,7 +84,7 @@ class Chef
converge_if_changed :path do
converge_by "create new plist: '#{new_resource.path}'" do
file new_resource.path do
- content {}.to_plist
+ content({}.to_plist)
owner new_resource.owner
group new_resource.group
mode new_resource.mode if property_is_set?(:mode)
@@ -188,7 +188,12 @@ class Chef
sep = " "
arg = case subcommand.to_s
when "add"
- type_to_commandline_string(value)
+ if value.is_a?(Hash)
+ sep = ":"
+ value.map { |k, v| "#{k} #{type_to_commandline_string(v)}" }
+ else
+ type_to_commandline_string(value)
+ end
when "set"
if value.is_a?(Hash)
sep = ":"
diff --git a/lib/chef/resource/support/client.erb b/lib/chef/resource/support/client.erb
index 8e96ca49e2..724adc9325 100644
--- a/lib/chef/resource/support/client.erb
+++ b/lib/chef/resource/support/client.erb
@@ -18,6 +18,7 @@
@pid_file
@policy_group
@policy_name
+ @rubygems_url
@ssl_verify_mode
@policy_persist_run_list).each do |prop| -%>
<% next if instance_variable_get(prop).nil? || instance_variable_get(prop).empty? -%>
diff --git a/lib/chef/run_context.rb b/lib/chef/run_context.rb
index 94f8a316e0..ce4d545aa4 100644
--- a/lib/chef/run_context.rb
+++ b/lib/chef/run_context.rb
@@ -145,6 +145,16 @@ class Chef
#
attr_accessor :input_collection
+ #
+ # @return [Symbol, nil]
+ #
+ attr_accessor :default_secret_service
+
+ #
+ # @return [Hash<Symbol,Object>]
+ #
+ attr_accessor :default_secret_config
+
# Pointer back to the Chef::Runner that created this
#
attr_accessor :runner
@@ -222,6 +232,8 @@ class Chef
@input_collection = Chef::Compliance::InputCollection.new(events)
@waiver_collection = Chef::Compliance::WaiverCollection.new(events)
@profile_collection = Chef::Compliance::ProfileCollection.new(events)
+ @default_secret_service = nil
+ @default_secret_config = {}
initialize_child_state
end
@@ -693,6 +705,10 @@ class Chef
cookbook_collection
cookbook_collection=
cookbook_compiler
+ default_secret_config
+ default_secret_config=
+ default_secret_service
+ default_secret_service=
definitions
events
events=
diff --git a/lib/chef/version.rb b/lib/chef/version.rb
index c471d743aa..53603b9d1c 100644
--- a/lib/chef/version.rb
+++ b/lib/chef/version.rb
@@ -23,7 +23,7 @@ require_relative "version_string"
class Chef
CHEF_ROOT = File.expand_path("..", __dir__)
- VERSION = Chef::VersionString.new("18.0.76")
+ VERSION = Chef::VersionString.new("18.0.87")
end
#