class InspecPlugins::PluginManager::CliCommand

Constants

INSTALL_TYPE_LABELS

Public Instance Methods

install(plugin_id_arg) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 117
def install(plugin_id_arg)
  if plugin_id_arg =~ /\.gem$/ # Does it end in .gem?
    install_from_gemfile(plugin_id_arg)
  elsif plugin_id_arg =~ %r{[\/\\]} || Dir.exist?(plugin_id_arg) # Does the argument have a slash, or exist as dir in the local directory?
    install_from_path(plugin_id_arg)
  else
    install_from_remote_gem(plugin_id_arg)
  end
end
list() click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 31
def list
  plugin_statuses = Inspec::Plugin::V2::Registry.instance.plugin_statuses
  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

  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
uninstall(plugin_name) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 168
def uninstall(plugin_name)
  status = Inspec::Plugin::V2::Registry.instance[plugin_name.to_sym]
  unless status
    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
  installer = Inspec::Plugin::V2::Installer.instance

  pre_uninstall_versions = installer.list_installed_plugin_gems.select { |spec| spec.name == plugin_name }.map { |spec| spec.version.to_s }
  old_version = pre_uninstall_versions.join(", ")

  installer.uninstall(plugin_name)

  if status.installation_type == :path
    ui.bold(plugin_name + " path-based plugin install has been " \
            "uninstalled\n")
  else
    ui.bold(plugin_name + " plugin, version #{old_version}, has " \
            "been uninstalled\n")
  end

  ui.exit Inspec::UI::EXIT_NORMAL
end
update(plugin_name) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 136
def update(plugin_name)
  pre_update_versions = installer.list_installed_plugin_gems.select { |spec| spec.name == plugin_name }.map { |spec| spec.version.to_s }
  old_version = pre_update_versions.join(", ")

  update_preflight_check(plugin_name, pre_update_versions)

  begin
    installer.update(plugin_name)
  rescue Inspec::Plugin::V2::UpdateError => ex
    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\n")
end

Private Instance Methods

check_plugin_name(plugin_name, action) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 484
def check_plugin_name(plugin_name, action)
  unless plugin_name =~ /^(inspec|train)-/
    ui.red("Invalid plugin name - #{plugin_name} - All inspec " \
           "plugins must begin with either 'inspec-' or 'train-' " \
           "- #{action} failed.\n")
    ui.exit Inspec::UI::EXIT_USAGE_ERROR
  end
end
install_attempt_install(plugin_name) click to toggle source

Rationale for RuboCop variance: This is a one-line method with heavy UX-focused error handling.

# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 412
def install_attempt_install(plugin_name) # rubocop: disable Metrics/AbcSize
  installer.install(plugin_name, version: options[:version], source: options[:source])
