summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLamont Granquist <lamont@scriptkiddie.org>2018-07-03 14:35:03 -0700
committerGitHub <noreply@github.com>2018-07-03 14:35:03 -0700
commitd58df7b53ed79d81eee0340adf95aa364d6a9a6e (patch)
tree93ccc263ba4702acee53da10ac2ff5ae9145ec07
parent7910a42ca5999ae44c5b6b3dc256558b32b42481 (diff)
parentf9e0314265a9f3200ad90eec5c7666ebce9afd6a (diff)
downloadchef-d58df7b53ed79d81eee0340adf95aa364d6a9a6e.tar.gz
Merge pull request #7390 from coderanger/yet-more-creds
knife config and a bunch of UX improvements
-rw-r--r--RELEASE_NOTES.md22
-rw-r--r--chef-config/lib/chef-config/config.rb20
-rw-r--r--chef-config/lib/chef-config/mixin/credentials.rb2
-rw-r--r--chef-config/lib/chef-config/mixin/dot_d.rb25
-rw-r--r--chef-config/lib/chef-config/path_helper.rb18
-rw-r--r--chef-config/lib/chef-config/workstation_config_loader.rb50
-rw-r--r--chef-config/spec/unit/config_spec.rb18
-rw-r--r--chef-config/spec/unit/workstation_config_loader_spec.rb64
-rw-r--r--lib/chef/application/knife.rb1
-rw-r--r--lib/chef/knife/config_get.rb126
-rw-r--r--spec/integration/knife/config_get_spec.rb183
-rw-r--r--spec/support/shared/integration/knife_support.rb8
12 files changed, 518 insertions, 19 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index 167e04fa3a..6b601bd569 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,27 @@
This file holds "in progress" release notes for the current release under development and is intended for consumption by the Chef Documentation team. Please see <https://docs.chef.io/release_notes.html> for the official Chef release notes.
+## New `knife config get` command
+
+The `knife config get` command has been added to help with debugging configuration
+issues with `knife` and other tools that use the `knife.rb` file.
+
+With no arguments, it will display all options you've set:
+
+```bash
+$ knife config get
+Loading from configuration file /Users/.../.chef/knife.rb
+chef_server_url: https://...
+client_key: /Users/.../.chef/user.pem
+config_file: /Users/.../.chef/knife.rb
+log_level: warn
+log_location: STDERR
+node_name: ...
+validation_key:
+```
+
+You can also pass specific keys to only display those `knife config get node_name client_key`,
+or use `--all` to display everything (including options that are using the default value).
+
## Simplification of `shell_out` APIs
The following methods are deprecated:
diff --git a/chef-config/lib/chef-config/config.rb b/chef-config/lib/chef-config/config.rb
index 07be3e9ef6..3bba8af7f4 100644
--- a/chef-config/lib/chef-config/config.rb
+++ b/chef-config/lib/chef-config/config.rb
@@ -286,7 +286,7 @@ module ChefConfig
# the cache path.
unless path_accessible?(primary_cache_path) || path_accessible?(primary_cache_root)
secondary_cache_path = PathHelper.join(user_home, ".chef")
- ChefConfig.logger.info("Unable to access cache at #{primary_cache_path}. Switching cache to #{secondary_cache_path}")
+ ChefConfig.logger.trace("Unable to access cache at #{primary_cache_path}. Switching cache to #{secondary_cache_path}")
secondary_cache_path
else
primary_cache_path
@@ -656,7 +656,15 @@ module ChefConfig
#
# If chef-zero is enabled, this defaults to nil (no authentication).
default(:validation_key) { chef_zero.enabled ? nil : platform_specific_path("/etc/chef/validation.pem") }
- default :validation_client_name, "chef-validator"
+ default :validation_client_name do
+ # If the URL is set and looks like a normal Chef Server URL, extract the
+ # org name and use that as part of the default.
+ if chef_server_url.to_s =~ %r{/organizations/(.*)$}
+ "#{$1}-validator"
+ else
+ "chef-validator"
+ end
+ end
default :validation_key_contents, nil
# When creating a new client via the validation_client account, Chef 11
@@ -962,10 +970,10 @@ module ChefConfig
# TODO add some post-file-parsing logic that automatically calls this so
# users don't have to
def self.export_proxies
- export_proxy("http", http_proxy, http_proxy_user, http_proxy_pass) if http_proxy
- export_proxy("https", https_proxy, https_proxy_user, https_proxy_pass) if https_proxy
- export_proxy("ftp", ftp_proxy, ftp_proxy_user, ftp_proxy_pass) if ftp_proxy
- export_no_proxy(no_proxy) if no_proxy
+ export_proxy("http", http_proxy, http_proxy_user, http_proxy_pass) if has_key?(:http_proxy) && http_proxy
+ export_proxy("https", https_proxy, https_proxy_user, https_proxy_pass) if has_key?(:https_proxy) && https_proxy
+ export_proxy("ftp", ftp_proxy, ftp_proxy_user, ftp_proxy_pass) if has_key?(:ftp_proxy) && ftp_proxy
+ export_no_proxy(no_proxy) if has_key?(:no_proxy) && no_proxy
end
# Character classes for Addressable
diff --git a/chef-config/lib/chef-config/mixin/credentials.rb b/chef-config/lib/chef-config/mixin/credentials.rb
index 4c0192fff8..5a73a49add 100644
--- a/chef-config/lib/chef-config/mixin/credentials.rb
+++ b/chef-config/lib/chef-config/mixin/credentials.rb
@@ -28,7 +28,7 @@ module ChefConfig
return unless File.file?(credentials_file)
- context = File.read(context_file) if File.file?(context_file)
+ context = File.read(context_file).strip if File.file?(context_file)
environment = ENV.fetch("CHEF_PROFILE", nil)
diff --git a/chef-config/lib/chef-config/mixin/dot_d.rb b/chef-config/lib/chef-config/mixin/dot_d.rb
index 778c25d7f9..4c9c998908 100644
--- a/chef-config/lib/chef-config/mixin/dot_d.rb
+++ b/chef-config/lib/chef-config/mixin/dot_d.rb
@@ -19,17 +19,22 @@ require "chef-config/path_helper"
module ChefConfig
module Mixin
module DotD
+ # Find available configuration files in a `.d/` style include directory.
+ #
+ # @api internal
+ # @param path [String] Base .d/ path to load from.
+ # @return [Array<String>]
+ def find_dot_d(path)
+ Dir["#{PathHelper.escape_glob_dir(path)}/*.rb"].select { |entry| File.file?(entry) }.sort
+ end
+
+ # Load configuration from a `.d/` style include directory.
+ #
+ # @api internal
+ # @param path [String] Base .d/ path to load from.
+ # @return [void]
def load_dot_d(path)
- dot_d_files =
- begin
- entries = Array.new
- entries << Dir.glob(File.join(
- ChefConfig::PathHelper.escape_glob_dir(path), "*.rb"))
- entries.flatten.select do |entry|
- File.file?(entry)
- end
- end
- dot_d_files.sort.map do |conf|
+ find_dot_d(path).each do |conf|
apply_config(IO.read(conf), conf)
end
end
diff --git a/chef-config/lib/chef-config/path_helper.rb b/chef-config/lib/chef-config/path_helper.rb
index 6341ffe4e6..fbb413578a 100644
--- a/chef-config/lib/chef-config/path_helper.rb
+++ b/chef-config/lib/chef-config/path_helper.rb
@@ -172,6 +172,18 @@ module ChefConfig
Pathname.new(cleanpath(to)).relative_path_from(Pathname.new(cleanpath(from)))
end
+ # Set the project-specific home directory environment variable.
+ #
+ # This can be used to allow per-tool home directory aliases like $KNIFE_HOME.
+ #
+ # @param [env_var] Key for an environment variable to use.
+ # @return [nil]
+ def self.per_tool_home_environment=(env_var)
+ @@per_tool_home_environment = env_var
+ # Reset this in case .home was already called.
+ @@home_dir = nil
+ end
+
# Retrieves the "home directory" of the current user while trying to ascertain the existence
# of said directory. The path returned uses / for all separators (the ruby standard format).
# If the home directory doesn't exist or an error is otherwise encountered, nil is returned.
@@ -185,7 +197,9 @@ module ChefConfig
# Home-path discovery is performed once. If a path is discovered, that value is memoized so
# that subsequent calls to home_dir don't bounce around.
#
- # See self.all_homes.
+ # @see all_homes
+ # @param args [Array<String>] Path components to look for under the home directory.
+ # @return [String]
def self.home(*args)
@@home_dir ||= all_homes { |p| break p }
if @@home_dir
@@ -203,6 +217,8 @@ module ChefConfig
# if no block is provided.
def self.all_homes(*args)
paths = []
+ paths << ENV[@@per_tool_home_environment] if defined?(@@per_tool_home_environment) && @@per_tool_home_environment && ENV[@@per_tool_home_environment]
+ paths << ENV["CHEF_HOME"] if ENV["CHEF_HOME"]
if ChefConfig.windows?
# By default, Ruby uses the the following environment variables to determine Dir.home:
# HOME
diff --git a/chef-config/lib/chef-config/workstation_config_loader.rb b/chef-config/lib/chef-config/workstation_config_loader.rb
index 13187d9975..2afe8de547 100644
--- a/chef-config/lib/chef-config/workstation_config_loader.rb
+++ b/chef-config/lib/chef-config/workstation_config_loader.rb
@@ -83,6 +83,8 @@ module ChefConfig
end
load_dot_d(Config[:config_d_dir]) if Config[:config_d_dir]
+
+ apply_defaults
end
# (Private API, public for test purposes)
@@ -210,6 +212,54 @@ module ChefConfig
raise ChefConfig::ConfigurationError, message
end
+ # Apply default configuration values for workstation-style tools.
+ #
+ # Global defaults should go in {ChefConfig::Config} instead, this is only
+ # for things like `knife` and `chef`.
+ #
+ # @api private
+ # @since 14.3
+ # @return [void]
+ def apply_defaults
+ # If we don't have a better guess use the username.
+ Config[:node_name] ||= Etc.getlogin
+ # If we don't have a key (path or inline) check user.pem and $node_name.pem.
+ unless Config.has_key?(:client_key) || Config.has_key?(:client_key_contents)
+ Config[:client_key] = find_default_key(["#{Config[:node_name]}.pem", "user.pem"])
+ end
+ # Similarly look for a validation key file, though this should be less
+ # common these days.
+ unless Config.has_key?(:validation_key) || Config.has_key?(:validation_key_contents)
+ Config[:validation_key] = find_default_key(["#{Config[:validation_client_name]}.pem", "validator.pem", "validation.pem"])
+ end
+ end
+
+ # Look for a default key file.
+ #
+ # This searches for any of a list of possible default keys, checking both
+ # the local `.chef/` folder and the home directory `~/.chef/`. Returns `nil`
+ # if no matching file is found.
+ #
+ # @api private
+ # @since 14.3
+ # @param key_names [Array<String>] A list of possible filenames to check for.
+ # The first one found will be returned.
+ # @return [String, nil]
+ def find_default_key(key_names)
+ key_names.each do |filename|
+ path = Pathname.new(filename)
+ # If we have a config location (like ./.chef/), look there first.
+ if config_location
+ local_path = path.expand_path(File.dirname(config_location))
+ return local_path.to_s if local_path.exist?
+ end
+ # Then check ~/.chef.
+ home_path = path.expand_path(home_chef_dir)
+ return home_path.to_s if home_path.exist?
+ end
+ nil
+ end
+
def highlight_config_error(file, line)
config_file_lines = []
IO.readlines(file).each_with_index { |l, i| config_file_lines << "#{(i + 1).to_s.rjust(3)}: #{l.chomp}" }
diff --git a/chef-config/spec/unit/config_spec.rb b/chef-config/spec/unit/config_spec.rb
index 71ea158840..7e32a1e742 100644
--- a/chef-config/spec/unit/config_spec.rb
+++ b/chef-config/spec/unit/config_spec.rb
@@ -1191,4 +1191,22 @@ RSpec.describe ChefConfig::Config do
end
+ describe "validation_client_name" do
+ context "with a normal server URL" do
+ before { ChefConfig::Config[:chef_server_url] = "https://chef.example/organizations/myorg" }
+
+ it "sets the validation client to myorg-validator" do
+ expect(ChefConfig::Config[:validation_client_name]).to eq "myorg-validator"
+ end
+ end
+
+ context "with an unusual server URL" do
+ before { ChefConfig::Config[:chef_server_url] = "https://chef.example/myorg" }
+
+ it "sets the validation client to chef-validator" do
+ expect(ChefConfig::Config[:validation_client_name]).to eq "chef-validator"
+ end
+ end
+ end
+
end
diff --git a/chef-config/spec/unit/workstation_config_loader_spec.rb b/chef-config/spec/unit/workstation_config_loader_spec.rb
index 6cebe1e09c..28fbdf63dd 100644
--- a/chef-config/spec/unit/workstation_config_loader_spec.rb
+++ b/chef-config/spec/unit/workstation_config_loader_spec.rb
@@ -271,6 +271,70 @@ RSpec.describe ChefConfig::WorkstationConfigLoader do
config_loader.load
expect(ChefConfig::Config.config_file).to eq(explicit_config_location)
end
+
+ it "loads a default value for node_name" do
+ allow(Etc).to receive(:getlogin).and_return("notauser")
+ config_loader.load
+ expect(ChefConfig::Config.node_name).to eq("notauser")
+ end
+
+ context "with a user.pem" do
+ before do
+ allow(Etc).to receive(:getlogin).and_return("notauser")
+ allow(FileTest).to receive(:exist?).and_call_original
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../notauser.pem", explicit_config_location)).and_return(false)
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../user.pem", explicit_config_location)).and_return(true)
+ end
+
+ it "loads a default value for client_key" do
+ config_loader.load
+ expect(ChefConfig::Config.client_key).to eq(File.expand_path("../user.pem", explicit_config_location))
+ end
+ end
+
+ context "with a notauser.pem" do
+ before do
+ allow(Etc).to receive(:getlogin).and_return("notauser")
+ allow(FileTest).to receive(:exist?).and_call_original
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../notauser.pem", explicit_config_location)).and_return(true)
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../user.pem", explicit_config_location)).and_return(false)
+ end
+
+ it "loads a default value for client_key" do
+ config_loader.load
+ expect(ChefConfig::Config.client_key).to eq(File.expand_path("../notauser.pem", explicit_config_location))
+ end
+ end
+
+ context "with a valclient.pem" do
+ before do
+ ChefConfig::Config.validation_client_name = "valclient"
+ allow(FileTest).to receive(:exist?).and_call_original
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../valclient.pem", explicit_config_location)).and_return(true)
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../validator.pem", explicit_config_location)).and_return(false)
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../validation.pem", explicit_config_location)).and_return(false)
+ end
+
+ it "loads a default value for validation_key" do
+ config_loader.load
+ expect(ChefConfig::Config.validation_key).to eq(File.expand_path("../valclient.pem", explicit_config_location))
+ end
+ end
+
+ context "with a validator.pem" do
+ before do
+ ChefConfig::Config.validation_client_name = "valclient"
+ allow(FileTest).to receive(:exist?).and_call_original
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../valclient.pem", explicit_config_location)).and_return(false)
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../validator.pem", explicit_config_location)).and_return(true)
+ allow(FileTest).to receive(:exist?).with(File.expand_path("../validation.pem", explicit_config_location)).and_return(false)
+ end
+
+ it "loads a default value for validation_key" do
+ config_loader.load
+ expect(ChefConfig::Config.validation_key).to eq(File.expand_path("../validator.pem", explicit_config_location))
+ end
+ end
end
context "and has a syntax error" do
diff --git a/lib/chef/application/knife.rb b/lib/chef/application/knife.rb
index d8818cd5aa..76dd5707fe 100644
--- a/lib/chef/application/knife.rb
+++ b/lib/chef/application/knife.rb
@@ -154,6 +154,7 @@ class Chef::Application::Knife < Chef::Application
# Run knife
def run
+ ChefConfig::PathHelper.per_tool_home_environment = "KNIFE_HOME"
Mixlib::Log::Formatter.show_time = false
validate_and_parse_options
quiet_traps
diff --git a/lib/chef/knife/config_get.rb b/lib/chef/knife/config_get.rb
new file mode 100644
index 0000000000..23085d177c
--- /dev/null
+++ b/lib/chef/knife/config_get.rb
@@ -0,0 +1,126 @@
+#
+# Author:: Joshua Timberman <opensource@housepub.org>
+# Copyright:: Copyright (c) 2012, Joshua Timberman
+# Copyright:: Copyright (c) 2018, Noah Kantrowitz
+# 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/knife"
+
+class Chef
+ class Knife
+ class ConfigGet < Knife
+ banner "knife config get [OPTION...] (options)\nDisplays the value of Chef::Config[OPTION] (or all config values)"
+
+ option :all,
+ short: "-a",
+ long: "--all",
+ description: "Include options that are not set in the configuration",
+ default: false
+
+ option :raw,
+ short: "-r",
+ long: "--raw",
+ description: "Display a each value with no formatting",
+ default: false
+
+ def run
+ if config[:format] == "summary" && !config[:raw]
+ # If using the default, human-readable output, also show which config files are being loaded.
+ # Some of this is a bit hacky since it duplicates
+ wcl = self.class.config_loader
+ if wcl.credentials_found
+ loading_from("credentials", ChefConfig::PathHelper.home(".chef", "credentials"))
+ end
+ if wcl.config_location
+ loading_from("configuration", wcl.config_location)
+ end
+ if Chef::Config[:config_d_dir]
+ wcl.find_dot_d(Chef::Config[:config_d_dir]).each do |path|
+ loading_from(".d/ configuration", wcl.config_location)
+ end
+ end
+ end
+
+ # Dump the whole config, including defaults is --all was given.
+ config_data = Chef::Config.save(config[:all])
+ # Two special cases, these are set during knife startup but we don't usually care about them.
+ unless config[:all]
+ config_data.delete(:color)
+ # Only keep these if true, false is much less important because it's the default.
+ config_data.delete(:local_mode) unless config_data[:local_mode]
+ config_data.delete(:enforce_path_sanity) unless config_data[:enforce_path_sanity]
+ end
+
+ # Extract the data to show.
+ output_data = {}
+ if @name_args.empty?
+ output_data = config_data
+ else
+ @name_args.each do |filter|
+ if filter =~ %r{^/(.*)/(i?)$}
+ # It's a regex.
+ filter_re = Regexp.new($1, $2 ? Regexp::IGNORECASE : 0)
+ config_data.each do |key, value|
+ output_data[key] = value if key.to_s =~ filter_re
+ end
+ else
+ # It's a dotted path string.
+ filter_parts = filter.split(/\./)
+ extract = lambda do |memo, filter_part|
+ memo.is_a?(Hash) ? memo[filter_part.to_sym] : nil
+ end
+ # Check against both config_data and all of the data, so that even
+ # in non-all mode, if you ask for a key that isn't in the non-all
+ # data, it will check against the broader set.
+ output_data[filter] = filter_parts.inject(config_data, &extract) || filter_parts.inject(Chef::Config.save(true), &extract)
+ end
+ end
+ end
+
+ # Fix up some values.
+ output_data.each do |key, value|
+ if value == STDOUT
+ output_data[key] = "STDOUT"
+ elsif value == STDERR
+ output_data[key] = "STDERR"
+ end
+ end
+
+ # Show the data.
+ if config[:raw]
+ output_data.each_value do |value|
+ ui.msg(value)
+ end
+ else
+ ui.output(output_data)
+ end
+ end
+
+ private
+
+ # Display a banner about loading from a config file.
+ #
+ # @api private
+ # @param type_of_file [String] Description of the file for the banner.
+ # @param path [String] Path of the file.
+ # @return [nil]
+ def loading_from(type_of_file, path)
+ path = Pathname.new(path).realpath
+ ui.msg(ui.color("Loading from #{type_of_file} file #{path}", :yellow))
+ end
+ end
+ end
+end
diff --git a/spec/integration/knife/config_get_spec.rb b/spec/integration/knife/config_get_spec.rb
new file mode 100644
index 0000000000..f34d096051
--- /dev/null
+++ b/spec/integration/knife/config_get_spec.rb
@@ -0,0 +1,183 @@
+#
+# Copyright 2018, Noah Kantrowitz
+#
+# 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 "support/shared/integration/integration_helper"
+require "support/shared/context/config"
+
+describe "knife config get", :workstation do
+ include IntegrationSupport
+ include KnifeSupport
+
+ include_context "default config options"
+ include_context "with a chef repo"
+
+ let(:cmd_args) { [] }
+
+ subject do
+ cmd = knife("config", "get", *cmd_args, instance_filter: lambda { |instance|
+ # Clear the stub set up in KnifeSupport.
+ allow(File).to receive(:file?).and_call_original
+ # Lies, damn lies, and config files. We need to allow normal config loading
+ # behavior to be able to test stuff.
+ instance.config.delete(:config_file)
+ $__KNIFE_INTEGRATION_FAILSAFE_CHECK << " ole"
+ })
+ cmd.stdout
+ end
+
+ around do |ex|
+ # Store and reset the value of some env vars.
+ old_chef_home = ENV["CHEF_HOME"]
+ old_knife_home = ENV["KNIFE_HOME"]
+ old_home = ENV["HOME"]
+ old_wd = Dir.pwd
+ ChefConfig::PathHelper.per_tool_home_environment = "KNIFE_HOME"
+ # Clear these out because they are cached permanently.
+ ChefConfig::PathHelper.class_exec { remove_class_variable(:@@home_dir) }
+ Chef::Knife::ConfigGet.reset_config_loader!
+ begin
+ ex.run
+ ensure
+ ENV["CHEF_HOME"] = old_chef_home
+ ENV["KNIFE_HOME"] = old_knife_home
+ ENV["HOME"] = old_home
+ Dir.chdir(old_wd)
+ ENV[ChefConfig.windows? ? "CD" : "PWD"] = Dir.pwd
+ ChefConfig::PathHelper.per_tool_home_environment = nil
+ end
+ end
+
+ before do
+ # Always run from the temp folder. This can't be in the `around` block above
+ # because it has to run after the before set in the "with a chef repo" shared context.
+ directory("repo")
+ Dir.chdir(path_to("repo"))
+ ENV[ChefConfig.windows? ? "CD" : "PWD"] = Dir.pwd
+ ENV["HOME"] = path_to(".")
+ end
+
+ context "with a global knife.rb" do
+ before { file(".chef/knife.rb", "node_name 'one'\n") }
+
+ it { is_expected.to match(%r{^Loading from configuration file .*/#{File.basename(path_to("."))}/.chef/knife.rb$}) }
+ it { is_expected.to match(/^node_name:\s+one$/) }
+ end
+
+ context "with a repo knife.rb" do
+ before { file("repo/.chef/knife.rb", "node_name 'two'\n") }
+
+ it { is_expected.to match(%r{^Loading from configuration file .*/#{File.basename(path_to("."))}/repo/.chef/knife.rb$}) }
+ it { is_expected.to match(/^node_name:\s+two$/) }
+ end
+
+ context "with both knife.rb" do
+ before do
+ file(".chef/knife.rb", "node_name 'one'\n")
+ file("repo/.chef/knife.rb", "node_name 'two'\n")
+ end
+
+ it { is_expected.to match(%r{^Loading from configuration file .*/#{File.basename(path_to("."))}/repo/.chef/knife.rb$}) }
+ it { is_expected.to match(/^node_name:\s+two$/) }
+ end
+
+ context "with a credentials file" do
+ before { file(".chef/credentials", "[default]\nclient_name = \"three\"\n") }
+
+ it { is_expected.to match(%r{^Loading from credentials file .*/#{File.basename(path_to("."))}/.chef/credentials$}) }
+ it { is_expected.to match(/^node_name:\s+three$/) }
+ end
+
+ context "with a credentials file and knife.rb" do
+ before do
+ file(".chef/knife.rb", "node_name 'one'\n")
+ file(".chef/credentials", "[default]\nclient_name = \"three\"\n")
+ end
+
+ it { is_expected.to match(%r{^Loading from configuration file .*/#{File.basename(path_to("."))}/.chef/knife.rb$}) }
+ it { is_expected.to match(%r{^Loading from credentials file .*/#{File.basename(path_to("."))}/.chef/credentials$}) }
+ it { is_expected.to match(/^node_name:\s+one$/) }
+ end
+
+ context "with a credentials file and CHEF_HOME" do
+ before do
+ file(".chef/credentials", "[default]\nclient_name = \"three\"\n")
+ file("foo/.chef/credentials", "[default]\nclient_name = \"four\"\n")
+ ENV["CHEF_HOME"] = path_to("foo")
+ end
+
+ it { is_expected.to match(%r{^Loading from credentials file .*/#{File.basename(path_to("."))}/foo/.chef/credentials$}) }
+ it { is_expected.to match(/^node_name:\s+four$/) }
+ end
+
+ context "with a credentials file and KNIFE_HOME" do
+ before do
+ file(".chef/credentials", "[default]\nclient_name = \"three\"\n")
+ file("bar/.chef/credentials", "[default]\nclient_name = \"four\"\n")
+ ENV["KNIFE_HOME"] = path_to("bar")
+ end
+
+ it { is_expected.to match(%r{^Loading from credentials file .*/#{File.basename(path_to("."))}/bar/.chef/credentials$}) }
+ it { is_expected.to match(/^node_name:\s+four$/) }
+ end
+
+ context "with single argument" do
+ let(:cmd_args) { %w{node_name} }
+ before { file(".chef/credentials", "[default]\nclient_name = \"three\"\n") }
+
+ it { is_expected.to match(/^node_name:\s+three\Z/) }
+ end
+
+ context "with two arguments" do
+ let(:cmd_args) { %w{node_name client_key} }
+ before { file(".chef/credentials", "[default]\nclient_name = \"three\"\nclient_key = \"three.pem\"") }
+
+ it { is_expected.to match(/^client_key:\s+\S*\/.chef\/three.pem\nnode_name:\s+three\Z/) }
+ end
+
+ context "with a dotted argument" do
+ let(:cmd_args) { %w{knife.ssh_user} }
+ before { file(".chef/credentials", "[default]\nclient_name = \"three\"\n[default.knife]\nssh_user = \"foo\"\n") }
+
+ it { is_expected.to match(/^knife.ssh_user:\s+foo\Z/) }
+ end
+
+ context "with regex argument" do
+ let(:cmd_args) { %w{/name/} }
+ before { file(".chef/credentials", "[default]\nclient_name = \"three\"\n") }
+
+ it { is_expected.to match(/^node_name:\s+three\Z/) }
+ end
+
+ context "with --all" do
+ let(:cmd_args) { %w{-a /key_contents/} }
+ before { file(".chef/credentials", "[default]\nclient_name = \"three\"\n") }
+
+ it { is_expected.to match(/^client_key_contents:\s+\nvalidation_key_contents:\s+\Z/) }
+ end
+
+ context "with --raw" do
+ let(:cmd_args) { %w{-r node_name} }
+ before { file(".chef/credentials", "[default]\nclient_name = \"three\"\n") }
+
+ it { is_expected.to eq("three\n") }
+ end
+
+ context "with --format=json" do
+ let(:cmd_args) { %w{--format=json node_name} }
+ before { file(".chef/credentials", "[default]\nclient_name = \"three\"\n") }
+
+ it { expect(JSON.parse(subject)).to eq({ "node_name" => "three" }) }
+ end
+end
diff --git a/spec/support/shared/integration/knife_support.rb b/spec/support/shared/integration/knife_support.rb
index 5058b7b278..87ce1bc0b0 100644
--- a/spec/support/shared/integration/knife_support.rb
+++ b/spec/support/shared/integration/knife_support.rb
@@ -24,7 +24,7 @@ require "chef/chef_fs/file_system_cache"
module KnifeSupport
DEBUG = ENV["DEBUG"]
- def knife(*args, input: nil)
+ def knife(*args, input: nil, instance_filter: nil)
# Allow knife('role from file roles/blah.json') rather than requiring the
# arguments to be split like knife('role', 'from', 'file', 'roles/blah.json')
# If any argument will have actual spaces in it, the long form is required.
@@ -88,9 +88,15 @@ module KnifeSupport
allow(File).to receive(:file?).and_call_original
allow(File).to receive(:file?).with(File.expand_path("~/.chef/credentials")).and_return(false)
+ # Set a canary that is modified by the default null_config.rb config file.
$__KNIFE_INTEGRATION_FAILSAFE_CHECK = "ole"
+
+ # Allow tweaking the knife instance before configuration.
+ instance_filter.call(instance) if instance_filter
+
instance.configure_chef
+ # The canary is incorrect, meaning the normal null_config.rb didn't run. Something is wrong.
unless $__KNIFE_INTEGRATION_FAILSAFE_CHECK == "ole ole"
raise Exception, "Potential misconfiguration of integration tests detected. Aborting test."
end