diff options
-rw-r--r-- | RELEASE_NOTES.md | 22 | ||||
-rw-r--r-- | chef-config/lib/chef-config/config.rb | 20 | ||||
-rw-r--r-- | chef-config/lib/chef-config/mixin/credentials.rb | 2 | ||||
-rw-r--r-- | chef-config/lib/chef-config/mixin/dot_d.rb | 25 | ||||
-rw-r--r-- | chef-config/lib/chef-config/path_helper.rb | 18 | ||||
-rw-r--r-- | chef-config/lib/chef-config/workstation_config_loader.rb | 50 | ||||
-rw-r--r-- | chef-config/spec/unit/config_spec.rb | 18 | ||||
-rw-r--r-- | chef-config/spec/unit/workstation_config_loader_spec.rb | 64 | ||||
-rw-r--r-- | lib/chef/application/knife.rb | 1 | ||||
-rw-r--r-- | lib/chef/knife/config_get.rb | 126 | ||||
-rw-r--r-- | spec/integration/knife/config_get_spec.rb | 183 | ||||
-rw-r--r-- | spec/support/shared/integration/knife_support.rb | 8 |
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 |