rescue Inspec::Plugin::V2::PluginExcludedError => ex
  ui.red("Plugin on Exclusion List - #{plugin_name} is listed as an " \
         "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_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
rescue Inspec::Plugin::V2::InstallError
  raise if Inspec::Log.level == :debug

  results = installer.search(plugin_name, exact: true)
  source_host = URI(options[:source] || "https://rubygems.org/").host
  if results.empty?
    ui.red("No such plugin gem #{plugin_name} could be found on " \
           "#{source_host} - 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 #{source_host} - " \
           "installation failed.\n")
  else
    ui.red("Unknown error occurred - installation failed.\n")
  end
  ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
install_from_gemfile(gem_file) click to toggle source
#
install breakdown
#
These are broken down because rubocop complained.
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 200
def install_from_gemfile(gem_file)
  unless File.exist? gem_file
    ui.red("No such plugin gem file #{gem_file} - installation failed.\n")
    ui.exit Inspec::UI::EXIT_USAGE_ERROR
  end

  plugin_name_parts = File.basename(gem_file, ".gem").split("-")
  version = plugin_name_parts.pop
  plugin_name = plugin_name_parts.join("-")
  check_plugin_name(plugin_name, "installation")

  installer.install(plugin_name, gem_file: gem_file)

  ui.bold("#{plugin_name} plugin, version #{version}, installed from " \
          "local .gem file\n")
  ui.exit Inspec::UI::EXIT_NORMAL
end
install_from_path(path) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 218
def install_from_path(path)
  unless File.exist? path
    ui.red("No such source code path #{path} - installation failed.\n")
    ui.exit Inspec::UI::EXIT_USAGE_ERROR
  end

  plugin_name = File.basename(path, ".rb")

  # While installer.install does some rudimentary checking,
  # this file has good UI access, so we promise to validate the
  # input a lot and hand the installer a sure-thing.

  # Name OK?
  check_plugin_name(plugin_name, "installation")

  # Already installed?
  if registry.known_plugin?(plugin_name.to_sym)
    ui.bold("Plugin already installed - #{plugin_name} - Use '#{EXEC_NAME} " \
            "plugin list' to see previously installed plugin - " \
            "installation failed.\n")
    ui.exit Inspec::UI::EXIT_NORMAL
  end

  # Can we figure out how to load it?
  entry_point = install_from_path__apply_entry_point_heuristics(path)

  # If you load it, does it act like a plugin?
  install_from_path__probe_load(entry_point, plugin_name)

  # OK, install it!
  installer.install(plugin_name, path: entry_point)

  ui.bold("#{plugin_name} plugin installed via source path reference, " \
          "resolved to entry point #{entry_point}\n")
  ui.exit Inspec::UI::EXIT_NORMAL
end
install_from_path__apply_entry_point_heuristics(path) click to toggle source

Rationale for rubocop variances: It's a heuristics method, and will be full of conditionals. The code is well-commented; refactoring into sub-methods would reduce clarity.

# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 258
def install_from_path__apply_entry_point_heuristics(path) # rubocop: disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
  given = Pathname.new(path)
  given = given.expand_path # Resolve any relative paths
  name_regex = /^(inspec|train)-/
  versioned_regex = /^(inspec|train)-[a-z0-9\-\_]+-\d+\.\d+\.\d+$/
  sha_ref_regex = /^(inspec|train)-[a-z0-9\-\_]+-[0-9a-f]{5,40}$/

  # What are the last four things like?
  parts = [
    given.parent.parent.basename,
    given.parent.basename,
    given.basename(".rb"),
    given.extname,
  ].map(&:to_s)

  # Case 1: Simplest case: it was a full entry point, as presented.
  # /home/you/projects/inspec-something/lib/inspec-something.rb
  #   parts index:           ^0^        ^1^      ^2^        ^3^
  if parts[0] =~ name_regex && parts[1] == "lib" && parts[2] == parts[0] && parts[3] == ".rb"
    return given.to_s
  end

  # Case 2: Also easy: they either referred to the internal library directory,
  # or left the extansion off.  Those are the same to us.
  # /home/you/projects/inspec-something/lib/inspec-something
  #   parts index:           ^0^        ^1^      ^2^          (3 is empty)
  if parts[0] =~ name_regex && parts[1] == "lib" && parts[2] == parts[0] && parts[3].empty?
    return given.to_s + ".rb"
  end

  # Case 3: Maybe they were refering to a path that is inside a gem installation, or an exploded gem?
  # In that case, we'll have a version on the plugin name in part 0
  # /home/you/.gems/2.4.0/gems/inspec-something-3.45.1/lib/inspec-something.rb
  #   parts index:                     ^0^             ^1^      ^2^         ^3^
  if (parts[0] =~ versioned_regex || parts[0] =~ sha_ref_regex) && parts[1] == "lib" && parts[0].start_with?(parts[2]) && parts[3] == ".rb"
    return given.to_s
  end

  # Case 4: Like case 3, but missing the .rb
  # /home/you/.gems/2.4.0/gems/inspec-something-3.45.1/lib/inspec-something
  #   parts index:                     ^0^             ^1^      ^2^         ^3^ (empty)
  if (parts[0] =~ versioned_regex || parts[0] =~ sha_ref_regex) && parts[1] == "lib" && parts[0].start_with?(parts[2]) && parts[3].empty?
    return given.to_s + ".rb"
  end

  # Case 5: Easy to recognize, but harder to handle: they referred to the project root.
  #                 /home/you/projects/inspec-something
  #   parts index:        ^0^   ^1^         ^2^          (3 is empty)
  #   0 and 1 are not meaningful to us, but we hope to find a parts[2]/lib/inspec-something.rb.
  entry_point_guess = File.join(given.to_s, "lib", parts[2] + ".rb")
  if parts[2] =~ name_regex && File.exist?(entry_point_guess)
    return entry_point_guess
  end

  # Well, if we got here, parts[2] matches an inspec/train prefix, but we have no idea about anything.
  # 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.\n")
  ui.exit Inspec::UI::EXIT_USAGE_ERROR
end
install_from_path__probe_load(entry_point, plugin_name) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 320
def install_from_path__probe_load(entry_point, plugin_name)
  # Brazenly attempt to load a file, and see if it registers a plugin.
  begin
    require entry_point
  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\n")
    ui.plain_line ex.message
    ui.exit Inspec::UI::EXIT_USAGE_ERROR
  end

  # OK, the wheels didn't fall off.  But is it a plugin?
  if plugin_name.to_s.start_with?("train")
    # Train internal names do not include the prix in their registry entries
    # And the registry is keyed on Strings
    registry_key = plugin_name.to_s.sub(/^train-/, "")
    unless Train::Plugins.registry.key?(registry_key)
      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.\n")
      ui.exit Inspec::UI::EXIT_USAGE_ERROR
    end
  else
    unless registry.known_plugin?(plugin_name.to_sym)
      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.\n")
      ui.exit Inspec::UI::EXIT_USAGE_ERROR
    end
  end
end
install_from_remote_gem(plugin_name) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 355
def install_from_remote_gem(plugin_name)
  requested_version = options[:version]

  check_plugin_name(plugin_name, "installation")

  # Version pre-flighting
  pre_installed_versions = installer.list_installed_plugin_gems.select { |spec| spec.name == plugin_name }.map { |spec| spec.version.to_s }
  install_from_remote_gem_verson_preflight_check(plugin_name, requested_version, pre_installed_versions)

  install_attempt_install(plugin_name)

  # Success messaging.  What did we actually install?
  post_installed_versions = installer.list_installed_plugin_gems.select { |spec| spec.name == plugin_name }.map { |spec| spec.version.to_s }
  new_version = (post_installed_versions - pre_installed_versions).first

  ui.bold("#{plugin_name} plugin, version #{new_version}, installed " \
          "from rubygems.org\n")
  ui.exit Inspec::UI::EXIT_NORMAL
end
install_from_remote_gem_verson_preflight_check(plugin_name, requested_version, pre_installed_versions) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 375
def install_from_remote_gem_verson_preflight_check(plugin_name, requested_version, pre_installed_versions)
  return if pre_installed_versions.empty?

  # Everything past here in the block is a code 2 error

  # If they didn't ask for a specific version, they implicitly ask for the latest.
  # Do an expensive search to determine the latest version.
  unless requested_version
    latest_version = installer.search(plugin_name, exact: true, scope: :latest)
    latest_version = latest_version[plugin_name]&.last
    if latest_version && !requested_version
      requested_version = latest_version
    end
  end

  # Check for already-installed at desired version conditions
  they_explicitly_asked_for_a_version = !options[:version].nil?
  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.bold("Plugin already installed at requested version - plugin " \
           "#{plugin_name} #{requested_version} - refusing to install.\n")
    ui.exit Inspec::UI::EXIT_NORMAL
  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.\n")
    ui.exit Inspec::UI::EXIT_NORMAL
  end

  # 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.\n")
  ui.exit Inspec::UI::EXIT_PLUGIN_ERROR
end
installer() click to toggle source
#
utilities
#
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 476
def installer
  Inspec::Plugin::V2::Installer.instance
end
make_pretty_install_type(status) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 517
def make_pretty_install_type(status)
  INSTALL_TYPE_LABELS[status.installation_type]
end
make_pretty_version(status) click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 493
def make_pretty_version(status)
  case status.installation_type
  when :core, :bundle
    Inspec::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 }
        .max_by(&:version)
        .version
    end
  when :path
    "src"
  end
end
registry() click to toggle source
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 480
def registry
  Inspec::Plugin::V2::Registry.instance
end
update_preflight_check(plugin_name, pre_update_versions) click to toggle source
#
update breakdown
#
# File lib/plugins/inspec-plugin-manager-cli/lib/inspec-plugin-manager-cli/cli_command.rb, line 445
def update_preflight_check(plugin_name, pre_update_versions)
  if pre_update_versions.empty?
    # Check for path install
    status = Inspec::Plugin::V2::Registry.instance[plugin_name.to_sym]
    if !status
      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_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")
      ui.exit Inspec::UI::EXIT_PLUGIN_ERROR
    end
  end

  # Check for latest version (and implicitly, existence)
  latest_version = installer.search(plugin_name, exact: true, scope: :latest)
  latest_version = latest_version[plugin_name]&.last

  if pre_update_versions.include?(latest_version)
    ui.plain_line("#{ui.bold("Already installed at latest version:", print: false)} " \
             "#{plugin_name} is at #{latest_version}, which the " \
             "latest - refusing to update")
    ui.exit Inspec::UI::EXIT_NORMAL
  end
end