diff --git a/lib/inspec/plugin/v2/filter.rb b/lib/inspec/plugin/v2/filter.rb index 8cbf11767..9949901de 100644 --- a/lib/inspec/plugin/v2/filter.rb +++ b/lib/inspec/plugin/v2/filter.rb @@ -60,4 +60,35 @@ module Inspec::Plugin::V2 end end end + + # To be a valid plugin name, the plugin must beign with either + # inspec- or train-, AND ALSO not be on the exclusion list. + # We maintain this exclusion list to avoid confusing users. + # For example, we want to have a real gem named inspec-test-fixture, + # but we don't want the users to see that. + module FilterPredicates + def train_plugin_name?(name) + valid_plugin_name?(name, :train) + end + + def inspec_plugin_name?(name) + valid_plugin_name?(name, :inspec) + end + + def valid_plugin_name?(name, kind = :either) + # Must have a permitted prefix. + return false unless case kind + when :inspec + name.to_s.start_with?("inspec-") + when :train + name.to_s.start_with?("train-") + when :either + name.to_s.match(/^(inspec|train)-/) + else false + end # rubocop: disable Layout/EndAlignment + + # And must not be on the exclusion list. + ! Inspec::Plugin::V2::PluginFilter.exclude?(name) + end + end end diff --git a/lib/inspec/plugin/v2/installer.rb b/lib/inspec/plugin/v2/installer.rb index fb820d879..8228593f6 100644 --- a/lib/inspec/plugin/v2/installer.rb +++ b/lib/inspec/plugin/v2/installer.rb @@ -60,14 +60,15 @@ module Inspec::Plugin::V2 # TODO: - check plugins.json for validity before trying anything that needs to modify it. validate_installation_opts(plugin_name, opts) - # TODO: change all of these to return installed spec/gem/thingy # TODO: return installed thingy if opts[:path] install_from_path(plugin_name, opts) elsif opts[:gem_file] - install_from_gem_file(plugin_name, opts) + gem_version = install_from_gem_file(plugin_name, opts) + opts[:version] = gem_version.to_s else - install_from_remote_gems(plugin_name, opts) + gem_version = install_from_remote_gems(plugin_name, opts) + opts[:version] = gem_version.to_s end update_plugin_config_file(plugin_name, opts.merge({ action: :install })) @@ -88,9 +89,9 @@ module Inspec::Plugin::V2 # TODO: Handle installing from a local file # TODO: Perform dependency checks to make sure the new solution is valid - install_from_remote_gems(plugin_name, opts) + gem_version = install_from_remote_gems(plugin_name, opts) - update_plugin_config_file(plugin_name, opts.merge({ action: :update })) + update_plugin_config_file(plugin_name, opts.merge({ action: :update, version: gem_version.to_s })) end # Uninstalls (removes) a plugin. Refers to plugin.json to determine if it @@ -335,13 +336,15 @@ module Inspec::Plugin::V2 # not obliged to during packaging.) # So, after each install, run a scan for all gem(specs) we manage, and copy in their gemspec file # into the exploded gem source area if absent. - loader.list_managed_gems.each do |spec| path_inside_source = File.join(spec.gem_dir, "#{spec.name}.gemspec") unless File.exist?(path_inside_source) File.write(path_inside_source, spec.to_ruby) end end + + # Locate the GemVersion for the new dependency and return it + solution.detect { |g| g.name == new_plugin_dependency.name }.version end #===================================================================# @@ -365,7 +368,7 @@ module Inspec::Plugin::V2 # excluding any that are path-or-core-based, excluding the gem to be removed plugin_deps_we_still_must_satisfy = registry.plugin_statuses plugin_deps_we_still_must_satisfy = plugin_deps_we_still_must_satisfy.select do |status| - status.installation_type == :gem && status.name != plugin_name_to_be_removed.to_sym + status.installation_type == :user_gem && status.name != plugin_name_to_be_removed.to_sym end plugin_deps_we_still_must_satisfy = plugin_deps_we_still_must_satisfy.map do |status| constraint = status.version || "> 0" diff --git a/lib/inspec/plugin/v2/loader.rb b/lib/inspec/plugin/v2/loader.rb index 34813b2dc..91c6bfcfe 100644 --- a/lib/inspec/plugin/v2/loader.rb +++ b/lib/inspec/plugin/v2/loader.rb @@ -1,5 +1,6 @@ require "inspec/log" require "inspec/plugin/v2/config_file" +require "inspec/plugin/v2/filter" # Add the current directory of the process to the load path $LOAD_PATH.unshift(".") unless $LOAD_PATH.include?(".") @@ -11,9 +12,16 @@ module Inspec::Plugin::V2 class Loader attr_reader :conf_file, :registry, :options + # For {inspec|train}_plugin_name? + include Inspec::Plugin::V2::FilterPredicates + extend Inspec::Plugin::V2::FilterPredicates + def initialize(options = {}) @options = options @registry = Inspec::Plugin::V2::Registry.instance + + # User plugins are those installed by the user via `inspec plugin install` + # and are installed under ~/.inspec/gems unless options[:omit_user_plugins] @conf_file = Inspec::Plugin::V2::ConfigFile.new read_conf_file_into_registry @@ -27,9 +35,8 @@ module Inspec::Plugin::V2 # and may be safely loaded detect_core_plugins unless options[:omit_core_plugins] - # Train plugins aren't InSpec plugins (they don't use our API) - # but InSpec CLI manages them. So, we have to wrap them a bit. - accommodate_train_plugins + # Identify plugins that inspec is co-installed with + detect_system_plugins unless options[:omit_sys_plugins] end def load_all @@ -46,7 +53,7 @@ module Inspec::Plugin::V2 begin # We could use require, but under testing, we need to repeatedly reload the same # plugin. However, gems only work with require (rubygems dooes not overload `load`) - if plugin_details.installation_type == :gem + if plugin_details.installation_type == :user_gem activate_managed_gems_for_plugin(plugin_name) require plugin_details.entry_point else @@ -130,10 +137,11 @@ module Inspec::Plugin::V2 end # Lists all plugin gems found in the plugin_gem_path. - # This is simply all gems that begin with train- or inspec-. + # This is simply all gems that begin with train- or inspec- + # and are not on the exclusion list. # @return [Array[Gem::Specification]] Specs of all gems found. def self.list_installed_plugin_gems - list_managed_gems.select { |spec| spec.name.match(/^(inspec|train)-/) } + list_managed_gems.select { |spec| valid_plugin_name?(spec.name) } end def list_installed_plugin_gems @@ -234,34 +242,70 @@ module Inspec::Plugin::V2 end end - def accommodate_train_plugins - registry.plugin_names.map(&:to_s).grep(/^train-/).each do |train_plugin_name| - status = registry[train_plugin_name.to_sym] - status.api_generation = :'train-1' - - if status.installation_type == :gem - # Activate the gem. This allows train to 'require' the gem later. - activate_managed_gems_for_plugin(train_plugin_name) - end - end - end - def read_conf_file_into_registry conf_file.each do |plugin_entry| status = Inspec::Plugin::V2::Status.new status.name = plugin_entry[:name] status.loaded = false - status.installation_type = (plugin_entry[:installation_type] || :gem) + status.installation_type = (plugin_entry[:installation_type] || :user_gem) case status.installation_type - when :gem + when :user_gem status.entry_point = status.name.to_s status.version = plugin_entry[:version] when :path status.entry_point = plugin_entry[:installation_path] end + # Train plugins are not true InSpec plugins; we need to decorate them a + # bit more to integrate them. + fixup_train_plugin_status(status) if train_plugin_name?(plugin_entry[:name]) + registry[status.name] = status end end + + def fixup_train_plugin_status(status) + status.api_generation = :'train-1' + if status.installation_type == :user_gem + # Activate the gem. This allows train to 'require' the gem later. + activate_managed_gems_for_plugin(status.entry_point) + end + end + + def detect_system_plugins + # Find the gemspec for inspec + inspec_gemspec = Gem::Specification.find_by_name("inspec", "=#{Inspec::VERSION}") + + # Make a RequestSet that represents the dependencies of inspec + inspec_deps_request_set = Gem::RequestSet.new(*inspec_gemspec.dependencies) + inspec_deps_request_set.remote = false + + # Resolve the request against the installed gem universe + gem_resolver = Gem::Resolver::CurrentSet.new + runtime_solution = inspec_deps_request_set.resolve(gem_resolver) + + inspec_gemspec.dependencies.each do |inspec_dep| + next unless inspec_plugin_name?(inspec_dep.name) || train_plugin_name?(inspec_dep.name) + + plugin_spec = runtime_solution.detect { |s| s.name == inspec_dep.name }.spec + + status = Inspec::Plugin::V2::Status.new + status.name = inspec_dep.name + status.entry_point = inspec_dep.name # gem-based, just 'require' the name + status.version = plugin_spec.version.to_s + status.loaded = false + status.installation_type = :system_gem + + if train_plugin_name?(status[:name]) + # Train plugins are not true InSpec plugins; we need to decorate them a + # bit more to integrate them. + fixup_train_plugin_status(status) + else + status.api_generation = 2 + end + + registry[status.name.to_sym] = status + end + end end end diff --git a/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb b/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb index c06bc1e9c..b49b625f9 100644 --- a/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb +++ b/lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb @@ -6,6 +6,14 @@ require "inspec/dist" module InspecPlugins module PluginManager class CliCommand < Inspec.plugin(2, :cli_command) + INSTALL_TYPE_LABELS = { + bundle: "core", # Calling this core, too - not much of a distinction + core: "core", + path: "path", + user_gem: "gem (user)", + system_gem: "gem (system)", + }.freeze + include Inspec::Dist subcommand_desc "plugin SUBCOMMAND", "Manage #{PRODUCT_NAME} and Train plugins" @@ -15,22 +23,36 @@ module InspecPlugins #==================================================================# desc "list [options]", "Lists user-installed #{PRODUCT_NAME} plugins." - option :all, desc: "Include plugins shipped with #{PRODUCT_NAME} as well.", type: :boolean, aliases: [:a] + option :all, desc: "List all types of plugins (default)", type: :boolean, default: true, aliases: [:a] + option :user, desc: "List user plugins, from ~/.inspec/gems", banner: "", type: :boolean, default: false, aliases: [:u] + option :system, desc: "List system plugins, those InSpec depends on", banner: "", type: :boolean, default: false, aliases: [:s] + option :core, desc: "List core plugins, those InSpec ships with", banner: "", type: :boolean, default: false, aliases: [:c] + def list plugin_statuses = Inspec::Plugin::V2::Registry.instance.plugin_statuses - plugin_statuses.reject! { |s| %i{core bundle}.include?(s.installation_type) } unless options[:all] - - puts - ui.bold(format(" %-30s%-10s%-8s%-6s", "Plugin Name", "Version", "Via", "ApiVer")) - ui.line - plugin_statuses.sort_by(&:name).each do |status| - ui.plain(format(" %-30s%-10s%-8s%-6s", status.name, - make_pretty_version(status), - status.installation_type, - status.api_generation.to_s)) + options[:all] = false if options[:core] || options[:user] || options[:system] + plugin_statuses.select! do |status| + type = status.installation_type + options[:all] || + (options[:core] && %i{core bundle}.include?(type)) || + (options[:user] && %i{user_gem path}.include?(type)) || + (options[:system] && :system_gem == type) end - ui.line - ui.plain(" #{plugin_statuses.count} plugin(s) total") + + unless plugin_statuses.empty? + ui.table do |t| + t.header = ["Plugin Name", "Version", "Via", "ApiVer"] + plugin_statuses.sort_by { |s| s.name.to_s }.each do |status| + t << [ + status.name, + make_pretty_version(status), + make_pretty_install_type(status), + status.api_generation, + ] + end + end + end + ui.plain_line(" #{plugin_statuses.count} plugin(s) total") puts end @@ -60,15 +82,15 @@ module InspecPlugins end puts - ui.bold(format(" %-30s%-50s", "Plugin Name", "Versions Available")) + ui.bold(format(" %-30s%-50s\n", "Plugin Name", "Versions Available")) ui.line search_results.keys.sort.each do |plugin_name| versions = options[:all] ? search_results[plugin_name] : [search_results[plugin_name].first] versions = "(" + versions.join(", ") + ")" - ui.plain(format(" %-30s%-50s", plugin_name, versions)) + ui.plain_line(format(" %-30s%-50s", plugin_name, versions)) end ui.line - ui.plain(" #{search_results.count} plugin(s) found") + ui.plain_line(" #{search_results.count} plugin(s) found") puts ui.exit Inspec::UI::EXIT_PLUGIN_ERROR if search_results.empty? @@ -118,14 +140,14 @@ module InspecPlugins begin installer.update(plugin_name) rescue Inspec::Plugin::V2::UpdateError => ex - ui.plain("#{ui.red("Update error:")} #{ex.message} - update failed") + ui.plain_line("#{ui.red("Update error:", print: false)} #{ex.message} - update failed") ui.exit Inspec::UI::EXIT_USAGE_ERROR end post_update_versions = installer.list_installed_plugin_gems.select { |spec| spec.name == plugin_name }.map { |spec| spec.version.to_s } new_version = (post_update_versions - pre_update_versions).first ui.bold(plugin_name + " plugin, version #{old_version} -> " \ - "#{new_version}, updated from rubygems.org") + "#{new_version}, updated from rubygems.org\n") end #-------------------------- @@ -144,7 +166,7 @@ module InspecPlugins def uninstall(plugin_name) status = Inspec::Plugin::V2::Registry.instance[plugin_name.to_sym] unless status - ui.plain("#{ui.red("No such plugin installed:")} #{plugin_name} is not " \ + ui.plain_line("#{ui.red("No such plugin installed:", print: false)} #{plugin_name} is not " \ "installed - uninstall failed") ui.exit Inspec::UI::EXIT_USAGE_ERROR end @@ -157,11 +179,12 @@ module InspecPlugins if status.installation_type == :path ui.bold(plugin_name + " path-based plugin install has been " \ - "uninstalled") + "uninstalled\n") else ui.bold(plugin_name + " plugin, version #{old_version}, has " \ - "been uninstalled") + "been uninstalled\n") end + ui.exit Inspec::UI::EXIT_NORMAL end @@ -174,7 +197,7 @@ module InspecPlugins def install_from_gemfile(gem_file) unless File.exist? gem_file - ui.red("No such plugin gem file #{gem_file} - installation failed.") + ui.red("No such plugin gem file #{gem_file} - installation failed.\n") ui.exit Inspec::UI::EXIT_USAGE_ERROR end @@ -186,13 +209,13 @@ module InspecPlugins installer.install(plugin_name, gem_file: gem_file) ui.bold("#{plugin_name} plugin, version #{version}, installed from " \ - "local .gem file") + "local .gem file\n") ui.exit Inspec::UI::EXIT_NORMAL end def install_from_path(path) unless File.exist? path - ui.red("No such source code path #{path} - installation failed.") + ui.red("No such source code path #{path} - installation failed.\n") ui.exit Inspec::UI::EXIT_USAGE_ERROR end @@ -209,7 +232,7 @@ module InspecPlugins if registry.known_plugin?(plugin_name.to_sym) ui.red("Plugin already installed - #{plugin_name} - Use '#{EXEC_NAME} " \ "plugin list' to see previously installed plugin - " \ - "installation failed.") + "installation failed.\n") ui.exit Inspec::UI::EXIT_PLUGIN_ERROR end @@ -223,7 +246,7 @@ module InspecPlugins installer.install(plugin_name, path: entry_point) ui.bold("#{plugin_name} plugin installed via source path reference, " \ - "resolved to entry point #{entry_point}") + "resolved to entry point #{entry_point}\n") ui.exit Inspec::UI::EXIT_NORMAL end @@ -288,7 +311,7 @@ module InspecPlugins # Give up. ui.red("Unrecognizable plugin structure - #{parts[2]} - When " \ "installing from a path, please provide the path of the " \ - "entry point file - installation failed.") + "entry point file - installation failed.\n") ui.exit Inspec::UI::EXIT_USAGE_ERROR end @@ -299,8 +322,8 @@ module InspecPlugins rescue LoadError => ex ui.red("Plugin contains errors - #{plugin_name} - Encountered " \ "errors while trying to test load the plugin entry point, " \ - "resolved to #{entry_point} - installation failed") - ui.plain ex.message + "resolved to #{entry_point} - installation failed\n") + ui.plain_line ex.message ui.exit Inspec::UI::EXIT_USAGE_ERROR end @@ -313,7 +336,7 @@ module InspecPlugins ui.red("Does not appear to be a plugin - #{plugin_name} - After " \ "probe-loading the supposed plugin, it did not register " \ "itself to Train. Ensure something inherits from " \ - "'Train.plugin(1)' - installation failed.") + "'Train.plugin(1)' - installation failed.\n") ui.exit Inspec::UI::EXIT_USAGE_ERROR end else @@ -321,7 +344,7 @@ module InspecPlugins ui.red("Does not appear to be a plugin - #{plugin_name} - After " \ "probe-loading the supposed plugin, it did not register " \ "itself to InSpec. Ensure something inherits from " \ - "'Inspec.plugin(2)' - installation failed.") + "'Inspec.plugin(2)' - installation failed.\n") ui.exit Inspec::UI::EXIT_USAGE_ERROR end end @@ -343,7 +366,7 @@ module InspecPlugins new_version = (post_installed_versions - pre_installed_versions).first ui.bold("#{plugin_name} plugin, version #{new_version}, installed " \ - "from rubygems.org") + "from rubygems.org\n") ui.exit Inspec::UI::EXIT_NORMAL end @@ -367,16 +390,16 @@ module InspecPlugins what_we_would_install_is_already_installed = pre_installed_versions.include?(requested_version) if what_we_would_install_is_already_installed && they_explicitly_asked_for_a_version ui.red("Plugin already installed at requested version - plugin " \ - "#{plugin_name} #{requested_version} - refusing to install.") + "#{plugin_name} #{requested_version} - refusing to install.\n") elsif what_we_would_install_is_already_installed && !they_explicitly_asked_for_a_version ui.red("Plugin already installed at latest version - plugin " \ - "#{plugin_name} #{requested_version} - refusing to install.") + "#{plugin_name} #{requested_version} - refusing to install.\n") else # There are existing versions installed, but none of them are what was requested ui.red("Update required - plugin #{plugin_name}, requested " \ "#{requested_version}, have " \ "#{pre_installed_versions.join(", ")}; use `inspec " \ - "plugin update` - refusing to install.") + "plugin update` - refusing to install.\n") end ui.exit Inspec::UI::EXIT_PLUGIN_ERROR @@ -387,11 +410,11 @@ module InspecPlugins installer.install(plugin_name, version: options[:version]) rescue Inspec::Plugin::V2::PluginExcludedError => ex ui.red("Plugin on Exclusion List - #{plugin_name} is listed as an " \ - "incompatible gem - refusing to install.") - ui.plain("Rationale: #{ex.details.rationale}") - ui.plain("Exclusion list location: " + + "incompatible gem - refusing to install.\n") + ui.plain_line("Rationale: #{ex.details.rationale}") + ui.plain_line("Exclusion list location: " + File.join(Inspec.src_root, "etc", "plugin_filters.json")) - ui.plain("If you disagree with this determination, please accept " \ + ui.plain_line("If you disagree with this determination, please accept " \ "our apologies for the misunderstanding, and open an issue " \ "at https://github.com/inspec/inspec/issues/new") ui.exit Inspec::UI::EXIT_PLUGIN_ERROR @@ -401,13 +424,13 @@ module InspecPlugins results = installer.search(plugin_name, exact: true) if results.empty? ui.red("No such plugin gem #{plugin_name} could be found on " \ - "rubygems.org - installation failed.") + "rubygems.org - installation failed.\n") elsif options[:version] && !results[plugin_name].include?(options[:version]) ui.red("No such version - #{plugin_name} exists, but no such " \ "version #{options[:version]} found on rubygems.org - " \ - "installation failed.") + "installation failed.\n") else - ui.red("Unknown error occured - installation failed.") + ui.red("Unknown error occured - installation failed.\n") end ui.exit Inspec::UI::EXIT_USAGE_ERROR end @@ -420,10 +443,10 @@ module InspecPlugins # Check for path install status = Inspec::Plugin::V2::Registry.instance[plugin_name.to_sym] if !status - ui.plain("#{ui.red("No such plugin installed:")} #{plugin_name} - update failed") + ui.plain_line("#{ui.red("No such plugin installed:", print: false)} #{plugin_name} - update failed") ui.exit Inspec::UI::EXIT_USAGE_ERROR elsif status.installation_type == :path - ui.plain("#{ui.red("Cannot update path-based install:")} " \ + ui.plain_line("#{ui.red("Cannot update path-based install:", print: false)} " \ "#{plugin_name} is installed via path reference; " \ "use `inspec plugin uninstall` to remove - refusing to" \ "update") @@ -436,7 +459,7 @@ module InspecPlugins latest_version = latest_version[plugin_name]&.last if pre_update_versions.include?(latest_version) - ui.plain("#{ui.red("Already installed at latest version:")} " \ + ui.plain_line("#{ui.red("Already installed at latest version:", print: false)} " \ "#{plugin_name} is at #{latest_version}, which the " \ "latest - refusing to update") ui.exit Inspec::UI::EXIT_PLUGIN_ERROR @@ -458,7 +481,7 @@ module InspecPlugins unless plugin_name =~ /^(inspec|train)-/ ui.red("Invalid plugin name - #{plugin_name} - All inspec " \ "plugins must begin with either 'inspec-' or 'train-' " \ - "- #{action} failed.") + "- #{action} failed.\n") ui.exit Inspec::UI::EXIT_USAGE_ERROR end end @@ -467,17 +490,29 @@ module InspecPlugins case status.installation_type when :core, :bundle Inspec::VERSION - when :gem - # TODO: this is naive, and assumes the latest version is the one that will be used. Logged on #3317 - # In fact, the logic to determine "what version would be used" belongs in the Loader. - Inspec::Plugin::V2::Loader.list_installed_plugin_gems - .select { |spec| spec.name == status.name.to_s } - .sort_by(&:version) - .last.version + when :user_gem, :system_gem + if status.version.nil? + "(unknown)" + elsif status.version =~ /^\d+\.\d+\.\d+$/ + status.version + else + # Assume it is a version constraint string and try to resolve + # TODO: this is naive, and assumes the latest version is the one that will be used. Logged on #3317 + # In fact, the logic to determine "what version would be used" belongs in the Loader. + plugin_name = status.name.to_s + Inspec::Plugin::V2::Loader.list_installed_plugin_gems + .select { |spec| spec.name == plugin_name } + .sort_by(&:version) + .last.version + end when :path "src" end end + + def make_pretty_install_type(status) + INSTALL_TYPE_LABELS[status.installation_type] + end end end end diff --git a/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb b/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb index 3169735ff..a51fabf73 100644 --- a/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb +++ b/lib/plugins/inspec-plugin-manager-cli/test/functional/inspec-plugin_test.rb @@ -15,7 +15,9 @@ module PluginManagerHelpers let(:list_after_run) do Proc.new do |run_result, tmp_dir| # After installing/uninstalling/whatevering, run list with config in the same dir, and capture it. - run_result.payload.list_result = run_inspec_process("plugin list", env: { INSPEC_CONFIG_DIR: tmp_dir }) + run_result.payload.list_result = parse_plugin_list_lines( + run_inspec_process("plugin list", env: { INSPEC_CONFIG_DIR: tmp_dir }).stdout + ) end end @@ -39,6 +41,27 @@ module PluginManagerHelpers end end + def parse_plugin_list_lines(stdout) + plugins = [] + + stdout.force_encoding("UTF-8").lines.each do |line| + next if line.strip.empty? + next if line.include? "─────" # This is some unicode glyphiness + next if line.include? "Plugin Name" + next if line.include? "plugin(s) total" + + parts = line.split(/│/u).map(&:strip!).compact + plugins << { + name: parts[0], + version: parts[1], + type: parts[2], + generation: parts[3], + raw: line, + } + end + plugins + end + def teardown clear_empty_config_dir end @@ -76,31 +99,41 @@ class PluginManagerCliList < Minitest::Test include CorePluginFunctionalHelper include PluginManagerHelpers - def test_list_when_no_user_plugins_installed - result = run_inspec_process_with_this_plugin("plugin list") - - skip_windows! - assert_includes result.stdout, "0 plugin(s) total", "Empty list should include zero count" - - assert_exit_code 0, result - end + # Listing all plugins is now default behavior + LIST_CASES = [ + { arg: "-c", name: "inspec-plugin-manager-cli", type: "core" }, + { arg: "-c", name: "inspec-supermarket", type: "core" }, + { arg: "-s", name: "train-aws", type: "gem (system)" }, + ].freeze def test_list_all_when_no_user_plugins_installed result = run_inspec_process_with_this_plugin("plugin list --all") - skip_windows! - assert_includes result.stdout, "6 plugin(s) total", "--all list should find six" - assert_includes result.stdout, "inspec-plugin-manager-cli", "--all list should find inspec-plugin-manager-cli" - assert_includes result.stdout, "habitat", "--all list should find habitat" + assert_empty result.stderr + plugins_seen = parse_plugin_list_lines(result.stdout) + + # Look for a specific plugin of each type - core, bundle, and system + LIST_CASES.each do |test_case| + plugin_line = plugins_seen.detect { |plugin| plugin[:name] == test_case[:name] } + refute_nil plugin_line, "#{test_case[:name]} should be detected in plugin list --all output" + assert_equal test_case[:type], plugin_line[:type], "#{test_case[:name]} should be detected as a '#{test_case[:type]}' type in list --all " + end assert_exit_code 0, result + end - # TODO: split - result = run_inspec_process_with_this_plugin("plugin list -a") + def test_list_selective_when_no_user_plugins_installed + LIST_CASES.each do |test_case| + result = run_inspec_process_with_this_plugin("plugin list #{test_case[:arg]}") + skip_windows! - assert_includes result.stdout, "6 plugin(s) total", "-a list should find six" - - assert_exit_code 0, result + assert_empty result.stderr + plugins_seen = parse_plugin_list_lines(result.stdout) + plugin_line = plugins_seen.detect { |plugin| plugin[:name] == test_case[:name] } + refute_nil plugin_line, "#{test_case[:name]} should be detected in plugin list #{test_case[:arg]} output" + assert_equal plugin_line[:type], test_case[:type], "#{test_case[:name]} should be detected as a '#{test_case[:type]}' type in list #{test_case[:arg]} " + assert_exit_code 0, result + end end def test_list_when_gem_and_path_plugins_installed @@ -109,21 +142,26 @@ class PluginManagerCliList < Minitest::Test copy_in_core_config_dir("test-fixture-1-float", tmp_dir) end - result = run_inspec_process_with_this_plugin("plugin list", pre_run: pre_block) - + result = run_inspec_process_with_this_plugin("plugin list --user ", pre_run: pre_block) skip_windows! - assert_includes result.stdout, "2 plugin(s) total", "gem+path should show two plugins" - # Plugin Name Version Via ApiVer - # ------------------------------------------------------- - # inspec-meaning-of-life src path 2 - # inspec-test-fixture 0.1.0 gem 2 - # ------------------------------------------------------- + assert_empty result.stderr + plugins_seen = parse_plugin_list_lines(result.stdout) + assert_equal 2, plugins_seen.count + # Plugin Name Version Via ApiVer + # --------------------------------------------------------- + # inspec-meaning-of-life src path 2 + # inspec-test-fixture 0.1.0 gem (user) 2 + # --------------------------------------------------------- # 2 plugin(s) total - gem_line = result.stdout.split("\n").grep(/gem/).first - assert_match(/\s*inspec-\S+\s+\d+\.\d+\.\d+\s+gem\s+2/, gem_line) - path_line = result.stdout.split("\n").grep(/path/).first - assert_match(/\s*inspec-\S+\s+src\s+path\s+2/, path_line) + meaning = plugins_seen.detect { |p| p[:name] == "inspec-meaning-of-life" } + refute_nil meaning + assert_equal "path", meaning[:type] + + fixture = plugins_seen.detect { |p| p[:name] == "inspec-test-fixture" } + refute_nil fixture + assert_equal "gem (user)", fixture[:type] + assert_equal "0.1.0", fixture[:version] assert_exit_code 0, result end @@ -134,21 +172,24 @@ class PluginManagerCliList < Minitest::Test copy_in_core_config_dir("train-test-fixture", tmp_dir) end - result = run_inspec_process_with_this_plugin("plugin list", pre_run: pre_block) - + result = run_inspec_process_with_this_plugin("plugin list --user ", pre_run: pre_block) skip_windows! + + assert_empty result.stderr + plugins_seen = parse_plugin_list_lines(result.stdout) + assert_equal 1, plugins_seen.count assert_includes result.stdout, "1 plugin(s) total", "list train should show one plugins" - # Plugin Name Version Via ApiVer - # ------------------------------------------------------- - # train-test-fixture 0.1.0 gem train-1 - # ------------------------------------------------------- + # Plugin Name Version Via ApiVer + # ------------------------------------------------------------- + # train-test-fixture 0.1.0 gem (user) train-1 + # ------------------------------------------------------------- # 1 plugin(s) total - train_line = result.stdout.split("\n").grep(/train/).first - assert_includes(train_line, "train-test-fixture") - assert_includes(train_line, "0.1.0") - assert_includes(train_line, "gem") - assert_includes(train_line, "train-1") + train_plugin = plugins_seen.detect { |p| p[:name] == "train-test-fixture" } + refute_nil train_plugin + assert_equal "gem (user)", train_plugin[:type] + assert_equal "train-1", train_plugin[:generation] + assert_equal "0.1.0", train_plugin[:version] assert_exit_code 0, result end @@ -333,23 +374,21 @@ class PluginManagerCliInstall < Minitest::Test # Check UX messaging success_message = install_result.stdout.split("\n").grep(/installed/).last skip_windows! + assert_empty install_result.stderr refute_nil success_message, "Should find a success message at the end" assert_includes success_message, fixture_info[:plugin_name] assert_includes success_message, "plugin installed via source path reference" # Check round-trip UX via list - list_result = install_result.payload.list_result - itf_line = list_result.stdout.split("\n").grep(Regexp.new(fixture_info[:plugin_name])).first - refute_nil itf_line, "plugin name should now appear in the output of inspec list" - assert_match(/\s*(inspec|train)-test-fixture\s+src\s+path\s+/, itf_line, "list output should show that it is a path installation") + itf_plugin = install_result.payload.list_result.detect { |p| p[:name] == fixture_info[:plugin_name] } + refute_nil itf_plugin, "plugin name should now appear in the output of inspec list" + assert_equal "path", itf_plugin[:type], "list output should show that it is a path installation" # Check plugin statefile. Extra important in this case, since all should resolve to the same entry point. plugin_data = install_result.payload.plugin_data entry = plugin_data["plugins"].detect { |e| e["name"] == fixture_info[:plugin_name] } assert_equal fixture_info[:resolved_path], entry["installation_path"], "Regardless of input, the entry point should be correct." - assert_empty install_result.stderr - assert_exit_code 0, install_result end end @@ -357,9 +396,9 @@ class PluginManagerCliInstall < Minitest::Test def test_fail_install_from_nonexistant_path bad_path = File.join(project_fixtures_path, "none", "such", "inspec-test-fixture-nonesuch.rb") install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}") - - error_message = install_result.stdout.split("\n").last skip_windows! + + error_message = install_result.stdout assert_includes error_message, "No such source code path" assert_includes error_message, "inspec-test-fixture-nonesuch.rb" assert_includes error_message, "installation failed" @@ -372,9 +411,9 @@ class PluginManagerCliInstall < Minitest::Test def test_fail_install_from_path_with_wrong_name bad_path = File.join(project_fixtures_path, "plugins", "wrong-name", "lib", "wrong-name.rb") install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}") - - error_message = install_result.stdout.split("\n").last skip_windows! + + error_message = install_result.stdout assert_includes error_message, "Invalid plugin name" assert_includes error_message, "wrong-name" assert_includes error_message, "All inspec plugins must begin with either 'inspec-' or 'train-'" @@ -388,9 +427,9 @@ class PluginManagerCliInstall < Minitest::Test def test_fail_install_from_path_when_it_is_not_a_plugin bad_path = File.join(project_fixtures_path, "plugins", "inspec-egg-white-omelette", "lib", "inspec-egg-white-omelette.rb") install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}") - - error_message = install_result.stdout.split("\n").last skip_windows! + + error_message = install_result.stdout assert_includes error_message, "Does not appear to be a plugin" assert_includes error_message, "inspec-egg-white-omelette" assert_includes error_message, "After probe-loading the supposed plugin, it did not register" @@ -413,9 +452,9 @@ class PluginManagerCliInstall < Minitest::Test end install_result = run_inspec_process_with_this_plugin("plugin install #{plugin_path}", pre_run: pre_block) - - error_message = install_result.stdout.split("\n").last skip_windows! + + error_message = install_result.stdout assert_includes error_message, "Plugin already installed" assert_includes error_message, "inspec-test-fixture" assert_includes error_message, "Use 'inspec plugin list' to see previously installed plugin" @@ -429,9 +468,9 @@ class PluginManagerCliInstall < Minitest::Test def test_fail_install_from_path_when_the_dir_structure_is_wrong bad_path = File.join(project_fixtures_path, "plugins", "inspec-wrong-structure") install_result = run_inspec_process_with_this_plugin("plugin install #{bad_path}") - - error_message = install_result.stdout.split("\n").last skip_windows! + + error_message = install_result.stdout assert_includes error_message, "Unrecognizable plugin structure" assert_includes error_message, "inspec-wrong-structure" assert_includes error_message, " When installing from a path, please provide the path of the entry point file" @@ -445,21 +484,18 @@ class PluginManagerCliInstall < Minitest::Test def test_install_from_gemfile fixture_gemfile_path = File.join(core_fixture_plugins_path, "inspec-test-fixture", "pkg", "inspec-test-fixture-0.1.0.gem") install_result = run_inspec_process_with_this_plugin("plugin install #{fixture_gemfile_path}", post_run: list_after_run) + skip_windows! success_message = install_result.stdout.split("\n").grep(/installed/).last - skip_windows! refute_nil success_message, "Should find a success message at the end" - assert_includes success_message, "inspec-test-fixture" - assert_includes success_message, "0.1.0" assert_includes success_message, "installed from local .gem file" - list_result = install_result.payload.list_result - itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first - refute_nil itf_line, "inspec-test-fixture should now appear in the output of inspec list" - assert_match(/\s*inspec-test-fixture\s+0.1.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version") + itf_plugin = install_result.payload.list_result.detect { |p| p[:name] == "inspec-test-fixture" } + refute_nil itf_plugin, "plugin name should now appear in the output of inspec list" + assert_equal "gem (user)", itf_plugin[:type] + assert_equal "0.1.0", itf_plugin[:version] assert_empty install_result.stderr - assert_exit_code 0, install_result end @@ -477,21 +513,20 @@ class PluginManagerCliInstall < Minitest::Test def test_install_from_rubygems_latest install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture", post_run: list_after_run) + skip_windows! success_message = install_result.stdout.split("\n").grep(/installed/).last - skip_windows! refute_nil success_message, "Should find a success message at the end" assert_includes success_message, "inspec-test-fixture" assert_includes success_message, "0.2.0" assert_includes success_message, "installed from rubygems.org" - list_result = install_result.payload.list_result - itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first - refute_nil itf_line, "inspec-test-fixture should now appear in the output of inspec list" - assert_match(/\s*inspec-test-fixture\s+0.2.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version") + itf_plugin = install_result.payload.list_result.detect { |p| p[:name] == "inspec-test-fixture" } + refute_nil itf_plugin, "plugin name should now appear in the output of inspec list" + assert_equal "gem (user)", itf_plugin[:type] + assert_equal "0.2.0", itf_plugin[:version] assert_empty install_result.stderr - assert_exit_code 0, install_result end @@ -516,10 +551,10 @@ class PluginManagerCliInstall < Minitest::Test assert_includes success_message, "0.1.0" assert_includes success_message, "installed from rubygems.org" - list_result = install_result.payload.list_result - itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first - refute_nil itf_line, "inspec-test-fixture should now appear in the output of inspec list" - assert_match(/\s*inspec-test-fixture\s+0.1.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version") + itf_plugin = install_result.payload.list_result.detect { |p| p[:name] == "inspec-test-fixture" } + refute_nil itf_plugin, "plugin name should now appear in the output of inspec list" + assert_equal "gem (user)", itf_plugin[:type] + assert_equal "0.1.0", itf_plugin[:version] assert_empty install_result.stderr @@ -600,21 +635,20 @@ class PluginManagerCliInstall < Minitest::Test def test_install_from_rubygems_latest_with_train_plugin install_result = run_inspec_process_with_this_plugin("plugin install train-test-fixture", post_run: list_after_run) + skip_windows! success_message = install_result.stdout.split("\n").grep(/installed/).last - skip_windows! refute_nil success_message, "Should find a success message at the end" assert_includes success_message, "train-test-fixture" assert_includes success_message, "0.1.0" assert_includes success_message, "installed from rubygems.org" - list_result = install_result.payload.list_result - itf_line = list_result.stdout.split("\n").grep(/train-test-fixture/).first - refute_nil itf_line, "train-test-fixture should now appear in the output of inspec list" - assert_match(/\s*train-test-fixture\s+0.1.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version") + ttf_plugin = install_result.payload.list_result.detect { |p| p[:name] == "train-test-fixture" } + refute_nil ttf_plugin, "plugin name should now appear in the output of inspec list" + assert_equal "gem (user)", ttf_plugin[:type] + assert_equal "0.1.0", ttf_plugin[:version] assert_empty install_result.stderr - assert_exit_code 0, install_result end @@ -649,8 +683,8 @@ class PluginManagerCliInstall < Minitest::Test skip "this test requires bundler to pass" unless defined? ::Bundler install_result = run_inspec_process_with_this_plugin("plugin install inspec-test-fixture -v 0.1.1 --log-level debug") - skip_windows! + assert_includes install_result.stdout, "DEBUG" assert_includes install_result.stderr, "can't activate rake" @@ -684,13 +718,12 @@ class PluginManagerCliUpdate < Minitest::Test assert_includes success_message, "0.2.0" assert_includes success_message, "updated from rubygems.org" - list_result = update_result.payload.list_result - itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first - refute_nil itf_line, "inspec-test-fixture should appear in the output of inspec list" - assert_match(/\s*inspec-test-fixture\s+0.2.0\s+gem\s+/, itf_line, "list output should show that it is a gem installation with version 0.2.0") + itf_plugin = update_result.payload.list_result.detect { |p| p[:name] == "inspec-test-fixture" } + refute_nil itf_plugin, "plugin name should now appear in the output of inspec list" + assert_equal "gem (user)", itf_plugin[:type] + assert_equal "0.2.0", itf_plugin[:version] assert_empty update_result.stderr - assert_exit_code 0, update_result end @@ -769,12 +802,10 @@ class PluginManagerCliUninstall < Minitest::Test assert_includes success_message, "0.1.0" assert_includes success_message, "has been uninstalled" - list_result = uninstall_result.payload.list_result - itf_line = list_result.stdout.split("\n").grep(/inspec-test-fixture/).first - assert_nil itf_line, "inspec-test-fixture should not appear in the output of inspec list" + itf_plugins = uninstall_result.payload.list_result.select { |p| p[:name] == "inspec-test-fixture" } + assert_empty itf_plugins, "inspec-test-fixture should not appear in the output of inspec list" assert_empty uninstall_result.stderr - assert_exit_code 0, uninstall_result end @@ -784,22 +815,19 @@ class PluginManagerCliUninstall < Minitest::Test # This fixture includes a path install for inspec-meaning-of-life copy_in_core_config_dir("test-fixture-1-float", tmp_dir) end - uninstall_result = run_inspec_process_with_this_plugin("plugin uninstall inspec-meaning-of-life", pre_run: pre_block, post_run: list_after_run) + skip_windows! success_message = uninstall_result.stdout.split("\n").grep(/uninstalled/).last - skip_windows! refute_nil success_message, "Should find a success message at the end" assert_includes success_message, "inspec-meaning-of-life" assert_includes success_message, "path-based plugin install" assert_includes success_message, "has been uninstalled" - list_result = uninstall_result.payload.list_result - itf_line = list_result.stdout.split("\n").grep(/inspec-meaning-of-life/).first - assert_nil itf_line, "inspec-meaning-of-life should not appear in the output of inspec list" + itf_plugins = uninstall_result.payload.list_result.select { |p| p[:name] == "inspec-meaning-of-life" } + assert_empty itf_plugins, "inspec-meaning-of-life should not appear in the output of inspec list" assert_empty uninstall_result.stderr - assert_exit_code 0, uninstall_result end diff --git a/lib/plugins/inspec-plugin-manager-cli/test/unit/cli_args_test.rb b/lib/plugins/inspec-plugin-manager-cli/test/unit/cli_args_test.rb index 803af52b0..f5789c148 100644 --- a/lib/plugins/inspec-plugin-manager-cli/test/unit/cli_args_test.rb +++ b/lib/plugins/inspec-plugin-manager-cli/test/unit/cli_args_test.rb @@ -13,13 +13,15 @@ class PluginManagerCliOptions < Minitest::Test def test_list_args arg_config = cli_class.all_commands["list"].options - assert_equal 1, arg_config.count, "The list command should have 1 option" + assert_equal 4, arg_config.count, "The list command should have 4 options" - assert_includes arg_config.keys, :all, "The list command should have an --all option" - assert_equal :boolean, arg_config[:all].type, "The --all option should be boolean" - assert_equal :a, arg_config[:all].aliases.first, "The --all option should be aliased as -a" - refute_nil arg_config[:all].description, "The --all option should have a description" - refute arg_config[:all].required, "The --all option should not be required" + { u: :user, a: :all, c: :core, s: :system }.each do |abbrev, option| + assert_includes arg_config.keys, option, "The list command should have an --#{option} option" + assert_equal :boolean, arg_config[option].type, "The --#{option} option should be boolean" + assert_equal abbrev, arg_config[option].aliases.first, "The --#{option} option should be aliased as -#{abbrev}" + refute_nil arg_config[option].description, "The --#{option} option should have a description" + refute arg_config[option].required, "The --#{option} option should not be required" + end assert_equal 0, cli_class.instance_method(:list).arity, "The list command should take no arguments" end diff --git a/test/unit/mock/config_dirs/test-fixture-1-float/plugins.json b/test/unit/mock/config_dirs/test-fixture-1-float/plugins.json index 793df146c..e721bbc72 100644 --- a/test/unit/mock/config_dirs/test-fixture-1-float/plugins.json +++ b/test/unit/mock/config_dirs/test-fixture-1-float/plugins.json @@ -2,7 +2,8 @@ "plugins_config_version" : "1.0.0", "plugins": [ { - "name": "inspec-test-fixture" + "name": "inspec-test-fixture", + "version": "0.1.0" }, { "name": "inspec-meaning-of-life", diff --git a/test/unit/mock/config_dirs/train-test-fixture/plugins.json b/test/unit/mock/config_dirs/train-test-fixture/plugins.json index 4ab04a0f7..fe139bfd8 100644 --- a/test/unit/mock/config_dirs/train-test-fixture/plugins.json +++ b/test/unit/mock/config_dirs/train-test-fixture/plugins.json @@ -2,7 +2,8 @@ "plugins_config_version" : "1.0.0", "plugins": [ { - "name": "train-test-fixture" + "name": "train-test-fixture", + "version": "0.1.0" } ] } \ No newline at end of file diff --git a/test/unit/plugin/v2/installer_test.rb b/test/unit/plugin/v2/installer_test.rb index d5088856b..b35478484 100644 --- a/test/unit/plugin/v2/installer_test.rb +++ b/test/unit/plugin/v2/installer_test.rb @@ -321,11 +321,11 @@ class PluginInstallerUpdaterTests < Minitest::Test spec_path = File.join(@installer.gem_path, "specifications", "inspec-test-fixture-0.1.0.gemspec") assert File.exist?(spec_path), "After update, the 0.1.0 gemspec should remain" - # Plugins file entry should not be version pinned + # Plugins file entry should now be version pinned to latest plugin_json_path = File.join(ENV["INSPEC_CONFIG_DIR"], "plugins.json") plugin_json_data = JSON.parse(File.read(plugin_json_path)) entry = plugin_json_data["plugins"].detect { |e| e["name"] == "inspec-test-fixture" } - refute_includes entry.keys, "version", "plugins.json should NOT include version pinning key" + assert_equal "= 0.2.0", entry["version"] end def test_update_to_specified_later_version diff --git a/test/unit/plugin/v2/loader_test.rb b/test/unit/plugin/v2/loader_test.rb index 94c03d8f3..e3151f9a6 100644 --- a/test/unit/plugin/v2/loader_test.rb +++ b/test/unit/plugin/v2/loader_test.rb @@ -28,6 +28,9 @@ class PluginLoaderTests < Minitest::Test inspec-habitat inspec-init } + @system_plugins = [ + "train-habitat", + ] end def teardown @@ -42,12 +45,6 @@ class PluginLoaderTests < Minitest::Test # basic constructor usage and bundle detection # #====================================================================# - def test_constructor_should_not_load_anything_automatically - reg = Inspec::Plugin::V2::Registry.instance - Inspec::Plugin::V2::Loader.new - assert_equal 0, reg.loaded_count, "\nRegistry load count" - end - def test_constructor_should_detect_bundled_plugins reg = Inspec::Plugin::V2::Registry.instance Inspec::Plugin::V2::Loader.new @@ -106,7 +103,7 @@ class PluginLoaderTests < Minitest::Test def test_load_no_plugins_should_load_no_plugins reg = Inspec::Plugin::V2::Registry.instance - loader = Inspec::Plugin::V2::Loader.new(omit_bundles: true, omit_core_plugins: true, omit_user_plugins: true) + loader = Inspec::Plugin::V2::Loader.new(omit_bundles: true, omit_core_plugins: true, omit_user_plugins: true, omit_sys_plugins: true) loader.load_all assert_equal 0, reg.loaded_count, "\nRegistry load count" end