summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNoah Kantrowitz <noah@coderanger.net>2018-07-11 12:55:18 -0700
committerNoah Kantrowitz <noah@coderanger.net>2018-07-11 12:55:18 -0700
commitd730505bb0d53d14c7b005c756ed4993b95fdf94 (patch)
tree5c66f3c25001fdcd654580dff3e2b195dc2135a0
parentac977e9494d516fbc44d6012bb157254c28c0a05 (diff)
downloadchef-d730505bb0d53d14c7b005c756ed4993b95fdf94.tar.gz
New `knife config list-profiles` command to show available profiles.
Signed-off-by: Noah Kantrowitz <noah@coderanger.net>
-rw-r--r--chef-config/lib/chef-config/mixin/credentials.rb39
-rw-r--r--lib/chef/knife/config_list_profiles.rb121
-rw-r--r--spec/integration/knife/config_list_profiles_spec.rb136
3 files changed, 286 insertions, 10 deletions
diff --git a/chef-config/lib/chef-config/mixin/credentials.rb b/chef-config/lib/chef-config/mixin/credentials.rb
index fb557f6093..0a7ca356c8 100644
--- a/chef-config/lib/chef-config/mixin/credentials.rb
+++ b/chef-config/lib/chef-config/mixin/credentials.rb
@@ -48,6 +48,33 @@ module ChefConfig
end
end
+ # Compute the path to the credentials file.
+ #
+ # @since 14.4
+ # @return [String]
+ def credentials_file_path
+ PathHelper.home(".chef", "credentials").freeze
+ 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
+
# Load and process the active credentials.
#
# @see WorkstationConfigLoader#apply_credentials
@@ -55,10 +82,9 @@ module ChefConfig
# normally set via a command-line option.
# @return [void]
def load_credentials(profile = nil)
- credentials_file = PathHelper.home(".chef", "credentials").freeze
- return unless File.file?(credentials_file)
profile = credentials_profile(profile)
- config = Tomlrb.load_file(credentials_file)
+ 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.
@@ -66,13 +92,6 @@ module ChefConfig
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/config_list_profiles.rb b/lib/chef/knife/config_list_profiles.rb
new file mode 100644
index 0000000000..8780f5824a
--- /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 = table_data.first.length.times.map 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/spec/integration/knife/config_list_profiles_spec.rb b/spec/integration/knife/config_list_profiles_spec.rb
new file mode 100644
index 0000000000..ccc0045081
--- /dev/null
+++ b/spec/integration/knife/config_list_profiles_spec.rb
@@ -0,0 +1,136 @@
+#
+# 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 of some of the output examples are because
+ # 1) ~ mode heredocs get sad about the unequal leading whitespace.
+ # 2) 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.gsub(/#/, '') }
+ 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.gsub(/#/, '') }
+ 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.gsub(/#/, '') }
+ Profile Client Key Server #
+----------------------------------------------------------------#
+*default https://example.com/organizations/testorg#
+EOH
+ end
+end