summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/kitchen.yml2
-rw-r--r--CHANGELOG.md17
-rw-r--r--Gemfile.lock24
-rw-r--r--VERSION2
-rw-r--r--chef-bin/lib/chef-bin/version.rb2
-rw-r--r--chef-config/lib/chef-config/version.rb2
-rw-r--r--chef-utils/lib/chef-utils/mash.rb8
-rw-r--r--chef-utils/lib/chef-utils/version.rb2
-rw-r--r--cspell.json9
-rw-r--r--docs/dev/how_to/building_chef_client_and_related_gems.md113
-rw-r--r--kitchen-tests/cookbooks/end_to_end/recipes/_chef_client_config.rb1
-rw-r--r--kitchen-tests/test/integration/end-to-end/_chef_client_config.rb1
-rw-r--r--knife/lib/chef/knife/version.rb2
-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
-rw-r--r--omnibus/Gemfile.lock76
-rw-r--r--omnibus_overrides.rb9
-rw-r--r--spec/functional/resource/plist_spec.rb25
-rw-r--r--spec/integration/client/client_spec.rb31
-rw-r--r--spec/unit/client_spec.rb52
-rw-r--r--spec/unit/dsl/secret_spec.rb150
-rw-r--r--spec/unit/http/authenticator_spec.rb67
-rw-r--r--spec/unit/resource/chef_client_config_spec.rb8
-rw-r--r--spec/unit/resource/rest_resource_spec.rb381
-rw-r--r--spec/unit/run_context_spec.rb16
39 files changed, 1731 insertions, 154 deletions
diff --git a/.github/workflows/kitchen.yml b/.github/workflows/kitchen.yml
index 10201b545d..c66a5204ff 100644
--- a/.github/workflows/kitchen.yml
+++ b/.github/workflows/kitchen.yml
@@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
- os: [windows-2022, windows-2019, windows-2016]
+ os: [windows-2022, windows-2019]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cbf670d25d..43735cf3de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,17 +1,28 @@
<!-- usage documentation: http://expeditor-docs.es.chef.io/configuration/changelog/ -->
This changelog lists individual merged pull requests to Chef Infra Client and geared towards developers. For a list of significant changes per release see the [Chef Infra Client Release Notes](https://docs.chef.io/release_notes_client/).
-<!-- latest_release 18.0.76 -->
-## [v18.0.76](https://github.com/chef/chef/tree/v18.0.76) (2022-03-25)
+<!-- latest_release 18.0.87 -->
+## [v18.0.87](https://github.com/chef/chef/tree/v18.0.87) (2022-04-05)
#### Merged Pull Requests
-- Package resource was calling a non-existent error method [#12722](https://github.com/chef/chef/pull/12722) ([johnmccrae](https://github.com/johnmccrae))
+- Updating gemfile.locks [#12751](https://github.com/chef/chef/pull/12751) ([johnmccrae](https://github.com/johnmccrae))
<!-- latest_release -->
<!-- release_rollup since=17.9.26 -->
### Changes not yet released to stable
#### Merged Pull Requests
+- Updating gemfile.locks [#12751](https://github.com/chef/chef/pull/12751) ([johnmccrae](https://github.com/johnmccrae)) <!-- 18.0.87 -->
+- Rest Resource Support [#12755](https://github.com/chef/chef/pull/12755) ([lamont-granquist](https://github.com/lamont-granquist)) <!-- 18.0.86 -->
+- Bump omnibus-software in Gemfile.lock [#12754](https://github.com/chef/chef/pull/12754) ([jeremiahsnapp](https://github.com/jeremiahsnapp)) <!-- 18.0.85 -->
+- Bump libxslt to 1.1.35 [#12752](https://github.com/chef/chef/pull/12752) ([jeremiahsnapp](https://github.com/jeremiahsnapp)) <!-- 18.0.84 -->
+- Fix attribute performance issues in node.read [#12742](https://github.com/chef/chef/pull/12742) ([lamont-granquist](https://github.com/lamont-granquist)) <!-- 18.0.83 -->
+- Add rubygems_url property to chef_client_config resource [#12724](https://github.com/chef/chef/pull/12724) ([DecoyJoe](https://github.com/DecoyJoe)) <!-- 18.0.82 -->
+- Bump libxml2 to 2.9.13 [#12739](https://github.com/chef/chef/pull/12739) ([johnmccrae](https://github.com/johnmccrae)) <!-- 18.0.81 -->
+- avoid coercion to block for new plist file content, fix adding a new dictionary [#12680](https://github.com/chef/chef/pull/12680) ([jazaval](https://github.com/jazaval)) <!-- 18.0.80 -->
+- Add support for default secret service and config [#12140](https://github.com/chef/chef/pull/12140) ([jasonwbarnett](https://github.com/jasonwbarnett)) <!-- 18.0.79 -->
+- Client code to put a certificate into the certstore and then retrieve it later during a chef-run. Unit tests included [#12640](https://github.com/chef/chef/pull/12640) ([johnmccrae](https://github.com/johnmccrae)) <!-- 18.0.78 -->
+- update libxml2 to 2.9.12 [#12730](https://github.com/chef/chef/pull/12730) ([johnmccrae](https://github.com/johnmccrae)) <!-- 18.0.77 -->
- Package resource was calling a non-existent error method [#12722](https://github.com/chef/chef/pull/12722) ([johnmccrae](https://github.com/johnmccrae)) <!-- 18.0.76 -->
- update chef client launchd to run at load [#12706](https://github.com/chef/chef/pull/12706) ([rishichawda](https://github.com/rishichawda)) <!-- 18.0.75 -->
- Enable ruby 3.1 testing on verify pipeline for unix, fix breakage [#12695](https://github.com/chef/chef/pull/12695) ([lamont-granquist](https://github.com/lamont-granquist)) <!-- 18.0.74 -->
diff --git a/Gemfile.lock b/Gemfile.lock
index 2375aa2961..a2a34e6eb7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -42,12 +42,12 @@ GIT
PATH
remote: .
specs:
- chef (18.0.76)
+ chef (18.0.87)
addressable
aws-sdk-s3 (~> 1.91)
aws-sdk-secretsmanager (~> 1.46)
- chef-config (= 18.0.76)
- chef-utils (= 18.0.76)
+ chef-config (= 18.0.87)
+ chef-utils (= 18.0.87)
chef-vault
chef-zero (>= 14.0.11)
corefoundation (~> 0.3.4)
@@ -75,13 +75,13 @@ PATH
train-winrm (>= 0.2.5)
uuidtools (>= 2.1.5, < 3.0)
vault (~> 0.16)
- chef (18.0.76-universal-mingw32)
+ chef (18.0.87-universal-mingw32)
addressable
aws-sdk-s3 (~> 1.91)
aws-sdk-secretsmanager (~> 1.46)
- chef-config (= 18.0.76)
+ chef-config (= 18.0.87)
chef-powershell (~> 1.0.12)
- chef-utils (= 18.0.76)
+ chef-utils (= 18.0.87)
chef-vault
chef-zero (>= 14.0.11)
corefoundation (~> 0.3.4)
@@ -124,15 +124,15 @@ PATH
PATH
remote: chef-bin
specs:
- chef-bin (18.0.76)
- chef (= 18.0.76)
+ chef-bin (18.0.87)
+ chef (= 18.0.87)
PATH
remote: chef-config
specs:
- chef-config (18.0.76)
+ chef-config (18.0.87)
addressable
- chef-utils (= 18.0.76)
+ chef-utils (= 18.0.87)
fuzzyurl
mixlib-config (>= 2.2.12, < 4.0)
mixlib-shellout (>= 2.0, < 4.0)
@@ -141,7 +141,7 @@ PATH
PATH
remote: chef-utils
specs:
- chef-utils (18.0.76)
+ chef-utils (18.0.87)
concurrent-ruby
GEM
@@ -227,6 +227,7 @@ GEM
net-ssh
ffi (1.15.5)
ffi (1.15.5-x64-mingw32)
+ ffi (1.15.5-x64-unknown)
ffi (1.15.5-x86-mingw32)
ffi-libarchive (1.1.3)
ffi (~> 1.0)
@@ -498,6 +499,7 @@ GEM
PLATFORMS
ruby
x64-mingw32
+ x64-unknown
x86-mingw32
DEPENDENCIES
diff --git a/VERSION b/VERSION
index 330859b680..e9b4a23ef2 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-18.0.76 \ No newline at end of file
+18.0.87 \ No newline at end of file
diff --git a/chef-bin/lib/chef-bin/version.rb b/chef-bin/lib/chef-bin/version.rb
index 34d7a71320..3ba159e902 100644
--- a/chef-bin/lib/chef-bin/version.rb
+++ b/chef-bin/lib/chef-bin/version.rb
@@ -21,7 +21,7 @@
module ChefBin
CHEFBIN_ROOT = File.expand_path("..", __dir__)
- VERSION = "18.0.76".freeze
+ VERSION = "18.0.87".freeze
end
#
diff --git a/chef-config/lib/chef-config/version.rb b/chef-config/lib/chef-config/version.rb
index 0b1e76e558..2edb961fc8 100644
--- a/chef-config/lib/chef-config/version.rb
+++ b/chef-config/lib/chef-config/version.rb
@@ -15,5 +15,5 @@
module ChefConfig
CHEFCONFIG_ROOT = File.expand_path("..", __dir__)
- VERSION = "18.0.76".freeze
+ VERSION = "18.0.87".freeze
end
diff --git a/chef-utils/lib/chef-utils/mash.rb b/chef-utils/lib/chef-utils/mash.rb
index bb48064aa3..14159d175a 100644
--- a/chef-utils/lib/chef-utils/mash.rb
+++ b/chef-utils/lib/chef-utils/mash.rb
@@ -106,6 +106,14 @@ module ChefUtils
alias_method :regular_update, :update
end
+ unless method_defined?(:regular_clear)
+ alias_method :regular_clear, :clear
+ end
+
+ unless method_defined?(:regular_delete)
+ alias_method :regular_delete, :delete
+ end
+
# @param key<Object> The key to get.
def [](key)
regular_reader(key)
diff --git a/chef-utils/lib/chef-utils/version.rb b/chef-utils/lib/chef-utils/version.rb
index 2ee5adc417..8f41fce4bc 100644
--- a/chef-utils/lib/chef-utils/version.rb
+++ b/chef-utils/lib/chef-utils/version.rb
@@ -16,5 +16,5 @@
module ChefUtils
CHEFUTILS_ROOT = File.expand_path("..", __dir__)
- VERSION = "18.0.76"
+ VERSION = "18.0.87"
end
diff --git a/cspell.json b/cspell.json
index ac96e3d643..681d67fc76 100644
--- a/cspell.json
+++ b/cspell.json
@@ -120,6 +120,7 @@
"borat",
"bsearch",
"bsevn",
+ "BSTR",
"bufptr",
"bufsize",
"bugfixing",
@@ -1497,7 +1498,13 @@
"ESRCH",
"domainuser",
"rfind",
- "gempath"
+ "gempath",
+ "recusively",
+ "postprocess",
+ "metaparameter",
+ "igroups",
+ "errorhandler",
+ "IPACK"
],
// flagWords - list of words to be always considered incorrect
// This is useful for offensive words and common spelling errors.
diff --git a/docs/dev/how_to/building_chef_client_and_related_gems.md b/docs/dev/how_to/building_chef_client_and_related_gems.md
new file mode 100644
index 0000000000..e8eafb2b55
--- /dev/null
+++ b/docs/dev/how_to/building_chef_client_and_related_gems.md
@@ -0,0 +1,113 @@
+# How to Build Chef Client and Associated Products
+
+
+
+This page endeavors to explain the vagaries of building out Chef Client and its accoutrements. The team is responsible for several gems in addition to the Chef Client codebase. We release the following gems:
+
+- Chef-Config
+- Chef-Utils
+- Chef-PowerShell
+- Knife
+- Chef-bin
+- Chef-Client
+
+
+
+### Contact Us: #eng-infra-chef in Slack
+
+**Codebase**: [chef-powershell-shim](https://github.com/chef/chef-powershell-shim)
+
+**Action**: Build
+
+**Access**: Github Actions
+
+**What to do**:
+
+When you peruse the repo you’ll note that there are a number of directories here. You will need to manage the accompanying .NET code bases in addition to the chef-powershell code itself.
+
+There are 2 ways to use this pipeline. The first is to merely merge your PR back into main once all the tests pass. This triggers an automated Github Actions pipeline that will push a compiled gem to RubyGems.org. The second way is to manually build and test the gem and then manually push it to Rubygems.org. You will need access to the RubyGems - chef-powershell repo via API key to do a manual upload. **NOTE**: This code base is entirely windows based. Meaning, you will compile and test on Windows and the gem will only run on a Windows device. The code supports PowerShell Core but the gcc libraries needed to run on Mac/Linux have not been built out yet.
+
+
+
+**Codebase**: [win32-certstore](https://github.com/chef/win32-certstore)
+
+**Access**: Pull Requests / Expeditor
+
+**What to do**:
+
+The Win32-Certstore code provides an FFI/Win32 based gem used to manage certificates on a Windows node.
+
+This one is a bit gnarly, see the notes below. Chef Infra team has backlogged issues to add functionality to the C++ libraries. If you have some spare cycles, we need the help.
+
+To start `git checkout -b`There are 2 parts of the code here. You’ll need to do your thing in C++ and then make sure the corresponding FFI/Ruby interfaces and methods are all working. Then push your branch back to main and open a PR. Once your code is green and merged, you’re done . **NOTE:** This code base is Windows based and contains C++ code. You’ll need a windows machine to test on. You can probably get away with developing on Mac or Linux.
+
+
+
+**Codebase**: [chef](https://github.com/chef/chef)
+
+**Action**: Promote a Build
+
+**Access**: Chef Internal Slack
+
+**What to do**:
+
+There are a few steps in performing a release. They are documented here : https://github.com/chef/chef/blob/main/docs/dev/how_to/releasing_chef_infra.md
+
+In Essence:
+
+1. Is your current build clean - no random errors, no busted tests?
+2. Have you documented all the changes in this build? This is a critical step to help customers and partner understand the changes we’re releasing.
+3. Promote the build
+4. Announce the build in Slack to #sous-chefs and #general
+5. Update Homebrew
+6. Update Chocolatey
+7. Backport to Chef 17 and Chef 16 as appropriate
+ 1. Git Pull Chef
+ 2. git checkout chef17
+ 3. git checkout -b my_branch_based_on_chef17
+ 4. Do my work on chef17 branch
+ 5. Merge it back to Chef17
+
+
+
+**Codebase**: [chef](https://github.com/chef/chef)
+
+**Action**: Build Chef Client
+
+**Access**: Pull Requests / Expeditor
+
+**What to do**:
+
+You have a feature or a bug you just fixed. Now what? Write your tests and, run rake to look for linting errors, spelling mistakes etc. Then push your branch back to main and create a pull request. This kicks off a build that will run your code against all 20 or so operating systems we support. Builds take a while. Once your build starts you have 2-3 hours or so to do something else. Once your build passes, get it approved and merged back to main. You’re done, unless you’re in charge of releases this week, in which case see the item just above about promoting builds
+
+
+
+**Codebase**: [chef](https://github.com/chef/chef)
+
+**Action**: Ad Hoc builds
+
+**Access**: Buildkite
+
+**What to do**:
+
+You have some code that may or may not really dodgy and you kinda need/want to see where the possible problems are with it. You can do an ad-hoc build against your branch to give it a go. To do that, you do this: There are 2 paths you can follow for a build. Chef stand-alone and Chef as part of the Chef Workstation product.
+
+[Chef Client Ad-Hoc Build Site](https://buildkite.com/chef/chef-chef-master-omnibus-adhoc/)
+
+[Chef Workstation Ad Hoc Build Site](https://buildkite.com/chef/chef-chef-workstation-master-omnibus-adhoc/)
+
+Steps:
+
+1. Click either link and if asked, confirm your login settings and then click the link in the verification email.
+
+2. Once past that you’ll need to add a ‘Pipeline’ - create a name for your pipeline and git it the root of github repo you want to build from
+
+3. Past that you’ll be asked to create a new build that uses your pipeline. Notice you can use any branch, you’ll enter yours here.
+
+4. You can use the options page to add environment variables that are unique to your build or do things like build only Windows nodes:
+
+ ```
+ OMNIBUS_BUILD_FILTER=windows*
+ ```
+
+
diff --git a/kitchen-tests/cookbooks/end_to_end/recipes/_chef_client_config.rb b/kitchen-tests/cookbooks/end_to_end/recipes/_chef_client_config.rb
index 31dac49fc6..c0e095a50e 100644
--- a/kitchen-tests/cookbooks/end_to_end/recipes/_chef_client_config.rb
+++ b/kitchen-tests/cookbooks/end_to_end/recipes/_chef_client_config.rb
@@ -4,6 +4,7 @@ chef_client_config "Create chef-client's client.rb" do
chef_license "accept"
ohai_optional_plugins %i{Passwd Lspci Sysctl}
ohai_disabled_plugins %i{Sessions Interrupts}
+ rubygems_url "https://rubygems.org/"
additional_config <<~CONFIG
begin
require 'aws-sdk'
diff --git a/kitchen-tests/test/integration/end-to-end/_chef_client_config.rb b/kitchen-tests/test/integration/end-to-end/_chef_client_config.rb
index 4cc1459b7b..60228978c2 100644
--- a/kitchen-tests/test/integration/end-to-end/_chef_client_config.rb
+++ b/kitchen-tests/test/integration/end-to-end/_chef_client_config.rb
@@ -7,5 +7,6 @@ client_rb = if os.windows?
describe file(client_rb) do
its("content") { should match(%r{chef_server_url "https://localhost"}) }
its("content") { should match(/chef_license "accept"/) }
+ its("content") { should match(%r{rubygems_url "https://rubygems.org/"}) }
its("content") { should match(/require 'aws-sdk'/) }
end
diff --git a/knife/lib/chef/knife/version.rb b/knife/lib/chef/knife/version.rb
index f267755ed1..a23cadde0a 100644
--- a/knife/lib/chef/knife/version.rb
+++ b/knife/lib/chef/knife/version.rb
@@ -17,7 +17,7 @@
class Chef
class Knife
KNIFE_ROOT = File.expand_path("../..", __dir__)
- VERSION = "18.0.76".freeze
+ VERSION = "18.0.87".freeze
end
end
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
#
diff --git a/omnibus/Gemfile.lock b/omnibus/Gemfile.lock
index b2c045d513..e244cba958 100644
--- a/omnibus/Gemfile.lock
+++ b/omnibus/Gemfile.lock
@@ -1,6 +1,6 @@
GIT
remote: https://github.com/chef/omnibus-software.git
- revision: 5947560ea75ff76243d39243ede62fa45f979895
+ revision: 1451aa249ab4177da82d78665f7fbe5eab0186d8
branch: main
specs:
omnibus-software (4.0.0)
@@ -33,8 +33,8 @@ GEM
artifactory (3.0.15)
awesome_print (1.9.2)
aws-eventstream (1.2.0)
- aws-partitions (1.563.0)
- aws-sdk-core (3.128.0)
+ aws-partitions (1.571.0)
+ aws-sdk-core (3.130.0)
aws-eventstream (~> 1, >= 1.0.2)
aws-partitions (~> 1, >= 1.525.0)
aws-sigv4 (~> 1.1)
@@ -46,12 +46,14 @@ GEM
aws-sdk-core (~> 3, >= 3.127.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.4)
- aws-sdk-secretsmanager (1.58.0)
+ aws-sdk-secretsmanager (1.59.0)
aws-sdk-core (~> 3, >= 3.127.0)
aws-sigv4 (~> 1.1)
aws-sigv4 (1.4.0)
aws-eventstream (~> 1, >= 1.0.2)
bcrypt_pbkdf (1.1.0)
+ bcrypt_pbkdf (1.1.0-x64-mingw32)
+ bcrypt_pbkdf (1.1.0-x86-mingw32)
berkshelf (7.2.2)
chef (>= 15.7.32)
chef-config
@@ -66,16 +68,16 @@ GEM
solve (~> 4.0)
thor (>= 0.20)
builder (3.2.4)
- chef (17.9.52)
+ chef (17.10.0)
addressable
aws-sdk-s3 (~> 1.91)
aws-sdk-secretsmanager (~> 1.46)
- chef-config (= 17.9.52)
- chef-utils (= 17.9.52)
+ chef-config (= 17.10.0)
+ chef-utils (= 17.10.0)
chef-vault
chef-zero (>= 14.0.11)
corefoundation (~> 0.3.4)
- diff-lcs (>= 1.2.4, < 1.4.0)
+ diff-lcs (>= 1.2.4, < 1.6.0, != 1.4.0)
erubis (~> 2.7)
ffi (>= 1.5.0)
ffi-libarchive (~> 1.0, >= 1.0.3)
@@ -97,16 +99,17 @@ GEM
train-winrm (>= 0.2.5)
uuidtools (>= 2.1.5, < 3.0)
vault (~> 0.16)
- chef (17.9.52-universal-mingw32)
+ chef (17.10.0-universal-mingw32)
addressable
aws-sdk-s3 (~> 1.91)
aws-sdk-secretsmanager (~> 1.46)
- chef-config (= 17.9.52)
- chef-utils (= 17.9.52)
+ chef-config (= 17.10.0)
+ chef-powershell (~> 1.0.12)
+ chef-utils (= 17.10.0)
chef-vault
chef-zero (>= 14.0.11)
corefoundation (~> 0.3.4)
- diff-lcs (>= 1.2.4, < 1.4.0)
+ diff-lcs (>= 1.2.4, < 1.6.0, != 1.4.0)
erubis (~> 2.7)
ffi (>= 1.5.0)
ffi-libarchive (~> 1.0, >= 1.0.3)
@@ -140,17 +143,20 @@ GEM
win32-taskscheduler (~> 2.0)
wmi-lite (~> 1.0)
chef-cleanroom (1.0.4)
- chef-config (17.9.52)
+ chef-config (17.10.0)
addressable
- chef-utils (= 17.9.52)
+ chef-utils (= 17.10.0)
fuzzyurl
mixlib-config (>= 2.2.12, < 4.0)
mixlib-shellout (>= 2.0, < 4.0)
tomlrb (~> 1.2)
+ chef-powershell (1.0.13)
+ ffi (~> 1.15)
+ ffi-yajl (~> 2.4)
chef-telemetry (1.1.1)
chef-config
concurrent-ruby (~> 1.0)
- chef-utils (17.9.52)
+ chef-utils (17.10.0)
concurrent-ruby
chef-vault (4.1.5)
chef-zero (15.0.11)
@@ -163,11 +169,11 @@ GEM
citrus (3.0.2)
cleanroom (1.0.0)
coderay (1.1.3)
- concurrent-ruby (1.1.9)
+ concurrent-ruby (1.1.10)
contracts (0.16.1)
corefoundation (0.3.13)
ffi (>= 1.15.0)
- diff-lcs (1.3)
+ diff-lcs (1.5.0)
ed25519 (1.3.0)
erubi (1.10.0)
erubis (2.7.0)
@@ -203,7 +209,7 @@ GEM
hashie (4.1.0)
httpclient (2.8.3)
iniparse (1.5.0)
- inspec-core (4.52.9)
+ inspec-core (4.56.19)
addressable (~> 2.4)
chef-telemetry (~> 1.0, >= 1.0.8)
faraday (>= 0.9.0, < 1.5)
@@ -216,7 +222,7 @@ GEM
parallel (~> 1.9)
parslet (>= 1.5, < 2.0)
pry (~> 0.13)
- rspec (>= 3.9, < 3.11)
+ rspec (>= 3.9, <= 3.11)
rspec-its (~> 1.2)
rubyzip (>= 1.2.2, < 3.0)
semverse (~> 3.0)
@@ -229,7 +235,7 @@ GEM
iostruct (0.0.4)
ipaddress (0.8.3)
iso8601 (0.13.0)
- jmespath (1.6.0)
+ jmespath (1.6.1)
json (2.6.1)
kitchen-vagrant (1.11.0)
test-kitchen (>= 1.4, < 4)
@@ -262,9 +268,9 @@ GEM
mixlib-versioning
thor
mixlib-log (3.0.9)
- mixlib-shellout (3.2.5)
+ mixlib-shellout (3.2.6)
chef-utils
- mixlib-shellout (3.2.5-universal-mingw32)
+ mixlib-shellout (3.2.6-universal-mingw32)
chef-utils
ffi-win32-extensions (~> 1.0.3)
win32-process (~> 0.9)
@@ -297,7 +303,7 @@ GEM
plist (~> 3.1)
train-core
wmi-lite (~> 1.0)
- parallel (1.21.0)
+ parallel (1.22.1)
parslet (1.8.2)
pastel (0.8.0)
tty-color (~> 0.5)
@@ -316,22 +322,22 @@ GEM
rack (2.2.3)
rainbow (3.1.1)
retryable (3.0.5)
- rspec (3.10.0)
- rspec-core (~> 3.10.0)
- rspec-expectations (~> 3.10.0)
- rspec-mocks (~> 3.10.0)
- rspec-core (3.10.2)
- rspec-support (~> 3.10.0)
- rspec-expectations (3.10.2)
+ rspec (3.11.0)
+ rspec-core (~> 3.11.0)
+ rspec-expectations (~> 3.11.0)
+ rspec-mocks (~> 3.11.0)
+ rspec-core (3.11.0)
+ rspec-support (~> 3.11.0)
+ rspec-expectations (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.10.0)
+ rspec-support (~> 3.11.0)
rspec-its (1.3.0)
rspec-core (>= 3.0.0)
rspec-expectations (>= 3.0.0)
- rspec-mocks (3.10.3)
+ rspec-mocks (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
- rspec-support (~> 3.10.0)
- rspec-support (3.10.3)
+ rspec-support (~> 3.11.0)
+ rspec-support (3.11.0)
ruby-progressbar (1.11.0)
ruby2_keywords (0.0.5)
rubyntlm (0.6.3)
@@ -369,7 +375,7 @@ GEM
toml-rb (2.1.2)
citrus (~> 3.0, > 3.0)
tomlrb (1.3.0)
- train-core (3.8.7)
+ train-core (3.8.9)
addressable (~> 2.5)
ffi (!= 1.13.0)
json (>= 1.8, < 3.0)
diff --git a/omnibus_overrides.rb b/omnibus_overrides.rb
index 64c435204d..e65b24c0c7 100644
--- a/omnibus_overrides.rb
+++ b/omnibus_overrides.rb
@@ -7,8 +7,13 @@ override "libffi", version: "3.4.2"
override "libiconv", version: "1.16"
override "liblzma", version: "5.2.5"
override "libtool", version: "2.4.2"
-override "libxml2", version: "2.9.10"
-override "libxslt", version: "1.1.34"
+
+# libxslt 1.1.35 does not build successfully with libxml2 2.9.13 on Windows so we will pin
+# windows builds to libxslt 1.1.34 and libxml2 2.9.10 for now and followup later with the
+# work to fix that issue in IPACK-145.
+override "libxml2", version: windows? ? "2.9.10" : "2.9.13"
+override "libxslt", version: windows? ? "1.1.34" : "1.1.35"
+
override "libyaml", version: "0.1.7"
override "makedepend", version: "1.0.5"
override "ncurses", version: "5.9"
diff --git a/spec/functional/resource/plist_spec.rb b/spec/functional/resource/plist_spec.rb
new file mode 100644
index 0000000000..324a01c0c6
--- /dev/null
+++ b/spec/functional/resource/plist_spec.rb
@@ -0,0 +1,25 @@
+require "spec_helper"
+require "plist"
+
+describe Chef::Resource::PlistResource, :macos_only, requires_root: true do
+ include RecipeDSLHelper
+
+ let(:global_prefs) do
+ File.join(Dir.mktmpdir, ".GlobalPreferences.plist")
+ end
+
+ before(:each) do
+ FileUtils.rm_f global_prefs
+ end
+
+ context "make Monday the first DOW" do
+ it "creates a new plist with a hash value" do
+ plist global_prefs do
+ entry "AppleFirstWeekday"
+ value(gregorian: 4)
+ end
+ expect(File.exist?(global_prefs))
+ expect(shell_out!("/usr/libexec/PlistBuddy -c 'Print :\"AppleFirstWeekday\":gregorian' \"#{global_prefs}\"").stdout.to_i).to eq(4)
+ end
+ end
+end
diff --git a/spec/integration/client/client_spec.rb b/spec/integration/client/client_spec.rb
index 9bf7497a9e..3960123acd 100644
--- a/spec/integration/client/client_spec.rb
+++ b/spec/integration/client/client_spec.rb
@@ -35,21 +35,22 @@ describe "chef-client" do
@server = @api = nil
end
- def install_certificate_in_store
+ def install_certificate_in_store(client_name)
if ChefUtils.windows?
powershell_exec!("New-SelfSignedCertificate -certstorelocation cert:\\localmachine\\my -Subject #{client_name} -FriendlyName #{client_name} -KeyExportPolicy Exportable")
end
end
def create_registry_key
- @win32registry = Chef::Win32::Registry.new
- path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
- unless @win32registry.key_exists?(path)
- @win32registry.create_key(path, true)
- end
- password = SOME_CHARS.sample(1 + rand(SOME_CHARS.count)).join[0...14]
- values = { name: "PfxPass", type: :string, data: password }
- @win32registry.set_value(path, values)
+ ::Chef::HTTP::Authenticator.get_cert_password
+ # @win32registry = Chef::Win32::Registry.new
+ # path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
+ # unless @win32registry.key_exists?(path)
+ # @win32registry.create_key(path, true)
+ # end
+ # password = SOME_CHARS.sample(1 + rand(SOME_CHARS.count)).join[0...14]
+ # values = { name: "PfxPass", type: :string, data: password }
+ # @win32registry.set_value(path, values)
end
def remove_certificate_from_store
@@ -59,7 +60,7 @@ describe "chef-client" do
end
def remove_registry_key
- powershell_exec!("Remove-Item -Path HKLM:\\SOFTWARE\\Progress -Recurse")
+ powershell_exec!("Remove-ItemProperty -Path HKLM:\\SOFTWARE\\Progress\\Authentication -Name 'PfxPass' ")
end
def verify_export_password_exists
@@ -92,6 +93,7 @@ describe "chef-client" do
let(:chef_client) { "bundle exec #{ChefUtils::Dist::Infra::CLIENT} --minimal-ohai --always-dump-stacktrace" }
let(:chef_solo) { "bundle exec #{ChefUtils::Dist::Solo::EXEC} --legacy-mode --minimal-ohai --always-dump-stacktrace" }
let(:client_name) { "chef-973334" }
+ let(:hostname) { "973334" }
context "when validation.pem in current Directory" do
let(:validation_path) { "" }
@@ -178,7 +180,6 @@ describe "chef-client" do
# FATAL: Configuration error NoMethodError: undefined method `xxx' for nil:NilClass
expect(result.stdout).to include("xxx")
end
-
end
it "should complete with success" do
@@ -194,19 +195,17 @@ describe "chef-client" do
if ChefUtils.windows?
context "and the private key is in the Windows CertStore" do
before do
- # install the p12/pfx and make sure the key and password are stored in the registry
- install_certificate_in_store
+ install_certificate_in_store(client_name)
create_registry_key
end
after do
- # remove the p12/pfx and remove the registry key
remove_certificate_from_store
remove_registry_key
end
it "should verify that the cert is loaded in the LocalMachine\\My" do
- expect(Chef::HTTP::Authenticator.check_certstore_for_key(client_name)).to eq(true)
+ expect(Chef::HTTP::Authenticator.check_certstore_for_key(hostname)).to eq(true)
end
it "should verify that the export password for the pfx is loaded in the Registry" do
@@ -214,7 +213,7 @@ describe "chef-client" do
end
it "should verify that a private key is returned to me" do
- expect(Chef::HTTP::Authenticator.retrieve_certificate_key(client_name)).not_to be_falsey
+ expect(Chef::HTTP::Authenticator.retrieve_certificate_key(client_name)).not_to be nil
end
end
end
diff --git a/spec/unit/client_spec.rb b/spec/unit/client_spec.rb
index 1e835ae398..7f02eacb39 100644
--- a/spec/unit/client_spec.rb
+++ b/spec/unit/client_spec.rb
@@ -23,6 +23,11 @@ require "chef/run_context"
require "chef/server_api"
require "rbconfig"
+begin
+ require "chef-powershell"
+rescue LoadError
+end
+
class FooError < RuntimeError
end
@@ -282,6 +287,53 @@ shared_examples "a failed run" do
end
end
+describe Chef::Client, :windows_only do
+ let(:hostname) { "test" }
+ let(:my_client) { Chef::Client.new }
+ let(:cert_name) { "chef-#{hostname}" }
+ let(:node_name) { "#{hostname}" }
+ let(:end_date) do
+ 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
+ end
+ # include_context "client"
+ before(:each) do
+ Chef::Config[:migrate_key_to_keystore] = true
+ end
+
+ after(:each) do
+ delete_certificate(cert_name)
+ end
+
+ context "when the client intially boots the first time" do
+ it "verfies that a certificate was correctly created and exists in the Cert Store" do
+ new_pfx = my_client.generate_pfx_package(cert_name, end_date)
+ my_client.import_pfx_to_store(new_pfx)
+ expect(my_client.check_certstore_for_key(cert_name)).not_to be false
+ end
+
+ it "correctly returns a new Publc Key" do
+ new_pfx = my_client.generate_pfx_package(cert_name, end_date)
+ cert_object = new_pfx.certificate.public_key.to_pem
+ expect(cert_object.to_s).to match(/PUBLIC KEY/)
+ end
+
+ end
+
+ def delete_certificate(cert_name)
+ require "chef/mixin/powershell_exec"
+ extend Chef::Mixin::PowershellExec
+ powershell_code = <<~CODE
+ Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "#{cert_name}" } | Remove-item
+ CODE
+ powershell_exec!(powershell_code)
+ end
+end
+
describe Chef::Client do
include_context "client"
diff --git a/spec/unit/dsl/secret_spec.rb b/spec/unit/dsl/secret_spec.rb
index 96a915c43d..9952980697 100644
--- a/spec/unit/dsl/secret_spec.rb
+++ b/spec/unit/dsl/secret_spec.rb
@@ -17,11 +17,14 @@
#
require "spec_helper"
+require "chef/exceptions"
require "chef/dsl/secret"
require "chef/secret_fetcher/base"
+
class SecretDSLTester
include Chef::DSL::Secret
- # Because DSL is invoked in the context of a recipe,
+
+ # Because DSL is invoked in the context of a recipe or attribute file
# we expect run_context to always be available when SecretFetcher::Base
# requests it - making it safe to mock here
def run_context
@@ -37,35 +40,136 @@ end
describe Chef::DSL::Secret do
let(:dsl) { SecretDSLTester.new }
- it "responds to 'secret'" do
- expect(dsl.respond_to?(:secret)).to eq true
+ let(:run_context) { Chef::RunContext.new(Chef::Node.new, {}, Chef::EventDispatch::Dispatcher.new) }
+
+ before do
+ allow(dsl).to receive(:run_context).and_return(run_context)
end
- it "uses SecretFetcher.for_service to find the fetcher" do
- substitute_fetcher = SecretFetcherImpl.new({}, nil)
- expect(Chef::SecretFetcher).to receive(:for_service).with(:example, {}, nil).and_return(substitute_fetcher)
- expect(substitute_fetcher).to receive(:fetch).with("key1", nil)
- dsl.secret(name: "key1", service: :example, config: {})
+ %w{
+ secret
+ default_secret_service
+ default_secret_config
+ with_secret_service
+ with_secret_config
+ }.each do |m|
+ it "responds to ##{m}" do
+ expect(dsl.respond_to?(m)).to eq true
+ end
+ end
+
+ describe "#default_secret_service" do
+ let(:service) { :hashi_vault }
+
+ it "persists the service passed in as an argument" do
+ expect(dsl.default_secret_service).to eq(nil)
+ dsl.default_secret_service(service)
+ expect(dsl.default_secret_service).to eq(service)
+ end
+
+ it "returns run_context.default_secret_service value when no argument is given" do
+ run_context.default_secret_service = :my_thing
+ expect(dsl.default_secret_service).to eq(:my_thing)
+ end
+
+ it "raises exception when service given is not valid" do
+ stub_const("Chef::SecretFetcher::SECRET_FETCHERS", %i{service_a service_b})
+ expect { dsl.default_secret_service(:unknown_service) }.to raise_error(Chef::Exceptions::Secret::InvalidFetcherService)
+ end
end
- it "resolves a secret when using the example fetcher" do
- secret_value = dsl.secret(name: "test1", service: :example, config: { "test1" => "secret value" })
- expect(secret_value).to eq "secret value"
+ describe "#with_secret_config" do
+ let(:service) { :hashi_vault }
+
+ it "sets the service for the scope of the block only" do
+ expect(dsl.default_secret_service).to eq(nil)
+ dsl.with_secret_service(service) do
+ expect(dsl.default_secret_service).to eq(service)
+ end
+ expect(dsl.default_secret_service).to eq(nil)
+ end
+
+ it "raises exception when block is not given" do
+ expect { dsl.with_secret_service(service) }.to raise_error(ArgumentError)
+ end
end
- context "when used within a resource" do
- let(:run_context) {
- Chef::RunContext.new(Chef::Node.new,
- Chef::CookbookCollection.new(Chef::CookbookLoader.new(File.join(CHEF_SPEC_DATA, "cookbooks"))),
- Chef::EventDispatch::Dispatcher.new)
- }
-
- it "marks that resource as 'sensitive'" do
- recipe = Chef::Recipe.new("secrets", "test", run_context)
- recipe.zen_master "secret_test" do
- peace secret(name: "test1", service: :example, config: { "test1" => true })
+ describe "#default_secret_config" do
+ let(:config) { { my_key: "value" } }
+
+ it "persists the config passed in as argument" do
+ expect(dsl.default_secret_config).to eq({})
+ dsl.default_secret_config(**config)
+ expect(dsl.default_secret_config).to eq(config)
+ end
+
+ it "returns run_context.default_secret_config value when no argument is given" do
+ run_context.default_secret_config = { my_thing: "that" }
+ expect(dsl.default_secret_config).to eq({ my_thing: "that" })
+ end
+ end
+
+ describe "#with_secret_config" do
+ let(:config) { { my_key: "value" } }
+
+ it "sets the config for the scope of the block only" do
+ expect(dsl.default_secret_config).to eq({})
+ dsl.with_secret_config(**config) do
+ expect(dsl.default_secret_config).to eq(config)
end
- expect(run_context.resource_collection.lookup("zen_master[secret_test]").sensitive).to eql(true)
+ expect(dsl.default_secret_config).to eq({})
+ end
+
+ it "raises exception when block is not given" do
+ expect { dsl.with_secret_config(**config) }.to raise_error(ArgumentError)
+ end
+ end
+
+ describe "#secret" do
+ it "uses SecretFetcher.for_service to find the fetcher" do
+ substitute_fetcher = SecretFetcherImpl.new({}, nil)
+ expect(Chef::SecretFetcher).to receive(:for_service).with(:example, {}, run_context).and_return(substitute_fetcher)
+ expect(substitute_fetcher).to receive(:fetch).with("key1", nil)
+ dsl.secret(name: "key1", service: :example, config: {})
+ end
+
+ it "resolves a secret when using the example fetcher" do
+ secret_value = dsl.secret(name: "test1", service: :example, config: { "test1" => "secret value" })
+ expect(secret_value).to eq "secret value"
+ end
+
+ context "when used within a resource" do
+ let(:run_context) {
+ Chef::RunContext.new(Chef::Node.new,
+ Chef::CookbookCollection.new(Chef::CookbookLoader.new(File.join(CHEF_SPEC_DATA, "cookbooks"))),
+ Chef::EventDispatch::Dispatcher.new)
+ }
+
+ it "marks that resource as 'sensitive'" do
+ recipe = Chef::Recipe.new("secrets", "test", run_context)
+ recipe.zen_master "secret_test" do
+ peace secret(name: "test1", service: :example, config: { "test1" => true })
+ end
+ expect(run_context.resource_collection.lookup("zen_master[secret_test]").sensitive).to eql(true)
+ end
+ end
+
+ it "passes default service to SecretFetcher.for_service" do
+ service = :example
+ dsl.default_secret_service(service)
+ substitute_fetcher = SecretFetcherImpl.new({}, nil)
+ expect(Chef::SecretFetcher).to receive(:for_service).with(service, {}, run_context).and_return(substitute_fetcher)
+ allow(substitute_fetcher).to receive(:fetch).with("key1", nil)
+ dsl.secret(name: "key1")
+ end
+
+ it "passes default config to SecretFetcher.for_service" do
+ config = { my_config: "value" }
+ dsl.default_secret_config(**config)
+ substitute_fetcher = SecretFetcherImpl.new({}, nil)
+ expect(Chef::SecretFetcher).to receive(:for_service).with(:example, config, run_context).and_return(substitute_fetcher)
+ allow(substitute_fetcher).to receive(:fetch).with("key1", nil)
+ dsl.secret(name: "key1", service: :example)
end
end
end
diff --git a/spec/unit/http/authenticator_spec.rb b/spec/unit/http/authenticator_spec.rb
index 4f43c19520..a1b67610c5 100644
--- a/spec/unit/http/authenticator_spec.rb
+++ b/spec/unit/http/authenticator_spec.rb
@@ -19,6 +19,69 @@
require "spec_helper"
require "chef/http/authenticator"
+describe Chef::HTTP::Authenticator, :windows_only do
+ let(:class_instance) { Chef::HTTP::Authenticator.new(client_name: "test") }
+ let(:method) { "GET" }
+ let(:url) { URI("https://chef.example.com/organizations/test") }
+ let(:headers) { {} }
+ let(:data) { "" }
+ let(:node_name) { "test" }
+ let(:passwrd) { "some_insecure_password" }
+
+ before do
+ Chef::Config[:node_name] = node_name
+ cert_name = "chef-#{node_name}"
+ d = Time.now
+ end_date = Time.new(d.year, d.month + 3, d.day, d.hour, d.min, d.sec).utc.iso8601
+
+ my_client = Chef::Client.new
+ pfx = my_client.generate_pfx_package(cert_name, end_date)
+ my_client.import_pfx_to_store(pfx)
+ end
+
+ after(:each) do
+ require "chef/mixin/powershell_exec"
+ extend Chef::Mixin::PowershellExec
+ cert_name = "chef-#{node_name}"
+ delete_certificate(cert_name)
+ end
+
+ context "when retrieving a certificate from the certificate store" do
+ it "retrieves a certificate password from the registry when the hive does not already exist" do
+ delete_registry_hive
+ expect { class_instance.get_cert_password }.not_to raise_error
+ end
+
+ it "should return a password of at least 14 characters in length" do
+ password = class_instance.get_cert_password
+ expect(password.length).to eql(14)
+ end
+
+ it "correctly retrieves a valid certificate in pem format from the certstore" do
+ require "openssl"
+ certificate = class_instance.retrieve_certificate_key(node_name)
+ cert_object = OpenSSL::PKey::RSA.new(certificate)
+ expect(cert_object.to_s).to match(/BEGIN RSA PRIVATE KEY/)
+ end
+ end
+
+ def delete_certificate(cert_name)
+ powershell_code = <<~CODE
+ Get-ChildItem -path cert:\\LocalMachine\\My -Recurse -Force | Where-Object { $_.Subject -Match "#{cert_name}" } | Remove-item
+ CODE
+ powershell_exec!(powershell_code)
+ end
+
+ def delete_registry_hive
+ @win32registry = Chef::Win32::Registry.new
+ path = "HKEY_LOCAL_MACHINE\\Software\\Progress\\Authentication"
+ present = @win32registry.get_values(path)
+ unless present.nil? || present.empty?
+ @win32registry.delete_key(path, true)
+ end
+ end
+end
+
describe Chef::HTTP::Authenticator do
let(:class_instance) { Chef::HTTP::Authenticator.new(client_name: "test") }
let(:method) { "GET" }
@@ -26,6 +89,10 @@ describe Chef::HTTP::Authenticator do
let(:headers) { {} }
let(:data) { "" }
+ before do
+ ::Chef::Config[:node_name] = "foo"
+ end
+
context "when handle_request is called" do
shared_examples_for "merging the server API version into the headers" do
before do
diff --git a/spec/unit/resource/chef_client_config_spec.rb b/spec/unit/resource/chef_client_config_spec.rb
index 7e6d7a5c5c..a7d1152e3d 100644
--- a/spec/unit/resource/chef_client_config_spec.rb
+++ b/spec/unit/resource/chef_client_config_spec.rb
@@ -134,4 +134,12 @@ describe Chef::Resource::ChefClientConfig do
expect(provider.format_handler([{ "class" => "Foo", "arguments" => ["'one'", "two", "three"] }])).to eql(["Foo.new('one',two,three)"])
end
end
+
+ describe "rubygems_url property" do
+ it "accepts nil, a single URL, or an array of URLs" do
+ expect { resource.rubygems_url(nil) }.not_to raise_error
+ expect { resource.rubygems_url("https://rubygems.internal.example.com") }.not_to raise_error
+ expect { resource.rubygems_url(["https://rubygems.east.example.com", "https://rubygems.west.example.com"]) }.not_to raise_error
+ end
+ end
end
diff --git a/spec/unit/resource/rest_resource_spec.rb b/spec/unit/resource/rest_resource_spec.rb
new file mode 100644
index 0000000000..43cb63d8de
--- /dev/null
+++ b/spec/unit/resource/rest_resource_spec.rb
@@ -0,0 +1,381 @@
+#
+# 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 "spec_helper"
+require "train"
+require "train-rest"
+
+class RestResourceByQuery < Chef::Resource
+ use "core::rest_resource"
+
+ provides :rest_resource_by_query, target_mode: true
+
+ property :address, String, required: true
+ property :prefix, Integer, required: true
+ property :gateway, String
+
+ rest_api_collection "/api/v1/addresses"
+ rest_api_document "/api/v1/address/?ip={address}"
+ rest_property_map({
+ address: "address",
+ prefix: "prefix",
+ gateway: "gateway",
+ })
+end
+
+class RestResourceByPath < RestResourceByQuery
+ provides :rest_resource_by_path, target_mode: true
+
+ rest_api_document "/api/v1/address/{address}"
+end
+
+describe "rest_resource using query-based addressing" do
+ let(:train) {
+ Train.create(
+ "rest", {
+ endpoint: "https://api.example.com/api/v1/",
+ debug_rest: true,
+ logger: Chef::Log,
+ }
+ ).connection
+ }
+
+ let(:run_context) do
+ cookbook_collection = Chef::CookbookCollection.new([])
+ node = Chef::Node.new
+ node.name "node1"
+ events = Chef::EventDispatch::Dispatcher.new
+ Chef::RunContext.new(node, cookbook_collection, events)
+ end
+
+ let(:resource) do
+ RestResourceByQuery.new("set_address", run_context).tap do |resource|
+ resource.address = "192.0.2.1"
+ resource.prefix = 24
+ resource.action :configure
+ end
+ end
+
+ let(:provider) do
+ resource.provider_for_action(:configure).tap do |provider|
+ provider.current_resource = resource # for some stubby tests that don't call LCR
+ allow(provider).to receive(:api_connection).and_return(train)
+ end
+ end
+
+ before(:each) do
+ allow(Chef::Provider).to receive(:new).and_return(provider)
+ end
+
+ it "should include :configure action" do
+ expect(provider).to respond_to(:action_configure)
+ end
+
+ it "should include :delete action" do
+ expect(provider).to respond_to(:action_delete)
+ end
+
+ it "should include :nothing action" do
+ expect(provider).to respond_to(:action_nothing)
+ end
+
+ it "sets the default action as :configure" do
+ expect(resource.action).to eql([:configure])
+ end
+
+ it "supports :configure action" do
+ expect { resource.action :configure }.not_to raise_error
+ end
+
+ it "supports :delete action" do
+ expect { resource.action :delete }.not_to raise_error
+ end
+
+ it "should mixin RestResourceDSL" do
+ expect(resource.class.ancestors).to include(Chef::DSL::RestResource)
+ end
+
+ describe "#rest_postprocess" do
+ before do
+ provider.singleton_class.send(:public, :rest_postprocess)
+ end
+ it "should have a default rest_postprocess implementation" do
+ expect(provider).to respond_to(:rest_postprocess)
+ end
+
+ it "should have a non-mutating rest_postprocess implementation" do
+ response = "{ data: nil }"
+
+ expect(provider.rest_postprocess(response.dup)).to eq(response)
+ end
+ end
+
+ describe "#rest_errorhandler" do
+ before do
+ provider.singleton_class.send(:public, :rest_errorhandler)
+ end
+
+ it "should have a default rest_errorhandler implementation" do
+ expect(provider).to respond_to(:rest_errorhandler)
+ end
+
+ it "should have a non-mutating rest_errorhandler implementation" do
+ error_obj = StandardError.new
+
+ expect(provider.rest_errorhandler(error_obj.dup)).to eq(error_obj)
+ end
+ end
+
+ describe "#required_properties" do
+ before do
+ provider.singleton_class.send(:public, :required_properties)
+ end
+
+ it "should include required properties only" do
+ expect(provider.required_properties).to contain_exactly(:address, :prefix)
+ end
+ end
+
+ describe "#property_map" do
+ before do
+ provider.singleton_class.send(:public, :property_map)
+ end
+
+ it "should map resource properties to values properly" do
+ expect(provider.property_map).to eq({
+ address: "192.0.2.1",
+ prefix: 24,
+ gateway: nil,
+ name: "set_address",
+ })
+ end
+ end
+
+ describe "#rest_url_collection" do
+ before do
+ provider.singleton_class.send(:public, :rest_url_collection)
+ end
+
+ it "should return collection URLs properly" do
+ expect(provider.rest_url_collection).to eq("/api/v1/addresses")
+ end
+ end
+
+ describe "#rest_url_document" do
+ before do
+ provider.singleton_class.send(:public, :rest_url_document)
+ end
+
+ it "should apply URI templates to document URLs using query syntax properly" do
+ expect(provider.rest_url_document).to eq("/api/v1/address/?ip=192.0.2.1")
+ end
+ end
+
+ # TODO: Test with path-style URLs
+ describe "#rest_identity_implicit" do
+ before do
+ provider.singleton_class.send(:public, :rest_identity_implicit)
+ end
+
+ it "should return implicit identity properties properly" do
+ expect(provider.rest_identity_implicit).to eq({ "ip" => :address })
+ end
+ end
+
+ describe "#rest_identity_values" do
+ before do
+ provider.singleton_class.send(:public, :rest_identity_values)
+ end
+
+ it "should return implicit identity properties and values properly" do
+ expect(provider.rest_identity_values).to eq({ "ip" => "192.0.2.1" })
+ end
+ end
+
+ # TODO: changed_value
+ # TODO: load_current_value
+
+ # this might be a functional test, but it runs on any O/S so I leave it here
+ describe "when managing a resource" do
+ before { WebMock.disable_net_connect! }
+ let(:addresses_exists) { JSON.generate([{ "address": "192.0.2.1" }]) }
+ let(:addresses_other) { JSON.generate([{ "address": "172.16.32.85" }]) }
+ let(:address_exists) { JSON.generate({ "address": "192.0.2.1", "prefix": 24, "gateway": "192.0.2.1" }) }
+ let(:prefix_wrong) { JSON.generate({ "address": "192.0.2.1", "prefix": 25, "gateway": "192.0.2.1" }) }
+
+ it "should be idempotent" do
+ stub_request(:get, "https://api.example.com/api/v1/addresses")
+ .to_return(status: 200, body: addresses_exists, headers: { "Content-Type" => "application/json" })
+ stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" })
+ resource.run_action(:configure)
+ expect(resource.updated_by_last_action?).to be false
+ end
+
+ it "should PATCH if a property is incorrect" do
+ stub_request(:get, "https://api.example.com/api/v1/addresses")
+ .to_return(status: 200, body: addresses_exists, headers: { "Content-Type" => "application/json" })
+ stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .to_return(status: 200, body: prefix_wrong, headers: { "Content-Type" => "application/json" })
+ stub_request(:patch, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .with(
+ body: "{\"address\":\"192.0.2.1\",\"prefix\":25}",
+ headers: {
+ "Accept" => "application/json",
+ "Content-Type" => "application/json",
+ }
+ )
+ .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" })
+ resource.run_action(:configure)
+ expect(resource.updated_by_last_action?).to be true
+ end
+
+ it "should POST if there's no resources at all" do
+ stub_request(:get, "https://api.example.com/api/v1/addresses")
+ .to_return(status: 200, body: "[]", headers: { "Content-Type" => "application/json" })
+ stub_request(:post, "https://api.example.com/api/v1/addresses")
+ .with(
+ body: "{\"address\":\"192.0.2.1\",\"prefix\":24,\"ip\":\"192.0.2.1\"}"
+ )
+ .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" })
+ resource.run_action(:configure)
+ expect(resource.updated_by_last_action?).to be true
+ end
+
+ it "should POST if the specific resource does not exist" do
+ stub_request(:get, "https://api.example.com/api/v1/addresses")
+ .to_return(status: 200, body: addresses_other, headers: { "Content-Type" => "application/json" })
+ stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .to_return(status: 404, body: "", headers: {})
+ stub_request(:post, "https://api.example.com/api/v1/addresses")
+ .with(
+ body: "{\"address\":\"192.0.2.1\",\"prefix\":24,\"ip\":\"192.0.2.1\"}"
+ )
+ .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" })
+ resource.run_action(:configure)
+ expect(resource.updated_by_last_action?).to be true
+ end
+
+ it "should be idempotent if the resouces needs deleting and there are no resources at all" do
+ stub_request(:get, "https://api.example.com/api/v1/addresses")
+ .to_return(status: 200, body: "[]", headers: { "Content-Type" => "application/json" })
+ resource.run_action(:delete)
+ expect(resource.updated_by_last_action?).to be false
+ end
+
+ it "should be idempotent if the resource doesn't exist" do
+ stub_request(:get, "https://api.example.com/api/v1/addresses")
+ .to_return(status: 200, body: addresses_other, headers: { "Content-Type" => "application/json" })
+ stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .to_return(status: 404, body: "", headers: {})
+ resource.run_action(:delete)
+ expect(resource.updated_by_last_action?).to be false
+ end
+
+ it "should DELETE the resource if it exists and matches" do
+ stub_request(:get, "https://api.example.com/api/v1/addresses")
+ .to_return(status: 200, body: addresses_exists, headers: { "Content-Type" => "application/json" })
+ stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .to_return(status: 200, body: address_exists, headers: { "Content-Type" => "application/json" })
+ stub_request(:delete, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .to_return(status: 200, body: "", headers: {})
+ resource.run_action(:delete)
+ expect(resource.updated_by_last_action?).to be true
+ end
+
+ it "should DELETE the resource if it exists and doesn't match" do
+ stub_request(:get, "https://api.example.com/api/v1/addresses")
+ .to_return(status: 200, body: addresses_exists, headers: { "Content-Type" => "application/json" })
+ stub_request(:get, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .to_return(status: 200, body: prefix_wrong, headers: { "Content-Type" => "application/json" })
+ stub_request(:delete, "https://api.example.com/api/v1/address/?ip=192.0.2.1")
+ .to_return(status: 200, body: "", headers: {})
+ resource.run_action(:delete)
+ expect(resource.updated_by_last_action?).to be true
+ end
+ end
+end
+
+describe "rest_resource using path-based addressing" do
+ let(:train) {
+ Train.create(
+ "rest", {
+ endpoint: "https://api.example.com/api/v1/",
+ debug_rest: true,
+ logger: Chef::Log,
+ }
+ ).connection
+ }
+
+ let(:run_context) do
+ cookbook_collection = Chef::CookbookCollection.new([])
+ node = Chef::Node.new
+ node.name "node1"
+ events = Chef::EventDispatch::Dispatcher.new
+ Chef::RunContext.new(node, cookbook_collection, events)
+ end
+
+ let(:resource) do
+ RestResourceByPath.new("set_address", run_context).tap do |resource|
+ resource.address = "192.0.2.1"
+ resource.prefix = 24
+ resource.action :configure
+ end
+ end
+
+ let(:provider) do
+ resource.provider_for_action(:configure).tap do |provider|
+ provider.current_resource = resource # for some stubby tests that don't call LCR
+ allow(provider).to receive(:api_connection).and_return(train)
+ end
+ end
+
+ before(:each) do
+ allow(Chef::Provider).to receive(:new).and_return(provider)
+ end
+
+ describe "#rest_url_document" do
+ before do
+ provider.singleton_class.send(:public, :rest_url_document)
+ end
+
+ it "should apply URI templates to document URLs using path syntax properly" do
+ expect(provider.rest_url_document).to eq("/api/v1/address/192.0.2.1")
+ end
+ end
+
+ describe "#rest_identity_implicit" do
+ before do
+ provider.singleton_class.send(:public, :rest_identity_implicit)
+ end
+
+ it "should return implicit identity properties properly" do
+ expect(provider.rest_identity_implicit).to eq({ "address" => :address })
+ end
+ end
+
+ describe "#rest_identity_values" do
+ before do
+ provider.singleton_class.send(:public, :rest_identity_values)
+ end
+
+ it "should return implicit identity properties and values properly" do
+ expect(provider.rest_identity_values).to eq({ "address" => "192.0.2.1" })
+ end
+ end
+
+end
diff --git a/spec/unit/run_context_spec.rb b/spec/unit/run_context_spec.rb
index 5bd7a360fb..3ec8eeed61 100644
--- a/spec/unit/run_context_spec.rb
+++ b/spec/unit/run_context_spec.rb
@@ -53,6 +53,22 @@ describe Chef::RunContext do
expect(run_context.node).to eq(node)
end
+ it "responds to #default_secret_service" do
+ expect(run_context).to respond_to(:default_secret_service)
+ end
+
+ it "responds to #default_secret_config" do
+ expect(run_context).to respond_to(:default_secret_config)
+ end
+
+ it "#default_secret_service defaults to nil" do
+ expect(run_context.default_secret_service).to eq(nil)
+ end
+
+ it "#default_secret_config defaults to {}" do
+ expect(run_context.default_secret_config).to eq({})
+ end
+
it "loads up node[:cookbooks]" do
expect(run_context.node[:cookbooks]).to eql(
{