summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTim Smith <tsmith@chef.io>2018-07-13 10:37:34 -0700
committerGitHub <noreply@github.com>2018-07-13 10:37:34 -0700
commitd99bc319e244e4d4fac57bc1853ea82f8b97c0e9 (patch)
tree77eac01f6baac6486308a30c8c3982f03309fb39
parent8baff8ae8f6dc0e88c1a467566b1ac595ceef267 (diff)
parent6fb425078b5986998c37450445f5b3ded61e02ac (diff)
downloadchef-d99bc319e244e4d4fac57bc1853ea82f8b97c0e9.tar.gz
Merge pull request #7455 from coderanger/config-commands
Add knife config get/use-profile commands
-rw-r--r--RELEASE_NOTES.md26
-rw-r--r--chef-config/lib/chef-config/mixin/credentials.rb91
-rw-r--r--lib/chef/knife.rb8
-rw-r--r--lib/chef/knife/config_get_profile.rb37
-rw-r--r--lib/chef/knife/config_list_profiles.rb121
-rw-r--r--lib/chef/knife/config_use_profile.rb50
-rw-r--r--lib/chef/knife/core/subcommand_loader.rb3
-rw-r--r--spec/integration/knife/config_get_profile_spec.rb112
-rw-r--r--spec/integration/knife/config_list_profiles_spec.rb188
-rw-r--r--spec/integration/knife/config_use_profile_spec.rb100
10 files changed, 708 insertions, 28 deletions
diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md
index dbc44a93f3..68a31e9882 100644
--- a/RELEASE_NOTES.md
+++ b/RELEASE_NOTES.md
@@ -1,5 +1,31 @@
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.
+## Knife configuration profile management commands
+
+Several new commands have been added under `knife config` to help manage multiple
+profiles in your `credentials` file.
+
+`knife config get-profile` will display the active profile.
+
+`knife config use-profile PROFILE` will set the workstation-level default
+profile. That default can still be overridden by the `--profile` command line
+option or the `$CHEF_PROFILE` environment variable.
+
+`knife config list-profiles` will display all your available profiles along with
+summary information on each.
+
+```bash
+$ knife config get-profile
+staging
+$ knife config use-profile prod
+Set default profile to prod
+$ knife config list-profiles
+ Profile Client Key Server
+-----------------------------------------------------------------------------
+ staging myuser ~/.chef/user.pem https://example.com/organizations/staging
+*prod myuser ~/.chef/user.pem https://example.com/organizations/prod
+```
+
# Chef Client Release Notes 14.3:
## New Preview Resources Concept
diff --git a/chef-config/lib/chef-config/mixin/credentials.rb b/chef-config/lib/chef-config/mixin/credentials.rb
index 5a73a49add..0a7ca356c8 100644
--- a/chef-config/lib/chef-config/mixin/credentials.rb
+++ b/chef-config/lib/chef-config/mixin/credentials.rb
@@ -20,37 +20,78 @@ require "chef-config/path_helper"
module ChefConfig
module Mixin
+ # Helper methods for working with credentials files.
+ #
+ # @since 13.7
+ # @api internal
module Credentials
-
- def load_credentials(profile = nil)
- credentials_file = PathHelper.home(".chef", "credentials").freeze
+ # Compute the active credentials profile name.
+ #
+ # The lookup order is argument (from --profile), environment variable
+ # ($CHEF_PROFILE), context file (~/.chef/context), and then "default" as
+ # a fallback.
+ #
+ # @since 14.4
+ # @param profile [String, nil] Optional override for the active profile,
+ # normally set via a command-line option.
+ # @return [String]
+ def credentials_profile(profile = nil)
context_file = PathHelper.home(".chef", "context").freeze
+ if !profile.nil?
+ profile
+ elsif ENV.include?("CHEF_PROFILE")
+ ENV["CHEF_PROFILE"]
+ elsif File.file?(context_file)
+ File.read(context_file).strip
+ else
+ "default"
+ end
+ end
- return unless File.file?(credentials_file)
-
- context = File.read(context_file).strip if File.file?(context_file)
-
- environment = ENV.fetch("CHEF_PROFILE", nil)
+ # Compute the path to the credentials file.
+ #
+ # @since 14.4
+ # @return [String]
+ def credentials_file_path
+ PathHelper.home(".chef", "credentials").freeze
+ end
- profile = if !profile.nil?
- profile
- elsif !environment.nil?
- environment
- elsif !context.nil?
- context
- else
- "default"
- end
+ # Load and parse the credentials file.
+ #
+ # Returns `nil` if the credentials file is unavailable.
+ #
+ # @since 14.4
+ # @return [String, nil]
+ def parse_credentials_file
+ credentials_file = credentials_file_path
+ return nil unless File.file?(credentials_file)
+ begin
+ Tomlrb.load_file(credentials_file)
+ rescue => e
+ # TOML's error messages are mostly rubbish, so we'll just give a generic one
+ message = "Unable to parse Credentials file: #{credentials_file}\n"
+ message << e.message
+ raise ChefConfig::ConfigurationError, message
+ end
+ end
- config = Tomlrb.load_file(credentials_file)
+ # Load and process the active credentials.
+ #
+ # @see WorkstationConfigLoader#apply_credentials
+ # @param profile [String, nil] Optional override for the active profile,
+ # normally set via a command-line option.
+ # @return [void]
+ def load_credentials(profile = nil)
+ profile = credentials_profile(profile)
+ config = parse_credentials_file
+ return if config.nil? # No credentials, nothing to do here.
+ if config[profile].nil?
+ # Unknown profile name. For "default" just silently ignore, otherwise
+ # raise an error.
+ return if profile == "default"
+ raise ChefConfig::ConfigurationError, "Profile #{profile} doesn't exist. Please add it to #{credentials_file}."
+ end
apply_credentials(config[profile], profile)
- rescue ChefConfig::ConfigurationError
- raise
- rescue => e
- # TOML's error messages are mostly rubbish, so we'll just give a generic one
- message = "Unable to parse Credentials file: #{credentials_file}\n"
- message << e.message
- raise ChefConfig::ConfigurationError, message
end
end
end
diff --git a/lib/chef/knife.rb b/lib/chef/knife.rb
index e28ca81f71..6e525bdf3d 100644
--- a/lib/chef/knife.rb
+++ b/lib/chef/knife.rb
@@ -304,8 +304,12 @@ class Chef
# knife node run_list add requires that we have extra logic to handle
# the case that command name words could be joined by an underscore :/
- command_name_words = command_name_words.join("_")
- @name_args.reject! { |name_arg| command_name_words == name_arg }
+ command_name_joined = command_name_words.join("_")
+ @name_args.reject! { |name_arg| command_name_joined == name_arg }
+
+ # Similar handling for hyphens.
+ command_name_joined = command_name_words.join("-")
+ @name_args.reject! { |name_arg| command_name_joined == name_arg }
if config[:help]
msg opt_parser
diff --git a/lib/chef/knife/config_get_profile.rb b/lib/chef/knife/config_get_profile.rb
new file mode 100644
index 0000000000..309b7f81e8
--- /dev/null
+++ b/lib/chef/knife/config_get_profile.rb
@@ -0,0 +1,37 @@
+#
+# 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 ConfigGetProfile < Knife
+ banner "knife config get-profile"
+
+ # Disable normal config loading since this shouldn't fail if the profile
+ # doesn't exist of the config is otherwise corrupted.
+ def configure_chef
+ apply_computed_config
+ end
+
+ def run
+ ui.msg(self.class.config_loader.credentials_profile(config[:profile]))
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/config_list_profiles.rb b/lib/chef/knife/config_list_profiles.rb
new file mode 100644
index 0000000000..16b0c5df27
--- /dev/null
+++ b/lib/chef/knife/config_list_profiles.rb
@@ -0,0 +1,121 @@
+#
+# 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"
+require "chef/workstation_config_loader"
+
+class Chef
+ class Knife
+ class ConfigListProfiles < Knife
+ banner "knife config list-profiles"
+
+ option :ignore_knife_rb,
+ short: "-i",
+ long: "--ignore-knife-rb",
+ description: "Ignore the current knife.rb configuration",
+ default: false
+
+ def run
+ credentials_data = self.class.config_loader.parse_credentials_file
+ if credentials_data.nil? || credentials_data.empty?
+ # Should this just show the ambient knife.rb config as "default" instead?
+ ui.fatal("No profiles found, #{self.class.config_loader.credentials_file_path} does not exist or is empty")
+ exit 1
+ end
+
+ current_profile = self.class.config_loader.credentials_profile(config[:profile])
+ profiles = credentials_data.keys.map do |profile|
+ if config[:ignore_knife_rb]
+ # Don't do any fancy loading nonsense, just the raw data.
+ profile_data = credentials_data[profile]
+ {
+ profile: profile,
+ active: profile == current_profile,
+ client_name: profile_data["client_name"] || profile_data["node_name"],
+ client_key: profile_data["client_key"],
+ server_url: profile_data["chef_server_url"],
+ }
+ else
+ # Fancy loading nonsense so we get what the actual config would be.
+ # Note that this modifies the global config, after this, all bets are
+ # off as to whats in the config.
+ Chef::Config.reset
+ wcl = Chef::WorkstationConfigLoader.new(nil, Chef::Log, profile: profile)
+ wcl.load
+ {
+ profile: profile,
+ active: profile == current_profile,
+ client_name: Chef::Config[:node_name],
+ client_key: Chef::Config[:client_key],
+ server_url: Chef::Config[:chef_server_url],
+ }
+ end
+ end
+
+ # Try to reset the config.
+ unless config[:ignore_knife_rb]
+ Chef::Config.reset
+ Chef::WorkstationConfigLoader.new(config[:config_file], Chef::Log, profile: config[:profile]).load
+ apply_computed_config
+ end
+
+ if ui.interchange?
+ # Machine-readable output.
+ ui.output(profiles)
+ else
+ # Table output.
+ ui.output(render_table(profiles))
+ end
+ end
+
+ private
+
+ def render_table(profiles, padding: 2)
+ # Replace the home dir in the client key path with ~.
+ profiles.each do |profile|
+ profile[:client_key] = profile[:client_key].to_s.gsub(/^#{Regexp.escape(Dir.home)}/, "~") if profile[:client_key]
+ end
+ # Render the data to a 2D array that will be used for the table.
+ table_data = [["", "Profile", "Client", "Key", "Server"]] + profiles.map do |profile|
+ [profile[:active] ? "*" : ""] + profile.values_at(:profile, :client_name, :client_key, :server_url).map(&:to_s)
+ end
+ # Compute column widths.
+ column_widths = Array.new(table_data.first.length) do |i|
+ table_data.map { |row| row[i].length + padding }.max
+ end
+ # Special case, the first col gets no padding (because indicator) and last
+ # get no padding because last.
+ column_widths[0] -= padding
+ column_widths[-1] -= padding
+ # Build the format string for each row.
+ format_string = column_widths.map { |w| "%-#{w}.#{w}s" }.join("")
+ format_string << "\n"
+ # Print the header row and a separator.
+ table = ui.color(format_string % table_data.first, :green)
+ table << "-" * column_widths.sum
+ table << "\n"
+ # Print the rest of the table.
+ table_data.drop(1).each do |row|
+ table << format_string % row
+ end
+ # Trim the last newline because ui.output adds one.
+ table.chomp!
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/config_use_profile.rb b/lib/chef/knife/config_use_profile.rb
new file mode 100644
index 0000000000..515c4a5336
--- /dev/null
+++ b/lib/chef/knife/config_use_profile.rb
@@ -0,0 +1,50 @@
+#
+# 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 "fileutils"
+
+require "chef/knife"
+
+class Chef
+ class Knife
+ class ConfigUseProfile < Knife
+ banner "knife config use-profile PROFILE"
+
+ # Disable normal config loading since this shouldn't fail if the profile
+ # doesn't exist of the config is otherwise corrupted.
+ def configure_chef
+ apply_computed_config
+ end
+
+ def run
+ context_file = ChefConfig::PathHelper.home(".chef", "context").freeze
+ profile = @name_args[0]&.strip
+ if profile && !profile.empty?
+ # Ensure the .chef/ folder exists.
+ FileUtils.mkdir_p(File.dirname(context_file))
+ IO.write(context_file, "#{profile}\n")
+ ui.msg("Set default profile to #{profile}")
+ else
+ show_usage
+ ui.fatal("You must specify a profile")
+ exit 1
+ end
+ end
+
+ end
+ end
+end
diff --git a/lib/chef/knife/core/subcommand_loader.rb b/lib/chef/knife/core/subcommand_loader.rb
index 026967d6ec..fb3723de50 100644
--- a/lib/chef/knife/core/subcommand_loader.rb
+++ b/lib/chef/knife/core/subcommand_loader.rb
@@ -139,9 +139,10 @@ class Chef
# hash composed of the given words joined by the separator.
#
def find_longest_key(hash, words, sep = "_")
+ words = words.dup
match = nil
until match || words.empty?
- candidate = words.join(sep)
+ candidate = words.join(sep).tr("-", "_")
if hash.key?(candidate)
match = candidate
else
diff --git a/spec/integration/knife/config_get_profile_spec.rb b/spec/integration/knife/config_get_profile_spec.rb
new file mode 100644
index 0000000000..e97b24b869
--- /dev/null
+++ b/spec/integration/knife/config_get_profile_spec.rb
@@ -0,0 +1,112 @@
+#
+# 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-profile", :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-profile", *cmd_args, instance_filter: lambda { |instance|
+ # Fake the failsafe check because this command doesn't actually process knife.rb.
+ $__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::ConfigGetProfile.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 no configuration" do
+ it { is_expected.to eq "default\n" }
+ end
+
+ context "with --profile" do
+ let(:cmd_args) { %w{--profile production} }
+ it { is_expected.to eq "production\n" }
+ end
+
+ context "with an environment variable" do
+ around do |ex|
+ old_chef_profile = ENV["CHEF_PROFILE"]
+ begin
+ ENV["CHEF_PROFILE"] = "staging"
+ ex.run
+ ensure
+ ENV["CHEF_PROFILE"] = old_chef_profile
+ end
+ end
+
+ it { is_expected.to eq "staging\n" }
+ end
+
+ context "with a context file" do
+ before { file(".chef/context", "development\n") }
+ it { is_expected.to eq "development\n" }
+ end
+
+ context "with a context file under $CHEF_HOME" do
+ before do
+ file("chefhome/.chef/context", "other\n")
+ ENV["CHEF_HOME"] = path_to("chefhome")
+ end
+
+ it { is_expected.to eq "other\n" }
+ end
+
+ context "with a context file under $KNIFE_HOME" do
+ before do
+ file("knifehome/.chef/context", "other\n")
+ ENV["KNIFE_HOME"] = path_to("knifehome")
+ end
+
+ it { is_expected.to eq "other\n" }
+ end
+end
diff --git a/spec/integration/knife/config_list_profiles_spec.rb b/spec/integration/knife/config_list_profiles_spec.rb
new file mode 100644
index 0000000000..32846f9999
--- /dev/null
+++ b/spec/integration/knife/config_list_profiles_spec.rb
@@ -0,0 +1,188 @@
+#
+# 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 list-profiles", :workstation do
+ include IntegrationSupport
+ include KnifeSupport
+
+ include_context "default config options"
+ include_context "with a chef repo"
+
+ let(:cmd_args) { [] }
+ let(:knife_list_profiles) do
+ knife("config", "list-profiles", *cmd_args, instance_filter: proc {
+ # Clear the stub set up in KnifeSupport.
+ allow(File).to receive(:file?).and_call_original
+ })
+ end
+ subject { knife_list_profiles.stdout }
+
+ around do |ex|
+ # Store and reset the value of some env vars.
+ old_home = ENV["HOME"]
+ old_wd = Dir.pwd
+ # Clear these out because they are cached permanently.
+ ChefConfig::PathHelper.class_exec { remove_class_variable(:@@home_dir) }
+ Chef::Knife::ConfigListProfiles.reset_config_loader!
+ begin
+ ex.run
+ ensure
+ ENV["HOME"] = old_home
+ Dir.chdir(old_wd)
+ ENV[ChefConfig.windows? ? "CD" : "PWD"] = Dir.pwd
+ 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
+
+ # NOTE: The funky formatting with # at the end of the line of some of the
+ # output examples are because of how the format strings are built, there is
+ # substantial trailing whitespace in most cases which many editors "helpfully" remove.
+
+ context "with no credentials file" do
+ subject { knife_list_profiles.stderr }
+ it { is_expected.to eq "FATAL: No profiles found, #{path_to(".chef/credentials")} does not exist or is empty\n" }
+ end
+
+ context "with an empty credentials file" do
+ before { file(".chef/credentials", "") }
+ subject { knife_list_profiles.stderr }
+ it { is_expected.to eq "FATAL: No profiles found, #{path_to(".chef/credentials")} does not exist or is empty\n" }
+ end
+
+ context "with a simple default profile" do
+ before { file(".chef/credentials", <<~EOH) }
+ [default]
+ client_name = "testuser"
+ client_key = "testkey.pem"
+ chef_server_url = "https://example.com/organizations/testorg"
+ EOH
+ it { is_expected.to eq <<~EOH.delete("#") }
+ Profile Client Key Server #
+ ----------------------------------------------------------------------------------#
+ *default testuser ~/.chef/testkey.pem https://example.com/organizations/testorg#
+ EOH
+ end
+
+ context "with multiple profiles" do
+ before { file(".chef/credentials", <<~EOH) }
+ [default]
+ client_name = "testuser"
+ client_key = "testkey.pem"
+ chef_server_url = "https://example.com/organizations/testorg"
+
+ [prod]
+ client_name = "testuser"
+ client_key = "testkey.pem"
+ chef_server_url = "https://example.com/organizations/prod"
+
+ [qa]
+ client_name = "qauser"
+ client_key = "~/src/qauser.pem"
+ chef_server_url = "https://example.com/organizations/testorg"
+ EOH
+ it { is_expected.to eq <<~EOH.delete("#") }
+ Profile Client Key Server #
+ ----------------------------------------------------------------------------------#
+ *default testuser ~/.chef/testkey.pem https://example.com/organizations/testorg#
+ prod testuser ~/.chef/testkey.pem https://example.com/organizations/prod #
+ qa qauser ~/src/qauser.pem https://example.com/organizations/testorg#
+ EOH
+ end
+
+ context "with a non-default active profile" do
+ let(:cmd_args) { %w{--profile prod} }
+ before { file(".chef/credentials", <<~EOH) }
+ [default]
+ client_name = "testuser"
+ client_key = "testkey.pem"
+ chef_server_url = "https://example.com/organizations/testorg"
+
+ [prod]
+ client_name = "testuser"
+ client_key = "testkey.pem"
+ chef_server_url = "https://example.com/organizations/prod"
+
+ [qa]
+ client_name = "qauser"
+ client_key = "~/src/qauser.pem"
+ chef_server_url = "https://example.com/organizations/testorg"
+ EOH
+ it { is_expected.to eq <<~EOH.delete("#") }
+ Profile Client Key Server #
+ ----------------------------------------------------------------------------------#
+ default testuser ~/.chef/testkey.pem https://example.com/organizations/testorg#
+ *prod testuser ~/.chef/testkey.pem https://example.com/organizations/prod #
+ qa qauser ~/src/qauser.pem https://example.com/organizations/testorg#
+ EOH
+ end
+
+ context "with a minimal profile" do
+ before { file(".chef/credentials", <<~EOH) }
+ [default]
+ chef_server_url = "https://example.com/organizations/testorg"
+ EOH
+ it { is_expected.to match %r{^*default .*? https://example.com/organizations/testorg$} }
+ end
+
+ context "with -i" do
+ let(:cmd_args) { %w{-i} }
+ before { file(".chef/credentials", <<~EOH) }
+ [default]
+ chef_server_url = "https://example.com/organizations/testorg"
+ EOH
+ it { is_expected.to eq <<~EOH.delete("#") }
+ Profile Client Key Server #
+ ----------------------------------------------------------------#
+ *default https://example.com/organizations/testorg#
+ EOH
+ end
+
+ context "with --format=json" do
+ let(:cmd_args) { %w{--format=json node_name} }
+ before { file(".chef/credentials", <<~EOH) }
+ [default]
+ client_name = "testuser"
+ client_key = "testkey.pem"
+ chef_server_url = "https://example.com/organizations/testorg"
+
+ [prod]
+ client_name = "testuser"
+ client_key = "testkey.pem"
+ chef_server_url = "https://example.com/organizations/prod"
+
+ [qa]
+ client_name = "qauser"
+ client_key = "~/src/qauser.pem"
+ chef_server_url = "https://example.com/organizations/testorg"
+ EOH
+ it {
+ expect(JSON.parse(subject)).to eq [
+ { "profile" => "default", "active" => true, "client_name" => "testuser", "client_key" => path_to(".chef/testkey.pem"), "server_url" => "https://example.com/organizations/testorg" },
+ { "profile" => "prod", "active" => false, "client_name" => "testuser", "client_key" => path_to(".chef/testkey.pem"), "server_url" => "https://example.com/organizations/prod" },
+ { "profile" => "qa", "active" => false, "client_name" => "qauser", "client_key" => path_to("src/qauser.pem"), "server_url" => "https://example.com/organizations/testorg" },
+ ] }
+ end
+end
diff --git a/spec/integration/knife/config_use_profile_spec.rb b/spec/integration/knife/config_use_profile_spec.rb
new file mode 100644
index 0000000000..a021dbbe6f
--- /dev/null
+++ b/spec/integration/knife/config_use_profile_spec.rb
@@ -0,0 +1,100 @@
+#
+# 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 use-profile", :workstation do
+ include IntegrationSupport
+ include KnifeSupport
+
+ include_context "default config options"
+ include_context "with a chef repo"
+
+ let(:cmd_args) { [] }
+
+ let(:knife_use_profile) do
+ knife("config", "use-profile", *cmd_args, instance_filter: lambda { |instance|
+ # Fake the failsafe check because this command doesn't actually process knife.rb.
+ $__KNIFE_INTEGRATION_FAILSAFE_CHECK << " ole"
+ })
+ end
+
+ subject { knife_use_profile.stdout }
+
+ 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::ConfigUseProfile.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 no argument" do
+ subject { knife_use_profile.stderr }
+ it { is_expected.to eq "FATAL: You must specify a profile\n" }
+ end
+
+ context "with an argument" do
+ let(:cmd_args) { %w{production} }
+ it do
+ is_expected.to eq "Set default profile to production\n"
+ expect(File.read(path_to(".chef/context"))).to eq "production\n"
+ end
+ end
+
+ context "with $CHEF_HOME" do
+ let(:cmd_args) { %w{staging} }
+ before { ENV["CHEF_HOME"] = path_to("chefhome"); file("chefhome/tmp", "") }
+ it do
+ is_expected.to eq "Set default profile to staging\n"
+ expect(File.read(path_to("chefhome/.chef/context"))).to eq "staging\n"
+ expect(File.exist?(path_to(".chef/context"))).to be_falsey
+ end
+ end
+
+ context "with $KNIFE_HOME" do
+ let(:cmd_args) { %w{development} }
+ before { ENV["KNIFE_HOME"] = path_to("knifehome"); file("knifehome/tmp", "") }
+ it do
+ is_expected.to eq "Set default profile to development\n"
+ expect(File.read(path_to("knifehome/.chef/context"))).to eq "development\n"
+ expect(File.exist?(path_to(".chef/context"))).to be_falsey
+ end
+ end
+end